workers-axiom 0.1.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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/producer/index.d.ts +4 -0
- package/dist/producer/index.d.ts.map +1 -0
- package/dist/producer/index.js +5 -0
- package/dist/producer/index.js.map +1 -0
- package/dist/producer/logger.d.ts +146 -0
- package/dist/producer/logger.d.ts.map +1 -0
- package/dist/producer/logger.js +179 -0
- package/dist/producer/logger.js.map +1 -0
- package/dist/producer/tracing/clock.d.ts +21 -0
- package/dist/producer/tracing/clock.d.ts.map +1 -0
- package/dist/producer/tracing/clock.js +17 -0
- package/dist/producer/tracing/clock.js.map +1 -0
- package/dist/producer/tracing/index.d.ts +3 -0
- package/dist/producer/tracing/index.d.ts.map +1 -0
- package/dist/producer/tracing/index.js +3 -0
- package/dist/producer/tracing/index.js.map +1 -0
- package/dist/producer/tracing/span.d.ts +63 -0
- package/dist/producer/tracing/span.d.ts.map +1 -0
- package/dist/producer/tracing/span.js +94 -0
- package/dist/producer/tracing/span.js.map +1 -0
- package/dist/protocol/index.d.ts +58 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +50 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/tail/index.d.ts +43 -0
- package/dist/tail/index.d.ts.map +1 -0
- package/dist/tail/index.js +155 -0
- package/dist/tail/index.js.map +1 -0
- package/dist/tail/spans.d.ts +19 -0
- package/dist/tail/spans.d.ts.map +1 -0
- package/dist/tail/spans.js +93 -0
- package/dist/tail/spans.js.map +1 -0
- package/package.json +41 -0
- package/src/producer/index.ts +28 -0
- package/src/producer/logger.ts +343 -0
- package/src/producer/tracing/clock.ts +38 -0
- package/src/producer/tracing/index.ts +8 -0
- package/src/producer/tracing/span.ts +148 -0
- package/src/protocol/index.ts +104 -0
- package/src/tail/index.ts +237 -0
- package/src/tail/spans.ts +137 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type AttributeValue, type SpanException, type SpanKind } from '../../protocol';
|
|
2
|
+
import type { Clock } from './clock';
|
|
3
|
+
export interface SpanOptions {
|
|
4
|
+
kind: SpanKind;
|
|
5
|
+
attributes?: Record<string, AttributeValue>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Trace identifiers carried by a logger.
|
|
9
|
+
*
|
|
10
|
+
* `span_id` is the id of the span this logger is currently bound to. A root
|
|
11
|
+
* logger created by `createLogger` is not yet bound to any span; its `span_id`
|
|
12
|
+
* is `undefined`. Once `logger.span(...)` runs, the child logger inside is
|
|
13
|
+
* bound to a real span and its `span_id` is defined — see {@link ChildTraceContext}.
|
|
14
|
+
*
|
|
15
|
+
* `parent_span_id` is the upstream span this trace continues (across services,
|
|
16
|
+
* via inbound `traceparent`). For root loggers with no inbound trace it's
|
|
17
|
+
* `undefined`; for loggers inside `runSpan` it's the outer logger's `span_id`
|
|
18
|
+
* (or, if the outer was a root logger with no bound span, the outer's
|
|
19
|
+
* `parent_span_id` — i.e. the upstream service's span).
|
|
20
|
+
*
|
|
21
|
+
* `sampled` is decided once at the trace root and inherited unchanged by every
|
|
22
|
+
* descendant span. The tail worker uses it to decide whether to forward spans
|
|
23
|
+
* to Axiom's traces store; logs/metrics/errors are always forwarded regardless.
|
|
24
|
+
*/
|
|
25
|
+
export interface TraceContext {
|
|
26
|
+
readonly trace_id: string;
|
|
27
|
+
readonly span_id: string | undefined;
|
|
28
|
+
readonly parent_span_id: string | undefined;
|
|
29
|
+
readonly sampled: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Trace identifiers for a logger bound to a span created inside `runSpan`.
|
|
33
|
+
* Always has a real `span_id`.
|
|
34
|
+
*/
|
|
35
|
+
export interface ChildTraceContext extends TraceContext {
|
|
36
|
+
readonly span_id: string;
|
|
37
|
+
}
|
|
38
|
+
export interface RunSpanInput<T> {
|
|
39
|
+
parent: TraceContext;
|
|
40
|
+
resource: {
|
|
41
|
+
service: string;
|
|
42
|
+
environment: string | undefined;
|
|
43
|
+
};
|
|
44
|
+
name: string;
|
|
45
|
+
options: SpanOptions;
|
|
46
|
+
clock: Clock;
|
|
47
|
+
isExpectedError?: (err: unknown) => boolean;
|
|
48
|
+
fn: (child: ChildTraceContext) => Promise<T>;
|
|
49
|
+
onUnexpectedError?: (err: unknown, child: ChildTraceContext) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run an async function inside a span. Emits a SpanEvent on completion (success or
|
|
53
|
+
* failure). Errors matched by `isExpectedError` are recorded as `status: 'ok'` but
|
|
54
|
+
* still re-thrown — they're expected user-facing failures, not span failures.
|
|
55
|
+
*/
|
|
56
|
+
export declare function runSpan<T>(input: RunSpanInput<T>): Promise<T>;
|
|
57
|
+
/**
|
|
58
|
+
* Build the OTel-shaped exception info from any thrown value. Field names
|
|
59
|
+
* (`type`, `message`, `stacktrace`, `escaped`) match OTel semantic conventions
|
|
60
|
+
* so the tail worker can map them directly to OTLP `exception.*` attributes.
|
|
61
|
+
*/
|
|
62
|
+
export declare function toSpanException(err: unknown): SpanException;
|
|
63
|
+
//# sourceMappingURL=span.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span.d.ts","sourceRoot":"","sources":["../../../src/producer/tracing/span.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EAInB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACd,MAAM,gBAAgB,CAAA;AACvB,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAEpC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CAC5C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAA;IAC3C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAA;IAC9D,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,WAAW,CAAA;IACpB,KAAK,EAAE,KAAK,CAAA;IACZ,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAA;IAC3C,EAAE,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAC5C,iBAAiB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAA;CACrE;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAoCnE;AAQD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAU3D"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { generateSpanId, SPAN_EVENT_TYPE, } from '../../protocol';
|
|
2
|
+
/**
|
|
3
|
+
* Run an async function inside a span. Emits a SpanEvent on completion (success or
|
|
4
|
+
* failure). Errors matched by `isExpectedError` are recorded as `status: 'ok'` but
|
|
5
|
+
* still re-thrown — they're expected user-facing failures, not span failures.
|
|
6
|
+
*/
|
|
7
|
+
export async function runSpan(input) {
|
|
8
|
+
const { parent, resource, name, options, clock, isExpectedError, fn, onUnexpectedError } = input;
|
|
9
|
+
const child = {
|
|
10
|
+
trace_id: parent.trace_id,
|
|
11
|
+
span_id: generateSpanId(),
|
|
12
|
+
parent_span_id: parent.span_id ?? parent.parent_span_id,
|
|
13
|
+
sampled: parent.sampled,
|
|
14
|
+
};
|
|
15
|
+
const startTimeUnixNano = clock.nowUnixNano();
|
|
16
|
+
const base = {
|
|
17
|
+
...child,
|
|
18
|
+
...resource,
|
|
19
|
+
name,
|
|
20
|
+
kind: options.kind,
|
|
21
|
+
attributes: options.attributes,
|
|
22
|
+
};
|
|
23
|
+
try {
|
|
24
|
+
const result = await fn(child);
|
|
25
|
+
emitSpan({ ...base, status: 'ok', startTimeUnixNano, endTimeUnixNano: clock.nowUnixNano() });
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const expected = isExpectedError?.(err) === true;
|
|
30
|
+
const exception = expected ? undefined : toSpanException(err);
|
|
31
|
+
emitSpan({
|
|
32
|
+
...base,
|
|
33
|
+
status: expected ? 'ok' : 'error',
|
|
34
|
+
statusMessage: exception?.message,
|
|
35
|
+
exception,
|
|
36
|
+
startTimeUnixNano,
|
|
37
|
+
endTimeUnixNano: clock.nowUnixNano(),
|
|
38
|
+
});
|
|
39
|
+
if (!expected)
|
|
40
|
+
onUnexpectedError?.(err, child);
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function emitSpan(input) {
|
|
45
|
+
const event = { type: SPAN_EVENT_TYPE, ...input };
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.log(JSON.stringify(event));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the OTel-shaped exception info from any thrown value. Field names
|
|
51
|
+
* (`type`, `message`, `stacktrace`, `escaped`) match OTel semantic conventions
|
|
52
|
+
* so the tail worker can map them directly to OTLP `exception.*` attributes.
|
|
53
|
+
*/
|
|
54
|
+
export function toSpanException(err) {
|
|
55
|
+
if (err instanceof Error) {
|
|
56
|
+
return {
|
|
57
|
+
type: err.name,
|
|
58
|
+
message: err.message,
|
|
59
|
+
stacktrace: err.stack,
|
|
60
|
+
escaped: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { message: serializeUnknown(err), escaped: true };
|
|
64
|
+
}
|
|
65
|
+
function serializeUnknown(value) {
|
|
66
|
+
if (value === null)
|
|
67
|
+
return 'null';
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
return 'undefined';
|
|
70
|
+
if (typeof value === 'string')
|
|
71
|
+
return value;
|
|
72
|
+
if (value instanceof Error) {
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
...value,
|
|
75
|
+
name: value.name,
|
|
76
|
+
message: value.message,
|
|
77
|
+
stack: value.stack,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(value);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
85
|
+
return String(value);
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === 'symbol')
|
|
88
|
+
return value.toString();
|
|
89
|
+
if (typeof value === 'function')
|
|
90
|
+
return '[function]';
|
|
91
|
+
return '[unserializable]';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=span.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span.js","sourceRoot":"","sources":["../../../src/producer/tracing/span.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACd,eAAe,GAIhB,MAAM,gBAAgB,CAAA;AAoDvB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,KAAsB;IACrD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAA;IAChG,MAAM,KAAK,GAAsB;QAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,OAAO,EAAE,cAAc,EAAE;QACzB,cAAc,EAAE,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,cAAc;QACvD,OAAO,EAAE,MAAM,CAAC,OAAO;KACxB,CAAA;IAED,MAAM,iBAAiB,GAAG,KAAK,CAAC,WAAW,EAAE,CAAA;IAC7C,MAAM,IAAI,GAAG;QACX,GAAG,KAAK;QACR,GAAG,QAAQ;QACX,IAAI;QACJ,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,UAAU,EAAE,OAAO,CAAC,UAAU;KAC/B,CAAA;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,CAAA;QAC9B,QAAQ,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,eAAe,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;QAC5F,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,CAAA;QAChD,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;QAC7D,QAAQ,CAAC;YACP,GAAG,IAAI;YACP,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;YACjC,aAAa,EAAE,SAAS,EAAE,OAAO;YACjC,SAAS;YACT,iBAAiB;YACjB,eAAe,EAAE,KAAK,CAAC,WAAW,EAAE;SACrC,CAAC,CAAA;QACF,IAAI,CAAC,QAAQ;YAAE,iBAAiB,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QAC9C,MAAM,GAAG,CAAA;IACX,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,KAA8B;IAC9C,MAAM,KAAK,GAAc,EAAE,IAAI,EAAE,eAAe,EAAE,GAAG,KAAK,EAAE,CAAA;IAC5D,sCAAsC;IACtC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY;IAC1C,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;QACzB,OAAO;YACL,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,OAAO,EAAE,GAAG,CAAC,OAAO;YACpB,UAAU,EAAE,GAAG,CAAC,KAAK;YACrB,OAAO,EAAE,IAAI;SACd,CAAA;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,gBAAgB,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1D,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAA;IACjC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,WAAW,CAAA;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC3C,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,SAAS,CAAC;YACpB,GAAG,KAAK;YACR,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;SACnB,CAAC,CAAA;IACJ,CAAC;IACD,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACzF,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;QACtB,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;QACtD,IAAI,OAAO,KAAK,KAAK,UAAU;YAAE,OAAO,YAAY,CAAA;QACpD,OAAO,kBAAkB,CAAA;IAC3B,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire format shared by the producer (logger) and the tail-worker consumer.
|
|
3
|
+
*
|
|
4
|
+
* The producer emits JSON lines via `console.log`; the tail worker parses
|
|
5
|
+
* them and forwards to Axiom. Both sides import these types so the format
|
|
6
|
+
* is enforced by the type system rather than coordinated by string matching.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SPAN_EVENT_TYPE: "span";
|
|
9
|
+
/** Tag identifying a record carrying fields to merge onto `invocation_summary`. */
|
|
10
|
+
export declare const SUMMARY_PROPERTIES_TYPE: "summary_properties";
|
|
11
|
+
export type SpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer';
|
|
12
|
+
export type SpanStatus = 'ok' | 'error';
|
|
13
|
+
export type AttributeValue = string | number | boolean;
|
|
14
|
+
export type SpanException = {
|
|
15
|
+
type?: string;
|
|
16
|
+
message: string;
|
|
17
|
+
stacktrace?: string;
|
|
18
|
+
escaped?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export interface SpanEvent {
|
|
21
|
+
type: typeof SPAN_EVENT_TYPE;
|
|
22
|
+
trace_id: string;
|
|
23
|
+
span_id: string;
|
|
24
|
+
parent_span_id?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
kind: SpanKind;
|
|
27
|
+
/** Unix epoch nanoseconds, encoded as string to avoid JS number precision loss. */
|
|
28
|
+
startTimeUnixNano: string;
|
|
29
|
+
endTimeUnixNano: string;
|
|
30
|
+
status: SpanStatus;
|
|
31
|
+
statusMessage?: string;
|
|
32
|
+
service: string;
|
|
33
|
+
environment?: string;
|
|
34
|
+
attributes?: Record<string, AttributeValue>;
|
|
35
|
+
/**
|
|
36
|
+
* Sampling verdict, fixed at trace root and propagated to every child span.
|
|
37
|
+
* The tail worker forwards spans to Axiom's traces store only when true.
|
|
38
|
+
*/
|
|
39
|
+
sampled: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Optional exception info attached when status === 'error'.
|
|
42
|
+
* Field names match OTel semantic conventions (`exception.type`,
|
|
43
|
+
* `exception.message`, `exception.stacktrace`, `exception.escaped`) so
|
|
44
|
+
* the tail worker maps them directly into the OTLP `exception` span event.
|
|
45
|
+
*/
|
|
46
|
+
exception?: SpanException;
|
|
47
|
+
}
|
|
48
|
+
export declare function isSpanEvent(value: unknown): value is SpanEvent;
|
|
49
|
+
export interface TraceParent {
|
|
50
|
+
trace_id: string;
|
|
51
|
+
span_id: string;
|
|
52
|
+
sampled: boolean;
|
|
53
|
+
}
|
|
54
|
+
export declare function parseTraceparent(header: string | null | undefined): TraceParent | undefined;
|
|
55
|
+
export declare function formatTraceparent(trace_id: string, span_id: string, sampled: boolean): string;
|
|
56
|
+
export declare function generateTraceId(): string;
|
|
57
|
+
export declare function generateSpanId(): string;
|
|
58
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,eAAe,EAAG,MAAe,CAAA;AAE9C,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,EAAG,oBAA6B,CAAA;AAEpE,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,CAAA;AAEjF,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,OAAO,CAAA;AAEvC,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAEtD,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,OAAO,eAAe,CAAA;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,QAAQ,CAAA;IACd,mFAAmF;IACnF,iBAAiB,EAAE,MAAM,CAAA;IACzB,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,UAAU,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;IAC3C;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,aAAa,CAAA;CAC1B;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAY9D;AAID,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAM3F;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAE7F;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire format shared by the producer (logger) and the tail-worker consumer.
|
|
3
|
+
*
|
|
4
|
+
* The producer emits JSON lines via `console.log`; the tail worker parses
|
|
5
|
+
* them and forwards to Axiom. Both sides import these types so the format
|
|
6
|
+
* is enforced by the type system rather than coordinated by string matching.
|
|
7
|
+
*/
|
|
8
|
+
export const SPAN_EVENT_TYPE = 'span';
|
|
9
|
+
/** Tag identifying a record carrying fields to merge onto `invocation_summary`. */
|
|
10
|
+
export const SUMMARY_PROPERTIES_TYPE = 'summary_properties';
|
|
11
|
+
export function isSpanEvent(value) {
|
|
12
|
+
if (typeof value !== 'object' || value === null)
|
|
13
|
+
return false;
|
|
14
|
+
const v = value;
|
|
15
|
+
return (v.type === SPAN_EVENT_TYPE &&
|
|
16
|
+
typeof v.trace_id === 'string' &&
|
|
17
|
+
typeof v.span_id === 'string' &&
|
|
18
|
+
typeof v.name === 'string' &&
|
|
19
|
+
typeof v.startTimeUnixNano === 'string' &&
|
|
20
|
+
typeof v.endTimeUnixNano === 'string' &&
|
|
21
|
+
typeof v.service === 'string');
|
|
22
|
+
}
|
|
23
|
+
const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
24
|
+
export function parseTraceparent(header) {
|
|
25
|
+
if (!header)
|
|
26
|
+
return undefined;
|
|
27
|
+
const match = TRACEPARENT_RE.exec(header.trim().toLowerCase());
|
|
28
|
+
if (!match)
|
|
29
|
+
return undefined;
|
|
30
|
+
const [, trace_id, span_id, flags] = match;
|
|
31
|
+
return { trace_id: trace_id, span_id: span_id, sampled: (parseInt(flags, 16) & 1) === 1 };
|
|
32
|
+
}
|
|
33
|
+
export function formatTraceparent(trace_id, span_id, sampled) {
|
|
34
|
+
return `00-${trace_id}-${span_id}-${sampled ? '01' : '00'}`;
|
|
35
|
+
}
|
|
36
|
+
export function generateTraceId() {
|
|
37
|
+
return randomHex(32);
|
|
38
|
+
}
|
|
39
|
+
export function generateSpanId() {
|
|
40
|
+
return randomHex(16);
|
|
41
|
+
}
|
|
42
|
+
function randomHex(length) {
|
|
43
|
+
const bytes = new Uint8Array(length / 2);
|
|
44
|
+
crypto.getRandomValues(bytes);
|
|
45
|
+
let out = '';
|
|
46
|
+
for (const b of bytes)
|
|
47
|
+
out += b.toString(16).padStart(2, '0');
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/protocol/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,eAAe,GAAG,MAAe,CAAA;AAE9C,mFAAmF;AACnF,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAA6B,CAAA;AA4CpE,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,CAAC,GAAG,KAAgC,CAAA;IAC1C,OAAO,CACL,CAAC,CAAC,IAAI,KAAK,eAAe;QAC1B,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAC7B,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,iBAAiB,KAAK,QAAQ;QACvC,OAAO,CAAC,CAAC,eAAe,KAAK,QAAQ;QACrC,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAC9B,CAAA;AACH,CAAC;AAED,MAAM,cAAc,GAAG,kDAAkD,CAAA;AAQzE,MAAM,UAAU,gBAAgB,CAAC,MAAiC;IAChE,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAC7B,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;IAC9D,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAA;IAC5B,MAAM,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,GAAG,KAAK,CAAA;IAC1C,OAAO,EAAE,QAAQ,EAAE,QAAS,EAAE,OAAO,EAAE,OAAQ,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,KAAM,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,EAAE,CAAA;AAC9F,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,OAAe,EAAE,OAAgB;IACnF,OAAO,MAAM,QAAQ,IAAI,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;AAC7D,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,SAAS,CAAC,EAAE,CAAC,CAAA;AACtB,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,SAAS,CAAC,EAAE,CAAC,CAAA;AACtB,CAAC;AAED,SAAS,SAAS,CAAC,MAAc;IAC/B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IACxC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC7B,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC7D,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface AxiomConfig {
|
|
2
|
+
/** Axiom API token. */
|
|
3
|
+
axiomToken: string;
|
|
4
|
+
/** Axiom dataset receiving both ingest events and OTLP traces. */
|
|
5
|
+
axiomDataset: string;
|
|
6
|
+
/**
|
|
7
|
+
* Axiom ingest endpoint. Defaults to the US edge:
|
|
8
|
+
* `https://us-east-1.aws.edge.axiom.co/v1/ingest`.
|
|
9
|
+
* Override with the EU edge or self-hosted Axiom URL as needed.
|
|
10
|
+
* The dataset name is appended automatically.
|
|
11
|
+
*/
|
|
12
|
+
ingestBaseUrl?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Axiom OTLP traces endpoint. Defaults to `https://api.axiom.co/v1/traces`.
|
|
15
|
+
*/
|
|
16
|
+
tracesEndpoint?: string;
|
|
17
|
+
}
|
|
18
|
+
export type TailHandler = (events: TraceItem[], env: unknown, ctx: ExecutionContext) => Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Build a Cloudflare Workers `tail()` handler that forwards logs, metrics,
|
|
21
|
+
* errors, and spans from any number of producing workers to Axiom.
|
|
22
|
+
*
|
|
23
|
+
* Configure each producing worker with:
|
|
24
|
+
* "tail_consumers": [{ "service": "<name-of-this-tail-worker>" }]
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { createTailHandler } from 'workers-axiom/tail'
|
|
29
|
+
*
|
|
30
|
+
* interface Env { AXIOM_TOKEN: string; AXIOM_DATASET: string }
|
|
31
|
+
*
|
|
32
|
+
* export default {
|
|
33
|
+
* tail: (events, env: Env, ctx) =>
|
|
34
|
+
* createTailHandler({
|
|
35
|
+
* axiomToken: env.AXIOM_TOKEN,
|
|
36
|
+
* axiomDataset: env.AXIOM_DATASET,
|
|
37
|
+
* })(events, env, ctx),
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function createTailHandler(config: AxiomConfig): TailHandler;
|
|
42
|
+
export { sendSpansToAxiom } from './spans';
|
|
43
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tail/index.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,uBAAuB;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,WAAW,GAAG,CACxB,MAAM,EAAE,SAAS,EAAE,EACnB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CA6FlE;AA8FD,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { isSpanEvent, SUMMARY_PROPERTIES_TYPE } from '../protocol';
|
|
2
|
+
import { sendSpansToAxiom } from './spans';
|
|
3
|
+
/**
|
|
4
|
+
* Build a Cloudflare Workers `tail()` handler that forwards logs, metrics,
|
|
5
|
+
* errors, and spans from any number of producing workers to Axiom.
|
|
6
|
+
*
|
|
7
|
+
* Configure each producing worker with:
|
|
8
|
+
* "tail_consumers": [{ "service": "<name-of-this-tail-worker>" }]
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createTailHandler } from 'workers-axiom/tail'
|
|
13
|
+
*
|
|
14
|
+
* interface Env { AXIOM_TOKEN: string; AXIOM_DATASET: string }
|
|
15
|
+
*
|
|
16
|
+
* export default {
|
|
17
|
+
* tail: (events, env: Env, ctx) =>
|
|
18
|
+
* createTailHandler({
|
|
19
|
+
* axiomToken: env.AXIOM_TOKEN,
|
|
20
|
+
* axiomDataset: env.AXIOM_DATASET,
|
|
21
|
+
* })(events, env, ctx),
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createTailHandler(config) {
|
|
26
|
+
const { axiomToken, axiomDataset } = config;
|
|
27
|
+
const ingestBase = config.ingestBaseUrl ?? 'https://us-east-1.aws.edge.axiom.co/v1/ingest';
|
|
28
|
+
return async function tail(events, _env, _ctx) {
|
|
29
|
+
const axiomEvents = [];
|
|
30
|
+
const spans = [];
|
|
31
|
+
for (const event of events) {
|
|
32
|
+
const worker = event.scriptName ?? 'unknown';
|
|
33
|
+
const eventTime = new Date(event.eventTimestamp ?? Date.now()).toISOString();
|
|
34
|
+
// Fields the app explicitly opted into surfacing on `invocation_summary`,
|
|
35
|
+
// collected from `summary_properties` records emitted via `logger.summary`.
|
|
36
|
+
// Last-write-wins.
|
|
37
|
+
const summaryProps = {};
|
|
38
|
+
// Last trace_id observed in this worker invocation. A single invocation
|
|
39
|
+
// can contain multiple traces; we keep the last one because it's the
|
|
40
|
+
// most specific to the invocation's outbound work.
|
|
41
|
+
let traceId;
|
|
42
|
+
for (const log of event.logs) {
|
|
43
|
+
const logTime = new Date(log.timestamp).toISOString();
|
|
44
|
+
for (const msg of log.message) {
|
|
45
|
+
if (typeof msg !== 'string')
|
|
46
|
+
continue;
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(msg);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
55
|
+
continue;
|
|
56
|
+
// Span events go to Axiom's /v1/traces endpoint, not the ingest
|
|
57
|
+
// dataset. Don't forward as a regular log row.
|
|
58
|
+
if (isSpanEvent(parsed)) {
|
|
59
|
+
spans.push(parsed);
|
|
60
|
+
traceId = parsed.trace_id;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const record = parsed;
|
|
64
|
+
if (typeof record.trace_id === 'string') {
|
|
65
|
+
traceId = record.trace_id;
|
|
66
|
+
}
|
|
67
|
+
if (record.type === SUMMARY_PROPERTIES_TYPE) {
|
|
68
|
+
for (const [key, value] of Object.entries(record)) {
|
|
69
|
+
if (key === 'type' || key === 'trace_id')
|
|
70
|
+
continue;
|
|
71
|
+
summaryProps[key] = value;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
axiomEvents.push({
|
|
76
|
+
_time: logTime,
|
|
77
|
+
worker,
|
|
78
|
+
...record,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const requestInfo = extractRequestInfo(event.event);
|
|
83
|
+
axiomEvents.push({
|
|
84
|
+
_time: eventTime,
|
|
85
|
+
worker,
|
|
86
|
+
type: 'invocation_summary',
|
|
87
|
+
trace_id: traceId,
|
|
88
|
+
...summaryProps,
|
|
89
|
+
...requestInfo,
|
|
90
|
+
cpuTime: event.cpuTime,
|
|
91
|
+
wallTime: event.wallTime,
|
|
92
|
+
traceEvent: event.event,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// Run synchronously (not via waitUntil) so failures surface in tail logs
|
|
96
|
+
// and OTLP/ingest errors aren't silently swallowed by the runtime.
|
|
97
|
+
await Promise.all([
|
|
98
|
+
sendToAxiom({ events: axiomEvents, axiomToken, axiomDataset, ingestBase }),
|
|
99
|
+
sendSpansToAxiom({
|
|
100
|
+
spans,
|
|
101
|
+
axiomToken,
|
|
102
|
+
axiomDataset,
|
|
103
|
+
...(config.tracesEndpoint !== undefined && { tracesEndpoint: config.tracesEndpoint }),
|
|
104
|
+
}),
|
|
105
|
+
]);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function extractRequestInfo(event) {
|
|
109
|
+
const fetchEvent = event;
|
|
110
|
+
if (!fetchEvent?.request)
|
|
111
|
+
return null;
|
|
112
|
+
const req = fetchEvent.request;
|
|
113
|
+
const headers = req.headers ?? {};
|
|
114
|
+
const cf = req.cf ?? {};
|
|
115
|
+
let path;
|
|
116
|
+
try {
|
|
117
|
+
if (req.url)
|
|
118
|
+
path = new URL(req.url).pathname;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Invalid URL
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
reqMethod: req.method,
|
|
125
|
+
reqUrl: req.url,
|
|
126
|
+
reqPath: path,
|
|
127
|
+
reqHost: headers['host'],
|
|
128
|
+
reqIp: headers['cf-connecting-ip'],
|
|
129
|
+
reqCountry: cf.country,
|
|
130
|
+
reqCity: cf.city,
|
|
131
|
+
reqAsn: cf.asn,
|
|
132
|
+
reqUserAgent: headers['user-agent'],
|
|
133
|
+
reqCfRay: headers['cf-ray'],
|
|
134
|
+
resStatus: fetchEvent.response?.status,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function sendToAxiom(params) {
|
|
138
|
+
const { events, axiomToken, axiomDataset, ingestBase } = params;
|
|
139
|
+
if (events.length === 0)
|
|
140
|
+
return;
|
|
141
|
+
const response = await fetch(`${ingestBase}/${axiomDataset}`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
'Content-Type': 'application/json',
|
|
145
|
+
Authorization: `Bearer ${axiomToken}`,
|
|
146
|
+
},
|
|
147
|
+
body: JSON.stringify(events),
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const text = await response.text().catch(() => '<no body>');
|
|
151
|
+
throw new Error(`Failed to send logs to Axiom: ${response.status} ${response.statusText} — ${text}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export { sendSpansToAxiom } from './spans';
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tail/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span forwarding to Axiom's OTLP `/v1/traces` endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Spans arrive as `console.log` JSON events with the {@link SpanEvent} wire format.
|
|
5
|
+
* The sampling verdict is decided once at the trace root by the producing logger
|
|
6
|
+
* and stamped on every span via `SpanEvent.sampled`; the tail worker is a pure
|
|
7
|
+
* router that forwards only `sampled === true` spans. Logs/metrics/errors are
|
|
8
|
+
* forwarded unconditionally by the caller — sampling gates traces, not log visibility.
|
|
9
|
+
*/
|
|
10
|
+
import type { SpanEvent } from '../protocol';
|
|
11
|
+
export interface SendSpansInput {
|
|
12
|
+
spans: SpanEvent[];
|
|
13
|
+
axiomToken: string;
|
|
14
|
+
axiomDataset: string;
|
|
15
|
+
/** Full URL of Axiom's OTLP traces endpoint. Defaults to `https://api.axiom.co/v1/traces`. */
|
|
16
|
+
tracesEndpoint?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function sendSpansToAxiom(input: SendSpansInput): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=spans.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spans.d.ts","sourceRoot":"","sources":["../../src/tail/spans.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAU5C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,8FAA8F;IAC9F,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB3E"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const SPAN_KIND_TO_OTLP = {
|
|
2
|
+
internal: 1,
|
|
3
|
+
server: 2,
|
|
4
|
+
client: 3,
|
|
5
|
+
producer: 4,
|
|
6
|
+
consumer: 5,
|
|
7
|
+
};
|
|
8
|
+
export async function sendSpansToAxiom(input) {
|
|
9
|
+
const { spans, axiomToken, axiomDataset } = input;
|
|
10
|
+
const endpoint = input.tracesEndpoint ?? 'https://api.axiom.co/v1/traces';
|
|
11
|
+
const kept = spans.filter((s) => s.sampled);
|
|
12
|
+
if (kept.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
const body = toOtlpPayload(kept);
|
|
15
|
+
const response = await fetch(endpoint, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
Authorization: `Bearer ${axiomToken}`,
|
|
20
|
+
'X-Axiom-Dataset': axiomDataset,
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
const text = await response.text().catch(() => '<no body>');
|
|
26
|
+
throw new Error(`Failed to send spans to Axiom: ${response.status} ${response.statusText} — ${text}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Group spans by `(service, environment)` so each unique pair becomes its own
|
|
31
|
+
* OTLP `ResourceSpans` entry. Axiom's Traces dashboard groups by `service.name`
|
|
32
|
+
* from the resource block, so this grouping is what makes per-service filtering work.
|
|
33
|
+
*/
|
|
34
|
+
function toOtlpPayload(spans) {
|
|
35
|
+
const byResource = new Map();
|
|
36
|
+
for (const span of spans) {
|
|
37
|
+
const key = `${span.service} ${span.environment ?? ''}`;
|
|
38
|
+
const list = byResource.get(key);
|
|
39
|
+
if (list)
|
|
40
|
+
list.push(span);
|
|
41
|
+
else
|
|
42
|
+
byResource.set(key, [span]);
|
|
43
|
+
}
|
|
44
|
+
const resourceSpans = [];
|
|
45
|
+
for (const group of byResource.values()) {
|
|
46
|
+
const first = group[0];
|
|
47
|
+
const resource = {
|
|
48
|
+
attributes: [
|
|
49
|
+
attr('service.name', first.service),
|
|
50
|
+
...(first.environment ? [attr('deployment.environment', first.environment)] : []),
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
resourceSpans.push({
|
|
54
|
+
resource,
|
|
55
|
+
scopeSpans: [{ scope: { name: first.service }, spans: group.map(toOtlpSpan) }],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return { resourceSpans };
|
|
59
|
+
}
|
|
60
|
+
function toOtlpSpan(span) {
|
|
61
|
+
const attributes = Object.entries(span.attributes ?? {}).map(([key, value]) => attr(key, value));
|
|
62
|
+
const result = {
|
|
63
|
+
traceId: span.trace_id,
|
|
64
|
+
spanId: span.span_id,
|
|
65
|
+
parentSpanId: span.parent_span_id,
|
|
66
|
+
name: span.name,
|
|
67
|
+
kind: SPAN_KIND_TO_OTLP[span.kind],
|
|
68
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
69
|
+
endTimeUnixNano: span.endTimeUnixNano,
|
|
70
|
+
status: { code: span.status === 'error' ? 2 : 1, message: span.statusMessage },
|
|
71
|
+
attributes,
|
|
72
|
+
};
|
|
73
|
+
if (span.exception) {
|
|
74
|
+
const eventAttrs = [];
|
|
75
|
+
for (const [key, value] of Object.entries(span.exception)) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
eventAttrs.push(attr(`exception.${key}`, value));
|
|
79
|
+
}
|
|
80
|
+
result.events = [
|
|
81
|
+
{ timeUnixNano: span.endTimeUnixNano, name: 'exception', attributes: eventAttrs },
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function attr(key, value) {
|
|
87
|
+
if (typeof value === 'string')
|
|
88
|
+
return { key, value: { stringValue: value } };
|
|
89
|
+
if (typeof value === 'boolean')
|
|
90
|
+
return { key, value: { boolValue: value } };
|
|
91
|
+
return { key, value: { intValue: value } };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=spans.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spans.js","sourceRoot":"","sources":["../../src/tail/spans.ts"],"names":[],"mappings":"AAWA,MAAM,iBAAiB,GAAsC;IAC3D,QAAQ,EAAE,CAAC;IACX,MAAM,EAAE,CAAC;IACT,MAAM,EAAE,CAAC;IACT,QAAQ,EAAE,CAAC;IACX,QAAQ,EAAE,CAAC;CACZ,CAAA;AAUD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAqB;IAC1D,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,KAAK,CAAA;IACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,IAAI,gCAAgC,CAAA;IACzE,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;IAC3C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAM;IAE7B,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;IAChC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE;QACrC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,aAAa,EAAE,UAAU,UAAU,EAAE;YACrC,iBAAiB,EAAE,YAAY;SAChC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAA;IAEF,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,CAAA;QAC3D,MAAM,IAAI,KAAK,CACb,kCAAkC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,MAAM,IAAI,EAAE,CACrF,CAAA;IACH,CAAC;AACH,CAAC;AAyBD;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAkB;IACvC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAA;IACjD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,WAAW,IAAI,EAAE,EAAE,CAAA;QACvD,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,IAAI;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;;YACpB,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAClC,CAAC;IAED,MAAM,aAAa,GAAwB,EAAE,CAAA;IAC7C,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;QACvB,MAAM,QAAQ,GAAG;YACf,UAAU,EAAE;gBACV,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC;gBACnC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,wBAAwB,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aAClF;SACF,CAAA;QACD,aAAa,CAAC,IAAI,CAAC;YACjB,QAAQ;YACR,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;SAC/E,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,EAAE,aAAa,EAAE,CAAA;AAC1B,CAAC;AAED,SAAS,UAAU,CAAC,IAAe;IACjC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;IAChG,MAAM,MAAM,GAAa;QACvB,OAAO,EAAE,IAAI,CAAC,QAAQ;QACtB,MAAM,EAAE,IAAI,CAAC,OAAO;QACpB,YAAY,EAAE,IAAI,CAAC,cAAc;QACjC,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAClC,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;QACzC,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE;QAC9E,UAAU;KACX,CAAA;IACD,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,MAAM,UAAU,GAAoB,EAAE,CAAA;QACtC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1D,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAQ;YACjC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,GAAG,EAAE,EAAE,KAAkC,CAAC,CAAC,CAAA;QAC/E,CAAC;QACD,MAAM,CAAC,MAAM,GAAG;YACd,EAAE,YAAY,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE;SAClF,CAAA;IACH,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,KAAgC;IACzD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAA;IAC5E,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CAAA;IAC3E,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAA;AAC5C,CAAC"}
|