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/lib/mini.js ADDED
@@ -0,0 +1,134 @@
1
+ const LEVELS = {
2
+ error: 0,
3
+ warn: 1,
4
+ info: 2,
5
+ http: 3,
6
+ verbose: 4,
7
+ debug: 5,
8
+ silly: 6,
9
+ };
10
+
11
+ const COLORS = {
12
+ error: 31,
13
+ warn: 33,
14
+ info: 32,
15
+ http: 35,
16
+ verbose: 36,
17
+ debug: 34,
18
+ silly: 90,
19
+ };
20
+
21
+ const NAMED_COLORS = {
22
+ black: 30,
23
+ red: 31,
24
+ green: 32,
25
+ yellow: 33,
26
+ blue: 34,
27
+ magenta: 35,
28
+ cyan: 36,
29
+ white: 37,
30
+ gray: 90,
31
+ grey: 90,
32
+ };
33
+
34
+ function resolveColorCode(value, level) {
35
+ if (value === undefined || value === null || value === false) return null;
36
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 255) return value;
37
+ if (typeof value === 'string' && NAMED_COLORS[value.toLowerCase()] !== undefined) return NAMED_COLORS[value.toLowerCase()];
38
+ throw new Error(`Unknown color "${value}" for level "${level}". Use one of: ${Object.keys(NAMED_COLORS).join(', ')}, an ANSI code 0-255, or false to disable`);
39
+ }
40
+
41
+ function resolveLevelColors(custom = {}) {
42
+ if (custom === null || typeof custom !== 'object' || Array.isArray(custom)) {
43
+ throw new Error('console.colors must be an object mapping level names to colors');
44
+ }
45
+ const map = {};
46
+ for (const level of Object.keys(COLORS)) {
47
+ if (custom[level] !== undefined) {
48
+ const code = resolveColorCode(custom[level], level);
49
+ if (code !== null) map[level] = code;
50
+ } else {
51
+ map[level] = COLORS[level];
52
+ }
53
+ }
54
+ return map;
55
+ }
56
+
57
+ function colorizeLevel(level, text, colors = COLORS) {
58
+ const code = colors[level] ?? 0;
59
+ if (!code) return text;
60
+ return `\x1b[${code}m${text}\x1b[0m`;
61
+ }
62
+
63
+ class NullTransport {
64
+ constructor() {
65
+ this.level = 'off';
66
+ }
67
+
68
+ write() {}
69
+
70
+ close() {}
71
+ }
72
+
73
+ class ConsoleTransport {
74
+ constructor({ level, render } = {}) {
75
+ this.level = level;
76
+ this.render = render;
77
+ }
78
+
79
+ write(entry) {
80
+ const line = this.render(entry);
81
+ const stream = entry.level === 'error' || entry.level === 'warn' ? process.stderr : process.stdout;
82
+ stream.write(line + '\n');
83
+ }
84
+
85
+ close() {}
86
+ }
87
+
88
+ class MiniLogger {
89
+ constructor({ level = 'debug' } = {}) {
90
+ this.level = level;
91
+ this.levels = LEVELS;
92
+ this.transports = [];
93
+ }
94
+
95
+ add(transport) {
96
+ this.transports.push(transport);
97
+ return this;
98
+ }
99
+
100
+ remove(transport) {
101
+ const i = this.transports.indexOf(transport);
102
+ if (i >= 0) this.transports.splice(i, 1);
103
+ try {
104
+ transport.close?.();
105
+ } catch {}
106
+ return this;
107
+ }
108
+
109
+ enabled(level, transportLevel) {
110
+ const want = LEVELS[level];
111
+ const current = LEVELS[transportLevel ?? this.level];
112
+ return want !== undefined && current !== undefined && want <= current;
113
+ }
114
+
115
+ log(level, message, ...meta) {
116
+ for (const t of [...this.transports]) {
117
+ if (this.enabled(level, t.level)) t.write({ level, message, meta });
118
+ }
119
+ }
120
+
121
+ child(meta = {}) {
122
+ const self = this;
123
+ const make = (level) => (message, ...args) => self.log(level, message, meta, ...args);
124
+ return {
125
+ error: make('error'),
126
+ warn: make('warn'),
127
+ info: make('info'),
128
+ debug: make('debug'),
129
+ log: (level, message, ...args) => self.log(level, message, meta, ...args),
130
+ };
131
+ }
132
+ }
133
+
134
+ module.exports = { LEVELS, ConsoleTransport, NullTransport, MiniLogger, colorizeLevel, resolveLevelColors, NAMED_COLORS };
package/lib/rotate.js ADDED
@@ -0,0 +1,144 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const zlib = require('zlib');
4
+ const dayjs = require('dayjs');
5
+
6
+ function parseSize(value, fallback) {
7
+ if (value == null) return fallback;
8
+ const m = String(value).trim().match(/^(\d+(?:\.\d+)?)\s*([kmg])?$/i);
9
+ if (!m) return fallback;
10
+ const mult = { k: 1024, m: 1024 ** 2, g: 1024 ** 3 }[(m[2] || '').toLowerCase()] ?? 1;
11
+ return Math.floor(parseFloat(m[1]) * mult);
12
+ }
13
+
14
+ function parseDays(value) {
15
+ if (value == null) return null;
16
+ const m = String(value).trim().match(/^(\d+)\s*d$/i);
17
+ return m ? parseInt(m[1], 10) : null;
18
+ }
19
+
20
+ class FileTransport {
21
+ constructor({ folder, filename, datePattern, maxSize = '20m', maxFiles = '14d', zippedArchive = false, level, render }) {
22
+ this.dirname = folder;
23
+ this.filename = filename;
24
+ this.options = { datePattern, maxSize, maxFiles, zippedArchive };
25
+ this.level = level;
26
+ this.render = render;
27
+ this.datePattern = datePattern;
28
+ this.maxBytes = parseSize(maxSize, Infinity);
29
+ this.maxFiles = maxFiles;
30
+ this.maxDays = parseDays(maxFiles);
31
+ this.zippedArchive = zippedArchive;
32
+ this.currentKey = null;
33
+ this.currentFile = null;
34
+ this.stream = null;
35
+ this.streamSize = 0;
36
+ this.streamErr = null;
37
+ }
38
+
39
+ fileNameFor(dateStr, n = 0) {
40
+ let name = this.filename.split('%DATE%').join(dateStr);
41
+ if (n > 0) {
42
+ const ext = path.extname(name) || '.log';
43
+ name = name.slice(0, -ext.length) + '-' + n + ext;
44
+ }
45
+ return name;
46
+ }
47
+
48
+ open(dateStr, startN = 0) {
49
+ if (!fs.existsSync(this.dirname)) fs.mkdirSync(this.dirname, { recursive: true });
50
+ let n = startN;
51
+ let full = path.join(this.dirname, this.fileNameFor(dateStr, n));
52
+ try {
53
+ let size = fs.existsSync(full) ? fs.statSync(full).size : 0;
54
+ while (size >= this.maxBytes && n < 1000) {
55
+ n += 1;
56
+ full = path.join(this.dirname, this.fileNameFor(dateStr, n));
57
+ size = fs.existsSync(full) ? fs.statSync(full).size : 0;
58
+ }
59
+ } catch {}
60
+ const stream = fs.createWriteStream(full, { flags: 'a' });
61
+ stream.on('error', (err) => { this.streamErr = err; });
62
+ this.stream = stream;
63
+ this.streamSize = 0;
64
+ try {
65
+ this.streamSize = fs.existsSync(full) ? fs.statSync(full).size : 0;
66
+ } catch {}
67
+ this.currentKey = dateStr + '#' + n;
68
+ this.currentFile = full;
69
+ }
70
+
71
+ rotateIfNeeded(dateStr, incoming) {
72
+ if (this.currentKey !== null && !this.currentKey.startsWith(dateStr + '#')) {
73
+ const previous = this.currentFile;
74
+ this.closeStream();
75
+ this.onDateChanged(previous);
76
+ this.currentKey = null;
77
+ }
78
+ if (this.stream === null) {
79
+ this.open(dateStr);
80
+ } else if (this.streamSize + incoming > this.maxBytes) {
81
+ const cur = parseInt(String(this.currentKey || '#0').split('#')[1] || '0', 10);
82
+ this.closeStream();
83
+ this.open(dateStr, cur + 1);
84
+ }
85
+ }
86
+
87
+ onDateChanged(previousFile) {
88
+ if (!previousFile) return;
89
+ try {
90
+ if (this.zippedArchive && fs.existsSync(previousFile)) {
91
+ const gz = previousFile + '.gz';
92
+ const data = fs.readFileSync(previousFile);
93
+ fs.writeFileSync(gz, zlib.gzipSync(data));
94
+ fs.rmSync(previousFile, { force: true });
95
+ }
96
+ } catch {}
97
+ this.cleanupOld();
98
+ }
99
+
100
+ cleanupOld() {
101
+ if (this.maxDays == null) return;
102
+ let entries;
103
+ try {
104
+ entries = fs.readdirSync(this.dirname);
105
+ } catch {
106
+ return;
107
+ }
108
+ const cutoff = Date.now() - this.maxDays * 86400000;
109
+ for (const entry of entries) {
110
+ if (!entry.endsWith('.log') && !entry.endsWith('.log.gz') && !entry.endsWith('.gz')) continue;
111
+ const full = path.join(this.dirname, entry);
112
+ try {
113
+ if (fs.statSync(full).mtimeMs < cutoff) fs.rmSync(full, { force: true });
114
+ } catch {}
115
+ }
116
+ }
117
+
118
+ write(entry) {
119
+ const line = this.render(entry);
120
+ const buf = Buffer.byteLength(line + '\n');
121
+ const dateStr = dayjs().format(this.datePattern);
122
+ this.rotateIfNeeded(dateStr, buf);
123
+ if (this.stream) {
124
+ this.stream.write(line + '\n');
125
+ this.streamSize += buf;
126
+ }
127
+ }
128
+
129
+ closeStream() {
130
+ if (this.stream) {
131
+ try {
132
+ this.stream.end();
133
+ } catch {}
134
+ this.stream = null;
135
+ }
136
+ }
137
+
138
+ close() {
139
+ this.closeStream();
140
+ this.currentKey = null;
141
+ }
142
+ }
143
+
144
+ module.exports = { FileTransport, parseSize, parseDays };
@@ -0,0 +1,88 @@
1
+ const path = require('path');
2
+
3
+ const VALID_LEVELS = ['error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly'];
4
+
5
+ function assertLevel(level) {
6
+ if (!VALID_LEVELS.includes(level)) throw new Error(`Invalid log level "${level}". Use one of: ${VALID_LEVELS.join(', ')}`);
7
+ }
8
+
9
+ function stripAnsi(str) {
10
+ let out = '';
11
+ let i = 0;
12
+ while (i < str.length) {
13
+ if (str[i] !== '\x1b' && str[i] !== '\x9b' && str[i] !== '\x9d') {
14
+ out += str[i];
15
+ i++;
16
+ continue;
17
+ }
18
+ if (str[i] === '\x9d' || str[i + 1] === ']') {
19
+ let j = i + 2;
20
+ while (j < str.length) {
21
+ if (str[j] === '\x07' || str[j] === '\x9c') { j++; break; }
22
+ if (str[j] === '\x1b' && str[j + 1] === '\\') { j += 2; break; }
23
+ j++;
24
+ }
25
+ i = j;
26
+ } else if (str[i] === '\x9b' || str[i + 1] === '[') {
27
+ let j = i + 2;
28
+ while (j < str.length && str[j] >= ' ' && str[j] <= '?') j++;
29
+ if (j < str.length) j++;
30
+ i = j;
31
+ } else if (str[i + 1] === '(' || str[i + 1] === ')') {
32
+ i += 3;
33
+ } else {
34
+ i += str[i + 1] === undefined ? 1 : 2;
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+ const CONTROL_RE = /[\x00-\x08\x0b\x0c\x0d\x0e-\x1f\x7f]/g;
40
+
41
+ function sanitizeMessage(msg) {
42
+ if (typeof msg !== 'string') return msg;
43
+ return stripAnsi(msg).replace(CONTROL_RE, '');
44
+ }
45
+
46
+ function assertFilenameSafe(filename) {
47
+ if (typeof filename !== 'string' || !filename.trim() || filename.includes('\0')) {
48
+ throw new Error('files.filename must be a non-empty string');
49
+ }
50
+ if (path.isAbsolute(filename) || filename.split(/[\\/]/).includes('..')) {
51
+ throw new Error('files.filename must not escape the log folder (.. and absolute paths are rejected)');
52
+ }
53
+ }
54
+
55
+ function isSafeChar(c) {
56
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c === '.' || c === '_' || c === '-';
57
+ }
58
+
59
+ function isPadChar(c) {
60
+ return c === '-' || c === '_' || c === '.';
61
+ }
62
+
63
+ function sanitizeAppName(name) {
64
+ if (typeof name !== 'string' || !name.trim()) return null;
65
+ const base = name.trim().slice(0, 200).split('/').pop();
66
+ let out = '';
67
+ for (let i = 0; i < base.length; i++) {
68
+ out += isSafeChar(base[i]) ? base[i] : '-';
69
+ }
70
+ let start = 0;
71
+ while (start < out.length && isPadChar(out[start])) start++;
72
+ let end = out.length;
73
+ while (end > start && isPadChar(out[end - 1])) end--;
74
+ const safe = out.slice(start, end);
75
+ return safe || null;
76
+ }
77
+
78
+ module.exports = {
79
+ VALID_LEVELS,
80
+ assertLevel,
81
+ stripAnsi,
82
+ CONTROL_RE,
83
+ sanitizeMessage,
84
+ assertFilenameSafe,
85
+ isSafeChar,
86
+ isPadChar,
87
+ sanitizeAppName,
88
+ };
package/lib/time.js ADDED
@@ -0,0 +1,64 @@
1
+ const dayjs = require('dayjs');
2
+
3
+ const DEFAULT_CONSOLE_FORMAT = 'YYYY-MM-DD HH:mm:ss';
4
+
5
+ function pad2(n) {
6
+ return String(n).padStart(2, '0');
7
+ }
8
+
9
+ function formatDefaultTimestamp(d = new Date()) {
10
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
11
+ }
12
+
13
+ function splitBrackets(fmt) {
14
+ const parts = [];
15
+ let current = '';
16
+ let i = 0;
17
+ while (i < fmt.length) {
18
+ if (fmt[i] === '[') {
19
+ const close = fmt.indexOf(']', i + 1);
20
+ if (close === -1) {
21
+ current += fmt.slice(i);
22
+ break;
23
+ }
24
+ if (current) {
25
+ parts.push(current);
26
+ current = '';
27
+ }
28
+ parts.push(fmt.slice(i, close + 1));
29
+ i = close + 1;
30
+ } else {
31
+ current += fmt[i];
32
+ i++;
33
+ }
34
+ }
35
+ if (current) parts.push(current);
36
+ return parts;
37
+ }
38
+
39
+ function compileTimestampFormatter(fmt) {
40
+ if (typeof fmt !== 'string' || !fmt) return formatDefaultTimestamp;
41
+ if (fmt === DEFAULT_CONSOLE_FORMAT) return formatDefaultTimestamp;
42
+ if (!fmt.includes('[') || !fmt.includes(']')) return () => dayjs().format(fmt);
43
+ const parts = splitBrackets(fmt).map((part) => {
44
+ if (part.startsWith('[') && part.endsWith(']')) {
45
+ const inner = part.slice(1, -1);
46
+ return inner ? () => '[' + dayjs().format(inner) + ']' : () => '[]';
47
+ }
48
+ return () => part;
49
+ });
50
+ return () => parts.map((f) => f()).join('');
51
+ }
52
+
53
+ function formatCustomTimestamp(fmt) {
54
+ return compileTimestampFormatter(fmt)();
55
+ }
56
+
57
+ module.exports = {
58
+ DEFAULT_CONSOLE_FORMAT,
59
+ pad2,
60
+ formatDefaultTimestamp,
61
+ splitBrackets,
62
+ compileTimestampFormatter,
63
+ formatCustomTimestamp,
64
+ };