@nxtedition/logger 1.0.4 → 2.0.1
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/LICENSE +21 -0
- package/README.md +115 -0
- package/lib/logger-worker.d.ts +2 -0
- package/lib/logger-worker.d.ts.map +1 -0
- package/lib/logger-worker.js +104 -0
- package/lib/logger-worker.js.map +1 -0
- package/lib/logger.d.ts +2 -8
- package/lib/logger.d.ts.map +1 -1
- package/lib/logger.js +109 -47
- package/lib/logger.js.map +1 -1
- package/package.json +10 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nxtedition
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @nxtedition/logger
|
|
2
|
+
|
|
3
|
+
A high-performance, structured JSON logger built on [pino](https://github.com/pinojs/pino) with a dedicated worker thread for I/O.
|
|
4
|
+
|
|
5
|
+
## Why a logger worker?
|
|
6
|
+
|
|
7
|
+
In Node.js, multiple threads writing to stdout concurrently can interleave
|
|
8
|
+
partial writes, producing corrupted or merged log lines. This is especially
|
|
9
|
+
problematic when several worker threads each emit structured JSON — a single
|
|
10
|
+
torn line breaks every downstream log parser.
|
|
11
|
+
|
|
12
|
+
This package solves the problem by routing all log output through a single
|
|
13
|
+
dedicated worker thread:
|
|
14
|
+
|
|
15
|
+
1. **No tearing / interleaving** — Only one thread ever calls `write(2)` on
|
|
16
|
+
fd 1, so every JSON line is atomically written.
|
|
17
|
+
2. **Better throughput** — Writers serialize log messages into lock-free ring
|
|
18
|
+
buffers backed by `SharedArrayBuffer` (SAB). The logger worker drains these
|
|
19
|
+
buffers and writes to stdout using synchronous I/O, avoiding back-pressure
|
|
20
|
+
from slow consumers stalling application threads.
|
|
21
|
+
3. **Minimal latency on the hot path** — `writeSync` into a shared ring buffer
|
|
22
|
+
is a memory copy + atomic store. No syscall, no serialization contention.
|
|
23
|
+
The actual `write(2)` happens asynchronously on the worker thread.
|
|
24
|
+
|
|
25
|
+
## Architecture
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
┌──────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
29
|
+
│ Main thread │ │ Worker A │ │ Worker B │
|
|
30
|
+
│ (logger) │ │ (logger) │ │ (logger) │
|
|
31
|
+
└──────┬───────┘ └──────┬──────┘ └──────┬──────┘
|
|
32
|
+
│ │ │
|
|
33
|
+
writeSync() writeSync() writeSync()
|
|
34
|
+
│ │ │
|
|
35
|
+
┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐
|
|
36
|
+
│ Ring buf │ │ Ring buf │ │ Ring buf │
|
|
37
|
+
│ (SAB) │ │ (SAB) │ │ (SAB) │
|
|
38
|
+
└────┬─────┘ └────┬─────┘ └────┬─────┘
|
|
39
|
+
│ │ │
|
|
40
|
+
└──────────┬───────┘─────────────────┘
|
|
41
|
+
│
|
|
42
|
+
┌──────▼──────┐
|
|
43
|
+
│Logger worker│
|
|
44
|
+
│ (single) │
|
|
45
|
+
│ │
|
|
46
|
+
│ readSome() │
|
|
47
|
+
│ fs.writeSync│
|
|
48
|
+
│ fd 1 │
|
|
49
|
+
└─────────────┘
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
SAB = `SharedArrayBuffer`
|
|
53
|
+
|
|
54
|
+
Each call to `createLogger()` allocates a 2 MiB `SharedArrayBuffer` ring
|
|
55
|
+
buffer. The writer (application side) serializes pino JSON into the buffer.
|
|
56
|
+
The logger worker polls all registered readers and flushes their contents to
|
|
57
|
+
stdout.
|
|
58
|
+
|
|
59
|
+
Registration and unregistration use `BroadcastChannel` with a
|
|
60
|
+
`SharedArrayBuffer`-based ack to ensure the worker has set up the reader
|
|
61
|
+
before the caller proceeds.
|
|
62
|
+
|
|
63
|
+
## Graceful shutdown
|
|
64
|
+
|
|
65
|
+
On process exit (`process.exit()`, uncaught exceptions, SIGTERM, SIGINT):
|
|
66
|
+
|
|
67
|
+
1. All writers call `flushSync()` to publish pending data to their ring
|
|
68
|
+
buffers.
|
|
69
|
+
2. The main thread signals the logger worker to drain via a shared
|
|
70
|
+
`Int32Array` flag.
|
|
71
|
+
3. The main thread blocks (up to 2 s) until the worker confirms drain is
|
|
72
|
+
complete.
|
|
73
|
+
4. The logger worker also registers a `process.on('exit')` handler as a
|
|
74
|
+
safety net — if the normal drain protocol fails, the handler performs a
|
|
75
|
+
final synchronous drain of all readers.
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { createLogger } from '@nxtedition/logger'
|
|
81
|
+
|
|
82
|
+
const logger = createLogger({ level: 'info' })
|
|
83
|
+
logger.info({ key: 'value' }, 'hello world')
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`createLogger` accepts all [pino options](https://getpino.io/#/docs/api?id=options).
|
|
87
|
+
Custom serializers are merged with the built-in set (err, req, res, etc.).
|
|
88
|
+
|
|
89
|
+
### Worker threads
|
|
90
|
+
|
|
91
|
+
Call `createLogger()` on the **main thread first** — this starts the logger
|
|
92
|
+
worker. Worker threads can then call `createLogger()` freely; each gets its
|
|
93
|
+
own ring buffer registered with the shared logger worker.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// main.ts
|
|
97
|
+
import { createLogger } from '@nxtedition/logger'
|
|
98
|
+
const logger = createLogger({ level: 'info' }) // starts worker
|
|
99
|
+
|
|
100
|
+
// worker.ts
|
|
101
|
+
import { createLogger } from '@nxtedition/logger'
|
|
102
|
+
const logger = createLogger({ level: 'debug' }) // registers with existing worker
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Development
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
yarn build # compile TypeScript → lib/
|
|
109
|
+
yarn test # run tests
|
|
110
|
+
yarn test:coverage # run tests with c8 coverage report
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
See [LICENSE](./LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-worker.d.ts","sourceRoot":"","sources":["../src/logger-worker.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { workerData } from 'node:worker_threads';
|
|
2
|
+
import { Reader } from '@nxtedition/shared';
|
|
3
|
+
import tp from 'node:timers/promises';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
const readersByToken = new Map();
|
|
6
|
+
let readers = [];
|
|
7
|
+
const backoffBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
8
|
+
function writeAll(buffer, offset, length) {
|
|
9
|
+
// fd 1 may be non-blocking in worker threads, so fs.writeSync can
|
|
10
|
+
// return partial writes or throw EAGAIN/EBUSY. Loop until all bytes
|
|
11
|
+
// are out, backing off with Atomics.wait after repeated retries.
|
|
12
|
+
let retries = 0;
|
|
13
|
+
while (length > 0) {
|
|
14
|
+
try {
|
|
15
|
+
const written = fs.writeSync(1, buffer, offset, length);
|
|
16
|
+
offset += written;
|
|
17
|
+
length -= written;
|
|
18
|
+
retries = 0;
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const code = err.code;
|
|
22
|
+
if (code === 'EAGAIN' || code === 'EBUSY') {
|
|
23
|
+
retries++;
|
|
24
|
+
if (retries > 4096) {
|
|
25
|
+
throw new Error(`writeAll: failed to make progress after ${retries} retries (${code})`);
|
|
26
|
+
}
|
|
27
|
+
else if (retries > 2) {
|
|
28
|
+
Atomics.wait(backoffBuffer, 0, 0, 1);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
Atomics.pause();
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readerDrain(r) {
|
|
40
|
+
while (r.readSome((data) => {
|
|
41
|
+
writeAll(data.buffer, data.byteOffset, data.byteLength);
|
|
42
|
+
}) > 0) {
|
|
43
|
+
// Keep draining until empty.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const bc = new BroadcastChannel('nxt:logger');
|
|
47
|
+
bc.onmessage = (event) => {
|
|
48
|
+
const { type, token, sharedBuffer, ackBuffer } = event.data;
|
|
49
|
+
if (type === 'nxt:logger:register' && token != null && sharedBuffer) {
|
|
50
|
+
const r = new Reader(sharedBuffer);
|
|
51
|
+
readersByToken.set(token, r);
|
|
52
|
+
readers = [...readersByToken.values()];
|
|
53
|
+
if (ackBuffer) {
|
|
54
|
+
Atomics.store(new Int32Array(ackBuffer), 0, 1);
|
|
55
|
+
Atomics.notify(new Int32Array(ackBuffer), 0);
|
|
56
|
+
}
|
|
57
|
+
readerDrain(r); // Drain any initial data...
|
|
58
|
+
}
|
|
59
|
+
else if (type === 'nxt:logger:unregister' && token != null) {
|
|
60
|
+
const r = readersByToken.get(token);
|
|
61
|
+
if (r) {
|
|
62
|
+
// Drain any remaining data before removing.
|
|
63
|
+
readerDrain(r);
|
|
64
|
+
readersByToken.delete(token);
|
|
65
|
+
readers = [...readersByToken.values()];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// Drain signal: 0 = running, 1 = drain requested, 2 = drain complete.
|
|
70
|
+
const drainState = new Int32Array(workerData.drainBuffer);
|
|
71
|
+
// Safety net: if the worker is terminated before the normal drain path runs
|
|
72
|
+
// (e.g. main thread crash, exit handler failure), do a final synchronous
|
|
73
|
+
// drain so buffered log data is not silently lost.
|
|
74
|
+
process.on('exit', () => {
|
|
75
|
+
for (let i = 0; i < readers.length; i++) {
|
|
76
|
+
readerDrain(readers[i]);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// Signal readiness to the parent thread so it can proceed with registration.
|
|
80
|
+
const readyState = new Int32Array(workerData.readyBuffer);
|
|
81
|
+
Atomics.store(readyState, 0, 1);
|
|
82
|
+
Atomics.notify(readyState, 0);
|
|
83
|
+
while (true) {
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (let i = 0; i < readers.length; i++) {
|
|
86
|
+
count += readers[i].readSome((data) => {
|
|
87
|
+
writeAll(data.buffer, data.byteOffset, data.byteLength);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Let bc.onmessage handlers run to pick up new readers or drain requests.
|
|
91
|
+
await (count > 0 ? tp.setImmediate() : tp.setTimeout(10));
|
|
92
|
+
// Check if drain was requested by the main thread.
|
|
93
|
+
if (Atomics.load(drainState, 0) === 1) {
|
|
94
|
+
// Drain all remaining data from every reader until empty.
|
|
95
|
+
for (let i = 0; i < readers.length; i++) {
|
|
96
|
+
readerDrain(readers[i]);
|
|
97
|
+
}
|
|
98
|
+
Atomics.store(drainState, 0, 2);
|
|
99
|
+
Atomics.notify(drainState, 0);
|
|
100
|
+
bc.close();
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=logger-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger-worker.js","sourceRoot":"","sources":["../src/logger-worker.ts"],"names":[],"mappings":"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"}
|
package/lib/logger.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import type { Logger as PinoLogger, LoggerOptions
|
|
2
|
-
import type { SonicBoom } from 'sonic-boom';
|
|
1
|
+
import type { Logger as PinoLogger, LoggerOptions } from 'pino';
|
|
3
2
|
export type Logger = PinoLogger;
|
|
4
|
-
|
|
5
|
-
flushInterval?: number;
|
|
6
|
-
stream?: SonicBoom | null;
|
|
7
|
-
}
|
|
8
|
-
export declare function createLogger({ level, flushInterval, stream, ...options }?: LoggerOptions, onTerminate?: unknown): Logger;
|
|
9
|
-
export {};
|
|
3
|
+
export declare function createLogger({ level, ...options }?: LoggerOptions): Logger;
|
|
10
4
|
//# sourceMappingURL=logger.d.ts.map
|
package/lib/logger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,aAAa,EAAE,MAAM,MAAM,CAAA;AAQ/D,MAAM,MAAM,MAAM,GAAG,UAAU,CAAA;AAmG/B,wBAAgB,YAAY,CAAC,EAC3B,KAAwC,EACxC,GAAG,OAAO,EACX,GAAE,aAAkB,GAAG,MAAM,CA+C7B"}
|
package/lib/logger.js
CHANGED
|
@@ -1,53 +1,116 @@
|
|
|
1
|
-
import { isMainThread } from 'node:worker_threads';
|
|
2
|
-
import serializers from './serializers.js';
|
|
1
|
+
import { isMainThread, Worker } from 'node:worker_threads';
|
|
3
2
|
import pino from 'pino';
|
|
3
|
+
import onExit from 'on-exit-leak-free';
|
|
4
|
+
import xuid from 'xuid';
|
|
5
|
+
import { Writer } from '@nxtedition/shared';
|
|
6
|
+
import serializers from './serializers.js';
|
|
4
7
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
let workerStarted = false;
|
|
9
|
+
const writers = new Set();
|
|
10
|
+
class SharedWriter {
|
|
11
|
+
#w;
|
|
12
|
+
#token;
|
|
13
|
+
#destroyed = false;
|
|
14
|
+
constructor(token, sharedBuffer) {
|
|
15
|
+
this.#token = token;
|
|
16
|
+
this.#w = new Writer(sharedBuffer);
|
|
17
|
+
writers.add(this);
|
|
18
|
+
}
|
|
19
|
+
write(msg) {
|
|
20
|
+
if (this.#destroyed) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
this.#w.writeSync(msg.length * 3, (data, msg) => data.byteOffset + data.buffer.write(msg, data.byteOffset, 'utf-8'), msg);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
flushSync() {
|
|
27
|
+
if (!this.#destroyed) {
|
|
28
|
+
this.#w.flushSync();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
end() {
|
|
32
|
+
if (this.#destroyed) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
this.#destroyed = true;
|
|
36
|
+
this.#w.flushSync();
|
|
37
|
+
writers.delete(this);
|
|
38
|
+
onExit.unregister(this);
|
|
39
|
+
const bc = new BroadcastChannel('nxt:logger');
|
|
40
|
+
bc.postMessage({ type: 'nxt:logger:unregister', token: this.#token });
|
|
41
|
+
bc.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Starts the logger worker. Must be called on the main thread before any
|
|
45
|
+
// worker thread calls createLogger, otherwise the BroadcastChannel
|
|
46
|
+
// registration message will be lost (no listener).
|
|
47
|
+
function startWorker() {
|
|
48
|
+
if (workerStarted) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
workerStarted = true;
|
|
52
|
+
const readyBuffer = new SharedArrayBuffer(4);
|
|
53
|
+
const drainBuffer = new SharedArrayBuffer(4);
|
|
54
|
+
const drainState = new Int32Array(drainBuffer);
|
|
55
|
+
const w = new Worker(new URL('./logger-worker.js', import.meta.url), {
|
|
56
|
+
workerData: { readyBuffer, drainBuffer },
|
|
57
|
+
execArgv: [], // Don't inherit CLI flags (e.g. --inspect) from parent.
|
|
58
|
+
})
|
|
59
|
+
.on('error', (err) => {
|
|
60
|
+
throw new Error('Logger worker crashed', { cause: err });
|
|
61
|
+
})
|
|
62
|
+
.on('exit', (code) => {
|
|
63
|
+
if (Atomics.load(drainState, 0) !== 2) {
|
|
64
|
+
throw new Error(`Logger worker exited unexpectedly with code ${code}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
w.unref();
|
|
68
|
+
// Block until the worker has set up its BroadcastChannel listener.
|
|
69
|
+
const res = Atomics.wait(new Int32Array(readyBuffer), 0, 0, 10_000);
|
|
70
|
+
if (res === 'timed-out') {
|
|
71
|
+
void w.terminate();
|
|
72
|
+
workerStarted = false;
|
|
73
|
+
throw new Error('Logger worker failed to start within 10 seconds.');
|
|
74
|
+
}
|
|
75
|
+
// On exit: flush writers, signal worker to drain, block until done.
|
|
76
|
+
onExit.register(drainState, (drainState) => {
|
|
77
|
+
for (const w of writers) {
|
|
78
|
+
w.flushSync();
|
|
79
|
+
}
|
|
80
|
+
Atomics.store(drainState, 0, 1);
|
|
81
|
+
Atomics.notify(drainState, 0);
|
|
82
|
+
Atomics.wait(drainState, 0, 1, 2000);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function createLogger({ level = isProduction ? 'debug' : 'trace', ...options } = {}) {
|
|
86
|
+
if (isMainThread) {
|
|
87
|
+
startWorker();
|
|
88
|
+
}
|
|
89
|
+
const sharedBuffer = new SharedArrayBuffer(2 * 1024 * 1024); // 2 MiB per thread
|
|
90
|
+
const token = xuid();
|
|
91
|
+
const stream = new SharedWriter(token, sharedBuffer);
|
|
92
|
+
{
|
|
93
|
+
const ackBuffer = new SharedArrayBuffer(4);
|
|
94
|
+
const bc = new BroadcastChannel('nxt:logger');
|
|
95
|
+
bc.postMessage({ type: 'nxt:logger:register', token, sharedBuffer, ackBuffer });
|
|
96
|
+
bc.close();
|
|
97
|
+
// Block until the logger worker acknowledges the registration.
|
|
98
|
+
// If createLogger() was never called on the main thread the ack
|
|
99
|
+
// will never arrive and we throw a clear error instead of silently
|
|
100
|
+
// dropping log output.
|
|
101
|
+
const res = Atomics.wait(new Int32Array(ackBuffer), 0, 0, 10_000);
|
|
102
|
+
if (res === 'timed-out') {
|
|
103
|
+
writers.delete(stream);
|
|
104
|
+
throw new Error('Logger worker did not acknowledge registration. ' +
|
|
105
|
+
'Ensure createLogger() is called on the main thread before any worker thread calls createLogger().');
|
|
10
106
|
}
|
|
11
107
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
else if (!isMainThread) {
|
|
19
|
-
// TODO (perf): Async mode doesn't work super well in workers.
|
|
20
|
-
stream = pino.destination({ fd: 1, sync: true, fsync: false });
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
stream = pino.destination({
|
|
24
|
-
sync: false,
|
|
25
|
-
fsync: false,
|
|
26
|
-
minLength: 4 * 1024,
|
|
27
|
-
maxWrite: 32 * 1024,
|
|
28
|
-
});
|
|
29
|
-
let flushing = 0;
|
|
30
|
-
const onFlush = () => {
|
|
31
|
-
flushing--;
|
|
32
|
-
};
|
|
33
|
-
const flushTimeout = setInterval(() => {
|
|
34
|
-
if (flushing >= 10) {
|
|
35
|
-
try {
|
|
36
|
-
logger.warn('logger is flushing too slow');
|
|
37
|
-
stream?.flushSync();
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
console.error(err);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
flushing++;
|
|
45
|
-
stream?.flush(onFlush);
|
|
46
|
-
}
|
|
47
|
-
flushTimeout.refresh();
|
|
48
|
-
}, flushInterval).unref();
|
|
49
|
-
}
|
|
50
|
-
const logger = pino({
|
|
108
|
+
// When this thread exits, flush the writer and tell the logger worker
|
|
109
|
+
// to remove the reader for this buffer.
|
|
110
|
+
onExit.register(stream, (stream) => {
|
|
111
|
+
stream.end();
|
|
112
|
+
});
|
|
113
|
+
return pino({
|
|
51
114
|
level,
|
|
52
115
|
...options,
|
|
53
116
|
serializers: {
|
|
@@ -55,6 +118,5 @@ export function createLogger({ level = isProduction ? 'debug' : 'trace', flushIn
|
|
|
55
118
|
...options.serializers,
|
|
56
119
|
},
|
|
57
120
|
}, stream);
|
|
58
|
-
return logger;
|
|
59
121
|
}
|
|
60
122
|
//# sourceMappingURL=logger.js.map
|
package/lib/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/logger",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -10,18 +10,23 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"lib"
|
|
12
12
|
],
|
|
13
|
-
"license": "
|
|
13
|
+
"license": "MIT",
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "rimraf lib && tsc",
|
|
16
16
|
"prepublishOnly": "yarn build",
|
|
17
17
|
"typecheck": "tsc --noEmit",
|
|
18
18
|
"test": "node --test",
|
|
19
|
-
"test:ci": "node --test"
|
|
19
|
+
"test:ci": "node --test",
|
|
20
|
+
"test:coverage": "c8 node --test"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
23
|
+
"@nxtedition/nxt-undici": "^7.3.2",
|
|
24
|
+
"@nxtedition/shared": "^4.0.3",
|
|
22
25
|
"fast-querystring": "^1.1.2",
|
|
26
|
+
"on-exit-leak-free": "^2.1.2",
|
|
23
27
|
"pino": "^10.3.1",
|
|
24
|
-
"request-target": "^1.0.
|
|
28
|
+
"request-target": "^1.0.0",
|
|
29
|
+
"xuid": "^4.1.5"
|
|
25
30
|
},
|
|
26
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "239c3eeff1f228d4d71ff6f8c884642f359065b5"
|
|
27
32
|
}
|