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 CHANGED
@@ -1,20 +1,20 @@
1
- # Changelog
2
-
3
- ## Unreleased
4
- - Fix TS types for dual CJS/ESM: `export =` + namespace in `index.d.ts`, dedicated `index.d.mts` for the ESM entry (`attw` clean)
5
- - Rename `files` options with backward-compatible aliases: `prefix` (`appName`), `filePattern` (`filename`/`pattern`), `datePattern` (`filesName`/`dateFormat`); `%datePattern%` placeholder; `prefix` + `filePattern` now throws instead of silent precedence
6
- - Default file `datePattern` is now `YYYY_MM_DD`
7
- - License changed to `Apache-2.0`
8
-
9
- ## 2.0.0
10
- - Replace `moment` with `dayjs`
11
- - Add ESM entry (`main.mjs`) + `index.d.ts` types
12
- - Add `log.setLevel/getLevel`, `LOG_LEVEL` env support
13
- - Add `log.createLogger()` isolated instances and `log.child()`
14
- - Add structured logging: objects, meta, `Error` stacks, `splat`
15
- - `files.filename` option, input validation in `settings()`
16
- - Fix license to `GPL-3.0-only`, add `exports`, `files`, `engines`
17
- - Add tests (`node --test`) and CI
18
-
19
- ## 1.0.5
20
- - Previous CJS-only release with `moment` + `winston`.
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+ - Fix TS types for dual CJS/ESM: `export =` + namespace in `index.d.ts`, dedicated `index.d.mts` for the ESM entry (`attw` clean)
5
+ - Rename `files` options with backward-compatible aliases: `prefix` (`appName`), `filePattern` (`filename`/`pattern`), `datePattern` (`filesName`/`dateFormat`); `%datePattern%` placeholder; `prefix` + `filePattern` now throws instead of silent precedence
6
+ - Default file `datePattern` is now `YYYY_MM_DD`
7
+ - License changed to `Apache-2.0`
8
+
9
+ ## 2.0.0
10
+ - Replace `moment` with `dayjs`
11
+ - Add ESM entry (`main.mjs`) + `index.d.ts` types
12
+ - Add `log.setLevel/getLevel`, `LOG_LEVEL` env support
13
+ - Add `log.createLogger()` isolated instances and `log.child()`
14
+ - Add structured logging: objects, meta, `Error` stacks, `splat`
15
+ - `files.filename` option, input validation in `settings()`
16
+ - Fix license to `GPL-3.0-only`, add `exports`, `files`, `engines`
17
+ - Add tests (`node --test`) and CI
18
+
19
+ ## 1.0.5
20
+ - Previous CJS-only release with `moment` + `winston`.
package/README.md CHANGED
@@ -1,107 +1,119 @@
1
- # LogXpert
2
-
3
- Colorful console logging for Node.js with optional daily file rotation.
4
-
5
- ## Installation
6
-
7
- ```sh
8
- npm install logxpert
9
- ```
10
-
11
- Requires Node.js `>=18`. Recommended minimum: Node.js 22 LTS. See [NODE_SUPPORT.md](NODE_SUPPORT.md) for the full support policy.
12
-
13
- ## Quick start
14
-
15
- ```js
16
- const log = require('logxpert');
17
- // or: import log from 'logxpert';
18
-
19
- log('general message'); // shortcut for info
20
- log.info('hello', { user: 1 }); // with metadata
21
- log.error(new Error('boom')); // Errors print message + stack
22
- log.debug(() => JSON.stringify(hugeObject)); // lazy: skipped if level disabled
23
- ```
24
-
25
- Levels: `error → warn → info → http → verbose → debug → silly` (default `debug`).
26
-
27
- ```js
28
- log.setLevel('info'); // or via LOG_LEVEL env variable
29
- ```
30
-
31
- ## File logging
32
-
33
- ```js
34
- log.settings({ files: { folder: 'logs' } });
35
- // → logs/logxpert-2026_10_08.log (prefix defaults to your package.json name)
36
-
37
- log.settings({ files: { folder: 'logs', prefix: 'api' } });
38
- // → logs/api-2026_10_08.log
39
-
40
- // Full control (omit prefix when used):
41
- log.settings({ files: { folder: 'logs', filePattern: 'app-%datePattern%.log' } });
42
- ```
43
-
44
- `%datePattern%` is replaced by the date (`datePattern` defaults to `YYYY_MM_DD`). Combining `prefix` with `filePattern` throws — pick one.
45
-
46
- ```js
47
- log.settings({
48
- files: {
49
- folder: 'logs',
50
- runNumber: true, // off by default → app-2026_10_08-1.log, -2.log, ...
51
- maxFile: '14d', // retention
52
- maxSize: '20m', // rotate above this size
53
- zippedArchive: true,
54
- },
55
- });
56
- log.close(); // call on shutdown
57
- ```
58
-
59
- ## Console & instances
60
-
61
- ```js
62
- log.settings({
63
- console: {
64
- enableTimestamp: true,
65
- timestampFormat: 'YYYY-MM-DD HH:mm:ss',
66
- colorize: true,
67
- },
68
- });
69
-
70
- const { createLogger } = require('logxpert');
71
- const apiLog = createLogger({ level: 'info' }); // independent instance
72
-
73
- const child = log.child({ service: 'api' }); // bound context
74
- ```
75
-
76
- ## Full configuration (copy-paste)
77
-
78
- ```js
79
- log.settings({
80
- level: 'debug', // or process.env.LOG_LEVEL
81
- console: {
82
- enableTimestamp: true,
83
- timestampFormat: 'YYYY-MM-DD HH:mm:ss',
84
- timestampPrefix: '',
85
- timestampSuffix: '',
86
- colorize: true,
87
- },
88
- files: {
89
- folder: 'logs',
90
- prefix: undefined, // defaults to your package.json name
91
- // filePattern: 'app-%datePattern%.log', // full control (omit prefix if used)
92
- datePattern: 'YYYY_MM_DD',
93
- runNumber: false, // or true, or { separator: '-', padding: 0, startAt: 1 }
94
- maxFile: '14d',
95
- maxSize: '20m',
96
- zippedArchive: false,
97
- }
98
- });
99
- ```
100
-
101
- Legacy aliases: `appName` (= `prefix`), `filename`/`pattern` (= `filePattern`), `filesName`/`dateFormat` (= `datePattern`).
102
-
103
- Security: messages are stripped of ANSI escapes and control characters (except `\n`, `\t`); file patterns escaping the log folder (`..`, absolute paths) are rejected.
104
-
105
- ## License
106
-
107
- Apache-2.0. See [LICENSE](LICENSE).
1
+ # LogXpert
2
+
3
+ Colorful console logging for Node.js with optional daily file rotation.
4
+
5
+ - Zero heavy dependencies — built-in engine.
6
+ - Daily file rotation with retention, size limits and gzip.
7
+ - Per-level colors, lazy messages, child loggers, isolated instances.
8
+ - Works from JS and TS, CommonJS and ESM.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install logxpert
14
+ ```
15
+
16
+ Requires Node.js `>=18`. Recommended minimum: Node.js 22 LTS. See [NODE_SUPPORT.md](NODE_SUPPORT.md) for the full support policy.
17
+
18
+ ## Quick start
19
+
20
+ ```js
21
+ const log = require('logxpert');
22
+ // or: import log from 'logxpert';
23
+
24
+ log('general message'); // shortcut for info
25
+ log.info('hello', { user: 1 }); // with metadata
26
+ log.error(new Error('boom')); // Errors print message + stack
27
+ log.debug(() => JSON.stringify(hugeObject)); // lazy: skipped if level disabled
28
+ ```
29
+
30
+ Levels: `error → warn → info → http → verbose → debug → silly` (default `debug`).
31
+
32
+ ```js
33
+ log.setLevel('info'); // or via LOG_LEVEL env variable
34
+ ```
35
+
36
+ ## File logging
37
+
38
+ ```js
39
+ log.settings({ files: { folder: 'logs' } });
40
+ // → logs/logxpert-2026_10_08.log (prefix defaults to your package.json name)
41
+
42
+ log.settings({ files: { folder: 'logs', prefix: 'api' } });
43
+ // → logs/api-2026_10_08.log
44
+
45
+ // Full control (omit prefix when used):
46
+ log.settings({ files: { folder: 'logs', filePattern: 'app-%datePattern%.log' } });
47
+ ```
48
+
49
+ `%datePattern%` is replaced by the date (`datePattern` defaults to `YYYY_MM_DD`). Combining `prefix` with `filePattern` throws — pick one.
50
+
51
+ ```js
52
+ log.settings({
53
+ files: {
54
+ folder: 'logs',
55
+ runNumber: true, // off by default → app-2026_10_08-1.log, -2.log, ...
56
+ maxFile: '14d', // retention
57
+ maxSize: '20m', // rotate above this size
58
+ zippedArchive: true,
59
+ },
60
+ });
61
+ log.close(); // call on shutdown
62
+ ```
63
+
64
+ ## Console & instances
65
+
66
+ ```js
67
+ log.settings({
68
+ console: {
69
+ enableTimestamp: true,
70
+ timestampFormat: 'YYYY-MM-DD HH:mm:ss',
71
+ colorize: true,
72
+ colors: { info: 'blue', error: false }, // names, ANSI 0-255, or false per level
73
+ },
74
+ });
75
+
76
+ // Default colors: error red, warn yellow, info green, http magenta,
77
+ // verbose cyan, debug blue, silly gray. Only the [level] tag is colored.
78
+
79
+ const { createLogger } = require('logxpert');
80
+ const apiLog = createLogger({ level: 'info' }); // independent instance
81
+
82
+ const child = log.child({ service: 'api' }); // bound context
83
+ ```
84
+
85
+ ## Full configuration (copy-paste)
86
+
87
+ ```js
88
+ log.settings({
89
+ level: 'debug', // or process.env.LOG_LEVEL
90
+ sanitize: true, // strip ANSI escapes and control chars; false = pass through untouched
91
+ console: {
92
+ enableTimestamp: true,
93
+ timestampFormat: 'YYYY-MM-DD HH:mm:ss',
94
+ timestampPrefix: '',
95
+ timestampSuffix: '',
96
+ colorize: true,
97
+ colors: { info: 'blue', warn: 208, error: false }
98
+ // enabled: false, // file-only logging: silence the console
99
+ },
100
+ files: {
101
+ folder: 'logs',
102
+ prefix: undefined, // defaults to your package.json name
103
+ // filePattern: 'app-%datePattern%.log', // full control (omit prefix if used)
104
+ datePattern: 'YYYY_MM_DD',
105
+ runNumber: false, // or true, or { separator: '-', padding: 0, startAt: 1 }
106
+ maxFile: '14d',
107
+ maxSize: '20m',
108
+ zippedArchive: false,
109
+ }
110
+ });
111
+ ```
112
+
113
+ Legacy aliases: `appName` (= `prefix`), `filename`/`pattern` (= `filePattern`), `filesName`/`dateFormat` (= `datePattern`).
114
+
115
+ Security: messages are stripped of ANSI escapes and control characters (except `\n`, `\t`); file patterns escaping the log folder (`..`, absolute paths) are rejected.
116
+
117
+ ## License
118
+
119
+ Apache-2.0. See [LICENSE](LICENSE).
package/index.d.mts CHANGED
@@ -14,6 +14,7 @@ export const info: typeof log.info;
14
14
  export const debug: typeof log.debug;
15
15
 
16
16
  export type LogLevel = log.LogLevel;
17
+ export type LevelColor = log.LevelColor;
17
18
  export type ConsoleOptions = log.ConsoleOptions;
18
19
  export type FilesOptions = log.FilesOptions;
19
20
  export type RunNumberOptions = log.RunNumberOptions;
package/index.d.ts CHANGED
@@ -1,79 +1,84 @@
1
- declare namespace log {
2
- export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly';
3
-
4
- export interface ConsoleOptions {
5
- enableTimestamp?: boolean;
6
- timestampFormat?: string;
7
- timestampPrefix?: string;
8
- timestampSuffix?: string;
9
- colorize?: boolean;
10
- level?: LogLevel;
11
- }
12
-
13
- export interface RunNumberOptions {
14
- enabled?: boolean;
15
- separator?: string;
16
- padding?: number;
17
- startAt?: number;
18
- }
19
-
20
- export interface FilesOptions {
21
- folder?: string;
22
- /** Preferred: date format for %DATE%. */
23
- datePattern?: string;
24
- /** @deprecated Use datePattern. */
25
- filesName?: string;
26
- dateFormat?: string;
27
- /** Preferred: filename template containing %DATE%. */
28
- filePattern?: string;
29
- pattern?: string;
30
- /** @deprecated Use filePattern. */
31
- filename?: string;
32
- /** Preferred: filename prefix used when no pattern is given. */
33
- prefix?: string;
34
- /** @deprecated Use prefix. */
35
- appName?: string;
36
- runNumber?: boolean | RunNumberOptions;
37
- maxFile?: string;
38
- maxFiles?: string;
39
- maxSize?: string;
40
- zippedArchive?: boolean;
41
- level?: LogLevel;
42
- }
43
-
44
- export interface LogSettingsOptions {
45
- console?: ConsoleOptions;
46
- files?: FilesOptions;
47
- level?: LogLevel;
48
- }
49
-
50
- export interface ChildLogger {
51
- error(message: unknown, ...meta: unknown[]): void;
52
- warn(message: unknown, ...meta: unknown[]): void;
53
- info(message: unknown, ...meta: unknown[]): void;
54
- debug(message: unknown, ...meta: unknown[]): void;
55
- log(level: LogLevel, message: unknown, ...meta: unknown[]): void;
56
- }
57
-
58
- export interface LogXpert {
59
- (message: unknown, ...meta: unknown[]): void;
60
- error(message: unknown, ...meta: unknown[]): void;
61
- warn(message: unknown, ...meta: unknown[]): void;
62
- info(message: unknown, ...meta: unknown[]): void;
63
- debug(message: unknown, ...meta: unknown[]): void;
64
- http(message: unknown, ...meta: unknown[]): void;
65
- verbose(message: unknown, ...meta: unknown[]): void;
66
- silly(message: unknown, ...meta: unknown[]): void;
67
- log(level: LogLevel, message: unknown, ...meta: unknown[]): void;
68
- settings(options?: LogSettingsOptions): void;
69
- setLevel(level: LogLevel): void;
70
- getLevel(): string;
71
- child(meta?: Record<string, unknown>): ChildLogger;
72
- close(): void;
73
- createLogger(options?: LogSettingsOptions): LogXpert;
74
- }
75
- }
76
-
77
- declare const log: log.LogXpert;
78
-
79
- export = log;
1
+ declare namespace log {
2
+ export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly';
3
+
4
+ export type LevelColor = string | number | false | null;
5
+
6
+ export interface ConsoleOptions {
7
+ enabled?: boolean;
8
+ enableTimestamp?: boolean;
9
+ timestampFormat?: string;
10
+ timestampPrefix?: string;
11
+ timestampSuffix?: string;
12
+ colorize?: boolean;
13
+ colors?: Partial<Record<LogLevel, LevelColor>>;
14
+ level?: LogLevel;
15
+ }
16
+
17
+ export interface RunNumberOptions {
18
+ enabled?: boolean;
19
+ separator?: string;
20
+ padding?: number;
21
+ startAt?: number;
22
+ }
23
+
24
+ export interface FilesOptions {
25
+ folder?: string;
26
+ /** Preferred: date format for %DATE%. */
27
+ datePattern?: string;
28
+ /** @deprecated Use datePattern. */
29
+ filesName?: string;
30
+ dateFormat?: string;
31
+ /** Preferred: filename template containing %DATE%. */
32
+ filePattern?: string;
33
+ pattern?: string;
34
+ /** @deprecated Use filePattern. */
35
+ filename?: string;
36
+ /** Preferred: filename prefix used when no pattern is given. */
37
+ prefix?: string;
38
+ /** @deprecated Use prefix. */
39
+ appName?: string;
40
+ runNumber?: boolean | RunNumberOptions;
41
+ maxFile?: string;
42
+ maxFiles?: string;
43
+ maxSize?: string;
44
+ zippedArchive?: boolean;
45
+ level?: LogLevel;
46
+ }
47
+
48
+ export interface LogSettingsOptions {
49
+ console?: ConsoleOptions | false;
50
+ files?: FilesOptions;
51
+ level?: LogLevel;
52
+ sanitize?: boolean;
53
+ }
54
+
55
+ export interface ChildLogger {
56
+ error(message: unknown, ...meta: unknown[]): void;
57
+ warn(message: unknown, ...meta: unknown[]): void;
58
+ info(message: unknown, ...meta: unknown[]): void;
59
+ debug(message: unknown, ...meta: unknown[]): void;
60
+ log(level: LogLevel, message: unknown, ...meta: unknown[]): void;
61
+ }
62
+
63
+ export interface LogXpert {
64
+ (message: unknown, ...meta: unknown[]): void;
65
+ error(message: unknown, ...meta: unknown[]): void;
66
+ warn(message: unknown, ...meta: unknown[]): void;
67
+ info(message: unknown, ...meta: unknown[]): void;
68
+ debug(message: unknown, ...meta: unknown[]): void;
69
+ http(message: unknown, ...meta: unknown[]): void;
70
+ verbose(message: unknown, ...meta: unknown[]): void;
71
+ silly(message: unknown, ...meta: unknown[]): void;
72
+ log(level: LogLevel, message: unknown, ...meta: unknown[]): void;
73
+ settings(options?: LogSettingsOptions): void;
74
+ setLevel(level: LogLevel): void;
75
+ getLevel(): string;
76
+ child(meta?: Record<string, unknown>): ChildLogger;
77
+ close(): void;
78
+ createLogger(options?: LogSettingsOptions): LogXpert;
79
+ }
80
+ }
81
+
82
+ declare const log: log.LogXpert;
83
+
84
+ export = log;
package/lib/files.js ADDED
@@ -0,0 +1,102 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { sanitizeAppName, isPadChar, assertFilenameSafe } = require('./sanitize');
4
+
5
+ let cachedAppName = null;
6
+ function getDefaultAppName() {
7
+ if (cachedAppName) return cachedAppName;
8
+ try {
9
+ const pkgPath = path.resolve(process.cwd(), 'package.json');
10
+ const raw = fs.readFileSync(pkgPath, 'utf8');
11
+ cachedAppName = sanitizeAppName(JSON.parse(raw)?.name) ?? 'application';
12
+ } catch {
13
+ cachedAppName = 'application';
14
+ }
15
+ return cachedAppName;
16
+ }
17
+
18
+ function resolveRunNumber(folder, prefix, opt) {
19
+ let enabled = false;
20
+ let separator = '-';
21
+ let padding = 0;
22
+ let startAt = 1;
23
+ if (opt === true) {
24
+ enabled = true;
25
+ } else if (opt && typeof opt === 'object') {
26
+ enabled = opt.enabled ?? true;
27
+ if (opt.separator !== undefined) {
28
+ if (typeof opt.separator !== 'string' || !opt.separator) throw new Error('files.runNumber.separator must be a non-empty string');
29
+ separator = opt.separator;
30
+ }
31
+ if (opt.padding !== undefined) {
32
+ if (!Number.isInteger(opt.padding) || opt.padding < 0) throw new Error('files.runNumber.padding must be a non-negative integer');
33
+ padding = opt.padding;
34
+ }
35
+ if (opt.startAt !== undefined) {
36
+ if (!Number.isInteger(opt.startAt) || opt.startAt < 0) throw new Error('files.runNumber.startAt must be a non-negative integer');
37
+ startAt = opt.startAt;
38
+ }
39
+ } else if (opt !== undefined && opt !== false) {
40
+ throw new Error('files.runNumber must be a boolean or an object');
41
+ }
42
+ if (!enabled) return null;
43
+ const counterFile = path.join(folder, `.${prefix}.run`);
44
+ let next = startAt;
45
+ try {
46
+ const raw = fs.readFileSync(counterFile, 'utf8').trim();
47
+ const prev = parseInt(raw, 10);
48
+ next = Number.isInteger(prev) ? prev + 1 : startAt;
49
+ } catch {}
50
+ try {
51
+ fs.writeFileSync(counterFile, String(next), 'utf8');
52
+ } catch {}
53
+ return separator + String(next).padStart(padding, '0');
54
+ }
55
+
56
+ function normalizeFilesOptions(files = {}) {
57
+ const rawFolder = files.folder ?? 'logs';
58
+ if (typeof rawFolder !== 'string' || !rawFolder.trim() || rawFolder.includes('\0')) {
59
+ throw new Error('files.folder must be a non-empty string');
60
+ }
61
+ const folder = path.normalize(rawFolder);
62
+ const datePattern = files.datePattern ?? files.dateFormat ?? files.filesName ?? 'YYYY_MM_DD';
63
+ if (typeof datePattern !== 'string' || !datePattern.trim()) {
64
+ throw new Error('files.datePattern must be a non-empty string');
65
+ }
66
+ const explicitPattern = files.filePattern ?? files.pattern ?? files.filename;
67
+ const explicitPrefix = files.prefix ?? files.appName;
68
+ if (explicitPattern !== undefined && explicitPrefix !== undefined) {
69
+ throw new Error('Do not combine "prefix" with "filePattern", pick one: prefix builds "<prefix>-%DATE%.log" for you, filePattern takes full control');
70
+ }
71
+ let filename = explicitPattern ?? `${sanitizeAppName(explicitPrefix) ?? getDefaultAppName()}-%DATE%.log`;
72
+ filename = filename.split('%datePattern%').join('%DATE%');
73
+ assertFilenameSafe(filename);
74
+ if (!filename.includes('%DATE%')) {
75
+ const ext = path.extname(filename) || '.log';
76
+ const base = path.basename(filename, ext);
77
+ filename = `${base}-%DATE%${ext}`;
78
+ }
79
+ assertFilenameSafe(filename);
80
+ if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
81
+ const rawPrefix = path.basename(filename).split('%DATE%')[0];
82
+ let prefixEnd = rawPrefix.length;
83
+ while (prefixEnd > 0 && isPadChar(rawPrefix[prefixEnd - 1])) prefixEnd--;
84
+ const prefix = rawPrefix.slice(0, prefixEnd) || 'application';
85
+ const runSuffix = resolveRunNumber(folder, sanitizeAppName(prefix) ?? 'application', files.runNumber ?? false);
86
+ if (runSuffix) {
87
+ const ext = path.extname(filename) || '.log';
88
+ const base = filename.slice(0, -ext.length);
89
+ filename = `${base}${runSuffix}${ext}`;
90
+ }
91
+ return {
92
+ folder,
93
+ datePattern,
94
+ filename,
95
+ maxFiles: files.maxFile ?? files.maxFiles ?? '14d',
96
+ maxSize: files.maxSize ?? '20m',
97
+ zippedArchive: files.zippedArchive ?? false,
98
+ level: files.level,
99
+ };
100
+ }
101
+
102
+ module.exports = { getDefaultAppName, resolveRunNumber, normalizeFilesOptions };
package/lib/format.js ADDED
@@ -0,0 +1,38 @@
1
+ const util = require('util');
2
+
3
+ function formatExtra(meta) {
4
+ const keys = Object.keys(meta).filter((k) => k !== 'splat' && k !== 'ms');
5
+ if (!keys.length) return '';
6
+ try {
7
+ const picked = {};
8
+ for (const k of keys) picked[k] = meta[k];
9
+ return JSON.stringify(picked);
10
+ } catch {
11
+ return '';
12
+ }
13
+ }
14
+
15
+ function mergeMeta(meta) {
16
+ const out = {};
17
+ for (const m of meta) {
18
+ if (m && typeof m === 'object') Object.assign(out, m);
19
+ }
20
+ return out;
21
+ }
22
+
23
+ function renderBody(message, meta) {
24
+ if (typeof message === 'string' && meta.length && /%[sdifoO%]/.test(message)) {
25
+ return util.format(message, ...meta);
26
+ }
27
+ const msg = typeof message === 'string' ? message : message == null ? '' : JSON.stringify(message);
28
+ const extra = formatExtra(mergeMeta(meta));
29
+ return [msg, extra].filter(Boolean).join(' ');
30
+ }
31
+
32
+ function buildLine(timestamp, level, message, meta, tag) {
33
+ const body = renderBody(message, meta);
34
+ const label = tag ?? `[${level}]`;
35
+ return timestamp ? `${timestamp} ${label}: ${body}` : `${label}: ${body}`;
36
+ }
37
+
38
+ module.exports = { formatExtra, mergeMeta, renderBody, buildLine };