autotel-edge 3.17.0 → 3.17.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.
Files changed (57) hide show
  1. package/LICENSE +191 -21
  2. package/README.md +2 -2
  3. package/dist/{config-CEF9DMGT.js → config-CFlfvmQ1.js} +2 -2
  4. package/dist/{config-CEF9DMGT.js.map → config-CFlfvmQ1.js.map} +1 -1
  5. package/dist/events.js +13 -9
  6. package/dist/events.js.map +1 -1
  7. package/dist/{execution-logger-CoxSsBmU.js → execution-logger-CSW2AyVi.js} +7 -4
  8. package/dist/{execution-logger-CoxSsBmU.js.map → execution-logger-CSW2AyVi.js.map} +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/logger.js +7 -3
  12. package/dist/logger.js.map +1 -1
  13. package/package.json +5 -6
  14. package/src/api/logger.test.ts +0 -1616
  15. package/src/api/logger.ts +0 -1038
  16. package/src/api/redact.test.ts +0 -432
  17. package/src/api/redact.ts +0 -402
  18. package/src/compose.ts +0 -243
  19. package/src/composition.test.ts +0 -130
  20. package/src/composition.ts +0 -132
  21. package/src/core/buffer.ts +0 -17
  22. package/src/core/config.test.ts +0 -413
  23. package/src/core/config.ts +0 -169
  24. package/src/core/context.ts +0 -245
  25. package/src/core/exporter.ts +0 -99
  26. package/src/core/native-bridge.test.ts +0 -176
  27. package/src/core/native-bridge.ts +0 -278
  28. package/src/core/provider.context.test.ts +0 -55
  29. package/src/core/provider.ts +0 -46
  30. package/src/core/span.ts +0 -222
  31. package/src/core/spanprocessor.test.ts +0 -521
  32. package/src/core/spanprocessor.ts +0 -232
  33. package/src/core/trace-context.ts +0 -95
  34. package/src/core/tracer.test.ts +0 -123
  35. package/src/core/tracer.ts +0 -216
  36. package/src/events/index.test.ts +0 -242
  37. package/src/events/index.ts +0 -338
  38. package/src/events.ts +0 -6
  39. package/src/execution-logger.test.ts +0 -368
  40. package/src/execution-logger.ts +0 -410
  41. package/src/functional.test.ts +0 -827
  42. package/src/functional.ts +0 -1002
  43. package/src/index.ts +0 -140
  44. package/src/logger.ts +0 -35
  45. package/src/middleware-toolkit.test.ts +0 -71
  46. package/src/middleware-toolkit.ts +0 -94
  47. package/src/native-routing.test.ts +0 -98
  48. package/src/parse-error.test.ts +0 -70
  49. package/src/parse-error.ts +0 -110
  50. package/src/plugin-runner.test.ts +0 -100
  51. package/src/plugin-runner.ts +0 -215
  52. package/src/sampling/index.test.ts +0 -297
  53. package/src/sampling/index.ts +0 -276
  54. package/src/sampling.ts +0 -6
  55. package/src/testing/index.ts +0 -9
  56. package/src/testing.ts +0 -6
  57. package/src/types.ts +0 -325
