From b5c47f8a3477f70261bee00537dfdf5b8b29e710 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:11:14 -0500 Subject: [PATCH 01/83] chore: add typescript --- .eslintrc.yml | 20 +++++++++++++------- .gitignore | 1 + .vscode/settings.json | 6 ++++++ package.json | 18 +++++++++++++++--- tsconfig.eslint.json | 11 +++++++++++ tsconfig.json | 17 +++++++++++++++++ 6 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 tsconfig.eslint.json create mode 100644 tsconfig.json diff --git a/.eslintrc.yml b/.eslintrc.yml index 7af189e..1870e36 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,9 +1,6 @@ --- root: true -parserOptions: - ecmaVersion: 2019 - env: node: true es6: true @@ -11,12 +8,11 @@ env: plugins: - 'jsdoc' - - 'node' + - '@typescript-eslint/eslint-plugin' extends: - 'xo-space' - 'plugin:jsdoc/recommended' - - 'plugin:node/recommended' rules: array-element-newline: off @@ -29,5 +25,15 @@ rules: no-negated-condition: off keyword-spacing: ['error', { overrides: { if: { after: false }, for: { after: false }, while: { after: false }, catch: { after: false } } }] jsdoc/newline-after-description: off - node/no-unsupported-features/es-syntax: ['error', { version: '>=10.0.0' }] - node/no-missing-require: off + +overrides: + - files: ['*.ts'] + parserOptions: + project: './tsconfig.eslint.json' + extends: + - 'xo-typescript/space' + rules: + '@typescript-eslint/keyword-spacing': ['error', { overrides: { if: { after: false }, for: { after: false }, while: { after: false }, catch: { after: false } } }] + '@typescript-eslint/object-curly-spacing': ['error', 'always'] + '@typescript-eslint/comma-dangle': ['error', 'never'] + '@typescript-eslint/naming-convention': off diff --git a/.gitignore b/.gitignore index f83be66..56b12f6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ package-lock.json # DS_Store file .DS_Store +dist res diff --git a/.vscode/settings.json b/.vscode/settings.json index 1d8964a..d4e99cf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,12 @@ "changeProcessCWD": true } ], + "eslint.validate": [ + "mdx", + "javascript", + "javascriptreact", + "typescript" + ], "[mdx]": { "editor.wordWrap": "on" } diff --git a/package.json b/package.json index e29ea25..487d6bb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ "scripts": { "test": "jest", "prepare": "is-ci || husky install", - "lint": "eslint index.js index.test.js \"lib/*.js\"" + "build": "npm run clean && npm run build:esm && npm run build:cjs", + "build:esm": "tsc --module es6 --outDir dist/esm --moduleResolution node", + "build:cjs": "tsc --module commonjs --outDir dist/cjs --moduleResolution node", + "lint": "eslint \"src/*.ts\"", + "clean": "rimraf dist" }, "repository": { "type": "git", @@ -34,7 +38,8 @@ "cloudwatch", "logs", "function", - "cloud" + "cloud", + "typescript" ], "author": "Kyle Ross ", "license": "MIT", @@ -50,14 +55,21 @@ "@commitlint/config-conventional": "^13.2.0", "@semantic-release/changelog": "^6.0.0", "@semantic-release/git": "^10.0.0", + "@types/jest": "^27.0.2", + "@typescript-eslint/eslint-plugin": "^5.1.0", + "@typescript-eslint/parser": "^5.1.0", "eslint": "^7.23.0", "eslint-config-xo-space": "^0.30.0", + "eslint-config-xo-typescript": "^0.45.2", "eslint-plugin-jsdoc": "^36.1.1", "eslint-plugin-node": "^11.1.0", "husky": "^7.0.2", "is-ci": "^3.0.0", "jest": "^27.2.5", - "semantic-release": "^18.0.0" + "rimraf": "^3.0.2", + "semantic-release": "^18.0.0", + "ts-jest": "^27.0.7", + "typescript": "^4.4.4" }, "dependencies": { "fast-safe-stringify": "^2.1.1" diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..8105934 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "*.ts", + "*.js", + "src/*.test.ts" + ], + "compilerOptions": { + "types": ["jest", "node"] + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cdd6746 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "include": [ + "src/index.ts" + ], + "compilerOptions": { + "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"], + "module": "commonjs", + "target": "es2019", + + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "suppressImplicitAnyIndexErrors": true, + "declaration": true, + "outDir": "dist" + } +} From 39abf26833646ef901952460796cfedcb0a54e18 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:11:40 -0500 Subject: [PATCH 02/83] chore: rename .releaserc to release.config.js --- .releaserc | 13 ------------- release.config.js | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 13 deletions(-) delete mode 100644 .releaserc create mode 100644 release.config.js diff --git a/.releaserc b/.releaserc deleted file mode 100644 index 3c8e3cc..0000000 --- a/.releaserc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - "@semantic-release/changelog", - "@semantic-release/npm", - ["@semantic-release/git", { - "assets": ["package.json", "CHANGELOG.md"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - }], - "@semantic-release/github" - ] -} diff --git a/release.config.js b/release.config.js new file mode 100644 index 0000000..4a1f6d4 --- /dev/null +++ b/release.config.js @@ -0,0 +1,14 @@ +module.exports = { + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + '@semantic-release/changelog', + '@semantic-release/npm', + ['@semantic-release/git', { + assets: ['package.json', 'CHANGELOG.md'], + // eslint-disable-next-line no-template-curly-in-string + message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' + }], + '@semantic-release/github' + ] +}; From 280065579846369ac241142f6c1dd71196af9b56 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:12:49 -0500 Subject: [PATCH 03/83] feat: rewrite into typescript BREAKING CHANGE --- lib/LambdaLog.js | 207 ------------- lib/LambdaLog.test.js | 285 ----------------- lib/LogMessage.js | 259 ---------------- lib/LogMessage.test.js | 419 ------------------------- src/LambdaLog.test.ts | 270 +++++++++++++++++ src/LambdaLog.ts | 292 ++++++++++++++++++ src/LogMessage.test.ts | 472 +++++++++++++++++++++++++++++ src/LogMessage.ts | 326 ++++++++++++++++++++ index.test.js => src/index.test.ts | 9 +- index.js => src/index.ts | 10 +- src/typings.ts | 64 ++++ src/utils.test.ts | 86 ++++++ src/utils.ts | 64 ++++ 13 files changed, 1587 insertions(+), 1176 deletions(-) delete mode 100644 lib/LambdaLog.js delete mode 100644 lib/LambdaLog.test.js delete mode 100644 lib/LogMessage.js delete mode 100644 lib/LogMessage.test.js create mode 100644 src/LambdaLog.test.ts create mode 100644 src/LambdaLog.ts create mode 100644 src/LogMessage.test.ts create mode 100644 src/LogMessage.ts rename index.test.js => src/index.test.ts (55%) rename index.js => src/index.ts (57%) create mode 100644 src/typings.ts create mode 100644 src/utils.test.ts create mode 100644 src/utils.ts diff --git a/lib/LambdaLog.js b/lib/LambdaLog.js deleted file mode 100644 index 16d071d..0000000 --- a/lib/LambdaLog.js +++ /dev/null @@ -1,207 +0,0 @@ -const EventEmitter = require('events'); -const LogMessage = require('./LogMessage'); - -const symbols = { - LEVELS: Symbol('levels') -}; - -/** - * @typedef {object} LambdaLogOptions - Configuration object for LambdaLog. - * @property {object} [meta={}] Global metadata to be included in all logs. - * @property {string[]|Function[]} [tags=[]] Global tags to be included in all logs. - * @property {Function} [dynamicMeta=null] Function that runs for each log that returns additional metadata. See [Dynamic Metadata](#dynamic-metadata). - * @property {boolean} [debug=false] Enables `log.debug()`. - * @property {boolean} [dev=false] Enable development mode which pretty-prints JSON to the console. - * @property {boolean} [silent=false] Disables logging to `console` but messages and events are still generated. - * @property {Function} [replacer=null] Replacer function for `JSON.stringify()` to allow handling of sensitive data before logs are written. See [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter). - * @property {object} [logHandler=console] A console-like object containing all standard console functions. Allows logs to be written to any custom location. See [Log Handler](#loghandler). - * @property {?string} [levelKey=_logLevel] Override the key name for the log level. Set to `null` to remove the key from the output. - * @property {string} [messageKey=msg] Override the key name for the message. - * @property {?string} [tagsKey=_tags] Override the key name for the tags. Set to `null` to remove the key from the output. - */ - -/** - * @augments EventEmitter - */ -class LambdaLog extends EventEmitter { - /** - * Constructor for the LambdaLog class. Provided to be utilized in more advanced cases to allow overriding and configuration. - * By default, this module will export an instance of this class, but you may access the class and create your own instance - * via `log.LambdaLog`. - * @class - * @param {LambdaLogOptions} [options={}] Options for configuring LambdaLog. - * @param {object.} [levels={}] Allows adding and customizing log levels. DEPRECATED - */ - constructor(options = {}, levels = {}) { - super(); - /** - * Access to the uninstantiated LambdaLog class. This allows more advanced functionality and customization. - * @type {LambdaLog} - */ - this.LambdaLog = LambdaLog; - - /** - * Access to the uninstantiated LogMessage class. You can override this property to use a custom logging class that - * inherits the same methods. - * @type {LogMessage} - * @since 2.2.0 - */ - this.LogMessage = LogMessage; - - /** - * @type {LambdaLogOptions} - */ - this.options = { - meta: {}, - tags: [], - dynamicMeta: null, - debug: false, - dev: false, - silent: ['true', 'yes', 'y', '1'].includes(process.env.LAMBDALOG_SILENT), - replacer: null, - logHandler: console, - levelKey: '_logLevel', - messageKey: 'msg', - tagsKey: '_tags', - ...options - }; - - /** - * Global configuration for log levels - * @type {object} - */ - this[symbols.LEVELS] = { - info: 'info', - warn: 'warn', - error: 'error', - debug() { - if(this.options.debug) return 'debug'; - return false; - }, - ...levels - }; - - /** - * Console-like log handler to use for logging messages - * @type {object} - */ - this.console = this.options.logHandler; - - const levelsConfig = this[symbols.LEVELS]; - for(const lvl in levelsConfig) { - if(Object.prototype.hasOwnProperty.call(levelsConfig, lvl)) { - this.addLevel(lvl, levelsConfig[lvl]); - } - } - } - - /** - * Add a new log level to this instance of LambdaLog. - * @since 3.0.0 - * @deprecated - * @param {string} name The name of the new log level. - * @param {string|Function} handler The string name of the `console` method to call or a function that returns a string method name. - * @returns {this} Instance of LambdaLog. - */ - addLevel(name, handler) { - this[symbols.LEVELS][name] = handler; - - /** - * Shortcut methods for `log.log()`. By default, the following methods are available: `log.info()`, `log.warn()`, `log.error()` and `log.debug()`. - * Additional methods will be added for any [custom log levels](#custom-log-levels) provided.

