From 2899014207d4a2c7ec39341c608d5158a306a1a0 Mon Sep 17 00:00:00 2001 From: commenthol Date: Sun, 9 Feb 2025 12:48:32 +0100 Subject: [PATCH 1/3] fix(node): prevent crash if color is undefined --- src/node.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/node.js b/src/node.js index 995eb7d..b1d85de 100644 --- a/src/node.js +++ b/src/node.js @@ -300,7 +300,11 @@ export class Log extends LogBase { * @private */ _color(str, color, isBold) { - return !this.opts.colors ? str : isBold ? color.bold(str) : color(str) + return !color || !this.opts?.colors + ? str + : isBold + ? color.bold(str) + : color(str) } } From 91db61026bd48fa2e28592b8197190fba493a1c9 Mon Sep 17 00:00:00 2001 From: commenthol Date: Sun, 9 Feb 2025 12:48:59 +0100 Subject: [PATCH 2/3] feat(node): ProcLog Decouple logging via process event 'log'. This allows to use a different logger framework than 'debug-level'. --- README.md | 41 ++++++++++++++++- package.json | 2 +- src/ProcLog.js | 93 ++++++++++++++++++++++++++++++++++++++ src/index.js | 3 +- test/ProcLog.test.js | 105 +++++++++++++++++++++++++++++++++++++++++++ types/ProcLog.d.ts | 50 +++++++++++++++++++++ types/index.d.ts | 4 +- 7 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 src/ProcLog.js create mode 100644 test/ProcLog.test.js create mode 100644 types/ProcLog.d.ts diff --git a/README.md b/README.md index f7ee31b..1444f5a 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Fully typed with JSDocs and Typescript. * [Settings](#settings) * [Environment Variables](#environment-variables) * [Options](#options) + * [Writing to file](#writing-to-file) * [Serializers](#serializers) * [Levels](#levels) * [Namespaces](#namespaces) @@ -46,6 +47,7 @@ Fully typed with JSDocs and Typescript. * [Wrap console logs](#wrap-console-logs) * [Wrap debug output](#wrap-debug-output) * [Handle node exit events](#handle-node-exit-events) +* [Emit Log events with ProcLog](#emit-log-events-with-proclog) * [Logging HTTP requests](#logging-http-requests) * [Logging Browser messages](#logging-browser-messages) * [Logging in Elastic Common Schema (ECS)](#logging-in-elastic-common-schema-ecs) @@ -253,7 +255,7 @@ log.debug({ object: 1 }) // ... Consider using a tool like [logrotate](https://github.com/logrotate/logrotate) to rotate the log-file. ```sh -$ node server.js 2> /var/log/server.log +$ node server.js 2> /var/log/server.log ``` To rotate the file with logrotate, add the following to `/etc/logrotate.d/server`: @@ -535,6 +537,43 @@ Log.handleExitEvents() Log.handleExitEvents('process-exit') ``` +## Emit Log events with ProcLog + +Decouple logging via process event 'log'. This allows to use a different +logger framework than 'debug-level'. In such cases you'd need to adapt your +framework of choice for logging. Check `initProcLog()` for inspiration. + +Emits the following process event: + +``` +process.emit('log', level, name, fmt, args) +``` + +where +- `level` is TRACE, DEBUG, INFO, WARN, ERROR, FATAL, LOG +- `name` is the namespace of the logger +- `fmt` is optional formatter, e.g. `%s` +- `args` is an array of arguments passed to the logger + +Only enabled namespaces emit log events. + +```js +import { ProcLog, initProcLog } from 'debug-level' + +// Initialize process event logging with 'debug-level' +// define here serializer, stream options, etc. +// If using a different logger you'd need to provide a custom initializer which +// connects to the framework of choice. +initProcLog({ serializers: {...}, Log: LogEcs }) + +// Add a logger with a namespace. +// Use options only for defining the log-level (or leave undefined to control +// via env-vars) +const log = new ProcLog('app:namespace') +// add some logging +log.info('show some logging') +``` + ## Logging HTTP requests To log http requests/ responses you can enable the `httpLogs` middleware in your diff --git a/package.json b/package.json index 8a2afa9..6b9eea7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "debug-level", - "version": "4.0.0", + "version": "4.1.0-0", "description": "debug with levels", "keywords": [ "debug", diff --git a/src/ProcLog.js b/src/ProcLog.js new file mode 100644 index 0000000..69efb8c --- /dev/null +++ b/src/ProcLog.js @@ -0,0 +1,93 @@ +import { LogBase } from './LogBase.js' +import { Log } from './node.js' +import { inspectOpts, inspectNamespaces, INFO } from './utils.js' + +/** + * @typedef {import('./node.js').LogOptions} LogOptions + */ +/** + * @typedef {LogOptions & {Log: typeof Log}} LogOptionsWithCustomLog + */ + +const EVENT = 'log' + +const defaultOptions = { + level: INFO, + namespaces: undefined +} + +/** + * Decouple logging via process event 'log'. This allows to use a different + * logger framework than 'debug-level'. In such cases you'd need to adapt your + * framework of choice for logging. Check `initProcLog()` for inspiration. + * + * Emits the following process event: + * ``` + * process.emit('log', level, name, fmt, args) + * ``` + * where + * - `level` is TRACE, DEBUG, INFO, WARN, ERROR, FATAL, LOG + * - `name` is namespace of the logger + * - `fmt` is optional formatter, e.g. `%s` + * - `args` is an array of arguments passed to the logger + * + * @example + * ```js + * import { ProcLog, initProcLog } from 'debug-level' + * + * // initialize process event logging with 'debug-level' + * // define here serializer, stream options, etc. + * initProcLog({ serializers: {...}, Log: LogEcs }) + * + * // add a logger with a namespace + * // use options only for defining the logLevel (or leave undefined to control + * // via env-vars) + * const log = new ProcLog('app:namespace') + * // add some logging + * log.info('show some logging') + * ``` + */ +export class ProcLog extends LogBase { + /** + * creates a new logger + * @param {String} name - namespace of Logger + * @param {LogOptions} [opts] - see Log.options + */ + constructor(name, opts) { + const _opts = { + ...defaultOptions, + ...inspectOpts(process.env), + ...inspectNamespaces(process.env), + ...opts, + // disallow numbers in event + levelNumbers: false, + // don't use serializers, define them in the initProcLog options + serializers: {} + } + super(name, _opts) + } + + _log(level, fmt, args) { + // @ts-expect-error + process.emit(EVENT, level, this.name, fmt, args) + } +} + +/** + * logging via process event 'log' + * @param {LogOptionsWithCustomLog} [options] + */ +export function initProcLog(options) { + const LogCls = options?.Log || Log + const logger = {} + const getLogger = (namespace) => + logger[namespace] || (logger[namespace] = new LogCls(namespace, options)) + + // prevent multiple log-lines from adding more than one listener + process.removeAllListeners(EVENT) + // listen on event + process.on(EVENT, (level, namespace, fmt, args) => { + const log = getLogger(namespace) + log[level.toLowerCase()]?.(fmt, ...args) + }) +} diff --git a/src/index.js b/src/index.js index 4f18a6c..0966485 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,8 @@ import { LogEcs } from './ecs/LogEcs.js' import { logger } from './logger.js' import { browserLogs } from './browserLogs.js' import { httpLogs } from './httpLogs.js' +import { ProcLog, initProcLog } from './ProcLog.js' export default Log -export { Log, LogEcs, logger, browserLogs, httpLogs } +export { Log, LogEcs, logger, ProcLog, initProcLog, browserLogs, httpLogs } diff --git a/test/ProcLog.test.js b/test/ProcLog.test.js new file mode 100644 index 0000000..2fde15e --- /dev/null +++ b/test/ProcLog.test.js @@ -0,0 +1,105 @@ +import assert from 'node:assert' +import { LogEcs } from '../src/index.js' +import { ProcLog, initProcLog } from '../src/ProcLog.js' + +describe('ProcLog', function () { + beforeEach(function () { + initProcLog() + }) + + it('should log via process.emit', function () { + const { lines } = myInitProcLog() + const log = new ProcLog('test:1') + log.log('a log line') + assert.deepEqual(lines, ['LOG', 'test:1', 'a log line', []]) + }) + + it('should log deep object', function () { + const { lines } = myInitProcLog() + const log = new ProcLog('test:2') + log.log({ a: { nested: 'object' } }) + assert.deepEqual(lines, [ + 'LOG', + 'test:2', + { + a: { + nested: 'object' + } + }, + [] + ]) + }) + + it('should log deep object with format', function () { + const { lines } = myInitProcLog() + const log = new ProcLog('test:2') + log.info('%j', { a: { nested: 'object' } }) + assert.deepEqual(lines, [ + 'INFO', + 'test:2', + '%j', + [ + { + a: { + nested: 'object' + } + } + ] + ]) + }) + + it('should use the Ecs logger', function () { + initProcLog({ Log: LogEcs, json: true, colors: false }) + const { lines } = myInitProcLog() + const log = new ProcLog('test:3') + log.warn('%j', { a: { nested: 'object' } }) + assert.deepEqual(lines, [ + 'WARN', + 'test:3', + '%j', + [ + { + a: { + nested: 'object' + } + } + ] + ]) + }) + + it('should not use number level or serializers', function () { + initProcLog({ + levelNumbers: true, + serializers: { + err: (err) => { + return err?.message + } + } + }) + const { lines } = myInitProcLog() + const log = new ProcLog('test:4') + log.error({ err: { name: 'Error', message: 'error' } }) + assert.deepEqual(lines, [ + 'ERROR', + 'test:4', + { + err: { + message: 'error', + name: 'Error' + } + }, + [] + ]) + }) +}) + +const myInitProcLog = () => { + let lines = [] + const reset = () => { + lines = [] + } + process.on('log', (...args) => { + lines.push(...args) + }) + return { lines, reset } +} diff --git a/types/ProcLog.d.ts b/types/ProcLog.d.ts new file mode 100644 index 0000000..9a55f65 --- /dev/null +++ b/types/ProcLog.d.ts @@ -0,0 +1,50 @@ +/** + * logging via process event 'log' + * @param {LogOptionsWithCustomLog} [options] + */ +export function initProcLog(options?: LogOptionsWithCustomLog): void; +/** + * Decouple logging via process event 'log'. This allows to use a different + * logger framework than 'debug-level'. In such cases you'd need to adapt your + * framework of choice for logging. Check `initProcLog()` for inspiration. + * + * Emits the following process event: + * ``` + * process.emit('log', level, name, fmt, args) + * ``` + * where + * - `level` is TRACE, DEBUG, INFO, WARN, ERROR, FATAL, LOG + * - `name` is namespace of the logger + * - `fmt` is optional formatter, e.g. `%s` + * - `args` is an array of arguments passed to the logger + * + * @example + * ```js + * import { ProcLog, initProcLog } from 'debug-level' + * + * // initialize process event logging with 'debug-level' + * // define here serializer, stream options, etc. + * initProcLog({ serializers: {...}, Log: LogEcs }) + * + * // add a logger with a namespace + * // use options only for defining the logLevel (or leave undefined to control + * // via env-vars) + * const log = new ProcLog('app:namespace') + * // add some logging + * log.info('show some logging') + * ``` + */ +export class ProcLog extends LogBase { + /** + * creates a new logger + * @param {String} name - namespace of Logger + * @param {LogOptions} [opts] - see Log.options + */ + constructor(name: string, opts?: LogOptions); +} +export type LogOptions = import("./node.js").LogOptions; +export type LogOptionsWithCustomLog = LogOptions & { + Log: typeof Log; +}; +import { LogBase } from './LogBase.js'; +import { Log } from './node.js'; diff --git a/types/index.d.ts b/types/index.d.ts index 2c6260d..7ad1cd2 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -12,6 +12,8 @@ export type IncomingMessageWithId = import("./httpLogs.js").IncomingMessageWithI import { Log } from './node.js'; import { LogEcs } from './ecs/LogEcs.js'; import { logger } from './logger.js'; +import { ProcLog } from './ProcLog.js'; +import { initProcLog } from './ProcLog.js'; import { browserLogs } from './browserLogs.js'; import { httpLogs } from './httpLogs.js'; -export { Log, LogEcs, logger, browserLogs, httpLogs }; +export { Log, LogEcs, logger, ProcLog, initProcLog, browserLogs, httpLogs }; From 66a67309af52e01f44b48cad9acd75abf0a85f9a Mon Sep 17 00:00:00 2001 From: commenthol Date: Sun, 9 Feb 2025 20:43:16 +0100 Subject: [PATCH 3/3] fix(node): rename proc-log event-name --- README.md | 2 +- src/ProcLog.js | 8 ++++---- src/index.js | 13 +++++++++++-- test/ProcLog.test.js | 4 ++-- types/ProcLog.d.ts | 7 +++++++ types/index.d.ts | 3 ++- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1444f5a..d032645 100644 --- a/README.md +++ b/README.md @@ -546,7 +546,7 @@ framework of choice for logging. Check `initProcLog()` for inspiration. Emits the following process event: ``` -process.emit('log', level, name, fmt, args) +process.emit('log-level', level, name, fmt, args) ``` where diff --git a/src/ProcLog.js b/src/ProcLog.js index 69efb8c..1601b22 100644 --- a/src/ProcLog.js +++ b/src/ProcLog.js @@ -9,7 +9,7 @@ import { inspectOpts, inspectNamespaces, INFO } from './utils.js' * @typedef {LogOptions & {Log: typeof Log}} LogOptionsWithCustomLog */ -const EVENT = 'log' +export const EVENT_NAME = 'log-level' const defaultOptions = { level: INFO, @@ -69,7 +69,7 @@ export class ProcLog extends LogBase { _log(level, fmt, args) { // @ts-expect-error - process.emit(EVENT, level, this.name, fmt, args) + process.emit(EVENT_NAME, level, this.name, fmt, args) } } @@ -84,9 +84,9 @@ export function initProcLog(options) { logger[namespace] || (logger[namespace] = new LogCls(namespace, options)) // prevent multiple log-lines from adding more than one listener - process.removeAllListeners(EVENT) + process.removeAllListeners(EVENT_NAME) // listen on event - process.on(EVENT, (level, namespace, fmt, args) => { + process.on(EVENT_NAME, (level, namespace, fmt, args) => { const log = getLogger(namespace) log[level.toLowerCase()]?.(fmt, ...args) }) diff --git a/src/index.js b/src/index.js index 0966485..b1e16e6 100644 --- a/src/index.js +++ b/src/index.js @@ -12,8 +12,17 @@ import { LogEcs } from './ecs/LogEcs.js' import { logger } from './logger.js' import { browserLogs } from './browserLogs.js' import { httpLogs } from './httpLogs.js' -import { ProcLog, initProcLog } from './ProcLog.js' +import { ProcLog, initProcLog, EVENT_NAME } from './ProcLog.js' export default Log -export { Log, LogEcs, logger, ProcLog, initProcLog, browserLogs, httpLogs } +export { + Log, + LogEcs, + logger, + ProcLog, + initProcLog, + EVENT_NAME, + browserLogs, + httpLogs +} diff --git a/test/ProcLog.test.js b/test/ProcLog.test.js index 2fde15e..6ca2d4e 100644 --- a/test/ProcLog.test.js +++ b/test/ProcLog.test.js @@ -1,6 +1,6 @@ import assert from 'node:assert' import { LogEcs } from '../src/index.js' -import { ProcLog, initProcLog } from '../src/ProcLog.js' +import { ProcLog, initProcLog, EVENT_NAME } from '../src/ProcLog.js' describe('ProcLog', function () { beforeEach(function () { @@ -98,7 +98,7 @@ const myInitProcLog = () => { const reset = () => { lines = [] } - process.on('log', (...args) => { + process.on(EVENT_NAME, (...args) => { lines.push(...args) }) return { lines, reset } diff --git a/types/ProcLog.d.ts b/types/ProcLog.d.ts index 9a55f65..83db385 100644 --- a/types/ProcLog.d.ts +++ b/types/ProcLog.d.ts @@ -3,6 +3,13 @@ * @param {LogOptionsWithCustomLog} [options] */ export function initProcLog(options?: LogOptionsWithCustomLog): void; +/** + * @typedef {import('./node.js').LogOptions} LogOptions + */ +/** + * @typedef {LogOptions & {Log: typeof Log}} LogOptionsWithCustomLog + */ +export const EVENT_NAME: "log-level"; /** * Decouple logging via process event 'log'. This allows to use a different * logger framework than 'debug-level'. In such cases you'd need to adapt your diff --git a/types/index.d.ts b/types/index.d.ts index 7ad1cd2..ffa8f3a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -14,6 +14,7 @@ import { LogEcs } from './ecs/LogEcs.js'; import { logger } from './logger.js'; import { ProcLog } from './ProcLog.js'; import { initProcLog } from './ProcLog.js'; +import { EVENT_NAME } from './ProcLog.js'; import { browserLogs } from './browserLogs.js'; import { httpLogs } from './httpLogs.js'; -export { Log, LogEcs, logger, ProcLog, initProcLog, browserLogs, httpLogs }; +export { Log, LogEcs, logger, ProcLog, initProcLog, EVENT_NAME, browserLogs, httpLogs };