@knowark/loggarkjs 0.4.0 → 0.5.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 +13 -0
- package/lib/logger.js +88 -13
- package/lib/logger.test.js +200 -2
- package/lib/utilities/journald-socket.js +51 -0
- package/lib/utilities/journald-socket.test.js +105 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,3 +50,16 @@ const logger = new Logger({
|
|
|
50
50
|
flat: false
|
|
51
51
|
})
|
|
52
52
|
```
|
|
53
|
+
|
|
54
|
+
## Journald integration
|
|
55
|
+
|
|
56
|
+
Set the boolean `globalThis.JOURNALD` flag to `true` and `Logger` will open `/run/systemd/journal/socket` for you. Every JSON record is translated into Journald fields (`PRIORITY`, `MESSAGE`, `LEVEL`, optional `SYSLOG_IDENTIFIER`, and uppercased context payloads), so you can pipe `logger.info()` calls directly into `journalctl` for centralized viewing.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
globalThis.JOURNALD = true
|
|
60
|
+
const logger = new Logger({ namespace: 'api' })
|
|
61
|
+
|
|
62
|
+
logger.info('User login', { userId: 'u-123', extra: undefined })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Undefined values are dropped, `null` turns into the string `'null'`, and functions/symbols are rendered so Journald receives a predictable payload. The socket connection is cached per process, so later `Logger` instances reuse the same transport without extra setup.
|
package/lib/logger.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { journaldSocket } from './utilities/journald-socket.js'
|
|
2
|
+
|
|
1
3
|
export class Logger {
|
|
2
4
|
static context () {
|
|
3
5
|
return null
|
|
@@ -14,6 +16,7 @@ export class Logger {
|
|
|
14
16
|
Boolean).join('_').toUpperCase().replaceAll(' ', '_')
|
|
15
17
|
this.global = global
|
|
16
18
|
this.#context = context
|
|
19
|
+
this.journald = getJournaldTransport(this.global)
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
get context () {
|
|
@@ -27,23 +30,23 @@ export class Logger {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
log (...args) {
|
|
30
|
-
if (this.logindex >= 0) log(this.global, 'log', this.context, args, this.namespace, this.format, this.flat)
|
|
33
|
+
if (this.logindex >= 0) log(this.global, 'log', this.context, args, this.namespace, this.format, this.flat, this.journald)
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
error (...args) {
|
|
34
|
-
if (this.logindex >= 0) log(this.global, 'error', this.context, args, this.namespace, this.format, this.flat)
|
|
37
|
+
if (this.logindex >= 0) log(this.global, 'error', this.context, args, this.namespace, this.format, this.flat, this.journald)
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
warn (...args) {
|
|
38
|
-
if (this.logindex >= 1) log(this.global, 'warn', this.context, args, this.namespace, this.format, this.flat)
|
|
41
|
+
if (this.logindex >= 1) log(this.global, 'warn', this.context, args, this.namespace, this.format, this.flat, this.journald)
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
info (...args) {
|
|
42
|
-
if (this.logindex >= 2) log(this.global, 'info', this.context, args, this.namespace, this.format, this.flat)
|
|
45
|
+
if (this.logindex >= 2) log(this.global, 'info', this.context, args, this.namespace, this.format, this.flat, this.journald)
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
debug (...args) {
|
|
46
|
-
if (this.logindex >= 3) log(this.global, 'debug', this.context, args, this.namespace, this.format, this.flat)
|
|
49
|
+
if (this.logindex >= 3) log(this.global, 'debug', this.context, args, this.namespace, this.format, this.flat, this.journald)
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -94,16 +97,73 @@ function stringifyJson (payload) {
|
|
|
94
97
|
})
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
export const JOURNAL_PRIORITY = {
|
|
101
|
+
log: 6,
|
|
102
|
+
error: 3,
|
|
103
|
+
warn: 4,
|
|
104
|
+
info: 6,
|
|
105
|
+
debug: 7
|
|
106
|
+
}
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
message
|
|
104
|
-
|
|
108
|
+
function toJournaldFields (record) {
|
|
109
|
+
const entry = {
|
|
110
|
+
PRIORITY: JOURNAL_PRIORITY[record.level] ?? 6,
|
|
111
|
+
MESSAGE: serializeJournaldValue(record.message) ?? '',
|
|
112
|
+
TIMESTAMP: record.timestamp,
|
|
113
|
+
LEVEL: record.level
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (record.namespace) {
|
|
117
|
+
entry.SYSLOG_IDENTIFIER = record.namespace
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const [key, value] of Object.entries(record)) {
|
|
121
|
+
if (['timestamp', 'level', 'namespace', 'message'].includes(key)) continue
|
|
122
|
+
const normalized = serializeJournaldValue(value)
|
|
123
|
+
if (normalized !== undefined) {
|
|
124
|
+
entry[key.toUpperCase()] = normalized
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return entry
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function serializeJournaldValue (value) {
|
|
132
|
+
if (value === undefined) return undefined
|
|
133
|
+
if (value === null) return 'null'
|
|
134
|
+
if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`
|
|
135
|
+
if (typeof value === 'symbol') return value.toString()
|
|
136
|
+
if (typeof value === 'object') return stringifyJson(value)
|
|
137
|
+
return String(value)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let defaultJournaldTransport = null
|
|
141
|
+
|
|
142
|
+
function getJournaldTransport (global) {
|
|
143
|
+
if (!global?.JOURNALD) return null
|
|
144
|
+
if (defaultJournaldTransport) return defaultJournaldTransport
|
|
145
|
+
defaultJournaldTransport = journaldSocket.createTransport()
|
|
146
|
+
return defaultJournaldTransport
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function sendToJournald (journald, fields) {
|
|
150
|
+
if (typeof journald === 'function') {
|
|
151
|
+
journald(fields)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (journald && typeof journald.send === 'function') {
|
|
156
|
+
journald.send(fields)
|
|
157
|
+
return
|
|
105
158
|
}
|
|
106
159
|
|
|
160
|
+
if (journald && typeof journald.log === 'function') {
|
|
161
|
+
journald.log(fields)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function log (global, level, context, args, namespace, format, flat, journald) {
|
|
166
|
+
const timestamp = new global.Date().toISOString()
|
|
107
167
|
const record = {
|
|
108
168
|
...(flat && context ? context : {}),
|
|
109
169
|
timestamp,
|
|
@@ -113,5 +173,20 @@ function log (global, level, context, args, namespace, format, flat) {
|
|
|
113
173
|
...parseArgs(args)
|
|
114
174
|
}
|
|
115
175
|
|
|
116
|
-
|
|
176
|
+
let consoleResult
|
|
177
|
+
|
|
178
|
+
if (format === 'plain') {
|
|
179
|
+
const message = [`${timestamp} [${level.toUpperCase()}]`]
|
|
180
|
+
if (context) message.push(stringifyJson(context))
|
|
181
|
+
message.push(...args)
|
|
182
|
+
consoleResult = global.console[level](...message)
|
|
183
|
+
} else {
|
|
184
|
+
consoleResult = global.console[level](stringifyJson(record))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (journald) {
|
|
188
|
+
sendToJournald(journald, toJournaldFields(record))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return consoleResult
|
|
117
192
|
}
|
package/lib/logger.test.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { it } from 'node:test'
|
|
3
|
-
|
|
2
|
+
import test, { it } from 'node:test'
|
|
3
|
+
|
|
4
|
+
const journaldTransportState = { lastFields: null }
|
|
5
|
+
|
|
6
|
+
const journaldSocketModule = await import('./utilities/journald-socket.js')
|
|
7
|
+
const { journaldSocket } = journaldSocketModule
|
|
8
|
+
test.mock.method(journaldSocket, 'createTransport', () => {
|
|
9
|
+
return {
|
|
10
|
+
send (fields) {
|
|
11
|
+
journaldTransportState.lastFields = fields
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const loggerModule = await import('./logger.js')
|
|
17
|
+
const { Logger, JOURNAL_PRIORITY } = loggerModule
|
|
4
18
|
|
|
5
19
|
function setup () {
|
|
6
20
|
const mockGlobal = {
|
|
@@ -392,3 +406,187 @@ it('supports plain formatting without context labels', () => {
|
|
|
392
406
|
'2025-02-15T00:00:00.000Z [INFO]'
|
|
393
407
|
])
|
|
394
408
|
})
|
|
409
|
+
|
|
410
|
+
it('reports JSON records to Journald when the global flag is enabled', () => {
|
|
411
|
+
const { mockGlobal } = setup()
|
|
412
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
413
|
+
journaldTransportState.lastFields = null
|
|
414
|
+
mockGlobal.JOURNALD = true
|
|
415
|
+
const logger = new Logger({ global: mockGlobal })
|
|
416
|
+
|
|
417
|
+
logger.info('Journald event', { action: 'create' })
|
|
418
|
+
|
|
419
|
+
const fields = journaldTransportState.lastFields
|
|
420
|
+
assert.ok(fields)
|
|
421
|
+
assert.deepStrictEqual(fields, {
|
|
422
|
+
PRIORITY: 6,
|
|
423
|
+
MESSAGE: 'Journald event',
|
|
424
|
+
TIMESTAMP: '2025-02-15T00:00:00.000Z',
|
|
425
|
+
LEVEL: 'info',
|
|
426
|
+
ACTION: 'create'
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
delete mockGlobal.JOURNALD
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('serializes nested context for Journald when flattening is disabled', () => {
|
|
433
|
+
const { mockGlobal } = setup()
|
|
434
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
435
|
+
const context = { correlationId: 'ABCD1234', nested: { role: 'admin' } }
|
|
436
|
+
journaldTransportState.lastFields = null
|
|
437
|
+
mockGlobal.JOURNALD = true
|
|
438
|
+
const logger = new Logger({ global: mockGlobal, context, flat: false })
|
|
439
|
+
|
|
440
|
+
logger.info('Nested context')
|
|
441
|
+
|
|
442
|
+
const fields = journaldTransportState.lastFields
|
|
443
|
+
assert.ok(fields)
|
|
444
|
+
assert.strictEqual(fields.CONTEXT, JSON.stringify(context))
|
|
445
|
+
assert.strictEqual(fields.MESSAGE, 'Nested context')
|
|
446
|
+
assert.strictEqual(fields.PRIORITY, 6)
|
|
447
|
+
|
|
448
|
+
delete mockGlobal.JOURNALD
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('defaults Journald MESSAGE to an empty string when no message is provided', () => {
|
|
452
|
+
const { mockGlobal } = setup()
|
|
453
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
454
|
+
mockGlobal.JOURNALD = true
|
|
455
|
+
const logger = new Logger({ global: mockGlobal })
|
|
456
|
+
let recorded
|
|
457
|
+
logger.journald = (fields) => { recorded = fields }
|
|
458
|
+
|
|
459
|
+
const payload = { action: 'create', detail: undefined }
|
|
460
|
+
logger.info(payload)
|
|
461
|
+
|
|
462
|
+
assert.ok(recorded)
|
|
463
|
+
assert.strictEqual(recorded.MESSAGE, '')
|
|
464
|
+
assert.strictEqual(recorded.ACTION, 'create')
|
|
465
|
+
assert.ok(!('DETAIL' in recorded))
|
|
466
|
+
|
|
467
|
+
delete mockGlobal.JOURNALD
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('falls back to the default Journald priority when the level is missing', () => {
|
|
471
|
+
const { mockGlobal } = setup()
|
|
472
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
473
|
+
mockGlobal.JOURNALD = true
|
|
474
|
+
const logger = new Logger({ global: mockGlobal })
|
|
475
|
+
let recorded
|
|
476
|
+
logger.journald = (fields) => { recorded = fields }
|
|
477
|
+
|
|
478
|
+
const original = JOURNAL_PRIORITY.info
|
|
479
|
+
delete JOURNAL_PRIORITY.info
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
logger.info('Fallback priority')
|
|
483
|
+
assert.ok(recorded)
|
|
484
|
+
assert.strictEqual(recorded.PRIORITY, 6)
|
|
485
|
+
} finally {
|
|
486
|
+
JOURNAL_PRIORITY.info = original
|
|
487
|
+
delete mockGlobal.JOURNALD
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('serializes null, function, and symbol values for Journald payloads', () => {
|
|
492
|
+
const { mockGlobal } = setup()
|
|
493
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
494
|
+
mockGlobal.JOURNALD = true
|
|
495
|
+
const logger = new Logger({ global: mockGlobal })
|
|
496
|
+
let recorded
|
|
497
|
+
logger.journald = (fields) => { recorded = fields }
|
|
498
|
+
|
|
499
|
+
const symbol = Symbol('token')
|
|
500
|
+
function namedFn () {}
|
|
501
|
+
const anonymousFn = Object.defineProperty(function () {}, 'name', { value: '' })
|
|
502
|
+
const payload = {
|
|
503
|
+
value: null,
|
|
504
|
+
handler: namedFn,
|
|
505
|
+
key: symbol,
|
|
506
|
+
unused: undefined,
|
|
507
|
+
data: { detail: 'rich' },
|
|
508
|
+
shadowed: anonymousFn
|
|
509
|
+
}
|
|
510
|
+
logger.info('Special types', payload)
|
|
511
|
+
|
|
512
|
+
assert.ok(recorded)
|
|
513
|
+
assert.strictEqual(recorded.VALUE, 'null')
|
|
514
|
+
assert.strictEqual(recorded.HANDLER, '[Function namedFn]')
|
|
515
|
+
assert.strictEqual(recorded.KEY, 'Symbol(token)')
|
|
516
|
+
assert.strictEqual(recorded.DATA, JSON.stringify(payload.data))
|
|
517
|
+
assert.strictEqual(recorded.SHADOWED, '[Function anonymous]')
|
|
518
|
+
assert.ok(!('UNUSED' in recorded))
|
|
519
|
+
|
|
520
|
+
delete mockGlobal.JOURNALD
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('includes the namespace as SYSLOG_IDENTIFIER in Journald payloads', () => {
|
|
524
|
+
const { mockGlobal } = setup()
|
|
525
|
+
mockGlobal.CORE_LOGLEVEL = 'info'
|
|
526
|
+
const logger = new Logger({ global: mockGlobal, namespace: 'core' })
|
|
527
|
+
|
|
528
|
+
let recorded
|
|
529
|
+
logger.journald = (fields) => {
|
|
530
|
+
recorded = fields
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
logger.info('Namespaced event', { module: 'inventory' })
|
|
534
|
+
|
|
535
|
+
assert.ok(recorded)
|
|
536
|
+
assert.strictEqual(recorded.SYSLOG_IDENTIFIER, 'core')
|
|
537
|
+
assert.strictEqual(recorded.MESSAGE, 'Namespaced event')
|
|
538
|
+
assert.strictEqual(recorded.MODULE, 'inventory')
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('calls journald transports that are functions directly', () => {
|
|
542
|
+
const { mockGlobal } = setup()
|
|
543
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
544
|
+
const logger = new Logger({ global: mockGlobal })
|
|
545
|
+
|
|
546
|
+
const payloads = []
|
|
547
|
+
logger.journald = (fields) => {
|
|
548
|
+
payloads.push(fields)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
logger.info('Function transport')
|
|
552
|
+
|
|
553
|
+
assert.strictEqual(payloads.length, 1)
|
|
554
|
+
assert.strictEqual(payloads[0].LEVEL, 'info')
|
|
555
|
+
assert.strictEqual(payloads[0].MESSAGE, 'Function transport')
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('prefers send() when the Journald transport exposes it', () => {
|
|
559
|
+
const { mockGlobal } = setup()
|
|
560
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
561
|
+
const logger = new Logger({ global: mockGlobal })
|
|
562
|
+
|
|
563
|
+
const payloads = []
|
|
564
|
+
logger.journald = {
|
|
565
|
+
send (fields) {
|
|
566
|
+
payloads.push(fields)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
logger.info('Send transport')
|
|
571
|
+
|
|
572
|
+
assert.strictEqual(payloads.length, 1)
|
|
573
|
+
assert.strictEqual(payloads[0].MESSAGE, 'Send transport')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('falls back to log() when send() is unavailable', () => {
|
|
577
|
+
const { mockGlobal } = setup()
|
|
578
|
+
mockGlobal.LOGLEVEL = 'info'
|
|
579
|
+
const logger = new Logger({ global: mockGlobal })
|
|
580
|
+
|
|
581
|
+
const payloads = []
|
|
582
|
+
logger.journald = {
|
|
583
|
+
log (fields) {
|
|
584
|
+
payloads.push(fields)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
logger.info('Log transport')
|
|
589
|
+
|
|
590
|
+
assert.strictEqual(payloads.length, 1)
|
|
591
|
+
assert.strictEqual(payloads[0].MESSAGE, 'Log transport')
|
|
592
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createSocket } from 'node:dgram'
|
|
2
|
+
|
|
3
|
+
const JOURNAL_SOCKET_PATH = '/run/systemd/journal/socket'
|
|
4
|
+
|
|
5
|
+
function buildTransport (createSocketImplementation) {
|
|
6
|
+
try {
|
|
7
|
+
const socket = createSocketImplementation({ type: 'unix_dgram' })
|
|
8
|
+
let activeSocket = socket
|
|
9
|
+
|
|
10
|
+
const cleanup = () => {
|
|
11
|
+
if (!activeSocket) return
|
|
12
|
+
activeSocket.close()
|
|
13
|
+
activeSocket = null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
socket.on('error', cleanup)
|
|
17
|
+
socket.on('close', cleanup)
|
|
18
|
+
socket.connect(JOURNAL_SOCKET_PATH, (error) => {
|
|
19
|
+
if (error) cleanup()
|
|
20
|
+
})
|
|
21
|
+
socket.unref?.()
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
send (fields) {
|
|
25
|
+
if (!activeSocket) return
|
|
26
|
+
const payload = Buffer.from(
|
|
27
|
+
Object.entries(fields).map(
|
|
28
|
+
([key, value]) => `${key}=${value}`).join('\n') + '\n')
|
|
29
|
+
activeSocket.send(payload, (error) => {
|
|
30
|
+
if (error) cleanup()
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw error
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createJournaldTransport ({ createSocket: createSocketImplementation } = {}) {
|
|
40
|
+
try {
|
|
41
|
+
return buildTransport(createSocketImplementation ?? createSocket)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const journaldSocket = {
|
|
48
|
+
createTransport (options) {
|
|
49
|
+
return createJournaldTransport(options)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { it } from 'node:test'
|
|
3
|
+
import { createJournaldTransport, journaldSocket } from './journald-socket.js'
|
|
4
|
+
|
|
5
|
+
function createFakeSocket ({ connectError = null, sendError = null } = {}) {
|
|
6
|
+
const events = {}
|
|
7
|
+
return {
|
|
8
|
+
events,
|
|
9
|
+
connectedPath: null,
|
|
10
|
+
sentPayload: null,
|
|
11
|
+
sendCalls: 0,
|
|
12
|
+
closed: false,
|
|
13
|
+
on (event, callback) {
|
|
14
|
+
events[event] = callback
|
|
15
|
+
},
|
|
16
|
+
connect (path, callback) {
|
|
17
|
+
this.connectedPath = path
|
|
18
|
+
callback?.(connectError)
|
|
19
|
+
},
|
|
20
|
+
send (payload, callback) {
|
|
21
|
+
this.sentPayload = payload
|
|
22
|
+
this.sendCalls += 1
|
|
23
|
+
callback?.(sendError)
|
|
24
|
+
},
|
|
25
|
+
close () {
|
|
26
|
+
this.closed = true
|
|
27
|
+
},
|
|
28
|
+
unref () {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
it('falls back to the built-in socket factory when no override is supplied', () => {
|
|
33
|
+
const transport = createJournaldTransport()
|
|
34
|
+
|
|
35
|
+
assert.ok(transport === null || typeof transport.send === 'function')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('establishes a journald socket and encodes payloads', () => {
|
|
39
|
+
const socket = createFakeSocket()
|
|
40
|
+
const transport = createJournaldTransport({ createSocket: () => socket })
|
|
41
|
+
|
|
42
|
+
assert.ok(transport)
|
|
43
|
+
assert.strictEqual(socket.connectedPath, '/run/systemd/journal/socket')
|
|
44
|
+
|
|
45
|
+
transport.send({ foo: 'bar', baz: 'qux' })
|
|
46
|
+
assert.strictEqual(socket.sendCalls, 1)
|
|
47
|
+
assert.strictEqual(socket.sentPayload.toString(), 'foo=bar\nbaz=qux\n')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('silently ignores send once cleanup runs after an error event', () => {
|
|
51
|
+
const socket = createFakeSocket()
|
|
52
|
+
const transport = createJournaldTransport({ createSocket: () => socket })
|
|
53
|
+
|
|
54
|
+
socket.events.error?.(new Error('boom'))
|
|
55
|
+
|
|
56
|
+
transport.send({ foo: 'bar' })
|
|
57
|
+
assert.strictEqual(socket.sendCalls, 0)
|
|
58
|
+
assert.strictEqual(socket.closed, true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns null when the socket factory throws', () => {
|
|
62
|
+
const transport = createJournaldTransport({ createSocket: () => { throw new Error('failed') } })
|
|
63
|
+
assert.strictEqual(transport, null)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('exposes the same transport via journaldSocket.createTransport', () => {
|
|
67
|
+
const socket = createFakeSocket()
|
|
68
|
+
const createSocket = () => socket
|
|
69
|
+
const transport = journaldSocket.createTransport({ createSocket })
|
|
70
|
+
|
|
71
|
+
assert.ok(transport)
|
|
72
|
+
assert.strictEqual(socket.connectedPath, '/run/systemd/journal/socket')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('runs cleanup when connect reports an error', () => {
|
|
76
|
+
const socket = createFakeSocket({ connectError: new Error('connect fail') })
|
|
77
|
+
const transport = createJournaldTransport({ createSocket: () => socket })
|
|
78
|
+
|
|
79
|
+
assert.ok(transport)
|
|
80
|
+
assert.strictEqual(socket.closed, true)
|
|
81
|
+
|
|
82
|
+
transport.send({ foo: 'bar' })
|
|
83
|
+
assert.strictEqual(socket.sendCalls, 0)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('runs cleanup when send reports an error', () => {
|
|
87
|
+
const socket = createFakeSocket({ sendError: new Error('send fail') })
|
|
88
|
+
const transport = createJournaldTransport({ createSocket: () => socket })
|
|
89
|
+
|
|
90
|
+
transport.send({ foo: 'bar' })
|
|
91
|
+
assert.strictEqual(socket.closed, true)
|
|
92
|
+
assert.strictEqual(socket.sendCalls, 1)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('safely ignores repeated cleanup calls', () => {
|
|
96
|
+
const socket = createFakeSocket()
|
|
97
|
+
const transport = createJournaldTransport({ createSocket: () => socket })
|
|
98
|
+
|
|
99
|
+
socket.events.error?.(new Error('boom'))
|
|
100
|
+
socket.events.error?.(new Error('boom again'))
|
|
101
|
+
|
|
102
|
+
transport.send({ foo: 'bar' })
|
|
103
|
+
assert.strictEqual(socket.closed, true)
|
|
104
|
+
assert.strictEqual(socket.sendCalls, 0)
|
|
105
|
+
})
|