@velum-labs/routekit-tracing 1.4.0 → 1.5.1
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/dist/carrier.d.ts +1 -0
- package/dist/carrier.js +16 -2
- package/dist/event.d.ts +8 -0
- package/dist/event.js +17 -0
- package/dist/exportable.d.ts +5 -0
- package/dist/exportable.js +55 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +6 -4
- package/dist/test/tracing.test.js +96 -1
- package/package.json +1 -1
package/dist/carrier.d.ts
CHANGED
package/dist/carrier.js
CHANGED
|
@@ -32,6 +32,7 @@ export function carrierOf(context) {
|
|
|
32
32
|
return newSessionCarrier().carrier;
|
|
33
33
|
return {
|
|
34
34
|
traceparent: target.traceparent,
|
|
35
|
+
...(target.tracestate !== undefined ? { tracestate: target.tracestate } : {}),
|
|
35
36
|
...(target.baggage !== undefined ? { baggage: target.baggage } : {})
|
|
36
37
|
};
|
|
37
38
|
}
|
|
@@ -44,17 +45,24 @@ export function carrierFromHeaders(headers) {
|
|
|
44
45
|
if (traceparent === undefined || traceparent.length === 0)
|
|
45
46
|
return undefined;
|
|
46
47
|
const baggage = first(headers.baggage);
|
|
47
|
-
|
|
48
|
+
const tracestate = first(headers.tracestate);
|
|
49
|
+
return {
|
|
50
|
+
traceparent,
|
|
51
|
+
...(tracestate !== undefined && tracestate.length > 0 ? { tracestate } : {}),
|
|
52
|
+
...(baggage !== undefined && baggage.length > 0 ? { baggage } : {})
|
|
53
|
+
};
|
|
48
54
|
}
|
|
49
55
|
export function headersOf(carrier) {
|
|
50
56
|
return {
|
|
51
57
|
traceparent: carrier.traceparent,
|
|
58
|
+
...(carrier.tracestate !== undefined ? { tracestate: carrier.tracestate } : {}),
|
|
52
59
|
...(carrier.baggage !== undefined ? { baggage: carrier.baggage } : {})
|
|
53
60
|
};
|
|
54
61
|
}
|
|
55
62
|
export function envOf(carrier) {
|
|
56
63
|
return {
|
|
57
64
|
TRACEPARENT: carrier.traceparent,
|
|
65
|
+
...(carrier.tracestate !== undefined ? { TRACESTATE: carrier.tracestate } : {}),
|
|
58
66
|
...(carrier.baggage !== undefined ? { BAGGAGE: carrier.baggage } : {})
|
|
59
67
|
};
|
|
60
68
|
}
|
|
@@ -64,6 +72,9 @@ export function carrierFromEnv(env = process.env) {
|
|
|
64
72
|
return undefined;
|
|
65
73
|
return {
|
|
66
74
|
traceparent,
|
|
75
|
+
...(env.TRACESTATE !== undefined && env.TRACESTATE.length > 0
|
|
76
|
+
? { tracestate: env.TRACESTATE }
|
|
77
|
+
: {}),
|
|
67
78
|
...(env.BAGGAGE !== undefined && env.BAGGAGE.length > 0 ? { baggage: env.BAGGAGE } : {})
|
|
68
79
|
};
|
|
69
80
|
}
|
|
@@ -81,7 +92,10 @@ export function withBaggage(carrier, entries) {
|
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
94
|
const baggage = [...members.values()].join(",");
|
|
84
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
...carrier,
|
|
97
|
+
...(baggage.length > 0 ? { baggage } : {})
|
|
98
|
+
};
|
|
85
99
|
}
|
|
86
100
|
export function baggageOf(carrier, keys) {
|
|
87
101
|
if (carrier?.baggage === undefined)
|
package/dist/event.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Attributes } from "@opentelemetry/api";
|
|
2
|
+
/** Structured OTel events use the existing provider and its export attribute policy. */
|
|
3
|
+
export declare function emitTraceEvent(input: Readonly<{
|
|
4
|
+
scope: string;
|
|
5
|
+
name: string;
|
|
6
|
+
attributes: Attributes;
|
|
7
|
+
error?: boolean;
|
|
8
|
+
}>): void;
|
package/dist/event.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { context } from "@opentelemetry/api";
|
|
2
|
+
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
|
|
3
|
+
/** Structured OTel events use the existing provider and its export attribute policy. */
|
|
4
|
+
export function emitTraceEvent(input) {
|
|
5
|
+
try {
|
|
6
|
+
logs.getLogger(input.scope).emit({
|
|
7
|
+
eventName: input.name,
|
|
8
|
+
context: context.active(),
|
|
9
|
+
severityNumber: input.error ? SeverityNumber.ERROR : SeverityNumber.INFO,
|
|
10
|
+
severityText: input.error ? "ERROR" : "INFO",
|
|
11
|
+
attributes: input.attributes
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// An unavailable exporter must not affect the operation being observed.
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/exportable.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import type { AttributeValue } from "@opentelemetry/api";
|
|
1
2
|
import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
|
|
2
3
|
import type { LogRecordExporter, ReadableLogRecord } from "@opentelemetry/sdk-logs";
|
|
3
4
|
export type AttributePolicy = {
|
|
4
5
|
allowed: ReadonlySet<string>;
|
|
5
6
|
redactedAttribute: string;
|
|
7
|
+
normalizeAttribute?: (key: string, value: AttributeValue) => AttributeValue | undefined;
|
|
8
|
+
normalizeSpanName?: (span: ReadableSpan) => string;
|
|
9
|
+
normalizeEventName?: (name: string) => string;
|
|
10
|
+
normalizeSeverityText?: (value: string) => string | undefined;
|
|
6
11
|
};
|
|
7
12
|
export declare function toExportableSpan(span: ReadableSpan, policy: AttributePolicy): ReadableSpan;
|
|
8
13
|
export declare function toExportableEvent(record: ReadableLogRecord, policy: AttributePolicy): ReadableLogRecord;
|
package/dist/exportable.js
CHANGED
|
@@ -1,41 +1,73 @@
|
|
|
1
|
+
const URL_IN_SPAN_NAME = /\b[a-z][a-z0-9+.-]*:\/\/[^\s]+/gi;
|
|
2
|
+
const RELATIVE_URL_QUERY_IN_SPAN_NAME = /(^|\s)(\/[^\s?#]*)[?#][^\s]*/g;
|
|
3
|
+
const UUID_IN_SPAN_NAME = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi;
|
|
4
|
+
function exportableSpanName(name) {
|
|
5
|
+
return name
|
|
6
|
+
.replace(URL_IN_SPAN_NAME, "[url]")
|
|
7
|
+
.replace(RELATIVE_URL_QUERY_IN_SPAN_NAME, "$1$2")
|
|
8
|
+
.replace(UUID_IN_SPAN_NAME, "[id]")
|
|
9
|
+
.slice(0, 256);
|
|
10
|
+
}
|
|
1
11
|
function filterAttributes(attributes, policy) {
|
|
2
12
|
const kept = {};
|
|
3
13
|
let dropped = 0;
|
|
14
|
+
let changed = false;
|
|
4
15
|
for (const [key, value] of Object.entries(attributes)) {
|
|
5
|
-
if (policy.allowed.has(key))
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
if (!policy.allowed.has(key) || value === undefined) {
|
|
17
|
+
dropped += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const normalized = policy.normalizeAttribute === undefined
|
|
21
|
+
? value
|
|
22
|
+
: policy.normalizeAttribute(key, value);
|
|
23
|
+
if (normalized === undefined)
|
|
8
24
|
dropped += 1;
|
|
25
|
+
else {
|
|
26
|
+
kept[key] = normalized;
|
|
27
|
+
if (normalized !== value)
|
|
28
|
+
changed = true;
|
|
29
|
+
}
|
|
9
30
|
}
|
|
10
|
-
return { kept, dropped };
|
|
31
|
+
return { kept, dropped, changed };
|
|
11
32
|
}
|
|
12
33
|
export function toExportableSpan(span, policy) {
|
|
13
34
|
const own = filterAttributes(span.attributes, policy);
|
|
14
35
|
let childDrops = 0;
|
|
36
|
+
let childChanged = false;
|
|
15
37
|
const events = span.events.map((event) => {
|
|
38
|
+
const name = policy.normalizeEventName?.(event.name) ?? event.name;
|
|
39
|
+
if (name !== event.name)
|
|
40
|
+
childChanged = true;
|
|
16
41
|
if (event.attributes === undefined)
|
|
17
|
-
return event;
|
|
42
|
+
return name === event.name ? event : { ...event, name };
|
|
18
43
|
const filtered = filterAttributes(event.attributes, policy);
|
|
19
44
|
childDrops += filtered.dropped;
|
|
20
|
-
|
|
45
|
+
childChanged ||= filtered.changed;
|
|
46
|
+
return filtered.dropped === 0 && !filtered.changed && name === event.name
|
|
47
|
+
? event
|
|
48
|
+
: { ...event, name, attributes: filtered.kept };
|
|
21
49
|
});
|
|
22
50
|
const links = span.links.map((link) => {
|
|
23
51
|
if (link.attributes === undefined)
|
|
24
52
|
return link;
|
|
25
53
|
const filtered = filterAttributes(link.attributes, policy);
|
|
26
54
|
childDrops += filtered.dropped;
|
|
27
|
-
|
|
55
|
+
childChanged ||= filtered.changed;
|
|
56
|
+
return filtered.dropped === 0 && !filtered.changed ? link : { ...link, attributes: filtered.kept };
|
|
28
57
|
});
|
|
29
|
-
|
|
58
|
+
const name = exportableSpanName(policy.normalizeSpanName?.(span) ?? span.name);
|
|
59
|
+
const status = span.status.message === undefined ? span.status : { code: span.status.code };
|
|
60
|
+
if (own.dropped + childDrops === 0 && !own.changed && !childChanged && name === span.name && status === span.status) {
|
|
30
61
|
return span;
|
|
62
|
+
}
|
|
31
63
|
return {
|
|
32
|
-
name
|
|
64
|
+
name,
|
|
33
65
|
kind: span.kind,
|
|
34
66
|
spanContext: () => span.spanContext(),
|
|
35
67
|
...(span.parentSpanContext !== undefined ? { parentSpanContext: span.parentSpanContext } : {}),
|
|
36
68
|
startTime: span.startTime,
|
|
37
69
|
endTime: span.endTime,
|
|
38
|
-
status
|
|
70
|
+
status,
|
|
39
71
|
attributes: { ...own.kept, [policy.redactedAttribute]: true },
|
|
40
72
|
links,
|
|
41
73
|
events,
|
|
@@ -50,16 +82,25 @@ export function toExportableSpan(span, policy) {
|
|
|
50
82
|
}
|
|
51
83
|
export function toExportableEvent(record, policy) {
|
|
52
84
|
const filtered = filterAttributes(record.attributes, policy);
|
|
53
|
-
|
|
85
|
+
const bodyDropped = record.body !== undefined;
|
|
86
|
+
const eventName = record.eventName === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: policy.normalizeEventName?.(record.eventName) ?? record.eventName;
|
|
89
|
+
const severityText = record.severityText === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: policy.normalizeSeverityText === undefined
|
|
92
|
+
? record.severityText
|
|
93
|
+
: policy.normalizeSeverityText(record.severityText);
|
|
94
|
+
if (filtered.dropped === 0 && !filtered.changed && !bodyDropped &&
|
|
95
|
+
eventName === record.eventName && severityText === record.severityText)
|
|
54
96
|
return record;
|
|
55
97
|
return {
|
|
56
98
|
hrTime: record.hrTime,
|
|
57
99
|
hrTimeObserved: record.hrTimeObserved,
|
|
58
100
|
...(record.spanContext !== undefined ? { spanContext: record.spanContext } : {}),
|
|
59
|
-
...(
|
|
101
|
+
...(severityText !== undefined ? { severityText } : {}),
|
|
60
102
|
...(record.severityNumber !== undefined ? { severityNumber: record.severityNumber } : {}),
|
|
61
|
-
...(
|
|
62
|
-
...(record.eventName !== undefined ? { eventName: record.eventName } : {}),
|
|
103
|
+
...(eventName !== undefined ? { eventName } : {}),
|
|
63
104
|
resource: record.resource,
|
|
64
105
|
instrumentationScope: record.instrumentationScope,
|
|
65
106
|
attributes: { ...filtered.kept, [policy.redactedAttribute]: true },
|
package/dist/index.d.ts
CHANGED
|
@@ -12,3 +12,4 @@ export { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-tr
|
|
|
12
12
|
export type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
13
13
|
export { InMemoryLogRecordExporter, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
14
14
|
export type { LogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
15
|
+
export { emitTraceEvent } from "./event.js";
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { isLoopbackOtlpEndpoint, PolicyLogExporter, PolicySpanExporter, toExport
|
|
|
5
5
|
export { flushTracing, initTracing, isEventExportConfigured, isTraceExportConfigured, isTracingActive, resetTracingForTest, shutdownTracing, tracingServiceName } from "./provider.js";
|
|
6
6
|
export { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
7
7
|
export { InMemoryLogRecordExporter, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
8
|
+
export { emitTraceEvent } from "./event.js";
|
package/dist/provider.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AttributePolicy } from "./exportable.js";
|
|
|
4
4
|
export type InitTracingOptions = {
|
|
5
5
|
serviceName: string;
|
|
6
6
|
attributePolicy: AttributePolicy;
|
|
7
|
+
allowLoopbackFullFidelity?: boolean;
|
|
7
8
|
fullFidelityEnvironmentVariable?: string;
|
|
8
9
|
logPrefix?: string;
|
|
9
10
|
spanProcessors?: SpanProcessor[];
|
package/dist/provider.js
CHANGED
|
@@ -36,7 +36,7 @@ export function initTracing(options) {
|
|
|
36
36
|
if (traceEndpoint !== undefined) {
|
|
37
37
|
const fullFidelity = (options.fullFidelityEnvironmentVariable !== undefined &&
|
|
38
38
|
process.env[options.fullFidelityEnvironmentVariable] === "1") ||
|
|
39
|
-
isLoopbackOtlpEndpoint(traceEndpoint);
|
|
39
|
+
(options.allowLoopbackFullFidelity !== false && isLoopbackOtlpEndpoint(traceEndpoint));
|
|
40
40
|
if (options.logPrefix !== undefined) {
|
|
41
41
|
process.stderr.write(fullFidelity
|
|
42
42
|
? `${options.logPrefix}: exporting full traces to ${traceEndpoint}\n`
|
|
@@ -48,7 +48,7 @@ export function initTracing(options) {
|
|
|
48
48
|
if (eventEndpoint !== undefined) {
|
|
49
49
|
const fullFidelity = (options.fullFidelityEnvironmentVariable !== undefined &&
|
|
50
50
|
process.env[options.fullFidelityEnvironmentVariable] === "1") ||
|
|
51
|
-
isLoopbackOtlpEndpoint(eventEndpoint);
|
|
51
|
+
(options.allowLoopbackFullFidelity !== false && isLoopbackOtlpEndpoint(eventEndpoint));
|
|
52
52
|
logProcessors.push(new BatchLogRecordProcessor({
|
|
53
53
|
exporter: new PolicyLogExporter(new OTLPLogExporter(), options.attributePolicy, fullFidelity),
|
|
54
54
|
scheduledDelayMillis: 500
|
|
@@ -71,8 +71,10 @@ export const isTracingActive = () => provider !== undefined &&
|
|
|
71
71
|
hasEventListeners() ||
|
|
72
72
|
extraProcessorsInstalled);
|
|
73
73
|
export async function flushTracing() {
|
|
74
|
-
await
|
|
75
|
-
|
|
74
|
+
await Promise.allSettled([
|
|
75
|
+
provider?.forceFlush(),
|
|
76
|
+
loggerProvider?.forceFlush()
|
|
77
|
+
]);
|
|
76
78
|
}
|
|
77
79
|
export async function shutdownTracing() {
|
|
78
80
|
const activeProvider = provider;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { baggageOf, carrierFromEnv, envOf, newSessionCarrier, traceIdOf, withBaggage } from "../index.js";
|
|
3
|
+
import { baggageOf, carrierFromEnv, envOf, newSessionCarrier, toExportableEvent, toExportableSpan, traceIdOf, withBaggage } from "../index.js";
|
|
4
4
|
test("generic trace carriers cross environment and baggage boundaries", () => {
|
|
5
5
|
const created = newSessionCarrier();
|
|
6
6
|
const enriched = withBaggage(created.carrier, { "route.owner": "example", attempt: 2 });
|
|
@@ -12,3 +12,98 @@ test("generic trace carriers cross environment and baggage boundaries", () => {
|
|
|
12
12
|
attempt: "2"
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
|
+
test("export policy removes URL-bearing names, status messages, and denied exception fields", () => {
|
|
16
|
+
const span = {
|
|
17
|
+
name: "fetch GET https://provider.test/private/path?token=secret",
|
|
18
|
+
status: { code: 2, message: "postgres://user:password@database/private" },
|
|
19
|
+
attributes: { "http.method": "GET", "http.url": "https://provider.test/private" },
|
|
20
|
+
events: [
|
|
21
|
+
{
|
|
22
|
+
name: "exception",
|
|
23
|
+
attributes: {
|
|
24
|
+
"exception.type": "TypeError",
|
|
25
|
+
"exception.message": "secret",
|
|
26
|
+
"exception.stacktrace": "secret stack"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
links: [],
|
|
31
|
+
droppedAttributesCount: 0
|
|
32
|
+
};
|
|
33
|
+
const exported = toExportableSpan(span, {
|
|
34
|
+
allowed: new Set(["http.method", "exception.type"]),
|
|
35
|
+
redactedAttribute: "test.redacted"
|
|
36
|
+
});
|
|
37
|
+
assert.equal(exported.name, "fetch GET [url]");
|
|
38
|
+
assert.deepEqual(exported.status, { code: 2 });
|
|
39
|
+
assert.deepEqual(exported.attributes, { "http.method": "GET", "test.redacted": true });
|
|
40
|
+
assert.deepEqual(exported.events[0]?.attributes, {
|
|
41
|
+
"exception.type": "TypeError"
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
test("export policy removes log bodies even when every attribute is allowed", () => {
|
|
45
|
+
const record = {
|
|
46
|
+
body: "credential=secret",
|
|
47
|
+
attributes: { "http.method": "POST" },
|
|
48
|
+
droppedAttributesCount: 0
|
|
49
|
+
};
|
|
50
|
+
const exported = toExportableEvent(record, {
|
|
51
|
+
allowed: new Set(["http.method"]),
|
|
52
|
+
redactedAttribute: "test.redacted"
|
|
53
|
+
});
|
|
54
|
+
assert.equal(exported.body, undefined);
|
|
55
|
+
assert.deepEqual(exported.attributes, {
|
|
56
|
+
"http.method": "POST",
|
|
57
|
+
"test.redacted": true
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
test("W3C tracestate survives headers, environment, context and baggage enrichment", async () => {
|
|
61
|
+
const { carrierFromHeaders, carrierOf, contextOf, headersOf, initTracing, resetTracingForTest } = await import("../index.js");
|
|
62
|
+
await resetTracingForTest();
|
|
63
|
+
initTracing({ serviceName: "carrier-test", attributePolicy: { allowed: new Set(), redactedAttribute: "redacted" } });
|
|
64
|
+
try {
|
|
65
|
+
const carrier = { ...newSessionCarrier().carrier, tracestate: "vendor=opaque" };
|
|
66
|
+
const enriched = withBaggage(carrier, { stage: "checkout" });
|
|
67
|
+
assert.equal(enriched.tracestate, "vendor=opaque");
|
|
68
|
+
assert.deepEqual(carrierFromHeaders(headersOf(enriched)), enriched);
|
|
69
|
+
assert.deepEqual(carrierFromEnv(envOf(enriched)), enriched);
|
|
70
|
+
assert.equal(carrierOf(contextOf(enriched)).tracestate, "vendor=opaque");
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await resetTracingForTest();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
test("trace and log flushes start together even when the trace flush is pending", async () => {
|
|
77
|
+
const { initTracing, flushTracing, resetTracingForTest } = await import("../index.js");
|
|
78
|
+
await resetTracingForTest();
|
|
79
|
+
let finishTrace;
|
|
80
|
+
const pendingTrace = new Promise((resolve) => { finishTrace = resolve; });
|
|
81
|
+
let logFlushStarted = false;
|
|
82
|
+
initTracing({
|
|
83
|
+
serviceName: "parallel-flush-test",
|
|
84
|
+
attributePolicy: { allowed: new Set(), redactedAttribute: "redacted" },
|
|
85
|
+
spanProcessors: [{
|
|
86
|
+
onStart() { },
|
|
87
|
+
onEnd() { },
|
|
88
|
+
forceFlush: () => pendingTrace,
|
|
89
|
+
shutdown: async () => { }
|
|
90
|
+
}],
|
|
91
|
+
logRecordProcessors: [{
|
|
92
|
+
onEmit() { },
|
|
93
|
+
forceFlush: async () => { logFlushStarted = true; },
|
|
94
|
+
shutdown: async () => { }
|
|
95
|
+
}]
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
const flushing = flushTracing();
|
|
99
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
100
|
+
assert.equal(logFlushStarted, true);
|
|
101
|
+
assert.ok(finishTrace);
|
|
102
|
+
finishTrace();
|
|
103
|
+
await flushing;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
finishTrace?.();
|
|
107
|
+
await resetTracingForTest();
|
|
108
|
+
}
|
|
109
|
+
});
|