autotel 2.22.0 → 2.23.0
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/README.md +112 -6
- package/dist/auto.cjs +3 -3
- package/dist/auto.js +2 -2
- package/dist/{chunk-EWH2542B.js → chunk-3AMR5XLZ.js} +3 -3
- package/dist/{chunk-EWH2542B.js.map → chunk-3AMR5XLZ.js.map} +1 -1
- package/dist/chunk-3QXBFGKP.js +344 -0
- package/dist/chunk-3QXBFGKP.js.map +1 -0
- package/dist/{chunk-VQFF2WMP.cjs → chunk-3ZFDJJWZ.cjs} +37 -29
- package/dist/chunk-3ZFDJJWZ.cjs.map +1 -0
- package/dist/{chunk-CQC6RVLR.cjs → chunk-4RZ4JUBY.cjs} +5 -5
- package/dist/{chunk-CQC6RVLR.cjs.map → chunk-4RZ4JUBY.cjs.map} +1 -1
- package/dist/{chunk-PAVYKPCQ.js → chunk-5XUEHX7J.js} +3 -3
- package/dist/{chunk-PAVYKPCQ.js.map → chunk-5XUEHX7J.js.map} +1 -1
- package/dist/chunk-6S5RUKU3.cjs +347 -0
- package/dist/chunk-6S5RUKU3.cjs.map +1 -0
- package/dist/{chunk-BS757SL2.js → chunk-724XLWR3.js} +9 -4
- package/dist/chunk-724XLWR3.js.map +1 -0
- package/dist/chunk-7EQ4G4SI.cjs +146 -0
- package/dist/chunk-7EQ4G4SI.cjs.map +1 -0
- package/dist/{chunk-CQP5SQT4.cjs → chunk-AXFWWJF3.cjs} +7 -7
- package/dist/{chunk-CQP5SQT4.cjs.map → chunk-AXFWWJF3.cjs.map} +1 -1
- package/dist/{chunk-7NH625MS.cjs → chunk-BSZP4URK.cjs} +5 -5
- package/dist/{chunk-7NH625MS.cjs.map → chunk-BSZP4URK.cjs.map} +1 -1
- package/dist/{chunk-GZFH6P5U.js → chunk-GY4CRZSV.js} +14 -6
- package/dist/chunk-GY4CRZSV.js.map +1 -0
- package/dist/{chunk-QKUGUDXJ.cjs → chunk-HSEIUH7F.cjs} +10 -5
- package/dist/chunk-HSEIUH7F.cjs.map +1 -0
- package/dist/{chunk-DTW3WB7Z.js → chunk-IPKXURBW.js} +3 -3
- package/dist/{chunk-DTW3WB7Z.js.map → chunk-IPKXURBW.js.map} +1 -1
- package/dist/chunk-J7VGRIAJ.js +64 -0
- package/dist/chunk-J7VGRIAJ.js.map +1 -0
- package/dist/chunk-KFOHQK7X.js +144 -0
- package/dist/chunk-KFOHQK7X.js.map +1 -0
- package/dist/{chunk-4UYR46UP.cjs → chunk-MSUHW2I4.cjs} +13 -13
- package/dist/{chunk-4UYR46UP.cjs.map → chunk-MSUHW2I4.cjs.map} +1 -1
- package/dist/chunk-T4B5LB6E.cjs +66 -0
- package/dist/chunk-T4B5LB6E.cjs.map +1 -0
- package/dist/{chunk-QHT4MUED.js → chunk-WCIIFRGL.js} +3 -3
- package/dist/{chunk-QHT4MUED.js.map → chunk-WCIIFRGL.js.map} +1 -1
- package/dist/decorators.cjs +3 -3
- package/dist/decorators.js +3 -3
- package/dist/drain-pipeline.cjs +13 -0
- package/dist/drain-pipeline.cjs.map +1 -0
- package/dist/drain-pipeline.d.cts +37 -0
- package/dist/drain-pipeline.d.ts +37 -0
- package/dist/drain-pipeline.js +4 -0
- package/dist/drain-pipeline.js.map +1 -0
- package/dist/event.cjs +6 -6
- package/dist/event.js +3 -3
- package/dist/functional.cjs +10 -10
- package/dist/functional.js +3 -3
- package/dist/index.cjs +256 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +72 -3
- package/dist/index.d.ts +72 -3
- package/dist/index.js +210 -11
- package/dist/index.js.map +1 -1
- package/dist/{init-BMiXSJNM.d.cts → init-BC5aN8bh.d.cts} +18 -0
- package/dist/{init-ByRbNTRo.d.ts → init-_FG4IbhF.d.ts} +18 -0
- package/dist/instrumentation.cjs +9 -9
- package/dist/instrumentation.js +2 -2
- package/dist/messaging.cjs +7 -7
- package/dist/messaging.js +4 -4
- package/dist/parse-error.cjs +13 -0
- package/dist/parse-error.cjs.map +1 -0
- package/dist/parse-error.d.cts +13 -0
- package/dist/parse-error.d.ts +13 -0
- package/dist/parse-error.js +4 -0
- package/dist/parse-error.js.map +1 -0
- package/dist/processors.cjs +2 -2
- package/dist/processors.d.cts +40 -4
- package/dist/processors.d.ts +40 -4
- package/dist/processors.js +1 -1
- package/dist/semantic-helpers.cjs +8 -8
- package/dist/semantic-helpers.js +4 -4
- package/dist/webhook.cjs +4 -4
- package/dist/webhook.js +3 -3
- package/dist/workflow-distributed.cjs +5 -5
- package/dist/workflow-distributed.js +3 -3
- package/dist/workflow.cjs +8 -8
- package/dist/workflow.js +4 -4
- package/dist/yaml-config.d.cts +2 -1
- package/dist/yaml-config.d.ts +2 -1
- package/package.json +11 -1
- package/src/drain-pipeline.test.ts +68 -0
- package/src/drain-pipeline.ts +199 -0
- package/src/flatten-attributes.test.ts +76 -0
- package/src/flatten-attributes.ts +80 -0
- package/src/functional.test.ts +63 -0
- package/src/functional.ts +11 -3
- package/src/index.ts +33 -0
- package/src/init.ts +22 -0
- package/src/parse-error.test.ts +73 -0
- package/src/parse-error.ts +112 -0
- package/src/pretty-log-formatter.test.ts +123 -0
- package/src/pretty-log-formatter.ts +210 -0
- package/src/processors/canonical-log-line-processor.test.ts +81 -25
- package/src/processors/canonical-log-line-processor.ts +130 -42
- package/src/request-logger.test.ts +124 -0
- package/src/request-logger.ts +140 -0
- package/src/structured-error.test.ts +76 -0
- package/src/structured-error.ts +86 -0
- package/dist/chunk-2RQDNGV3.js +0 -126
- package/dist/chunk-2RQDNGV3.js.map +0 -1
- package/dist/chunk-BS757SL2.js.map +0 -1
- package/dist/chunk-GZFH6P5U.js.map +0 -1
- package/dist/chunk-ONK2Y22L.cjs +0 -128
- package/dist/chunk-ONK2Y22L.cjs.map +0 -1
- package/dist/chunk-QKUGUDXJ.cjs.map +0 -1
- package/dist/chunk-VQFF2WMP.cjs.map +0 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { trace as otelTrace } from '@opentelemetry/api';
|
|
2
|
+
import type { TraceContext } from './trace-context';
|
|
3
|
+
import { createTraceContext } from './trace-context';
|
|
4
|
+
import { recordStructuredError } from './structured-error';
|
|
5
|
+
import { flattenToAttributes } from './flatten-attributes';
|
|
6
|
+
|
|
7
|
+
export interface RequestLogger {
|
|
8
|
+
set(fields: Record<string, unknown>): void;
|
|
9
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
10
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
11
|
+
error(error: Error | string, fields?: Record<string, unknown>): void;
|
|
12
|
+
getContext(): Record<string, unknown>;
|
|
13
|
+
emitNow(overrides?: Record<string, unknown>): RequestLogSnapshot;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RequestLogSnapshot {
|
|
17
|
+
timestamp: string;
|
|
18
|
+
traceId: string;
|
|
19
|
+
spanId: string;
|
|
20
|
+
correlationId: string;
|
|
21
|
+
context: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RequestLoggerOptions {
|
|
25
|
+
/** Callback invoked by emitNow() for manual fan-out. */
|
|
26
|
+
onEmit?: (snapshot: RequestLogSnapshot) => void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveContext(ctx?: TraceContext): TraceContext {
|
|
30
|
+
if (ctx) return ctx;
|
|
31
|
+
|
|
32
|
+
const span = otelTrace.getActiveSpan();
|
|
33
|
+
if (!span) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'[autotel] getRequestLogger() requires an active span. Wrap your handler with trace().',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return createTraceContext(span);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getRequestLogger(
|
|
42
|
+
ctx?: TraceContext,
|
|
43
|
+
options?: RequestLoggerOptions,
|
|
44
|
+
): RequestLogger {
|
|
45
|
+
const activeContext = resolveContext(ctx);
|
|
46
|
+
let contextState: Record<string, unknown> = {};
|
|
47
|
+
|
|
48
|
+
const addLogEvent = (
|
|
49
|
+
level: 'info' | 'warn' | 'error',
|
|
50
|
+
message: string,
|
|
51
|
+
fields?: Record<string, unknown>,
|
|
52
|
+
) => {
|
|
53
|
+
const attrs = fields ? flattenToAttributes(fields) : undefined;
|
|
54
|
+
activeContext.addEvent(`log.${level}`, {
|
|
55
|
+
message,
|
|
56
|
+
...attrs,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
set(fields: Record<string, unknown>) {
|
|
62
|
+
contextState = {
|
|
63
|
+
...contextState,
|
|
64
|
+
...fields,
|
|
65
|
+
};
|
|
66
|
+
activeContext.setAttributes(flattenToAttributes(fields));
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
info(message: string, fields?: Record<string, unknown>) {
|
|
70
|
+
addLogEvent('info', message, fields);
|
|
71
|
+
if (fields) {
|
|
72
|
+
contextState = {
|
|
73
|
+
...contextState,
|
|
74
|
+
...fields,
|
|
75
|
+
};
|
|
76
|
+
activeContext.setAttributes(flattenToAttributes(fields));
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
warn(message: string, fields?: Record<string, unknown>) {
|
|
81
|
+
addLogEvent('warn', message, fields);
|
|
82
|
+
activeContext.setAttribute('autotel.log.level', 'warn');
|
|
83
|
+
if (fields) {
|
|
84
|
+
contextState = {
|
|
85
|
+
...contextState,
|
|
86
|
+
...fields,
|
|
87
|
+
};
|
|
88
|
+
activeContext.setAttributes(flattenToAttributes(fields));
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
error(error: Error | string, fields?: Record<string, unknown>) {
|
|
93
|
+
const err = typeof error === 'string' ? new Error(error) : error;
|
|
94
|
+
recordStructuredError(activeContext, err);
|
|
95
|
+
addLogEvent('error', err.message, fields);
|
|
96
|
+
|
|
97
|
+
if (fields) {
|
|
98
|
+
contextState = {
|
|
99
|
+
...contextState,
|
|
100
|
+
...fields,
|
|
101
|
+
};
|
|
102
|
+
activeContext.setAttributes(flattenToAttributes(fields));
|
|
103
|
+
}
|
|
104
|
+
activeContext.setAttribute('autotel.log.level', 'error');
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
getContext() {
|
|
108
|
+
return { ...contextState };
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
emitNow(overrides?: Record<string, unknown>): RequestLogSnapshot {
|
|
112
|
+
const mergedContext = {
|
|
113
|
+
...contextState,
|
|
114
|
+
...(overrides ?? {}),
|
|
115
|
+
};
|
|
116
|
+
const flattened = flattenToAttributes(mergedContext);
|
|
117
|
+
activeContext.setAttributes(flattened);
|
|
118
|
+
|
|
119
|
+
const snapshot: RequestLogSnapshot = {
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
traceId: activeContext.traceId,
|
|
122
|
+
spanId: activeContext.spanId,
|
|
123
|
+
correlationId: activeContext.correlationId,
|
|
124
|
+
context: mergedContext,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
activeContext.addEvent('log.emit.manual', {
|
|
128
|
+
...flattened,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (options?.onEmit) {
|
|
132
|
+
Promise.resolve(options.onEmit(snapshot)).catch((error) => {
|
|
133
|
+
console.warn('[autotel] request logger onEmit failed:', error);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return snapshot;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createStructuredError,
|
|
4
|
+
getStructuredErrorAttributes,
|
|
5
|
+
recordStructuredError,
|
|
6
|
+
} from './structured-error';
|
|
7
|
+
import type { TraceContext } from './trace-context';
|
|
8
|
+
|
|
9
|
+
describe('structured-error helpers', () => {
|
|
10
|
+
it('creates an error with structured diagnostic fields', () => {
|
|
11
|
+
const err = createStructuredError({
|
|
12
|
+
message: 'Payment failed',
|
|
13
|
+
why: 'Card declined by issuer',
|
|
14
|
+
fix: 'Use a different card',
|
|
15
|
+
link: 'https://docs.example.com/errors/card-declined',
|
|
16
|
+
code: 'PAYMENT_DECLINED',
|
|
17
|
+
status: 402,
|
|
18
|
+
details: { retryable: false, provider: 'stripe' },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(err.message).toBe('Payment failed');
|
|
22
|
+
expect(err.why).toBe('Card declined by issuer');
|
|
23
|
+
expect(err.fix).toBe('Use a different card');
|
|
24
|
+
expect(err.link).toBe('https://docs.example.com/errors/card-declined');
|
|
25
|
+
expect(err.code).toBe('PAYMENT_DECLINED');
|
|
26
|
+
expect(err.status).toBe(402);
|
|
27
|
+
expect(err.details).toEqual({ retryable: false, provider: 'stripe' });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('extracts canonical attributes for span logging', () => {
|
|
31
|
+
const err = createStructuredError({
|
|
32
|
+
message: 'Export failed',
|
|
33
|
+
why: 'Template not found',
|
|
34
|
+
fix: 'Upload template and retry',
|
|
35
|
+
details: { export: { format: 'pdf' } },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const attrs = getStructuredErrorAttributes(err);
|
|
39
|
+
expect(attrs).toMatchObject({
|
|
40
|
+
'error.type': 'StructuredError',
|
|
41
|
+
'error.message': 'Export failed',
|
|
42
|
+
'error.why': 'Template not found',
|
|
43
|
+
'error.fix': 'Upload template and retry',
|
|
44
|
+
'error.details.export.format': 'pdf',
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('records structured error onto trace context', () => {
|
|
49
|
+
const ctx = {
|
|
50
|
+
recordException: vi.fn(),
|
|
51
|
+
setStatus: vi.fn(),
|
|
52
|
+
setAttributes: vi.fn(),
|
|
53
|
+
} as unknown as TraceContext;
|
|
54
|
+
|
|
55
|
+
const err = createStructuredError({
|
|
56
|
+
message: 'Checkout failed',
|
|
57
|
+
why: 'Inventory mismatch',
|
|
58
|
+
fix: 'Re-sync inventory and retry',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
recordStructuredError(ctx, err);
|
|
62
|
+
|
|
63
|
+
expect(ctx.recordException).toHaveBeenCalledWith(err);
|
|
64
|
+
expect(ctx.setStatus).toHaveBeenCalledWith({
|
|
65
|
+
code: 2,
|
|
66
|
+
message: 'Checkout failed',
|
|
67
|
+
});
|
|
68
|
+
expect(ctx.setAttributes).toHaveBeenCalledWith(
|
|
69
|
+
expect.objectContaining({
|
|
70
|
+
'error.message': 'Checkout failed',
|
|
71
|
+
'error.why': 'Inventory mismatch',
|
|
72
|
+
'error.fix': 'Re-sync inventory and retry',
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
import type { AttributeValue, TraceContext } from './trace-context';
|
|
3
|
+
import { flattenToAttributes } from './flatten-attributes';
|
|
4
|
+
|
|
5
|
+
export interface StructuredErrorInput {
|
|
6
|
+
message: string;
|
|
7
|
+
why?: string;
|
|
8
|
+
fix?: string;
|
|
9
|
+
link?: string;
|
|
10
|
+
code?: string | number;
|
|
11
|
+
status?: number;
|
|
12
|
+
cause?: unknown;
|
|
13
|
+
details?: Record<string, unknown>;
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StructuredError extends Error {
|
|
18
|
+
why?: string;
|
|
19
|
+
fix?: string;
|
|
20
|
+
link?: string;
|
|
21
|
+
code?: string | number;
|
|
22
|
+
status?: number;
|
|
23
|
+
details?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createStructuredError(
|
|
27
|
+
input: StructuredErrorInput,
|
|
28
|
+
): StructuredError {
|
|
29
|
+
const error = new Error(input.message, {
|
|
30
|
+
cause: input.cause,
|
|
31
|
+
}) as StructuredError;
|
|
32
|
+
|
|
33
|
+
error.name = input.name ?? 'StructuredError';
|
|
34
|
+
if (input.why !== undefined) error.why = input.why;
|
|
35
|
+
if (input.fix !== undefined) error.fix = input.fix;
|
|
36
|
+
if (input.link !== undefined) error.link = input.link;
|
|
37
|
+
if (input.code !== undefined) error.code = input.code;
|
|
38
|
+
if (input.status !== undefined) error.status = input.status;
|
|
39
|
+
if (input.details !== undefined) error.details = input.details;
|
|
40
|
+
|
|
41
|
+
return error;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getStructuredErrorAttributes(
|
|
45
|
+
error: Error,
|
|
46
|
+
): Record<string, AttributeValue> {
|
|
47
|
+
const structured = error as StructuredError;
|
|
48
|
+
const attributes: Record<string, AttributeValue> = {
|
|
49
|
+
'error.type': error.name || 'Error',
|
|
50
|
+
'error.message': error.message,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (error.stack) attributes['error.stack'] = error.stack;
|
|
54
|
+
if (structured.why) attributes['error.why'] = structured.why;
|
|
55
|
+
if (structured.fix) attributes['error.fix'] = structured.fix;
|
|
56
|
+
if (structured.link) attributes['error.link'] = structured.link;
|
|
57
|
+
if (structured.code !== undefined) {
|
|
58
|
+
attributes['error.code'] =
|
|
59
|
+
typeof structured.code === 'string'
|
|
60
|
+
? structured.code
|
|
61
|
+
: String(structured.code);
|
|
62
|
+
}
|
|
63
|
+
if (structured.status !== undefined) {
|
|
64
|
+
attributes['error.status'] = structured.status;
|
|
65
|
+
}
|
|
66
|
+
if (structured.details) {
|
|
67
|
+
Object.assign(
|
|
68
|
+
attributes,
|
|
69
|
+
flattenToAttributes(structured.details, 'error.details'),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return attributes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function recordStructuredError(
|
|
77
|
+
ctx: Pick<TraceContext, 'recordException' | 'setAttributes' | 'setStatus'>,
|
|
78
|
+
error: Error,
|
|
79
|
+
): void {
|
|
80
|
+
ctx.recordException(error);
|
|
81
|
+
ctx.setStatus({
|
|
82
|
+
code: SpanStatusCode.ERROR,
|
|
83
|
+
message: error.message,
|
|
84
|
+
});
|
|
85
|
+
ctx.setAttributes(getStructuredErrorAttributes(error));
|
|
86
|
+
}
|
package/dist/chunk-2RQDNGV3.js
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { logs, SeverityNumber } from '@opentelemetry/api-logs';
|
|
2
|
-
|
|
3
|
-
// src/processors/canonical-log-line-processor.ts
|
|
4
|
-
var CanonicalLogLineProcessor = class {
|
|
5
|
-
logger;
|
|
6
|
-
rootSpansOnly;
|
|
7
|
-
minLevel;
|
|
8
|
-
messageFormat;
|
|
9
|
-
includeResourceAttributes;
|
|
10
|
-
attributeRedactor;
|
|
11
|
-
getOTelLogger = null;
|
|
12
|
-
constructor(options = {}) {
|
|
13
|
-
this.logger = options.logger;
|
|
14
|
-
this.rootSpansOnly = options.rootSpansOnly ?? false;
|
|
15
|
-
this.minLevel = options.minLevel ?? "info";
|
|
16
|
-
this.messageFormat = options.messageFormat ?? ((span) => `[${span.name}] Request completed`);
|
|
17
|
-
this.includeResourceAttributes = options.includeResourceAttributes ?? true;
|
|
18
|
-
this.attributeRedactor = options.attributeRedactor;
|
|
19
|
-
if (!this.logger) {
|
|
20
|
-
this.getOTelLogger = () => logs.getLogger("autotel.canonical-log-line");
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
onStart() {
|
|
24
|
-
}
|
|
25
|
-
onEnd(span) {
|
|
26
|
-
if (this.rootSpansOnly && span.parentSpanContext?.spanId && !span.parentSpanContext.isRemote) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const level = this.getLogLevel(span);
|
|
30
|
-
if (!this.shouldLog(level)) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const canonicalLogLine = this.buildCanonicalLogLine(span);
|
|
34
|
-
if (this.logger) {
|
|
35
|
-
this.emitViaLogger(level, span, canonicalLogLine);
|
|
36
|
-
} else if (this.getOTelLogger) {
|
|
37
|
-
const otelLogger = this.getOTelLogger();
|
|
38
|
-
this.emitViaOTel(level, span, canonicalLogLine, otelLogger);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
buildCanonicalLogLine(span) {
|
|
42
|
-
const durationMs = span.duration[0] * 1e3 + span.duration[1] / 1e6;
|
|
43
|
-
const timestamp = new Date(
|
|
44
|
-
span.startTime[0] * 1e3 + span.startTime[1] / 1e6
|
|
45
|
-
).toISOString();
|
|
46
|
-
const canonicalLogLine = {};
|
|
47
|
-
const attributes = this.redactAttributes(span.attributes);
|
|
48
|
-
Object.assign(canonicalLogLine, attributes);
|
|
49
|
-
if (this.includeResourceAttributes) {
|
|
50
|
-
const resourceAttrs = this.redactAttributes(
|
|
51
|
-
span.resource.attributes
|
|
52
|
-
);
|
|
53
|
-
Object.assign(canonicalLogLine, resourceAttrs);
|
|
54
|
-
}
|
|
55
|
-
canonicalLogLine.operation = span.name;
|
|
56
|
-
canonicalLogLine.traceId = span.spanContext().traceId;
|
|
57
|
-
canonicalLogLine.spanId = span.spanContext().spanId;
|
|
58
|
-
canonicalLogLine.correlationId = span.spanContext().traceId.slice(0, 16);
|
|
59
|
-
canonicalLogLine.duration_ms = Math.round(durationMs * 100) / 100;
|
|
60
|
-
canonicalLogLine.status_code = span.status.code;
|
|
61
|
-
canonicalLogLine.status_message = span.status.message || void 0;
|
|
62
|
-
canonicalLogLine.timestamp = timestamp;
|
|
63
|
-
return canonicalLogLine;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Apply attribute redaction if a redactor is configured
|
|
67
|
-
*/
|
|
68
|
-
redactAttributes(attributes) {
|
|
69
|
-
if (!this.attributeRedactor) {
|
|
70
|
-
return { ...attributes };
|
|
71
|
-
}
|
|
72
|
-
const redacted = {};
|
|
73
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
74
|
-
if (value !== void 0) {
|
|
75
|
-
redacted[key] = this.attributeRedactor(key, value);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return redacted;
|
|
79
|
-
}
|
|
80
|
-
emitViaLogger(level, span, canonicalLogLine) {
|
|
81
|
-
const message = this.messageFormat(span);
|
|
82
|
-
this.logger[level](canonicalLogLine, message);
|
|
83
|
-
}
|
|
84
|
-
emitViaOTel(level, span, canonicalLogLine, otelLogger) {
|
|
85
|
-
const message = this.messageFormat(span);
|
|
86
|
-
const otelAttributes = {};
|
|
87
|
-
for (const [key, value] of Object.entries(canonicalLogLine)) {
|
|
88
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
89
|
-
otelAttributes[key] = value;
|
|
90
|
-
} else if (value !== null && value !== void 0) {
|
|
91
|
-
otelAttributes[key] = String(value);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
otelLogger.emit({
|
|
95
|
-
severityNumber: this.getSeverityNumber(level),
|
|
96
|
-
severityText: level.toUpperCase(),
|
|
97
|
-
body: message,
|
|
98
|
-
attributes: otelAttributes
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
getLogLevel(span) {
|
|
102
|
-
if (span.status.code === 2) return "error";
|
|
103
|
-
return "info";
|
|
104
|
-
}
|
|
105
|
-
shouldLog(level) {
|
|
106
|
-
const levels = ["debug", "info", "warn", "error"];
|
|
107
|
-
return levels.indexOf(level) >= levels.indexOf(this.minLevel);
|
|
108
|
-
}
|
|
109
|
-
getSeverityNumber(level) {
|
|
110
|
-
const mapping = {
|
|
111
|
-
debug: SeverityNumber.DEBUG,
|
|
112
|
-
info: SeverityNumber.INFO,
|
|
113
|
-
warn: SeverityNumber.WARN,
|
|
114
|
-
error: SeverityNumber.ERROR
|
|
115
|
-
};
|
|
116
|
-
return mapping[level] ?? SeverityNumber.INFO;
|
|
117
|
-
}
|
|
118
|
-
async forceFlush() {
|
|
119
|
-
}
|
|
120
|
-
async shutdown() {
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export { CanonicalLogLineProcessor };
|
|
125
|
-
//# sourceMappingURL=chunk-2RQDNGV3.js.map
|
|
126
|
-
//# sourceMappingURL=chunk-2RQDNGV3.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/processors/canonical-log-line-processor.ts"],"names":[],"mappings":"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","file":"chunk-2RQDNGV3.js","sourcesContent":["/**\n * Canonical Log Line Processor\n *\n * Automatically emits spans as canonical log lines (wide events) when they end.\n * Implements canonical log line\" pattern: one comprehensive\n * event per request with all context.\n *\n * When a span ends, this processor creates a log record with ALL span attributes,\n * making the span itself the canonical log line that can be queried like structured data.\n *\n * @example\n * ```typescript\n * import { init } from 'autotel';\n *\n * init({\n * service: 'my-app',\n * canonicalLogLines: {\n * enabled: true,\n * rootSpansOnly: true, // One canonical log line per request\n * },\n * });\n * ```\n */\n\nimport type {\n SpanProcessor,\n ReadableSpan,\n} from '@opentelemetry/sdk-trace-base';\nimport type { Attributes, AttributeValue } from '@opentelemetry/api';\nimport { logs, SeverityNumber } from '@opentelemetry/api-logs';\nimport type { Logger } from '../logger';\n\n/**\n * Function to redact sensitive attribute values\n */\nexport type AttributeRedactorFn = (\n key: string,\n value: AttributeValue,\n) => AttributeValue;\n\nexport interface CanonicalLogLineOptions {\n /** Logger to use for emitting canonical log lines (defaults to OTel Logs API) */\n logger?: Logger;\n /** Only emit canonical log lines for root spans (default: false) */\n rootSpansOnly?: boolean;\n /** Minimum log level for canonical log lines (default: 'info') */\n minLevel?: 'debug' | 'info' | 'warn' | 'error';\n /** Custom message format (default: uses span name) */\n messageFormat?: (span: ReadableSpan) => string;\n /** Whether to include resource attributes (default: true) */\n includeResourceAttributes?: boolean;\n /**\n * Attribute redactor function to apply before logging.\n * This ensures sensitive data is redacted in canonical log lines,\n * matching the behavior of attributeRedactor in init().\n */\n attributeRedactor?: AttributeRedactorFn;\n}\n\n/**\n * Span processor that automatically emits spans as canonical log lines\n *\n * When a span ends, this processor creates a log record with ALL span attributes.\n * This implements the \"canonical log line\" pattern: one comprehensive event\n * per request with all context, queryable as structured data.\n *\n * **Key Benefits:**\n * - One log line per request with all context (wide event)\n * - High-cardinality, high-dimensionality data for powerful queries\n * - Automatic - no manual logging needed\n * - Works with any logger or OTel Logs API\n *\n * @example Basic usage\n * ```typescript\n * import { init } from 'autotel';\n *\n * init({\n * service: 'checkout-api',\n * canonicalLogLines: {\n * enabled: true,\n * rootSpansOnly: true, // One canonical log line per request\n * },\n * });\n * ```\n *\n * @example With custom logger\n * ```typescript\n * import pino from 'pino';\n * import { init } from 'autotel';\n *\n * const logger = pino();\n * init({\n * service: 'my-app',\n * logger,\n * canonicalLogLines: {\n * enabled: true,\n * logger, // Use Pino for canonical log lines\n * rootSpansOnly: true,\n * },\n * });\n * ```\n *\n * @example Custom message format\n * ```typescript\n * init({\n * service: 'my-app',\n * canonicalLogLines: {\n * enabled: true,\n * messageFormat: (span) => {\n * const status = span.status.code === 2 ? 'ERROR' : 'SUCCESS';\n * return `${span.name} [${status}]`;\n * },\n * },\n * });\n * ```\n */\nexport class CanonicalLogLineProcessor implements SpanProcessor {\n private logger?: Logger;\n private rootSpansOnly: boolean;\n private minLevel: 'debug' | 'info' | 'warn' | 'error';\n private messageFormat: (span: ReadableSpan) => string;\n private includeResourceAttributes: boolean;\n private attributeRedactor?: AttributeRedactorFn;\n private getOTelLogger: (() => ReturnType<typeof logs.getLogger>) | null =\n null;\n\n constructor(options: CanonicalLogLineOptions = {}) {\n this.logger = options.logger;\n this.rootSpansOnly = options.rootSpansOnly ?? false;\n this.minLevel = options.minLevel ?? 'info';\n this.messageFormat =\n options.messageFormat ?? ((span) => `[${span.name}] Request completed`);\n this.includeResourceAttributes = options.includeResourceAttributes ?? true;\n this.attributeRedactor = options.attributeRedactor;\n\n // Lazy-load OTel logger if no custom logger provided\n // We can't initialize it here because logs API might not be ready\n if (!this.logger) {\n this.getOTelLogger = () => logs.getLogger('autotel.canonical-log-line');\n }\n }\n\n onStart(): void {\n // No-op - we only care about span end\n }\n\n onEnd(span: ReadableSpan): void {\n // Skip if rootSpansOnly and this span has a LOCAL parent (same service)\n // We still emit for spans with REMOTE parents (from distributed tracing)\n // because those are the entry points (\"roots\") for THIS service.\n // Check if parent is remote (from another service via traceparent/b3 headers)\n // If isRemote is true, this span is a service entry point and should emit\n // If isRemote is false/undefined, this is a local child span and should be skipped\n if (\n this.rootSpansOnly &&\n span.parentSpanContext?.spanId &&\n !span.parentSpanContext.isRemote\n ) {\n return;\n }\n\n // Determine log level from span status\n const level = this.getLogLevel(span);\n if (!this.shouldLog(level)) {\n return;\n }\n\n // Build canonical log line with ALL span attributes\n const canonicalLogLine = this.buildCanonicalLogLine(span);\n\n // Emit via logger or OTel Logs API\n if (this.logger) {\n this.emitViaLogger(level, span, canonicalLogLine);\n } else if (this.getOTelLogger) {\n const otelLogger = this.getOTelLogger();\n this.emitViaOTel(level, span, canonicalLogLine, otelLogger);\n }\n }\n\n private buildCanonicalLogLine(span: ReadableSpan): Record<string, unknown> {\n // Convert duration from [seconds, nanoseconds] to milliseconds\n // duration[0] is seconds, duration[1] is nanoseconds (fractional part)\n const durationMs = span.duration[0] * 1000 + span.duration[1] / 1_000_000;\n\n // Convert start time from [seconds, nanoseconds] to ISO string\n // startTime[0] is seconds, startTime[1] is nanoseconds (fractional part)\n const timestamp = new Date(\n span.startTime[0] * 1000 + span.startTime[1] / 1_000_000,\n ).toISOString();\n\n // Start with span attributes (potentially redacted)\n // We add these FIRST so core metadata fields below can't be overwritten\n const canonicalLogLine: Record<string, unknown> = {};\n\n // Apply redaction to span attributes if redactor is configured\n const attributes = this.redactAttributes(span.attributes);\n Object.assign(canonicalLogLine, attributes);\n\n // Include resource attributes (service-level context), also redacted\n if (this.includeResourceAttributes) {\n const resourceAttrs = this.redactAttributes(\n span.resource.attributes as Attributes,\n );\n Object.assign(canonicalLogLine, resourceAttrs);\n }\n\n // Set core metadata fields LAST to prevent span attributes from overwriting them\n // (e.g., if a span has an attribute named \"traceId\" or \"timestamp\")\n canonicalLogLine.operation = span.name;\n canonicalLogLine.traceId = span.spanContext().traceId;\n canonicalLogLine.spanId = span.spanContext().spanId;\n canonicalLogLine.correlationId = span.spanContext().traceId.slice(0, 16);\n canonicalLogLine.duration_ms = Math.round(durationMs * 100) / 100;\n canonicalLogLine.status_code = span.status.code;\n canonicalLogLine.status_message = span.status.message || undefined;\n canonicalLogLine.timestamp = timestamp;\n\n return canonicalLogLine;\n }\n\n /**\n * Apply attribute redaction if a redactor is configured\n */\n private redactAttributes(attributes: Attributes): Record<string, unknown> {\n if (!this.attributeRedactor) {\n // No redaction configured, return as-is\n return { ...attributes };\n }\n\n const redacted: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(attributes)) {\n if (value !== undefined) {\n redacted[key] = this.attributeRedactor(key, value);\n }\n }\n return redacted;\n }\n\n private emitViaLogger(\n level: 'debug' | 'info' | 'warn' | 'error',\n span: ReadableSpan,\n canonicalLogLine: Record<string, unknown>,\n ): void {\n const message = this.messageFormat(span);\n // Pino-compatible signature: (extra, message)\n this.logger;\n }\n\n private emitViaOTel(\n level: 'debug' | 'info' | 'warn' | 'error',\n span: ReadableSpan,\n canonicalLogLine: Record<string, unknown>,\n otelLogger: ReturnType<typeof logs.getLogger>,\n ): void {\n const message = this.messageFormat(span);\n // Convert unknown values to strings for OTel Logs API compatibility\n const otelAttributes: Record<string, string | number | boolean> = {};\n for (const [key, value] of Object.entries(canonicalLogLine)) {\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n otelAttributes[key] = value;\n } else if (value !== null && value !== undefined) {\n otelAttributes[key] = String(value);\n }\n }\n otelLogger.emit({\n severityNumber: this.getSeverityNumber(level),\n severityText: level.toUpperCase(),\n body: message,\n attributes: otelAttributes,\n });\n }\n\n private getLogLevel(span: ReadableSpan): 'debug' | 'info' | 'warn' | 'error' {\n // ERROR status code is 2\n if (span.status.code === 2) return 'error';\n\n // Could check for slow spans, etc. in the future\n // For now, default to info\n return 'info';\n }\n\n private shouldLog(level: string): boolean {\n const levels = ['debug', 'info', 'warn', 'error'];\n return levels.indexOf(level) >= levels.indexOf(this.minLevel);\n }\n\n private getSeverityNumber(level: string): SeverityNumber {\n const mapping: Record<string, SeverityNumber> = {\n debug: SeverityNumber.DEBUG,\n info: SeverityNumber.INFO,\n warn: SeverityNumber.WARN,\n error: SeverityNumber.ERROR,\n };\n return mapping[level] ?? SeverityNumber.INFO;\n }\n\n async forceFlush(): Promise<void> {\n // No-op - logging is fire-and-forget\n }\n\n async shutdown(): Promise<void> {\n // No-op - logging is fire-and-forget\n }\n}\n"]}
|