@knowark/loggarkjs 0.3.10 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/lib/index.d.ts +6 -2
- package/lib/logger.js +77 -11
- package/lib/logger.test.js +317 -54
- package/lib/translator.test.js +24 -17
- package/package.json +3 -7
- package/.vscode/settings.json +0 -7
package/README.md
CHANGED
|
@@ -1,2 +1,52 @@
|
|
|
1
1
|
# loggark
|
|
2
2
|
Utilitarian Logging Library
|
|
3
|
+
|
|
4
|
+
## Logger output format
|
|
5
|
+
|
|
6
|
+
`Logger` now emits a single JSON object per log line by default (JSON Lines). This format is easier to consume with `journalctl` pipelines (`jq`, `grep`, field selection).
|
|
7
|
+
|
|
8
|
+
- Default: `format: 'json'`
|
|
9
|
+
- Backward compatibility: `format: 'plain'`
|
|
10
|
+
- Context flattening in JSON mode: `flat: true` (default)
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { Logger } from '@knowark/loggarkjs'
|
|
16
|
+
|
|
17
|
+
const logger = new Logger({ namespace: 'api' })
|
|
18
|
+
logger.info('User login', { userId: 'u-123' })
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Outputs:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{"timestamp":"2025-02-15T00:00:00.000Z","level":"info","namespace":"api","message":"User login","userId":"u-123"}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Additional object parameters are merged into the base log object. If keys collide, later merged values overwrite earlier keys.
|
|
28
|
+
|
|
29
|
+
If you prefer context keys at top level (for simpler `jq` filters), enable flattening:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
const logger = new Logger({
|
|
33
|
+
namespace: 'api',
|
|
34
|
+
context: { correlationId: 'ABCD1234', interactor: 'Informer' },
|
|
35
|
+
flat: true
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
logger.info('User login')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{"timestamp":"2025-02-15T00:00:00.000Z","level":"info","namespace":"api","correlationId":"ABCD1234","interactor":"Informer","message":"User login"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To keep nested context under `context`, disable flattening:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const logger = new Logger({
|
|
49
|
+
context: { correlationId: 'ABCD1234' },
|
|
50
|
+
flat: false
|
|
51
|
+
})
|
|
52
|
+
```
|
package/lib/index.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ export declare class Logger {
|
|
|
4
4
|
constructor (dependencies?: {
|
|
5
5
|
namespace?: string,
|
|
6
6
|
context?: object,
|
|
7
|
-
global?: object
|
|
7
|
+
global?: object,
|
|
8
|
+
format?: 'json' | 'plain',
|
|
9
|
+
flat?: boolean
|
|
8
10
|
})
|
|
9
11
|
|
|
10
12
|
context?: object | null
|
|
@@ -32,4 +34,6 @@ export declare class Translator {
|
|
|
32
34
|
t(key: string, options?: object): string
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
export declare function t(key: string, options?: object): string
|
|
37
|
+
export declare function t(key: string, options?: object): string
|
|
38
|
+
|
|
39
|
+
export declare function lt(key: string, options?: object): () => string
|
package/lib/logger.js
CHANGED
|
@@ -5,8 +5,11 @@ export class Logger {
|
|
|
5
5
|
|
|
6
6
|
#context = {}
|
|
7
7
|
|
|
8
|
-
constructor ({ namespace = '', context = null, global = globalThis } = {}) {
|
|
8
|
+
constructor ({ namespace = '', context = null, global = globalThis, format = 'json', flat = true } = {}) {
|
|
9
9
|
this.levels = ['error', 'warn', 'info', 'debug']
|
|
10
|
+
this.namespace = namespace
|
|
11
|
+
this.format = format
|
|
12
|
+
this.flat = flat
|
|
10
13
|
this.logvar = [namespace, 'LOGLEVEL'].filter(
|
|
11
14
|
Boolean).join('_').toUpperCase().replaceAll(' ', '_')
|
|
12
15
|
this.global = global
|
|
@@ -24,28 +27,91 @@ export class Logger {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
log (...args) {
|
|
27
|
-
if (this.logindex >= 0) log(this.global, 'log', this.context, args)
|
|
30
|
+
if (this.logindex >= 0) log(this.global, 'log', this.context, args, this.namespace, this.format, this.flat)
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
error (...args) {
|
|
31
|
-
if (this.logindex >= 0) log(this.global, 'error', this.context, args)
|
|
34
|
+
if (this.logindex >= 0) log(this.global, 'error', this.context, args, this.namespace, this.format, this.flat)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
warn (...args) {
|
|
35
|
-
if (this.logindex >= 1) log(this.global, 'warn', this.context, args)
|
|
38
|
+
if (this.logindex >= 1) log(this.global, 'warn', this.context, args, this.namespace, this.format, this.flat)
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
info (...args) {
|
|
39
|
-
if (this.logindex >= 2) log(this.global, 'info', this.context, args)
|
|
42
|
+
if (this.logindex >= 2) log(this.global, 'info', this.context, args, this.namespace, this.format, this.flat)
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
debug (...args) {
|
|
43
|
-
if (this.logindex >= 3) log(this.global, 'debug', this.context, args)
|
|
46
|
+
if (this.logindex >= 3) log(this.global, 'debug', this.context, args, this.namespace, this.format, this.flat)
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
function parseArgs (args) {
|
|
51
|
+
const payload = {}
|
|
52
|
+
|
|
53
|
+
args.forEach((value, index) => {
|
|
54
|
+
if (index === 0 && typeof value === 'string') {
|
|
55
|
+
payload.message = value
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Object.assign(payload, toLogFields(value, index))
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return payload
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toLogFields (value, index) {
|
|
66
|
+
if (value instanceof Error) return toSerializableError(value)
|
|
67
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value
|
|
68
|
+
return { [`arg${index}`]: value }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toSerializableError (error) {
|
|
72
|
+
return {
|
|
73
|
+
name: error.name,
|
|
74
|
+
message: error.message,
|
|
75
|
+
stack: error.stack,
|
|
76
|
+
...error
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stringifyJson (payload) {
|
|
81
|
+
const seen = new WeakSet()
|
|
82
|
+
return JSON.stringify(payload, (_key, value) => {
|
|
83
|
+
if (typeof value === 'bigint') return value.toString()
|
|
84
|
+
if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`
|
|
85
|
+
if (typeof value === 'symbol') return value.toString()
|
|
86
|
+
if (value instanceof Error) return toSerializableError(value)
|
|
87
|
+
|
|
88
|
+
if (value && typeof value === 'object') {
|
|
89
|
+
if (seen.has(value)) return '[Circular]'
|
|
90
|
+
seen.add(value)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function log (global, level, context, args, namespace, format, flat) {
|
|
98
|
+
const timestamp = new global.Date().toISOString()
|
|
99
|
+
|
|
100
|
+
if (format === 'plain') {
|
|
101
|
+
const message = [`${timestamp} [${level.toUpperCase()}]`]
|
|
102
|
+
if (context) message.push(stringifyJson(context))
|
|
103
|
+
message.push(...args)
|
|
104
|
+
return global.console[level](...message)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const record = {
|
|
108
|
+
...(flat && context ? context : {}),
|
|
109
|
+
timestamp,
|
|
110
|
+
level,
|
|
111
|
+
...(namespace ? { namespace } : {}),
|
|
112
|
+
...(!flat && context ? { context } : {}),
|
|
113
|
+
...parseArgs(args)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return global.console[level](stringifyJson(record))
|
|
117
|
+
}
|
package/lib/logger.test.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { it } from 'node:test'
|
|
2
3
|
import { Logger } from './logger.js'
|
|
3
4
|
|
|
4
5
|
function setup () {
|
|
@@ -11,22 +12,29 @@ function setup () {
|
|
|
11
12
|
debug (...args) { mockGlobal.debugArgs = args }
|
|
12
13
|
},
|
|
13
14
|
Date: class {
|
|
14
|
-
toISOString() {
|
|
15
|
+
toISOString () {
|
|
15
16
|
return new Date('2025-02-15').toISOString()
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
return { mockGlobal }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseRecord (args) {
|
|
25
|
+
assert.ok(args)
|
|
26
|
+
assert.strictEqual(args.length, 1)
|
|
27
|
+
return JSON.parse(args[0])
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
it('can be instantiated', () => {
|
|
23
31
|
const logger = new Logger()
|
|
24
|
-
|
|
32
|
+
assert.ok(logger)
|
|
25
33
|
})
|
|
26
34
|
|
|
27
|
-
it('
|
|
35
|
+
it('returns null context when no static or instance context is available', () => {
|
|
28
36
|
const logger = new Logger()
|
|
29
|
-
|
|
37
|
+
assert.strictEqual(logger.context, null)
|
|
30
38
|
})
|
|
31
39
|
|
|
32
40
|
it('is disabled by default', () => {
|
|
@@ -34,98 +42,353 @@ it('is disabled by default', () => {
|
|
|
34
42
|
const logger = new Logger({ global: mockGlobal })
|
|
35
43
|
|
|
36
44
|
logger.log('Logging something...')
|
|
37
|
-
|
|
45
|
+
assert.ok(!mockGlobal.logArgs)
|
|
38
46
|
|
|
39
47
|
logger.error('Logging something...')
|
|
40
|
-
|
|
48
|
+
assert.ok(!mockGlobal.errorArgs)
|
|
41
49
|
|
|
42
50
|
logger.warn('Logging something...')
|
|
43
|
-
|
|
51
|
+
assert.ok(!mockGlobal.warnArgs)
|
|
44
52
|
|
|
45
53
|
logger.info('Logging something...')
|
|
46
|
-
|
|
54
|
+
assert.ok(!mockGlobal.infoArgs)
|
|
47
55
|
|
|
48
56
|
logger.debug('Logging something...')
|
|
49
|
-
|
|
57
|
+
assert.ok(!mockGlobal.debugArgs)
|
|
50
58
|
})
|
|
51
59
|
|
|
52
|
-
it('
|
|
60
|
+
it('emits JSON logs by default according to loglevel', () => {
|
|
53
61
|
const { mockGlobal } = setup()
|
|
54
62
|
mockGlobal.LOGLEVEL = 'debug'
|
|
55
63
|
const logger = new Logger({ global: mockGlobal })
|
|
56
64
|
|
|
57
65
|
logger.log('Logging something...')
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.logArgs), {
|
|
67
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
68
|
+
level: 'log',
|
|
69
|
+
message: 'Logging something...'
|
|
70
|
+
})
|
|
60
71
|
|
|
61
72
|
logger.error('Logging something...')
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.errorArgs), {
|
|
74
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
75
|
+
level: 'error',
|
|
76
|
+
message: 'Logging something...'
|
|
77
|
+
})
|
|
64
78
|
|
|
65
79
|
logger.warn('Logging something...')
|
|
66
|
-
|
|
67
|
-
|
|
80
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.warnArgs), {
|
|
81
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
82
|
+
level: 'warn',
|
|
83
|
+
message: 'Logging something...'
|
|
84
|
+
})
|
|
68
85
|
|
|
69
86
|
logger.info('Logging something...')
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
88
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
89
|
+
level: 'info',
|
|
90
|
+
message: 'Logging something...'
|
|
91
|
+
})
|
|
72
92
|
|
|
73
93
|
logger.debug('Logging something...')
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.debugArgs), {
|
|
95
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
96
|
+
level: 'debug',
|
|
97
|
+
message: 'Logging something...'
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('merges object parameters into the base JSON log object', () => {
|
|
102
|
+
const { mockGlobal } = setup()
|
|
103
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
104
|
+
const logger = new Logger({ global: mockGlobal })
|
|
105
|
+
|
|
106
|
+
logger.info('Logging something...', { action: 'create', entity: 'product' })
|
|
107
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
108
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
109
|
+
level: 'info',
|
|
110
|
+
message: 'Logging something...',
|
|
111
|
+
action: 'create',
|
|
112
|
+
entity: 'product'
|
|
113
|
+
})
|
|
76
114
|
})
|
|
77
115
|
|
|
78
|
-
it('
|
|
116
|
+
it('can emit JSON logs without message arguments', () => {
|
|
117
|
+
const { mockGlobal } = setup()
|
|
118
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
119
|
+
const logger = new Logger({ global: mockGlobal })
|
|
120
|
+
|
|
121
|
+
logger.info()
|
|
122
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
123
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
124
|
+
level: 'info'
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('can emit JSON logs without context when flat is disabled', () => {
|
|
129
|
+
const { mockGlobal } = setup()
|
|
130
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
131
|
+
const logger = new Logger({ global: mockGlobal, flat: false })
|
|
132
|
+
|
|
133
|
+
logger.info('Logging something...')
|
|
134
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
135
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
136
|
+
level: 'info',
|
|
137
|
+
message: 'Logging something...'
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('flattens provided logging context labels by default', () => {
|
|
79
142
|
const { mockGlobal } = setup()
|
|
80
143
|
mockGlobal.LOGLEVEL = 'debug'
|
|
81
144
|
const context = { correlationId: 'ABCD1234', interactor: 'Informer' }
|
|
82
145
|
const logger = new Logger({ global: mockGlobal, context })
|
|
83
146
|
|
|
84
|
-
logger.
|
|
85
|
-
|
|
86
|
-
'2025-02-15T00:00:00.000Z
|
|
87
|
-
|
|
88
|
-
|
|
147
|
+
logger.info('Logging something...')
|
|
148
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
149
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
150
|
+
level: 'info',
|
|
151
|
+
correlationId: 'ABCD1234',
|
|
152
|
+
interactor: 'Informer',
|
|
153
|
+
message: 'Logging something...'
|
|
154
|
+
})
|
|
155
|
+
})
|
|
89
156
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
157
|
+
it('can flatten context labels into top-level JSON fields with flat option', () => {
|
|
158
|
+
const { mockGlobal } = setup()
|
|
159
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
160
|
+
const context = { correlationId: 'ABCD1234', interactor: 'Informer' }
|
|
161
|
+
const logger = new Logger({ global: mockGlobal, context, flat: true })
|
|
95
162
|
|
|
96
|
-
logger.
|
|
97
|
-
|
|
98
|
-
'2025-02-15T00:00:00.000Z
|
|
99
|
-
|
|
100
|
-
|
|
163
|
+
logger.info('Logging something...')
|
|
164
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
165
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
166
|
+
level: 'info',
|
|
167
|
+
correlationId: 'ABCD1234',
|
|
168
|
+
interactor: 'Informer',
|
|
169
|
+
message: 'Logging something...'
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('can keep nested context when flat option is disabled', () => {
|
|
174
|
+
const { mockGlobal } = setup()
|
|
175
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
176
|
+
const context = { correlationId: 'ABCD1234', interactor: 'Informer' }
|
|
177
|
+
const logger = new Logger({ global: mockGlobal, context, flat: false })
|
|
101
178
|
|
|
102
179
|
logger.info('Logging something...')
|
|
103
|
-
|
|
104
|
-
'2025-02-15T00:00:00.000Z
|
|
105
|
-
|
|
106
|
-
|
|
180
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
181
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
182
|
+
level: 'info',
|
|
183
|
+
context,
|
|
184
|
+
message: 'Logging something...'
|
|
185
|
+
})
|
|
186
|
+
})
|
|
107
187
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
188
|
+
it('keeps log metadata and payload fields over flat context collisions', () => {
|
|
189
|
+
const { mockGlobal } = setup()
|
|
190
|
+
mockGlobal.CORE_LOGLEVEL = 'info'
|
|
191
|
+
const context = {
|
|
192
|
+
timestamp: 'old',
|
|
193
|
+
level: 'fatal',
|
|
194
|
+
namespace: 'context-namespace',
|
|
195
|
+
message: 'context message',
|
|
196
|
+
args: ['context args'],
|
|
197
|
+
correlationId: 'ABCD1234'
|
|
198
|
+
}
|
|
199
|
+
const logger = new Logger({
|
|
200
|
+
global: mockGlobal,
|
|
201
|
+
namespace: 'core',
|
|
202
|
+
context,
|
|
203
|
+
flat: true
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
logger.info('real message', 'value')
|
|
207
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
208
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
209
|
+
level: 'info',
|
|
210
|
+
namespace: 'core',
|
|
211
|
+
message: 'real message',
|
|
212
|
+
args: ['context args'],
|
|
213
|
+
correlationId: 'ABCD1234',
|
|
214
|
+
arg1: 'value'
|
|
215
|
+
})
|
|
113
216
|
})
|
|
114
217
|
|
|
115
218
|
it('supports including static context labels', () => {
|
|
219
|
+
const originalContext = Logger.context
|
|
116
220
|
Logger.context = () => {
|
|
117
221
|
return { correlationId: 'ABCD1234' }
|
|
118
222
|
}
|
|
119
223
|
|
|
224
|
+
try {
|
|
225
|
+
const { mockGlobal } = setup()
|
|
226
|
+
mockGlobal.LOGLEVEL = 'debug'
|
|
227
|
+
const context = { model: 'Product' }
|
|
228
|
+
const logger = new Logger({ global: mockGlobal, context })
|
|
229
|
+
|
|
230
|
+
logger.info('Logging something...')
|
|
231
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
232
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
233
|
+
level: 'info',
|
|
234
|
+
correlationId: 'ABCD1234',
|
|
235
|
+
model: 'Product',
|
|
236
|
+
message: 'Logging something...'
|
|
237
|
+
})
|
|
238
|
+
} finally {
|
|
239
|
+
Logger.context = originalContext
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('includes namespace in JSON output and uses the namespace loglevel variable', () => {
|
|
120
244
|
const { mockGlobal } = setup()
|
|
121
|
-
mockGlobal.
|
|
122
|
-
const
|
|
123
|
-
const logger = new Logger({ global: mockGlobal, context })
|
|
245
|
+
mockGlobal.CORE_LOGLEVEL = 'info'
|
|
246
|
+
const logger = new Logger({ global: mockGlobal, namespace: 'core' })
|
|
124
247
|
|
|
125
248
|
logger.info('Logging something...')
|
|
126
|
-
|
|
249
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
250
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
251
|
+
level: 'info',
|
|
252
|
+
namespace: 'core',
|
|
253
|
+
message: 'Logging something...'
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('merges non-string object payloads into the JSON record', () => {
|
|
258
|
+
const { mockGlobal } = setup()
|
|
259
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
260
|
+
const logger = new Logger({ global: mockGlobal })
|
|
261
|
+
|
|
262
|
+
logger.info({ code: 'E_UNEXPECTED' })
|
|
263
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
264
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
265
|
+
level: 'info',
|
|
266
|
+
code: 'E_UNEXPECTED'
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('stores null payloads as positional fields in JSON format', () => {
|
|
271
|
+
const { mockGlobal } = setup()
|
|
272
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
273
|
+
const logger = new Logger({ global: mockGlobal })
|
|
274
|
+
|
|
275
|
+
logger.info(null)
|
|
276
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
277
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
278
|
+
level: 'info',
|
|
279
|
+
arg0: null
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('serializes Error payloads in JSON format', () => {
|
|
284
|
+
const { mockGlobal } = setup()
|
|
285
|
+
mockGlobal.LOGLEVEL = 'error'
|
|
286
|
+
const logger = new Logger({ global: mockGlobal })
|
|
287
|
+
const error = new Error('Unexpected failure')
|
|
288
|
+
error.code = 'E_UNEXPECTED'
|
|
289
|
+
|
|
290
|
+
logger.error(error)
|
|
291
|
+
const record = parseRecord(mockGlobal.errorArgs)
|
|
292
|
+
|
|
293
|
+
assert.strictEqual(record.level, 'error')
|
|
294
|
+
assert.strictEqual(record.name, 'Error')
|
|
295
|
+
assert.strictEqual(record.message, 'Unexpected failure')
|
|
296
|
+
assert.strictEqual(record.code, 'E_UNEXPECTED')
|
|
297
|
+
assert.match(record.stack, /Unexpected failure/)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('serializes nested Error values inside object payloads', () => {
|
|
301
|
+
const { mockGlobal } = setup()
|
|
302
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
303
|
+
const logger = new Logger({ global: mockGlobal })
|
|
304
|
+
const nested = new Error('Nested failure')
|
|
305
|
+
nested.code = 'E_NESTED'
|
|
306
|
+
|
|
307
|
+
logger.info({ nested })
|
|
308
|
+
const record = parseRecord(mockGlobal.infoArgs)
|
|
309
|
+
|
|
310
|
+
assert.strictEqual(record.nested.name, 'Error')
|
|
311
|
+
assert.strictEqual(record.nested.message, 'Nested failure')
|
|
312
|
+
assert.strictEqual(record.nested.code, 'E_NESTED')
|
|
313
|
+
assert.match(record.nested.stack, /Nested failure/)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('lets additional payload fields overwrite previously defined keys', () => {
|
|
317
|
+
const { mockGlobal } = setup()
|
|
318
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
319
|
+
const logger = new Logger({ global: mockGlobal })
|
|
320
|
+
const error = new TypeError('Bad input')
|
|
321
|
+
error.field = 'name'
|
|
322
|
+
|
|
323
|
+
logger.info('Validation failed', error)
|
|
324
|
+
const record = parseRecord(mockGlobal.infoArgs)
|
|
325
|
+
|
|
326
|
+
assert.strictEqual(record.message, 'Bad input')
|
|
327
|
+
assert.strictEqual(record.name, 'TypeError')
|
|
328
|
+
assert.strictEqual(record.field, 'name')
|
|
329
|
+
assert.match(record.stack, /Bad input/)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('serializes special values and circular references in merged JSON fields', () => {
|
|
333
|
+
const { mockGlobal } = setup()
|
|
334
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
335
|
+
const logger = new Logger({ global: mockGlobal })
|
|
336
|
+
const payload = {
|
|
337
|
+
count: 10n,
|
|
338
|
+
token: Symbol('auth')
|
|
339
|
+
}
|
|
340
|
+
payload.self = payload
|
|
341
|
+
|
|
342
|
+
logger.info('Special payload', (function () {}), function namedFn () {}, payload)
|
|
343
|
+
const record = parseRecord(mockGlobal.infoArgs)
|
|
344
|
+
|
|
345
|
+
assert.strictEqual(record.message, 'Special payload')
|
|
346
|
+
assert.strictEqual(record.arg1, '[Function anonymous]')
|
|
347
|
+
assert.strictEqual(record.arg2, '[Function namedFn]')
|
|
348
|
+
assert.strictEqual(record.count, '10')
|
|
349
|
+
assert.strictEqual(record.token, 'Symbol(auth)')
|
|
350
|
+
assert.strictEqual(record.self.count, '10')
|
|
351
|
+
assert.strictEqual(record.self.token, 'Symbol(auth)')
|
|
352
|
+
assert.strictEqual(record.self.self, '[Circular]')
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('stores array payloads as positional fields in JSON format', () => {
|
|
356
|
+
const { mockGlobal } = setup()
|
|
357
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
358
|
+
const logger = new Logger({ global: mockGlobal })
|
|
359
|
+
|
|
360
|
+
logger.info('Array payload', [1, 2, 3])
|
|
361
|
+
assert.deepStrictEqual(parseRecord(mockGlobal.infoArgs), {
|
|
362
|
+
timestamp: '2025-02-15T00:00:00.000Z',
|
|
363
|
+
level: 'info',
|
|
364
|
+
message: 'Array payload',
|
|
365
|
+
arg1: [1, 2, 3]
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('supports plain formatting for backward compatibility', () => {
|
|
370
|
+
const { mockGlobal } = setup()
|
|
371
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
372
|
+
const context = { correlationId: 'ABCD1234' }
|
|
373
|
+
const logger = new Logger({ global: mockGlobal, context, format: 'plain' })
|
|
374
|
+
|
|
375
|
+
logger.info('Logging something...', 0, false)
|
|
376
|
+
assert.deepStrictEqual(mockGlobal.infoArgs, [
|
|
127
377
|
'2025-02-15T00:00:00.000Z [INFO]',
|
|
128
|
-
JSON.stringify(
|
|
129
|
-
'Logging something...'
|
|
378
|
+
JSON.stringify(context),
|
|
379
|
+
'Logging something...',
|
|
380
|
+
0,
|
|
381
|
+
false
|
|
382
|
+
])
|
|
383
|
+
})
|
|
130
384
|
|
|
131
|
-
|
|
385
|
+
it('supports plain formatting without context labels', () => {
|
|
386
|
+
const { mockGlobal } = setup()
|
|
387
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
388
|
+
const logger = new Logger({ global: mockGlobal, format: 'plain' })
|
|
389
|
+
|
|
390
|
+
logger.info()
|
|
391
|
+
assert.deepStrictEqual(mockGlobal.infoArgs, [
|
|
392
|
+
'2025-02-15T00:00:00.000Z [INFO]'
|
|
393
|
+
])
|
|
394
|
+
})
|
package/lib/translator.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { it } from 'node:test'
|
|
2
3
|
import { Translator, t, lt } from './translator.js'
|
|
3
4
|
|
|
4
5
|
it('can be instantiated', () => {
|
|
5
6
|
const translator = new Translator()
|
|
6
|
-
|
|
7
|
+
assert.ok(translator)
|
|
7
8
|
})
|
|
8
9
|
|
|
9
10
|
it('defines a translate function with options', () => {
|
|
@@ -11,7 +12,7 @@ it('defines a translate function with options', () => {
|
|
|
11
12
|
|
|
12
13
|
const result = translator.translate('any.key')
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
assert.strictEqual(result, 'any.key')
|
|
15
16
|
})
|
|
16
17
|
|
|
17
18
|
it('defines a t shortcut for function translate', () => {
|
|
@@ -19,30 +20,36 @@ it('defines a t shortcut for function translate', () => {
|
|
|
19
20
|
|
|
20
21
|
const result = t('any.key')
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
assert.strictEqual(result, 'any.key')
|
|
23
24
|
})
|
|
24
25
|
|
|
25
26
|
it('defines a global t shortcut translate', () => {
|
|
26
27
|
const originalTranslate = Translator.translate
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
try {
|
|
29
|
+
Translator.translate = (key, _options) => {
|
|
30
|
+
return `translated:${key}`
|
|
31
|
+
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const result = t('any.key')
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
assert.strictEqual(result, 'translated:any.key')
|
|
36
|
+
} finally {
|
|
37
|
+
Translator.translate = originalTranslate
|
|
38
|
+
}
|
|
35
39
|
})
|
|
36
40
|
|
|
37
41
|
it('defines a global lt lazy-translation function', () => {
|
|
38
42
|
const originalTranslate = Translator.translate
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
try {
|
|
44
|
+
Translator.translate = (key, _options) => {
|
|
45
|
+
return `translated:${key}`
|
|
46
|
+
}
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
const result = lt('any.key')
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
assert.ok(result instanceof Function)
|
|
51
|
+
assert.strictEqual(result(), 'translated:any.key')
|
|
52
|
+
} finally {
|
|
53
|
+
Translator.translate = originalTranslate
|
|
54
|
+
}
|
|
48
55
|
})
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knowark/loggarkjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Utilitarian Logging Library",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
9
|
-
"test": "npm run options -- npx jest --coverage"
|
|
8
|
+
"test": "node --test --experimental-test-coverage --test-coverage-exclude='**/*.test.js'"
|
|
10
9
|
},
|
|
11
10
|
"repository": {
|
|
12
11
|
"type": "git",
|
|
@@ -21,8 +20,5 @@
|
|
|
21
20
|
"bugs": {
|
|
22
21
|
"url": "https://github.com/knowark/loggarkjs/issues"
|
|
23
22
|
},
|
|
24
|
-
"homepage": "https://github.com/knowark/loggarkjs#readme"
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"jest": "^29.7.0"
|
|
27
|
-
}
|
|
23
|
+
"homepage": "https://github.com/knowark/loggarkjs#readme"
|
|
28
24
|
}
|