Skip to content

Commit

Permalink
Merge pull request #29 from commenthol/feat-proclog
Browse files Browse the repository at this point in the history
Feature: ProcLog
  • Loading branch information
commenthol authored Feb 9, 2025
2 parents 177b0ce + 66a6730 commit 016f14d
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 5 deletions.
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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`:
Expand Down Expand Up @@ -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', 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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "debug-level",
"version": "4.0.0",
"version": "4.1.0-0",
"description": "debug with levels",
"keywords": [
"debug",
Expand Down
93 changes: 93 additions & 0 deletions src/ProcLog.js
Original file line number Diff line number Diff line change
@@ -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
*/

export const EVENT_NAME = 'log-level'

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_NAME, 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_NAME)
// listen on event
process.on(EVENT_NAME, (level, namespace, fmt, args) => {
const log = getLogger(namespace)
log[level.toLowerCase()]?.(fmt, ...args)
})
}
12 changes: 11 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +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, EVENT_NAME } from './ProcLog.js'

export default Log

export { Log, LogEcs, logger, browserLogs, httpLogs }
export {
Log,
LogEcs,
logger,
ProcLog,
initProcLog,
EVENT_NAME,
browserLogs,
httpLogs
}
6 changes: 5 additions & 1 deletion src/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
105 changes: 105 additions & 0 deletions test/ProcLog.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import assert from 'node:assert'
import { LogEcs } from '../src/index.js'
import { ProcLog, initProcLog, EVENT_NAME } 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(EVENT_NAME, (...args) => {
lines.push(...args)
})
return { lines, reset }
}
57 changes: 57 additions & 0 deletions types/ProcLog.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* logging via process event 'log'
* @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
* 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';
5 changes: 4 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ 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 { EVENT_NAME } 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, EVENT_NAME, browserLogs, httpLogs };

0 comments on commit 016f14d

Please sign in to comment.