logxpert 2.0.3 → 2.1.2
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/CHANGELOG.md +20 -20
- package/README.md +119 -107
- package/index.d.mts +1 -0
- package/index.d.ts +84 -79
- package/lib/files.js +102 -0
- package/lib/format.js +38 -0
- package/lib/mini.js +134 -0
- package/lib/rotate.js +144 -0
- package/lib/sanitize.js +88 -0
- package/lib/time.js +64 -0
- package/main.js +44 -275
- package/package.json +6 -6
package/main.js
CHANGED
|
@@ -1,277 +1,40 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
require('
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (fmt[i] === '[') {
|
|
24
|
-
const close = fmt.indexOf(']', i + 1);
|
|
25
|
-
if (close === -1) {
|
|
26
|
-
current += fmt.slice(i);
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
if (current) {
|
|
30
|
-
parts.push(current);
|
|
31
|
-
current = '';
|
|
32
|
-
}
|
|
33
|
-
parts.push(fmt.slice(i, close + 1));
|
|
34
|
-
i = close + 1;
|
|
35
|
-
} else {
|
|
36
|
-
current += fmt[i];
|
|
37
|
-
i++;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (current) parts.push(current);
|
|
41
|
-
return parts;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function compileTimestampFormatter(fmt) {
|
|
45
|
-
if (typeof fmt !== 'string' || !fmt) return formatDefaultTimestamp;
|
|
46
|
-
if (fmt === DEFAULT_CONSOLE_FORMAT) return formatDefaultTimestamp;
|
|
47
|
-
if (!fmt.includes('[') || !fmt.includes(']')) return () => dayjs().format(fmt);
|
|
48
|
-
const parts = splitBrackets(fmt).map((part) => {
|
|
49
|
-
if (part.startsWith('[') && part.endsWith(']')) {
|
|
50
|
-
const inner = part.slice(1, -1);
|
|
51
|
-
return inner ? () => '[' + dayjs().format(inner) + ']' : () => '[]';
|
|
52
|
-
}
|
|
53
|
-
return () => part;
|
|
54
|
-
});
|
|
55
|
-
return () => parts.map((f) => f()).join('');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function formatCustomTimestamp(fmt) {
|
|
59
|
-
return compileTimestampFormatter(fmt)();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function formatExtra(meta) {
|
|
63
|
-
const keys = Object.keys(meta).filter((k) => k !== 'splat' && k !== 'ms');
|
|
64
|
-
if (!keys.length) return '';
|
|
65
|
-
try {
|
|
66
|
-
const picked = {};
|
|
67
|
-
for (const k of keys) picked[k] = meta[k];
|
|
68
|
-
return JSON.stringify(picked);
|
|
69
|
-
} catch {
|
|
70
|
-
return '';
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const prettyFormat = format.printf(({ timestamp, level, message, ...meta }) => {
|
|
75
|
-
const msg = typeof message === 'string' ? message : message == null ? '' : JSON.stringify(message);
|
|
76
|
-
const extra = formatExtra(meta);
|
|
77
|
-
const body = [msg, extra].filter(Boolean).join(' ');
|
|
78
|
-
return timestamp ? `${timestamp} [${level}]: ${body}` : `[${level}]: ${body}`;
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
function assertLevel(level) {
|
|
82
|
-
if (!VALID_LEVELS.includes(level)) throw new Error(`Invalid log level "${level}". Use one of: ${VALID_LEVELS.join(', ')}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function stripAnsi(str) {
|
|
86
|
-
let out = '';
|
|
87
|
-
let i = 0;
|
|
88
|
-
while (i < str.length) {
|
|
89
|
-
if (str[i] !== '\x1b') {
|
|
90
|
-
out += str[i];
|
|
91
|
-
i++;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const next = str[i + 1];
|
|
95
|
-
if (next === ']') {
|
|
96
|
-
let j = i + 2;
|
|
97
|
-
while (j < str.length) {
|
|
98
|
-
if (str[j] === '\x07') { j++; break; }
|
|
99
|
-
if (str[j] === '\x1b' && str[j + 1] === '\\') { j += 2; break; }
|
|
100
|
-
j++;
|
|
101
|
-
}
|
|
102
|
-
i = j;
|
|
103
|
-
} else if (next === '[') {
|
|
104
|
-
let j = i + 2;
|
|
105
|
-
while (j < str.length && str[j] >= ' ' && str[j] <= '?') j++;
|
|
106
|
-
if (j < str.length) j++;
|
|
107
|
-
i = j;
|
|
108
|
-
} else if (next === '(' || next === ')') {
|
|
109
|
-
i += 3;
|
|
110
|
-
} else {
|
|
111
|
-
i += next === undefined ? 1 : 2;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
const CONTROL_RE = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/g;
|
|
117
|
-
|
|
118
|
-
function sanitizeMessage(msg) {
|
|
119
|
-
if (typeof msg !== 'string') return msg;
|
|
120
|
-
return stripAnsi(msg).replace(CONTROL_RE, '');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function assertFilenameSafe(filename) {
|
|
124
|
-
if (typeof filename !== 'string' || !filename.trim() || filename.includes('\0')) {
|
|
125
|
-
throw new Error('files.filename must be a non-empty string');
|
|
126
|
-
}
|
|
127
|
-
if (path.isAbsolute(filename) || filename.split(/[\\/]/).includes('..')) {
|
|
128
|
-
throw new Error('files.filename must not escape the log folder (.. and absolute paths are rejected)');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isSafeChar(c) {
|
|
133
|
-
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '.' || c === '_' || c === '-';
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isPadChar(c) {
|
|
137
|
-
return c === '-' || c === '_' || c === '.';
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function sanitizeAppName(name) {
|
|
141
|
-
if (typeof name !== 'string' || !name.trim()) return null;
|
|
142
|
-
const base = name.trim().slice(0, 200).split('/').pop();
|
|
143
|
-
let out = '';
|
|
144
|
-
for (let i = 0; i < base.length; i++) {
|
|
145
|
-
out += isSafeChar(base[i]) ? base[i] : '-';
|
|
146
|
-
}
|
|
147
|
-
let start = 0;
|
|
148
|
-
while (start < out.length && isPadChar(out[start])) start++;
|
|
149
|
-
let end = out.length;
|
|
150
|
-
while (end > start && isPadChar(out[end - 1])) end--;
|
|
151
|
-
const safe = out.slice(start, end);
|
|
152
|
-
return safe || null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let cachedAppName = null;
|
|
156
|
-
function getDefaultAppName() {
|
|
157
|
-
if (cachedAppName) return cachedAppName;
|
|
158
|
-
try {
|
|
159
|
-
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
160
|
-
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
161
|
-
cachedAppName = sanitizeAppName(JSON.parse(raw)?.name) ?? 'application';
|
|
162
|
-
} catch {
|
|
163
|
-
cachedAppName = 'application';
|
|
164
|
-
}
|
|
165
|
-
return cachedAppName;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function resolveRunNumber(folder, prefix, opt) {
|
|
169
|
-
let enabled = false;
|
|
170
|
-
let separator = '-';
|
|
171
|
-
let padding = 0;
|
|
172
|
-
let startAt = 1;
|
|
173
|
-
if (opt === true) {
|
|
174
|
-
enabled = true;
|
|
175
|
-
} else if (opt && typeof opt === 'object') {
|
|
176
|
-
enabled = opt.enabled ?? true;
|
|
177
|
-
if (opt.separator !== undefined) {
|
|
178
|
-
if (typeof opt.separator !== 'string' || !opt.separator) throw new Error('files.runNumber.separator must be a non-empty string');
|
|
179
|
-
separator = opt.separator;
|
|
180
|
-
}
|
|
181
|
-
if (opt.padding !== undefined) {
|
|
182
|
-
if (!Number.isInteger(opt.padding) || opt.padding < 0) throw new Error('files.runNumber.padding must be a non-negative integer');
|
|
183
|
-
padding = opt.padding;
|
|
184
|
-
}
|
|
185
|
-
if (opt.startAt !== undefined) {
|
|
186
|
-
if (!Number.isInteger(opt.startAt) || opt.startAt < 0) throw new Error('files.runNumber.startAt must be a non-negative integer');
|
|
187
|
-
startAt = opt.startAt;
|
|
188
|
-
}
|
|
189
|
-
} else if (opt !== undefined && opt !== false) {
|
|
190
|
-
throw new Error('files.runNumber must be a boolean or an object');
|
|
191
|
-
}
|
|
192
|
-
if (!enabled) return null;
|
|
193
|
-
const counterFile = path.join(folder, `.${prefix}.run`);
|
|
194
|
-
let next = startAt;
|
|
195
|
-
try {
|
|
196
|
-
const raw = fs.readFileSync(counterFile, 'utf8').trim();
|
|
197
|
-
const prev = parseInt(raw, 10);
|
|
198
|
-
next = Number.isInteger(prev) ? prev + 1 : startAt;
|
|
199
|
-
} catch {}
|
|
200
|
-
try {
|
|
201
|
-
fs.writeFileSync(counterFile, String(next), 'utf8');
|
|
202
|
-
} catch {}
|
|
203
|
-
return separator + String(next).padStart(padding, '0');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function normalizeFilesOptions(files = {}) {
|
|
207
|
-
const rawFolder = files.folder ?? 'logs';
|
|
208
|
-
if (typeof rawFolder !== 'string' || !rawFolder.trim() || rawFolder.includes('\0')) {
|
|
209
|
-
throw new Error('files.folder must be a non-empty string');
|
|
210
|
-
}
|
|
211
|
-
const folder = path.normalize(rawFolder);
|
|
212
|
-
const datePattern = files.datePattern ?? files.dateFormat ?? files.filesName ?? 'YYYY_MM_DD';
|
|
213
|
-
if (typeof datePattern !== 'string' || !datePattern.trim()) {
|
|
214
|
-
throw new Error('files.datePattern must be a non-empty string');
|
|
215
|
-
}
|
|
216
|
-
const explicitPattern = files.filePattern ?? files.pattern ?? files.filename;
|
|
217
|
-
const explicitPrefix = files.prefix ?? files.appName;
|
|
218
|
-
if (explicitPattern !== undefined && explicitPrefix !== undefined) {
|
|
219
|
-
throw new Error('Do not combine "prefix" with "filePattern", pick one: prefix builds "<prefix>-%DATE%.log" for you, filePattern takes full control');
|
|
220
|
-
}
|
|
221
|
-
let filename = explicitPattern ?? `${sanitizeAppName(explicitPrefix) ?? getDefaultAppName()}-%DATE%.log`;
|
|
222
|
-
filename = filename.split('%datePattern%').join('%DATE%');
|
|
223
|
-
assertFilenameSafe(filename);
|
|
224
|
-
if (!filename.includes('%DATE%')) {
|
|
225
|
-
const ext = path.extname(filename) || '.log';
|
|
226
|
-
const base = path.basename(filename, ext);
|
|
227
|
-
filename = `${base}-%DATE%${ext}`;
|
|
228
|
-
}
|
|
229
|
-
assertFilenameSafe(filename);
|
|
230
|
-
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
|
|
231
|
-
const rawPrefix = path.basename(filename).split('%DATE%')[0];
|
|
232
|
-
let prefixEnd = rawPrefix.length;
|
|
233
|
-
while (prefixEnd > 0 && isPadChar(rawPrefix[prefixEnd - 1])) prefixEnd--;
|
|
234
|
-
const prefix = rawPrefix.slice(0, prefixEnd) || 'application';
|
|
235
|
-
const runSuffix = resolveRunNumber(folder, sanitizeAppName(prefix) ?? 'application', files.runNumber ?? false);
|
|
236
|
-
if (runSuffix) {
|
|
237
|
-
const ext = path.extname(filename) || '.log';
|
|
238
|
-
const base = filename.slice(0, -ext.length);
|
|
239
|
-
filename = `${base}${runSuffix}${ext}`;
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
folder,
|
|
243
|
-
datePattern,
|
|
244
|
-
filename,
|
|
245
|
-
maxFiles: files.maxFile ?? files.maxFiles ?? '14d',
|
|
246
|
-
maxSize: files.maxSize ?? '20m',
|
|
247
|
-
zippedArchive: files.zippedArchive ?? false,
|
|
248
|
-
level: files.level,
|
|
3
|
+
const { MiniLogger, ConsoleTransport, NullTransport, colorizeLevel, resolveLevelColors } = require('./lib/mini');
|
|
4
|
+
const { FileTransport } = require('./lib/rotate');
|
|
5
|
+
const { DEFAULT_CONSOLE_FORMAT, compileTimestampFormatter } = require('./lib/time');
|
|
6
|
+
const { buildLine } = require('./lib/format');
|
|
7
|
+
const { assertLevel, sanitizeMessage } = require('./lib/sanitize');
|
|
8
|
+
const { normalizeFilesOptions } = require('./lib/files');
|
|
9
|
+
|
|
10
|
+
function normalizeConsoleOpts(input) {
|
|
11
|
+
if (input === false) return { enabled: false };
|
|
12
|
+
return input || {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildConsoleTransport({ enabled = true, enableTimestamp = true, timestampFormat = DEFAULT_CONSOLE_FORMAT, timestampPrefix = '', timestampSuffix = '', colorize = true, colors, level } = {}) {
|
|
16
|
+
if (!enabled) return new NullTransport();
|
|
17
|
+
const stamp = enableTimestamp ? compileTimestampFormatter(timestampFormat) : null;
|
|
18
|
+
const palette = resolveLevelColors(colors);
|
|
19
|
+
const render = ({ level, message, meta }) => {
|
|
20
|
+
const timestamp = stamp ? `${timestampPrefix}${stamp()}${timestampSuffix}` : '';
|
|
21
|
+
const tag = colorize ? colorizeLevel(level, `[${level}]`, palette) : `[${level}]`;
|
|
22
|
+
return buildLine(timestamp, level, message, meta, tag);
|
|
249
23
|
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function buildConsoleTransport({ enableTimestamp = true, timestampFormat = DEFAULT_CONSOLE_FORMAT, timestampPrefix = '', timestampSuffix = '', colorize = true, level } = {}) {
|
|
253
|
-
const parts = [];
|
|
254
|
-
if (colorize) parts.push(format.colorize());
|
|
255
|
-
if (enableTimestamp) {
|
|
256
|
-
const stamp = compileTimestampFormatter(timestampFormat);
|
|
257
|
-
parts.push(format.timestamp({ format: () => `${timestampPrefix}${stamp()}${timestampSuffix}` }));
|
|
258
|
-
} else {
|
|
259
|
-
parts.push(format((info) => { delete info.timestamp; return info; })());
|
|
260
|
-
}
|
|
261
|
-
parts.push(format.errors({ stack: true }), format.splat(), prettyFormat);
|
|
262
|
-
return new transports.Console({ format: format.combine(...parts), level });
|
|
24
|
+
return new ConsoleTransport({ level, render });
|
|
263
25
|
}
|
|
264
26
|
|
|
265
27
|
function buildFileTransport({ folder, datePattern, filename, maxFiles, maxSize, zippedArchive, level }) {
|
|
266
28
|
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
|
|
267
|
-
return new
|
|
268
|
-
|
|
29
|
+
return new FileTransport({
|
|
30
|
+
folder: path.normalize(folder),
|
|
31
|
+
filename,
|
|
269
32
|
datePattern,
|
|
270
33
|
zippedArchive,
|
|
271
34
|
maxSize,
|
|
272
35
|
maxFiles,
|
|
273
36
|
level,
|
|
274
|
-
|
|
37
|
+
render: ({ level, message, meta }) => buildLine(new Date().toISOString(), level, message, meta),
|
|
275
38
|
});
|
|
276
39
|
}
|
|
277
40
|
|
|
@@ -279,27 +42,30 @@ function createLoggerInstance(initial = {}) {
|
|
|
279
42
|
const globalLevel = process.env.LOG_LEVEL || initial.level || 'debug';
|
|
280
43
|
assertLevel(globalLevel);
|
|
281
44
|
|
|
282
|
-
const logger =
|
|
283
|
-
level: globalLevel,
|
|
284
|
-
format: format.combine(format.errors({ stack: true }), format.splat()),
|
|
285
|
-
});
|
|
45
|
+
const logger = new MiniLogger({ level: globalLevel });
|
|
286
46
|
|
|
287
|
-
let consoleTransport = buildConsoleTransport(initial.console);
|
|
288
|
-
consoleTransport.level = initial.console
|
|
47
|
+
let consoleTransport = buildConsoleTransport(normalizeConsoleOpts(initial.console));
|
|
48
|
+
consoleTransport.level = normalizeConsoleOpts(initial.console).level;
|
|
289
49
|
logger.add(consoleTransport);
|
|
290
50
|
|
|
291
51
|
let fileTransport = null;
|
|
292
|
-
let consoleOpts = { ...(initial.console
|
|
52
|
+
let consoleOpts = { ...normalizeConsoleOpts(initial.console) };
|
|
293
53
|
let filesOpts = null;
|
|
54
|
+
let sanitizeEnabled = initial.sanitize ?? true;
|
|
294
55
|
|
|
295
56
|
function applySettings(options = {}) {
|
|
296
57
|
if (options.level !== undefined) {
|
|
297
58
|
assertLevel(options.level);
|
|
298
59
|
logger.level = options.level;
|
|
299
60
|
}
|
|
300
|
-
if (options.
|
|
301
|
-
if (options.
|
|
302
|
-
|
|
61
|
+
if (options.sanitize !== undefined) {
|
|
62
|
+
if (typeof options.sanitize !== 'boolean') throw new Error('log.settings sanitize must be a boolean');
|
|
63
|
+
sanitizeEnabled = options.sanitize;
|
|
64
|
+
}
|
|
65
|
+
if (options.console !== undefined) {
|
|
66
|
+
const next = normalizeConsoleOpts(options.console);
|
|
67
|
+
if (next.level !== undefined && next.level !== null) assertLevel(next.level);
|
|
68
|
+
consoleOpts = { ...consoleOpts, ...next };
|
|
303
69
|
logger.remove(consoleTransport);
|
|
304
70
|
consoleTransport = buildConsoleTransport(consoleOpts);
|
|
305
71
|
consoleTransport.level = consoleOpts.level;
|
|
@@ -327,10 +93,11 @@ function createLoggerInstance(initial = {}) {
|
|
|
327
93
|
message = message();
|
|
328
94
|
}
|
|
329
95
|
if (message instanceof Error) {
|
|
330
|
-
|
|
96
|
+
const text = sanitizeEnabled ? sanitizeMessage(message.message) : message.message;
|
|
97
|
+
logger.log(level, text, { stack: message.stack, ...meta[0] });
|
|
331
98
|
return;
|
|
332
99
|
}
|
|
333
|
-
logger.log(level, sanitizeMessage(message), ...meta);
|
|
100
|
+
logger.log(level, sanitizeEnabled ? sanitizeMessage(message) : message, ...meta);
|
|
334
101
|
}
|
|
335
102
|
|
|
336
103
|
function callable(message, ...meta) {
|
|
@@ -366,7 +133,9 @@ function createLoggerInstance(initial = {}) {
|
|
|
366
133
|
callable._winston = logger;
|
|
367
134
|
|
|
368
135
|
if (initial.files) applySettings({ files: initial.files });
|
|
369
|
-
if (initial.console || initial.level
|
|
136
|
+
if (initial.console || initial.level || initial.sanitize !== undefined) {
|
|
137
|
+
applySettings({ console: initial.console, level: initial.level, sanitize: initial.sanitize });
|
|
138
|
+
}
|
|
370
139
|
|
|
371
140
|
return callable;
|
|
372
141
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "logxpert",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "A powerful logging library for Node.js with colorful output and daily file rotation",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"main.mjs",
|
|
23
23
|
"index.d.ts",
|
|
24
24
|
"index.d.mts",
|
|
25
|
+
"lib/",
|
|
25
26
|
"README.md",
|
|
26
27
|
"LICENSE",
|
|
27
28
|
"CHANGELOG.md"
|
|
@@ -47,14 +48,13 @@
|
|
|
47
48
|
"node": ">=18"
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
|
-
"test": "node --test test/logger.test.js",
|
|
51
|
-
"
|
|
51
|
+
"test": "node --test --test-reporter=./test/reporter.js test/logger.test.js test/console.test.js test/files.test.js test/lib.test.js",
|
|
52
|
+
"test:demo": "node --test --test-reporter=./test/reporter.js demo/reporter.demo.test.js",
|
|
53
|
+
"check:types": "tsc -p test-d --noEmit && echo TYPES-OK",
|
|
52
54
|
"attw": "attw --pack"
|
|
53
55
|
},
|
|
54
56
|
"dependencies": {
|
|
55
|
-
"dayjs": "^1.11.23"
|
|
56
|
-
"winston": "^3.19.0",
|
|
57
|
-
"winston-daily-rotate-file": "^5.0.0"
|
|
57
|
+
"dayjs": "^1.11.23"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@arethetypeswrong/cli": "^0.18.5",
|