@lakutata/logger 0.0.0-stage → 3.0.0-beta.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/LICENSE +23 -0
- package/README.md +137 -2
- package/THIRD_PARTY_NOTICES.md +158 -0
- package/dist/cjs/Logger.d.ts +291 -0
- package/dist/cjs/Logger.js +185 -0
- package/dist/cjs/exports/Logger.d.ts +2 -0
- package/dist/cjs/exports/Logger.js +20 -0
- package/dist/cjs/lib/Colors.d.ts +8 -0
- package/dist/cjs/lib/Colors.js +48 -0
- package/dist/cjs/lib/Destination.d.ts +38 -0
- package/dist/cjs/lib/Destination.js +103 -0
- package/dist/cjs/lib/Format.d.ts +7 -0
- package/dist/cjs/lib/Format.js +106 -0
- package/dist/cjs/lib/Pretty.d.ts +9 -0
- package/dist/cjs/lib/Pretty.js +265 -0
- package/dist/cjs/lib/Record.d.ts +25 -0
- package/dist/cjs/lib/Record.js +139 -0
- package/dist/cjs/lib/Serializers.d.ts +26 -0
- package/dist/cjs/lib/Serializers.js +150 -0
- package/dist/cjs/lib/Stringify.d.ts +5 -0
- package/dist/cjs/lib/Stringify.js +110 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Logger.js +182 -0
- package/dist/esm/exports/Logger.js +1 -0
- package/dist/esm/lib/Colors.js +45 -0
- package/dist/esm/lib/Destination.js +99 -0
- package/dist/esm/lib/Format.js +103 -0
- package/dist/esm/lib/Pretty.js +262 -0
- package/dist/esm/lib/Record.js +134 -0
- package/dist/esm/lib/Serializers.js +143 -0
- package/dist/esm/lib/Stringify.js +107 -0
- package/dist/types/Logger.d.ts +291 -0
- package/dist/types/exports/Logger.d.ts +2 -0
- package/dist/types/lib/Colors.d.ts +8 -0
- package/dist/types/lib/Destination.d.ts +38 -0
- package/dist/types/lib/Format.d.ts +7 -0
- package/dist/types/lib/Pretty.d.ts +9 -0
- package/dist/types/lib/Record.d.ts +25 -0
- package/dist/types/lib/Serializers.d.ts +26 -0
- package/dist/types/lib/Stringify.d.ts +5 -0
- package/package.json +43 -4
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stringify = stringify;
|
|
4
|
+
//Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
|
|
5
|
+
const MAXIMUM_DEPTH = 5;
|
|
6
|
+
const MAXIMUM_BREADTH = 100;
|
|
7
|
+
const TypedArrayToStringTag = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Int8Array.prototype), Symbol.toStringTag).get;
|
|
8
|
+
/**
|
|
9
|
+
* The count of the omitted items
|
|
10
|
+
* @param count
|
|
11
|
+
*/
|
|
12
|
+
function itemCount(count) {
|
|
13
|
+
return count === 1 ? '1 item' : `${count} items`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a value which JSON cannot (circular references, bigints):
|
|
17
|
+
* sorted keys, circular references as "[Circular]", limited depth and breadth
|
|
18
|
+
* @param key
|
|
19
|
+
* @param value
|
|
20
|
+
* @param stack
|
|
21
|
+
*/
|
|
22
|
+
function safeStringify(key, value, stack) {
|
|
23
|
+
switch (typeof value) {
|
|
24
|
+
case 'string':
|
|
25
|
+
return JSON.stringify(value);
|
|
26
|
+
case 'number':
|
|
27
|
+
return isFinite(value) ? String(value) : 'null';
|
|
28
|
+
case 'boolean':
|
|
29
|
+
return value ? 'true' : 'false';
|
|
30
|
+
case 'bigint':
|
|
31
|
+
return String(value);
|
|
32
|
+
case 'object': {
|
|
33
|
+
if (value === null)
|
|
34
|
+
return 'null';
|
|
35
|
+
if (typeof value.toJSON === 'function') {
|
|
36
|
+
value = value.toJSON(key);
|
|
37
|
+
if (typeof value !== 'object')
|
|
38
|
+
return safeStringify(key, value, stack);
|
|
39
|
+
if (value === null)
|
|
40
|
+
return 'null';
|
|
41
|
+
}
|
|
42
|
+
if (stack.includes(value))
|
|
43
|
+
return '"[Circular]"';
|
|
44
|
+
let result = '';
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
if (!value.length)
|
|
47
|
+
return '[]';
|
|
48
|
+
if (MAXIMUM_DEPTH < stack.length + 1)
|
|
49
|
+
return '"[Array]"';
|
|
50
|
+
stack.push(value);
|
|
51
|
+
const count = Math.min(value.length, MAXIMUM_BREADTH);
|
|
52
|
+
for (let index = 0; index < count; index++) {
|
|
53
|
+
result += `${index ? ',' : ''}${safeStringify(String(index), value[index], stack) ?? 'null'}`;
|
|
54
|
+
}
|
|
55
|
+
if (value.length - 1 > MAXIMUM_BREADTH)
|
|
56
|
+
result += `,"... ${itemCount(value.length - MAXIMUM_BREADTH - 1)} not stringified"`;
|
|
57
|
+
stack.pop();
|
|
58
|
+
return `[${result}]`;
|
|
59
|
+
}
|
|
60
|
+
let keys = Object.keys(value);
|
|
61
|
+
const keyCount = keys.length;
|
|
62
|
+
if (!keyCount)
|
|
63
|
+
return '{}';
|
|
64
|
+
if (MAXIMUM_DEPTH < stack.length + 1)
|
|
65
|
+
return '"[Object]"';
|
|
66
|
+
let separator = '';
|
|
67
|
+
let count = Math.min(keyCount, MAXIMUM_BREADTH);
|
|
68
|
+
if (value.length !== undefined && TypedArrayToStringTag.call(value) !== undefined && value.length !== 0) {
|
|
69
|
+
const items = Math.min(value.length, MAXIMUM_BREADTH);
|
|
70
|
+
for (let index = 0; index < items; index++)
|
|
71
|
+
result += `${index ? ',' : ''}"${index}":${value[index]}`;
|
|
72
|
+
keys = keys.slice(value.length);
|
|
73
|
+
count -= value.length;
|
|
74
|
+
separator = ',';
|
|
75
|
+
}
|
|
76
|
+
keys.sort();
|
|
77
|
+
stack.push(value);
|
|
78
|
+
for (let index = 0; index < count; index++) {
|
|
79
|
+
const item = safeStringify(keys[index], value[keys[index]], stack);
|
|
80
|
+
if (item === undefined)
|
|
81
|
+
continue;
|
|
82
|
+
result += `${separator}${JSON.stringify(keys[index])}:${item}`;
|
|
83
|
+
separator = ',';
|
|
84
|
+
}
|
|
85
|
+
if (keyCount > MAXIMUM_BREADTH)
|
|
86
|
+
result += `${separator}"...":"${itemCount(keyCount - MAXIMUM_BREADTH)} not stringified"`;
|
|
87
|
+
stack.pop();
|
|
88
|
+
return `{${result}}`;
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Serialize a value to JSON, the values JSON cannot serialize are serialized safely
|
|
96
|
+
* @param value
|
|
97
|
+
*/
|
|
98
|
+
function stringify(value) {
|
|
99
|
+
try {
|
|
100
|
+
return JSON.stringify(value);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
try {
|
|
104
|
+
return safeStringify('', value, []);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return '"[unable to serialize, circular reference is too complex to analyze]"';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
import { Application, Component, DTO } from '@lakutata/core';
|
|
11
|
+
import { Configurable, Singleton } from '@lakutata/core/decorator/di';
|
|
12
|
+
import { Stream } from 'node:stream';
|
|
13
|
+
import { logBindings, buildRecord, LEVELS } from './lib/Record.js';
|
|
14
|
+
import { REDACTED_HEADERS } from './lib/Serializers.js';
|
|
15
|
+
import { pretty } from './lib/Pretty.js';
|
|
16
|
+
import { COLOR_PALETTE, PLAIN_PALETTE } from './lib/Colors.js';
|
|
17
|
+
import { Destination } from './lib/Destination.js';
|
|
18
|
+
//The loggers with buffered lines to write when the process exits
|
|
19
|
+
const activeLoggers = new Set();
|
|
20
|
+
process.once('exit', () => activeLoggers.forEach((logger) => logger.flush()));
|
|
21
|
+
/**
|
|
22
|
+
* The logger component of the applications built with `lakutata`: the package presets it as the `log` component (and
|
|
23
|
+
* creates it first), so the framework and the application log through it. It writes the pino log format: one JSON
|
|
24
|
+
* object per line, with `level` (10 trace, 20 debug, 30 info, 40 warn, 50 error, 60 fatal), `time` (milliseconds since the epoch),
|
|
25
|
+
* `pid`, `hostname`, `name` (the application's name), the properties of the logged object and `msg`; with `pretty` on
|
|
26
|
+
* (the default), it writes readable lines instead: `[HH:MM:ss.SSS] LEVEL (name/pid): message`, then one indented line
|
|
27
|
+
* per property, in the local time zone.
|
|
28
|
+
*
|
|
29
|
+
* Configure it through the `log` entry of the application's `components` (no `class` needed:
|
|
30
|
+
* `log: {level: 'info', pretty: false}`), and get it with `@Inject('log')` or `getObject('log')`; it is a singleton.
|
|
31
|
+
* The lines are buffered and written at the next turn of the event loop (or once 16 KB are pending), and the pending
|
|
32
|
+
* lines are written synchronously when the process exits or the component is destroyed; `sync: true` writes each line
|
|
33
|
+
* at once instead. A stream with a file descriptor (stdout, stderr, an opened file) is written synchronously through
|
|
34
|
+
* it, so the lines keep their order.
|
|
35
|
+
*
|
|
36
|
+
* An HTTP request or response logged as the object of a line is written under `req` or `res` with the values of its
|
|
37
|
+
* credential headers (`Authorization`, `Cookie`, `Set-Cookie`, `Proxy-Authorization`) replaced by `[Redacted]` (see
|
|
38
|
+
* the `redactedHeaders` option). Nothing else is redacted and there are no child loggers: the other properties are
|
|
39
|
+
* written as is, so log the fields you need rather than secrets or whole objects holding them.
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import {Application, Component} from 'lakutata'
|
|
43
|
+
* import {Inject} from 'lakutata/decorator/di'
|
|
44
|
+
* import {Logger} from 'lakutata/com/logger'
|
|
45
|
+
*
|
|
46
|
+
* class Orders extends Component {
|
|
47
|
+
* @Inject('log')
|
|
48
|
+
* protected readonly log: Logger
|
|
49
|
+
*
|
|
50
|
+
* public pay(orderId: number, amount: number): void {
|
|
51
|
+
* //{"level":30,...,"orderId":42,"msg":"order 42 paid: 9.90"}
|
|
52
|
+
* this.log.info({orderId: orderId}, 'order %d paid: %s', orderId, amount.toFixed(2))
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* public fail(error: Error): void {
|
|
56
|
+
* //The error serialized under "err", its message as the message unless one is given
|
|
57
|
+
* this.log.error(error, 'payment failed')
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* Application.run(() => ({
|
|
62
|
+
* id: 'shop.app',
|
|
63
|
+
* name: 'Shop',
|
|
64
|
+
* components: {
|
|
65
|
+
* //The preset logger: JSON lines from the info level in production
|
|
66
|
+
* log: {level: 'info', pretty: process.env.NODE_ENV !== 'production'},
|
|
67
|
+
* orders: {class: Orders}
|
|
68
|
+
* }
|
|
69
|
+
* }))
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
let Logger = class Logger extends Component {
|
|
73
|
+
#threshold;
|
|
74
|
+
#bindings;
|
|
75
|
+
#pretty;
|
|
76
|
+
#outputs;
|
|
77
|
+
#redacted;
|
|
78
|
+
/**
|
|
79
|
+
* Set up the destinations and the level, and register the logger to flush its pending lines when the process exits.
|
|
80
|
+
* @protected
|
|
81
|
+
*/
|
|
82
|
+
async init() {
|
|
83
|
+
this.#threshold = LEVELS[this.level];
|
|
84
|
+
this.#bindings = logBindings(Application.appName);
|
|
85
|
+
this.#redacted = new Set(this.redactedHeaders.map((name) => name.toLowerCase()));
|
|
86
|
+
this.#outputs = this.destinations.map((stream) => ({
|
|
87
|
+
destination: new Destination(stream, this.sync),
|
|
88
|
+
palette: this.pretty ? (stream === process.stdout && this.colorize ? COLOR_PALETTE : PLAIN_PALETTE) : undefined
|
|
89
|
+
}));
|
|
90
|
+
this.#pretty = this.#outputs.some((output) => !!output.palette);
|
|
91
|
+
activeLoggers.add(this);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Write the pending lines and stop flushing at the process exit.
|
|
95
|
+
* @protected
|
|
96
|
+
*/
|
|
97
|
+
async destroy() {
|
|
98
|
+
activeLoggers.delete(this);
|
|
99
|
+
this.flush();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build a log line and write it to every destination, unless its level is below the configured `level`.
|
|
103
|
+
* @param level The numeric level of the line: 10 trace, 20 debug, 30 info, 40 warn, 50 error, 60 fatal.
|
|
104
|
+
* @param args The arguments of the log method: `[obj, msg, ...args]` or `[msg, ...args]`.
|
|
105
|
+
* @protected
|
|
106
|
+
*/
|
|
107
|
+
write(level, args) {
|
|
108
|
+
if (level < this.#threshold)
|
|
109
|
+
return;
|
|
110
|
+
const record = buildRecord(level, this.#bindings, args, this.#pretty, this.#redacted);
|
|
111
|
+
let prettyLines;
|
|
112
|
+
for (const output of this.#outputs) {
|
|
113
|
+
if (!output.palette) {
|
|
114
|
+
output.destination.write(record.line);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
prettyLines ??= new Map();
|
|
118
|
+
let line = prettyLines.get(output.palette);
|
|
119
|
+
if (line === undefined) {
|
|
120
|
+
//The parsed line is changed by the formatting, it is used once
|
|
121
|
+
line = pretty(record.line.slice(0, -1), output.palette, prettyLines.size ? undefined : record.log);
|
|
122
|
+
prettyLines.set(output.palette, line);
|
|
123
|
+
}
|
|
124
|
+
output.destination.write(line);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Write the buffered lines now, synchronously for the streams with a file descriptor (before a `process.exit()`
|
|
129
|
+
* in a signal handler, for instance). Does nothing with `sync: true`.
|
|
130
|
+
*/
|
|
131
|
+
flush() {
|
|
132
|
+
this.#outputs?.forEach((output) => output.destination.flush());
|
|
133
|
+
}
|
|
134
|
+
fatal(obj, ...msg) {
|
|
135
|
+
this.write(60, [obj, ...msg]);
|
|
136
|
+
//The process may end without a turn of the event loop
|
|
137
|
+
this.flush();
|
|
138
|
+
}
|
|
139
|
+
error(obj, ...msg) {
|
|
140
|
+
return this.write(50, [obj, ...msg]);
|
|
141
|
+
}
|
|
142
|
+
warn(obj, ...msg) {
|
|
143
|
+
return this.write(40, [obj, ...msg]);
|
|
144
|
+
}
|
|
145
|
+
info(obj, ...msg) {
|
|
146
|
+
return this.write(30, [obj, ...msg]);
|
|
147
|
+
}
|
|
148
|
+
debug(obj, ...msg) {
|
|
149
|
+
return this.write(20, [obj, ...msg]);
|
|
150
|
+
}
|
|
151
|
+
trace(obj, ...msg) {
|
|
152
|
+
return this.write(10, [obj, ...msg]);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
__decorate([
|
|
156
|
+
Configurable(DTO.Boolean().optional().default(true)),
|
|
157
|
+
__metadata("design:type", Boolean)
|
|
158
|
+
], Logger.prototype, "pretty", void 0);
|
|
159
|
+
__decorate([
|
|
160
|
+
Configurable(DTO.String().valid(...Object.keys(LEVELS)).optional().default('trace')),
|
|
161
|
+
__metadata("design:type", String)
|
|
162
|
+
], Logger.prototype, "level", void 0);
|
|
163
|
+
__decorate([
|
|
164
|
+
Configurable(DTO.Boolean().optional().default(true)),
|
|
165
|
+
__metadata("design:type", Boolean)
|
|
166
|
+
], Logger.prototype, "colorize", void 0);
|
|
167
|
+
__decorate([
|
|
168
|
+
Configurable(DTO.Boolean().optional().default(false)),
|
|
169
|
+
__metadata("design:type", Boolean)
|
|
170
|
+
], Logger.prototype, "sync", void 0);
|
|
171
|
+
__decorate([
|
|
172
|
+
Configurable(DTO.Array(DTO.InstanceOf(Stream)).optional().default(() => [process.stdout])),
|
|
173
|
+
__metadata("design:type", Array)
|
|
174
|
+
], Logger.prototype, "destinations", void 0);
|
|
175
|
+
__decorate([
|
|
176
|
+
Configurable(DTO.Array(DTO.String()).optional().default(() => [...REDACTED_HEADERS])),
|
|
177
|
+
__metadata("design:type", Array)
|
|
178
|
+
], Logger.prototype, "redactedHeaders", void 0);
|
|
179
|
+
Logger = __decorate([
|
|
180
|
+
Singleton()
|
|
181
|
+
], Logger);
|
|
182
|
+
export { Logger };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../Logger.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace the close sequences of a text by the open sequence, so a color is not closed early by a nested one
|
|
3
|
+
* @param text
|
|
4
|
+
* @param index
|
|
5
|
+
* @param close
|
|
6
|
+
* @param open
|
|
7
|
+
*/
|
|
8
|
+
function replaceClose(text, index, close, open) {
|
|
9
|
+
let result = '';
|
|
10
|
+
let tail = text;
|
|
11
|
+
while (index >= 0) {
|
|
12
|
+
result += tail.substring(0, index) + open;
|
|
13
|
+
tail = tail.substring(index + close.length);
|
|
14
|
+
index = tail.indexOf(close);
|
|
15
|
+
}
|
|
16
|
+
return result + tail;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* An ANSI color
|
|
20
|
+
* @param open
|
|
21
|
+
* @param close
|
|
22
|
+
*/
|
|
23
|
+
function color(open, close) {
|
|
24
|
+
const openSequence = `\x1b[${open}m`;
|
|
25
|
+
const closeSequence = `\x1b[${close}m`;
|
|
26
|
+
return (input) => {
|
|
27
|
+
//Empty texts stay uncolored
|
|
28
|
+
if (input === '' || input === undefined)
|
|
29
|
+
return '';
|
|
30
|
+
const text = `${input}`;
|
|
31
|
+
const index = text.indexOf(closeSequence, openSequence.length + 1);
|
|
32
|
+
return index < 0 ? openSequence + text + closeSequence : openSequence + replaceClose(text, index, closeSequence, openSequence) + closeSequence;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const plain = (input) => input;
|
|
36
|
+
export const PLAIN_PALETTE = {
|
|
37
|
+
level: { default: plain, 60: plain, 50: plain, 40: plain, 30: plain, 20: plain, 10: plain },
|
|
38
|
+
message: plain,
|
|
39
|
+
property: plain
|
|
40
|
+
};
|
|
41
|
+
export const COLOR_PALETTE = {
|
|
42
|
+
level: { default: color(37, 39), 60: color(41, 49), 50: color(31, 39), 40: color(33, 39), 30: color(32, 39), 20: color(34, 39), 10: color(90, 39) },
|
|
43
|
+
message: color(36, 39),
|
|
44
|
+
property: color(35, 39)
|
|
45
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { writeSync } from 'node:fs';
|
|
2
|
+
//The buffered text written at once
|
|
3
|
+
const HIGH_WATER_MARK = 16384;
|
|
4
|
+
/**
|
|
5
|
+
* The array the writes wait on while a pipe is full, created when first needed: SharedArrayBuffer is missing where the
|
|
6
|
+
* logger may be loaded without logging (a renderer of Electron which is not cross-origin isolated)
|
|
7
|
+
*/
|
|
8
|
+
let sleeper;
|
|
9
|
+
/**
|
|
10
|
+
* Wait synchronously
|
|
11
|
+
* @param ms
|
|
12
|
+
*/
|
|
13
|
+
function sleepSync(ms) {
|
|
14
|
+
if (typeof SharedArrayBuffer === 'function') {
|
|
15
|
+
sleeper ??= new Int32Array(new SharedArrayBuffer(4));
|
|
16
|
+
Atomics.wait(sleeper, 0, 0, ms);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const until = Date.now() + ms;
|
|
20
|
+
while (Date.now() < until) {
|
|
21
|
+
//Busy wait, without SharedArrayBuffer
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A destination of the log lines, buffered unless synchronous
|
|
26
|
+
* The buffered lines are written in the next event loop iteration, and synchronously when the process exits
|
|
27
|
+
* The streams with a file descriptor (stdout, stderr, files) are written through it, so the lines stay in order
|
|
28
|
+
*/
|
|
29
|
+
export class Destination {
|
|
30
|
+
#buffer = '';
|
|
31
|
+
#scheduled = false;
|
|
32
|
+
constructor(stream, sync) {
|
|
33
|
+
this.stream = stream;
|
|
34
|
+
this.sync = sync;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The file descriptor of the stream, if it has one
|
|
38
|
+
* @protected
|
|
39
|
+
*/
|
|
40
|
+
get fd() {
|
|
41
|
+
const fd = this.stream.fd;
|
|
42
|
+
return typeof fd === 'number' ? fd : undefined;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Write a text to the file descriptor, retrying when the descriptor is busy
|
|
46
|
+
* @param fd
|
|
47
|
+
* @param text
|
|
48
|
+
* @protected
|
|
49
|
+
*/
|
|
50
|
+
writeFd(fd, text) {
|
|
51
|
+
let data = Buffer.from(text);
|
|
52
|
+
while (data.length) {
|
|
53
|
+
try {
|
|
54
|
+
data = data.subarray(writeSync(fd, data));
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (e.code !== 'EAGAIN')
|
|
58
|
+
throw e;
|
|
59
|
+
//A pipe is full, wait for its reader
|
|
60
|
+
sleepSync(10);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Write a text
|
|
66
|
+
* @param text
|
|
67
|
+
*/
|
|
68
|
+
write(text) {
|
|
69
|
+
if (this.sync)
|
|
70
|
+
return this.output(text);
|
|
71
|
+
this.#buffer += text;
|
|
72
|
+
if (this.#buffer.length >= HIGH_WATER_MARK)
|
|
73
|
+
return this.flush();
|
|
74
|
+
if (this.#scheduled)
|
|
75
|
+
return;
|
|
76
|
+
this.#scheduled = true;
|
|
77
|
+
setImmediate(() => this.flush());
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Write a text to the stream, through its file descriptor if it has one
|
|
81
|
+
* @param text
|
|
82
|
+
* @protected
|
|
83
|
+
*/
|
|
84
|
+
output(text) {
|
|
85
|
+
const fd = this.fd;
|
|
86
|
+
fd === undefined ? this.stream.write(text) : this.writeFd(fd, text);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Write the buffered text
|
|
90
|
+
*/
|
|
91
|
+
flush() {
|
|
92
|
+
this.#scheduled = false;
|
|
93
|
+
if (!this.#buffer)
|
|
94
|
+
return;
|
|
95
|
+
const text = this.#buffer;
|
|
96
|
+
this.#buffer = '';
|
|
97
|
+
this.output(text);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//Derived from the upstream projects listed in THIRD_PARTY_NOTICES.md (rewritten in TypeScript)
|
|
2
|
+
import { stringify } from './Stringify.js';
|
|
3
|
+
/**
|
|
4
|
+
* Format a log message with its arguments: %s, %d, %f, %i, %o, %O, %j and %%
|
|
5
|
+
* The arguments without placeholder are ignored
|
|
6
|
+
* @param template
|
|
7
|
+
* @param args
|
|
8
|
+
*/
|
|
9
|
+
export function format(template, args) {
|
|
10
|
+
if (typeof template === 'object' && template !== null) {
|
|
11
|
+
const count = args.length + 1;
|
|
12
|
+
if (count === 1)
|
|
13
|
+
return template;
|
|
14
|
+
//Serialize the object and the arguments after the first one (the behavior of 2.x)
|
|
15
|
+
const objects = new Array(count);
|
|
16
|
+
objects[0] = stringify(template);
|
|
17
|
+
for (let index = 1; index < count; index++)
|
|
18
|
+
objects[index] = stringify(args[index]);
|
|
19
|
+
return objects.join(' ');
|
|
20
|
+
}
|
|
21
|
+
if (typeof template !== 'string' || !args.length)
|
|
22
|
+
return template;
|
|
23
|
+
let result = '';
|
|
24
|
+
let argument = 0;
|
|
25
|
+
let last = -1;
|
|
26
|
+
const length = template.length;
|
|
27
|
+
for (let index = 0; index < length;) {
|
|
28
|
+
if (template.charCodeAt(index) === 37 && index + 1 < length) {
|
|
29
|
+
last = last > -1 ? last : 0;
|
|
30
|
+
switch (template.charCodeAt(index + 1)) {
|
|
31
|
+
//d, f
|
|
32
|
+
case 100:
|
|
33
|
+
case 102:
|
|
34
|
+
if (argument >= args.length || args[argument] == null)
|
|
35
|
+
break;
|
|
36
|
+
if (last < index)
|
|
37
|
+
result += template.slice(last, index);
|
|
38
|
+
result += Number(args[argument]);
|
|
39
|
+
last = index + 2;
|
|
40
|
+
index++;
|
|
41
|
+
break;
|
|
42
|
+
//i
|
|
43
|
+
case 105:
|
|
44
|
+
if (argument >= args.length || args[argument] == null)
|
|
45
|
+
break;
|
|
46
|
+
if (last < index)
|
|
47
|
+
result += template.slice(last, index);
|
|
48
|
+
result += Math.floor(Number(args[argument]));
|
|
49
|
+
last = index + 2;
|
|
50
|
+
index++;
|
|
51
|
+
break;
|
|
52
|
+
//O, o, j
|
|
53
|
+
case 79:
|
|
54
|
+
case 111:
|
|
55
|
+
case 106: {
|
|
56
|
+
if (argument >= args.length || args[argument] === undefined)
|
|
57
|
+
break;
|
|
58
|
+
if (last < index)
|
|
59
|
+
result += template.slice(last, index);
|
|
60
|
+
const value = args[argument];
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
result += `'${value}'`;
|
|
63
|
+
}
|
|
64
|
+
else if (typeof value === 'function') {
|
|
65
|
+
result += value.name || '<anonymous>';
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
result += stringify(value);
|
|
69
|
+
}
|
|
70
|
+
last = index + 2;
|
|
71
|
+
index++;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
//s
|
|
75
|
+
case 115:
|
|
76
|
+
if (argument >= args.length)
|
|
77
|
+
break;
|
|
78
|
+
if (last < index)
|
|
79
|
+
result += template.slice(last, index);
|
|
80
|
+
result += String(args[argument]);
|
|
81
|
+
last = index + 2;
|
|
82
|
+
index++;
|
|
83
|
+
break;
|
|
84
|
+
//%
|
|
85
|
+
case 37:
|
|
86
|
+
if (last < index)
|
|
87
|
+
result += template.slice(last, index);
|
|
88
|
+
result += '%';
|
|
89
|
+
last = index + 2;
|
|
90
|
+
index++;
|
|
91
|
+
argument--;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
++argument;
|
|
95
|
+
}
|
|
96
|
+
++index;
|
|
97
|
+
}
|
|
98
|
+
if (last === -1)
|
|
99
|
+
return template;
|
|
100
|
+
if (last < length)
|
|
101
|
+
result += template.slice(last);
|
|
102
|
+
return result;
|
|
103
|
+
}
|