Skip to content

Commit 91db610

Browse files
committed
feat(node): ProcLog
Decouple logging via process event 'log'. This allows to use a different logger framework than 'debug-level'.
1 parent 2899014 commit 91db610

File tree

7 files changed

+294
-4
lines changed

7 files changed

+294
-4
lines changed

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Fully typed with JSDocs and Typescript.
3535
* [Settings](#settings)
3636
* [Environment Variables](#environment-variables)
3737
* [Options](#options)
38+
* [Writing to file](#writing-to-file)
3839
* [Serializers](#serializers)
3940
* [Levels](#levels)
4041
* [Namespaces](#namespaces)
@@ -46,6 +47,7 @@ Fully typed with JSDocs and Typescript.
4647
* [Wrap console logs](#wrap-console-logs)
4748
* [Wrap debug output](#wrap-debug-output)
4849
* [Handle node exit events](#handle-node-exit-events)
50+
* [Emit Log events with ProcLog](#emit-log-events-with-proclog)
4951
* [Logging HTTP requests](#logging-http-requests)
5052
* [Logging Browser messages](#logging-browser-messages)
5153
* [Logging in Elastic Common Schema (ECS)](#logging-in-elastic-common-schema-ecs)
@@ -253,7 +255,7 @@ log.debug({ object: 1 }) // ...
253255
Consider using a tool like [logrotate](https://github.com/logrotate/logrotate) to rotate the log-file.
254256

255257
```sh
256-
$ node server.js 2> /var/log/server.log
258+
$ node server.js 2> /var/log/server.log
257259
```
258260

259261
To rotate the file with logrotate, add the following to `/etc/logrotate.d/server`:
@@ -535,6 +537,43 @@ Log.handleExitEvents()
535537
Log.handleExitEvents('process-exit')
536538
```
537539

540+
## Emit Log events with ProcLog
541+
542+
Decouple logging via process event 'log'. This allows to use a different
543+
logger framework than 'debug-level'. In such cases you'd need to adapt your
544+
framework of choice for logging. Check `initProcLog()` for inspiration.
545+
546+
Emits the following process event:
547+
548+
```
549+
process.emit('log', level, name, fmt, args)
550+
```
551+
552+
where
553+
- `level` is TRACE, DEBUG, INFO, WARN, ERROR, FATAL, LOG
554+
- `name` is the namespace of the logger
555+
- `fmt` is optional formatter, e.g. `%s`
556+
- `args` is an array of arguments passed to the logger
557+
558+
Only enabled namespaces emit log events.
559+
560+
```js
561+
import { ProcLog, initProcLog } from 'debug-level'
562+
563+
// Initialize process event logging with 'debug-level'
564+
// define here serializer, stream options, etc.
565+
// If using a different logger you'd need to provide a custom initializer which
566+
// connects to the framework of choice.
567+
initProcLog({ serializers: {...}, Log: LogEcs })
568+
569+
// Add a logger with a namespace.
570+
// Use options only for defining the log-level (or leave undefined to control
571+
// via env-vars)
572+
const log = new ProcLog('app:namespace')
573+
// add some logging
574+
log.info('show some logging')
575+
```
576+
538577
## Logging HTTP requests
539578

540579
To log http requests/ responses you can enable the `httpLogs` middleware in your

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "debug-level",
3-
"version": "4.0.0",
3+
"version": "4.1.0-0",
44
"description": "debug with levels",
55
"keywords": [
66
"debug",

src/ProcLog.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { LogBase } from './LogBase.js'
2+
import { Log } from './node.js'
3+
import { inspectOpts, inspectNamespaces, INFO } from './utils.js'
4+
5+
/**
6+
* @typedef {import('./node.js').LogOptions} LogOptions
7+
*/
8+
/**
9+
* @typedef {LogOptions & {Log: typeof Log}} LogOptionsWithCustomLog
10+
*/
11+
12+
const EVENT = 'log'
13+
14+
const defaultOptions = {
15+
level: INFO,
16+
namespaces: undefined
17+
}
18+
19+
/**
20+
* Decouple logging via process event 'log'. This allows to use a different
21+
* logger framework than 'debug-level'. In such cases you'd need to adapt your
22+
* framework of choice for logging. Check `initProcLog()` for inspiration.
23+
*
24+
* Emits the following process event:
25+
* ```
26+
* process.emit('log', level, name, fmt, args)
27+
* ```
28+
* where
29+
* - `level` is TRACE, DEBUG, INFO, WARN, ERROR, FATAL, LOG
30+
* - `name` is namespace of the logger
31+
* - `fmt` is optional formatter, e.g. `%s`
32+
* - `args` is an array of arguments passed to the logger
33+
*
34+
* @example
35+
* ```js
36+
* import { ProcLog, initProcLog } from 'debug-level'
37+
*
38+
* // initialize process event logging with 'debug-level'
39+
* // define here serializer, stream options, etc.
40+
* initProcLog({ serializers: {...}, Log: LogEcs })
41+
*
42+
* // add a logger with a namespace
43+
* // use options only for defining the logLevel (or leave undefined to control
44+
* // via env-vars)
45+
* const log = new ProcLog('app:namespace')
46+
* // add some logging
47+
* log.info('show some logging')
48+
* ```
49+
*/
50+
export class ProcLog extends LogBase {
51+
/**
52+
* creates a new logger
53+
* @param {String} name - namespace of Logger
54+
* @param {LogOptions} [opts] - see Log.options
55+
*/
56+
constructor(name, opts) {
57+
const _opts = {
58+
...defaultOptions,
59+
...inspectOpts(process.env),
60+
...inspectNamespaces(process.env),
61+
...opts,
62+
// disallow numbers in event
63+
levelNumbers: false,
64+
// don't use serializers, define them in the initProcLog options
65+
serializers: {}
66+
}
67+
super(name, _opts)
68+
}
69+
70+
_log(level, fmt, args) {
71+
// @ts-expect-error
72+
process.emit(EVENT, level, this.name, fmt, args)
73+
}
74+
}
75+
76+
/**
77+
* logging via process event 'log'
78+
* @param {LogOptionsWithCustomLog} [options]
79+
*/
80+
export function initProcLog(options) {
81+
const LogCls = options?.Log || Log
82+
const logger = {}
83+
const getLogger = (namespace) =>
84+
logger[namespace] || (logger[namespace] = new LogCls(namespace, options))
85+
86+
// prevent multiple log-lines from adding more than one listener
87+
process.removeAllListeners(EVENT)
88+
// listen on event
89+
process.on(EVENT, (level, namespace, fmt, args) => {
90+
const log = getLogger(namespace)
91+
log[level.toLowerCase()]?.(fmt, ...args)
92+
})
93+
}

src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { LogEcs } from './ecs/LogEcs.js'
1212
import { logger } from './logger.js'
1313
import { browserLogs } from './browserLogs.js'
1414
import { httpLogs } from './httpLogs.js'
15+
import { ProcLog, initProcLog } from './ProcLog.js'
1516

1617
export default Log
1718

18-
export { Log, LogEcs, logger, browserLogs, httpLogs }
19+
export { Log, LogEcs, logger, ProcLog, initProcLog, browserLogs, httpLogs }

test/ProcLog.test.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import assert from 'node:assert'
2+
import { LogEcs } from '../src/index.js'
3+
import { ProcLog, initProcLog } from '../src/ProcLog.js'
4+
5+
describe('ProcLog', function () {
6+
beforeEach(function () {
7+
initProcLog()
8+
})
9+
10+
it('should log via process.emit', function () {
11+
const { lines } = myInitProcLog()
12+
const log = new ProcLog('test:1')
13+
log.log('a log line')
14+
assert.deepEqual(lines, ['LOG', 'test:1', 'a log line', []])
15+
})
16+
17+
it('should log deep object', function () {
18+
const { lines } = myInitProcLog()
19+
const log = new ProcLog('test:2')
20+
log.log({ a: { nested: 'object' } })
21+
assert.deepEqual(lines, [
22+
'LOG',
23+
'test:2',
24+
{
25+
a: {
26+
nested: 'object'
27+
}
28+
},
29+
[]
30+
])
31+
})
32+
33+
it('should log deep object with format', function () {
34+
const { lines } = myInitProcLog()
35+
const log = new ProcLog('test:2')
36+
log.info('%j', { a: { nested: 'object' } })
37+
assert.deepEqual(lines, [
38+
'INFO',
39+
'test:2',
40+
'%j',
41+
[
42+
{
43+
a: {
44+
nested: 'object'
45+
}
46+
}
47+
]
48+
])
49+
})
50+
51+
it('should use the Ecs logger', function () {
52+
initProcLog({ Log: LogEcs, json: true, colors: false })
53+
const { lines } = myInitProcLog()
54+
const log = new ProcLog('test:3')
55+
log.warn('%j', { a: { nested: 'object' } })
56+
assert.deepEqual(lines, [
57+
'WARN',
58+
'test:3',
59+
'%j',
60+
[
61+
{
62+
a: {
63+
nested: 'object'
64+
}
65+
}
66+
]
67+
])
68+
})
69+
70+
it('should not use number level or serializers', function () {
71+
initProcLog({
72+
levelNumbers: true,
73+
serializers: {
74+
err: (err) => {
75+
return err?.message
76+
}
77+
}
78+
})
79+
const { lines } = myInitProcLog()
80+
const log = new ProcLog('test:4')
81+
log.error({ err: { name: 'Error', message: 'error' } })
82+
assert.deepEqual(lines, [
83+
'ERROR',
84+
'test:4',
85+
{
86+
err: {
87+
message: 'error',
88+
name: 'Error'
89+
}
90+
},
91+
[]
92+
])
93+
})
94+
})
95+
96+
const myInitProcLog = () => {
97+
let lines = []
98+
const reset = () => {
99+
lines = []
100+
}
101+
process.on('log', (...args) => {
102+
lines.push(...args)
103+
})
104+
return { lines, reset }
105+
}

types/ProcLog.d.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* logging via process event 'log'
3+
* @param {LogOptionsWithCustomLog} [options]
4+
*/
5+
export function initProcLog(options?: LogOptionsWithCustomLog): void;
6+
/**
7+
* Decouple logging via process event 'log'. This allows to use a different
8+
* logger framework than 'debug-level'. In such cases you'd need to adapt your
9+
* framework of choice for logging. Check `initProcLog()` for inspiration.
10+
*
11+
* Emits the following process event:
12+
* ```
13+
* process.emit('log', level, name, fmt, args)
14+
* ```
15+
* where
16+
* - `level` is TRACE, DEBUG, INFO, WARN, ERROR, FATAL, LOG
17+
* - `name` is namespace of the logger
18+
* - `fmt` is optional formatter, e.g. `%s`
19+
* - `args` is an array of arguments passed to the logger
20+
*
21+
* @example
22+
* ```js
23+
* import { ProcLog, initProcLog } from 'debug-level'
24+
*
25+
* // initialize process event logging with 'debug-level'
26+
* // define here serializer, stream options, etc.
27+
* initProcLog({ serializers: {...}, Log: LogEcs })
28+
*
29+
* // add a logger with a namespace
30+
* // use options only for defining the logLevel (or leave undefined to control
31+
* // via env-vars)
32+
* const log = new ProcLog('app:namespace')
33+
* // add some logging
34+
* log.info('show some logging')
35+
* ```
36+
*/
37+
export class ProcLog extends LogBase {
38+
/**
39+
* creates a new logger
40+
* @param {String} name - namespace of Logger
41+
* @param {LogOptions} [opts] - see Log.options
42+
*/
43+
constructor(name: string, opts?: LogOptions);
44+
}
45+
export type LogOptions = import("./node.js").LogOptions;
46+
export type LogOptionsWithCustomLog = LogOptions & {
47+
Log: typeof Log;
48+
};
49+
import { LogBase } from './LogBase.js';
50+
import { Log } from './node.js';

types/index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export type IncomingMessageWithId = import("./httpLogs.js").IncomingMessageWithI
1212
import { Log } from './node.js';
1313
import { LogEcs } from './ecs/LogEcs.js';
1414
import { logger } from './logger.js';
15+
import { ProcLog } from './ProcLog.js';
16+
import { initProcLog } from './ProcLog.js';
1517
import { browserLogs } from './browserLogs.js';
1618
import { httpLogs } from './httpLogs.js';
17-
export { Log, LogEcs, logger, browserLogs, httpLogs };
19+
export { Log, LogEcs, logger, ProcLog, initProcLog, browserLogs, httpLogs };

0 commit comments

Comments
 (0)