autotel-edge 3.16.0 → 3.16.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/src/api/logger.ts CHANGED
@@ -1,32 +1,199 @@
1
1
  /**
2
- * Zero-dependency structured logger for edge environments
2
+ * Pino-compatible structured logger for edge environments.
3
3
  *
4
- * This logger is ~100 LOC and provides:
5
- * - Structured JSON logging
6
- * - Auto trace context injection (traceId, spanId)
7
- * - Dynamic log level control (per-request via context)
8
- * - Level support (info, error, warn, debug)
9
- * - Zero dependencies (console-based)
4
+ * Supports the practical Pino surface used by app code:
5
+ * - Pino-style `LogFn` calls: string-first, object-first, and error-first
6
+ * - Child loggers, level control, custom levels, and `msgPrefix`
7
+ * - Serializers, formatters, mixins, redaction, and browser-style `transmit`
8
+ * - Trace context injection (`traceId`, `spanId`, `correlationId`)
9
+ * - Worker-safe EventEmitter-like methods for `level-change`
10
10
  *
11
- * Unlike Pino/Winston (~500KB), this is <1KB minified!
11
+ * The implementation stays dependency-light and console/write based so it
12
+ * works in Cloudflare Workers and similar edge runtimes.
12
13
  */
13
14
 
14
- import { trace, context as api_context, createContextKey } from '@opentelemetry/api';
15
+ import {
16
+ trace,
17
+ context as api_context,
18
+ createContextKey,
19
+ } from '@opentelemetry/api';
20
+ import { createRedactor, type RedactorConfig } from './redact';
15
21
 
16
- export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
22
+ export type LogLevel =
23
+ | 'trace'
24
+ | 'debug'
25
+ | 'info'
26
+ | 'warn'
27
+ | 'error'
28
+ | 'fatal'
29
+ | 'silent';
30
+ type LogAttrs = Record<string, any>;
31
+ type LevelWithSilentOrString = LogLevel | (string & {});
32
+ type SerializerFn = (value: any) => any;
33
+ type LevelFormatter = (label: string, number: number) => object;
34
+ type BindingsFormatter = (bindings: LogAttrs) => object;
35
+ type LogFormatter = (object: Record<string, unknown>) => Record<string, unknown>;
36
+ type MixinFn = (mergeObject: object, level: number, logger: EdgeLogger) => object;
37
+ type MixinMergeStrategyFn = (mergeObject: object, mixinObject: object) => object;
38
+ type PlaceholderSpecifier = 'd' | 's' | 'j' | 'o' | 'O';
39
+ type PlaceholderTypeMapping<T extends PlaceholderSpecifier> = T extends 'd'
40
+ ? number
41
+ : T extends 's'
42
+ ? unknown
43
+ : T extends 'j' | 'o' | 'O'
44
+ ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
45
+ {} | null
46
+ : never;
47
+ type ParseLogFnArgs<T, Acc extends unknown[] = []> =
48
+ T extends `${infer _}%${infer Placeholder}${infer Rest}`
49
+ ? Placeholder extends PlaceholderSpecifier
50
+ ? ParseLogFnArgs<Rest, [...Acc, PlaceholderTypeMapping<Placeholder>]>
51
+ : ParseLogFnArgs<Rest, Acc>
52
+ : Acc;
53
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
54
+ export interface LogFnFields {}
55
+ type LogFn = {
56
+ <TMsg extends string = string>(msg: TMsg, ...args: ParseLogFnArgs<TMsg>): void;
57
+ <T, TMsg extends string = string>(
58
+ obj: [T] extends [object] ? T & LogFnFields : T,
59
+ msg?: T extends string ? never : TMsg,
60
+ ...args: ParseLogFnArgs<TMsg> | []
61
+ ): void;
62
+ <T, TMsg extends string = string>(
63
+ obj: [T] extends [object] ? T & LogFnFields : T,
64
+ msg?: T extends string ? never : TMsg,
65
+ ...args: ParseLogFnArgs<TMsg> extends [unknown, ...unknown[]]
66
+ ? ParseLogFnArgs<TMsg>
67
+ : unknown[]
68
+ ): void;
69
+ };
70
+ type ChildLoggerOptions = {
71
+ level?: LevelWithSilentOrString;
72
+ msgPrefix?: string;
73
+ serializers?: Record<string, SerializerFn>;
74
+ formatters?: {
75
+ level?: LevelFormatter;
76
+ bindings?: BindingsFormatter;
77
+ log?: LogFormatter;
78
+ };
79
+ redact?: RedactorConfig;
80
+ };
81
+ type LevelMapping = {
82
+ values: Record<string, number>;
83
+ labels: Record<number, string>;
84
+ };
85
+ type LevelChangeEventListener = (
86
+ levelLabel: LevelWithSilentOrString,
87
+ levelValue: number,
88
+ previousLevelLabel: LevelWithSilentOrString,
89
+ previousLevelValue: number,
90
+ logger: EdgeLogger,
91
+ ) => void;
92
+ type OnChildCallback = (child: EdgeLogger) => void;
93
+ type EventListener = (...args: any[]) => void;
94
+ export type WriteFn = (o: object) => void;
95
+ export type TimeFn = () => string;
96
+
97
+ export interface LogEvent {
98
+ ts: number;
99
+ messages: any[];
100
+ bindings: LogAttrs[];
101
+ level: { label: string; value: number };
102
+ }
103
+
104
+ interface EdgeEventEmitter {
105
+ addListener(event: string | symbol, listener: (...args: any[]) => any): any;
106
+ emit(event: string | symbol, ...args: any[]): boolean;
107
+ eventNames(): (string | symbol)[];
108
+ getMaxListeners(): number;
109
+ listenerCount(event: string | symbol): number;
110
+ listeners(event: string | symbol): ((...args: any[]) => any)[];
111
+ off(event: string | symbol, listener: (...args: any[]) => any): any;
112
+ on(event: string | symbol, listener: (...args: any[]) => any): any;
113
+ once(event: string | symbol, listener: (...args: any[]) => any): any;
114
+ prependListener(event: string | symbol, listener: (...args: any[]) => any): any;
115
+ prependOnceListener(event: string | symbol, listener: (...args: any[]) => any): any;
116
+ rawListeners(event: string | symbol): ((...args: any[]) => any)[];
117
+ removeAllListeners(event?: string | symbol): any;
118
+ removeListener(event: string | symbol, listener: (...args: any[]) => any): any;
119
+ setMaxListeners(n: number): any;
120
+ }
17
121
 
