@kikiutils/shared 18.3.0 → 20.0.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 +0 -7
- package/dist/buffer.d.ts +0 -2
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +1 -2
- package/dist/classes/enhanced-date.d.ts +0 -2
- package/dist/classes/enhanced-date.d.ts.map +1 -1
- package/dist/classes/enhanced-date.js +1 -2
- package/dist/classes/path.d.ts +1 -3
- package/dist/classes/path.d.ts.map +1 -1
- package/dist/classes/path.js +1 -2
- package/dist/classes/precision-number.d.ts +0 -2
- package/dist/classes/precision-number.d.ts.map +1 -1
- package/dist/classes/precision-number.js +1 -2
- package/dist/clipboard.d.ts +0 -2
- package/dist/clipboard.d.ts.map +1 -1
- package/dist/clipboard.js +1 -2
- package/dist/crypto-hash.d.ts +0 -2
- package/dist/crypto-hash.d.ts.map +1 -1
- package/dist/crypto-hash.js +1 -2
- package/dist/datetime.d.ts +0 -2
- package/dist/datetime.d.ts.map +1 -1
- package/dist/datetime.js +1 -2
- package/dist/element-plus.d.ts +0 -2
- package/dist/element-plus.d.ts.map +1 -1
- package/dist/element-plus.js +1 -2
- package/dist/enum.d.ts +0 -2
- package/dist/enum.d.ts.map +1 -1
- package/dist/enum.js +1 -2
- package/dist/env.d.ts +0 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +1 -2
- package/dist/event-awaiter.d.ts +0 -2
- package/dist/event-awaiter.d.ts.map +1 -1
- package/dist/event-awaiter.js +1 -2
- package/dist/file.d.ts +0 -2
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +1 -2
- package/dist/general.d.ts +0 -2
- package/dist/general.d.ts.map +1 -1
- package/dist/general.js +1 -2
- package/dist/hash.d.ts +0 -2
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +1 -2
- package/dist/logger/_internals/formatter.d.ts +13 -0
- package/dist/logger/_internals/formatter.d.ts.map +1 -0
- package/dist/logger/_internals/formatter.js +57 -0
- package/dist/logger/_internals/formatter.js.map +1 -0
- package/dist/logger/_internals/levels.d.ts +54 -0
- package/dist/logger/_internals/levels.d.ts.map +1 -0
- package/dist/logger/_internals/levels.js +59 -0
- package/dist/logger/_internals/levels.js.map +1 -0
- package/dist/logger/_internals/logger.d.ts +12 -0
- package/dist/logger/_internals/logger.d.ts.map +1 -0
- package/dist/logger/_internals/logger.js +147 -0
- package/dist/logger/_internals/logger.js.map +1 -0
- package/dist/logger/_internals/types.d.ts +12 -0
- package/dist/logger/_internals/types.d.ts.map +1 -0
- package/dist/logger/_internals/types.js +1 -0
- package/dist/logger/index.d.ts +10 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +25 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/node/_internals/file-writer.d.ts +11 -0
- package/dist/logger/node/_internals/file-writer.d.ts.map +1 -0
- package/dist/logger/node/_internals/file-writer.js +188 -0
- package/dist/logger/node/_internals/file-writer.js.map +1 -0
- package/dist/logger/node/_internals/formatter.d.ts +4 -0
- package/dist/logger/node/_internals/formatter.d.ts.map +1 -0
- package/dist/logger/node/_internals/formatter.js +21 -0
- package/dist/logger/node/_internals/formatter.js.map +1 -0
- package/dist/logger/node/index.d.ts +10 -0
- package/dist/logger/node/index.d.ts.map +1 -0
- package/dist/logger/node/index.js +123 -0
- package/dist/logger/node/index.js.map +1 -0
- package/dist/logger/node/types.d.ts +43 -0
- package/dist/logger/node/types.d.ts.map +1 -0
- package/dist/logger/node/types.js +1 -0
- package/dist/logger/types.d.ts +50 -0
- package/dist/logger/types.d.ts.map +1 -0
- package/dist/logger/types.js +1 -0
- package/dist/math.d.ts +0 -2
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +1 -2
- package/dist/number.d.ts +0 -2
- package/dist/number.d.ts.map +1 -1
- package/dist/number.js +1 -2
- package/dist/random.d.ts +0 -2
- package/dist/random.d.ts.map +1 -1
- package/dist/random.js +1 -2
- package/dist/redis/script-runner.d.ts +0 -2
- package/dist/redis/script-runner.d.ts.map +1 -1
- package/dist/redis/script-runner.js +1 -2
- package/dist/storages/lru/keyed-store.d.ts +0 -2
- package/dist/storages/lru/keyed-store.d.ts.map +1 -1
- package/dist/storages/lru/keyed-store.js +1 -2
- package/dist/storages/redis/keyed-store.d.ts +4 -4
- package/dist/storages/redis/keyed-store.d.ts.map +1 -1
- package/dist/storages/redis/keyed-store.js +7 -4
- package/dist/storages/redis/keyed-store.js.map +1 -1
- package/dist/storages/redis/msgpack.d.ts +10 -3
- package/dist/storages/redis/msgpack.d.ts.map +1 -1
- package/dist/storages/redis/msgpack.js +18 -3
- package/dist/storages/redis/msgpack.js.map +1 -1
- package/dist/storages/redis/types.d.ts +30 -2
- package/dist/storages/redis/types.d.ts.map +1 -1
- package/dist/string.d.ts +0 -2
- package/dist/string.d.ts.map +1 -1
- package/dist/string.js +1 -2
- package/dist/time.d.ts +0 -2
- package/dist/time.d.ts.map +1 -1
- package/dist/time.js +1 -2
- package/dist/types/filtered-key-path.d.ts +0 -2
- package/dist/types/filtered-key-path.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/vue.d.ts +0 -2
- package/dist/types/vue.d.ts.map +1 -1
- package/dist/url.d.ts +0 -2
- package/dist/url.d.ts.map +1 -1
- package/dist/url.js +1 -2
- package/dist/vue.d.ts +0 -2
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +1 -2
- package/dist/web.d.ts +0 -2
- package/dist/web.d.ts.map +1 -1
- package/dist/web.js +1 -2
- package/package.json +73 -45
- package/src/logger/_internals/formatter.ts +80 -0
- package/src/logger/_internals/levels.ts +60 -0
- package/src/logger/_internals/logger.ts +224 -0
- package/src/logger/_internals/types.ts +12 -0
- package/src/logger/index.ts +34 -0
- package/src/logger/node/_internals/file-writer.ts +298 -0
- package/src/logger/node/_internals/formatter.ts +33 -0
- package/src/logger/node/index.ts +173 -0
- package/src/logger/node/types.ts +58 -0
- package/src/logger/types.ts +60 -0
- package/src/storages/redis/keyed-store.ts +6 -2
- package/src/storages/redis/msgpack.ts +17 -1
- package/src/storages/redis/types.ts +34 -0
- package/src/types/index.ts +0 -10
- package/dist/classes/ssh-client.d.ts +0 -40
- package/dist/classes/ssh-client.d.ts.map +0 -1
- package/dist/classes/ssh-client.js +0 -130
- package/dist/classes/ssh-client.js.map +0 -1
- package/dist/consola.d.ts +0 -24
- package/dist/consola.d.ts.map +0 -1
- package/dist/consola.js +0 -29
- package/dist/consola.js.map +0 -1
- package/dist/object.d.ts +0 -44
- package/dist/object.d.ts.map +0 -1
- package/dist/object.js +0 -59
- package/dist/object.js.map +0 -1
- package/dist/pino.d.ts +0 -25
- package/dist/pino.d.ts.map +0 -1
- package/dist/pino.js +0 -34
- package/dist/pino.js.map +0 -1
- package/src/classes/ssh-client.ts +0 -175
- package/src/consola.ts +0 -25
- package/src/object.ts +0 -78
- package/src/pino.ts +0 -33
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { LoggerMethod } from '../types';
|
|
2
|
+
|
|
3
|
+
// Constants/Variables
|
|
4
|
+
export const loggerMethods = {
|
|
5
|
+
debug: {
|
|
6
|
+
ansiColor: '36',
|
|
7
|
+
consoleMethod: 'debug',
|
|
8
|
+
cssColor: '#0891b2',
|
|
9
|
+
icon: '⚙',
|
|
10
|
+
severity: 20,
|
|
11
|
+
},
|
|
12
|
+
error: {
|
|
13
|
+
ansiColor: '31',
|
|
14
|
+
consoleMethod: 'error',
|
|
15
|
+
cssColor: '#dc2626',
|
|
16
|
+
icon: '✖',
|
|
17
|
+
severity: 50,
|
|
18
|
+
},
|
|
19
|
+
fatal: {
|
|
20
|
+
ansiColor: '31',
|
|
21
|
+
consoleMethod: 'error',
|
|
22
|
+
cssColor: '#dc2626',
|
|
23
|
+
icon: '✖',
|
|
24
|
+
severity: 60,
|
|
25
|
+
},
|
|
26
|
+
info: {
|
|
27
|
+
ansiColor: '36',
|
|
28
|
+
consoleMethod: 'info',
|
|
29
|
+
cssColor: '#0891b2',
|
|
30
|
+
icon: 'ℹ',
|
|
31
|
+
severity: 30,
|
|
32
|
+
},
|
|
33
|
+
success: {
|
|
34
|
+
ansiColor: '32',
|
|
35
|
+
consoleMethod: 'info',
|
|
36
|
+
cssColor: '#16a34a',
|
|
37
|
+
icon: '✔',
|
|
38
|
+
severity: 30,
|
|
39
|
+
},
|
|
40
|
+
trace: {
|
|
41
|
+
ansiColor: '90',
|
|
42
|
+
consoleMethod: 'debug',
|
|
43
|
+
cssColor: '#808080',
|
|
44
|
+
icon: '·',
|
|
45
|
+
severity: 10,
|
|
46
|
+
},
|
|
47
|
+
warn: {
|
|
48
|
+
ansiColor: '33',
|
|
49
|
+
consoleMethod: 'warn',
|
|
50
|
+
cssColor: '#ca8a04',
|
|
51
|
+
icon: '⚠',
|
|
52
|
+
severity: 40,
|
|
53
|
+
},
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
// Functions
|
|
57
|
+
export function getLoggerStreamName(type: LoggerMethod): 'stderr' | 'stdout' {
|
|
58
|
+
const method = loggerMethods[type].consoleMethod;
|
|
59
|
+
return method === 'warn' || method === 'error' ? 'stderr' : 'stdout';
|
|
60
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Logger,
|
|
3
|
+
LoggerDiagnostic,
|
|
4
|
+
LoggerLevel,
|
|
5
|
+
LoggerMethod,
|
|
6
|
+
LoggerOptions,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
import { loggerMethods } from './levels';
|
|
10
|
+
import type { LoggerRecord } from './types';
|
|
11
|
+
|
|
12
|
+
interface LoggerHooks {
|
|
13
|
+
/** A non-throwing reporter created by createDiagnosticReporter. */
|
|
14
|
+
report: (event: LoggerDiagnostic) => void;
|
|
15
|
+
write: (record: LoggerRecord) => Promise<void> | void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Constants/Variables
|
|
19
|
+
const booleanPresentationKeys = [
|
|
20
|
+
'console',
|
|
21
|
+
'icons',
|
|
22
|
+
'timestamp',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const stringPresentationKeys = [
|
|
26
|
+
'prefix',
|
|
27
|
+
'suffix',
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
// Functions
|
|
31
|
+
|
|
32
|
+
/** A non-recursive, rate-limited diagnostic boundary shared by server and browser adapters. */
|
|
33
|
+
export function createDiagnosticReporter(
|
|
34
|
+
onError?: (event: Readonly<LoggerDiagnostic>) => void,
|
|
35
|
+
fallback?: (message: string) => void,
|
|
36
|
+
): (event: LoggerDiagnostic) => void {
|
|
37
|
+
let reporting = false;
|
|
38
|
+
let callbackPending = false;
|
|
39
|
+
const stateByType = new Map<LoggerDiagnostic['type'], { lastReportedAt: number; suppressedCount: number }>();
|
|
40
|
+
|
|
41
|
+
return (event) => {
|
|
42
|
+
if (reporting) return;
|
|
43
|
+
try {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const state = stateByType.get(event.type);
|
|
46
|
+
if (state && now - state.lastReportedAt < 1000) {
|
|
47
|
+
state.suppressedCount++;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
reporting = true;
|
|
52
|
+
const suppressedCount = state?.suppressedCount ?? 0;
|
|
53
|
+
if (suppressedCount) {
|
|
54
|
+
event = {
|
|
55
|
+
...event,
|
|
56
|
+
suppressedCount,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
stateByType.set(
|
|
61
|
+
event.type,
|
|
62
|
+
{
|
|
63
|
+
lastReportedAt: now,
|
|
64
|
+
suppressedCount: 0,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const summary = suppressedCount ? ` (${suppressedCount} similar diagnostics suppressed)` : '';
|
|
70
|
+
fallback?.(`[logger] ${event.message}${summary}\n`);
|
|
71
|
+
} catch {
|
|
72
|
+
// Diagnostics must never re-enter the normal logging pipeline.
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!callbackPending) {
|
|
76
|
+
try {
|
|
77
|
+
callbackPending = true;
|
|
78
|
+
const result: unknown = onError?.(Object.freeze({ ...event }));
|
|
79
|
+
if (result !== null && (typeof result === 'object' || typeof result === 'function')) {
|
|
80
|
+
void Promise.resolve(result).then(
|
|
81
|
+
() => {
|
|
82
|
+
callbackPending = false;
|
|
83
|
+
},
|
|
84
|
+
() => {
|
|
85
|
+
callbackPending = false;
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
callbackPending = false;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
callbackPending = false;
|
|
93
|
+
// User diagnostic callbacks have the same non-throwing boundary.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch { /* Diagnostics must never escape into the logging pipeline. */ } finally {
|
|
97
|
+
reporting = false;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createLoggerCore(options: LoggerOptions, hooks: LoggerHooks): Readonly<Logger> {
|
|
103
|
+
validateLevel(options.level);
|
|
104
|
+
validatePresentation(options);
|
|
105
|
+
if (options.onError !== undefined && typeof options.onError !== 'function') {
|
|
106
|
+
throw new TypeError('Logger onError must be a function');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const presentation = {
|
|
110
|
+
console: options.console ?? true,
|
|
111
|
+
icons: options.icons ?? true,
|
|
112
|
+
prefix: options.prefix ?? '',
|
|
113
|
+
suffix: options.suffix ?? '',
|
|
114
|
+
timestamp: options.timestamp ?? true,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const { report } = hooks;
|
|
118
|
+
|
|
119
|
+
let override = options.level === 'env' ? undefined : options.level;
|
|
120
|
+
let previousEnvironment: string | undefined;
|
|
121
|
+
let currentLevel: LoggerLevel = override ?? 'info';
|
|
122
|
+
let environmentRead = false;
|
|
123
|
+
|
|
124
|
+
function refreshLevel() {
|
|
125
|
+
if (override !== undefined) return;
|
|
126
|
+
const input = readEnvironmentLevel();
|
|
127
|
+
if (environmentRead && input === previousEnvironment) return;
|
|
128
|
+
environmentRead = true;
|
|
129
|
+
previousEnvironment = input;
|
|
130
|
+
const normalized = input?.trim().toLowerCase() || undefined;
|
|
131
|
+
currentLevel = isLevel(normalized) ? normalized : 'info';
|
|
132
|
+
if (normalized !== undefined && !isLevel(normalized)) {
|
|
133
|
+
report({
|
|
134
|
+
message: 'Invalid LOG_LEVEL; using info',
|
|
135
|
+
type: 'invalid-level',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const logger = {
|
|
141
|
+
configure(configuration) {
|
|
142
|
+
validatePresentation(configuration);
|
|
143
|
+
for (const name of booleanPresentationKeys) {
|
|
144
|
+
const value = configuration[name];
|
|
145
|
+
if (value !== undefined) presentation[name] = value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const name of stringPresentationKeys) {
|
|
149
|
+
const value = configuration[name];
|
|
150
|
+
if (value !== undefined) presentation[name] = value;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
isLevelEnabled(level) {
|
|
154
|
+
if (level !== 'success' && !isLevel(level)) throw new TypeError('Invalid logger level');
|
|
155
|
+
refreshLevel();
|
|
156
|
+
return level !== 'silent' && getLevelSeverity(level) >= getLevelSeverity(currentLevel);
|
|
157
|
+
},
|
|
158
|
+
setLevel(level) {
|
|
159
|
+
validateLevel(level);
|
|
160
|
+
if (level === undefined) throw new TypeError('Invalid logger level');
|
|
161
|
+
override = level === 'env' ? undefined : level;
|
|
162
|
+
environmentRead = false;
|
|
163
|
+
if (override !== undefined) {
|
|
164
|
+
currentLevel = override;
|
|
165
|
+
} else {
|
|
166
|
+
refreshLevel();
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
} as Logger;
|
|
170
|
+
|
|
171
|
+
for (const type of Object.keys(loggerMethods) as LoggerMethod[]) {
|
|
172
|
+
logger[type] = async (...args: unknown[]) => {
|
|
173
|
+
try {
|
|
174
|
+
refreshLevel();
|
|
175
|
+
if (loggerMethods[type].severity < getLevelSeverity(currentLevel)) return;
|
|
176
|
+
await hooks.write({
|
|
177
|
+
...presentation,
|
|
178
|
+
args,
|
|
179
|
+
date: new Date(),
|
|
180
|
+
type,
|
|
181
|
+
});
|
|
182
|
+
} catch (error) {
|
|
183
|
+
report({
|
|
184
|
+
error,
|
|
185
|
+
message: 'Logger output failed',
|
|
186
|
+
type: 'format',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return Object.freeze(logger);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getLevelSeverity(level: LoggerLevel | LoggerMethod): number {
|
|
196
|
+
return level === 'silent' ? Number.POSITIVE_INFINITY : loggerMethods[level].severity;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isLevel(value: unknown): value is LoggerLevel {
|
|
200
|
+
return typeof value === 'string'
|
|
201
|
+
&& (value === 'silent' || (value !== 'success' && Object.hasOwn(loggerMethods, value)));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readEnvironmentLevel(): string | undefined {
|
|
205
|
+
return (globalThis as { process?: { env?: { LOG_LEVEL?: string } } }).process?.env?.LOG_LEVEL;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateLevel(value: unknown): asserts value is 'env' | LoggerLevel | undefined {
|
|
209
|
+
if (value !== undefined && value !== 'env' && !isLevel(value)) throw new TypeError('Invalid logger level');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function validatePresentation(options: Pick<LoggerOptions, 'console' | 'icons' | 'prefix' | 'suffix' | 'timestamp'>) {
|
|
213
|
+
for (const name of booleanPresentationKeys) {
|
|
214
|
+
if (options[name] !== undefined && typeof options[name] !== 'boolean') {
|
|
215
|
+
throw new TypeError(`Logger ${name} must be a boolean`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const name of stringPresentationKeys) {
|
|
220
|
+
if (options[name] !== undefined && typeof options[name] !== 'string') {
|
|
221
|
+
throw new TypeError(`Logger ${name} must be a string`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { formatLoggerConsoleArgs } from './_internals/formatter';
|
|
2
|
+
import { loggerMethods } from './_internals/levels';
|
|
3
|
+
import {
|
|
4
|
+
createDiagnosticReporter,
|
|
5
|
+
createLoggerCore,
|
|
6
|
+
} from './_internals/logger';
|
|
7
|
+
import type {
|
|
8
|
+
Logger,
|
|
9
|
+
LoggerOptions,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
// Functions
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates an independent, browser-safe console logger.
|
|
16
|
+
*
|
|
17
|
+
* Log calls never throw or reject for recoverable output failures. Awaiting a call
|
|
18
|
+
* waits for the console invocation, not display or durable delivery. Unspecified
|
|
19
|
+
* levels follow LOG_LEVEL when a process environment is available, otherwise info.
|
|
20
|
+
*/
|
|
21
|
+
export function createLogger(options: LoggerOptions = {}): Readonly<Logger> {
|
|
22
|
+
const target = globalThis.console;
|
|
23
|
+
const fallback = target?.error?.bind(target);
|
|
24
|
+
return createLoggerCore(
|
|
25
|
+
options,
|
|
26
|
+
{
|
|
27
|
+
report: createDiagnosticReporter(options.onError, (message) => fallback?.(message.trimEnd())),
|
|
28
|
+
write(record) {
|
|
29
|
+
if (!record.console) return;
|
|
30
|
+
target?.[loggerMethods[record.type].consoleMethod](...formatLoggerConsoleArgs(record));
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { mkdir } from 'node:fs/promises';
|
|
4
|
+
import {
|
|
5
|
+
basename,
|
|
6
|
+
dirname,
|
|
7
|
+
} from 'node:path';
|
|
8
|
+
import type { Writable } from 'node:stream';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import { Path } from '../../../classes/path';
|
|
12
|
+
import type { LoggerDiagnostic } from '../../types';
|
|
13
|
+
import type {
|
|
14
|
+
LoggerFileOptions,
|
|
15
|
+
NodeLoggerStats,
|
|
16
|
+
} from '../types';
|
|
17
|
+
|
|
18
|
+
interface LoggerFileWriter {
|
|
19
|
+
close: () => Promise<void>;
|
|
20
|
+
getStats: () => Readonly<NodeLoggerStats>;
|
|
21
|
+
write: (text: string) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Constants/Variables
|
|
25
|
+
const defaultMaxPendingBytes = 8 * 1024 * 1024;
|
|
26
|
+
|
|
27
|
+
// Functions
|
|
28
|
+
|
|
29
|
+
/** A bounded admission layer over the stream's own ordered write buffer; not a second FIFO. */
|
|
30
|
+
export async function createFileWriter(
|
|
31
|
+
options: LoggerFileOptions,
|
|
32
|
+
report: (event: LoggerDiagnostic) => void,
|
|
33
|
+
): Promise<Readonly<LoggerFileWriter>> {
|
|
34
|
+
const {
|
|
35
|
+
maxPendingBytes,
|
|
36
|
+
path,
|
|
37
|
+
rotate,
|
|
38
|
+
} = validateFileOptions(options);
|
|
39
|
+
|
|
40
|
+
const createRotatingStream = rotate ? (await import('rotating-file-stream')).createStream : undefined;
|
|
41
|
+
await mkdir(dirname(path), { recursive: true });
|
|
42
|
+
const stream: Writable = rotate && createRotatingStream
|
|
43
|
+
? createRotatingStream(
|
|
44
|
+
basename(path),
|
|
45
|
+
{
|
|
46
|
+
compress: rotate.compress === false ? false : 'gzip',
|
|
47
|
+
...rotate.interval === undefined ? {} : { interval: rotate.interval === 'hourly' ? '1h' : '1d' },
|
|
48
|
+
intervalBoundary: true,
|
|
49
|
+
intervalUTC: false,
|
|
50
|
+
maxFiles: rotate.keepFiles ?? 30,
|
|
51
|
+
...rotate.maxTotalSize === undefined ? {} : { maxSize: rotate.maxTotalSize },
|
|
52
|
+
path: dirname(path),
|
|
53
|
+
...rotate.maxSize === undefined ? {} : { size: rotate.maxSize },
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
: createWriteStream(path, { flags: 'a' });
|
|
57
|
+
|
|
58
|
+
let pendingBytes = 0;
|
|
59
|
+
let droppedCount = 0;
|
|
60
|
+
let writeErrorCount = 0;
|
|
61
|
+
let failed = false;
|
|
62
|
+
let closing = false;
|
|
63
|
+
let closed = false;
|
|
64
|
+
let closePromise: Promise<void> | undefined;
|
|
65
|
+
let initializing = true;
|
|
66
|
+
const unfinished = new Set<() => void>();
|
|
67
|
+
let rejectInitialization: ((error: unknown) => void) | undefined;
|
|
68
|
+
|
|
69
|
+
const settleAll = () => {
|
|
70
|
+
for (const settle of unfinished) settle();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const fail = (error: unknown) => {
|
|
74
|
+
rejectInitialization?.(error);
|
|
75
|
+
if (!failed) {
|
|
76
|
+
failed = true;
|
|
77
|
+
if (!initializing) {
|
|
78
|
+
writeErrorCount++;
|
|
79
|
+
report({
|
|
80
|
+
error,
|
|
81
|
+
message: 'File output failed and has been disabled.',
|
|
82
|
+
type: 'file',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
settleAll();
|
|
88
|
+
if (!stream.destroyed) stream.destroy();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
stream.on('error', fail);
|
|
92
|
+
stream.on(
|
|
93
|
+
'warning',
|
|
94
|
+
(error: unknown) => {
|
|
95
|
+
writeErrorCount++;
|
|
96
|
+
report({
|
|
97
|
+
error,
|
|
98
|
+
message: 'File rotation cleanup failed.',
|
|
99
|
+
type: 'file',
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
stream.on(
|
|
105
|
+
'close',
|
|
106
|
+
() => {
|
|
107
|
+
closed = true;
|
|
108
|
+
if (!closing && !failed) fail(new Error('File stream closed unexpectedly.'));
|
|
109
|
+
settleAll();
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// A write callback also waits for RFS's asynchronous initialization and initial rotation.
|
|
114
|
+
try {
|
|
115
|
+
await new Promise<void>((resolveReady, rejectReady) => {
|
|
116
|
+
rejectInitialization = rejectReady;
|
|
117
|
+
stream.write(
|
|
118
|
+
Buffer.alloc(0),
|
|
119
|
+
(error) => {
|
|
120
|
+
if (error) rejectReady(error);
|
|
121
|
+
else resolveReady();
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (failed || closed) throw new Error('File output failed during initialization.');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (!stream.destroyed) stream.destroy();
|
|
129
|
+
if (!closed) {
|
|
130
|
+
await new Promise<void>((resolveClosed) => {
|
|
131
|
+
stream.once('close', resolveClosed);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw error;
|
|
136
|
+
} finally {
|
|
137
|
+
rejectInitialization = undefined;
|
|
138
|
+
initializing = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Object.freeze({
|
|
142
|
+
close() {
|
|
143
|
+
if (closePromise) return closePromise;
|
|
144
|
+
closing = true;
|
|
145
|
+
closePromise = new Promise<void>((resolveClosed) => {
|
|
146
|
+
if (closed) {
|
|
147
|
+
resolveClosed();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
stream.once('close', resolveClosed);
|
|
152
|
+
try {
|
|
153
|
+
if (!stream.destroyed) stream.end();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
fail(error);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return closePromise;
|
|
160
|
+
},
|
|
161
|
+
getStats: () => Object.freeze({
|
|
162
|
+
droppedCount,
|
|
163
|
+
pendingBytes,
|
|
164
|
+
writeErrorCount,
|
|
165
|
+
}),
|
|
166
|
+
write(text: string) {
|
|
167
|
+
if (closing || closed || failed) {
|
|
168
|
+
report({
|
|
169
|
+
message: 'File output is closed or unavailable.',
|
|
170
|
+
type: 'closed',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return Promise.resolve();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const bytes = Buffer.byteLength(text);
|
|
177
|
+
if (bytes > maxPendingBytes - pendingBytes) {
|
|
178
|
+
droppedCount++;
|
|
179
|
+
report({
|
|
180
|
+
droppedCount,
|
|
181
|
+
message: 'File output queue is full; the newest record was discarded.',
|
|
182
|
+
type: 'overflow',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return Promise.resolve();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
pendingBytes += bytes;
|
|
189
|
+
return new Promise<void>((resolveWritten) => {
|
|
190
|
+
let settled = false;
|
|
191
|
+
const settle = () => {
|
|
192
|
+
if (settled) return;
|
|
193
|
+
settled = true;
|
|
194
|
+
pendingBytes -= bytes;
|
|
195
|
+
unfinished.delete(settle);
|
|
196
|
+
resolveWritten();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
unfinished.add(settle);
|
|
200
|
+
try {
|
|
201
|
+
stream.write(
|
|
202
|
+
text,
|
|
203
|
+
(error) => {
|
|
204
|
+
if (error) fail(error);
|
|
205
|
+
settle();
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
fail(error);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Validate before mkdir, stream creation, or other filesystem side effects. */
|
|
217
|
+
function validateFileOptions(options: LoggerFileOptions) {
|
|
218
|
+
if (
|
|
219
|
+
!options
|
|
220
|
+
|| typeof options !== 'object'
|
|
221
|
+
|| !(
|
|
222
|
+
typeof options.path === 'string'
|
|
223
|
+
|| options.path instanceof Path
|
|
224
|
+
|| options.path instanceof URL
|
|
225
|
+
|| Buffer.isBuffer(options.path)
|
|
226
|
+
)
|
|
227
|
+
) {
|
|
228
|
+
throw new TypeError('file.path must be a string, Buffer, file URL, or Path');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const path = options.path instanceof URL
|
|
232
|
+
? fileURLToPath(options.path)
|
|
233
|
+
: options.path.toString();
|
|
234
|
+
|
|
235
|
+
if (Buffer.isBuffer(options.path) && !Buffer.from(path).equals(options.path)) {
|
|
236
|
+
throw new TypeError('file.path Buffer must contain valid UTF-8');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!path.trim() || path.includes('\0')) {
|
|
240
|
+
throw new TypeError('file.path must be a nonempty path without null bytes');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const maxPendingBytes = options.maxPendingBytes ?? defaultMaxPendingBytes;
|
|
244
|
+
if (!Number.isSafeInteger(maxPendingBytes) || maxPendingBytes <= 0) {
|
|
245
|
+
throw new RangeError('file.maxPendingBytes must be a positive safe integer');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rotate = options.rotate;
|
|
249
|
+
if (rotate !== undefined && rotate !== false) {
|
|
250
|
+
if (!rotate || typeof rotate !== 'object') {
|
|
251
|
+
throw new TypeError('file.rotate must be false or an options object');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (rotate.interval !== undefined && rotate.interval !== 'hourly' && rotate.interval !== 'daily') {
|
|
255
|
+
throw new TypeError('file.rotate.interval must be hourly or daily');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (rotate.interval === undefined && rotate.maxSize === undefined) {
|
|
259
|
+
throw new TypeError('file.rotate requires interval or maxSize');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const keepFiles = rotate.keepFiles ?? 30;
|
|
263
|
+
if (!Number.isSafeInteger(keepFiles) || keepFiles <= 0) {
|
|
264
|
+
throw new RangeError('file.rotate.keepFiles must be a positive safe integer');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (rotate.compress !== undefined && typeof rotate.compress !== 'boolean') {
|
|
268
|
+
throw new TypeError('file.rotate.compress must be a boolean');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
validateSize(rotate.maxSize, 'file.rotate.maxSize');
|
|
272
|
+
validateSize(rotate.maxTotalSize, 'file.rotate.maxTotalSize');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
maxPendingBytes,
|
|
277
|
+
path: Path.resolve(path).value,
|
|
278
|
+
rotate,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function validateSize(value: string | undefined, name: string) {
|
|
283
|
+
if (value === undefined) return;
|
|
284
|
+
if (typeof value !== 'string' || !/^[1-9]\d*[BKMG]$/.test(value)) {
|
|
285
|
+
throw new TypeError(`${name} must be a positive integer followed by B, K, M, or G`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const multiplier = {
|
|
289
|
+
B: 1,
|
|
290
|
+
G: 1024 ** 3,
|
|
291
|
+
K: 1024,
|
|
292
|
+
M: 1024 ** 2,
|
|
293
|
+
}[value.at(-1) as 'B' | 'G' | 'K' | 'M'];
|
|
294
|
+
|
|
295
|
+
if (!Number.isSafeInteger(Number(value.slice(0, -1)) * multiplier)) {
|
|
296
|
+
throw new RangeError(`${name} exceeds the supported size range`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
|
|
3
|
+
import { formatLoggerParts } from '../../_internals/formatter';
|
|
4
|
+
import type { LoggerRecord } from '../../_internals/types';
|
|
5
|
+
|
|
6
|
+
// Functions
|
|
7
|
+
|
|
8
|
+
/** Preserve user ANSI for console output without invoking custom inspect callbacks/getters. */
|
|
9
|
+
export function formatNodeLoggerRecord(record: LoggerRecord, colors = false) {
|
|
10
|
+
const message = record.args.map(
|
|
11
|
+
(arg) => typeof arg === 'string'
|
|
12
|
+
? arg
|
|
13
|
+
: inspect(
|
|
14
|
+
arg,
|
|
15
|
+
{
|
|
16
|
+
colors: false,
|
|
17
|
+
customInspect: false,
|
|
18
|
+
depth: 6,
|
|
19
|
+
getters: false,
|
|
20
|
+
maxArrayLength: 100,
|
|
21
|
+
maxStringLength: 10_000,
|
|
22
|
+
},
|
|
23
|
+
),
|
|
24
|
+
).join(' ');
|
|
25
|
+
|
|
26
|
+
return formatLoggerParts(
|
|
27
|
+
{
|
|
28
|
+
...record,
|
|
29
|
+
args: record.args.length ? [message] : [],
|
|
30
|
+
},
|
|
31
|
+
colors,
|
|
32
|
+
).join(' ');
|
|
33
|
+
}
|