package/src/api/logger.ts DELETED
@@ -1,1038 +0,0 @@
1
- /**
2
- * Pino-compatible structured logger for edge environments.
3
- *
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
- *
11
- * The implementation stays dependency-light and console/write based so it
12
- * works in Cloudflare Workers and similar edge runtimes.
13
- */
14
-
15
- import {
16
- trace,
17
- context as api_context,
18
- createContextKey,
19
- } from '@opentelemetry/api';
20
- import { createRedactor, type RedactorConfig } from './redact';
21
-
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
- }
121
-
122
- /**
123
- * Context key for storing active log level (enables per-request log levels)
124
- */
125
- const LOG_LEVEL_KEY = createContextKey('autotel-edge-log-level');
126
-
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;
173
- }
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
-
197
- /**
198
- * Get the active log level from context (if set)
199
- * Falls back to undefined if no log level is set in context
200
- */
201
- export function getActiveLogLevel(): LogLevel | undefined {
202
- return api_context.active().getValue(LOG_LEVEL_KEY) as LogLevel | undefined;
203
- }
204
-
205
- /**
206
- * Run a function with a specific log level
207
- * The log level is stored in OpenTelemetry context and applies to all logger calls within the callback
208
- *
209
- * This works in edge runtimes (uses OTel context, not Node.js AsyncLocalStorage)
210
- *
211
- * @example
212
- * ```typescript
213
- * // Enable debug logging for a specific request
214
- * runWithLogLevel('debug', () => {
215
- * log.debug('This will be logged')
216
- * processRequest()
217
- * })
218
- *
219
- * // Disable logging temporarily
220
- * runWithLogLevel('silent', () => {
221
- * log.info('This will NOT be logged')
222
- * })
223
- * ```
224
- */
225
- export function runWithLogLevel<T>(level: LogLevel, callback: () => T): T {
226
- const ctx = api_context.active().setValue(LOG_LEVEL_KEY, level);
227
- return api_context.with(ctx, callback);
228
- }
229
-
230
- /**
231
- * Get current trace context from active span
232
- */
233
- function getTraceContext(): {
234
- traceId: string;
235
- spanId: string;
236
- correlationId: string;
237
- } | null {
238
- const span = trace.getActiveSpan();
239
- if (!span) return null;
240
-
241
- const ctx = span.spanContext();
242
- return {
243
- traceId: ctx.traceId,
244
- spanId: ctx.spanId,
245
- correlationId: ctx.traceId.slice(0, 16), // First 16 chars for grouping
246
- };
247
- }
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
-
427
- /**
428
- * Create a lightweight structured logger
429
- *
430
- * @param service - Service name for logging
431
- * @param options - Optional configuration
432
- *
433
- * @example
434
- * ```typescript
435
- * const log = createEdgeLogger('user-service')
436
- *
437
- * log.info({ email: 'test@example.com' }, 'Creating user')
438
- * // Output: {"level":"info","service":"user-service","msg":"Creating user",
439
- * // "email":"test@example.com","traceId":"...","spanId":"..."}
440
- *
441
- * // Dynamic log level control per-request
442
- * runWithLogLevel('debug', () => {
443
- * log.debug('This will be logged even if logger was created with level: "info"')
444
- * })
445
- * ```
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
-
493
- export function createEdgeLogger(
494
- service: string,
495
- options?: EdgeLoggerOptions,
496
- ): EdgeLogger {
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;
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
- // Default serializer for the error key, so `log.error({ err }, msg)` keeps the
522
- // message/stack instead of JSON-stringifying an Error to `{}`. Mirrors the
523
- // first-arg Error shape (toErrorAttrs). Overridable via options.serializers.
524
- if (!(errorKey in serializers)) {
525
- serializers[errorKey] = (value: unknown) =>
526
- value instanceof Error
527
- ? { message: value.message, type: value.name, stack: value.stack }
528
- : value;
529
- }
530
- const bindingsFormatter = options?.formatters?.bindings;
531
- const levelFormatter = options?.formatters?.level;
532
- const logFormatter = options?.formatters?.log;
533
- const mixin = options?.mixin;
534
- const levelComparison = options?.levelComparison;
535
- const mixinMergeStrategy =
536
- options?.mixinMergeStrategy ?? ((mergeObject: object, mixinObject: object) => ({ ...mergeObject, ...mixinObject }));
537
- let enabled = options?.enabled ?? true;
538
- const loggerName = options?.name;
539
- const base = options?.base;
540
- const timestamp = options?.timestamp ?? true;
541
- const safe = options?.safe ?? true;
542
- const crlf = options?.crlf ?? false;
543
- const depthLimit = options?.depthLimit ?? 5;
544
- const edgeLimitOpt = options?.edgeLimit ?? 100;
545
- const hooks = options?.hooks;
546
- const writeFn = options?.write;
547
- const transmit = options?.transmit;
548
- const bindingsChain: LogAttrs[] = options?._bindingsChain ?? [];
549
-
550
- const compareLevels = (
551
- current: number,
552
- expected: number,
553
- comparison: 'ASC' | 'DESC' | ((current: number, expected: number) => boolean) | undefined,
554
- ) => {
555
- if (typeof comparison === 'function') return comparison(current, expected);
556
- if (comparison === 'DESC') return current <= expected;
557
- return current >= expected;
558
- };
559
-
560
- const addListener = (
561
- event: string | symbol,
562
- listener: EventListener,
563
- prepend = false,
564
- ) => {
565
- const activeListeners = listeners.get(event) ?? [];
566
- if (prepend) {
567
- activeListeners.unshift(listener);
568
- } else {
569
- activeListeners.push(listener);
570
- }
571
- listeners.set(event, activeListeners);
572
- if (event === 'level-change') {
573
- levelListeners.length = 0;
574
- levelListeners.push(
575
- ...(listeners.get(event) as LevelChangeEventListener[] | undefined ?? []),
576
- );
577
- }
578
- };
579
-
580
- const notifyLevelChange = (
581
- nextLevel: LevelWithSilentOrString,
582
- previousLevel: LevelWithSilentOrString,
583
- instance: EdgeLogger,
584
- ) => {
585
- const nextValue = levelValues[nextLevel] ?? Infinity;
586
- const previousValue = levelValues[previousLevel] ?? Infinity;
587
- const snapshot = [...levelListeners];
588
- for (const listener of snapshot) {
589
- listener(nextLevel, nextValue, previousLevel, previousValue, instance);
590
- }
591
- };
592
-
593
- const removeEventListener = (event: string | symbol, listener: EventListener) => {
594
- const activeListeners = listeners.get(event);
595
- if (!activeListeners) {
596
- return;
597
- }
598
- const index = activeListeners.indexOf(listener);
599
- if (index !== -1) {
600
- activeListeners.splice(index, 1);
601
- }
602
- if (activeListeners.length === 0) {
603
- listeners.delete(event);
604
- } else {
605
- listeners.set(event, activeListeners);
606
- }
607
- if (event === 'level-change') {
608
- levelListeners.length = 0;
609
- levelListeners.push(
610
- ...((listeners.get(event) as LevelChangeEventListener[] | undefined) ??
611
- []),
612
- );
613
- }
614
- };
615
-
616
- const shouldLog = (level: LevelWithSilentOrString): boolean => {
617
- if (!enabled) return false;
618
-
619
- // Priority: context level > options level > 'info' default
620
- const activeLevel = getActiveLogLevel() ?? currentLevel;
621
-
622
- // 'silent' means suppress all logging
623
- if (activeLevel === 'silent') return false;
624
- const currentValue = levelValues[activeLevel];
625
- const expectedValue = levelValues[level];
626
- if (currentValue === undefined || expectedValue === undefined) return false;
627
-
628
- return compareLevels(
629
- expectedValue,
630
- currentValue,
631
- levelComparison,
632
- );
633
- };
634
-
635
- const stringify = (obj: unknown): string => {
636
- if (safe) {
637
- return safeStringify(obj, depthLimit, edgeLimitOpt);
638
- }
639
- return JSON.stringify(obj);
640
- };
641
-
642
- const getTimestamp = (): string | undefined => {
643
- if (timestamp === false) return undefined;
644
- if (typeof timestamp === 'function') return timestamp();
645
- return new Date().toISOString();
646
- };
647
-
648
- const writeOutput = (level: LevelWithSilentOrString, logObject: Record<string, any>) => {
649
- if (writeFn) {
650
- if (typeof writeFn === 'function') {
651
- writeFn(logObject);
652
- } else if (typeof writeFn === 'object' && writeFn[level]) {
653
- writeFn[level](logObject);
654
- } else {
655
- console.log(stringify(logObject));
656
- }
657
- } else if (pretty) {
658
- return; // pretty mode handles its own output
659
- } else {
660
- const json = stringify(logObject);
661
- console.log(crlf ? json + '\r\n' : json);
662
- }
663
- };
664
-
665
- const applySerializers = (obj: Record<string, any>): Record<string, any> => {
666
- if (Object.keys(serializers).length === 0) return obj;
667
- return Object.fromEntries(
668
- Object.entries(obj).map(([key, value]) => [
669
- key,
670
- key in serializers ? serializers[key](value) : value,
671
- ]),
672
- );
673
- };
674
-
675
- const emitTransmit = (
676
- level: LevelWithSilentOrString,
677
- rawArgs: unknown[],
678
- ) => {
679
- if (!transmit) return;
680
- if (transmit.level) {
681
- const transmitValue = levelValues[transmit.level];
682
- const logValue = levelValues[level];
683
- if (transmitValue !== undefined && logValue !== undefined && logValue < transmitValue) {
684
- return;
685
- }
686
- }
687
- // Apply serializers then redaction to messages
688
- const processedMessages = rawArgs.map((m) => {
689
- if (typeof m !== 'object' || m === null) return m;
690
- const serialized = applySerializers(m as Record<string, any>);
691
- return redactor ? redactor(serialized) : serialized;
692
- });
693
- // Apply serializers then redaction to bindings
694
- const processedBindings = bindingsChain.map((b) => {
695
- const serialized = applySerializers({ ...b });
696
- return redactor ? (redactor(serialized) as LogAttrs) : serialized;
697
- });
698
- const logEvent: LogEvent = {
699
- ts: Date.now(),
700
- messages: processedMessages,
701
- bindings: processedBindings,
702
- level: { label: level, value: levelValues[level] },
703
- };
704
- transmit.send(level, logEvent);
705
- };
706
-
707
- const log = (
708
- level: LevelWithSilentOrString,
709
- msg: string,
710
- attrs?: Record<string, any>,
711
- rawArgs?: unknown[],
712
- ) => {
713
- if (!shouldLog(level)) return;
714
-
715
- const ctx = getTraceContext();
716
- const message = msgPrefix ? `${msgPrefix}${msg}` : msg;
717
- const baseBindings = bindingsFormatter
718
- ? (bindingsFormatter({ ...currentBindings }) as LogAttrs)
719
- : { ...currentBindings };
720
- const serializedAttrs = attrs
721
- ? Object.fromEntries(
722
- Object.entries(attrs).map(([key, value]) => [
723
- key,
724
- key in serializers ? serializers[key](value) : value,
725
- ]),
726
- )
727
- : undefined;
728
- const mergeObject = nestedKey
729
- ? ({ [nestedKey]: serializedAttrs ?? {} } as Record<string, unknown>)
730
- : (serializedAttrs ?? {});
731
- const mixinObject =
732
- mixin?.(mergeObject, levelValues[level], logger) ?? {};
733
- const mergedLogObject = mixinMergeStrategy(mergeObject, mixinObject) as Record<
734
- string,
735
- unknown
736
- >;
737
- const formattedLogObject = logFormatter
738
- ? logFormatter(mergedLogObject)
739
- : mergedLogObject;
740
- const formattedLevel = levelFormatter
741
- ? levelFormatter(level, levelValues[level])
742
- : {
743
- level: logger.useLevelLabels ? level : levelValues[level],
744
- };
745
- const ts = getTimestamp();
746
- const logEntry: Record<string, any> = {
747
- ...formattedLevel,
748
- ...(loggerName === undefined ? {} : { name: loggerName }),
749
- service,
750
- ...(message === '' ? {} : { [messageKey]: message }),
751
- ...(base === null || base === undefined ? {} : base),
752
- ...baseBindings,
753
- ...formattedLogObject,
754
- ...ctx, // Auto-inject traceId, spanId, correlationId
755
- ...(ts === undefined ? {} : { timestamp: ts }),
756
- };
757
-
758
- if (serializedAttrs && !nestedKey && errorKey !== 'error' && 'error' in logEntry) {
759
- logEntry[errorKey] = logEntry.error;
760
- delete logEntry.error;
761
- }
762
-
763
- if (pretty && !writeFn) {
764
- // Pretty print for development
765
- const color = ANSI_COLORS[level] ?? ANSI_COLORS.info;
766
- const symbol = LEVEL_SYMBOLS[level] ?? '●';
767
- const time = formatPrettyTimestamp();
768
- const traceInfo = ctx
769
- ? ` ${ANSI_DIM}[${ctx.traceId.slice(0, 8)}/${ctx.spanId.slice(0, 8)}]${ANSI_RESET}`
770
- : '';
771
- const prettyAttrs = {
772
- ...baseBindings,
773
- ...formattedLogObject,
774
- };
775
- const safeAttrs = redactor
776
- ? (redactor(prettyAttrs) as LogAttrs)
777
- : prettyAttrs;
778
- const attrsStr = formatPrettyAttrs(safeAttrs);
779
- console.log(
780
- `${ANSI_DIM}${time}${ANSI_RESET} ${color}${ANSI_BOLD}${symbol} ${level.toUpperCase()}${ANSI_RESET}${traceInfo} ${ANSI_DIM}(${service})${ANSI_RESET} ${message}${attrsStr}`,
781
- );
782
- } else {
783
- // Structured JSON for production (or custom write)
784
- const safeEntry = redactor ? (redactor(logEntry) as LogAttrs) : logEntry;
785
- writeOutput(level, safeEntry);
786
- }
787
-
788
- emitTransmit(level, rawArgs ?? []);
789
- };
790
-
791
- const makeLogMethod = (levelName: string): LogFn => {
792
- return ((...args: unknown[]) => {
793
- if (hooks?.logMethod) {
794
- const method = ((...methodArgs: unknown[]) => {
795
- const { msg, attrs } = parseLogArgs(methodArgs);
796
- log(levelName, msg, attrs, methodArgs);
797
- }) as LogFn;
798
- hooks.logMethod.call(logger, args as Parameters<LogFn>, method, levelValues[levelName]);
799
- return;
800
- }
801
- const { msg, attrs } = parseLogArgs(args);
802
- log(levelName, msg, attrs, args);
803
- }) as LogFn;
804
- };
805
-
806
- const logger: EdgeLogger = {
807
- version: LOGGER_VERSION,
808
-
809
- get name() {
810
- return loggerName;
811
- },
812
-
813
- levels: {
814
- values: levelValues,
815
- labels: Object.fromEntries(
816
- Object.entries(levelValues).map(([label, value]) => [value, label]),
817
- ),
818
- },
819
- useLevelLabels: options?.useLevelLabels ?? false,
820
- customLevels: { ...customLevels },
821
- useOnlyCustomLevels: options?.useOnlyCustomLevels ?? false,
822
-
823
- get level() {
824
- return currentLevel;
825
- },
826
-
827
- set level(level: LevelWithSilentOrString) {
828
- const previousLevel = currentLevel;
829
- currentLevel = level;
830
- notifyLevelChange(level, previousLevel, logger);
831
- },
832
-
833
- get levelVal() {
834
- return levelValues[currentLevel] ?? Infinity;
835
- },
836
-
837
- get msgPrefix() {
838
- return msgPrefix;
839
- },
840
-
841
- onChild: onChildCallback,
842
- get enabled() {
843
- return enabled;
844
- },
845
-
846
- set enabled(value: boolean) {
847
- enabled = value;
848
- },
849
-
850
- info: makeLogMethod('info'),
851
-
852
- error: makeLogMethod('error'),
853
-
854
- warn: makeLogMethod('warn'),
855
-
856
- debug: makeLogMethod('debug'),
857
-
858
- trace: makeLogMethod('trace'),
859
-
860
- fatal: makeLogMethod('fatal'),
861
-
862
- silent: (() => {}) as LogFn,
863
-
864
- child: (bindings: LogAttrs, childOptions?: ChildLoggerOptions) => {
865
- const childLogger = createEdgeLogger(service, {
866
- level: childOptions?.level ?? currentLevel,
867
- pretty,
868
- bindings: { ...currentBindings, ...bindings },
869
- redact: childOptions?.redact ?? options?.redact,
870
- msgPrefix:
871
- childOptions?.msgPrefix === undefined
872
- ? msgPrefix
873
- : `${msgPrefix ?? ''}${childOptions.msgPrefix}`,
874
- useLevelLabels: logger.useLevelLabels,
875
- onChild: onChildCallback,
876
- enabled,
877
- messageKey,
878
- errorKey,
879
- nestedKey,
880
- serializers: { ...serializers, ...childOptions?.serializers },
881
- formatters: {
882
- level: childOptions?.formatters?.level ?? levelFormatter,
883
- bindings: childOptions?.formatters?.bindings ?? bindingsFormatter,
884
- log: childOptions?.formatters?.log ?? logFormatter,
885
- },
886
- mixin,
887
- mixinMergeStrategy,
888
- customLevels,
889
- useOnlyCustomLevels: options?.useOnlyCustomLevels,
890
- levelComparison: options?.levelComparison,
891
- name: loggerName,
892
- base,
893
- timestamp,
894
- safe,
895
- crlf,
896
- depthLimit,
897
- edgeLimit: edgeLimitOpt,
898
- hooks,
899
- write: writeFn,
900
- transmit,
901
- _bindingsChain: [...bindingsChain, bindings],
902
- });
903
- onChildCallback(childLogger);
904
- return childLogger;
905
- },
906
-
907
- isLevelEnabled: (level: LevelWithSilentOrString) => shouldLog(level),
908
-
909
- bindings: () => ({ ...currentBindings }),
910
-
911
- setBindings: (bindings: LogAttrs) => {
912
- for (const [key, value] of Object.entries(bindings)) {
913
- if (!(key in currentBindings)) {
914
- currentBindings[key] = value;
915
- }
916
- }
917
- },
918
-
919
- flush: (cb?: (err?: Error) => void) => {
920
- cb?.();
921
- },
922
-
923
- emit: (event: string | symbol, ...args: any[]) => {
924
- const activeListeners = listeners.get(event);
925
- if (!activeListeners || activeListeners.length === 0) {
926
- return false;
927
- }
928
- const snapshot = [...activeListeners];
929
- for (const listener of snapshot) {
930
- listener(...args);
931
- }
932
- return true;
933
- },
934
-
935
- eventNames: () => [...listeners.keys()],
936
-
937
- getMaxListeners: () => maxListeners,
938
-
939
- listenerCount: (event: string | symbol) => listeners.get(event)?.length ?? 0,
940
-
941
- listeners: (event: string | symbol) => [...(listeners.get(event) ?? [])],
942
-
943
- on: (event: string | symbol, listener: EventListener) => {
944
- addListener(event, listener);
945
- return logger;
946
- },
947
-
948
- addListener: (event: string | symbol, listener: EventListener) => {
949
- addListener(event, listener);
950
- return logger;
951
- },
952
-
953
- once: (event: string | symbol, listener: EventListener) => {
954
- const wrapped: EventListener = (...args: any[]) => {
955
- removeEventListener(event, wrapped);
956
- listener(...args);
957
- };
958
- addListener(event, wrapped);
959
- return logger;
960
- },
961
-
962
- prependListener: (
963
- event: string | symbol,
964
- listener: EventListener,
965
- ) => {
966
- addListener(event, listener, true);
967
- return logger;
968
- },
969
-
970
- prependOnceListener: (
971
- event: string | symbol,
972
- listener: EventListener,
973
- ) => {
974
- const wrapped: EventListener = (...args: any[]) => {
975
- removeEventListener(event, wrapped);
976
- listener(...args);
977
- };
978
- addListener(event, wrapped, true);
979
- return logger;
980
- },
981
-
982
- rawListeners: (event: string | symbol) => [...(listeners.get(event) ?? [])],
983
-
984
- off: (event: string | symbol, listener: EventListener) => {
985
- removeEventListener(event, listener);
986
- return logger;
987
- },
988
-
989
- removeAllListeners: (event?: string | symbol) => {
990
- if (event === undefined) {
991
- listeners.clear();
992
- levelListeners.length = 0;
993
- return logger;
994
- }
995
- listeners.delete(event);
996
- if (event === 'level-change') {
997
- levelListeners.length = 0;
998
- }
999
- return logger;
1000
- },
1001
-
1002
- removeListener: (
1003
- event: string | symbol,
1004
- listener: EventListener,
1005
- ) => {
1006
- removeEventListener(event, listener);
1007
- return logger;
1008
- },
1009
-
1010
- setMaxListeners: (n: number) => {
1011
- maxListeners = n;
1012
- return logger;
1013
- },
1014
- };
1015
-
1016
- for (const levelName of Object.keys(customLevels)) {
1017
- logger[levelName] = makeLogMethod(levelName);
1018
- }
1019
-
1020
- return logger;
1021
- }
1022
-
1023
- /**
1024
- * Helper to get trace context (useful for BYOL - Bring Your Own Logger)
1025
- *
1026
- * @example
1027
- * ```typescript
1028
- * import bunyan from 'bunyan'
1029
- * import { getEdgeTraceContext } from 'autotel-edge/api/logger'
1030
- *
1031
- * const bunyanLogger = bunyan.createLogger({ name: 'myapp' })
1032
- * const ctx = getEdgeTraceContext()
1033
- * bunyanLogger.info({ ...ctx, email: 'test@example.com' }, 'Creating user')
1034
- * ```
1035
- */
1036
- export function getEdgeTraceContext() {
1037
- return getTraceContext();
1038
- }