18
122
  /**
19
123
  * Context key for storing active log level (enables per-request log levels)
20
124
  */
21
125
  const LOG_LEVEL_KEY = createContextKey('autotel-edge-log-level');
22
126
 
23
- export interface EdgeLogger {
24
- info(msg: string, attrs?: Record<string, any>): void;
25
- error(msg: string, error?: Error | unknown, attrs?: Record<string, any>): void;
26
- warn(msg: string, attrs?: Record<string, any>): void;
27
- debug(msg: string, attrs?: Record<string, any>): void;
127
+ export interface EdgeLogger extends EdgeEventEmitter {
128
+ readonly version: string;
129
+ readonly name: string | undefined;
130
+ levels: LevelMapping;
131
+ useLevelLabels: boolean;
132
+ level: LevelWithSilentOrString;
133
+ readonly levelVal: number;
134
+ readonly msgPrefix: string | undefined;
135
+ onChild: OnChildCallback;
136
+ enabled: boolean;
137
+ info: LogFn;
138
+ error: LogFn;
139
+ warn: LogFn;
140
+ debug: LogFn;
141
+ trace: LogFn;
142
+ fatal: LogFn;
143
+ silent: LogFn;
144
+ /**
145
+ * Create a child logger with merged bindings.
146
+ * Like pino's child() — every log call from the child
147
+ * includes the parent's attrs plus the child's bindings.
148
+ */
149
+ child(bindings: LogAttrs, options?: ChildLoggerOptions): EdgeLogger;
150
+ isLevelEnabled(level: LevelWithSilentOrString): boolean;
151
+ bindings(): LogAttrs;
152
+ setBindings(bindings: LogAttrs): void;
153
+ flush(cb?: (err?: Error) => void): void;
154
+ on(event: 'level-change', listener: LevelChangeEventListener): EdgeLogger;
155
+ addListener(
156
+ event: 'level-change',
157
+ listener: LevelChangeEventListener,
158
+ ): EdgeLogger;
159
+ once(event: 'level-change', listener: LevelChangeEventListener): EdgeLogger;
160
+ prependListener(
161
+ event: 'level-change',
162
+ listener: LevelChangeEventListener,
163
+ ): EdgeLogger;
164
+ prependOnceListener(
165
+ event: 'level-change',
166
+ listener: LevelChangeEventListener,
167
+ ): EdgeLogger;
168
+ removeListener(
169
+ event: 'level-change',
170
+ listener: LevelChangeEventListener,
171
+ ): EdgeLogger;
172
+ [key: string]: unknown;
28
173
  }
29
174
 
175
+ const PINO_LEVELS: LevelMapping = {
176
+ values: {
177
+ trace: 10,
178
+ debug: 20,
179
+ info: 30,
180
+ warn: 40,
181
+ error: 50,
182
+ fatal: 60,
183
+ silent: Infinity,
184
+ },
185
+ labels: {
186
+ 10: 'trace',
187
+ 20: 'debug',
188
+ 30: 'info',
189
+ 40: 'warn',
190
+ 50: 'error',
191
+ 60: 'fatal',
192
+ [Infinity]: 'silent',
193
+ },
194
+ };
195
+ const LOGGER_VERSION = 'autotel-edge';
196
+
30
197
  /**
31
198
  * Get the active log level from context (if set)
32
199
  * Falls back to undefined if no log level is set in context
@@ -50,7 +217,7 @@ export function getActiveLogLevel(): LogLevel | undefined {
50
217
  * })
51
218
  *
52
219
  * // Disable logging temporarily
53
- * runWithLogLevel('none', () => {
220
+ * runWithLogLevel('silent', () => {
54
221
  * log.info('This will NOT be logged')
55
222
  * })
56
223
  * ```
@@ -63,9 +230,11 @@ export function runWithLogLevel<T>(level: LogLevel, callback: () => T): T {
63
230
  /**
64
231
  * Get current trace context from active span
65
232
  */
66
- function getTraceContext():
67
- | { traceId: string; spanId: string; correlationId: string }
68
- | null {
233
+ function getTraceContext(): {
234
+ traceId: string;
235
+ spanId: string;
236
+ correlationId: string;
237
+ } | null {
69
238
  const span = trace.getActiveSpan();
70
239
  if (!span) return null;
71
240
 
@@ -77,6 +246,184 @@ function getTraceContext():
77
246
  };
78
247
  }
79
248
 
249
+ function isRecord(value: unknown): value is LogAttrs {
250
+ return typeof value === 'object' && value !== null;
251
+ }
252
+
253
+ function toErrorAttrs(error: unknown): LogAttrs {
254
+ if (error instanceof Error) {
255
+ return {
256
+ error: {
257
+ message: error.message,
258
+ type: error.name,
259
+ stack: error.stack,
260
+ },
261
+ };
262
+ }
263
+
264
+ return { error: { message: String(error), type: 'Error' } };
265
+ }
266
+
267
+ function formatMessage(template: string, args: unknown[]): string {
268
+ if (args.length === 0) return template;
269
+
270
+ let argIndex = 0;
271
+ const formatted = template.replaceAll(/%[sdjifoO%]/g, (token) => {
272
+ if (token === '%%') return '%';
273
+ if (argIndex >= args.length) return token;
274
+
275
+ const value = args[argIndex++];
276
+
277
+ switch (token) {
278
+ case '%d':
279
+ case '%i':
280
+ case '%f': {
281
+ return String(Number(value));
282
+ }
283
+ case '%j': {
284
+ try {
285
+ return JSON.stringify(value);
286
+ } catch {
287
+ return '[Circular]';
288
+ }
289
+ }
290
+ case '%s': {
291
+ return String(value);
292
+ }
293
+ case '%o':
294
+ case '%O': {
295
+ try {
296
+ return JSON.stringify(value);
297
+ } catch {
298
+ return String(value);
299
+ }
300
+ }
301
+ default: {
302
+ return token;
303
+ }
304
+ }
305
+ });
306
+
307
+ return formatted;
308
+ }
309
+
310
+ // ANSI color helpers (no dependencies)
311
+ const ANSI_RESET = '\u001B[0m';
312
+ const ANSI_DIM = '\u001B[2m';
313
+ const ANSI_BOLD = '\u001B[1m';
314
+ const ANSI_COLORS: Record<string, string> = {
315
+ fatal: '\u001B[41m\u001B[37m', // white on red bg
316
+ error: '\u001B[31m', // red
317
+ warn: '\u001B[33m', // yellow
318
+ info: '\u001B[36m', // cyan
319
+ debug: '\u001B[34m', // blue
320
+ trace: '\u001B[90m', // gray
321
+ };
322
+ const LEVEL_SYMBOLS: Record<string, string> = {
323
+ fatal: '✗',
324
+ error: '✗',
325
+ warn: '⚠',
326
+ info: '●',
327
+ debug: '◦',
328
+ trace: '…',
329
+ };
330
+
331
+ function formatPrettyTimestamp(): string {
332
+ const d = new Date();
333
+ const h = String(d.getHours()).padStart(2, '0');
334
+ const m = String(d.getMinutes()).padStart(2, '0');
335
+ const s = String(d.getSeconds()).padStart(2, '0');
336
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
337
+ return `${h}:${m}:${s}.${ms}`;
338
+ }
339
+
340
+ function formatPrettyAttrs(attrs: Record<string, any>): string {
341
+ const keys = Object.keys(attrs);
342
+ if (keys.length === 0) return '';
343
+ return '\n' + JSON.stringify(attrs, null, 2)
344
+ .split('\n')
345
+ .map((line) => ` ${line}`)
346
+ .join('\n');
347
+ }
348
+
349
+ function safeStringify(
350
+ obj: unknown,
351
+ depthLimit: number,
352
+ edgeLimit: number,
353
+ ): string {
354
+ // Stack-based approach: track ancestors on the current path.
355
+ // Objects are added when entered and removed when the replacer
356
+ // moves back up, so shared (non-circular) references serialize
357
+ // normally while true cycles produce '[Circular]'.
358
+ const ancestors: object[] = [];
359
+
360
+ return JSON.stringify(obj, function replacer(this: any, _key: string, value: unknown) {
361
+ if (typeof value === 'object' && value !== null) {
362
+ // Unwind: `this` is the object that owns the current key.
363
+ // Pop ancestors until we find `this` (our direct parent).
364
+ while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
365
+ ancestors.pop();
366
+ }
367
+
368
+ // True circular reference: value is already an ancestor
369
+ if (ancestors.includes(value as object)) return '[Circular]';
370
+
371
+ if (ancestors.length >= depthLimit) return '[Object]';
372
+
373
+ ancestors.push(value as object);
374
+
375
+ if (!Array.isArray(value)) {
376
+ const keys = Object.keys(value as Record<string, unknown>);
377
+ if (keys.length > edgeLimit) {
378
+ const truncated: Record<string, unknown> = {};
379
+ for (let i = 0; i < edgeLimit; i++) {
380
+ truncated[keys[i]] = (value as Record<string, unknown>)[keys[i]];
381
+ }
382
+ truncated['...'] = `[${keys.length - edgeLimit} more properties]`;
383
+ return truncated;
384
+ }
385
+ }
386
+ }
387
+ return value;
388
+ });
389
+ }
390
+
391
+ function parseLogArgs(args: unknown[]): { msg: string; attrs?: LogAttrs } {
392
+ const [first, ...rest] = args;
393
+
394
+ if (typeof first === 'string') {
395
+ return {
396
+ msg: formatMessage(first, rest),
397
+ attrs: undefined,
398
+ };
399
+ }
400
+
401
+ if (first instanceof Error) {
402
+ return {
403
+ msg:
404
+ typeof rest[0] === 'string'
405
+ ? formatMessage(rest[0], rest.slice(1))
406
+ : first.message,
407
+ attrs: toErrorAttrs(first),
408
+ };
409
+ }
410
+
411
+ if (isRecord(first)) {
412
+ return {
413
+ msg:
414
+ typeof rest[0] === 'string'
415
+ ? formatMessage(rest[0], rest.slice(1))
416
+ : '',
417
+ attrs: first,
418
+ };
419
+ }
420
+
421
+ return {
422
+ msg: String(first ?? ''),
423
+ attrs: undefined,
424
+ };
425
+ }
426
+
80
427
  /**
81
428
  * Create a lightweight structured logger
82
429
  *
@@ -87,7 +434,7 @@ function getTraceContext():
87
434
  * ```typescript
88
435
  * const log = createEdgeLogger('user-service')
89
436
  *
90
- * log.info('Creating user', { email: 'test@example.com' })
437
+ * log.info({ email: 'test@example.com' }, 'Creating user')
91
438
  * // Output: {"level":"info","service":"user-service","msg":"Creating user",
92
439
  * // "email":"test@example.com","traceId":"...","spanId":"..."}
93
440
  *
@@ -97,86 +444,571 @@ function getTraceContext():
97
444
  * })
98
445
  * ```
99
446
  */
