├── .babelrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS └── pull_request_template.md ├── .gitignore ├── .huskyrc.js ├── .lintstagedrc.js ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── lib ├── __tests__ │ └── logger.test.js └── logger.js ├── package-lock.json ├── package.json └── secret-squirrel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "14.15" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | ft-snyk-orb: financial-times/ft-snyk-orb@0 5 | 6 | references: 7 | container_config: &container_config 8 | working_directory: ~/project/lambda-logger 9 | docker: 10 | - image: cimg/node:14.18.3 11 | 12 | workspace_root: &workspace_root ~/project 13 | 14 | persist_workspace: &persist_workspace 15 | persist_to_workspace: 16 | root: ~/project 17 | paths: 18 | - lambda-logger 19 | 20 | attach_workspace: &attach_workspace 21 | attach_workspace: 22 | at: *workspace_root 23 | 24 | npm_cache_key: &npm_cache_key v5-dependency-npm-{{ checksum "package-lock.json" }} 25 | 26 | restore_node_modules: &restore_node_modules 27 | restore_cache: 28 | keys: 29 | - *npm_cache_key 30 | 31 | cache_node_modules: &cache_node_modules 32 | save_cache: 33 | key: *npm_cache_key 34 | paths: 35 | - ./node_modules 36 | 37 | only_version_tags: &only_version_tags 38 | tags: 39 | only: /^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)\.\d+)?$/ 40 | 41 | jobs: 42 | install: 43 | <<: *container_config 44 | steps: 45 | - checkout 46 | - *restore_node_modules 47 | 48 | - run: 49 | name: Install dependencies 50 | command: npm install 51 | 52 | - *cache_node_modules 53 | - *persist_workspace 54 | 55 | build: 56 | <<: *container_config 57 | steps: 58 | - *attach_workspace 59 | 60 | - run: 61 | name: Run build 62 | command: make build 63 | 64 | - *persist_workspace 65 | 66 | test: 67 | <<: *container_config 68 | steps: 69 | - *attach_workspace 70 | 71 | - run: 72 | name: Run verification 73 | command: make verify -j 4 74 | 75 | - run: 76 | name: Run tests 77 | command: npm test 78 | 79 | release: 80 | <<: *container_config 81 | steps: 82 | - *attach_workspace 83 | 84 | - deploy: 85 | name: npm publish 86 | description: 'Overwrite the default npm registry URL with the access token appended to it. Once authenticated, publish the lambda-logger to the npm registry' 87 | command: | 88 | echo "//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}" > ${HOME}/.npmrc 89 | make npm-publish 90 | 91 | workflows: 92 | version: 2.1 93 | build-and-deploy: 94 | jobs: 95 | - install: 96 | filters: 97 | tags: 98 | only: /.*/ 99 | - build: 100 | requires: 101 | - install 102 | filters: 103 | tags: 104 | only: /.*/ 105 | - test: 106 | context: rel-eng-creds 107 | requires: 108 | - install 109 | - build 110 | filters: 111 | tags: 112 | only: /.*/ 113 | #Scan package.json for vulnerable dependencies while developing 114 | - ft-snyk-orb/scan-js-packages: 115 | context: rel-eng-creds 116 | requires: 117 | - install 118 | filters: *only_version_tags 119 | - release: 120 | context: npm-publish-token 121 | requires: 122 | - test 123 | filters: 124 | branches: 125 | ignore: /.*/ 126 | tags: 127 | only: /.*/ 128 | #Scan and monitor vulnerabilities once in production 129 | - ft-snyk-orb/scan-and-monitor-js-packages: 130 | name: snyk-scan-and-monitor 131 | context: rel-eng-creds 132 | requires: 133 | - release 134 | filters: 135 | <<: *only_version_tags 136 | branches: 137 | ignore: /.*/ 138 | 139 | experimental: 140 | notify: 141 | branches: 142 | only: 143 | - main 144 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor config configuration: https://editorconfig.org/ 2 | # Generated by rel-engage 3 | 4 | root = true 5 | 6 | [**{.js,.jsx,.ts,.tsx,.html,.hbs,.mustache,.xml,.xsl,.scss,.css,.sh,.vcl,.mk,Makefile}] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = tab 12 | 13 | [**{.json,.yml,.yaml}] 14 | charset = utf-8 15 | end_of_line = lf 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | indent_size = 2 19 | indent_style = space 20 | 21 | [*.md] 22 | indent_size = unset 23 | indent_style = unset 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore file for eslint: https://eslint.org/docs/user-guide/configuring#eslintignore 2 | # Generated by rel-engage 3 | 4 | *.json 5 | 6 | .serverless 7 | .cache-loader 8 | .webpack 9 | 10 | build 11 | dist 12 | coverage 13 | 14 | secret-squirrel.js 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | ESLint config file: https://eslint.org/docs/user-guide/configuring 3 | Configures Javascript linting 4 | Generated by rel-engage 5 | */ 6 | 7 | 'use strict'; 8 | 9 | module.exports = { 10 | extends: [ 11 | // Cannot use `@financial-times/rel-engage/packages/dotfiles/eslint` 12 | // as this is translated to `eslint-config-@financial-times/rel-engage/packages/dotfiles/eslint 13 | './node_modules/@financial-times/rel-engage/packages/dotfiles/eslint.js', 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Guessed from teams who have admin access in github 2 | * @Financial-Times/reliability-engineering 3 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Why? 2 | 3 | - Copy (if there is one) the text of the original Trello/ Jira ticket in here, with a link back to it for the curious. 4 | - What's the main motivation of this work? New feature, bug fix, tech debt ... ? 5 | 6 | ## What? 7 | 8 | - Please be specific and try to describe your thought process. 9 | - State the obvious, since this might be the first time the reviewer is looking at the code. 10 | - Include screenshots of the change if relevant. 11 | - Remember to update the documentation if relevant. 12 | 13 | ### Anything in particular you'd like to highlight to reviewers? 14 | 15 | Mention here sections of code which you would like reviewers to pay extra attention to, for example: 16 | 17 | _Would appreciate a second pair of eyes on the test_ 18 | _I am not quite sure how this bit works_ 19 | _Is there a better module I can use or approach I can take to achieve X_ 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,intellij,sublimetext,visualstudiocode 3 | 4 | ### Intellij ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff: 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/dictionaries 12 | 13 | # Sensitive or high-churn files: 14 | .idea/**/dataSources/ 15 | .idea/**/dataSources.ids 16 | .idea/**/dataSources.xml 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | 22 | # Gradle: 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | 26 | # CMake 27 | cmake-build-debug/ 28 | 29 | # Mongo Explorer plugin: 30 | .idea/**/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | # IntelliJ 38 | /out/ 39 | 40 | # mpeltonen/sbt-idea plugin 41 | .idea_modules/ 42 | 43 | # JIRA plugin 44 | atlassian-ide-plugin.xml 45 | 46 | # Cursive Clojure plugin 47 | .idea/replstate.xml 48 | 49 | # Crashlytics plugin (for Android Studio and IntelliJ) 50 | com_crashlytics_export_strings.xml 51 | crashlytics.properties 52 | crashlytics-build.properties 53 | fabric.properties 54 | 55 | ### Intellij Patch ### 56 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 57 | 58 | # *.iml 59 | # modules.xml 60 | # .idea/misc.xml 61 | # *.ipr 62 | 63 | # Sonarlint plugin 64 | .idea/sonarlint 65 | 66 | ### Node ### 67 | # Logs 68 | logs 69 | *.log 70 | npm-debug.log* 71 | yarn-debug.log* 72 | yarn-error.log* 73 | 74 | # Runtime data 75 | pids 76 | *.pid 77 | *.seed 78 | *.pid.lock 79 | 80 | # Directory for instrumented libs generated by jscoverage/JSCover 81 | lib-cov 82 | 83 | # Coverage directory used by tools like istanbul 84 | coverage 85 | 86 | # nyc test coverage 87 | .nyc_output 88 | 89 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 90 | .grunt 91 | 92 | # Bower dependency directory (https://bower.io/) 93 | bower_components 94 | 95 | # node-waf configuration 96 | .lock-wscript 97 | 98 | # Compiled binary addons (http://nodejs.org/api/addons.html) 99 | build/Release 100 | 101 | # Dependency directories 102 | node_modules/ 103 | jspm_packages/ 104 | 105 | # Typescript v1 declaration files 106 | typings/ 107 | 108 | # Optional npm cache directory 109 | .npm 110 | 111 | # Optional eslint cache 112 | .eslintcache 113 | 114 | # Optional REPL history 115 | .node_repl_history 116 | 117 | # Output of 'npm pack' 118 | *.tgz 119 | 120 | # Yarn Integrity file 121 | .yarn-integrity 122 | 123 | # dotenv environment variables file 124 | .env 125 | 126 | # Build output 127 | /dist 128 | 129 | ### SublimeText ### 130 | # cache files for sublime text 131 | *.tmlanguage.cache 132 | *.tmPreferences.cache 133 | *.stTheme.cache 134 | 135 | # workspace files are user-specific 136 | *.sublime-workspace 137 | 138 | # project files should be checked into the repository, unless a significant 139 | # proportion of contributors will probably not be using SublimeText 140 | # *.sublime-project 141 | 142 | # sftp configuration file 143 | sftp-config.json 144 | 145 | # Package control specific files 146 | Package Control.last-run 147 | Package Control.ca-list 148 | Package Control.ca-bundle 149 | Package Control.system-ca-bundle 150 | Package Control.cache/ 151 | Package Control.ca-certs/ 152 | Package Control.merged-ca-bundle 153 | Package Control.user-ca-bundle 154 | oscrypto-ca-bundle.crt 155 | bh_unicode_properties.cache 156 | 157 | # Sublime-github package stores a github token in this file 158 | # https://packagecontrol.io/packages/sublime-github 159 | GitHub.sublime-settings 160 | 161 | ### VisualStudioCode ### 162 | .vscode/* 163 | !.vscode/settings.json 164 | !.vscode/tasks.json 165 | !.vscode/launch.json 166 | !.vscode/extensions.json 167 | .history 168 | 169 | # End of https://www.gitignore.io/api/node,intellij,sublimetext,visualstudiocode 170 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved, import/no-extraneous-dependencies */ 2 | /* 3 | Husky config file: https://github.com/typicode/husky 4 | Configures git hooks 5 | Generated by rel-engage 6 | */ 7 | 8 | 'use strict'; 9 | 10 | module.exports = require('@financial-times/rel-engage/packages/dotfiles/husky'); 11 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved, import/no-extraneous-dependencies */ 2 | /* 3 | Lint-staged config file: https://github.com/okonet/lint-staged 4 | Configures per-file-extension tasks ran on pre-commit 5 | Generated by rel-engage 6 | */ 7 | 8 | 'use strict'; 9 | 10 | module.exports = require('@financial-times/rel-engage/packages/dotfiles/lint-staged'); 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .circleci 3 | __tests__ 4 | .editorconfig 5 | .eslintcache 6 | .eslintrc.js 7 | .nvmrc 8 | .prettierrc 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore file for prettier: https://prettier.io/docs/en/ignore.html 2 | # Generated by rel-engage 3 | 4 | .webpack 5 | .cache-loader 6 | .DS_Store 7 | .serverless 8 | *.mk 9 | build 10 | coverage 11 | dist 12 | Makefile 13 | node_modules 14 | bower_components 15 | package-lock.json 16 | package.json 17 | bower.json 18 | tslint.json 19 | .vscode 20 | .idea 21 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved, import/no-extraneous-dependencies */ 2 | /* 3 | Prettier config file: https://prettier.io/docs/en/configuration.html 4 | Configures file formatting behaviour 5 | Generated by rel-engage 6 | */ 7 | 8 | 'use strict'; 9 | 10 | module.exports = require('@financial-times/rel-engage/packages/dotfiles/prettier'); 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Financial Times 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # --------------------------- 2 | # Generated by rel-engage 3 | 4 | # This task tells make how to 'build' n-gage. It npm installs n-gage, and 5 | # Once that's done it overwrites the file with its own contents - this 6 | # ensures the timestamp on the file is recent, so make won't think the file 7 | # is out of date and try to rebuild it every time 8 | node_modules/@financial-times/rel-engage/index.mk: 9 | @echo "Updating rel-engage" 10 | @npm install --save-dev @financial-times/rel-engage 11 | @touch $@ 12 | 13 | # If, by the end of parsing your `Makefile`, `make` finds that any files 14 | # referenced with `-include` don't exist or are out of date, it will run any 15 | # tasks it finds that match the missing file. So if n-gage *is* installed 16 | # it will just be included; if not, it will look for a task to run 17 | -include node_modules/@financial-times/rel-engage/index.mk 18 | 19 | verify: 20 | 21 | install: 22 | 23 | # End generated by rel-engage 24 | # --------------------------- 25 | 26 | build: 27 | npx microbundle -i index.js -o dist 28 | watch: 29 | npx microbundle -i index.js -o dist --watch 30 | test: 31 | npx jest 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ This library is deprecated please use [DotCom logger](https://github.com/Financial-Times/dotcom-reliability-kit) instead ⚠️ 2 | 3 | # lambda-logger 4 | 5 | Logger useful for AWS lambda applications, particularly those which are aggregated in Splunk. Logs in JSON format using [pino](https://github.com/pinojs/pino). 6 | 7 | This was created to provide a simple logger, compatible with lambda, which outputs in a JSON format ([n-logger](https://github.com/Financial-Times/n-logger)) was previously used but didn't handle nested JSON fields or provide a JSON option). 8 | 9 | This does make `process.stdout.write` a blocking function (`process.stdout._handle.setBlocking(true);`), as AWS Lambda previously streamed to an output which was synchronous, but has since changed to asynchronous behaviour, leading to lost logs. 10 | 11 | [![CircleCI](https://circleci.com/gh/Financial-Times/lambda-logger.svg?style=svg&circle-token=95d28799bf7519d6c9628cb0cdb053f08ff9ff30)](https://circleci.com/gh/Financial-Times/lambda-logger) 12 | 13 | ## Usage 14 | 15 | ```js 16 | const logger = require('@financial-times/lambda-logger'); 17 | 18 | logger.info({ importantField: 'some-field' }, 'Logging a thing'); 19 | ``` 20 | 21 | ### Build exports 22 | 23 | This module exports both 24 | 25 | - a commonjs build (the `main` field in `package.json`) 26 | - an ESM (ecmascript module) build (the `module` field in `package.json`) 27 | 28 | If you're using commonjs and webpack, say with [serverless-webpack](https://github.com/serverless-heaven/serverless-webpack) it will try to load the ESM build out of the box. This exports a `default` export, and as such won't work if using commonjs. 29 | 30 | The solutions to this problem are: 31 | 32 | 1. Use `import/export` syntax locally and ensure your local tooling uses the ESM build, e.g. by using the [esm](https://www.npmjs.com/package/esm) module. 33 | 2. Setup a webpack alias to the commonjs build: 34 | 35 | ```js 36 | // webpack.config.js 37 | 38 | module.exports = { 39 | ... 40 | resolve: { 41 | alias: { 42 | // use commonjs export of lambda-logger to avoid having to use import/export syntax locally 43 | '@financial-times/lambda-logger': 44 | '@financial-times/lambda-logger/dist/lambda-logger.js', 45 | }, 46 | }, 47 | }; 48 | ``` 49 | 50 | ## API 51 | 52 | The logger's API is identical to that of pino with the following exceptions: 53 | 54 | - The property `sourcetype: _json` is added to logs in production for Splunk compatibility. 55 | - Lambda related environment variables are added by default: 56 | - - `AWS_REGION` 57 | - `AWS_EXECUTION_ENV`, 58 | - `AWS_LAMBDA_FUNCTION_NAME`, 59 | - `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, 60 | - `AWS_LAMBDA_FUNCTION_VERSION` 61 | - Defaults to ISO timestamp logging for splunk compatiblity. At the time of writing this incurs a 25% pino performance penalty. 62 | 63 | ### Pino properties 64 | 65 | Pino adds the following properties to logs by default: 66 | 67 | - `level` - the log level in string form. This is translated from the `pino` default of logging an integer representation. 68 | - `v` - the pino logger API version. 69 | - `hostname` - the hostname the process is running on. 70 | - `pid` - the process PID. 71 | 72 | ## Configuration 73 | 74 | - `NODE_ENV` - pretty printing is enabled when value of `NODE_ENV` is not same as `p`, `prod` or `production`. 75 | - `CONSOLE_LOG_LEVEL` - determines the level to log at (pinto `level` option). Defaults to `info`. 76 | - `SYSTEM_CODE` - adds the `systemCode` property to every log. 77 | - `ENVIRONMENT|STAGE` - adds the `environment` property to every log. `STAGE` is used as a fallback due to it's default definition in the [serverless](https://serverless.com) framework. 78 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import createLogger from './lib/logger'; 2 | 3 | export default createLogger(); 4 | -------------------------------------------------------------------------------- /lib/__tests__/logger.test.js: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | import createLogger from '../logger'; 3 | 4 | const setupEnv = ({ envKey, value, setter }) => { 5 | const backupEnv = process.env[envKey]; 6 | 7 | if (typeof setter === 'function') { 8 | setter(); 9 | } else { 10 | process.env[envKey] = value; 11 | } 12 | return function restore() { 13 | if (typeof backupEnv !== 'undefined') { 14 | process.env[envKey] = backupEnv; 15 | } else { 16 | delete process.env[envKey]; 17 | } 18 | }; 19 | }; 20 | 21 | ['production', 'development'].forEach(NODE_ENV => { 22 | describe(`When process.env.NODE_ENV is ${NODE_ENV}`, () => { 23 | test('it logs at info level without exception', () => { 24 | const restore = setupEnv({ 25 | envKey: 'NODE_ENV', 26 | value: NODE_ENV, 27 | }); 28 | const logger = createLogger(); 29 | try { 30 | logger.info( 31 | { someObject: { withNesting: true } }, 32 | 'someMessage', 33 | ); 34 | } finally { 35 | restore(); 36 | } 37 | }); 38 | }); 39 | }); 40 | 41 | describe('log formatting', () => { 42 | const stdoutSpy = jest.spyOn(process.stdout, 'write'); 43 | const backupNodeEnv = process.env.NODE_ENV; 44 | let logger; 45 | 46 | const getLogObject = (filterOutput = true) => { 47 | expect(stdoutSpy).toHaveBeenCalled(); 48 | const callJson = JSON.parse(stdoutSpy.mock.calls[0]); 49 | if (filterOutput) { 50 | delete callJson.pid; 51 | delete callJson.time; 52 | delete callJson.hostname; 53 | } 54 | return callJson; 55 | }; 56 | 57 | beforeEach(() => { 58 | process.env.NODE_ENV = 'production'; 59 | }); 60 | 61 | afterEach(() => { 62 | process.env.NODE_ENV = backupNodeEnv; 63 | jest.resetModules(); 64 | jest.resetAllMocks(); 65 | }); 66 | 67 | test('it logs JSON to stdout in the correct format', () => { 68 | const expectedLog = { 69 | level: 'info', 70 | message: 'someMessage', 71 | someObject: { withNesting: true }, 72 | sourceType: '_json', 73 | v: 1, 74 | }; 75 | 76 | logger = createLogger(); 77 | logger.info({ someObject: { withNesting: true } }, 'someMessage'); 78 | 79 | const callJson = getLogObject(); 80 | expect(callJson).toEqual(expectedLog); 81 | }); 82 | 83 | Object.keys(pino.levels.values).forEach(level => { 84 | test(`it writes log level as the appropriate strings when the level is ${level}`, () => { 85 | const restore = setupEnv({ 86 | envKey: 'CONSOLE_LOG_LEVEL', 87 | value: 'trace', 88 | }); 89 | try { 90 | logger = createLogger(); 91 | logger[level]( 92 | { someObject: { withNesting: true } }, 93 | 'someMessage', 94 | ); 95 | 96 | const callJson = getLogObject(); 97 | expect(callJson).toHaveProperty('level', level); 98 | } finally { 99 | restore(); 100 | } 101 | }); 102 | }); 103 | 104 | test('logs at info level when CONSOLE_LOG_LEVEL is not set', () => { 105 | const restore = setupEnv({ 106 | envKey: 'CONSOLE_LOG_LEVEL', 107 | value: '', 108 | }); 109 | logger = createLogger(); 110 | try { 111 | logger.debug({ someObject: { withNesting: true } }, 'someMessage'); 112 | logger.info({ someObject: { withNesting: true } }, 'someMessage'); 113 | expect(stdoutSpy).toHaveBeenCalledTimes(1); 114 | } finally { 115 | restore(); 116 | } 117 | }); 118 | 119 | test('logs at the given CONSOLE_LOG_LEVEL', () => { 120 | const restore = setupEnv({ 121 | envKey: 'CONSOLE_LOG_LEVEL', 122 | value: 'debug', 123 | }); 124 | logger = createLogger(); 125 | try { 126 | logger.trace({ someObject: { withNesting: true } }, 'someMessage'); 127 | logger.debug({ someObject: { withNesting: true } }, 'someMessage'); 128 | expect(stdoutSpy).toHaveBeenCalledTimes(1); 129 | } finally { 130 | restore(); 131 | } 132 | }); 133 | 134 | test('it includes variable properties', () => { 135 | logger = createLogger(); 136 | logger.info({ someObject: { withNesting: true } }, 'someMessage'); 137 | 138 | const callJson = getLogObject(false); 139 | expect(callJson).toHaveProperty('hostname'); 140 | expect(callJson).toHaveProperty('time'); 141 | expect(callJson).toHaveProperty('pid'); 142 | }); 143 | 144 | describe('metadata from environment variables', () => { 145 | [ 146 | { 147 | property: 'sourceType', 148 | envKey: 'NODE_ENV', 149 | setter() { 150 | process.env.NODE_ENV = 'production'; 151 | }, 152 | value: '_json', 153 | }, 154 | { 155 | property: 'systemCode', 156 | envKey: 'SYSTEM_CODE', 157 | value: 'stubSystemCode', 158 | }, 159 | { 160 | property: 'environment', 161 | envKey: 'ENVIRONMENT', 162 | value: 'env-test', 163 | }, 164 | { 165 | property: 'environment', 166 | envKey: 'STAGE', 167 | value: 'stage-test', 168 | }, 169 | ].forEach(({ property, envKey, value, setter }) => { 170 | test(`it should log the property '${property}' when the environment variable '${envKey} is set'`, () => { 171 | const restore = setupEnv({ envKey, value, setter }); 172 | logger = createLogger(); 173 | logger.info('dummyMessage'); 174 | 175 | const callJson = getLogObject(); 176 | try { 177 | expect(callJson).toHaveProperty(property, value); 178 | } finally { 179 | restore(); 180 | } 181 | }); 182 | }); 183 | }); 184 | 185 | describe('Error serializer', () => { 186 | const givenError = new Error('Error object message'); 187 | 188 | test('it serializes a first argument error as expected, overwriting the message with the given message', () => { 189 | const givenAdditional = 'bar'; 190 | const givenMessage = 'Log message'; 191 | 192 | logger = createLogger(); 193 | logger.info( 194 | Object.assign(givenError, { additional: givenAdditional }), 195 | givenMessage, 196 | ); 197 | 198 | const callJson = getLogObject(); 199 | expect(callJson).toHaveProperty('stack'); 200 | expect(callJson).toHaveProperty('message', givenMessage); 201 | expect(callJson).toHaveProperty('additional', givenAdditional); 202 | }); 203 | 204 | test('it does not serialize a first argument error if it is shallow cloned using Object.assign (no error prototype)', () => { 205 | const givenAdditional = 'bar'; 206 | const givenMessage = 'Log message'; 207 | 208 | logger = createLogger(); 209 | logger.info( 210 | { ...givenError, additional: givenAdditional }, 211 | givenMessage, 212 | ); 213 | 214 | const callJson = getLogObject(); 215 | expect(callJson).not.toHaveProperty('stack'); 216 | expect(callJson).toHaveProperty('message', givenMessage); 217 | expect(callJson).toHaveProperty('additional', givenAdditional); 218 | }); 219 | 220 | ['err', 'error'].forEach(key => { 221 | test(`it serializes an error given as property '${key}' as expected`, () => { 222 | const givenAdditional = 'bar'; 223 | const givenMessage = 'Log message'; 224 | logger = createLogger(); 225 | 226 | logger.info( 227 | { 228 | [key]: Object.assign(givenError, { 229 | additional: 'bar', 230 | }), 231 | }, 232 | givenMessage, 233 | ); 234 | 235 | const callJson = getLogObject(); 236 | expect(callJson).toHaveProperty(key); 237 | expect(callJson[key]).toHaveProperty('stack'); 238 | expect(callJson[key]).toHaveProperty( 239 | 'additional', 240 | givenAdditional, 241 | ); 242 | expect(callJson).toHaveProperty('message', givenMessage); 243 | }); 244 | }); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | 3 | const prodTerms = ['production', 'prod', 'p']; 4 | const isProduction = () => prodTerms.includes(process.env.NODE_ENV); 5 | const isLambda = () => 6 | !!( 7 | (process.env.LAMBDA_TASK_ROOT && process.env.AWS_EXECUTION_ENV) || 8 | false 9 | ); 10 | 11 | const writeLog = chunk => { 12 | process.stdout.write(chunk); 13 | }; 14 | 15 | const getProductionStream = () => ({ 16 | write: writeLog, 17 | }); 18 | 19 | // enable ISO time stamps rather than epoch time 20 | // note: this results in much slower logging 21 | // https://github.com/pinojs/pino/blob/238fe2857501dca963783d93915506012c8b43bf/docs/legacy.md#v5-4 22 | const getIsoTime = () => `,"time":"${new Date().toISOString()}"`; 23 | 24 | const getBaseLogger = () => { 25 | const level = process.env.CONSOLE_LOG_LEVEL || 'info'; 26 | const options = { 27 | level, 28 | useLevelLabels: true, 29 | messageKey: 'message', 30 | timestamp: getIsoTime, 31 | serializers: { 32 | ...pino.stdSerializers, 33 | error: pino.stdSerializers.err, 34 | request: pino.stdSerializers.req, 35 | response: pino.stdSerializers.res, 36 | }, 37 | }; 38 | if (!isProduction()) { 39 | return pino({ ...options, prettyPrint: true }); 40 | } 41 | return pino(options, getProductionStream()); 42 | }; 43 | 44 | const getMetaData = () => ({ 45 | sourceType: isProduction() ? '_json' : undefined, 46 | systemCode: process.env.SYSTEM_CODE, 47 | environment: process.env.ENVIRONMENT || process.env.STAGE, 48 | lambda: isLambda() 49 | ? { 50 | region: process.env.AWS_REGION, 51 | executionEnv: process.env.AWS_EXECUTION_ENV, 52 | functionName: process.env.AWS_LAMBDA_FUNCTION_NAME, 53 | functionMemorySize: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE, 54 | functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION, 55 | logStreamName: process.env.AWS_LAMBDA_LOG_STREAM_NAME, 56 | } 57 | : undefined, 58 | }); 59 | 60 | const getLoggerWithMetadata = () => { 61 | /* eslint-disable no-underscore-dangle */ 62 | if ( 63 | isLambda() && 64 | process.stdout._handle && 65 | typeof process.stdout._handle.setBlocking === 'function' 66 | ) { 67 | process.stdout._handle.setBlocking(true); 68 | } 69 | /* eslint-enable no-underscore-dangle */ 70 | const logger = getBaseLogger(); 71 | const metadata = getMetaData(); 72 | 73 | const definedMetadata = Object.keys(metadata) 74 | .filter(key => typeof metadata[key] !== 'undefined') 75 | .reduce((result, key) => ({ ...result, [key]: metadata[key] }), {}); 76 | return logger.child(definedMetadata); 77 | }; 78 | 79 | export default getLoggerWithMetadata; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@financial-times/lambda-logger", 3 | "version": "0.0.0", 4 | "description": "Logger used by the lambda team. Logs in JSON format using pino", 5 | "main": "dist/lambda-logger.js", 6 | "browser": "dist/lambda-logger.umd.js", 7 | "module": "dist/lambda-logger.mjs", 8 | "scripts": { 9 | "test": "make test" 10 | }, 11 | "engines": { 12 | "node": "14.x" 13 | }, 14 | "rel-engage": { 15 | "eslint": { 16 | "esModules": true 17 | } 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/Financial-Times/lambda-logger.git" 22 | }, 23 | "author": "Charlie Briggs ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/Financial-Times/lambda-logger/issues" 27 | }, 28 | "homepage": "https://github.com/Financial-Times/lambda-logger#readme", 29 | "dependencies": { 30 | "pino": "^5.11.1", 31 | "pino-pretty": "^2.5.0" 32 | }, 33 | "devDependencies": { 34 | "@financial-times/rel-engage": "^8.0.8", 35 | "babel-core": "^6.26.3", 36 | "babel-jest": "^28.1.0", 37 | "babel-preset-env": "^1.7.0", 38 | "jest": "^28.1.0", 39 | "microbundle": "^0.13.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /secret-squirrel.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: { 3 | allow: [ 4 | '.nvmrc' 5 | ], 6 | allowOverrides: [] 7 | }, 8 | strings: { 9 | deny: [], 10 | denyOverrides: [] 11 | } 12 | }; 13 | --------------------------------------------------------------------------------