The provided msg can be any type, although a string - * or `Error` is recommended. If `Error` is provided the stack trace is added as metadata to the log as `stack`. - * @param {*} msg Message to log. Can be any type, but string or `Error` reccommended. - * @param {object} [meta={}] Optional meta data to attach to the log. - * @param {string[]} [tags=[]] Additional tags to append to this log. - * @returns {LogMessage} The LogMessage instance for the log. - */ - this[name] = (msg, meta = {}, tags = []) => this.log(name, msg, meta, tags); - - return this; - } - - /** - * Generates JSON log message based on the provided parameters and the global configuration. Once the JSON message is created, it is properly logged to the `console` - * and emitted through an event. If an `Error` or `Error`-like object is provided for `msg`, it will parse out the message and include the stacktrace in the metadata. - * @throws {Error} If improper log level is provided. - * @param {string} level Log level (`info`, `debug`, `warn`, `error` or a [custom log level](#custom-log-levels)) - * @param {*} msg Message to log. Can be any type, but string or `Error` reccommended. - * @param {object} [meta={}] Optional meta data to attach to the log. - * @param {string[]} [tags=[]] Additional tags to append to this log. - * @returns {LogMessage|boolean} Returns instance of LogMessage or `false` if `level = "debug"` and `options.debug = false`. May also return `false` when a [custom log level](#custom-log-levels) handler function prevents the log from being logged. - */ - log(level, msg, meta = {}, tags = []) { - if(!Object.prototype.hasOwnProperty.call(this[symbols.LEVELS], level)) { - throw new Error(`"${level}" is not a valid log level`); - } - - const message = new this.LogMessage({ - level, - msg, - meta, - tags - }, this.options); - - let method = this[symbols.LEVELS][level]; - - if(typeof method === 'function') { - method = method.call(this, message); - } - - if(!method) return false; - - if(!this.options.silent) { - this.console[method](message.toJSON(this.options.dev)); - } - - /** - * The log event is emitted (using EventEmitter) for every log generated. This allows for custom integrations, such as logging to a thrid-party service. - * This event is emitted with the [LogMessage](#logmessage) instance for the log. You may control events using all the methods of EventEmitter. - * @event LambdaLog#log - * @type {LogMessage} - */ - this.emit('log', message); - return message; - } - - /** - * Generates a log message if `test` is a falsy value. If `test` is truthy, the log message is skipped and returns `false`. Allows creating log messages without the need to - * wrap them in an if statement. The log level will be `error`. - * @since 1.4.0 - * @param {*} test A value which is tested for a falsy value. - * @param {*} msg Message to log if `test` is falsy. Can be any type, but string or `Error` reccommended. - * @param {object} [meta={}] Optional meta data to attach to the log. - * @param {string[]|Function[]} [tags=[]] Additional tags to append to this log. - * @returns {LogMessage|boolean} The LogMessage instance for the log or `false` if test passed. - */ - assert(test, msg, meta = {}, tags = []) { - if(test) return false; - return this.log('error', msg, meta, tags); - } - - /** - * Generates a log message with the result or error provided by a promise. Useful for debugging and testing. - * @since 2.3.0 - * @param {Promise} promise A promise or promise-like object to retrieve a value from. - * @param {object} [meta={}] Optional meta data to attach to the log. - * @param {string[]|Function[]} [tags=[]] Additional tags to append to this log. - * @returns {Promise} A new Promise that resolves with the LogMessage object after the promise completes. - */ - result(promise, meta = {}, tags = []) { - if(!promise || typeof promise.then !== 'function') { - throw new Error('A promise must be provided as the first argument'); - } - - const wrapper = new Promise(resolve => { - promise - .then(value => resolve(this.log('info', value, meta, tags))) - .catch(err => resolve(this.log('error', err, meta, tags))); - }); - - return wrapper; - } -} - -LambdaLog.symbols = symbols; - -module.exports = LambdaLog; diff --git a/lib/LambdaLog.test.js b/lib/LambdaLog.test.js deleted file mode 100644 index 38ab17a..0000000 --- a/lib/LambdaLog.test.js +++ /dev/null @@ -1,285 +0,0 @@ -const LambdaLog = require('./LambdaLog'); -const LogMessage = require('./LogMessage'); - -let log; - -const noop = function () {}; -const mockConsole = { - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - debug: jest.fn() -}; - -beforeEach(() => { - log = new LambdaLog(); -}); - -describe('Statics', () => { - test('should have static property "symbols"', () => { - expect(LambdaLog.symbols).toBeDefined(); - }); - - describe('Static symbols', () => { - test('should have a symbol for levels', () => { - expect(LambdaLog.symbols).toHaveProperty('LEVELS'); - expect(LambdaLog.symbols.LEVELS.toString()).toEqual('Symbol(levels)'); - }); - }); -}); - -describe('Constructor', () => { - test.each([ - ['meta', {}], - ['tags', []], - ['dynamicMeta', null], - ['debug', false], - ['dev', false], - ['silent', false], - ['replacer', null], - ['logHandler', console], - ['levelKey', '_logLevel'], - ['messageKey', 'msg'], - ['tagsKey', '_tags'] - ])('should set default option %s to equal %p', (key, expected) => { - expect(log.options[key]).toStrictEqual(expected); - }); - - test.each([ - ['meta', { test: 'test' }], - ['tags', ['custom-tag']], - ['dynamicMeta', function () {}], - ['debug', true], - ['dev', true], - ['silent', true], - ['replacer', function () {}], - ['logHandler', function () {}], - ['levelKey', 'level'], - ['messageKey', 'message'], - ['tagsKey', 'tags'] - ])('should override the default option for %s with %p', (key, expected) => { - const log = new LambdaLog({ - [key]: expected - }); - - expect(log.options[key]).toStrictEqual(expected); - }); - - describe('Environment Variables', () => { - afterEach(() => { - process.env.LAMBALOG_SILENT = null; - }); - - test.each([ - ['true'], - ['yes'], - ['y'], - ['1'] - ])('should enable silent mode when LAMBDALOG_SILENT is set to %s', val => { - process.env.LAMBDALOG_SILENT = val; - const log = new LambdaLog(); - - expect(log.options.silent).toBe(true); - }); - }); - - describe('Custom Log Levels', () => { - test('should add custom log level to configuration', () => { - const log = new LambdaLog({}, { - test: 'log' - }); - - expect(log[LambdaLog.symbols.LEVELS]).toHaveProperty('test', 'log'); - }); - }); -}); - -describe('Properties', () => { - test.each([ - ['LambdaLog', LambdaLog], - ['LogMessage', LogMessage] - ])('should have access to uninstantiated %s class', (name, expected) => { - expect(log).toHaveProperty(name); - expect(log[name]).toBe(expected); - }); - - describe('Log Levels', () => { - test('should have log levels configuration object', () => { - expect(typeof log[LambdaLog.symbols.LEVELS]).toBe('object'); - }); - - test('should have default log level info', () => { - expect(log[LambdaLog.symbols.LEVELS]).toHaveProperty('info', 'info'); - }); - - test('should have default log level warn', () => { - expect(log[LambdaLog.symbols.LEVELS]).toHaveProperty('warn', 'warn'); - }); - - test('should have default log level error', () => { - expect(log[LambdaLog.symbols.LEVELS]).toHaveProperty('error', 'error'); - }); - - test('should have default log level debug', () => { - expect(log[LambdaLog.symbols.LEVELS]).toHaveProperty('debug'); - expect(typeof log[LambdaLog.symbols.LEVELS].debug).toBe('function'); - }); - - test('should return false from debug level function when options.debug is false', () => { - expect(log[LambdaLog.symbols.LEVELS].debug.call(log)).toBe(false); - }); - - test('should return debug from debug level function when options.debug is true', () => { - const log = new LambdaLog({ debug: true }); - expect(log[LambdaLog.symbols.LEVELS].debug.call(log)).toBe('debug'); - }); - }); - - test('should have console property that is equal to options.logHandler', () => { - expect(log.console).toBe(console); - }); -}); - -describe('Methods', () => { - describe('addLevel()', () => { - test('should add a custom log level to the configuration', () => { - const log = new LambdaLog(); - log.addLevel('test', 'log'); - - expect(log[LambdaLog.symbols.LEVELS]).toHaveProperty('test', 'log'); - }); - - test('should add a dynamic method to the class', () => { - const log = new LambdaLog(); - log.addLevel('test', 'log'); - - expect(typeof log.test).toBe('function'); - }); - }); - - describe('log()', () => { - test('should throw error when unknown log level is passed in', () => { - expect(() => { - log.log('foo', 'test'); - }).toThrow('"foo" is not a valid log level'); - }); - - test('should return false for a disable log level', () => { - const result = log.log('debug', 'test'); - expect(result).toBe(false); - }); - - test('should not log message when silent is enabled', () => { - const log = new LambdaLog({ logHandler: mockConsole, silent: true }); - log.log('info', 'test'); - expect(mockConsole.info).toBeCalledTimes(0); - }); - - test('should return instance of LogMessage', () => { - const result = log.log('info', 'test'); - expect(result).toBeInstanceOf(LogMessage); - }); - - test('should emit "log" event with instance of LogMessage', done => { - const log = new LambdaLog({ logHandler: mockConsole }); - - log.on('log', msg => { - expect(msg).toBeInstanceOf(LogMessage); - done(); - }); - - log.log('info', 'test'); - }); - }); - - describe('assert()', () => { - test('should return false when test is a truthy value', () => { - const result = log.assert(true, 'test'); - expect(result).toBe(false); - }); - - test('should log error if test is a falsy value', () => { - const log = new LambdaLog({ logHandler: mockConsole }); - const result = log.assert(false, 'test'); - - expect(result).toBeInstanceOf(LogMessage); - expect(result.level).toBe('error'); - }); - }); - - describe('result()', () => { - test('should throw error if a Promise is not provided', () => { - expect(() => { - log.result(null); - }).toThrow('A promise must be provided as the first argument'); - }); - - test('should throw error if non-Promise is provided', () => { - expect(() => { - log.result(noop); - }).toThrow('A promise must be provided as the first argument'); - }); - - test('should log promise results as info message on resolve', done => { - const log = new LambdaLog({ logHandler: mockConsole }); - const promise = log.result(Promise.resolve('Success!')); - - promise.then(msg => { - expect(msg).toBeInstanceOf(LogMessage); - expect(msg.level).toBe('info'); - expect(msg.msg).toBe('Success!'); - done(); - }); - }); - - test('should log promise error as an error message on reject', done => { - const log = new LambdaLog({ logHandler: mockConsole }); - const promise = log.result(Promise.reject(new Error('Failed!'))); - - promise.then(msg => { - expect(msg).toBeInstanceOf(LogMessage); - expect(msg.level).toBe('error'); - expect(msg.msg).toBe('Failed!'); - done(); - }); - }); - }); -}); - -describe('Dynamic Methods', () => { - test.each([ - ['info'], - ['warn'], - ['error'], - ['debug'] - ])('should automatically create method log.%s', method => { - const log = new LambdaLog({ - logHandler: mockConsole, - silent: false, - debug: true - }); - - expect(typeof log[method]).toBe('function'); - const message = log[method]('test'); - expect(mockConsole[method]).toHaveBeenCalled(); - - expect(message).toBeInstanceOf(LogMessage); - }); - - test('should automatically create method for custom log levels', () => { - const log = new LambdaLog({ - logHandler: mockConsole, - silent: false, - debug: true - }, { - test: 'log' - }); - - expect(typeof log.test).toBe('function'); - const message = log.test('test'); - expect(mockConsole.log).toHaveBeenCalled(); - - expect(message).toBeInstanceOf(LogMessage); - }); -}); diff --git a/lib/LogMessage.js b/lib/LogMessage.js deleted file mode 100644 index 7feea64..0000000 --- a/lib/LogMessage.js +++ /dev/null @@ -1,259 +0,0 @@ -const stringify = require('fast-safe-stringify'); - -const symbols = { - LOG: Symbol('log'), - META: Symbol('meta'), - ERROR: Symbol('error'), - OPTS: Symbol('opts') -}; - -/** - * The LogMessage class is a private/internal class that is used for generating log messages. All log methods return an instance of LogMessage allowing for a chainable api. - * Having a seperate class and instance for each log allows chaining and the ability to further customize this module in the future without major breaking changes. The documentation - * provided here is what is available to you for each log message. - */ -class LogMessage { - /** - * Constructor for LogMessage - * @class - * @param {object} log Object containing all the information for a log. - * @param {string} log.level The log level. - * @param {*} log.msg The message for the log. - * @param {object} [log.meta] Metadata attached to the log. - * @param {string[]|Function[]} [log.tags] Additional tags to attach to the log. - * @param {object} opts Configuration options from LambdaLog. - */ - constructor(log, opts) { - this[symbols.LOG] = log; - this[symbols.META] = {}; - this[symbols.ERROR] = null; - this[symbols.OPTS] = opts; - - const { meta, tags } = this[symbols.LOG]; - if(meta && (typeof meta !== 'object' || Array.isArray(meta))) { - this[symbols.LOG].meta = { meta }; - } - - if(!meta) this[symbols.LOG].meta = {}; - if(!tags) this[symbols.LOG].tags = []; - - // If `msg` is an Error-like object, use the message and add the `stack` to `meta` - if(LogMessage.isError(log.msg)) { - const err = log.msg; - this[symbols.ERROR] = err; - this[symbols.META].stack = err.stack; - this[symbols.LOG].msg = err.message; - } - } - - /** - * String log level of the message. - * @type {string} - */ - get level() { - return this[symbols.LOG].level; - } - - /** - * The message for the log. If an Error was provided, it will be the message of the error. - * @type {string} - */ - get msg() { - return this[symbols.LOG].msg; - } - - /** - * Update the message for this log to something else. - * @param {string} msg A string to update the message with. - */ - set msg(msg) { - this[symbols.LOG].msg = msg; - } - - /** - * Alias for `this.msg`. - * @type {string} - */ - get message() { - return this.msg; - } - - /** - * Alias for `this.msg = 'New message';` - * @param {string} msg A string to update the message with. - */ - set message(msg) { - this.msg = msg; - } - - /** - * The fully compiled metadata object for the log. Includes global and dynamic metadata. - * @type {object} - */ - get meta() { - const opts = this[symbols.OPTS]; - - let meta = { - ...this[symbols.META], - ...this[symbols.OPTS].meta, - ...this[symbols.LOG].meta - }; - - if(opts.dynamicMeta && typeof opts.dynamicMeta === 'function') { - const dynMeta = opts.dynamicMeta.call(this, this, opts); - - if(typeof dynMeta === 'object') { - meta = Object.assign(meta, dynMeta); - } - } - - for(const [key, val] of Object.entries(meta)) { - if(typeof val !== 'object') continue; - if(LogMessage.isError(val)) { - meta[key] = LogMessage.stubError(val); - } - } - - return meta; - } - - /** - * Set additional metadata on the log message. - * @param {object} obj An object with properties to append or overwrite in the metadata. - */ - set meta(obj) { - this[symbols.LOG].meta = { - ...this[symbols.LOG].meta, - ...obj - }; - } - - /** - * Array of tags attached to this log. Includes global tags. - * @type {string[]} - */ - get tags() { - const opts = this[symbols.OPTS]; - const tags = [].concat(opts.tags, this[symbols.LOG].tags); - - return tags.map(tag => { - if(typeof tag === 'function') { - return tag.call(this, { - level: this.level, - meta: this.meta, - options: opts - }); - } - - const hasVar = tag.match(/(<<(.*)>>)/); - if(!hasVar) return tag; - - const varName = hasVar[2]; - if(varName === 'level') return tag.replace(hasVar[1], this.level); - - return tag; - }).filter(tag => tag !== null && tag !== undefined && tag !== ''); - } - - /** - * Appends additional tags to this log message. - * @param {string[]|Function[]} tags Array of string tags or enhanced tag functions to append to the tags array. - */ - set tags(tags) { - this[symbols.LOG].tags = this[symbols.LOG].tags.concat(tags); - } - - /** - * The full log object. This is the object used in logMessage.toJSON() and when the log is written to the console. - * @returns {object} The full log object. - */ - get value() { - const opts = this[symbols.OPTS]; - return { - [opts.levelKey]: opts.levelKey ? this.level : undefined, - [opts.messageKey]: this.msg, - ...this.meta, - [opts.tagsKey]: opts.tagsKey ? this.tags : undefined - }; - } - - /** - * Alias of `logMessage.value`. - * @returns {object} The full log object. - */ - get log() { - return this.value; - } - - /** - * Throws the log. If an error was not provided, one will be generated for you and thrown. This is useful in cases where you need to log an - * error, but also throw it. - * @throws {Error} The provided error, or a newly generated error. - */ - get throw() { - const err = this[symbols.ERROR] || new Error(this.msg); - err.log = this; - - throw err; - } - - /** - * Returns the compiled log object converted into JSON. This method utilizes `options.replacer` for the replacer function. It also uses - * [fast-safe-stringify](https://www.npmjs.com/package/fast-safe-stringify) to prevent circular reference issues. - * @param {boolean} [format=false] Enable pretty-printing of the JSON object (4 space indentation). - * @returns {string} Log object stringified as JSON. - */ - toJSON(format) { - return stringify(this.value, this[symbols.OPTS].replacer || null, format ? 4 : 0); - } - - /** - * Checks if value is an Error or Error-like object - * @static - * @param {*} val Value to test - * @returns {boolean} Whether the value is an Error or Error-like object - */ - static isError(val) { - return Boolean(val) && typeof val === 'object' && ( - val instanceof Error || ( - Object.prototype.hasOwnProperty.call(val, 'message') && - Object.prototype.hasOwnProperty.call(val, 'stack') - ) - ); - } - - /** - * Stubs an Error or Error-like object to include a toJSON method. - * @static - * @param {Error} err An Error or Error-like object. - * @returns {Error} The original error stubbed with a toJSON() method. - */ - static stubError(err) { - if(typeof err.toJSON === 'function') return err; - - err.toJSON = function () { - const keys = [ - 'name', - 'message', - 'stack' - ].concat(Object.keys(err)); - - return keys.reduce((obj, key) => { - if(key in err) { - const val = err[key]; - - if(typeof val === 'function') return obj; - obj[key] = val; - } - - return obj; - }, {}); - }; - - return err; - } -} - -LogMessage.symbols = symbols; - -module.exports = LogMessage; diff --git a/lib/LogMessage.test.js b/lib/LogMessage.test.js deleted file mode 100644 index 0408323..0000000 --- a/lib/LogMessage.test.js +++ /dev/null @@ -1,419 +0,0 @@ -const LogMessage = require('./LogMessage'); - -const logData = { - info: { - level: 'info', - msg: 'info test', - meta: {}, - tags: [] - }, - - error: { - level: 'error', - msg: new Error('error test'), - meta: {}, - tags: [] - }, - - withMeta: { - level: 'info', - msg: 'info test', - meta: { test: true }, - tags: [] - }, - - withTags: { - level: 'info', - msg: 'info test', - meta: {}, - tags: ['test'] - } -}; - -const defaultOpts = { - meta: {}, - dynamicMeta: null, - tags: [], - levelKey: '_logLevel', - messageKey: 'msg', - tagsKey: '_tags', - replacer: null -}; - -describe('Statics', () => { - test('should have static property "symbols"', () => { - expect(LogMessage.symbols).toBeDefined(); - }); - - describe('Static symbols', () => { - test.each([ - ['LOG', 'Symbol(log)'], - ['META', 'Symbol(meta)'], - ['ERROR', 'Symbol(error)'], - ['OPTS', 'Symbol(opts)'] - ])('should have a symbol for %s', (prop, value) => { - expect(LogMessage.symbols).toHaveProperty(prop); - expect(LogMessage.symbols[prop].toString()).toEqual(value); - }); - }); - - describe('Static Methods', () => { - describe('isError()', () => { - test('should return true for an Error', () => { - expect(LogMessage.isError(new Error('test'))).toBe(true); - }); - - test('should return true for an Error-like object', () => { - expect(LogMessage.isError({ - message: 'test', - stack: 'stack' - })).toBe(true); - }); - - test('should return false for anything else', () => { - expect(LogMessage.isError()).toBe(false); - expect(LogMessage.isError(null)).toBe(false); - expect(LogMessage.isError(true)).toBe(false); - expect(LogMessage.isError(123)).toBe(false); - expect(LogMessage.isError('A string')).toBe(false); - expect(LogMessage.isError({ message: 'test' })).toBe(false); - expect(LogMessage.isError([1, 2, 3, 'test'])).toBe(false); - expect(LogMessage.isError({ stack: 'stack' })).toBe(false); - }); - }); - - describe('stubError()', () => { - let err; - beforeEach(() => { - const e = new Error('test'); - e.test = true; - err = LogMessage.stubError(e); - }); - - test('should add a toJSON method to an error', () => { - expect(err).toHaveProperty('toJSON'); - expect(typeof err.toJSON).toBe('function'); - }); - - test('calling toJSON should create an serializable object', () => { - const result = err.toJSON(); - - expect(typeof result).toBe('object'); - expect(result).toHaveProperty('name', 'Error'); - expect(result).toHaveProperty('message', 'test'); - expect(result).toHaveProperty('stack', err.stack); - expect(result).toHaveProperty('test', true); - }); - - test('should skip custom toJSON fn if error already has one', () => { - const e = new Error('test'); - const noop = () => { /* noop */ }; - Object.defineProperty(e, 'toJSON', { - value: noop - }); - - const error = LogMessage.stubError(e); - expect(error.toJSON.toString()).toBe(noop.toString()); - }); - }); - }); -}); - -describe('Constructor', () => { - test.each([ - [LogMessage.symbols.LOG, logData.info], - [LogMessage.symbols.META, {}], - [LogMessage.symbols.ERROR, null], - [LogMessage.symbols.OPTS, defaultOpts] - ])('should set property %p on class', (symbol, expected) => { - const msg = new LogMessage({ ...logData.info }, defaultOpts); - - expect(msg[symbol]).toStrictEqual(expected); - }); - - test.each([ - ['string', 'string meta'], - ['array', ['array', 'meta']], - ['boolean', true], - ['number', 123] - ])('should convert non-object (%s) meta to an object', (_, value) => { - const msg = new LogMessage({ - ...logData.info, - meta: value - }, defaultOpts); - - expect(msg[LogMessage.symbols.LOG].meta).toStrictEqual({ meta: value }); - }); - - test('should set log meta as an empty object if not provided', () => { - const msg = new LogMessage({ ...logData.info, meta: undefined }, defaultOpts); - - expect(msg[LogMessage.symbols.LOG].meta).toStrictEqual({}); - }); - - test('should set log tags as an empty array if not provided', () => { - const msg = new LogMessage({ ...logData.info, tags: undefined }, defaultOpts); - - expect(msg[LogMessage.symbols.LOG].tags).toStrictEqual([]); - }); - - test('should update all properties when an error is passed in', () => { - const msg = new LogMessage({ ...logData.error }, defaultOpts); - - expect(msg[LogMessage.symbols.LOG].msg).toBe('error test'); - expect(msg[LogMessage.symbols.ERROR]).toStrictEqual(logData.error.msg); - expect(msg[LogMessage.symbols.META].stack).toBe(logData.error.msg.stack); - }); -}); - -describe('Getters', () => { - test('"level" should return the log level', () => { - const msg = new LogMessage({ ...logData.info }, defaultOpts); - - expect(msg.level).toBe('info'); - }); - - test('"msg" and "message" should return the log message', () => { - const msg = new LogMessage({ ...logData.info }, defaultOpts); - - expect(msg.msg).toBe('info test'); - expect(msg.message).toBe(msg.msg); - }); - - test('"meta" should return metadata object', () => { - const msg = new LogMessage({ ...logData.withMeta }, defaultOpts); - - expect(msg.meta).toHaveProperty('test', true); - }); - - test('"meta" should combine all metadata', () => { - const msg = new LogMessage({ ...logData.withMeta }, { - ...defaultOpts, - meta: { foo: 'bar' }, - dynamicMeta: () => ({ dynamic: 'meta' }) - }); - - expect(msg.meta).toHaveProperty('test', true); - expect(msg.meta).toHaveProperty('foo', 'bar'); - expect(msg.meta).toHaveProperty('dynamic', 'meta'); - }); - - test('"meta" should call dynamicMeta', () => { - const msg = new LogMessage({ ...logData.info }, { - ...defaultOpts, - dynamicMeta(cls, opts) { - expect(this).toBeInstanceOf(LogMessage); - expect(cls).toBeInstanceOf(LogMessage); - expect(typeof opts).toBe('object'); - return { dynamic: 'meta' }; - } - }); - - expect(msg.meta).toHaveProperty('dynamic', 'meta'); - }); - - test('"meta" should convert errors in metadata to object', () => { - const msg = new LogMessage({ ...logData.withMeta }, { - ...defaultOpts, - meta: { - foo: 'bar', - prop: new Error('meta error'), - obj: { not: 'an error' } - } - }); - - expect(typeof msg.meta.prop).toBe('object'); - expect(msg.meta.foo).toBe('bar'); - expect(typeof msg.meta.obj).toBe('object'); - }); - - test('"tags" should return tags array', () => { - const msg = new LogMessage({ ...logData.withTags }, defaultOpts); - - expect(Array.isArray(msg.tags)).toBe(true); - expect(msg.tags).toHaveLength(1); - expect(msg.tags).toContain('test'); - }); - - test('"tags" should combine all tags', () => { - const msg = new LogMessage({ ...logData.withTags }, { - ...defaultOpts, - tags: ['global'] - }); - - expect(msg.tags).toHaveLength(2); - expect(msg.tags).toContain('test'); - expect(msg.tags).toContain('global'); - }); - - test('"tags" should execute functions', () => { - const msg = new LogMessage({ ...logData.info }, { - ...defaultOpts, - tags: [function (props) { - expect(this).toBeInstanceOf(LogMessage); - expect(typeof props).toBe('object'); - expect(props).toHaveProperty('level', 'info'); - expect(props).toHaveProperty('meta', {}); - expect(props).toHaveProperty('options'); - - return 'dynamic'; - }] - }); - - const { tags } = msg; - - expect(tags).toHaveLength(1); - expect(tags).toContain('dynamic'); - }); - - test('"tags" should replace <> variable with log level', () => { - const msg = new LogMessage({ ...logData.info }, { - ...defaultOpts, - tags: ['', '<>', '<>'] - }); - - const { tags } = msg; - - expect(tags).toHaveLength(3); - expect(tags).toContain(''); - expect(tags).toContain('info'); - expect(tags).toContain('<>'); - }); - - test('"value" should return log object', () => { - const msg = new LogMessage({ ...logData.info, meta: { test: true }, tags: ['test'] }, defaultOpts); - const { value } = msg; - - expect(value).toHaveProperty('_logLevel', 'info'); - expect(value).toHaveProperty('msg', 'info test'); - expect(value).toHaveProperty('test', true); - expect(value).toHaveProperty('_tags', ['test']); - }); - - test('"value" should change key names based on options', () => { - const msg = new LogMessage({ ...logData.info, meta: { test: true }, tags: ['test'] }, { - ...defaultOpts, - levelKey: 'lvl', - messageKey: 'message', - tagsKey: 'tags' - }); - const { value } = msg; - - expect(value).toHaveProperty('lvl', 'info'); - expect(value).toHaveProperty('message', 'info test'); - expect(value).toHaveProperty('test', true); - expect(value).toHaveProperty('tags', ['test']); - }); - - test('"value" should hide level and tags when options is set to falsy value', () => { - const msg = new LogMessage({ ...logData.info, tags: ['test'] }, { - ...defaultOpts, - levelKey: undefined, - tagsKey: null - }); - const { value } = msg; - - expect(value).not.toHaveProperty('_logLevel'); - expect(value).toHaveProperty('msg', 'info test'); - expect(value).not.toHaveProperty('_tags'); - }); - - test('"log" should match "value"', () => { - const msg = new LogMessage({ ...logData.info }, defaultOpts); - - expect(msg.log).toStrictEqual(msg.value); - }); - - test('"throw" should throw the error passed to the log', () => { - const msg = new LogMessage({ ...logData.error }, defaultOpts); - - expect(() => msg.throw).toThrow('error test'); - }); - - test('"throw" should include access to LogMessage instance in thrown error', () => { - const msg = new LogMessage({ ...logData.error }, defaultOpts); - - try { - return msg.throw; - } catch(err) { - expect(err.log).toBeInstanceOf(LogMessage); - } - }); - - test('"throw" should throw a new error for non-errors passed to the log', () => { - const msg = new LogMessage({ ...logData.info }, defaultOpts); - - expect(() => msg.throw).toThrow('info test'); - }); -}); - -describe('Setters', () => { - test('set "msg" should overwrite the log message', () => { - const msg = new LogMessage({ ...logData.info }, defaultOpts); - msg.msg = 'test'; - - expect(msg.msg).toBe('test'); - }); - - test('set "message" should overwrite the log message', () => { - const msg = new LogMessage({ ...logData.info }, defaultOpts); - msg.message = 'test'; - - expect(msg.message).toBe('test'); - }); - - test('set "meta" should add additional properties to the log message', () => { - const msg = new LogMessage({ ...logData.info, meta: { custom: 123 } }, defaultOpts); - msg.meta = { prop: 'test' }; - - expect(msg.meta).toHaveProperty('prop', 'test'); - expect(msg.meta).toHaveProperty('custom', 123); - }); - - test('set "tags" should add new tags to the log message', () => { - const msg = new LogMessage({ ...logData.info, tags: ['test'] }, defaultOpts); - msg.tags = ['another-tag']; - - expect(msg.tags).toHaveLength(2); - expect(msg.tags).toContain('test'); - expect(msg.tags).toContain('another-tag'); - }); -}); - -describe('Methods', () => { - describe('toJSON()', () => { - const msg = new LogMessage({ ...logData.info }, { - ...defaultOpts, - meta: { ssn: '444-55-6666' }, - replacer(key, value) { - if(key === 'ssn') { - return `${value.substr(0, 3)}-**-****`; - } - - return value; - } - }); - - test('should return log in JSON format', () => { - expect(typeof msg.toJSON()).toBe('string'); - expect(msg.toJSON()).toMatch(/^\{.*\}$/); - }); - - test('should run replacer function', () => { - expect(JSON.parse(msg.toJSON()).ssn).toBe('444-**-****'); - }); - - test('should not run replacer function when not defined', () => { - const msg = new LogMessage({ ...logData.info }, { - ...defaultOpts, - meta: { ssn: '444-55-6666' } - }); - - expect(JSON.parse(msg.toJSON()).ssn).toBe('444-55-6666'); - }); - - test('should pretty print JSON when format is "true"', () => { - expect(/\n/g.test(msg.toJSON(true))).toBe(true); - }); - }); -}); diff --git a/src/LambdaLog.test.ts b/src/LambdaLog.test.ts new file mode 100644 index 0000000..e8fe872 --- /dev/null +++ b/src/LambdaLog.test.ts @@ -0,0 +1,270 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-empty-function */ +import LambdaLog, { defaultOptions } from './LambdaLog'; +import LogMessage from './LogMessage'; +import { ConsoleObject } from './typings'; + +const mockConsole = { + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn() +}; + +describe('LambdaLog', () => { + it.each([ + ['meta', {}], + ['tags', []], + ['dynamicMeta', null], + ['dev', false], + ['silent', false], + ['replacer', null], + ['logHandler', console], + ['levelKey', '__level'], + ['messageKey', 'msg'], + ['tagsKey', '__tags'] + ])('should set default option %s to equal %p', (key, expected) => { + const log = new LambdaLog(); + expect(log.options[key]).toStrictEqual(expected); + }); + + it.each([ + ['meta', { test: 'test' }], + ['tags', ['custom-tag']], + ['dynamicMeta', function () {}], + ['debug', true], + ['dev', true], + ['silent', true], + ['replacer', function () {}], + ['logHandler', function () {}], + ['levelKey', 'level'], + ['messageKey', 'message'], + ['tagsKey', 'tags'] + ])('should override the default option for %s with %p', (key, expected) => { + const log = new LambdaLog({ + [key]: expected + }); + + expect(log.options[key]).toStrictEqual(expected); + }); + + describe('Environment Variables', () => { + const truthyValues = ['true', '1', 'yes', 'y', 'on']; + const falsyValues = ['false', '0', 'no', 'n', 'off']; + + describe('LAMBALOG_LEVEL', () => { + it.each([ + 'trace', 'debug', 'info', 'warn', 'error', 'fatal' + ])('should set max log level when LAMBDALOG_LEVEL is set to %s', val => { + process.env.LAMBDALOG_LEVEL = val; + const log = new LambdaLog(); + + expect(log.options.level).toBe(val); + delete process.env.LAMBDALOG_LEVEL; + }); + + it('should only set the max log level when LAMBDALOG_LEVEL is set to a valid log level', () => { + process.env.LAMBDALOG_LEVEL = 'foo'; + const log = new LambdaLog(); + + expect(log.options.level).toBe('info'); + delete process.env.LAMBDALOG_LEVEL; + }); + }); + + describe.each([ + ['enable', truthyValues, true], + ['disable', falsyValues, false] + ])('LAMBALOG_DEV', (action, vals, expected) => { + it.each(vals)(`should ${action} dev mode when LAMBDALOG_DEV is set to %s`, val => { + process.env.LAMBDALOG_DEV = val; + const log = new LambdaLog(); + + expect(log.options.dev).toBe(expected); + delete process.env.LAMBDALOG_DEV; + }); + }); + + describe.each([ + ['enable', truthyValues, true], + ['disable', falsyValues, false] + ])('LAMBALOG_SILENT', (action, vals, expected) => { + it.each(vals)(`should ${action} silent mode when LAMBDALOG_SILENT is set to %s`, val => { + process.env.LAMBDALOG_SILENT = val; + const log = new LambdaLog(); + + expect(log.options.silent).toBe(expected); + delete process.env.LAMBDALOG_SILENT; + }); + }); + }); + + describe('Properties', () => { + it.each([ + ['LambdaLog', LambdaLog], + ['LogMessage', LogMessage] + ])('should have access to uninstantiated %s class', (name, expected) => { + const log = new LambdaLog(); + expect(log).toHaveProperty(name); + expect(log[name]).toBe(expected); + }); + }); + + describe('Methods', () => { + beforeEach(() => { + defaultOptions.logHandler = mockConsole as unknown as ConsoleObject; + }); + + afterAll(() => { + defaultOptions.logHandler = console; + }); + + afterEach(() => { + mockConsole.log.mockClear(); + mockConsole.info.mockClear(); + mockConsole.warn.mockClear(); + mockConsole.error.mockClear(); + mockConsole.debug.mockClear(); + }); + + describe('log()', () => { + it('should throw error for invalid log level', () => { + const log = new LambdaLog(); + expect(() => log.log('foo' as any, 'test')).toThrowError(/^"foo" is not a valid log level$/); + }); + + it('should throw error for no log level', () => { + const log = new LambdaLog(); + expect(() => log.log(null as any, 'test')).toThrowError(/is not a valid log level$/); + }); + + it('should return false for a disabled log level', () => { + const log = new LambdaLog({ level: 'fatal' }); + const result = log.log('debug', 'test'); + expect(result).toBe(false); + }); + + it('should return log message instance', () => { + const log = new LambdaLog(); + const result = log.log('info', 'test'); + expect(result).toBeInstanceOf(LogMessage); + }); + + it('should not log message when silent is enabled', () => { + const log = new LambdaLog({ silent: true }); + log.log('info', 'test'); + expect(mockConsole.info).toBeCalledTimes(0); + }); + + it('should emit "log" event with instance of LogMessage', done => { + const log = new LambdaLog(); + + log.on('log', msg => { + expect(msg).toBeInstanceOf(LogMessage); + done(); + }); + + const res = log.log('error', 'test'); + expect(res).toBeInstanceOf(LogMessage); + }); + + it('should default to `console` if logHandler is not provided', () => { + const log = new LambdaLog({ + logHandler: undefined + }); + + const info = jest.spyOn(console, 'info'); + log.log('info', 'test'); + expect(info).toBeCalled(); + info.mockRestore(); + }); + }); + + describe('assert()', () => { + it('should return false when test is a truthy value', () => { + const log = new LambdaLog(); + const result = log.assert(true, 'test'); + expect(result).toBe(false); + }); + + it('should log error if test is a falsy value', () => { + const log = new LambdaLog(); + const result = log.assert(false, 'test'); + + expect(result).toBeInstanceOf(LogMessage); + if(result !== false) { + expect(result.level).toBe('error'); + } + }); + }); + + describe('result()', () => { + it('should log promise results as info message on resolve', done => { + const log = new LambdaLog(); + const promise = log.result(Promise.resolve('Success!')); + + promise.then(msg => { + expect(msg).toBeInstanceOf(LogMessage); + if(msg !== false) { + expect(msg.level).toBe('info'); + expect(msg.msg).toBe('Success!'); + done(); + } + }); + }); + + it('should log promise error as an error message on reject', done => { + const log = new LambdaLog(); + const promise = log.result(Promise.reject(new Error('Failed!'))); + + promise.then(msg => { + expect(msg).toBeInstanceOf(LogMessage); + if(msg !== false) { + expect(msg.level).toBe('error'); + expect(msg.msg).toBe('Failed!'); + done(); + } + }); + }); + }); + + describe('shortcut methods', () => { + it('should log trace message', () => { + const log = new LambdaLog({ level: 'trace' }); + log.trace('test'); + expect(mockConsole.debug).toBeCalledTimes(1); + }); + + it('should log debug message', () => { + const log = new LambdaLog({ level: 'debug' }); + log.debug('test'); + expect(mockConsole.debug).toBeCalledTimes(1); + }); + + it('should log info message', () => { + const log = new LambdaLog({ level: 'info' }); + log.info('test'); + expect(mockConsole.info).toBeCalledTimes(1); + }); + + it('should log warn message', () => { + const log = new LambdaLog({ level: 'warn' }); + log.warn('test'); + expect(mockConsole.warn).toBeCalledTimes(1); + }); + + it('should log error message', () => { + const log = new LambdaLog({ level: 'error' }); + log.error('test'); + expect(mockConsole.error).toBeCalledTimes(1); + }); + + it('should log fatal message', () => { + const log = new LambdaLog({ level: 'fatal' }); + log.fatal('test'); + expect(mockConsole.error).toBeCalledTimes(1); + }); + }); + }); +}); diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts new file mode 100644 index 0000000..9e2ceeb --- /dev/null +++ b/src/LambdaLog.ts @@ -0,0 +1,292 @@ +import EventEmitter from 'events'; +import { LambdaLogOptions, Message, GenericRecord, LogLevels, LogObject, Tag, ConsoleObject } from './typings'; +import LogMessage from './LogMessage'; +import { toBool } from './utils'; + + +const levels = [{ + name: 'fatal', + method: 'error' +}, { + name: 'error', + method: 'error' +}, { + name: 'warn', + method: 'warn' +}, { + name: 'info', + method: 'info' +}, { + name: 'debug', + method: 'debug' +}, { + name: 'trace', + method: 'debug' +}]; + +/** + * Default options for the LambdaLog class. Stored globally so that it can be modified by the user. + */ +export const defaultOptions: LambdaLogOptions = { + meta: {}, + tags: [], + dynamicMeta: null, + level: 'info', + dev: false, + silent: false, + replacer: null, + logHandler: console, + levelKey: '__level', + messageKey: 'msg', + tagsKey: '__tags' +}; + + +export default class LambdaLog extends EventEmitter { + /** + * Access to the uninstantiated LambdaLog class. + * @readonly + * @type {LambdaLog} + */ + readonly LambdaLog = LambdaLog; + /** + * Access to the uninstantiated LogMessage class. + * @type {LogMessage} + */ + LogMessage = LogMessage; + + /** + * The options object for this instance of LambdaLog. + * @type {LambdaLogOptions} + */ + options: LambdaLogOptions; + + /** + * Access to the log levels configuration. + * @readonly + */ + readonly levels = levels; + + /** + * Returns the console object to use for logging. + * @readonly + * @private + * @returns {ConsoleObject} The configured console object or `console` if none is provided. + */ + private get console(): ConsoleObject { + return this.options.logHandler ?? console; + } + + /** + * Constructor for the LambdaLog class. Provided to be utilized in more advanced cases to allow overriding and configuration. + * By default, this module will export an instance of this class, but you may access the class and create your own instance + * via `log.LambdaLog`. + * @class + * @param {LambdaLogOptions} [options={}] Configuration options for LambdaLog. + */ + constructor(options: LambdaLogOptions = {}) { + super(); + + this.options = { + ...defaultOptions, + ...options + }; + + // Override the max log level from the environment variable + if(process.env.LAMBDALOG_LEVEL) { + const lvl = this.getLevel(process.env.LAMBDALOG_LEVEL); + if(lvl) this.options.level = lvl.name as keyof LambdaLogOptions['level']; + } + + // Override the dev setting from the environment variable + if(process.env.LAMBDALOG_DEV) { + this.options.dev = toBool(process.env.LAMBDALOG_DEV); + } + + // Override the silent setting from the environment variable + if(process.env.LAMBDALOG_SILENT) { + this.options.silent = toBool(process.env.LAMBDALOG_SILENT); + } + } + + /** + * Generates JSON log message based on the provided parameters and the global configuration. Once the JSON message is created, it is properly logged to the `console` + * and emitted through an event. If an `Error` or `Error`-like object is provided for `msg`, it will parse out the message and include the stacktrace in the metadata. + * @throws {Error} If improper log level is provided. + * @template T The type of the message to log. + * @param {string} level Log level (`info`, `debug`, `warn`, `error`, or `fatal`) + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {object} [meta={}] Optional meta data to attach to the log. + * @param {string[]} [tags=[]] Additional tags to append to this log. + * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + */ + log(level: LogLevels, msg: T, meta: GenericRecord = {}, tags: Tag[] = []) { + const lvl = this.getLevel(level); + if(!lvl) { + throw new Error(`"${level}" is not a valid log level`); + } + + // Check if we can log this level + if(lvl.idx > this.maxLevelIdx) return false; + + // Generate the log message instance + const message = new this.LogMessage({ + level, + msg, + meta, + tags + } as LogObject, this.options); + + const consoleObj = this.console; + // Log the message to the console + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + consoleObj[lvl.method](message.toString()); + + /** + * The log event is emitted (using EventEmitter) for every log generated. This allows for custom integrations, such as logging to a thrid-party service. + * This event is emitted with the [LogMessage](#logmessage) instance for the log. You may control events using all the methods of EventEmitter. + * @event LambdaLog#log + * @type {LogMessage} + */ + this.emit('log', message); + return message; + } + + /** + * Logs a message at the `trace` log level. + * @template T The type of the message to log. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Tag[]} [tags] Additional tags to append to this log. + * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + */ + trace(msg: T, meta?: GenericRecord, tags?: Tag[]) { + return this.log('trace', msg, meta, tags); + } + + /** + * Logs a message at the `debug` log level. + * @template T The type of the message to log. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Tag[]} [tags] Additional tags to append to this log. + * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + */ + debug(msg: T, meta?: GenericRecord, tags?: Tag[]) { + return this.log('debug', msg, meta, tags); + } + + /** + * Logs a message at the `info` log level. + * @template T The type of the message to log. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Tag[]} [tags] Additional tags to append to this log. + * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + */ + info(msg: T, meta?: GenericRecord, tags?: Tag[]) { + return this.log('info', msg, meta, tags); + } + + /** + * Logs a message at the `warn` log level. + * @template T The type of the message to log. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Tag[]} [tags] Additional tags to append to this log. + * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + */ + warn(msg: T, meta?: GenericRecord, tags?: Tag[]) { + return this.log('warn', msg, meta, tags); + } + + /** + * Logs a message at the `error` log level. + * @template T The type of the message to log. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Tag[]} [tags] Additional tags to append to this log. + * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + */ + error(msg: T, meta?: GenericRecord, tags?: Tag[]) { + return this.log('error', msg, meta, tags); + } + + /** + * Logs a message at the `error` log level. + * @template T The type of the message to log. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Tag[]} [tags] Additional tags to append to this log. + * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + */ + fatal(msg: T, meta?: GenericRecord, tags?: Tag[]) { + return this.log('fatal', msg, meta, tags); + } + + /** + * Generates a log message if `test` is a falsy value. If `test` is truthy, the log message is skipped and returns `false`. Allows creating log messages without the need to + * wrap them in an if statement. The log level will be `error`. + * @since 1.4.0 + * @template T The type of the message to log. + * @param {*} test Value to test for a falsy value. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {object} [meta={}] Optional meta data to attach to the log. + * @param {string[]} [tags=[]] Additional tags to append to this log. + * @returns {LogMessage|false} The generated log message or `false` if assertion passed. + */ + assert(test: unknown, msg: T, meta?: GenericRecord, tags?: Tag[]) { + if(test) return false; + return this.log('error', msg, meta, tags); + } + + /** + * Generates a log message with the result or error provided by a promise. Useful for debugging and testing. + * @since 2.3.0 + * @param {Promise<*>} promise Promise to log the results of. + * @param {object} [meta={}] Optional meta data to attach to the log. + * @param {string[]} [tags=[]] Additional tags to append to this log. + * @returns {Promise} A Promise that resolves with the log message. + */ + async result(promise: Promise, meta?: GenericRecord, tags?: Tag[]) { + const wrapper = new Promise(resolve => { + promise + .then(value => { + resolve(this.log('info', value as string, meta, tags)); + }) + .catch(err => { + resolve(this.log('error', err as Error, meta, tags)); + }); + }); + + return wrapper; + } + + /** + * Validates and gets the configuration for the provided log level. + * @private + * @param {string} level The provided log level string. + * @returns {object} Returns the configuration for the provided log level. + */ + private getLevel(level: string) { + if(!level) return false; + const lvl = levels.findIndex(l => l.name === level.toLowerCase()); + if(lvl === -1) return false; + + return { + idx: lvl, + ...levels[lvl] + }; + } + + /** + * Returns the index of the configured maximum log level. + * @readonly + * @private + * @returns {number} The index of the configured maximum log level. + */ + private get maxLevelIdx() { + if(!this.options.level || this.options.silent) return -1; + return levels.findIndex(l => l.name === this.options.level); + } +} diff --git a/src/LogMessage.test.ts b/src/LogMessage.test.ts new file mode 100644 index 0000000..d9a7b28 --- /dev/null +++ b/src/LogMessage.test.ts @@ -0,0 +1,472 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import LogMessage from './LogMessage'; +import { StubbedError } from './typings'; + +const logData = { + info: { + level: 'info', + msg: 'info test', + meta: {}, + tags: [] + }, + + error: { + level: 'error', + msg: new Error('error test'), + meta: {}, + tags: [] + }, + + withMeta: { + level: 'info', + msg: 'info test', + meta: { test: true }, + tags: [] + }, + + withTags: { + level: 'info', + msg: 'info test', + meta: {}, + tags: ['test'] + } +}; + +const defaultOpts = { + meta: {}, + dynamicMeta: null, + tags: [], + levelKey: '_logLevel', + messageKey: 'msg', + tagsKey: '_tags', + replacer: null +}; + +describe('LogMessage', () => { + it.each([ + ['__msg', logData.info.msg], + ['__meta', {}], + ['__tags', []], + ['__error', null], + ['__opts', defaultOpts] + ])('should set property %s on class', (key, expected) => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + + expect(msg[key]).toStrictEqual(expected); + }); + + it.each([ + ['string', 'string meta'], + ['array', ['array', 'meta']], + ['boolean', true], + ['number', 123] + ])('should convert non-object (%s) meta to an object', (_, value) => { + const msg = new LogMessage({ + ...logData.info, + meta: value as any + }, defaultOpts); + + expect(msg.__meta).toStrictEqual({ meta: value }); + }); + + it('should set log meta as an empty object if not provided', () => { + const msg = new LogMessage({ ...logData.info, meta: undefined }, defaultOpts); + + expect(msg.__meta).toStrictEqual({}); + }); + + it('should set log tags as an empty array if not provided', () => { + const msg = new LogMessage({ ...logData.info, tags: undefined }, defaultOpts); + + expect(msg.__tags).toStrictEqual([]); + }); + + it('should update all properties when an error is passed in', () => { + const msg = new LogMessage({ ...logData.error }, defaultOpts); + + expect(msg.__msg).toBe('error test'); + expect(msg.__error).toStrictEqual(logData.error.msg); + expect(msg.__meta.stack).toBe(logData.error.msg.stack); + }); + + + describe('Getters', () => { + it('"level" should return the log level', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + + expect(msg.level).toBe('info'); + }); + + it('"msg" and "message" should return the log message', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + + expect(msg.msg).toBe('info test'); + expect(msg.message).toBe(msg.msg); + }); + + it('"meta" should return metadata object', () => { + const msg = new LogMessage({ ...logData.withMeta }, defaultOpts); + + expect(msg.meta).toHaveProperty('test', true); + }); + + it('"meta" should combine all metadata', () => { + const msg = new LogMessage({ ...logData.withMeta }, { + ...defaultOpts, + meta: { foo: 'bar' }, + dynamicMeta: () => ({ dynamic: 'meta' }) + }); + + expect(msg.meta).toHaveProperty('test', true); + expect(msg.meta).toHaveProperty('foo', 'bar'); + expect(msg.meta).toHaveProperty('dynamic', 'meta'); + }); + + it('"meta" should call dynamicMeta', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + dynamicMeta(cls, opts) { + expect(this).toBeInstanceOf(LogMessage); + expect(cls).toBeInstanceOf(LogMessage); + expect(typeof opts).toBe('object'); + return { dynamic: 'meta' }; + } + }); + + expect(msg.meta).toHaveProperty('dynamic', 'meta'); + }); + + it('"meta" should convert errors in metadata to object', () => { + const msg = new LogMessage({ ...logData.withMeta }, { + ...defaultOpts, + meta: { + foo: 'bar', + prop: new Error('meta error'), + obj: { not: 'an error' } + } + }); + + expect(typeof msg.meta.prop).toBe('object'); + expect(msg.meta.foo).toBe('bar'); + expect(typeof msg.meta.obj).toBe('object'); + }); + + it('"tags" should return tags array', () => { + const msg = new LogMessage({ ...logData.withTags }, defaultOpts); + + expect(Array.isArray(msg.tags)).toBe(true); + expect(msg.tags).toHaveLength(1); + expect(msg.tags).toContain('test'); + }); + + it('"tags" should combine all tags', () => { + const msg = new LogMessage({ ...logData.withTags }, { + ...defaultOpts, + tags: ['global'] + }); + + expect(msg.tags).toHaveLength(2); + expect(msg.tags).toContain('test'); + expect(msg.tags).toContain('global'); + }); + + it('"tags" should execute functions', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + tags: [function (this: typeof LogMessage, props) { + expect(this).toBeInstanceOf(LogMessage); + expect(typeof props).toBe('object'); + expect(props).toHaveProperty('level', 'info'); + expect(props).toHaveProperty('meta', {}); + expect(props).toHaveProperty('options'); + + return 'dynamic'; + }] + }); + + const { tags } = msg; + + expect(tags).toHaveLength(1); + expect(tags).toContain('dynamic'); + }); + + it('"tags" should replace <> variable with log level', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + tags: ['', '<>', '<>'] + }); + + const { tags } = msg; + + expect(tags).toHaveLength(3); + expect(tags).toContain(''); + expect(tags).toContain('info'); + expect(tags).toContain('<>'); + }); + + it('"value" should return log object', () => { + const msg = new LogMessage({ ...logData.info, meta: { test: true }, tags: ['test'] }, defaultOpts); + const { value } = msg; + + expect(value).toHaveProperty('_logLevel', 'info'); + expect(value).toHaveProperty('msg', 'info test'); + expect(value).toHaveProperty('test', true); + expect(value).toHaveProperty('_tags', ['test']); + }); + + it('"value" should change key names based on options', () => { + const msg = new LogMessage({ ...logData.info, meta: { test: true }, tags: ['test'] }, { + ...defaultOpts, + levelKey: 'lvl', + messageKey: 'message', + tagsKey: 'tags' + }); + const { value } = msg; + + expect(value).toHaveProperty('lvl', 'info'); + expect(value).toHaveProperty('message', 'info test'); + expect(value).toHaveProperty('test', true); + expect(value).toHaveProperty('tags', ['test']); + }); + + it('"value" should hide level and tags when options is set to falsy value', () => { + const msg = new LogMessage({ ...logData.info, tags: ['test'] }, { + ...defaultOpts, + levelKey: false, + tagsKey: false, + messageKey: undefined + }); + const { value } = msg; + + expect(value).not.toHaveProperty('__level'); + expect(value).toHaveProperty('msg', 'info test'); + expect(value).not.toHaveProperty('__tags'); + }); + + it('"value" should default level and tags key when set to null or undefined', () => { + const msg = new LogMessage({ ...logData.info, tags: ['test'] }, { + ...defaultOpts, + levelKey: undefined, + tagsKey: undefined + }); + const { value } = msg; + + expect(value).toHaveProperty('__level'); + expect(value).toHaveProperty('__tags'); + }); + + it('"value" should compile the log with `onCompile` is set in the options', () => { + const msg = new LogMessage({ ...logData.info, meta: { test: true }, tags: ['test'] }, { + ...defaultOpts, + onCompile(level, msg, meta, tags) { + return { + testLevel: level, + testMsg: msg, + testMeta: meta, + testTags: tags + }; + } + }); + + const { value } = msg; + + expect(value).toHaveProperty('testLevel', 'info'); + expect(value).toHaveProperty('testMsg', 'info test'); + expect(value).toHaveProperty('testMeta', { test: true }); + expect(value).toHaveProperty('testTags', ['test']); + }); + + it('"log" should match "value"', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + + expect(msg.log).toStrictEqual(msg.value); + }); + + it('"throw" should throw the error passed to the log', () => { + const msg = new LogMessage({ ...logData.error }, defaultOpts); + + expect(() => msg.throw).toThrow('error test'); + }); + + it('"throw" should include access to LogMessage instance in thrown error', () => { + const msg = new LogMessage({ ...logData.error }, defaultOpts); + + try { + return msg.throw; + } catch(err: unknown) { + expect((err as StubbedError).log).toBeInstanceOf(LogMessage); + } + }); + + it('"throw" should throw a new error for non-errors passed to the log', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + + expect(() => msg.throw).toThrow('info test'); + }); + }); + + + describe('Setters', () => { + it('set "msg" should overwrite the log message', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + msg.msg = 'test'; + + expect(msg.msg).toBe('test'); + }); + + it('set "msg" should utilize `onParse` if set in the options', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + onParse(msg) { + return { msg: `${msg as string} custom`, meta: { test: 'meta' }, tags: ['custom'], error: new Error('custom error') }; + } + }); + + expect(msg.msg).toBe('info test custom'); + expect(msg.meta).toHaveProperty('test', 'meta'); + expect(msg.tags).toHaveLength(1); + expect(msg.tags).toContain('custom'); + expect(msg.__error).toBeInstanceOf(Error); + expect((msg.__error!).message).toBe('custom error'); + }); + + it('set "msg" should json stringify a message that is an object', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + msg.msg = { test: true } as any; + + expect(msg.msg).toBe('{"test":true}'); + }); + + it('set "msg" should return empty string if message is null or undefined', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + + msg.msg = null as any; + expect(msg.msg).toBe(''); + + msg.msg = undefined as any; + expect(msg.msg).toBe(''); + }); + + it('set "message" should overwrite the log message', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + msg.message = 'test'; + + expect(msg.message).toBe('test'); + }); + + it('set "meta" should add additional properties to the log message', () => { + const msg = new LogMessage({ ...logData.info, meta: { custom: 123 } }, defaultOpts); + msg.meta = { prop: 'test' }; + + expect(msg.meta).toHaveProperty('prop', 'test'); + expect(msg.meta).toHaveProperty('custom', 123); + }); + + it('set "tags" should add new tags to the log message', () => { + const msg = new LogMessage({ ...logData.info, tags: ['test'] }, defaultOpts); + msg.tags = ['another-tag']; + + expect(msg.tags).toHaveLength(2); + expect(msg.tags).toContain('test'); + expect(msg.tags).toContain('another-tag'); + }); + }); + + describe('Methods', () => { + describe('toJSON()', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + meta: { ssn: '444-55-6666' }, + replacer(key, value) { + if(key === 'ssn') { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call + return `${value.substr(0, 3)}-**-****`; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + } + }); + + it('should return log in JSON format', () => { + expect(typeof msg.toJSON()).toBe('string'); + expect(msg.toJSON()).toMatch(/^\{.*\}$/); + }); + + it('should run replacer function', () => { + expect(JSON.parse(msg.toJSON()).ssn).toBe('444-**-****'); + }); + + it('should not run replacer function when not defined', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + meta: { ssn: '444-55-6666' } + }); + + expect(JSON.parse(msg.toJSON()).ssn).toBe('444-55-6666'); + }); + + it('should pretty print JSON when format is "true"', () => { + expect(/\n/g.test(msg.toJSON(true))).toBe(true); + }); + }); + + describe('toString()', () => { + it('should return log in string format', () => { + const msg = new LogMessage({ ...logData.info }, defaultOpts); + + expect(typeof msg.toString()).toBe('string'); + }); + + it('should format as json with onFormat set to `json`', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + onFormat: 'json' + }); + + expect(msg.toString()).toMatch(/^\{.*\}$/); + }); + + it('should format with "clean" template with onFormat set to `clean`', () => { + const msg = new LogMessage({ ...logData.info, tags: ['test'] }, { + ...defaultOpts, + onFormat: 'clean' + }); + + expect(msg.toString()).toMatch(/^INFO\tinfo test\n\t├→ test/); + }); + + it('should not add tags when none are set in `clean` formatter', () => { + const msg = new LogMessage({ ...logData.info, meta: { test: 123 } }, { + ...defaultOpts, + onFormat: 'clean' + }); + + expect(msg.toString()).toMatch(/^INFO\tinfo test\n\t└→ \{/); + }); + + it('should format with "minimal" template with onFormat set to `minimal`', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + onFormat: 'minimal' + }); + + expect(msg.toString()).toMatch(/^INFO\tinfo test$/); + }); + + it('should format with a custom `onFormat` function', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + tags: ['tag1', 'tag2'], + onFormat(message: LogMessage, opts, stringify) { + expect(stringify).toBeInstanceOf(Function); + + return `custom ${message.msg} ${opts.tags ? opts.tags.join('|') : ''}`; + } + }); + + expect(msg.toString()).toBe('custom info test tag1|tag2'); + }); + }); + }); +}); diff --git a/src/LogMessage.ts b/src/LogMessage.ts new file mode 100644 index 0000000..be7e450 --- /dev/null +++ b/src/LogMessage.ts @@ -0,0 +1,326 @@ +import stringify from 'fast-safe-stringify'; +import { LambdaLogOptions, Message, LogObject, Tag, GenericRecord, Formatter, StubbedError, Empty } from './typings'; +import { isError, stubError } from './utils'; + +export interface ILogMessage { + readonly __opts: LambdaLogOptions; + __level: string; + __msg: string; + __meta: GenericRecord; + __tags: Tag[]; + __error: Error | StubbedError | Empty; + + get level(): string; + get msg(): string; + set msg(msg: Message); + get message(): string; + set message(msg: Message); + get meta(): GenericRecord; + set meta(obj: GenericRecord); + get tags(): Tag[]; + set tags(tags: Tag[]); + get value(): GenericRecord; + get log(): GenericRecord; + get throw(): void; + + toJSON(format: boolean): string; +} + +/** + * The LogMessage class is a private/internal class that is used for generating log messages. All log methods return an instance of LogMessage allowing for a chainable api. + * Having a seperate class and instance for each log allows chaining and the ability to further customize this module in the future without major breaking changes. The documentation + * provided here is what is available to you for each log message. + */ +export default class LogMessage implements ILogMessage { + readonly __opts: LambdaLogOptions = {}; + __level: string; + __msg = ''; + __meta: GenericRecord = {}; + __tags: Tag[] = []; + __error: Error | null | undefined = null; + + + /** + * Constructor for LogMessage + * @param {LogObject} log The log object to construct the LogMessage from. + * @param {LambdaLogOptions} opts The options for LambdaLog. + * @class + */ + constructor(log: LogObject, opts: LambdaLogOptions) { + // LambdaLog options + this.__opts = opts; + // Log level + this.__level = log.level; + + // Compile log metadata + if(log.meta) { + this.__meta = typeof log.meta !== 'object' || Array.isArray(log.meta) ? { meta: log.meta } : log.meta; + } + + // Compile log tags + if(log.tags && Array.isArray(log.tags)) { + this.__tags = log.tags; + } + + // Compile log message + this.setMessage(log.msg); + } + + /** + * String log level of the message. + * @returns {string} The log level. + */ + get level() { + return this.__level; + } + + /** + * The message for the log. If an Error was provided, it will be the message of the error. + * @returns {string} The message for the log. + */ + get msg(): string { + return this.__msg; + } + + /** + * Update the message for this log to something else. + * @param {Message} msg The new message for this log. + */ + set msg(msg: Message) { + this.setMessage(msg); + } + + /** + * Alias for `this.msg`. + * @returns {string} The message for the log. + */ + get message() { + return this.msg; + } + + /** + * Alias for `this.msg = 'New message';` + * @param {Message} msg The new message for this log. + */ + set message(msg) { + this.msg = msg; + } + + /** + * The fully compiled metadata object for the log. Includes global and dynamic metadata. + * @returns {GenericRecord} The metadata object. + */ + get meta(): GenericRecord { + const { __opts, __meta } = this; + + let meta: GenericRecord = { + ...__meta, + ...__opts.meta + }; + + if(typeof __opts.dynamicMeta === 'function') { + const dynMeta = __opts.dynamicMeta.call(this, this, __opts); + + if(typeof dynMeta === 'object') { + meta = { ...meta, ...dynMeta }; + } + } + + for(const [key, val] of Object.entries(meta)) { + if(typeof val !== 'object' || !val) continue; + if(isError(val)) { + meta[key] = stubError(val as Error); + } + } + + return meta; + } + + /** + * Set additional metadata on the log message. + * @param {GenericRecord} obj The metadata to add to the log. + */ + set meta(obj) { + this.__meta = { + ...this.__meta, + ...obj + }; + } + + /** + * Array of tags attached to this log. Includes global tags. + * @returns {Tag[]} The tags attached to this log. + */ + get tags() { + const { __opts, __tags } = this; + let tags: Tag[] = [...__tags]; + + if(Array.isArray(__opts.tags)) { + tags = [...__opts.tags, ...tags]; + } + + return tags.map(tag => { + if(typeof tag === 'function') { + const tagRes = tag.call(this, { + level: this.level, + meta: this.meta, + options: __opts + }); + + if(tagRes) tag = tagRes; + } + + if(typeof tag === 'string') { + tag = tag.replace(/(<<([a-z0-9_]+)>>)/gi, (_, tplVar: string, key: string) => { + if(key === 'level') return this.level; + return tplVar; + }); + } + + return tag; + }).filter(tag => typeof tag === 'string' && tag); + } + + /** + * Appends additional tags to this log message. + * @param {Tag[]} tags The tags to add to this log. + */ + set tags(tags: Tag[]) { + this.__tags = [...this.__tags, ...tags]; + } + + /** + * The full log object. This is the object used in logMessage.toJSON() and when the log is written to the console. + * @returns {GenericRecord} The compiled log object. + */ + get value() { + const { __opts } = this; + + if(typeof __opts.onCompile === 'function') { + return __opts.onCompile.call(this, this.level, this.msg, this.meta, this.tags, __opts); + } + + let log: GenericRecord = {}; + if(__opts.levelKey !== false) log[__opts.levelKey ?? '__level'] = this.level; + log[__opts.messageKey ?? 'msg'] = this.msg; + log = { ...log, ...this.meta }; + if(__opts.tagsKey !== false) log[__opts.tagsKey ?? '__tags'] = this.tags; + + return log; + } + + /** + * Alias of `logMessage.value`. + * @returns {GenericRecord} The compiled log object. + */ + get log() { + return this.value; + } + + /** + * Throws the log. If an error was not provided, one will be generated for you and thrown. This is useful in cases where you need to log an + * error, but also throw it. + * @throws {Error} The provided error, or a newly generated error. + */ + get throw() { + const err = (this.__error ?? new Error(this.msg)) as StubbedError; + err.log = this; + + throw err; + } + + /** + * Returns the compiled log object converted into JSON. This method utilizes `options.replacer` for the replacer function. It also uses + * [fast-safe-stringify](https://www.npmjs.com/package/fast-safe-stringify) to prevent circular reference issues. + * @param {boolean} format Whether to format the log object with line breaks and indentation. + * @returns {string} The JSON string. + */ + toJSON(format?: boolean) { + return stringify(this.value, this.__opts.replacer ?? undefined, format ? 2 : 0); + } + + /** + * Converts the log to a string using a specific/custom formatter. + * @returns {string} The formatted log as a string. + */ + toString() { + return this.formatMessage(this.__opts.onFormat); + } + + /** + * Converts the log to a string using a specific/custom formatter. + * @protected + * @param {Formatter} [formatter] The formatter to use or custom formatter function. + * @returns {string} The formatted log as a string. + */ + protected formatMessage(formatter?: Formatter) { + const { __opts } = this; + + if(typeof formatter === 'function') { + return formatter.call(this, this, __opts, stringify); + } + + switch (formatter) { + // Clean Formatter + case 'clean': + return [ + `${this.level.toUpperCase()}\t${this.msg}`, + this.tags.length ? `\t├→ ${this.tags.join(', ')}` : null, + Object.keys(this.meta).length ? `\t└→ ${stringify(this.meta, undefined, 4).replace(/\n/g, '\n\t┊ ')}` : null + ].filter(v => Boolean(v)).join('\n'); + + // Minimal Formatter + case 'minimal': + return `${this.level.toUpperCase()}\t${this.msg}`; + + // JSON Formatter (default) + case 'json': + default: + return this.toJSON(__opts.dev); + } + } + + /** + * Takes a log message, parses it, and sets any corresponding properties on the log. + * @protected + * @param {Message} message The incoming message. + */ + protected setMessage(message: Message) { + // Compile log message + const { msg, meta, error, tags } = this.parseMessage(message); + this.__msg = msg; + + // Add any meta, error object, or tags derrived from the message + if(meta) this.__meta = { ...this.__meta, ...meta }; + if(error) this.__error = error; + if(tags) this.__tags = [...this.__tags, ...tags]; + } + + /** + * Parses a log message and returns any meta, error object, or tags derrived from the message. + * @protected + * @param {Message} msg The incoming message. + * @returns {object} Object continaing msg, meta, error, and tags. + */ + protected parseMessage(msg: Message): { msg: string; meta?: GenericRecord; error?: Error; tags?: Tag[] } { + const { __opts } = this; + + if(typeof __opts.onParse === 'function') { + const res = __opts.onParse.call(this, msg, __opts); + if(res && typeof res === 'object') return res; + } + + if(msg === null || msg === undefined) return { msg: '' }; + + if(isError(msg)) { + const err = msg as Error; + return { msg: err.message, meta: { stack: err.stack }, error: err }; + } + + if(typeof msg === 'object') { + return { msg: JSON.stringify(msg) }; + } + + return { msg: msg.toString() }; + } +} diff --git a/index.test.js b/src/index.test.ts similarity index 55% rename from index.test.js rename to src/index.test.ts index 9083681..435b7cc 100644 --- a/index.test.js +++ b/src/index.test.ts @@ -1,5 +1,6 @@ -const log = require('.'); -const LambdaLog = require('./lib/LambdaLog'); +import log from '.'; +import LambdaLog from './LambdaLog'; +import LogMessage from './LogMessage'; describe('Default Export', () => { test('returns an instance of the LambdaLog class', () => { @@ -9,4 +10,8 @@ describe('Default Export', () => { test('to have access to uninstantiated LambdaLog class', () => { expect(log.LambdaLog).toEqual(LambdaLog); }); + + test('to have access to uninstantiated LogMessage class', () => { + expect(log.LogMessage).toEqual(LogMessage); + }); }); diff --git a/index.js b/src/index.ts similarity index 57% rename from index.js rename to src/index.ts index b7a4919..13f1595 100644 --- a/index.js +++ b/src/index.ts @@ -1,9 +1,11 @@ -const LambdaLog = require('./lib/LambdaLog'); +import LambdaLog from './LambdaLog'; +import LogMessage from './LogMessage'; + /** * Instance of the LambdaLog class which is exported when calling `require('lambda-log')`. For more * advanced usage, you can create a new instance of the LambdaLog class via `new log.LambdaLog()`. * @type {LambdaLog} */ -const log = new LambdaLog(); - -module.exports = log; +export default new LambdaLog(); +export { LambdaLog, LogMessage }; +export * from './typings'; diff --git a/src/typings.ts b/src/typings.ts new file mode 100644 index 0000000..d1fa587 --- /dev/null +++ b/src/typings.ts @@ -0,0 +1,64 @@ +import LogMessage from './LogMessage'; +import stringify from 'fast-safe-stringify'; + +export type GenericRecord = Record; +export type Message = T; +export type Empty = false | null | undefined; + +type TagFnObject = { + level: string; + meta: GenericRecord; + options: LambdaLogOptions; +}; + +export type Tag = string | number | ((data: TagFnObject) => string | Empty) | Empty; + +type StringifyType = typeof stringify; + +export type Formatter = + 'json' | + 'clean' | + 'minimal' | + ((ctx: LogMessage, options: LambdaLogOptions, stringify: StringifyType) => string); + +export type LogObject = { + level: string; + msg: Message; + meta?: GenericRecord; + tags?: Tag[]; +}; + +export type LogLevels = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; + +export type LambdaLogOptions = { + [key: string]: any; + meta?: GenericRecord; + tags?: Tag[]; + dynamicMeta?: ((ctx: LogMessage, options: LambdaLogOptions) => GenericRecord | Empty) | Empty; + level?: false | LogLevels; + dev?: boolean; + silent?: boolean; + replacer?: ((key: string, val: any) => any) | null | undefined; + logHandler?: ConsoleObject; + levelKey?: string | false; + messageKey?: string; + tagsKey?: string | false; + onParse?: (msg: Message, options: LambdaLogOptions) => { msg: string; meta?: GenericRecord; error?: Error; tags?: Tag[] } | Empty; + onCompile?: (level?: string, msg?: Message, meta?: GenericRecord, tags?: Tag[], options?: LambdaLogOptions) => GenericRecord; + onFormat?: Formatter; +}; + +export interface ConsoleObject extends Console { + [key: string]: any; + log: (...params: any) => void; + info: (...params: any) => void; + warn: (...params: any) => void; + error: (...params: any) => void; + debug: (...params: any) => void; +} + +export interface StubbedError extends Error { + [key: string]: any; + log?: LogMessage; + toJSON?: () => GenericRecord; +} diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..b655477 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,86 @@ +import { StubbedError } from './typings'; +import { isError, stubError, toBool } from './utils'; + +interface TestError extends StubbedError { + test?: boolean; +} + +describe('utils', () => { + describe('isError()', () => { + it('should return true for an Error', () => { + expect(isError(new Error('test'))).toBe(true); + }); + + it('should return true for an Error-like object', () => { + expect(isError({ + message: 'test', + stack: 'stack' + })).toBe(true); + }); + + it('should return false for anything else', () => { + expect(isError(null)).toBe(false); + expect(isError(true)).toBe(false); + expect(isError(123)).toBe(false); + expect(isError('A string')).toBe(false); + expect(isError({ message: 'test' })).toBe(false); + expect(isError([1, 2, 3, 'test'])).toBe(false); + expect(isError({ stack: 'stack' })).toBe(false); + }); + }); + + describe('stubError()', () => { + it('should add a toJSON method to an error', () => { + const err = stubError(new Error('test')); + expect(err).toHaveProperty('toJSON'); + expect(typeof err.toJSON).toBe('function'); + }); + + it('calling toJSON should create an serializable object', () => { + const e = new Error('test') as TestError; + e.test = true; + const err = stubError(e); + const result = err.toJSON!(); + + expect(typeof result).toBe('object'); + expect(result).toHaveProperty('name', 'Error'); + expect(result).toHaveProperty('message', 'test'); + expect(result).toHaveProperty('stack', err.stack); + expect(result).toHaveProperty('test', true); + }); + + it('should skip custom toJSON fn if error already has one', () => { + const e = new Error('test'); + const noop = () => { /* noop */ }; + Object.defineProperty(e, 'toJSON', { + value: noop + }); + + const error = stubError(e); + expect(error.toJSON!.toString()).toBe(noop.toString()); + }); + }); + + describe('toBool()', () => { + it.each([ + ['true', true], + ['false', false], + ['1', true], + ['0', false], + ['yes', true], + ['no', false], + ['y', true], + ['n', false], + ['on', true], + ['off', false], + ['', false], + ['test', false], + [null, false], + [undefined, false], + [1, true], + [0, false] + ])('should accept "%s" and return %p', (input, expected) => { + expect(toBool(input as any)).toBe(expected); + }); + }); +}); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..684676f --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,64 @@ +import { GenericRecord, StubbedError } from './typings'; + +/** + * Checks if value is an Error or Error-like object + * @param {any} val The value to check + * @returns {boolean} Whether the value is an Error or Error-like object + */ +export function isError(val: unknown) { + return Boolean(val) && typeof val === 'object' && ( + val instanceof Error || ( + Object.prototype.hasOwnProperty.call(val, 'message') && + Object.prototype.hasOwnProperty.call(val, 'stack') + ) + ); +} + +/** + * Stubs an Error or Error-like object to include a toJSON method. + * @param {Error} error The error to stub. + * @returns {StubbedError} The original error object with a toJSON method. + */ +export function stubError(error: Error) { + const err = error as StubbedError; + + if(typeof err.toJSON === 'function') return err; + + err.toJSON = function () { + const keys = [ + 'name', + 'message', + 'stack' + ].concat(Object.keys(err)); + + return keys.reduce((obj: GenericRecord, key) => { + if(key in err) { + const val: unknown = err[key]; + if(typeof val !== 'function') { + obj[key] = val; + } + } + + return obj; + }, {}); + }; + + return err; +} + +/** + * Converts a string or number value to a boolean. + * @param {string|number|boolean} val The value to convert. + * @returns {boolean} The converted value as a boolean. + */ +export function toBool(val: string | number | boolean) { + if(typeof val === 'string') { + return ['true', 'yes', 'y', 'on', '1'].includes(val.toLowerCase()); + } + + if(typeof val === 'number') { + return val === 1; + } + + return Boolean(val); +} From 0e8f0ac907be771290627d3aac25b507b955c529 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:13:19 -0500 Subject: [PATCH 04/83] feat: support esm and cjs --- package.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/package.json b/package.json index 487d6bb..ab16741 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,22 @@ "version": "3.1.0", "description": "Lightweight logging library for any Node 10+ applications", "main": "index.js", + "main": "dist/cjs/index.js", + "types": "dist/cjs/index.d.ts", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + }, + "./LambdaLog": { + "import": "./dist/esm/LambdaLog.js", + "require": "./dist/cjs/LambdaLog.js" + }, + "./LogMessage": { + "import": "./dist/esm/LogMessage.js", + "require": "./dist/cjs/LogMessage.js" + } + }, "scripts": { "test": "jest", "prepare": "is-ci || husky install", From 726a209040eb6aac5e64e7155ba40b1fedd8249b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:14:03 -0500 Subject: [PATCH 05/83] feat: change minimum support to node v12+ --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index ab16741..af793c5 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { "name": "lambda-log", "version": "3.1.0", - "description": "Lightweight logging library for any Node 10+ applications", - "main": "index.js", + "description": "Lightweight logging library for any Node 12+ applications", "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", "exports": { @@ -64,7 +63,7 @@ }, "homepage": "https://lambdalog.dev", "engines": { - "node": ">=10.0.0" + "node": ">=12" }, "devDependencies": { "@commitlint/cli": "^13.2.1", From 9ffcf9af7ce676c5342d61a5fbcb3ccc107b2798 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:14:50 -0500 Subject: [PATCH 06/83] chore: update jest to add esm support and watch command --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index af793c5..d6e20ce 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ } }, "scripts": { - "test": "jest", + "test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest", + "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest --watchAll", "prepare": "is-ci || husky install", "build": "npm run clean && npm run build:esm && npm run build:cjs", "build:esm": "tsc --module es6 --outDir dist/esm --moduleResolution node", @@ -73,6 +74,7 @@ "@types/jest": "^27.0.2", "@typescript-eslint/eslint-plugin": "^5.1.0", "@typescript-eslint/parser": "^5.1.0", + "cross-env": "^7.0.3", "eslint": "^7.23.0", "eslint-config-xo-space": "^0.30.0", "eslint-config-xo-typescript": "^0.45.2", From 1fb42dd40c7694f98aa4fd6749be951b92938a72 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:15:24 -0500 Subject: [PATCH 07/83] chore: update jest config to support typescript --- jest.config.js | 10 ---------- jest.config.ts | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) delete mode 100644 jest.config.js create mode 100644 jest.config.ts diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 0a96c75..0000000 --- a/jest.config.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - clearMocks: true, - collectCoverage: true, - coverageDirectory: 'coverage', - coverageProvider: 'v8', - moduleFileExtensions: [ - 'js' - ], - testEnvironment: 'node' -}; diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..cab010b --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,17 @@ +import type { Config } from '@jest/types'; + +const config: Config.InitialOptions = { + testEnvironment: 'node', + testRegex: '\\.test\\.ts$', + collectCoverage: true, + collectCoverageFrom: ['src/**/*.ts'], + coverageDirectory: 'coverage', + coverageProvider: 'v8', + coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/src/typings.ts'], + transform: { + '^.+\\.ts$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'js', 'json', 'node'] +}; + +export default config; From f518ec1bc653c7fba4f58fd51451b331dac43c10 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:17:37 -0500 Subject: [PATCH 08/83] chore: add support for pre-release branches in module workflow --- .github/workflows/module.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index 95be265..f15393c 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -3,6 +3,9 @@ on: push: branches: - master + - next + - beta + - alpha pull_request: branches: - master From 00afd02a22cd75a3fc86d4e5c3742e346266a763 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:18:08 -0500 Subject: [PATCH 09/83] chore: update tests to run on node 12, 14, and 16 --- .github/workflows/module.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index f15393c..f47bf00 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -14,7 +14,7 @@ jobs: test: strategy: matrix: - node: [ '10', '12', '14', '16' ] + node: [ '12', '14', '16' ] name: test/node v${{ matrix.node }} runs-on: ubuntu-latest steps: From ead55076da8e757cc9f412a87f435a2e2e18b3e1 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:18:26 -0500 Subject: [PATCH 10/83] chore: only run code coverage on master branch --- .github/workflows/module.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index f47bf00..6cb5bc9 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -53,7 +53,7 @@ jobs: threshold: 100 - name: Upload Coverage to Code Climate uses: paambaati/codeclimate-action@v2.7.2 - if: ${{ github.event_name == 'push' }} + if: ${{ github.event_name == 'push' && github.branch == 'master' }} env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} with: From 9110c865ab57086480d19ede0167092543e47d34 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:18:47 -0500 Subject: [PATCH 11/83] chore: update eslint glob in workflow --- .github/workflows/module.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index 6cb5bc9..6233671 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -82,7 +82,7 @@ jobs: dry_run: "false" - uses: bradennapier/eslint-plus-action@v3.4.2 with: - includeGlob: 'index.js,index.test.js,lib/*.js' + includeGlob: 'src/*.ts' release: name: Release From 8c16c07317f7c9c94f8f11446f07d7dc78745e1d Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:19:07 -0500 Subject: [PATCH 12/83] chore: build the package prior to release in workflow --- .github/workflows/module.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index 6233671..ba5204a 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -95,9 +95,11 @@ jobs: - name: Setup Node uses: actions/setup-node@master with: - node-version: 14 + node-version: 16 - name: Install Dependencies run: npm i + - name: Build Package + run: npm run build - name: Publish to NPM env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4ce3053e20bfc54a6ca54daa8ae19bc33e57df99 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:19:27 -0500 Subject: [PATCH 13/83] chore: add additional files/folders to npmignore --- .npmignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.npmignore b/.npmignore index 8faba7d..b83b8f2 100644 --- a/.npmignore +++ b/.npmignore @@ -4,8 +4,10 @@ gh-pages site .eslintrc.yml -.releaserc +release.config.js commitlint.config.js +tsconfig.json +tsconfig.eslint.json *.test.js jest.config.js .all-contributorsrc @@ -13,3 +15,4 @@ jest.config.js babel.config.js CHANGELOG.md README.md +src From df7b2719825366fe5c1fdd697abcae786a034be5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:19:50 -0500 Subject: [PATCH 14/83] chore: update dependencies --- package.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index d6e20ce..57968e0 100644 --- a/package.json +++ b/package.json @@ -67,22 +67,22 @@ "node": ">=12" }, "devDependencies": { - "@commitlint/cli": "^13.2.1", - "@commitlint/config-conventional": "^13.2.0", - "@semantic-release/changelog": "^6.0.0", - "@semantic-release/git": "^10.0.0", + "@commitlint/cli": "^14.1.0", + "@commitlint/config-conventional": "^14.1.0", + "@semantic-release/changelog": "^6.0.1", + "@semantic-release/git": "^10.0.1", "@types/jest": "^27.0.2", - "@typescript-eslint/eslint-plugin": "^5.1.0", - "@typescript-eslint/parser": "^5.1.0", + "@typescript-eslint/eslint-plugin": "^5.4.0", + "@typescript-eslint/parser": "^5.4.0", "cross-env": "^7.0.3", "eslint": "^7.23.0", "eslint-config-xo-space": "^0.30.0", "eslint-config-xo-typescript": "^0.45.2", - "eslint-plugin-jsdoc": "^36.1.1", + "eslint-plugin-jsdoc": "^37.0.3", "eslint-plugin-node": "^11.1.0", - "husky": "^7.0.2", - "is-ci": "^3.0.0", - "jest": "^27.2.5", + "husky": "^7.0.4", + "is-ci": "^3.0.1", + "jest": "^27.3.1", "rimraf": "^3.0.2", "semantic-release": "^18.0.0", "ts-jest": "^27.0.7", From 764cd62147d290ec1799b7f44ddce3c082fe359a Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:20:13 -0500 Subject: [PATCH 15/83] docs: update readme for prerelease --- README.md | 102 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 1b35847..d6c4a9d 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,80 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-10-orange.svg?style=flat-square)](#contributors-) +## CAUTION! + +This branch and release (`@next`) is under heavy development. This exists for testing and anyone who is willing to help out with testing. **DO NOT USE IN PRODUCTION!** + +If you do decide to try out this brand new version of LambdaLog, please report any issues you face as I can only catch so much. Any help would be greatly appreciated, especially if you are more versed in TypeScript than I am! + + + +### What's new? + +This is the next major release of LambdaLog (v4.0.0) which contains many major breaking changes, new features, and TypeScript support. + +#### Breaking Changes + +- Complete rewrite of most of the core functionality into TypeScript. +- Custom Log Levels has been removed. +- Log levels are now hardcoded and cannot be changed. This greatly simplifies the code and allows us to support TypeScript. +- Use of `Symbol` has been removed along with any static properties on the classes to retrieve the symbols. +- The `debug` option has been removed. +- The default key name for `levelKey` has changed from `_logLevel` to `__level`. +- The default key name for `tagsKey` has changed from `_tags` to `__tags`. +- The shortcut log methods are now hardcoded instead of dynamically generated. +- Removed `addLevel()` method from LambdaLog class. +- Removed `isError()` and `stubError()` static methods from the LogMessage class and moved them into utility functions. +- Environment variables that configure different options now take precedence over locally configured options. +- Internally used properties on the LogMessage class have been renamed. + +#### New Features + +- You can now configure the verbosity of your log messages through the new `level` option or `LAMBDALOG_LEVEL` env variable. +- Added `fatal` and `trace` log levels. +- Added `onParse` option that allows for custom parsing of log messages. +- Added `onCompile` option that allows for custom structuring of the object that is logged to the console. +- Added `onFormat` option that allows for custom formatting of the logged message. This gives the ability to render log messages in any format, not just JSON. (#75) + - Added `clean` formatter that logs a message in a readable format. + - Added `minimal` formatter that logs a message in a readable format without tags and metadata. + - Ability to provide your own custom formatter as a function. +- Added ability to configure the `level`, `dev`, and `silent` options via environment variables. +- Full written in TypeScript with complete type definitions. (#63, #64) +- The package now provides an ESM and CommonJS version to support your project. + +#### Fixes + +- Fixed issue where directly setting `log.options.logHandler` did not work (#45). + +#### Misc. Changes + +- Rewrote most tests into TypeScript and still maintaining 100% coverage. + + + +### What's left? + +It has been a pretty large undertaking for me, but one that this package desperately needed. This was my first real dive into TypeScript, so I'm sure there are things than can be written better. **If there is anyone with more advanced TypeScript knowledge, I'm in great need of someone who can review the code and suggest any changes or help refactor the code to get this production-ready.** _(I'll even add you as a maintainer!)_ + +- Testing package implementation in Node for both vanilla JavaScript and TypeScript. +- Testing ESM support with Node and ensuring CommonJS works correctly as well. +- Ensuring the end-user has access to all the proper files and functionality. +- Documentation. + + + +### Release Date + +As much as I am trying to rush this out the door to get these features in the hands of all you awesome developers/engineers, I'm also limited on time (eg. I'm currently writing this documentation at midnight knowing that I have to wake up at 5:30am). Not only that, but I want to ensure I ship this next version of LambdaLog with performance, features, and stability in mind. I hope this will be the last major version for awhile. + +Enough with all of that cheesy sob-story, let's talk about when this will be released, well, what I'm aiming for at this point. I hope to finish up most of my to-do list in the next couple of weeks and try to solicit some outside help for testing/analysis. Once that is complete, I hope to get this released as the latest stable version towards the end of 2021 or at the beginning of 2022. That's not guarantee at this point, but it will definitely be feasible if anyone in the community can help out! + +--- + LambdaLog is a [Node.js package](https://www.npmjs.com/package/lambda-log) facilitates and enforces logging standards in Node.js processes or applications **anywhere** by formatting your log messages as JSON for simple parsing and filtering within log management tools, such as CloudWatch Logs. _Works with all of the supported versions of Node.js on Lambda._ Originally created for AWS Lambda Functions, LambdaLog is a lightweight and feature-rich library that has **no** dependency on AWS or Lambda, meaning you can use it in any type of Node.js project you wish. **It's really a universal JSON logger.** - **Why another lambda logger?** There are plenty of other logging libraries in the NPM ecosystem but most are convoluted, included more functionality than needed, not maintained, or are not configurable enough. I created LambdaLog to include the important functionality from other loggers, but still maintaining simplicity with minimal dependencies. @@ -26,41 +95,14 @@ Anyone can log JSON to the `console`, but with Lambda Log you also get: - Over 1.5 million downloads and more than 35k weekly downloads. - Small footprint. -#### New in Version 3.0.0 - -Version 3.0.0 of Lambda Log brings a bunch of changes, new features, and a [new website](https://lambdalog.dev). - -**Broad Changes:** - -* Refactor all code to meet new ESLint specifications and to stay up-to-date with newer ecmascript specifications. -* New website with better documentation. -* Tests are now using Jest instead of Mocha. -* Switched from TravisCI to Github Actions. - -**New Features:** - -- Added `levelKey` configuration option to be able to change the key name for log levels. -- Added `messageKey` configuration option to be able to change the key name for log messages. -- Added `tagsKey` configuration option to be able to change the key name for tags. -- Added ability to remove log level and tags from the outputted log JSON. -- Added `addLevel()` method to quickly add a custom log level to an instance of LambdaLog. -- Tags can now be functions that return a dynamic tag for log messages. -- Tags now have variable support. -- Tags that are `null`, `undefined` or `""` are now removed from the tags array. -- Metadata that contains `Error` objects are now automatically converted to a plain object. - -**Breaking Changes:** - -- All of the private properties of both the LambdaLog and LogMessage classes are stored using Symbols. This may break some advanced uses of Lambda Log from version 2. -- Tags no longer contain any default, built-in tags and are empty by default. -- Some of the properties of LogMessage have been moved from the constructor to their own getter functions. - ## Documentation Documentation for Lambda Log has moved to our [new website](https://lambdalog.dev). +*Note: The documentation has NOT been updated for this new release yet. I'm working on it!* + ## Tests From 23e3efd3abaccf99d02cf8563b67affb25854293 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 17 Nov 2021 05:23:45 +0000 Subject: [PATCH 16/83] chore(release): 3.2.0 [skip ci] # [3.2.0](https://github.com/KyleRoss/node-lambda-log/compare/v3.1.0...v3.2.0) (2021-11-17) ### Features * change minimum support to node v12+ ([726a209](https://github.com/KyleRoss/node-lambda-log/commit/726a209040eb6aac5e64e7155ba40b1fedd8249b)) * rewrite into typescript ([2800655](https://github.com/KyleRoss/node-lambda-log/commit/280065579846369ac241142f6c1dd71196af9b56)) * support esm and cjs ([0e8f0ac](https://github.com/KyleRoss/node-lambda-log/commit/0e8f0ac907be771290627d3aac25b507b955c529)) --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce465f..1021bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [3.2.0](https://github.com/KyleRoss/node-lambda-log/compare/v3.1.0...v3.2.0) (2021-11-17) + + +### Features + +* change minimum support to node v12+ ([726a209](https://github.com/KyleRoss/node-lambda-log/commit/726a209040eb6aac5e64e7155ba40b1fedd8249b)) +* rewrite into typescript ([2800655](https://github.com/KyleRoss/node-lambda-log/commit/280065579846369ac241142f6c1dd71196af9b56)) +* support esm and cjs ([0e8f0ac](https://github.com/KyleRoss/node-lambda-log/commit/0e8f0ac907be771290627d3aac25b507b955c529)) + # [3.1.0](https://github.com/KyleRoss/node-lambda-log/compare/v3.0.2...v3.1.0) (2021-10-14) diff --git a/package.json b/package.json index 57968e0..961ad99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-log", - "version": "3.1.0", + "version": "3.2.0", "description": "Lightweight logging library for any Node 12+ applications", "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", From fe34fe6cb26f05c966d5879c6a87baa1681d0c57 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 00:37:56 -0500 Subject: [PATCH 17/83] feat: 4.0.0-beta BREAKING CHANGE: This is the beta version for the next major release of LambdaLog. Do not use in production yet! Please report any issues you may find! --- CHANGELOG.md | 9 --------- commitlint.config.js | 3 ++- package.json | 2 +- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1021bdb..dce465f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,3 @@ -# [3.2.0](https://github.com/KyleRoss/node-lambda-log/compare/v3.1.0...v3.2.0) (2021-11-17) - - -### Features - -* change minimum support to node v12+ ([726a209](https://github.com/KyleRoss/node-lambda-log/commit/726a209040eb6aac5e64e7155ba40b1fedd8249b)) -* rewrite into typescript ([2800655](https://github.com/KyleRoss/node-lambda-log/commit/280065579846369ac241142f6c1dd71196af9b56)) -* support esm and cjs ([0e8f0ac](https://github.com/KyleRoss/node-lambda-log/commit/0e8f0ac907be771290627d3aac25b507b955c529)) - # [3.1.0](https://github.com/KyleRoss/node-lambda-log/compare/v3.0.2...v3.1.0) (2021-10-14) diff --git a/commitlint.config.js b/commitlint.config.js index 8fb8ba0..43580ac 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,6 +1,7 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { - 'body-max-line-length': [0] + 'body-max-line-length': [0], + 'footer-max-line-length': [0] } }; diff --git a/package.json b/package.json index 961ad99..57968e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-log", - "version": "3.2.0", + "version": "3.1.0", "description": "Lightweight logging library for any Node 12+ applications", "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", From 461ca47d2d926603b44ecac58d8be7444879f243 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 17 Nov 2021 05:41:24 +0000 Subject: [PATCH 18/83] chore(release): 4.0.0-beta.1 [skip ci] # [4.0.0-beta.1](https://github.com/KyleRoss/node-lambda-log/compare/v3.2.0...v4.0.0-beta.1) (2021-11-17) ### Features * 4.0.0-beta ([fe34fe6](https://github.com/KyleRoss/node-lambda-log/commit/fe34fe6cb26f05c966d5879c6a87baa1681d0c57)) ### BREAKING CHANGES * This is the beta version for the next major release of LambdaLog. Do not use in production yet! Please report any issues you may find! --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dce465f..8391648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [4.0.0-beta.1](https://github.com/KyleRoss/node-lambda-log/compare/v3.2.0...v4.0.0-beta.1) (2021-11-17) + + +### Features + +* 4.0.0-beta ([fe34fe6](https://github.com/KyleRoss/node-lambda-log/commit/fe34fe6cb26f05c966d5879c6a87baa1681d0c57)) + + +### BREAKING CHANGES + +* This is the beta version for the next major release of LambdaLog. Do not use in production yet! Please report any issues you may find! + # [3.1.0](https://github.com/KyleRoss/node-lambda-log/compare/v3.0.2...v3.1.0) (2021-10-14) diff --git a/package.json b/package.json index 57968e0..bbf361f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-log", - "version": "3.1.0", + "version": "4.0.0-beta.1", "description": "Lightweight logging library for any Node 12+ applications", "main": "dist/cjs/index.js", "types": "dist/cjs/index.d.ts", From da55f8d89035a194d2fdf07536b843a33e69daf7 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 14:48:20 -0500 Subject: [PATCH 19/83] chore: do not run workflow when there are only changes to site --- .github/workflows/module.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index ba5204a..da6608c 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -6,9 +6,13 @@ on: - next - beta - alpha + paths-ignore: + - 'site/**' pull_request: branches: - master + paths-ignore: + - 'site/**' jobs: test: From 15751615bc8c2ae47008a2754a718c345aaeeb54 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 14:52:38 -0500 Subject: [PATCH 20/83] chore: simplify code coverage workflow --- .github/workflows/module.yml | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index da6608c..c75e63d 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -32,32 +32,9 @@ jobs: run: npm i - name: Run Tests run: npm test - - coverage: - name: Code Coverage - needs: [ test ] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@master - - name: Setup Node - uses: actions/setup-node@master - with: - node-version: 14 - - name: Install Dependencies - run: npm i - - name: Run Tests - if: ${{ github.event_name == 'pull_request' }} - run: npm test - - name: Comment Coverage on PR - if: ${{ github.event_name == 'pull_request' }} - uses: artiomtr/jest-coverage-report-action@fdabb5bd42fa8a55bcfdfb55d855122cabfb7911 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - threshold: 100 - name: Upload Coverage to Code Climate - uses: paambaati/codeclimate-action@v2.7.2 - if: ${{ github.event_name == 'push' && github.branch == 'master' }} + uses: paambaati/codeclimate-action@v3.0.0 + if: ${{ matrix.node == '16' }} env: CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} with: @@ -75,7 +52,7 @@ jobs: - name: Setup Node uses: actions/setup-node@master with: - node-version: 14 + node-version: 16 - name: Install Dependencies run: npm i - name: Delete expired artifacts From 48602b9124b6c74bfe81a87ca13defca21f0c2e9 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 14:53:00 -0500 Subject: [PATCH 21/83] chore: only trigger workflow when a file in `gh_pages` is changed --- .github/workflows/site.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 379f65c..2aa195e 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -5,6 +5,8 @@ on: - master tags: - '!*' + paths: + - 'gh-pages/**' jobs: build: From 77e8000c04214092d33409b0926b8d7832c5b5f5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 14:53:32 -0500 Subject: [PATCH 22/83] chore: update jest settings --- jest.config.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index cab010b..81a8613 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,14 +4,15 @@ const config: Config.InitialOptions = { testEnvironment: 'node', testRegex: '\\.test\\.ts$', collectCoverage: true, + coverageReporters: [['json', { file: 'report.json' }], 'lcov', 'text', 'text-summary', 'html'], collectCoverageFrom: ['src/**/*.ts'], coverageDirectory: 'coverage', - coverageProvider: 'v8', - coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/src/typings.ts'], + coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/src/typings.ts'], transform: { '^.+\\.ts$': 'ts-jest' }, - moduleFileExtensions: ['ts', 'js', 'json', 'node'] + moduleFileExtensions: ['ts', 'js', 'json', 'node'], + watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/site/'] }; export default config; From e58a1686e7728c45b15d7d097956aabd92ae20b4 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 14:53:49 -0500 Subject: [PATCH 23/83] chore: add ignore patterns to eslint config --- .eslintrc.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.eslintrc.yml b/.eslintrc.yml index 1870e36..0da1c48 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,6 +1,11 @@ --- root: true +ignorePatterns: + - 'node_modules/**/*' + - 'dist' + - 'site' + env: node: true es6: true From 1d91fe603ff1e7b79c52035bb95ceabb5f911975 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 14:58:17 -0500 Subject: [PATCH 24/83] chore: update codeclimate settings --- .codeclimate.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 34d70ca..38f0336 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -11,7 +11,7 @@ checks: file-lines: enabled: true config: - threshold: 350 + threshold: 500 method-complexity: enabled: true config: @@ -50,4 +50,5 @@ exclude_patterns: - "**/node_modules/" - "site/" - "**/*.d.ts" + - "**/*.test.ts" - "**/*.test.js" From db5a2d7c1ee136094203a5b54d370b17171657a6 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Wed, 17 Nov 2021 15:00:41 -0500 Subject: [PATCH 25/83] chore: remove coverage job from `needs` --- .github/workflows/module.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml index c75e63d..8d27d6b 100644 --- a/.github/workflows/module.yml +++ b/.github/workflows/module.yml @@ -68,7 +68,7 @@ jobs: release: name: Release if: ${{ github.event_name == 'push' }} - needs: [ test, coverage ] + needs: [ test ] runs-on: ubuntu-latest steps: - name: Checkout From 551e43d1b7a3aaceacd60c1e325bb0f39c274dbc Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:21:45 -0500 Subject: [PATCH 26/83] chore: disable declaration in tsconfig --- tsconfig.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index cdd6746..05a6d96 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,14 +4,15 @@ ], "compilerOptions": { "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"], - "module": "commonjs", "target": "es2019", + "module": "commonjs", + "moduleResolution": "node", "strict": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "suppressImplicitAnyIndexErrors": true, - "declaration": true, - "outDir": "dist" + "declaration": false, + "outDir": "./dist" } } From 2fcf7707528c7a07c7d0e02ab4e1ecf1b699363d Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:22:49 -0500 Subject: [PATCH 27/83] fix: add `.js` file extensions on imports to support node ESM --- src/LambdaLog.ts | 6 +++--- src/LogMessage.ts | 4 ++-- src/index.ts | 10 ++-------- src/typings.ts | 2 +- src/utils.ts | 2 +- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index 9e2ceeb..0e51788 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -1,7 +1,7 @@ import EventEmitter from 'events'; -import { LambdaLogOptions, Message, GenericRecord, LogLevels, LogObject, Tag, ConsoleObject } from './typings'; -import LogMessage from './LogMessage'; -import { toBool } from './utils'; +import { LambdaLogOptions, Message, GenericRecord, LogLevels, LogObject, Tag, ConsoleObject } from './typings.js'; +import LogMessage from './LogMessage.js'; +import { toBool } from './utils.js'; const levels = [{ diff --git a/src/LogMessage.ts b/src/LogMessage.ts index be7e450..8f3c6f9 100644 --- a/src/LogMessage.ts +++ b/src/LogMessage.ts @@ -1,6 +1,6 @@ import stringify from 'fast-safe-stringify'; -import { LambdaLogOptions, Message, LogObject, Tag, GenericRecord, Formatter, StubbedError, Empty } from './typings'; -import { isError, stubError } from './utils'; +import { LambdaLogOptions, Message, LogObject, Tag, GenericRecord, Formatter, StubbedError, Empty } from './typings.js'; +import { isError, stubError } from './utils.js'; export interface ILogMessage { readonly __opts: LambdaLogOptions; diff --git a/src/index.ts b/src/index.ts index 13f1595..0a02b9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,5 @@ -import LambdaLog from './LambdaLog'; -import LogMessage from './LogMessage'; +import LambdaLog from './LambdaLog.js'; +import LogMessage from './LogMessage.js'; -/** - * Instance of the LambdaLog class which is exported when calling `require('lambda-log')`. For more - * advanced usage, you can create a new instance of the LambdaLog class via `new log.LambdaLog()`. - * @type {LambdaLog} - */ export default new LambdaLog(); export { LambdaLog, LogMessage }; -export * from './typings'; diff --git a/src/typings.ts b/src/typings.ts index d1fa587..fe3726b 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -1,4 +1,4 @@ -import LogMessage from './LogMessage'; +import LogMessage from './LogMessage.js'; import stringify from 'fast-safe-stringify'; export type GenericRecord = Record; diff --git a/src/utils.ts b/src/utils.ts index 684676f..7d7a64b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import { GenericRecord, StubbedError } from './typings'; +import { GenericRecord, StubbedError } from './typings.js'; /** * Checks if value is an Error or Error-like object From 62da1ec8d119bbcdee783db75545252e90755310 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:25:18 -0500 Subject: [PATCH 28/83] fix: add root index file to alleviate the need to call `.default` in cjs --- index.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 index.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..5101830 --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +const main = require('./dist/cjs'); +module.exports = main.default; From 8b18e195b84e151d56b6db3666bd18377a285648 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:25:51 -0500 Subject: [PATCH 29/83] chore: update to the latest typescript version --- .vscode/settings.json | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d4e99cf..a67b0ae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,5 +23,6 @@ ], "[mdx]": { "editor.wordWrap": "on" - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/package.json b/package.json index bbf361f..693ab00 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "rimraf": "^3.0.2", "semantic-release": "^18.0.0", "ts-jest": "^27.0.7", - "typescript": "^4.4.4" + "typescript": "^4.5.2" }, "dependencies": { "fast-safe-stringify": "^2.1.1" From 54cfce0255ae025a7a4d97b66e5eb1ada190f8bf Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:26:39 -0500 Subject: [PATCH 30/83] chore: add jest-ts-webcompat-resolver to fix issues resolving `.js` files in jest --- jest.config.ts | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/jest.config.ts b/jest.config.ts index 81a8613..3e3a1b8 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,6 +3,7 @@ import type { Config } from '@jest/types'; const config: Config.InitialOptions = { testEnvironment: 'node', testRegex: '\\.test\\.ts$', + resolver: 'jest-ts-webcompat-resolver', collectCoverage: true, coverageReporters: [['json', { file: 'report.json' }], 'lcov', 'text', 'text-summary', 'html'], collectCoverageFrom: ['src/**/*.ts'], diff --git a/package.json b/package.json index 693ab00..fc4ef44 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "husky": "^7.0.4", "is-ci": "^3.0.1", "jest": "^27.3.1", + "jest-ts-webcompat-resolver": "^1.0.0", "rimraf": "^3.0.2", "semantic-release": "^18.0.0", "ts-jest": "^27.0.7", From d268e91f8d2f4e26e1ddf0d798635a87c13c6a7d Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:27:01 -0500 Subject: [PATCH 31/83] fix: add `src` as a module directory to jest --- jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/jest.config.ts b/jest.config.ts index 3e3a1b8..bc68548 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -13,6 +13,7 @@ const config: Config.InitialOptions = { '^.+\\.ts$': 'ts-jest' }, moduleFileExtensions: ['ts', 'js', 'json', 'node'], + moduleDirectories: ['node_modules', 'src'], watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/site/'] }; From 1c6376c027014473e31123cdd39d61c260e36bad Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:27:34 -0500 Subject: [PATCH 32/83] fix: ignore jest.config.ts when publishing to npm --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index b83b8f2..2fd8803 100644 --- a/.npmignore +++ b/.npmignore @@ -10,6 +10,7 @@ tsconfig.json tsconfig.eslint.json *.test.js jest.config.js +jest.config.ts .all-contributorsrc .codeclimate.yml babel.config.js From 39a9f437637a3ec3443fef79e79cd93d641d876f Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:28:33 -0500 Subject: [PATCH 33/83] fix: update `main` to point to root index.js file for cjs usage --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fc4ef44..1ed23fd 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,11 @@ "name": "lambda-log", "version": "4.0.0-beta.1", "description": "Lightweight logging library for any Node 12+ applications", - "main": "dist/cjs/index.js", + "main": "index.js", "types": "dist/cjs/index.d.ts", "exports": { ".": { "import": "./dist/esm/index.js", - "require": "./dist/cjs/index.js" }, "./LambdaLog": { "import": "./dist/esm/LambdaLog.js", @@ -16,6 +15,7 @@ "./LogMessage": { "import": "./dist/esm/LogMessage.js", "require": "./dist/cjs/LogMessage.js" + "require": "./index.js" } }, "scripts": { From 1323bf4866ba14fb1c86b6daae4e0daad2bcfcba Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:29:02 -0500 Subject: [PATCH 34/83] fix: add `module` field to point to esm --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1ed23fd..3c17cf4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "4.0.0-beta.1", "description": "Lightweight logging library for any Node 12+ applications", "main": "index.js", + "module": "dist/esm/index.js", "types": "dist/cjs/index.d.ts", "exports": { ".": { From 23453920e736d329618365a4593d922a15a89c86 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:29:33 -0500 Subject: [PATCH 35/83] chore: remove extra "exports" from package.json that aren't needed --- package.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package.json b/package.json index 3c17cf4..9d7cdba 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,6 @@ "exports": { ".": { "import": "./dist/esm/index.js", - }, - "./LambdaLog": { - "import": "./dist/esm/LambdaLog.js", - "require": "./dist/cjs/LambdaLog.js" - }, - "./LogMessage": { - "import": "./dist/esm/LogMessage.js", - "require": "./dist/cjs/LogMessage.js" "require": "./index.js" } }, From c65fd52cbcf6cf5d08dc6cde4f221f70dd4e92a3 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:31:43 -0500 Subject: [PATCH 36/83] fix: point `types` to separate declaration file --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d7cdba..c2cecf5 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Lightweight logging library for any Node 12+ applications", "main": "index.js", "module": "dist/esm/index.js", - "types": "dist/cjs/index.d.ts", + "types": "dist/types/index.d.ts", "exports": { ".": { "import": "./dist/esm/index.js", From 0d48d63d6456b2976cf938bf65f0c0aa60bccb5c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:32:13 -0500 Subject: [PATCH 37/83] chore: cleanup build scripts --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c2cecf5..f9a60cb 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest --watchAll", "prepare": "is-ci || husky install", "build": "npm run clean && npm run build:esm && npm run build:cjs", - "build:esm": "tsc --module es6 --outDir dist/esm --moduleResolution node", - "build:cjs": "tsc --module commonjs --outDir dist/cjs --moduleResolution node", + "build:esm": "tsc --module ES2020 --outDir dist/esm", + "build:cjs": "tsc --module commonjs --outDir dist/cjs", "lint": "eslint \"src/*.ts\"", "clean": "rimraf dist" }, From c43deef9aa2080637055e48a93851f5fc537bc20 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:32:41 -0500 Subject: [PATCH 38/83] fix: add build:declaration script and run it during build --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f9a60cb..211a514 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,10 @@ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest", "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest --watchAll", "prepare": "is-ci || husky install", - "build": "npm run clean && npm run build:esm && npm run build:cjs", + "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:declaration", "build:esm": "tsc --module ES2020 --outDir dist/esm", "build:cjs": "tsc --module commonjs --outDir dist/cjs", + "build:declaration": "tsc --module system --declaration --emitDeclarationOnly --outFile dist/types/index.d.ts", "lint": "eslint \"src/*.ts\"", "clean": "rimraf dist" }, From 433cb70287352dd9e16b9b51f3ce5b31c4f8f098 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:33:05 -0500 Subject: [PATCH 39/83] fix: add `ts-node` as a dev dependency for jest --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 211a514..620a043 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "rimraf": "^3.0.2", "semantic-release": "^18.0.0", "ts-jest": "^27.0.7", + "ts-node": "^10.4.0", "typescript": "^4.5.2" }, "dependencies": { From 64950843a0edf7cc5c41d19fa8e2a2b6ce704187 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 18 Nov 2021 11:33:19 -0500 Subject: [PATCH 40/83] chore: upgrade deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 620a043..1b9b443 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,8 @@ "node": ">=12" }, "devDependencies": { - "@commitlint/cli": "^14.1.0", - "@commitlint/config-conventional": "^14.1.0", + "@commitlint/cli": "^15.0.0", + "@commitlint/config-conventional": "^15.0.0", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", "@types/jest": "^27.0.2", From 4f15d05544b9b70fa3e04a0cb6f8c6ab0a5040b6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 18 Nov 2021 16:41:33 +0000 Subject: [PATCH 41/83] chore(release): 4.0.0-beta.2 [skip ci] # [4.0.0-beta.2](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.1...v4.0.0-beta.2) (2021-11-18) ### Bug Fixes * add `.js` file extensions on imports to support node ESM ([2fcf770](https://github.com/KyleRoss/node-lambda-log/commit/2fcf7707528c7a07c7d0e02ab4e1ecf1b699363d)) * add `module` field to point to esm ([1323bf4](https://github.com/KyleRoss/node-lambda-log/commit/1323bf4866ba14fb1c86b6daae4e0daad2bcfcba)) * add `src` as a module directory to jest ([d268e91](https://github.com/KyleRoss/node-lambda-log/commit/d268e91f8d2f4e26e1ddf0d798635a87c13c6a7d)) * add `ts-node` as a dev dependency for jest ([433cb70](https://github.com/KyleRoss/node-lambda-log/commit/433cb70287352dd9e16b9b51f3ce5b31c4f8f098)) * add build:declaration script and run it during build ([c43deef](https://github.com/KyleRoss/node-lambda-log/commit/c43deef9aa2080637055e48a93851f5fc537bc20)) * add root index file to alleviate the need to call `.default` in cjs ([62da1ec](https://github.com/KyleRoss/node-lambda-log/commit/62da1ec8d119bbcdee783db75545252e90755310)) * ignore jest.config.ts when publishing to npm ([1c6376c](https://github.com/KyleRoss/node-lambda-log/commit/1c6376c027014473e31123cdd39d61c260e36bad)) * point `types` to separate declaration file ([c65fd52](https://github.com/KyleRoss/node-lambda-log/commit/c65fd52cbcf6cf5d08dc6cde4f221f70dd4e92a3)) * update `main` to point to root index.js file for cjs usage ([39a9f43](https://github.com/KyleRoss/node-lambda-log/commit/39a9f437637a3ec3443fef79e79cd93d641d876f)) --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8391648..7af9454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# [4.0.0-beta.2](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.1...v4.0.0-beta.2) (2021-11-18) + + +### Bug Fixes + +* add `.js` file extensions on imports to support node ESM ([2fcf770](https://github.com/KyleRoss/node-lambda-log/commit/2fcf7707528c7a07c7d0e02ab4e1ecf1b699363d)) +* add `module` field to point to esm ([1323bf4](https://github.com/KyleRoss/node-lambda-log/commit/1323bf4866ba14fb1c86b6daae4e0daad2bcfcba)) +* add `src` as a module directory to jest ([d268e91](https://github.com/KyleRoss/node-lambda-log/commit/d268e91f8d2f4e26e1ddf0d798635a87c13c6a7d)) +* add `ts-node` as a dev dependency for jest ([433cb70](https://github.com/KyleRoss/node-lambda-log/commit/433cb70287352dd9e16b9b51f3ce5b31c4f8f098)) +* add build:declaration script and run it during build ([c43deef](https://github.com/KyleRoss/node-lambda-log/commit/c43deef9aa2080637055e48a93851f5fc537bc20)) +* add root index file to alleviate the need to call `.default` in cjs ([62da1ec](https://github.com/KyleRoss/node-lambda-log/commit/62da1ec8d119bbcdee783db75545252e90755310)) +* ignore jest.config.ts when publishing to npm ([1c6376c](https://github.com/KyleRoss/node-lambda-log/commit/1c6376c027014473e31123cdd39d61c260e36bad)) +* point `types` to separate declaration file ([c65fd52](https://github.com/KyleRoss/node-lambda-log/commit/c65fd52cbcf6cf5d08dc6cde4f221f70dd4e92a3)) +* update `main` to point to root index.js file for cjs usage ([39a9f43](https://github.com/KyleRoss/node-lambda-log/commit/39a9f437637a3ec3443fef79e79cd93d641d876f)) + # [4.0.0-beta.1](https://github.com/KyleRoss/node-lambda-log/compare/v3.2.0...v4.0.0-beta.1) (2021-11-17) diff --git a/package.json b/package.json index 1b9b443..e0f0686 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-log", - "version": "4.0.0-beta.1", + "version": "4.0.0-beta.2", "description": "Lightweight logging library for any Node 12+ applications", "main": "index.js", "module": "dist/esm/index.js", From bb7bd7e658e6d7dc3cfb2c6afb20bf888a4506e2 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 08:16:29 -0500 Subject: [PATCH 42/83] chore: add types for node --- package.json | 1 + tsconfig.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index 1b9b443..2500c73 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "typescript": "^4.5.2" }, "dependencies": { + "@types/node": "^16.11.9", "fast-safe-stringify": "^2.1.1" } } diff --git a/tsconfig.json b/tsconfig.json index 05a6d96..5c187ad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "target": "es2019", "module": "commonjs", "moduleResolution": "node", + "types": ["node"], "strict": true, "esModuleInterop": true, From 467c9e42628ed05a6ba28bb2cea79f4129d6a2f5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:23:31 -0500 Subject: [PATCH 43/83] fix: rename index to `lambda-log` to ensure the types are generated properly --- src/{index.test.ts => lambda-log.spec.ts} | 2 +- src/lambda-log.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) rename src/{index.test.ts => lambda-log.spec.ts} (93%) create mode 100644 src/lambda-log.ts diff --git a/src/index.test.ts b/src/lambda-log.spec.ts similarity index 93% rename from src/index.test.ts rename to src/lambda-log.spec.ts index 435b7cc..5fcb9ab 100644 --- a/src/index.test.ts +++ b/src/lambda-log.spec.ts @@ -1,4 +1,4 @@ -import log from '.'; +import log from './lambda-log'; import LambdaLog from './LambdaLog'; import LogMessage from './LogMessage'; diff --git a/src/lambda-log.ts b/src/lambda-log.ts new file mode 100644 index 0000000..6fa05c5 --- /dev/null +++ b/src/lambda-log.ts @@ -0,0 +1,6 @@ +import LambdaLog from './LambdaLog.js'; +import LogMessage from './LogMessage.js'; +import * as Types from './typings.js'; + +export default new LambdaLog(); +export { LambdaLog, LogMessage, Types }; From d7861de9269532ab31631e559ec8da7a77c2870a Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:24:24 -0500 Subject: [PATCH 44/83] fix: add entrypoint file for advanced usage --- src/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0a02b9a..0b016ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,3 @@ -import LambdaLog from './LambdaLog.js'; -import LogMessage from './LogMessage.js'; +import lambdaLog from './lambda-log.js'; -export default new LambdaLog(); -export { LambdaLog, LogMessage }; +export default lambdaLog; From d3408a583bf67b03533e792bf63d9ed54c177081 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:26:18 -0500 Subject: [PATCH 45/83] chore: add separate tsconfig for tests --- .eslintrc.yml | 4 +++- .npmignore | 2 ++ jest.config.ts | 14 ++++++++++---- tsconfig.eslint.json | 2 +- tsconfig.spec.json | 14 ++++++++++++++ 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 tsconfig.spec.json diff --git a/.eslintrc.yml b/.eslintrc.yml index 0da1c48..5fa6e13 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -34,7 +34,9 @@ rules: overrides: - files: ['*.ts'] parserOptions: - project: './tsconfig.eslint.json' + project: + - './tsconfig.eslint.json' + - './tsconfig.spec.json' extends: - 'xo-typescript/space' rules: diff --git a/.npmignore b/.npmignore index 2fd8803..fc12790 100644 --- a/.npmignore +++ b/.npmignore @@ -8,7 +8,9 @@ release.config.js commitlint.config.js tsconfig.json tsconfig.eslint.json +tsconfig.spec.json *.test.js +*.spec.js jest.config.js jest.config.ts .all-contributorsrc diff --git a/jest.config.ts b/jest.config.ts index bc68548..b4f1e3b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,20 +1,26 @@ import type { Config } from '@jest/types'; const config: Config.InitialOptions = { + preset: 'ts-jest', testEnvironment: 'node', - testRegex: '\\.test\\.ts$', + testRegex: '\\.spec\\.ts$', resolver: 'jest-ts-webcompat-resolver', collectCoverage: true, coverageReporters: [['json', { file: 'report.json' }], 'lcov', 'text', 'text-summary', 'html'], collectCoverageFrom: ['src/**/*.ts'], coverageDirectory: 'coverage', - coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/src/typings.ts'], + coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/src/index.ts', '/src/typings.ts'], transform: { '^.+\\.ts$': 'ts-jest' }, - moduleFileExtensions: ['ts', 'js', 'json', 'node'], + moduleFileExtensions: ['ts', 'js', 'json'], moduleDirectories: ['node_modules', 'src'], - watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/site/'] + watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/site/'], + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.spec.json' + } + } }; export default config; diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 8105934..a785379 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -3,7 +3,7 @@ "include": [ "*.ts", "*.js", - "src/*.test.ts" + "src/**/*.ts" ], "compilerOptions": { "types": ["jest", "node"] diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000..2fd4706 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "types": ["jest", "node"], + "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"], + "target": "es2019", + "module": "commonjs", + + "strict": true, + "declaration": false + }, + "include": [ + "src/*.spec.ts" + ] +} From 7a949dd2eaba4d50bdd9d359ef05ba0e9e88f736 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:26:49 -0500 Subject: [PATCH 46/83] chore: rename tests from .test.ts to .spec.ts --- src/{LambdaLog.test.ts => LambdaLog.spec.ts} | 0 ...{LogMessage.test.ts => LogMessage.spec.ts} | 23 +++++++++++++++++++ src/{utils.test.ts => utils.spec.ts} | 0 3 files changed, 23 insertions(+) rename src/{LambdaLog.test.ts => LambdaLog.spec.ts} (100%) rename src/{LogMessage.test.ts => LogMessage.spec.ts} (95%) rename src/{utils.test.ts => utils.spec.ts} (100%) diff --git a/src/LambdaLog.test.ts b/src/LambdaLog.spec.ts similarity index 100% rename from src/LambdaLog.test.ts rename to src/LambdaLog.spec.ts diff --git a/src/LogMessage.test.ts b/src/LogMessage.spec.ts similarity index 95% rename from src/LogMessage.test.ts rename to src/LogMessage.spec.ts index d9a7b28..5eeeed0 100644 --- a/src/LogMessage.test.ts +++ b/src/LogMessage.spec.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import 'expect-more-jest'; import LogMessage from './LogMessage'; import { StubbedError } from './typings'; @@ -133,6 +134,15 @@ describe('LogMessage', () => { } }); + expect(msg.meta).toBeEmptyObject(); + }); + + it('"meta" should skip dynamicMeta if function does not return an object', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + dynamicMeta: false + }); + expect(msg.meta).toHaveProperty('dynamic', 'meta'); }); @@ -330,6 +340,19 @@ describe('LogMessage', () => { expect((msg.__error!).message).toBe('custom error'); }); + it('set "msg" should skip `onParse` if the custom function does not return an object', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + onParse() { + return false; + } + }); + + expect(msg.msg).toBe('info test'); + expect(msg.tags).toHaveLength(0); + expect(msg.__error).toBe(null); + }); + it('set "msg" should json stringify a message that is an object', () => { const msg = new LogMessage({ ...logData.info }, defaultOpts); msg.msg = { test: true } as any; diff --git a/src/utils.test.ts b/src/utils.spec.ts similarity index 100% rename from src/utils.test.ts rename to src/utils.spec.ts From 50dd03d8651799ea4f865fcadae5b3d5abcfe121 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:27:41 -0500 Subject: [PATCH 47/83] feat: add support for direct entrypoint --- package.json | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 2500c73..d923604 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,19 @@ "name": "lambda-log", "version": "4.0.0-beta.1", "description": "Lightweight logging library for any Node 12+ applications", - "main": "index.js", - "module": "dist/esm/index.js", - "types": "dist/types/index.d.ts", + "main": "./index.js", + "module": "./dist/esm/lambda-log.js", + "types": "./dist/types/index.d.ts", "exports": { ".": { - "import": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/lambda-log.js", "require": "./index.js" + }, + "./esm": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm", + "require": "./dist/esm" } }, "scripts": { From 6c77c7971def43eb8b3f5e66a071f8b87d8b6bb8 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:28:15 -0500 Subject: [PATCH 48/83] fix: add package.json to esm and cjs directories after build --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d923604..13d9591 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,9 @@ "test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest --watchAll", "prepare": "is-ci || husky install", "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:declaration", - "build:esm": "tsc --module ES2020 --outDir dist/esm", - "build:cjs": "tsc --module commonjs --outDir dist/cjs", - "build:declaration": "tsc --module system --declaration --emitDeclarationOnly --outFile dist/types/index.d.ts", + "build:esm": "tsc --module ES2020 --outDir dist/esm && echo \"{\n \\\"type\\\": \\\"module\\\"\n}\" > ./dist/esm/package.json", + "build:cjs": "tsc --module commonjs --outDir dist/cjs && echo \"{\n \\\"type\\\": \\\"commonjs\\\"\n}\" > ./dist/cjs/package.json", + "build:declaration": "tsc --module none --declaration --emitDeclarationOnly --outFile dist/types/index.d.ts", "lint": "eslint \"src/*.ts\"", "clean": "rimraf dist" }, From 3becce8e42f90b3e4a6f767872e6b2c9a9abab90 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:28:36 -0500 Subject: [PATCH 49/83] chore: add `expect-more-jest` as a dev dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 13d9591..71ab4ad 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "eslint-config-xo-typescript": "^0.45.2", "eslint-plugin-jsdoc": "^37.0.3", "eslint-plugin-node": "^11.1.0", + "expect-more-jest": "^5.4.0", "husky": "^7.0.4", "is-ci": "^3.0.1", "jest": "^27.3.1", From 89db70742c011405534c6ce1a3c8916190d85a66 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 10:29:16 -0500 Subject: [PATCH 50/83] fix: change legacy cjs file to require `lambda-log.js` instead --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 5101830..893e2bc 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,2 @@ -const main = require('./dist/cjs'); +const main = require('./dist/cjs/lambda-log.js'); module.exports = main.default; From 5592cda800737e7facd3a1f471039cc961120d85 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 11:00:23 -0500 Subject: [PATCH 51/83] chore: add missing properties to tsconfig.spec.json --- tsconfig.spec.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 2fd4706..0860d46 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -6,6 +6,9 @@ "module": "commonjs", "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "suppressImplicitAnyIndexErrors": true, "declaration": false }, "include": [ From 42bffa2902eb0e068ee2df758a3a44d63bf013e9 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 11:00:46 -0500 Subject: [PATCH 52/83] test: add missing tests --- src/LogMessage.spec.ts | 55 +++++++++++++++++++++++++++--------------- src/utils.spec.ts | 7 ++++++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/LogMessage.spec.ts b/src/LogMessage.spec.ts index 5eeeed0..d9e7738 100644 --- a/src/LogMessage.spec.ts +++ b/src/LogMessage.spec.ts @@ -134,16 +134,16 @@ describe('LogMessage', () => { } }); - expect(msg.meta).toBeEmptyObject(); + expect(msg.meta).toHaveProperty('dynamic', 'meta'); }); it('"meta" should skip dynamicMeta if function does not return an object', () => { const msg = new LogMessage({ ...logData.info }, { ...defaultOpts, - dynamicMeta: false + dynamicMeta: () => false }); - expect(msg.meta).toHaveProperty('dynamic', 'meta'); + expect(msg.meta).toBeEmptyObject(); }); it('"meta" should convert errors in metadata to object', () => { @@ -156,17 +156,15 @@ describe('LogMessage', () => { } }); - expect(typeof msg.meta.prop).toBe('object'); + expect(msg.meta.prop).toBeInstanceOf(Error); expect(msg.meta.foo).toBe('bar'); - expect(typeof msg.meta.obj).toBe('object'); + expect(msg.meta.obj).toBeObject(); }); it('"tags" should return tags array', () => { const msg = new LogMessage({ ...logData.withTags }, defaultOpts); - expect(Array.isArray(msg.tags)).toBe(true); - expect(msg.tags).toHaveLength(1); - expect(msg.tags).toContain('test'); + expect(msg.tags).toBeArrayIncludingOnly(['test']); }); it('"tags" should combine all tags', () => { @@ -176,8 +174,7 @@ describe('LogMessage', () => { }); expect(msg.tags).toHaveLength(2); - expect(msg.tags).toContain('test'); - expect(msg.tags).toContain('global'); + expect(msg.tags).toBeArrayIncludingOnly(['test', 'global']); }); it('"tags" should execute functions', () => { @@ -185,7 +182,7 @@ describe('LogMessage', () => { ...defaultOpts, tags: [function (this: typeof LogMessage, props) { expect(this).toBeInstanceOf(LogMessage); - expect(typeof props).toBe('object'); + expect(props).toBeObject(); expect(props).toHaveProperty('level', 'info'); expect(props).toHaveProperty('meta', {}); expect(props).toHaveProperty('options'); @@ -200,6 +197,19 @@ describe('LogMessage', () => { expect(tags).toContain('dynamic'); }); + it('"tags" should skip values that are falsy', () => { + const msg = new LogMessage({ ...logData.info }, { + ...defaultOpts, + tags: [function () { + return false; + }] + }); + + const { tags } = msg; + + expect(tags).toHaveLength(0); + }); + it('"tags" should replace <> variable with log level', () => { const msg = new LogMessage({ ...logData.info }, { ...defaultOpts, @@ -209,9 +219,16 @@ describe('LogMessage', () => { const { tags } = msg; expect(tags).toHaveLength(3); - expect(tags).toContain(''); - expect(tags).toContain('info'); - expect(tags).toContain('<>'); + expect(tags).toBeArrayIncludingOnly(['', 'info', '<>']); + }); + + it('"tags" should not combine global tags if not an array', () => { + const msg = new LogMessage({ ...logData.withTags }, { + ...defaultOpts, + tags: 'invalid' as any + }); + + expect(msg.tags).toBeArrayIncludingOnly(['test']); }); it('"value" should return log object', () => { @@ -390,8 +407,7 @@ describe('LogMessage', () => { msg.tags = ['another-tag']; expect(msg.tags).toHaveLength(2); - expect(msg.tags).toContain('test'); - expect(msg.tags).toContain('another-tag'); + expect(msg.tags).toBeArrayIncludingOnly(['another-tag', 'test']); }); }); @@ -412,8 +428,7 @@ describe('LogMessage', () => { }); it('should return log in JSON format', () => { - expect(typeof msg.toJSON()).toBe('string'); - expect(msg.toJSON()).toMatch(/^\{.*\}$/); + expect(msg.toJSON()).toBeJsonString(); }); it('should run replacer function', () => { @@ -438,7 +453,7 @@ describe('LogMessage', () => { it('should return log in string format', () => { const msg = new LogMessage({ ...logData.info }, defaultOpts); - expect(typeof msg.toString()).toBe('string'); + expect(msg.toString()).toBeString(); }); it('should format as json with onFormat set to `json`', () => { @@ -447,7 +462,7 @@ describe('LogMessage', () => { onFormat: 'json' }); - expect(msg.toString()).toMatch(/^\{.*\}$/); + expect(msg.toString()).toBeJsonString(); }); it('should format with "clean" template with onFormat set to `clean`', () => { diff --git a/src/utils.spec.ts b/src/utils.spec.ts index b655477..c92c220 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -59,6 +59,13 @@ describe('utils', () => { const error = stubError(e); expect(error.toJSON!.toString()).toBe(noop.toString()); }); + + it('should skip over keys that are not on the Error instance', () => { + const e = new Error('test'); + delete e.stack; + const err = stubError(e); + expect(err).not.toHaveProperty('stack'); + }); }); describe('toBool()', () => { From be4002f2cdd94467a605f1e80f0df9a5e24171f9 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 11:01:22 -0500 Subject: [PATCH 53/83] chore: ignore branch that does not need to be tested --- src/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.ts b/src/utils.ts index 7d7a64b..ad4a5bf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -32,6 +32,7 @@ export function stubError(error: Error) { ].concat(Object.keys(err)); return keys.reduce((obj: GenericRecord, key) => { + /* istanbul ignore next */ if(key in err) { const val: unknown = err[key]; if(typeof val !== 'function') { From cc42f2242fba457625ab54aaee1f8a28ac33be38 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Fri, 19 Nov 2021 11:02:30 -0500 Subject: [PATCH 54/83] chore: cleanup npmignore --- .npmignore | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.npmignore b/.npmignore index fc12790..2c6e44c 100644 --- a/.npmignore +++ b/.npmignore @@ -9,9 +9,8 @@ commitlint.config.js tsconfig.json tsconfig.eslint.json tsconfig.spec.json -*.test.js -*.spec.js -jest.config.js +*.test.ts +*.spec.ts jest.config.ts .all-contributorsrc .codeclimate.yml From 81030164223bae0a9453993b31c1c035463a1b9e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 19 Nov 2021 16:05:24 +0000 Subject: [PATCH 55/83] chore(release): 4.0.0-beta.3 [skip ci] # [4.0.0-beta.3](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.2...v4.0.0-beta.3) (2021-11-19) ### Bug Fixes * add entrypoint file for advanced usage ([d7861de](https://github.com/KyleRoss/node-lambda-log/commit/d7861de9269532ab31631e559ec8da7a77c2870a)) * add package.json to esm and cjs directories after build ([6c77c79](https://github.com/KyleRoss/node-lambda-log/commit/6c77c7971def43eb8b3f5e66a071f8b87d8b6bb8)) * change legacy cjs file to require `lambda-log.js` instead ([89db707](https://github.com/KyleRoss/node-lambda-log/commit/89db70742c011405534c6ce1a3c8916190d85a66)) * rename index to `lambda-log` to ensure the types are generated properly ([467c9e4](https://github.com/KyleRoss/node-lambda-log/commit/467c9e42628ed05a6ba28bb2cea79f4129d6a2f5)) ### Features * add support for direct entrypoint ([50dd03d](https://github.com/KyleRoss/node-lambda-log/commit/50dd03d8651799ea4f865fcadae5b3d5abcfe121)) --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af9454..b01c916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# [4.0.0-beta.3](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.2...v4.0.0-beta.3) (2021-11-19) + + +### Bug Fixes + +* add entrypoint file for advanced usage ([d7861de](https://github.com/KyleRoss/node-lambda-log/commit/d7861de9269532ab31631e559ec8da7a77c2870a)) +* add package.json to esm and cjs directories after build ([6c77c79](https://github.com/KyleRoss/node-lambda-log/commit/6c77c7971def43eb8b3f5e66a071f8b87d8b6bb8)) +* change legacy cjs file to require `lambda-log.js` instead ([89db707](https://github.com/KyleRoss/node-lambda-log/commit/89db70742c011405534c6ce1a3c8916190d85a66)) +* rename index to `lambda-log` to ensure the types are generated properly ([467c9e4](https://github.com/KyleRoss/node-lambda-log/commit/467c9e42628ed05a6ba28bb2cea79f4129d6a2f5)) + + +### Features + +* add support for direct entrypoint ([50dd03d](https://github.com/KyleRoss/node-lambda-log/commit/50dd03d8651799ea4f865fcadae5b3d5abcfe121)) + # [4.0.0-beta.2](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.1...v4.0.0-beta.2) (2021-11-18) diff --git a/package.json b/package.json index f428c14..da5e70d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-log", - "version": "4.0.0-beta.2", + "version": "4.0.0-beta.3", "description": "Lightweight logging library for any Node 12+ applications", "main": "./index.js", "module": "./dist/esm/lambda-log.js", From 1b7fcf9e448b95bf5f255d35e7291c7fb3b6146c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 25 Nov 2021 11:10:24 -0500 Subject: [PATCH 56/83] chore: add *.spec.ts files to exclude patterns for codeclimate --- .codeclimate.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.codeclimate.yml b/.codeclimate.yml index 38f0336..73ff741 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -52,3 +52,5 @@ exclude_patterns: - "**/*.d.ts" - "**/*.test.ts" - "**/*.test.js" + - "**/*.spec.ts" + - "**/*.spec.js" From 9fc18573373e62a09c46f71a3517f862b74aac77 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 25 Nov 2021 11:25:20 -0500 Subject: [PATCH 57/83] fix: destructure import of EventEmitter to support TS projects --- src/LambdaLog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index 0e51788..d3317df 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -1,4 +1,4 @@ -import EventEmitter from 'events'; +import { EventEmitter } from 'events'; import { LambdaLogOptions, Message, GenericRecord, LogLevels, LogObject, Tag, ConsoleObject } from './typings.js'; import LogMessage from './LogMessage.js'; import { toBool } from './utils.js'; From 35cd48bd9c0dbf6472831407edfc8d445aa6781b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 25 Nov 2021 16:27:36 +0000 Subject: [PATCH 58/83] chore(release): 4.0.0-beta.4 [skip ci] # [4.0.0-beta.4](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.3...v4.0.0-beta.4) (2021-11-25) ### Bug Fixes * destructure import of EventEmitter to support TS projects ([9fc1857](https://github.com/KyleRoss/node-lambda-log/commit/9fc18573373e62a09c46f71a3517f862b74aac77)) --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01c916..60e4f4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [4.0.0-beta.4](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.3...v4.0.0-beta.4) (2021-11-25) + + +### Bug Fixes + +* destructure import of EventEmitter to support TS projects ([9fc1857](https://github.com/KyleRoss/node-lambda-log/commit/9fc18573373e62a09c46f71a3517f862b74aac77)) + # [4.0.0-beta.3](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.2...v4.0.0-beta.3) (2021-11-19) diff --git a/package.json b/package.json index da5e70d..be17cf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-log", - "version": "4.0.0-beta.3", + "version": "4.0.0-beta.4", "description": "Lightweight logging library for any Node 12+ applications", "main": "./index.js", "module": "./dist/esm/lambda-log.js", From 4491110fd5e87cb0eb1c78b94104bec5ac87080e Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:12:39 -0500 Subject: [PATCH 59/83] chore: specify key of `reduce` as string --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index ad4a5bf..9ed5d81 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,7 +31,7 @@ export function stubError(error: Error) { 'stack' ].concat(Object.keys(err)); - return keys.reduce((obj: GenericRecord, key) => { + return keys.reduce((obj: GenericRecord, key: string) => { /* istanbul ignore next */ if(key in err) { const val: unknown = err[key]; From cc10311b3e0580e840792e452d11dcc43e911544 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:13:15 -0500 Subject: [PATCH 60/83] fix: remove generic from Message type --- src/typings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typings.ts b/src/typings.ts index fe3726b..5c3376d 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -2,7 +2,7 @@ import LogMessage from './LogMessage.js'; import stringify from 'fast-safe-stringify'; export type GenericRecord = Record; -export type Message = T; +export type Message = string | number | Error; export type Empty = false | null | undefined; type TagFnObject = { From 9bafeb01c0182feb94f1e88afa306a7a3616c171 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:14:01 -0500 Subject: [PATCH 61/83] chore: add new Metadata type to use for incoming metadata --- src/typings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/typings.ts b/src/typings.ts index 5c3376d..9cb661a 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -3,6 +3,7 @@ import stringify from 'fast-safe-stringify'; export type GenericRecord = Record; export type Message = string | number | Error; +export type Metadata = GenericRecord | string | number | null | undefined; export type Empty = false | null | undefined; type TagFnObject = { From cd1c74697a99840a48f29d49509ad3409e145de5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:14:59 -0500 Subject: [PATCH 62/83] fix: add generic to LogObject type to specify type of Message --- src/typings.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/typings.ts b/src/typings.ts index 9cb661a..7000363 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -23,9 +23,10 @@ export type Formatter = ((ctx: LogMessage, options: LambdaLogOptions, stringify: StringifyType) => string); export type LogObject = { +export type LogObject = { level: string; - msg: Message; - meta?: GenericRecord; + msg: T; + meta?: Metadata; tags?: Tag[]; }; From 8c6b3ca98fa8a6a6ac5491c07e9b8dc77d1ae119 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:15:42 -0500 Subject: [PATCH 63/83] chore: separate Formatter type into individual "plugin" types --- src/typings.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/typings.ts b/src/typings.ts index 7000363..e05b5df 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -16,13 +16,6 @@ export type Tag = string | number | ((data: TagFnObject) => string | Empty) | Em type StringifyType = typeof stringify; -export type Formatter = - 'json' | - 'clean' | - 'minimal' | - ((ctx: LogMessage, options: LambdaLogOptions, stringify: StringifyType) => string); - -export type LogObject = { export type LogObject = { level: string; msg: T; @@ -32,6 +25,13 @@ export type LogObject = { export type LogLevels = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; +export type ParsePlugin = (msg: Message, options: LambdaLogOptions) => { msg: string; meta?: GenericRecord; error?: Error; tags?: Tag[] } | Empty; +export type CompilePlugin = (level?: string, msg?: Message, meta?: GenericRecord, tags?: Tag[], options?: LambdaLogOptions) => GenericRecord; +export type FormatPlugin = { + (ctx: LogMessage, options: LambdaLogOptions, stringify: StringifyType): string; + _cfg?: Record; +}; + export type LambdaLogOptions = { [key: string]: any; meta?: GenericRecord; From fddc993d1c43309be07d7e85aa14df2aff980f92 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:16:11 -0500 Subject: [PATCH 64/83] chore: update options to use new "plugin" types --- src/typings.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/typings.ts b/src/typings.ts index e05b5df..c883826 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -45,9 +45,9 @@ export type LambdaLogOptions = { levelKey?: string | false; messageKey?: string; tagsKey?: string | false; - onParse?: (msg: Message, options: LambdaLogOptions) => { msg: string; meta?: GenericRecord; error?: Error; tags?: Tag[] } | Empty; - onCompile?: (level?: string, msg?: Message, meta?: GenericRecord, tags?: Tag[], options?: LambdaLogOptions) => GenericRecord; - onFormat?: Formatter; + onParse?: ParsePlugin; + onCompile?: CompilePlugin; + onFormat?: FormatPlugin; }; export interface ConsoleObject extends Console { From f8ecac7ed12b25495c634a904f489857a7413f80 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:16:54 -0500 Subject: [PATCH 65/83] feat: separate built-in formatters into separate files --- src/formatters/full.spec.ts | 64 +++++++++++++++++++++++++++ src/formatters/full.ts | 62 ++++++++++++++++++++++++++ src/formatters/index.ts | 3 ++ src/formatters/json.spec.ts | 81 ++++++++++++++++++++++++++++++++++ src/formatters/json.ts | 12 +++++ src/formatters/minimal.spec.ts | 73 ++++++++++++++++++++++++++++++ src/formatters/minimal.ts | 36 +++++++++++++++ 7 files changed, 331 insertions(+) create mode 100644 src/formatters/full.spec.ts create mode 100644 src/formatters/full.ts create mode 100644 src/formatters/index.ts create mode 100644 src/formatters/json.spec.ts create mode 100644 src/formatters/json.ts create mode 100644 src/formatters/minimal.spec.ts create mode 100644 src/formatters/minimal.ts diff --git a/src/formatters/full.spec.ts b/src/formatters/full.spec.ts new file mode 100644 index 0000000..8136de0 --- /dev/null +++ b/src/formatters/full.spec.ts @@ -0,0 +1,64 @@ +import 'expect-more-jest'; +import stringify from 'fast-safe-stringify'; +import LogMessage from '../LogMessage'; +import fullFormatter from './full'; + +const defaultOpts = { + meta: {}, + dynamicMeta: null, + tags: [], + levelKey: '_logLevel', + messageKey: 'msg', + tagsKey: '_tags', + replacer: null +}; + +describe('formatters/full', () => { + it('should export a closure function', () => { + expect(typeof fullFormatter).toBe('function'); + }); + + it('should return a formatter function', () => { + expect(typeof fullFormatter()).toBe('function'); + }); + + it('should overrride configuration', () => { + const formatter = fullFormatter({ + includeTimestamp: false, + includeTags: false, + includeMeta: false, + separator: '\t', + inspectOptions: { + colors: false, + maxArrayLength: 25 + } + }); + + const cfg = formatter._cfg!; + + expect(cfg.includeTimestamp).toBe(false); + expect(cfg.includeTags).toBe(false); + expect(cfg.includeMeta).toBe(false); + expect(cfg.separator).toBe('\t'); + expect(cfg.inspectOptions).toEqual({ + depth: Infinity, + colors: false, + maxArrayLength: 25 + }); + }); + + it('should skip the timestamp if includeTimestamp is false', () => { + const msg = new LogMessage({ + level: 'info', + msg: 'info test', + meta: {}, + tags: [] + }, defaultOpts); + + const formatter = fullFormatter({ + includeTimestamp: false + }); + + expect(formatter(msg, defaultOpts, stringify)).toMatch(/^INFO\tinfo test$/); + }); +}); diff --git a/src/formatters/full.ts b/src/formatters/full.ts new file mode 100644 index 0000000..01b9bf4 --- /dev/null +++ b/src/formatters/full.ts @@ -0,0 +1,62 @@ +import { FormatPlugin } from '../typings.js'; +import { inspect, InspectOptions } from 'util'; + +type FullFormatterCfg = { + includeTimestamp?: boolean; + formatTimestamp?: (timestamp: Date) => string; + includeTags?: boolean; + includeMeta?: boolean; + separator?: string; + inspectOptions?: InspectOptions; +}; + +/** + * Full formatter for log messages. + * @param {object} cfg Configuration object for the formatter. + * @returns {FormatPlugin} The full formatter function. + */ +export default function fullFormatter(cfg: FullFormatterCfg = {}): FormatPlugin { + const fmCfg = { + includeTimestamp: true, + formatTimestamp: (timestamp: Date) => timestamp.toISOString(), + includeTags: true, + includeMeta: true, + separator: '\t', + ...cfg + }; + + fmCfg.inspectOptions = { + depth: Infinity, + colors: true, + ...(fmCfg.inspectOptions ?? {}) + }; + + const fullFmt: FormatPlugin = (ctx): string => { + const msg = []; + if(fmCfg.includeTimestamp) { + msg.push(fmCfg.formatTimestamp(new Date())); + } + + msg.push(ctx.level.toUpperCase(), ctx.msg); + + const parts = [ + msg.join(fmCfg.separator) + ]; + + if(fmCfg.includeTags && ctx.tags.length) { + const tags = ctx.tags.map(tag => `${tag}`).join(', '); + parts.push(`→ ${tags}`); + } + + if(fmCfg.includeMeta && Object.keys(ctx.meta).length) { + const meta = inspect(ctx.meta, fmCfg.inspectOptions!); + parts.push(`→ ${meta.replace(/\n/g, '\n ')}`); + } + + return parts.join('\n'); + }; + + fullFmt._cfg = fmCfg; + + return fullFmt; +} diff --git a/src/formatters/index.ts b/src/formatters/index.ts new file mode 100644 index 0000000..2e0156a --- /dev/null +++ b/src/formatters/index.ts @@ -0,0 +1,3 @@ +export { default as json } from './json.js'; +export { default as full } from './full.js'; +export { default as minimal } from './minimal.js'; diff --git a/src/formatters/json.spec.ts b/src/formatters/json.spec.ts new file mode 100644 index 0000000..4607a6f --- /dev/null +++ b/src/formatters/json.spec.ts @@ -0,0 +1,81 @@ +import 'expect-more-jest'; +import stringify from 'fast-safe-stringify'; +import LogMessage from '../LogMessage'; +import jsonFormatter from './json'; + +const defaultOpts = { + meta: {}, + dynamicMeta: null, + tags: [], + levelKey: '_logLevel', + messageKey: 'msg', + tagsKey: '_tags', + replacer: null +}; + +const logObject = { + level: 'info', + msg: 'info test', + meta: {}, + tags: [] +}; + + +describe('formatters/json', () => { + it('should export a closure function', () => { + expect(typeof jsonFormatter).toBe('function'); + }); + + it('should return a formatter function', () => { + expect(typeof jsonFormatter()).toBe('function'); + }); + + const replacerOpts = { + ...defaultOpts, + meta: { ssn: '444-55-6666' }, + replacer(key: string, value: unknown) { + if(key === 'ssn') { + return `${(value as string).substring(0, 3)}-**-****`; + } + + return value; + } + }; + + const msg = new LogMessage({ ...logObject }, replacerOpts); + + const formatter = jsonFormatter(); + const result = formatter(msg, replacerOpts, stringify); + + it('should return log in JSON format', () => { + expect(result).toBeJsonString(); + }); + + it('should run replacer function', () => { + expect(JSON.parse(result).ssn).toBe('444-**-****'); + }); + + it('should not run replacer function when not defined', () => { + const msgNoReplacer = new LogMessage({ ...logObject }, { + ...defaultOpts, + meta: { ssn: '444-55-6666' } + }); + + const noReplacerResult = formatter(msgNoReplacer, defaultOpts, stringify); + + expect(JSON.parse(noReplacerResult).ssn).toBe('444-55-6666'); + }); + + it('should pretty print JSON when dev is "true"', () => { + const opts = { + ...defaultOpts, + dev: true, + meta: { ssn: '444-55-6666' } + }; + + const msgDev = new LogMessage({ ...logObject }, opts); + const prettyResult = formatter(msgDev, opts, stringify); + + expect(/\n/g.test(prettyResult)).toBe(true); + }); +}); diff --git a/src/formatters/json.ts b/src/formatters/json.ts new file mode 100644 index 0000000..24e1551 --- /dev/null +++ b/src/formatters/json.ts @@ -0,0 +1,12 @@ +import { FormatPlugin } from '../typings.js'; + +/** + * JSON formatter for log messages. + * @returns {FormatPlugin} The JSON formatter function. + */ +export default function jsonFormatter(): FormatPlugin { + const jsonFmt: FormatPlugin = (ctx, options, stringify): string => + stringify(ctx.value, options.replacer ?? undefined, options.dev ? 2 : 0); + + return jsonFmt; +} diff --git a/src/formatters/minimal.spec.ts b/src/formatters/minimal.spec.ts new file mode 100644 index 0000000..f851f45 --- /dev/null +++ b/src/formatters/minimal.spec.ts @@ -0,0 +1,73 @@ +import 'expect-more-jest'; +import stringify from 'fast-safe-stringify'; +import LogMessage from '../LogMessage'; +import minimalFormatter from './minimal'; + +const defaultOpts = { + meta: {}, + dynamicMeta: null, + tags: [], + levelKey: '_logLevel', + messageKey: 'msg', + tagsKey: '_tags', + replacer: null +}; + +describe('formatters/minmal', () => { + it('should export a closure function', () => { + expect(typeof minimalFormatter).toBe('function'); + }); + + it('should return a formatter function', () => { + expect(typeof minimalFormatter()).toBe('function'); + }); + + it('should overrride configuration', () => { + const formatter = minimalFormatter({ + includeTimestamp: false, + separator: '\t' + }); + + const cfg = formatter._cfg!; + + expect(cfg.includeTimestamp).toBe(false); + expect(cfg.separator).toBe('\t'); + }); + + it('should format timestamp as ISO string by default', () => { + const formatter = minimalFormatter(); + + // @ts-expect-error - we're testing the internals here + expect((formatter._cfg!).formatTimestamp(new Date())).toMatch(/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)$/); + }); + + it('should include the timestamp if includeTimestamp is true', () => { + const msg = new LogMessage({ + level: 'info', + msg: 'info test', + meta: {}, + tags: [] + }, defaultOpts); + + const formatter = minimalFormatter({ + includeTimestamp: true + }); + + expect(formatter(msg, defaultOpts, stringify)).toMatch(/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z) | INFO | info test$/); + }); + + it('should skip the timestamp if includeTimestamp is false', () => { + const msg = new LogMessage({ + level: 'info', + msg: 'info test', + meta: {}, + tags: [] + }, defaultOpts); + + const formatter = minimalFormatter({ + includeTimestamp: false + }); + + expect(formatter(msg, defaultOpts, stringify)).toMatch(/^INFO | info test$/); + }); +}); diff --git a/src/formatters/minimal.ts b/src/formatters/minimal.ts new file mode 100644 index 0000000..28c7cb6 --- /dev/null +++ b/src/formatters/minimal.ts @@ -0,0 +1,36 @@ +import { FormatPlugin } from '../typings.js'; + +type MinimalFormatterCfg = { + includeTimestamp?: boolean; + formatTimestamp?: (timestamp: Date) => string; + separator?: string; +}; + +/** + * Minimal formatter for log messages. + * @param {object} cfg Configuration object for the formatter. + * @returns {FormatPlugin} The minimal formatter function. + */ +export default function minimalFormatter(cfg: MinimalFormatterCfg = {}): FormatPlugin { + const fmCfg = { + includeTimestamp: false, + formatTimestamp: (timestamp: Date) => timestamp.toISOString(), + separator: ' | ', + ...cfg + }; + + const minimalFmt: FormatPlugin = (ctx): string => { + const parts = []; + if(fmCfg.includeTimestamp) { + parts.push(fmCfg.formatTimestamp(new Date())); + } + + parts.push(ctx.level.toUpperCase(), ctx.msg); + + return parts.join(fmCfg.separator); + }; + + minimalFmt._cfg = fmCfg; + + return minimalFmt; +} From 223785f7dcea872efba3abc182d2abf5236b8e4c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:18:43 -0500 Subject: [PATCH 66/83] chore: provide multiple methods of accessing built-in formatters --- src/LambdaLog.ts | 5 +++++ src/lambda-log.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index d3317df..b021aca 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -1,6 +1,7 @@ import { EventEmitter } from 'events'; import { LambdaLogOptions, Message, GenericRecord, LogLevels, LogObject, Tag, ConsoleObject } from './typings.js'; import LogMessage from './LogMessage.js'; +import * as logFormatters from './formatters/index.js'; import { toBool } from './utils.js'; @@ -41,8 +42,12 @@ export const defaultOptions: LambdaLogOptions = { tagsKey: '__tags' }; +export const formatters = logFormatters; + export default class LambdaLog extends EventEmitter { + static formatters = formatters; + /** * Access to the uninstantiated LambdaLog class. * @readonly diff --git a/src/lambda-log.ts b/src/lambda-log.ts index 6fa05c5..588a3cf 100644 --- a/src/lambda-log.ts +++ b/src/lambda-log.ts @@ -1,6 +1,7 @@ import LambdaLog from './LambdaLog.js'; import LogMessage from './LogMessage.js'; +import * as formatters from './formatters/index.js'; import * as Types from './typings.js'; export default new LambdaLog(); -export { LambdaLog, LogMessage, Types }; +export { LambdaLog, LogMessage, formatters, Types }; From 31281dfe04f560fe0853727aefae5be297003cfb Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:19:13 -0500 Subject: [PATCH 67/83] chore: default to the built-in json formatter --- src/LambdaLog.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index b021aca..55153c2 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -39,7 +39,8 @@ export const defaultOptions: LambdaLogOptions = { logHandler: console, levelKey: '__level', messageKey: 'msg', - tagsKey: '__tags' + tagsKey: '__tags', + onFormat: logFormatters.json() }; export const formatters = logFormatters; From cde1d2dab11ff6ddc15a0dad3a9f17aa70986a2f Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:20:00 -0500 Subject: [PATCH 68/83] chore: provide additional access to `defaultOptions` object as a static property --- src/LambdaLog.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index 55153c2..d0d9d3a 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -47,6 +47,7 @@ export const formatters = logFormatters; export default class LambdaLog extends EventEmitter { + static defaultOptions = defaultOptions; static formatters = formatters; /** From 7317bf52418af6fed65db8066b94d8c2dbe21023 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:21:05 -0500 Subject: [PATCH 69/83] fix: rename the `log` method to `_log` --- src/LambdaLog.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index d0d9d3a..094515b 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -123,11 +123,11 @@ export default class LambdaLog extends EventEmitter { * @template T The type of the message to log. * @param {string} level Log level (`info`, `debug`, `warn`, `error`, or `fatal`) * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. - * @param {object} [meta={}] Optional meta data to attach to the log. + * @param {object|string|number} [meta={}] Optional meta data to attach to the log. * @param {string[]} [tags=[]] Additional tags to append to this log. - * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + * @returns {LogMessage} Returns instance of LogMessage. */ - log(level: LogLevels, msg: T, meta: GenericRecord = {}, tags: Tag[] = []) { + _log(level: LogLevels, msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { const lvl = this.getLevel(level); if(!lvl) { throw new Error(`"${level}" is not a valid log level`); From a3c1f163976de3f68c49e05f3a84ab7c1791c0e5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:22:44 -0500 Subject: [PATCH 70/83] feat: add `log` shortcut method as an alias for `info` to match the `console` pattern --- src/LambdaLog.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index 094515b..fedda7e 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -193,6 +193,16 @@ export default class LambdaLog extends EventEmitter { */ info(msg: T, meta?: GenericRecord, tags?: Tag[]) { return this.log('info', msg, meta, tags); + /** + * Alias for `info`. + * @template T The type of the message to log. + * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. + * @param {Metadata} [meta] Optional meta data to attach to the log. + * @param {Tag[]} [tags] Additional tags to append to this log. + * @returns {LogMessage} Returns instance of LogMessage. + */ + log(msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { + return this._log('info', msg, meta, tags); } /** From 6e13564da87fe5b189a2b9262d8e3613a3f138e5 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:23:50 -0500 Subject: [PATCH 71/83] fix: still generate the log message just don't log it when verbosity is set --- src/LambdaLog.ts | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index fedda7e..0ee211e 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -133,29 +133,30 @@ export default class LambdaLog extends EventEmitter { throw new Error(`"${level}" is not a valid log level`); } - // Check if we can log this level - if(lvl.idx > this.maxLevelIdx) return false; - // Generate the log message instance const message = new this.LogMessage({ level, msg, meta, tags - } as LogObject, this.options); - - const consoleObj = this.console; - // Log the message to the console - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - consoleObj[lvl.method](message.toString()); - - /** - * The log event is emitted (using EventEmitter) for every log generated. This allows for custom integrations, such as logging to a thrid-party service. - * This event is emitted with the [LogMessage](#logmessage) instance for the log. You may control events using all the methods of EventEmitter. - * @event LambdaLog#log - * @type {LogMessage} - */ - this.emit('log', message); + } as LogObject, this.options); + + // Check if we can log this level + if(lvl.idx <= this.maxLevelIdx) { + const consoleObj = this.console; + // Log the message to the console + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + consoleObj[lvl.method](message.toString()); + + /** + * The log event is emitted (using EventEmitter) for every log generated. This allows for custom integrations, such as logging to a thrid-party service. + * This event is emitted with the [LogMessage](#logmessage) instance for the log. You may control events using all the methods of EventEmitter. + * @event LambdaLog#log + * @type {LogMessage} + */ + this.emit('log', message); + } + return message; } From 7e48904076802adf571385f73ac83294c0d4f875 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:25:24 -0500 Subject: [PATCH 72/83] fix: ts generics and typings on shortcut methods --- src/LambdaLog.ts | 66 ++++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index 0ee211e..70f8202 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -164,36 +164,38 @@ export default class LambdaLog extends EventEmitter { * Logs a message at the `trace` log level. * @template T The type of the message to log. * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. - * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Metadata} [meta] Optional meta data to attach to the log. * @param {Tag[]} [tags] Additional tags to append to this log. - * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + * @returns {LogMessage} Returns instance of LogMessage. */ - trace(msg: T, meta?: GenericRecord, tags?: Tag[]) { - return this.log('trace', msg, meta, tags); + trace(msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { + return this._log('trace', msg, meta, tags); } /** * Logs a message at the `debug` log level. * @template T The type of the message to log. * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. - * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Metadata} [meta] Optional meta data to attach to the log. * @param {Tag[]} [tags] Additional tags to append to this log. - * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + * @returns {LogMessage} Returns instance of LogMessage. */ - debug(msg: T, meta?: GenericRecord, tags?: Tag[]) { - return this.log('debug', msg, meta, tags); + debug(msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { + return this._log('debug', msg, meta, tags); } /** * Logs a message at the `info` log level. * @template T The type of the message to log. * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. - * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Metadata} [meta] Optional meta data to attach to the log. * @param {Tag[]} [tags] Additional tags to append to this log. - * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + * @returns {LogMessage} Returns instance of LogMessage. */ - info(msg: T, meta?: GenericRecord, tags?: Tag[]) { - return this.log('info', msg, meta, tags); + info(msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { + return this._log('info', msg, meta, tags); + } + /** * Alias for `info`. * @template T The type of the message to log. @@ -210,42 +212,41 @@ export default class LambdaLog extends EventEmitter { * Logs a message at the `warn` log level. * @template T The type of the message to log. * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. - * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Metadata} [meta] Optional meta data to attach to the log. * @param {Tag[]} [tags] Additional tags to append to this log. - * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + * @returns {LogMessage} Returns instance of LogMessage. */ - warn(msg: T, meta?: GenericRecord, tags?: Tag[]) { - return this.log('warn', msg, meta, tags); + warn(msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { + return this._log('warn', msg, meta, tags); } /** * Logs a message at the `error` log level. * @template T The type of the message to log. * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. - * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Metadata} [meta] Optional meta data to attach to the log. * @param {Tag[]} [tags] Additional tags to append to this log. - * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + * @returns {LogMessage} Returns instance of LogMessage. */ - error(msg: T, meta?: GenericRecord, tags?: Tag[]) { - return this.log('error', msg, meta, tags); + error(msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { + return this._log('error', msg, meta, tags); } /** * Logs a message at the `error` log level. * @template T The type of the message to log. * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. - * @param {GenericRecord} [meta] Optional meta data to attach to the log. + * @param {Metadata} [meta] Optional meta data to attach to the log. * @param {Tag[]} [tags] Additional tags to append to this log. - * @returns {LogMessage|false} Returns instance of LogMessage or `false` if the level of the log exceeds to the maximum set log level. + * @returns {LogMessage} Returns instance of LogMessage. */ - fatal(msg: T, meta?: GenericRecord, tags?: Tag[]) { - return this.log('fatal', msg, meta, tags); + fatal(msg: T, meta?: Metadata, tags?: Tag[]): LogMessage { + return this._log('fatal', msg, meta, tags); } /** * Generates a log message if `test` is a falsy value. If `test` is truthy, the log message is skipped and returns `false`. Allows creating log messages without the need to * wrap them in an if statement. The log level will be `error`. - * @since 1.4.0 * @template T The type of the message to log. * @param {*} test Value to test for a falsy value. * @param {T} msg Message to log. Can be any type, but string or `Error` is reccommended. @@ -253,27 +254,26 @@ export default class LambdaLog extends EventEmitter { * @param {string[]} [tags=[]] Additional tags to append to this log. * @returns {LogMessage|false} The generated log message or `false` if assertion passed. */ - assert(test: unknown, msg: T, meta?: GenericRecord, tags?: Tag[]) { + assert(test: unknown, msg: T, meta?: Metadata, tags?: Tag[]): LogMessage | false { if(test) return false; - return this.log('error', msg, meta, tags); + return this._log('error', msg, meta, tags); } /** * Generates a log message with the result or error provided by a promise. Useful for debugging and testing. - * @since 2.3.0 * @param {Promise<*>} promise Promise to log the results of. * @param {object} [meta={}] Optional meta data to attach to the log. * @param {string[]} [tags=[]] Additional tags to append to this log. - * @returns {Promise} A Promise that resolves with the log message. + * @returns {Promise} A Promise that resolves with the log message. */ - async result(promise: Promise, meta?: GenericRecord, tags?: Tag[]) { - const wrapper = new Promise(resolve => { + async result(promise: Promise, meta?: Metadata, tags?: Tag[]): Promise { + const wrapper = new Promise(resolve => { promise .then(value => { - resolve(this.log('info', value as string, meta, tags)); + resolve(this._log('info', value, meta, tags)); }) .catch(err => { - resolve(this.log('error', err as Error, meta, tags)); + resolve(this._log('error', err as Error, meta, tags)); }); }); From 3562d252a986c22fe932bc1b0194759b45fd1523 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:25:56 -0500 Subject: [PATCH 73/83] chore: misc cleanup and type updates --- src/LambdaLog.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/LambdaLog.ts b/src/LambdaLog.ts index 70f8202..e7aac18 100644 --- a/src/LambdaLog.ts +++ b/src/LambdaLog.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import { LambdaLogOptions, Message, GenericRecord, LogLevels, LogObject, Tag, ConsoleObject } from './typings.js'; +import { LambdaLogOptions, Message, Metadata, LogLevels, LogObject, Tag, ConsoleObject } from './typings.js'; import LogMessage from './LogMessage.js'; import * as logFormatters from './formatters/index.js'; import { toBool } from './utils.js'; @@ -53,18 +53,15 @@ export default class LambdaLog extends EventEmitter { /** * Access to the uninstantiated LambdaLog class. * @readonly - * @type {LambdaLog} */ readonly LambdaLog = LambdaLog; /** * Access to the uninstantiated LogMessage class. - * @type {LogMessage} */ LogMessage = LogMessage; /** * The options object for this instance of LambdaLog. - * @type {LambdaLogOptions} */ options: LambdaLogOptions; @@ -76,7 +73,6 @@ export default class LambdaLog extends EventEmitter { /** * Returns the console object to use for logging. - * @readonly * @private * @returns {ConsoleObject} The configured console object or `console` if none is provided. */ @@ -286,7 +282,7 @@ export default class LambdaLog extends EventEmitter { * @param {string} level The provided log level string. * @returns {object} Returns the configuration for the provided log level. */ - private getLevel(level: string) { + private getLevel(level: string): { idx: number; name: string; method: string } | false { if(!level) return false; const lvl = levels.findIndex(l => l.name === level.toLowerCase()); if(lvl === -1) return false; @@ -299,11 +295,10 @@ export default class LambdaLog extends EventEmitter { /** * Returns the index of the configured maximum log level. - * @readonly * @private * @returns {number} The index of the configured maximum log level. */ - private get maxLevelIdx() { + private get maxLevelIdx(): number { if(!this.options.level || this.options.silent) return -1; return levels.findIndex(l => l.name === this.options.level); } From b483dbb386cc2a11c704d40c08a308a25cd8e185 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:26:19 -0500 Subject: [PATCH 74/83] test: update tests for LambdaLog class to reflect recent changes --- src/LambdaLog.spec.ts | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/LambdaLog.spec.ts b/src/LambdaLog.spec.ts index e8fe872..59b02c2 100644 --- a/src/LambdaLog.spec.ts +++ b/src/LambdaLog.spec.ts @@ -128,32 +128,33 @@ describe('LambdaLog', () => { mockConsole.debug.mockClear(); }); - describe('log()', () => { + describe('_log()', () => { it('should throw error for invalid log level', () => { const log = new LambdaLog(); - expect(() => log.log('foo' as any, 'test')).toThrowError(/^"foo" is not a valid log level$/); + expect(() => log._log('foo' as any, 'test')).toThrowError(/^"foo" is not a valid log level$/); }); it('should throw error for no log level', () => { const log = new LambdaLog(); - expect(() => log.log(null as any, 'test')).toThrowError(/is not a valid log level$/); + expect(() => log._log(null as any, 'test')).toThrowError(/is not a valid log level$/); }); - it('should return false for a disabled log level', () => { + it('should not log for a disabled log level', () => { const log = new LambdaLog({ level: 'fatal' }); - const result = log.log('debug', 'test'); - expect(result).toBe(false); + const result = log._log('debug', 'test'); + expect(result).toBeInstanceOf(LogMessage); + expect(mockConsole.debug).not.toHaveBeenCalled(); }); it('should return log message instance', () => { const log = new LambdaLog(); - const result = log.log('info', 'test'); + const result = log._log('info', 'test'); expect(result).toBeInstanceOf(LogMessage); }); it('should not log message when silent is enabled', () => { const log = new LambdaLog({ silent: true }); - log.log('info', 'test'); + log._log('info', 'test'); expect(mockConsole.info).toBeCalledTimes(0); }); @@ -165,7 +166,7 @@ describe('LambdaLog', () => { done(); }); - const res = log.log('error', 'test'); + const res = log._log('error', 'test'); expect(res).toBeInstanceOf(LogMessage); }); @@ -175,7 +176,7 @@ describe('LambdaLog', () => { }); const info = jest.spyOn(console, 'info'); - log.log('info', 'test'); + log._log('info', 'test'); expect(info).toBeCalled(); info.mockRestore(); }); @@ -206,11 +207,9 @@ describe('LambdaLog', () => { promise.then(msg => { expect(msg).toBeInstanceOf(LogMessage); - if(msg !== false) { - expect(msg.level).toBe('info'); - expect(msg.msg).toBe('Success!'); - done(); - } + expect(msg.level).toBe('info'); + expect(msg.msg).toBe('Success!'); + done(); }); }); @@ -220,11 +219,9 @@ describe('LambdaLog', () => { promise.then(msg => { expect(msg).toBeInstanceOf(LogMessage); - if(msg !== false) { - expect(msg.level).toBe('error'); - expect(msg.msg).toBe('Failed!'); - done(); - } + expect(msg.level).toBe('error'); + expect(msg.msg).toBe('Failed!'); + done(); }); }); }); @@ -248,6 +245,12 @@ describe('LambdaLog', () => { expect(mockConsole.info).toBeCalledTimes(1); }); + it('should log info message (log)', () => { + const log = new LambdaLog({ level: 'info' }); + log.log('test'); + expect(mockConsole.info).toBeCalledTimes(1); + }); + it('should log warn message', () => { const log = new LambdaLog({ level: 'warn' }); log.warn('test'); From df6ac1afe3525aa0df1a69294bb093a4f3880c57 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:27:50 -0500 Subject: [PATCH 75/83] chore: update LogMessage to use new formatters --- src/LogMessage.ts | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/LogMessage.ts b/src/LogMessage.ts index 8f3c6f9..003cc10 100644 --- a/src/LogMessage.ts +++ b/src/LogMessage.ts @@ -1,6 +1,7 @@ import stringify from 'fast-safe-stringify'; import { LambdaLogOptions, Message, LogObject, Tag, GenericRecord, Formatter, StubbedError, Empty } from './typings.js'; import { isError, stubError } from './utils.js'; +import jsonFormatter from './formatters/json.js'; export interface ILogMessage { readonly __opts: LambdaLogOptions; @@ -250,34 +251,17 @@ export default class LogMessage implements ILogMessage { /** * Converts the log to a string using a specific/custom formatter. * @protected - * @param {Formatter} [formatter] The formatter to use or custom formatter function. + * @param {FormatPlugin} [formatter] The formatter to use or custom formatter function. * @returns {string} The formatted log as a string. */ - protected formatMessage(formatter?: Formatter) { + protected formatMessage(formatter?: FormatPlugin): string { const { __opts } = this; - if(typeof formatter === 'function') { - return formatter.call(this, this, __opts, stringify); + if(!formatter || typeof formatter !== 'function') { + formatter = jsonFormatter(); } - switch (formatter) { - // Clean Formatter - case 'clean': - return [ - `${this.level.toUpperCase()}\t${this.msg}`, - this.tags.length ? `\t├→ ${this.tags.join(', ')}` : null, - Object.keys(this.meta).length ? `\t└→ ${stringify(this.meta, undefined, 4).replace(/\n/g, '\n\t┊ ')}` : null - ].filter(v => Boolean(v)).join('\n'); - - // Minimal Formatter - case 'minimal': - return `${this.level.toUpperCase()}\t${this.msg}`; - - // JSON Formatter (default) - case 'json': - default: - return this.toJSON(__opts.dev); - } + return formatter.call(this, this, __opts, stringify); } /** From 36fbdaa738fb1fa0daf046dc71eb84adc18ae55c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:28:29 -0500 Subject: [PATCH 76/83] fix: remove toJSON() method from LogMessage since it's been moved to a formatter --- src/LogMessage.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/LogMessage.ts b/src/LogMessage.ts index 003cc10..04bfb5a 100644 --- a/src/LogMessage.ts +++ b/src/LogMessage.ts @@ -230,16 +230,6 @@ export default class LogMessage implements ILogMessage { throw err; } - /** - * Returns the compiled log object converted into JSON. This method utilizes `options.replacer` for the replacer function. It also uses - * [fast-safe-stringify](https://www.npmjs.com/package/fast-safe-stringify) to prevent circular reference issues. - * @param {boolean} format Whether to format the log object with line breaks and indentation. - * @returns {string} The JSON string. - */ - toJSON(format?: boolean) { - return stringify(this.value, this.__opts.replacer ?? undefined, format ? 2 : 0); - } - /** * Converts the log to a string using a specific/custom formatter. * @returns {string} The formatted log as a string. From b1a280e0098dd5fedc1636077468cd13f31011f3 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:29:22 -0500 Subject: [PATCH 77/83] chore: remove toJSON() method from interface --- src/LogMessage.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/LogMessage.ts b/src/LogMessage.ts index 04bfb5a..49adf93 100644 --- a/src/LogMessage.ts +++ b/src/LogMessage.ts @@ -23,8 +23,6 @@ export interface ILogMessage { get value(): GenericRecord; get log(): GenericRecord; get throw(): void; - - toJSON(format: boolean): string; } /** From fd873e3a2da24f3b90985708771adeb2901ce39c Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:30:26 -0500 Subject: [PATCH 78/83] fix: explicitly set type to `string[]` for compiled tags returned from LogMessage --- src/LogMessage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LogMessage.ts b/src/LogMessage.ts index 49adf93..078f6f2 100644 --- a/src/LogMessage.ts +++ b/src/LogMessage.ts @@ -18,7 +18,7 @@ export interface ILogMessage { set message(msg: Message); get meta(): GenericRecord; set meta(obj: GenericRecord); - get tags(): Tag[]; + get tags(): string[]; set tags(tags: Tag[]); get value(): GenericRecord; get log(): GenericRecord; @@ -150,9 +150,9 @@ export default class LogMessage implements ILogMessage { * Array of tags attached to this log. Includes global tags. * @returns {Tag[]} The tags attached to this log. */ - get tags() { + get tags(): string[] { const { __opts, __tags } = this; - let tags: Tag[] = [...__tags]; + let tags = [...__tags]; if(Array.isArray(__opts.tags)) { tags = [...__opts.tags, ...tags]; @@ -177,7 +177,7 @@ export default class LogMessage implements ILogMessage { } return tag; - }).filter(tag => typeof tag === 'string' && tag); + }).filter(tag => typeof tag === 'string' && tag) as string[]; } /** From 6e6775a44bf881d3d7d64d9ce2f40ff2bce858da Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:31:08 -0500 Subject: [PATCH 79/83] fix: add generic support to LogMessage class for specifying the message type --- src/LogMessage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LogMessage.ts b/src/LogMessage.ts index 078f6f2..67a1247 100644 --- a/src/LogMessage.ts +++ b/src/LogMessage.ts @@ -30,7 +30,7 @@ export interface ILogMessage { * Having a seperate class and instance for each log allows chaining and the ability to further customize this module in the future without major breaking changes. The documentation * provided here is what is available to you for each log message. */ -export default class LogMessage implements ILogMessage { +export default class LogMessage implements ILogMessage { readonly __opts: LambdaLogOptions = {}; __level: string; __msg = ''; @@ -45,7 +45,7 @@ export default class LogMessage implements ILogMessage { * @param {LambdaLogOptions} opts The options for LambdaLog. * @class */ - constructor(log: LogObject, opts: LambdaLogOptions) { + constructor(log: LogObject, opts: LambdaLogOptions) { // LambdaLog options this.__opts = opts; // Log level From 274d5d1e5ec20d79c545108b4feb96ebe8a35d7b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:31:33 -0500 Subject: [PATCH 80/83] chore: misc cleanup and typing updates --- src/LogMessage.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/LogMessage.ts b/src/LogMessage.ts index 67a1247..d31f124 100644 --- a/src/LogMessage.ts +++ b/src/LogMessage.ts @@ -1,5 +1,5 @@ import stringify from 'fast-safe-stringify'; -import { LambdaLogOptions, Message, LogObject, Tag, GenericRecord, Formatter, StubbedError, Empty } from './typings.js'; +import { LambdaLogOptions, Message, LogObject, Tag, GenericRecord, StubbedError, Empty, FormatPlugin } from './typings.js'; import { isError, stubError } from './utils.js'; import jsonFormatter from './formatters/json.js'; @@ -93,7 +93,7 @@ export default class LogMessage implements ILogMes * Alias for `this.msg`. * @returns {string} The message for the log. */ - get message() { + get message(): string { return this.msg; } @@ -101,7 +101,7 @@ export default class LogMessage implements ILogMes * Alias for `this.msg = 'New message';` * @param {Message} msg The new message for this log. */ - set message(msg) { + set message(msg: Message) { this.msg = msg; } @@ -189,7 +189,7 @@ export default class LogMessage implements ILogMes } /** - * The full log object. This is the object used in logMessage.toJSON() and when the log is written to the console. + * The log represented as an object that is useful for stringifying or pulling data from. * @returns {GenericRecord} The compiled log object. */ get value() { @@ -232,7 +232,7 @@ export default class LogMessage implements ILogMes * Converts the log to a string using a specific/custom formatter. * @returns {string} The formatted log as a string. */ - toString() { + toString(): string { return this.formatMessage(this.__opts.onFormat); } From eac9df679f012b0ec76b9064afed323f17e13713 Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:31:50 -0500 Subject: [PATCH 81/83] test: update LogMessage tests to reflect recent changes --- src/LogMessage.spec.ts | 56 ++++++++---------------------------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/src/LogMessage.spec.ts b/src/LogMessage.spec.ts index d9e7738..9d089c0 100644 --- a/src/LogMessage.spec.ts +++ b/src/LogMessage.spec.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import 'expect-more-jest'; import LogMessage from './LogMessage'; +import * as formatters from './formatters'; import { StubbedError } from './typings'; const logData = { @@ -412,43 +413,6 @@ describe('LogMessage', () => { }); describe('Methods', () => { - describe('toJSON()', () => { - const msg = new LogMessage({ ...logData.info }, { - ...defaultOpts, - meta: { ssn: '444-55-6666' }, - replacer(key, value) { - if(key === 'ssn') { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call - return `${value.substr(0, 3)}-**-****`; - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - } - }); - - it('should return log in JSON format', () => { - expect(msg.toJSON()).toBeJsonString(); - }); - - it('should run replacer function', () => { - expect(JSON.parse(msg.toJSON()).ssn).toBe('444-**-****'); - }); - - it('should not run replacer function when not defined', () => { - const msg = new LogMessage({ ...logData.info }, { - ...defaultOpts, - meta: { ssn: '444-55-6666' } - }); - - expect(JSON.parse(msg.toJSON()).ssn).toBe('444-55-6666'); - }); - - it('should pretty print JSON when format is "true"', () => { - expect(/\n/g.test(msg.toJSON(true))).toBe(true); - }); - }); - describe('toString()', () => { it('should return log in string format', () => { const msg = new LogMessage({ ...logData.info }, defaultOpts); @@ -459,37 +423,37 @@ describe('LogMessage', () => { it('should format as json with onFormat set to `json`', () => { const msg = new LogMessage({ ...logData.info }, { ...defaultOpts, - onFormat: 'json' + onFormat: formatters.json() }); expect(msg.toString()).toBeJsonString(); }); - it('should format with "clean" template with onFormat set to `clean`', () => { + it('should format with "full" template with onFormat set to `full`', () => { const msg = new LogMessage({ ...logData.info, tags: ['test'] }, { ...defaultOpts, - onFormat: 'clean' + onFormat: formatters.full() }); - expect(msg.toString()).toMatch(/^INFO\tinfo test\n\t├→ test/); + expect(msg.toString()).toMatch(/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)\tINFO\tinfo test\n→ test/); }); - it('should not add tags when none are set in `clean` formatter', () => { + it('should not add tags when none are set in `full` formatter', () => { const msg = new LogMessage({ ...logData.info, meta: { test: 123 } }, { ...defaultOpts, - onFormat: 'clean' + onFormat: formatters.full() }); - expect(msg.toString()).toMatch(/^INFO\tinfo test\n\t└→ \{/); + expect(msg.toString()).toMatch(/^\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)\tINFO\tinfo test\n→ \{/); }); it('should format with "minimal" template with onFormat set to `minimal`', () => { const msg = new LogMessage({ ...logData.info }, { ...defaultOpts, - onFormat: 'minimal' + onFormat: formatters.minimal() }); - expect(msg.toString()).toMatch(/^INFO\tinfo test$/); + expect(msg.toString()).toMatch(/^INFO | info test$/); }); it('should format with a custom `onFormat` function', () => { From 0c3c0162be12690088f5fc1e6f4a152e77ebde5b Mon Sep 17 00:00:00 2001 From: Kyle Ross Date: Thu, 9 Dec 2021 22:32:34 -0500 Subject: [PATCH 82/83] chore: update dependencies to the latest versions --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index da5e70d..5e159f1 100644 --- a/package.json +++ b/package.json @@ -71,28 +71,28 @@ "@commitlint/config-conventional": "^15.0.0", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", - "@types/jest": "^27.0.2", - "@typescript-eslint/eslint-plugin": "^5.4.0", - "@typescript-eslint/parser": "^5.4.0", + "@types/jest": "^27.0.3", + "@typescript-eslint/eslint-plugin": "^5.6.0", + "@typescript-eslint/parser": "^5.6.0", "cross-env": "^7.0.3", "eslint": "^7.23.0", "eslint-config-xo-space": "^0.30.0", "eslint-config-xo-typescript": "^0.45.2", - "eslint-plugin-jsdoc": "^37.0.3", + "eslint-plugin-jsdoc": "^37.2.0", "eslint-plugin-node": "^11.1.0", "expect-more-jest": "^5.4.0", "husky": "^7.0.4", "is-ci": "^3.0.1", - "jest": "^27.3.1", + "jest": "^27.4.3", "jest-ts-webcompat-resolver": "^1.0.0", "rimraf": "^3.0.2", - "semantic-release": "^18.0.0", - "ts-jest": "^27.0.7", + "semantic-release": "^18.0.1", + "ts-jest": "^27.1.1", "ts-node": "^10.4.0", - "typescript": "^4.5.2" + "typescript": "^4.5.3" }, "dependencies": { - "@types/node": "^16.11.9", + "@types/node": "^16.11.12", "fast-safe-stringify": "^2.1.1" } } From 65d98c7fec5d5df54047e06bcedfcd9171604322 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 10 Dec 2021 03:35:29 +0000 Subject: [PATCH 83/83] chore(release): 4.0.0-beta.5 [skip ci] # [4.0.0-beta.5](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.4...v4.0.0-beta.5) (2021-12-10) ### Bug Fixes * add generic support to LogMessage class for specifying the message type ([6e6775a](https://github.com/KyleRoss/node-lambda-log/commit/6e6775a44bf881d3d7d64d9ce2f40ff2bce858da)) * add generic to LogObject type to specify type of Message ([cd1c746](https://github.com/KyleRoss/node-lambda-log/commit/cd1c74697a99840a48f29d49509ad3409e145de5)) * explicitly set type to `string[]` for compiled tags returned from LogMessage ([fd873e3](https://github.com/KyleRoss/node-lambda-log/commit/fd873e3a2da24f3b90985708771adeb2901ce39c)) * remove generic from Message type ([cc10311](https://github.com/KyleRoss/node-lambda-log/commit/cc10311b3e0580e840792e452d11dcc43e911544)) * remove toJSON() method from LogMessage since it's been moved to a formatter ([36fbdaa](https://github.com/KyleRoss/node-lambda-log/commit/36fbdaa738fb1fa0daf046dc71eb84adc18ae55c)) * rename the `log` method to `_log` ([7317bf5](https://github.com/KyleRoss/node-lambda-log/commit/7317bf52418af6fed65db8066b94d8c2dbe21023)) * still generate the log message just don't log it when verbosity is set ([6e13564](https://github.com/KyleRoss/node-lambda-log/commit/6e13564da87fe5b189a2b9262d8e3613a3f138e5)) * ts generics and typings on shortcut methods ([7e48904](https://github.com/KyleRoss/node-lambda-log/commit/7e48904076802adf571385f73ac83294c0d4f875)) ### Features * add `log` shortcut method as an alias for `info` to match the `console` pattern ([a3c1f16](https://github.com/KyleRoss/node-lambda-log/commit/a3c1f163976de3f68c49e05f3a84ab7c1791c0e5)) * separate built-in formatters into separate files ([f8ecac7](https://github.com/KyleRoss/node-lambda-log/commit/f8ecac7ed12b25495c634a904f489857a7413f80)) --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60e4f4d..d35f835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# [4.0.0-beta.5](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.4...v4.0.0-beta.5) (2021-12-10) + + +### Bug Fixes + +* add generic support to LogMessage class for specifying the message type ([6e6775a](https://github.com/KyleRoss/node-lambda-log/commit/6e6775a44bf881d3d7d64d9ce2f40ff2bce858da)) +* add generic to LogObject type to specify type of Message ([cd1c746](https://github.com/KyleRoss/node-lambda-log/commit/cd1c74697a99840a48f29d49509ad3409e145de5)) +* explicitly set type to `string[]` for compiled tags returned from LogMessage ([fd873e3](https://github.com/KyleRoss/node-lambda-log/commit/fd873e3a2da24f3b90985708771adeb2901ce39c)) +* remove generic from Message type ([cc10311](https://github.com/KyleRoss/node-lambda-log/commit/cc10311b3e0580e840792e452d11dcc43e911544)) +* remove toJSON() method from LogMessage since it's been moved to a formatter ([36fbdaa](https://github.com/KyleRoss/node-lambda-log/commit/36fbdaa738fb1fa0daf046dc71eb84adc18ae55c)) +* rename the `log` method to `_log` ([7317bf5](https://github.com/KyleRoss/node-lambda-log/commit/7317bf52418af6fed65db8066b94d8c2dbe21023)) +* still generate the log message just don't log it when verbosity is set ([6e13564](https://github.com/KyleRoss/node-lambda-log/commit/6e13564da87fe5b189a2b9262d8e3613a3f138e5)) +* ts generics and typings on shortcut methods ([7e48904](https://github.com/KyleRoss/node-lambda-log/commit/7e48904076802adf571385f73ac83294c0d4f875)) + + +### Features + +* add `log` shortcut method as an alias for `info` to match the `console` pattern ([a3c1f16](https://github.com/KyleRoss/node-lambda-log/commit/a3c1f163976de3f68c49e05f3a84ab7c1791c0e5)) +* separate built-in formatters into separate files ([f8ecac7](https://github.com/KyleRoss/node-lambda-log/commit/f8ecac7ed12b25495c634a904f489857a7413f80)) + # [4.0.0-beta.4](https://github.com/KyleRoss/node-lambda-log/compare/v4.0.0-beta.3...v4.0.0-beta.4) (2021-11-25) diff --git a/package.json b/package.json index f1f18b3..c72b325 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-log", - "version": "4.0.0-beta.4", + "version": "4.0.0-beta.5", "description": "Lightweight logging library for any Node 12+ applications", "main": "./index.js", "module": "./dist/esm/lambda-log.js",