447
+ export type EdgeLoggerOptions = {
448
+ level?: LevelWithSilentOrString;
449
+ pretty?: boolean;
450
+ bindings?: Record<string, any>;
451
+ redact?: RedactorConfig | string[];
452
+ msgPrefix?: string;
453
+ useLevelLabels?: boolean;
454
+ onChild?: OnChildCallback;
455
+ enabled?: boolean;
456
+ messageKey?: string;
457
+ errorKey?: string;
458
+ nestedKey?: string;
459
+ serializers?: Record<string, SerializerFn>;
460
+ formatters?: {
461
+ level?: LevelFormatter;
462
+ bindings?: BindingsFormatter;
463
+ log?: LogFormatter;
464
+ };
465
+ mixin?: MixinFn;
466
+ mixinMergeStrategy?: MixinMergeStrategyFn;
467
+ customLevels?: Record<string, number>;
468
+ useOnlyCustomLevels?: boolean;
469
+ levelComparison?: 'ASC' | 'DESC' | ((current: number, expected: number) => boolean);
470
+ name?: string;
471
+ base?: Record<string, any> | null;
472
+ timestamp?: TimeFn | boolean;
473
+ safe?: boolean;
474
+ crlf?: boolean;
475
+ depthLimit?: number;
476
+ edgeLimit?: number;
477
+ hooks?: {
478
+ logMethod?: (
479
+ args: Parameters<LogFn>,
480
+ method: LogFn,
481
+ level: number,
482
+ ) => void;
483
+ };
484
+ write?: WriteFn | ({ [level: string]: WriteFn });
485
+ transmit?: {
486
+ level?: LevelWithSilentOrString;
487
+ send: (level: string, logEvent: LogEvent) => void;
488
+ };
489
+ /** @internal Binding chain for hierarchical transmit bindings */
490
+ _bindingsChain?: LogAttrs[];
491
+ };
492
+
100
493
  export function createEdgeLogger(
101
494
  service: string,
102
- options?: {
103
- level?: LogLevel;
104
- pretty?: boolean; // For development
105
- },
495
+ options?: EdgeLoggerOptions,
106
496
  ): EdgeLogger {
107
- const defaultLevel = options?.level || 'info';
497
+ const customLevels: Record<string, number> = {
498
+ ...options?.customLevels,
499
+ };
500
+ const availableLevels: Record<string, number> = options?.useOnlyCustomLevels
501
+ ? { ...customLevels, silent: Infinity }
502
+ : { ...PINO_LEVELS.values, ...customLevels };
503
+ const defaultLevel = options?.level ?? (options?.useOnlyCustomLevels ? Object.keys(customLevels)[0] : 'info') ?? 'info';
504
+ let currentLevel = defaultLevel as LevelWithSilentOrString;
108
505
  const pretty = options?.pretty || false;
506
+ const currentBindings = { ...options?.bindings };
507
+ const resolvedRedact = Array.isArray(options?.redact)
508
+ ? { paths: options.redact }
509
+ : options?.redact;
510
+ const redactor = resolvedRedact ? createRedactor(resolvedRedact) : null;
511
+ const msgPrefix = options?.msgPrefix;
512
+ const levelValues = availableLevels;
513
+ const levelListeners: LevelChangeEventListener[] = [];
514
+ const onChildCallback: OnChildCallback = options?.onChild ?? (() => {});
515
+ const listeners = new Map<string | symbol, EventListener[]>();
516
+ let maxListeners = 10;
517
+ const messageKey = options?.messageKey ?? 'msg';
518
+ const errorKey = options?.errorKey ?? 'err';
519
+ const nestedKey = options?.nestedKey;
520
+ const serializers = { ...options?.serializers };
521
+ const bindingsFormatter = options?.formatters?.bindings;
522
+ const levelFormatter = options?.formatters?.level;
523
+ const logFormatter = options?.formatters?.log;
524
+ const mixin = options?.mixin;
525
+ const levelComparison = options?.levelComparison;
526
+ const mixinMergeStrategy =
527
+ options?.mixinMergeStrategy ?? ((mergeObject: object, mixinObject: object) => ({ ...mergeObject, ...mixinObject }));
528
+ let enabled = options?.enabled ?? true;
529
+ const loggerName = options?.name;
530
+ const base = options?.base;
531
+ const timestamp = options?.timestamp ?? true;
532
+ const safe = options?.safe ?? true;
533
+ const crlf = options?.crlf ?? false;
534
+ const depthLimit = options?.depthLimit ?? 5;
535
+ const edgeLimitOpt = options?.edgeLimit ?? 100;
536
+ const hooks = options?.hooks;
537
+ const writeFn = options?.write;
538
+ const transmit = options?.transmit;
539
+ const bindingsChain: LogAttrs[] = options?._bindingsChain ?? [];
109
540
 
110
- const levelPriority: Record<LogLevel, number> = {
111
- none: -1,
112
- debug: 0,
113
- info: 1,
114
- warn: 2,
115
- error: 3,
541
+ const compareLevels = (
542
+ current: number,
543
+ expected: number,
544
+ comparison: 'ASC' | 'DESC' | ((current: number, expected: number) => boolean) | undefined,
545
+ ) => {
546
+ if (typeof comparison === 'function') return comparison(current, expected);
547
+ if (comparison === 'DESC') return current <= expected;
548
+ return current >= expected;
116
549
  };
117
550
 
118
- const shouldLog = (level: LogLevel): boolean => {
551
+ const addListener = (
552
+ event: string | symbol,
553
+ listener: EventListener,
554
+ prepend = false,
555
+ ) => {
556
+ const activeListeners = listeners.get(event) ?? [];
557
+ if (prepend) {
558
+ activeListeners.unshift(listener);
559
+ } else {
560
+ activeListeners.push(listener);
561
+ }
562
+ listeners.set(event, activeListeners);
563
+ if (event === 'level-change') {
564
+ levelListeners.length = 0;
565
+ levelListeners.push(
566
+ ...(listeners.get(event) as LevelChangeEventListener[] | undefined ?? []),
567
+ );
568
+ }
569
+ };
570
+
571
+ const notifyLevelChange = (
572
+ nextLevel: LevelWithSilentOrString,
573
+ previousLevel: LevelWithSilentOrString,
574
+ instance: EdgeLogger,
575
+ ) => {
576
+ const nextValue = levelValues[nextLevel] ?? Infinity;
577
+ const previousValue = levelValues[previousLevel] ?? Infinity;
578
+ const snapshot = [...levelListeners];
579
+ for (const listener of snapshot) {
580
+ listener(nextLevel, nextValue, previousLevel, previousValue, instance);
581
+ }
582
+ };
583
+
584
+ const removeEventListener = (event: string | symbol, listener: EventListener) => {
585
+ const activeListeners = listeners.get(event);
586
+ if (!activeListeners) {
587
+ return;
588
+ }
589
+ const index = activeListeners.indexOf(listener);
590
+ if (index !== -1) {
591
+ activeListeners.splice(index, 1);
592
+ }
593
+ if (activeListeners.length === 0) {
594
+ listeners.delete(event);
595
+ } else {
596
+ listeners.set(event, activeListeners);
597
+ }
598
+ if (event === 'level-change') {
599
+ levelListeners.length = 0;
600
+ levelListeners.push(
601
+ ...((listeners.get(event) as LevelChangeEventListener[] | undefined) ??
602
+ []),
603
+ );
604
+ }
605
+ };
606
+
607
+ const shouldLog = (level: LevelWithSilentOrString): boolean => {
608
+ if (!enabled) return false;
609
+
119
610
  // Priority: context level > options level > 'info' default
120
- const activeLevel = getActiveLogLevel() ?? defaultLevel;
611
+ const activeLevel = getActiveLogLevel() ?? currentLevel;
612
+
613
+ // 'silent' means suppress all logging
614
+ if (activeLevel === 'silent') return false;
615
+ const currentValue = levelValues[activeLevel];
616
+ const expectedValue = levelValues[level];
617
+ if (currentValue === undefined || expectedValue === undefined) return false;
618
+
619
+ return compareLevels(
620
+ expectedValue,
621
+ currentValue,
622
+ levelComparison,
623
+ );
624
+ };
625
+
626
+ const stringify = (obj: unknown): string => {
627
+ if (safe) {
628
+ return safeStringify(obj, depthLimit, edgeLimitOpt);
629
+ }
630
+ return JSON.stringify(obj);
631
+ };
632
+
633
+ const getTimestamp = (): string | undefined => {
634
+ if (timestamp === false) return undefined;
635
+ if (typeof timestamp === 'function') return timestamp();
636
+ return new Date().toISOString();
637
+ };
121
638
 
122
- // 'none' means suppress all logging
123
- if (activeLevel === 'none') return false;
639
+ const writeOutput = (level: LevelWithSilentOrString, logObject: Record<string, any>) => {
640
+ if (writeFn) {
641
+ if (typeof writeFn === 'function') {
642
+ writeFn(logObject);
643
+ } else if (typeof writeFn === 'object' && writeFn[level]) {
644
+ writeFn[level](logObject);
645
+ } else {
646
+ console.log(stringify(logObject));
647
+ }
648
+ } else if (pretty) {
649
+ return; // pretty mode handles its own output
650
+ } else {
651
+ const json = stringify(logObject);
652
+ console.log(crlf ? json + '\r\n' : json);
653
+ }
654
+ };
124
655
 
125
- return levelPriority[level] >= levelPriority[activeLevel];
656
+ const applySerializers = (obj: Record<string, any>): Record<string, any> => {
657
+ if (Object.keys(serializers).length === 0) return obj;
658
+ return Object.fromEntries(
659
+ Object.entries(obj).map(([key, value]) => [
660
+ key,
661
+ key in serializers ? serializers[key](value) : value,
662
+ ]),
663
+ );
664
+ };
665
+
666
+ const emitTransmit = (
667
+ level: LevelWithSilentOrString,
668
+ rawArgs: unknown[],
669
+ ) => {
670
+ if (!transmit) return;
671
+ if (transmit.level) {
672
+ const transmitValue = levelValues[transmit.level];
673
+ const logValue = levelValues[level];
674
+ if (transmitValue !== undefined && logValue !== undefined && logValue < transmitValue) {
675
+ return;
676
+ }
677
+ }
678
+ // Apply serializers then redaction to messages
679
+ const processedMessages = rawArgs.map((m) => {
680
+ if (typeof m !== 'object' || m === null) return m;
681
+ const serialized = applySerializers(m as Record<string, any>);
682
+ return redactor ? redactor(serialized) : serialized;
683
+ });
684
+ // Apply serializers then redaction to bindings
685
+ const processedBindings = bindingsChain.map((b) => {
686
+ const serialized = applySerializers({ ...b });
687
+ return redactor ? (redactor(serialized) as LogAttrs) : serialized;
688
+ });
689
+ const logEvent: LogEvent = {
690
+ ts: Date.now(),
691
+ messages: processedMessages,
692
+ bindings: processedBindings,
693
+ level: { label: level, value: levelValues[level] },
694
+ };
695
+ transmit.send(level, logEvent);
126
696
  };
127
697
 
128
698
  const log = (
129
- level: 'info' | 'error' | 'warn' | 'debug',
699
+ level: LevelWithSilentOrString,
130
700
  msg: string,
131
701
  attrs?: Record<string, any>,
702
+ rawArgs?: unknown[],
132
703
  ) => {
133
704
  if (!shouldLog(level)) return;
134
705
 
135
706
  const ctx = getTraceContext();
707
+ const message = msgPrefix ? `${msgPrefix}${msg}` : msg;
708
+ const baseBindings = bindingsFormatter
709
+ ? (bindingsFormatter({ ...currentBindings }) as LogAttrs)
710
+ : { ...currentBindings };
711
+ const serializedAttrs = attrs
712
+ ? Object.fromEntries(
713
+ Object.entries(attrs).map(([key, value]) => [
714
+ key,
715
+ key in serializers ? serializers[key](value) : value,
716
+ ]),
717
+ )
718
+ : undefined;
719
+ const mergeObject = nestedKey
720
+ ? ({ [nestedKey]: serializedAttrs ?? {} } as Record<string, unknown>)
721
+ : (serializedAttrs ?? {});
722
+ const mixinObject =
723
+ mixin?.(mergeObject, levelValues[level], logger) ?? {};
724
+ const mergedLogObject = mixinMergeStrategy(mergeObject, mixinObject) as Record<
725
+ string,
726
+ unknown
727
+ >;
728
+ const formattedLogObject = logFormatter
729
+ ? logFormatter(mergedLogObject)
730
+ : mergedLogObject;
731
+ const formattedLevel = levelFormatter
732
+ ? levelFormatter(level, levelValues[level])
733
+ : {
734
+ level: logger.useLevelLabels ? level : levelValues[level],
735
+ };
736
+ const ts = getTimestamp();
136
737
  const logEntry: Record<string, any> = {
137
- level,
738
+ ...formattedLevel,
739
+ ...(loggerName === undefined ? {} : { name: loggerName }),
138
740
  service,
139
- msg,
140
- ...attrs,
741
+ ...(message === '' ? {} : { [messageKey]: message }),
742
+ ...(base === null || base === undefined ? {} : base),
743
+ ...baseBindings,
744
+ ...formattedLogObject,
141
745
  ...ctx, // Auto-inject traceId, spanId, correlationId
142
- timestamp: new Date().toISOString(),
746
+ ...(ts === undefined ? {} : { timestamp: ts }),
143
747
  };
144
748
 
145
- if (pretty) {
749
+ if (serializedAttrs && !nestedKey && errorKey !== 'error' && 'error' in logEntry) {
750
+ logEntry[errorKey] = logEntry.error;
751
+ delete logEntry.error;
752
+ }
753
+
754
+ if (pretty && !writeFn) {
146
755
  // Pretty print for development
756
+ const color = ANSI_COLORS[level] ?? ANSI_COLORS.info;
757
+ const symbol = LEVEL_SYMBOLS[level] ?? '●';
758
+ const time = formatPrettyTimestamp();
147
759
  const traceInfo = ctx
148
- ? ` [${ctx.traceId.slice(0, 8)}.../${ctx.spanId.slice(0, 8)}...]`
760
+ ? ` ${ANSI_DIM}[${ctx.traceId.slice(0, 8)}/${ctx.spanId.slice(0, 8)}]${ANSI_RESET}`
149
761
  : '';
762
+ const prettyAttrs = {
763
+ ...baseBindings,
764
+ ...formattedLogObject,
765
+ };
766
+ const safeAttrs = redactor
767
+ ? (redactor(prettyAttrs) as LogAttrs)
768
+ : prettyAttrs;
769
+ const attrsStr = formatPrettyAttrs(safeAttrs);
150
770
  console.log(
151
- `[${level.toUpperCase()}]${traceInfo} ${service}: ${msg}`,
152
- attrs || '',
771
+ `${ANSI_DIM}${time}${ANSI_RESET} ${color}${ANSI_BOLD}${symbol} ${level.toUpperCase()}${ANSI_RESET}${traceInfo} ${ANSI_DIM}(${service})${ANSI_RESET} ${message}${attrsStr}`,
153
772
  );
154
773
  } else {
155
- // Structured JSON for production
156
- console.log(JSON.stringify(logEntry));
774
+ // Structured JSON for production (or custom write)
775
+ const safeEntry = redactor ? (redactor(logEntry) as LogAttrs) : logEntry;
776
+ writeOutput(level, safeEntry);
157
777
  }
778
+
779
+ emitTransmit(level, rawArgs ?? []);
158
780
  };
159
781
 
160
- return {
161
- info: (msg: string, attrs?: Record<string, any>) => log('info', msg, attrs),
162
-
163
- error: (msg: string, error?: Error | unknown, attrs?: Record<string, any>) => {
164
- const errorAttrs = error instanceof Error
165
- ? {
166
- error: error.message,
167
- stack: error.stack,
168
- name: error.name,
169
- ...attrs,
170
- }
171
- : { error: String(error), ...attrs };
782
+ const makeLogMethod = (levelName: string): LogFn => {
783
+ return ((...args: unknown[]) => {
784
+ if (hooks?.logMethod) {
785
+ const method = ((...methodArgs: unknown[]) => {
786
+ const { msg, attrs } = parseLogArgs(methodArgs);
787
+ log(levelName, msg, attrs, methodArgs);
788
+ }) as LogFn;
789
+ hooks.logMethod.call(logger, args as Parameters<LogFn>, method, levelValues[levelName]);
790
+ return;
791
+ }
792
+ const { msg, attrs } = parseLogArgs(args);
793
+ log(levelName, msg, attrs, args);
794
+ }) as LogFn;
795
+ };
796
+
797
+ const logger: EdgeLogger = {
798
+ version: LOGGER_VERSION,
799
+
800
+ get name() {
801
+ return loggerName;
802
+ },
803
+
804
+ levels: {
805
+ values: levelValues,
806
+ labels: Object.fromEntries(
807
+ Object.entries(levelValues).map(([label, value]) => [value, label]),
808
+ ),
809
+ },
810
+ useLevelLabels: options?.useLevelLabels ?? false,
811
+ customLevels: { ...customLevels },
812
+ useOnlyCustomLevels: options?.useOnlyCustomLevels ?? false,
813
+
814
+ get level() {
815
+ return currentLevel;
816
+ },
817
+
818
+ set level(level: LevelWithSilentOrString) {
819
+ const previousLevel = currentLevel;
820
+ currentLevel = level;
821
+ notifyLevelChange(level, previousLevel, logger);
822
+ },
172
823
 
173
- log('error', msg, errorAttrs);
824
+ get levelVal() {
825
+ return levelValues[currentLevel] ?? Infinity;
174
826
  },
175
827
 
176
- warn: (msg: string, attrs?: Record<string, any>) => log('warn', msg, attrs),
828
+ get msgPrefix() {
829
+ return msgPrefix;
830
+ },
831
+
832
+ onChild: onChildCallback,
833
+ get enabled() {
834
+ return enabled;
835
+ },
836
+
837
+ set enabled(value: boolean) {
838
+ enabled = value;
839
+ },
840
+
841
+ info: makeLogMethod('info'),
842
+
843
+ error: makeLogMethod('error'),
844
+
845
+ warn: makeLogMethod('warn'),
846
+
847
+ debug: makeLogMethod('debug'),
177
848
 
178
- debug: (msg: string, attrs?: Record<string, any>) => log('debug', msg, attrs),
849
+ trace: makeLogMethod('trace'),
850
+
851
+ fatal: makeLogMethod('fatal'),
852
+
853
+ silent: (() => {}) as LogFn,
854
+
855
+ child: (bindings: LogAttrs, childOptions?: ChildLoggerOptions) => {
856
+ const childLogger = createEdgeLogger(service, {
857
+ level: childOptions?.level ?? currentLevel,
858
+ pretty,
859
+ bindings: { ...currentBindings, ...bindings },
860
+ redact: childOptions?.redact ?? options?.redact,
861
+ msgPrefix:
862
+ childOptions?.msgPrefix === undefined
863
+ ? msgPrefix
864
+ : `${msgPrefix ?? ''}${childOptions.msgPrefix}`,
865
+ useLevelLabels: logger.useLevelLabels,
866
+ onChild: onChildCallback,
867
+ enabled,
868
+ messageKey,
869
+ errorKey,
870
+ nestedKey,
871
+ serializers: { ...serializers, ...childOptions?.serializers },
872
+ formatters: {
873
+ level: childOptions?.formatters?.level ?? levelFormatter,
874
+ bindings: childOptions?.formatters?.bindings ?? bindingsFormatter,
875
+ log: childOptions?.formatters?.log ?? logFormatter,
876
+ },
877
+ mixin,
878
+ mixinMergeStrategy,
879
+ customLevels,
880
+ useOnlyCustomLevels: options?.useOnlyCustomLevels,
881
+ levelComparison: options?.levelComparison,
882
+ name: loggerName,
883
+ base,
884
+ timestamp,
885
+ safe,
886
+ crlf,
887
+ depthLimit,
888
+ edgeLimit: edgeLimitOpt,
889
+ hooks,
890
+ write: writeFn,
891
+ transmit,
892
+ _bindingsChain: [...bindingsChain, bindings],
893
+ });
894
+ onChildCallback(childLogger);
895
+ return childLogger;
896
+ },
897
+
898
+ isLevelEnabled: (level: LevelWithSilentOrString) => shouldLog(level),
899
+
900
+ bindings: () => ({ ...currentBindings }),
901
+
902
+ setBindings: (bindings: LogAttrs) => {
903
+ for (const [key, value] of Object.entries(bindings)) {
904
+ if (!(key in currentBindings)) {
905
+ currentBindings[key] = value;
906
+ }
907
+ }
908
+ },
909
+
910
+ flush: (cb?: (err?: Error) => void) => {
911
+ cb?.();
912
+ },
913
+
914
+ emit: (event: string | symbol, ...args: any[]) => {
915
+ const activeListeners = listeners.get(event);
916
+ if (!activeListeners || activeListeners.length === 0) {
917
+ return false;
918
+ }
919
+ const snapshot = [...activeListeners];
920
+ for (const listener of snapshot) {
921
+ listener(...args);
922
+ }
923
+ return true;
924
+ },
925
+
926
+ eventNames: () => [...listeners.keys()],
927
+
928
+ getMaxListeners: () => maxListeners,
929
+
930
+ listenerCount: (event: string | symbol) => listeners.get(event)?.length ?? 0,
931
+
932
+ listeners: (event: string | symbol) => [...(listeners.get(event) ?? [])],
933
+
934
+ on: (event: string | symbol, listener: EventListener) => {
935
+ addListener(event, listener);
936
+ return logger;
937
+ },
938
+
939
+ addListener: (event: string | symbol, listener: EventListener) => {
940
+ addListener(event, listener);
941
+ return logger;
942
+ },
943
+
944
+ once: (event: string | symbol, listener: EventListener) => {
945
+ const wrapped: EventListener = (...args: any[]) => {
946
+ removeEventListener(event, wrapped);
947
+ listener(...args);
948
+ };
949
+ addListener(event, wrapped);
950
+ return logger;
951
+ },
952
+
953
+ prependListener: (
954
+ event: string | symbol,
955
+ listener: EventListener,
956
+ ) => {
957
+ addListener(event, listener, true);
958
+ return logger;
959
+ },
960
+
961
+ prependOnceListener: (
962
+ event: string | symbol,
963
+ listener: EventListener,
964
+ ) => {
965
+ const wrapped: EventListener = (...args: any[]) => {
966
+ removeEventListener(event, wrapped);
967
+ listener(...args);
968
+ };
969
+ addListener(event, wrapped, true);
970
+ return logger;
971
+ },
972
+
973
+ rawListeners: (event: string | symbol) => [...(listeners.get(event) ?? [])],
974
+
975
+ off: (event: string | symbol, listener: EventListener) => {
976
+ removeEventListener(event, listener);
977
+ return logger;
978
+ },
979
+
980
+ removeAllListeners: (event?: string | symbol) => {
981
+ if (event === undefined) {
982
+ listeners.clear();
983
+ levelListeners.length = 0;
984
+ return logger;
985
+ }
986
+ listeners.delete(event);
987
+ if (event === 'level-change') {
988
+ levelListeners.length = 0;
989
+ }
990
+ return logger;
991
+ },
992
+
993
+ removeListener: (
994
+ event: string | symbol,
995
+ listener: EventListener,
996
+ ) => {
997
+ removeEventListener(event, listener);
998
+ return logger;
999
+ },
1000
+
1001
+ setMaxListeners: (n: number) => {
1002
+ maxListeners = n;
1003
+ return logger;
1004
+ },
179
1005
  };
1006
+
1007
+ for (const levelName of Object.keys(customLevels)) {
1008
+ logger[levelName] = makeLogMethod(levelName);
1009
+ }
1010
+
1011
+ return logger;
180
1012
  }
181
1013
 
182
1014
  /**