@telemetry-dev/sdk 0.1.2 → 0.1.5
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 +20 -1
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +44 -4
- package/package.json +3 -3
- package/src/attrs.ts +10 -0
- package/src/capture.ts +6 -0
- package/src/client.ts +32 -0
- package/src/config.ts +15 -0
- package/src/index.ts +6 -0
- package/src/logs.ts +35 -2
- package/src/noop.ts +1 -0
- package/src/observe.ts +11 -0
- package/src/span.ts +96 -6
package/README.md
CHANGED
|
@@ -60,6 +60,12 @@ Other `init()` options: `enabled` (kill switch), `registerGlobal`, `exportMode:
|
|
|
60
60
|
`exportTimeoutMillis` 30000), `spanFilter`, `resourceAttributes`, `logLevel` (SDK diagnostics,
|
|
61
61
|
default `"warn"`), `fetch`, `waitUntil`, `onError`.
|
|
62
62
|
|
|
63
|
+
Root spans remain independent by default. `init({ sessionMode: "process" })` groups otherwise
|
|
64
|
+
uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK initialization.
|
|
65
|
+
Explicit and propagated session IDs take precedence, and valid parent trace context is preserved.
|
|
66
|
+
The generated session ends when the client is shut down or replaced; a later `init()` generates a
|
|
67
|
+
new ID. It is not persisted across processes.
|
|
68
|
+
|
|
63
69
|
`sampler` accepts an OpenTelemetry `Sampler` and overrides `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG`.
|
|
64
70
|
The default is parent-based always-on. Session roots keep the configured root policy.
|
|
65
71
|
Nested spans and explicitly parented spans keep the real parent's policy.
|
|
@@ -71,7 +77,8 @@ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-at
|
|
|
71
77
|
- `observe(fn, options?)` — wrap a sync/async function: args → input, return → output, errors
|
|
72
78
|
captured and rethrown; activates context for nesting.
|
|
73
79
|
- `startSpan(name, options?)` — manual handle; does **not** activate context. `handle.update()`,
|
|
74
|
-
`handle.end()`, `handle.traceparent`, `handle.span`
|
|
80
|
+
`handle.end()`, `handle.recordOutputChunk(timestampMs?)`, `handle.traceparent`, `handle.span`
|
|
81
|
+
(raw OTel span).
|
|
75
82
|
- `startActiveSpan(name, options?, fn)` — callback-scoped; activates context, auto-ends.
|
|
76
83
|
- `updateActiveSpan(fields)` — update the innermost active span.
|
|
77
84
|
- `propagateAttributes({ userId, sessionId, metadata }, fn)` — stamps `user.id`,
|
|
@@ -93,6 +100,18 @@ GenAI semantic conventions; cost is computed server-side from usage unless `cost
|
|
|
93
100
|
Duration and token-usage histograms (`gen_ai.client.operation.duration`,
|
|
94
101
|
`gen_ai.client.token.usage`) are recorded automatically for generation/agent/embedding/tool spans.
|
|
95
102
|
|
|
103
|
+
Generation spans also emit `gen_ai.client.operation.time_to_first_chunk` when a valid
|
|
104
|
+
`timeToFirstChunkMs` is supplied. `handle.recordOutputChunk()` records each nonempty output chunk's
|
|
105
|
+
arrival; after the first chunk, intervals contribute to
|
|
106
|
+
`gen_ai.client.operation.time_per_output_chunk`. Both histograms use seconds and are emitted only
|
|
107
|
+
after the ended span passes `spanFilter`, including interrupted streams. Chunk timing uses bounded
|
|
108
|
+
histogram state and does not retain individual intervals. Pull-based timing includes consumer delay
|
|
109
|
+
between reads. An explicit timestamp uses the same monotonic millisecond clock for every chunk.
|
|
110
|
+
|
|
111
|
+
Provider integrations feature-detect chunk recording. With an older core SDK that lacks
|
|
112
|
+
`recordOutputChunk`, streams and existing tracing continue normally, but chunk-interval metrics are
|
|
113
|
+
unavailable. Upgrade the core SDK with provider integrations to enable the new metric.
|
|
114
|
+
|
|
96
115
|
## Serverless
|
|
97
116
|
|
|
98
117
|
Use `exportMode: "immediate"` and either `await flush()` before returning or pass the platform
|
package/dist/index.d.mts
CHANGED
|
@@ -20,6 +20,8 @@ interface TelemetryOptions {
|
|
|
20
20
|
serviceName?: string;
|
|
21
21
|
/** Kill switch: `false` makes the SDK a complete no-op (tests). Default true. */
|
|
22
22
|
enabled?: boolean;
|
|
23
|
+
/** Group otherwise uncorrelated spans and logs for this SDK initialization. Default "explicit". */
|
|
24
|
+
sessionMode?: "explicit" | "process";
|
|
23
25
|
/** Register this SDK's TracerProvider/context manager/propagator globally. Default false (isolated). */
|
|
24
26
|
registerGlobal?: boolean;
|
|
25
27
|
/** "batched" (default) buffers spans; "immediate" exports per span (serverless). */
|
|
@@ -170,6 +172,8 @@ interface SpanHandle {
|
|
|
170
172
|
readonly traceparent: string | null;
|
|
171
173
|
readonly isRecording: boolean;
|
|
172
174
|
update(fields: SpanFields): SpanHandle;
|
|
175
|
+
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
176
|
+
recordOutputChunk(timestampMs?: number): void;
|
|
173
177
|
end(fields?: SpanFields & {
|
|
174
178
|
endTime?: Date | number;
|
|
175
179
|
}): void;
|
package/dist/index.mjs
CHANGED
|
@@ -3,10 +3,11 @@ import { CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator, p
|
|
|
3
3
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
4
|
import { BatchLogRecordProcessor, LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
5
5
|
import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
6
|
-
import { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, PROPAGATED_KEY, SCOPE_NAME, SCOPE_NAME as SCOPE_NAME$1, SCOPE_VERSION, StampingSpanProcessor, TelemetrySpanProcessor, activeContext, activeContext as activeContext$1, als, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, sessionIdOf, sessionRootTracerProvider, sessionSampler, setLogLevel, withContext, withContext as withContext$1, withSessionParent } from "@telemetry-dev/otel";
|
|
6
|
+
import { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, DURATION_BUCKETS, OUTPUT_CHUNK_HISTOGRAM, PROPAGATED_KEY, SCOPE_NAME, SCOPE_NAME as SCOPE_NAME$1, SCOPE_VERSION, StampingSpanProcessor, TelemetrySpanProcessor, activeContext, activeContext as activeContext$1, als, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, sessionIdOf, sessionRootTracerProvider, sessionSampler, setLogLevel, withContext, withContext as withContext$1, withSessionParent } from "@telemetry-dev/otel";
|
|
7
7
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
8
8
|
function resolveConfig(options = {}) {
|
|
9
9
|
const env = resolveEnv();
|
|
10
|
+
if (options.sessionMode !== void 0 && options.sessionMode !== "explicit" && options.sessionMode !== "process") throw new TypeError(`Invalid sessionMode: ${String(options.sessionMode)}`);
|
|
10
11
|
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
11
12
|
return {
|
|
12
13
|
apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
|
|
@@ -14,6 +15,7 @@ function resolveConfig(options = {}) {
|
|
|
14
15
|
environment: options.environment ?? env.TELEMETRY_DEV_ENVIRONMENT ?? "production",
|
|
15
16
|
serviceName: options.serviceName ?? env.OTEL_SERVICE_NAME ?? "unknown_service",
|
|
16
17
|
enabled: options.enabled ?? true,
|
|
18
|
+
sessionMode: options.sessionMode ?? "explicit",
|
|
17
19
|
registerGlobal: options.registerGlobal ?? false,
|
|
18
20
|
exportMode: options.exportMode ?? "batched",
|
|
19
21
|
sampler: options.sampler,
|
|
@@ -73,6 +75,7 @@ function initInner(options, overrides) {
|
|
|
73
75
|
activeClient = NOOP_CLIENT;
|
|
74
76
|
return NOOP_CLIENT;
|
|
75
77
|
}
|
|
78
|
+
if (config.sessionMode === "process") config.processSessionId = globalThis.crypto.randomUUID();
|
|
76
79
|
const resource = resourceFromAttributes({
|
|
77
80
|
"service.name": config.serviceName,
|
|
78
81
|
"deployment.environment.name": config.environment,
|
|
@@ -237,7 +240,14 @@ function log(message, options) {
|
|
|
237
240
|
onError: cfg.onError
|
|
238
241
|
};
|
|
239
242
|
const severity = SEVERITIES[options?.level ?? "info"];
|
|
240
|
-
const
|
|
243
|
+
const active = activeContext$1();
|
|
244
|
+
const explicitSession = options?.attributes?.["gen_ai.conversation.id"];
|
|
245
|
+
const propagatedSession = propagatedFromContext(active)?.["gen_ai.conversation.id"];
|
|
246
|
+
const sessionId = explicitSession !== void 0 ? void 0 : typeof propagatedSession === "string" ? propagatedSession : cfg.processSessionId;
|
|
247
|
+
const ctx = sessionId === void 0 ? active : active.setValue(PROPAGATED_KEY, {
|
|
248
|
+
...propagatedFromContext(active),
|
|
249
|
+
"gen_ai.conversation.id": sessionId
|
|
250
|
+
});
|
|
241
251
|
const attributes = {};
|
|
242
252
|
for (const [key, value] of Object.entries(options?.attributes ?? {})) {
|
|
243
253
|
if (value === void 0) continue;
|
|
@@ -353,6 +363,7 @@ const NOOP_SPAN_HANDLE = {
|
|
|
353
363
|
traceparent: null,
|
|
354
364
|
isRecording: false,
|
|
355
365
|
update: () => NOOP_SPAN_HANDLE,
|
|
366
|
+
recordOutputChunk: () => {},
|
|
356
367
|
end: () => {}
|
|
357
368
|
};
|
|
358
369
|
//#endregion
|
|
@@ -425,7 +436,11 @@ function metaFor(core, options) {
|
|
|
425
436
|
function createSpanHandle(core, name, options = {}) {
|
|
426
437
|
const meta = metaFor(core, options);
|
|
427
438
|
const resolved = resolveParent(options.parent);
|
|
428
|
-
const
|
|
439
|
+
const sessionId = sessionIdOf(resolved, options.attributes) ?? core.config.processSessionId;
|
|
440
|
+
const parentCtx = withSessionParent(sessionId === void 0 ? resolved : resolved.setValue(PROPAGATED_KEY, {
|
|
441
|
+
...propagatedFromContext(resolved),
|
|
442
|
+
"gen_ai.conversation.id": sessionId
|
|
443
|
+
}), sessionId, core.config.apiKey);
|
|
429
444
|
const attrs = fieldsToAttributes(options, meta.type, meta.capture);
|
|
430
445
|
const span = core.tracer.startSpan(name, {
|
|
431
446
|
kind: options.kind ?? SpanKind.INTERNAL,
|
|
@@ -442,6 +457,7 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
442
457
|
if (options.name) span.updateName(options.name);
|
|
443
458
|
if (options.error !== void 0) applyError(span, options.error);
|
|
444
459
|
const spanContext = span.spanContext();
|
|
460
|
+
let previousOutputChunkTime;
|
|
445
461
|
const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
|
|
446
462
|
const handle = {
|
|
447
463
|
span,
|
|
@@ -460,6 +476,28 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
460
476
|
}
|
|
461
477
|
return handle;
|
|
462
478
|
},
|
|
479
|
+
recordOutputChunk(timestampMs) {
|
|
480
|
+
const now = timestampMs ?? performance.now();
|
|
481
|
+
if (!span.isRecording() || !Number.isFinite(now) || previousOutputChunkTime !== void 0 && now < previousOutputChunkTime) return;
|
|
482
|
+
if (previousOutputChunkTime !== void 0) {
|
|
483
|
+
const seconds = Math.max(0, now - previousOutputChunkTime) / 1e3;
|
|
484
|
+
const target = span;
|
|
485
|
+
const histogram = target[OUTPUT_CHUNK_HISTOGRAM] ??= {
|
|
486
|
+
count: 0,
|
|
487
|
+
sum: 0,
|
|
488
|
+
min: seconds,
|
|
489
|
+
max: seconds,
|
|
490
|
+
bucketCounts: Array.from({ length: DURATION_BUCKETS.length + 1 }, () => 0)
|
|
491
|
+
};
|
|
492
|
+
histogram.count++;
|
|
493
|
+
histogram.sum += seconds;
|
|
494
|
+
histogram.min = Math.min(histogram.min, seconds);
|
|
495
|
+
histogram.max = Math.max(histogram.max, seconds);
|
|
496
|
+
const bucket = DURATION_BUCKETS.findIndex((boundary) => seconds <= boundary);
|
|
497
|
+
histogram.bucketCounts[bucket < 0 ? DURATION_BUCKETS.length : bucket]++;
|
|
498
|
+
}
|
|
499
|
+
previousOutputChunkTime = now;
|
|
500
|
+
},
|
|
463
501
|
end(fields) {
|
|
464
502
|
try {
|
|
465
503
|
if (fields) applyFields(span, meta, fields);
|
|
@@ -529,7 +567,9 @@ function injectW3cContext(context, carrier, options = {}) {
|
|
|
529
567
|
const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
|
|
530
568
|
for (const key of Object.keys(carrier)) {
|
|
531
569
|
const normalized = key.toLowerCase();
|
|
532
|
-
if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage")
|
|
570
|
+
if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") {
|
|
571
|
+
if (!Reflect.deleteProperty(carrier, key)) throw new TypeError(`Cannot delete stale W3C carrier property: ${key}`);
|
|
572
|
+
}
|
|
533
573
|
}
|
|
534
574
|
propagator.inject(context, carrier, defaultTextMapSetter);
|
|
535
575
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Pure TypeScript SDK for telemetry.dev: LLM traces, logs, and GenAI metrics on OpenTelemetry, for Node and edge runtimes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"genai",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/telemetry-dev/telemetry.
|
|
19
|
+
"url": "git+https://github.com/telemetry-dev/telemetry.git",
|
|
20
20
|
"directory": "packages/sdk"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@opentelemetry/sdk-logs": "^0.218.0",
|
|
43
43
|
"@opentelemetry/sdk-metrics": "^2.7.1",
|
|
44
44
|
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
45
|
-
"@telemetry-dev/otel": "^0.1.
|
|
45
|
+
"@telemetry-dev/otel": "^0.1.5"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opentelemetry/api": "^1.9.1",
|
package/src/attrs.ts
CHANGED
|
@@ -126,32 +126,42 @@ export function fieldsToAttributes(
|
|
|
126
126
|
|
|
127
127
|
if (fields.systemInstructions !== undefined && cfg.captureInput) {
|
|
128
128
|
const value = prepareCaptureValue("gen_ai.system_instructions", fields.systemInstructions, cfg);
|
|
129
|
+
|
|
129
130
|
if (value !== undefined) attrs["gen_ai.system_instructions"] = value;
|
|
130
131
|
}
|
|
132
|
+
|
|
131
133
|
if (fields.input !== undefined && cfg.captureInput) {
|
|
132
134
|
const key = inputKeyFor(type);
|
|
133
135
|
const value = prepareCaptureValue(key, fields.input, cfg);
|
|
136
|
+
|
|
134
137
|
if (value !== undefined) attrs[key] = value;
|
|
135
138
|
}
|
|
139
|
+
|
|
136
140
|
if (fields.output !== undefined && cfg.captureOutput) {
|
|
137
141
|
const key = outputKeyFor(type);
|
|
138
142
|
const value = prepareCaptureValue(key, fields.output, cfg);
|
|
143
|
+
|
|
139
144
|
if (value !== undefined) attrs[key] = value;
|
|
140
145
|
}
|
|
146
|
+
|
|
141
147
|
if (fields.metadata) {
|
|
142
148
|
for (const [key, value] of Object.entries(fields.metadata)) {
|
|
143
149
|
if (RESERVED_METADATA_KEYS.has(key)) {
|
|
144
150
|
diag.debug(`metadata key "${key}" is reserved; pass it via propagateAttributes instead`);
|
|
145
151
|
continue;
|
|
146
152
|
}
|
|
153
|
+
|
|
147
154
|
const attr = typeof value === "string" ? value : jsonAttr(value);
|
|
155
|
+
|
|
148
156
|
if (attr !== undefined) {
|
|
149
157
|
attrs[`td.metadata.${key}`] = truncate(attr, cfg.maxAttributeLength);
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
160
|
}
|
|
161
|
+
|
|
153
162
|
if (fields.attributes) {
|
|
154
163
|
Object.assign(attrs, omitUndefined(fields.attributes));
|
|
155
164
|
}
|
|
165
|
+
|
|
156
166
|
return attrs;
|
|
157
167
|
}
|
package/src/capture.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface CaptureConfig {
|
|
|
13
13
|
|
|
14
14
|
export function truncate(value: string, maxLength: number): string {
|
|
15
15
|
if (value.length <= maxLength) return value;
|
|
16
|
+
|
|
16
17
|
// Total stays within maxLength so the provider's attributeValueLengthLimit backstop
|
|
17
18
|
// (set to the same cap) never slices the marker off.
|
|
18
19
|
return value.slice(0, Math.max(maxLength - TRUNCATION_MARKER.length, 0)) + TRUNCATION_MARKER;
|
|
@@ -25,15 +26,20 @@ export function prepareCaptureValue(
|
|
|
25
26
|
cfg: CaptureConfig,
|
|
26
27
|
): string | undefined {
|
|
27
28
|
let masked = value;
|
|
29
|
+
|
|
28
30
|
if (cfg.mask) {
|
|
29
31
|
try {
|
|
30
32
|
masked = cfg.mask(value, { key });
|
|
31
33
|
} catch (error) {
|
|
32
34
|
reportError(cfg.onError, error instanceof Error ? error : new Error(String(error)));
|
|
35
|
+
|
|
33
36
|
return undefined;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
39
|
+
|
|
36
40
|
const serialized = jsonAttr(masked);
|
|
41
|
+
|
|
37
42
|
if (serialized === undefined) return undefined;
|
|
43
|
+
|
|
38
44
|
return truncate(serialized, cfg.maxAttributeLength);
|
|
39
45
|
}
|
package/src/client.ts
CHANGED
|
@@ -89,6 +89,7 @@ export function init(options?: TelemetryOptions, overrides?: ClientOverrides): T
|
|
|
89
89
|
} catch (error) {
|
|
90
90
|
reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
|
|
91
91
|
activeClient = NOOP_CLIENT;
|
|
92
|
+
|
|
92
93
|
return NOOP_CLIENT;
|
|
93
94
|
}
|
|
94
95
|
}
|
|
@@ -104,19 +105,25 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
const enabled = config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter);
|
|
108
|
+
|
|
107
109
|
if (!enabled) {
|
|
108
110
|
if (config.enabled && !config.apiKey) {
|
|
109
111
|
diag.debug("no api key (apiKey option or TELEMETRY_DEV_API_KEY); telemetry disabled");
|
|
110
112
|
}
|
|
113
|
+
|
|
111
114
|
activeClient = NOOP_CLIENT;
|
|
115
|
+
|
|
112
116
|
return NOOP_CLIENT;
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
if (config.sessionMode === "process") config.processSessionId = globalThis.crypto.randomUUID();
|
|
120
|
+
|
|
115
121
|
const resource = resourceFromAttributes({
|
|
116
122
|
"service.name": config.serviceName,
|
|
117
123
|
"deployment.environment.name": config.environment,
|
|
118
124
|
...config.resourceAttributes,
|
|
119
125
|
});
|
|
126
|
+
|
|
120
127
|
const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError };
|
|
121
128
|
const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
|
|
122
129
|
|
|
@@ -129,6 +136,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
129
136
|
(headers
|
|
130
137
|
? createMetricExporter({ url: `${config.baseUrl}/v1/metrics`, headers }, transport)
|
|
131
138
|
: undefined);
|
|
139
|
+
|
|
132
140
|
const metrics = metricExporter
|
|
133
141
|
? createMetricsPipeline({
|
|
134
142
|
resource,
|
|
@@ -159,21 +167,26 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
159
167
|
spanProcessors: [processor],
|
|
160
168
|
spanLimits: { attributeValueLengthLimit: config.maxAttributeLength },
|
|
161
169
|
});
|
|
170
|
+
|
|
162
171
|
const tracer = provider.getTracer(SCOPE_NAME, SCOPE_VERSION);
|
|
163
172
|
|
|
164
173
|
let registeredTrace = false;
|
|
165
174
|
let registeredContext = false;
|
|
166
175
|
let registeredPropagation = false;
|
|
176
|
+
|
|
167
177
|
if (config.registerGlobal) {
|
|
168
178
|
registeredTrace = trace.setGlobalTracerProvider(
|
|
169
179
|
config.sessionRootOf
|
|
170
180
|
? sessionRootTracerProvider(provider, config.apiKey, config.sessionRootOf, config.onError)
|
|
171
181
|
: provider,
|
|
172
182
|
);
|
|
183
|
+
|
|
173
184
|
if (als) {
|
|
174
185
|
registeredContext = apiContext.setGlobalContextManager(new AlsContextManager(als));
|
|
175
186
|
}
|
|
187
|
+
|
|
176
188
|
registeredPropagation = propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
189
|
+
|
|
177
190
|
if (!registeredTrace) {
|
|
178
191
|
diag.warn(
|
|
179
192
|
"a global TracerProvider is already registered; attach the TelemetrySpanProcessor from '@telemetry-dev/otel' to your own provider instead",
|
|
@@ -186,43 +199,56 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
186
199
|
(headers
|
|
187
200
|
? createLogExporter({ url: `${config.baseUrl}/v1/logs`, headers }, transport)
|
|
188
201
|
: undefined);
|
|
202
|
+
|
|
189
203
|
// Lazy: the log pipeline only exists once log() is first called.
|
|
190
204
|
let logs: { logger: Logger; forceFlush(): Promise<void>; shutdown(): Promise<void> } | undefined;
|
|
205
|
+
|
|
191
206
|
const ensureLogger = (): Logger | undefined => {
|
|
192
207
|
if (!logExporter) return undefined;
|
|
208
|
+
|
|
193
209
|
if (!logs) {
|
|
194
210
|
const logProcessor =
|
|
195
211
|
config.exportMode === "immediate"
|
|
196
212
|
? new SimpleLogRecordProcessor(logExporter)
|
|
197
213
|
: new BatchLogRecordProcessor(logExporter);
|
|
214
|
+
|
|
198
215
|
const loggerProvider = new LoggerProvider({
|
|
199
216
|
resource,
|
|
200
217
|
logRecordLimits: { attributeValueLengthLimit: config.maxAttributeLength },
|
|
201
218
|
processors: [logProcessor],
|
|
202
219
|
});
|
|
220
|
+
|
|
203
221
|
logs = {
|
|
204
222
|
logger: loggerProvider.getLogger(SCOPE_NAME, SCOPE_VERSION),
|
|
205
223
|
forceFlush: () => loggerProvider.forceFlush(),
|
|
206
224
|
shutdown: () => loggerProvider.shutdown(),
|
|
207
225
|
};
|
|
208
226
|
}
|
|
227
|
+
|
|
209
228
|
return logs.logger;
|
|
210
229
|
};
|
|
211
230
|
|
|
212
231
|
let torn = false;
|
|
232
|
+
|
|
213
233
|
const collect = (op: "forceFlush" | "shutdown"): Promise<void> => {
|
|
214
234
|
const parts: Promise<void>[] = [provider[op]()];
|
|
235
|
+
|
|
215
236
|
if (logs) parts.push(logs[op]());
|
|
237
|
+
|
|
216
238
|
if (metrics) parts.push(metrics[op]());
|
|
239
|
+
|
|
217
240
|
return Promise.all(parts)
|
|
218
241
|
.then(() => undefined)
|
|
219
242
|
.catch((error: Error) => reportError(config.onError, error));
|
|
220
243
|
};
|
|
244
|
+
|
|
221
245
|
const handoff = (p: Promise<void>): Promise<void> => {
|
|
222
246
|
if (config.waitUntil) {
|
|
223
247
|
config.waitUntil(p);
|
|
248
|
+
|
|
224
249
|
return Promise.resolve();
|
|
225
250
|
}
|
|
251
|
+
|
|
226
252
|
return p;
|
|
227
253
|
};
|
|
228
254
|
|
|
@@ -234,14 +260,20 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
234
260
|
shutdown: () => {
|
|
235
261
|
if (torn) return Promise.resolve();
|
|
236
262
|
torn = true;
|
|
263
|
+
|
|
237
264
|
if (registeredTrace) trace.disable();
|
|
265
|
+
|
|
238
266
|
if (registeredContext) apiContext.disable();
|
|
267
|
+
|
|
239
268
|
if (registeredPropagation) propagation.disable();
|
|
240
269
|
handle.core = undefined;
|
|
270
|
+
|
|
241
271
|
return handoff(collect("shutdown"));
|
|
242
272
|
},
|
|
243
273
|
};
|
|
274
|
+
|
|
244
275
|
handle.core = { config, tracer, ensureLogger };
|
|
245
276
|
activeClient = handle;
|
|
277
|
+
|
|
246
278
|
return handle;
|
|
247
279
|
}
|
package/src/config.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface TelemetryOptions {
|
|
|
26
26
|
serviceName?: string;
|
|
27
27
|
/** Kill switch: `false` makes the SDK a complete no-op (tests). Default true. */
|
|
28
28
|
enabled?: boolean;
|
|
29
|
+
/** Group otherwise uncorrelated spans and logs for this SDK initialization. Default "explicit". */
|
|
30
|
+
sessionMode?: "explicit" | "process";
|
|
29
31
|
/** Register this SDK's TracerProvider/context manager/propagator globally. Default false (isolated). */
|
|
30
32
|
registerGlobal?: boolean;
|
|
31
33
|
/** "batched" (default) buffers spans; "immediate" exports per span (serverless). */
|
|
@@ -69,6 +71,8 @@ export interface ResolvedConfig {
|
|
|
69
71
|
environment: string;
|
|
70
72
|
serviceName: string;
|
|
71
73
|
enabled: boolean;
|
|
74
|
+
sessionMode: "explicit" | "process";
|
|
75
|
+
processSessionId?: string;
|
|
72
76
|
registerGlobal: boolean;
|
|
73
77
|
exportMode: ExportMode;
|
|
74
78
|
sampler?: Sampler;
|
|
@@ -91,16 +95,27 @@ export const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
|
|
|
91
95
|
|
|
92
96
|
export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
|
|
93
97
|
const env = resolveEnv();
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
options.sessionMode !== undefined &&
|
|
101
|
+
options.sessionMode !== "explicit" &&
|
|
102
|
+
options.sessionMode !== "process"
|
|
103
|
+
) {
|
|
104
|
+
throw new TypeError(`Invalid sessionMode: ${String(options.sessionMode)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(
|
|
95
108
|
/\/+$/,
|
|
96
109
|
"",
|
|
97
110
|
);
|
|
111
|
+
|
|
98
112
|
return {
|
|
99
113
|
apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
|
|
100
114
|
baseUrl,
|
|
101
115
|
environment: options.environment ?? env.TELEMETRY_DEV_ENVIRONMENT ?? "production",
|
|
102
116
|
serviceName: options.serviceName ?? env.OTEL_SERVICE_NAME ?? "unknown_service",
|
|
103
117
|
enabled: options.enabled ?? true,
|
|
118
|
+
sessionMode: options.sessionMode ?? "explicit",
|
|
104
119
|
registerGlobal: options.registerGlobal ?? false,
|
|
105
120
|
exportMode: options.exportMode ?? "batched",
|
|
106
121
|
sampler: options.sampler,
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,9 @@ export type {
|
|
|
6
6
|
TokenUsage,
|
|
7
7
|
ToolFields,
|
|
8
8
|
} from "./attrs.ts";
|
|
9
|
+
|
|
9
10
|
export { type ClientOverrides, flush, init, shutdown, type TelemetryClient } from "./client.ts";
|
|
11
|
+
|
|
10
12
|
export type {
|
|
11
13
|
BatchOptions,
|
|
12
14
|
ExportMode,
|
|
@@ -15,6 +17,7 @@ export type {
|
|
|
15
17
|
SdkLogLevel,
|
|
16
18
|
TelemetryOptions,
|
|
17
19
|
} from "./config.ts";
|
|
20
|
+
|
|
18
21
|
export {
|
|
19
22
|
propagateAttributes,
|
|
20
23
|
type PropagatedAttributes,
|
|
@@ -22,8 +25,11 @@ export {
|
|
|
22
25
|
TelemetrySpanProcessor,
|
|
23
26
|
type TelemetrySpanProcessorOptions,
|
|
24
27
|
} from "@telemetry-dev/otel";
|
|
28
|
+
|
|
25
29
|
export { log, type LogOptions } from "./logs.ts";
|
|
30
|
+
|
|
26
31
|
export { observe, type ObserveOptions } from "./observe.ts";
|
|
32
|
+
|
|
27
33
|
export {
|
|
28
34
|
activeContext,
|
|
29
35
|
extractW3cContext,
|
package/src/logs.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { AttributeValue } from "@opentelemetry/api";
|
|
2
2
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
activeContext,
|
|
6
|
+
PROPAGATED_KEY,
|
|
7
|
+
propagatedFromContext,
|
|
8
|
+
reportError,
|
|
9
|
+
} from "@telemetry-dev/otel";
|
|
5
10
|
|
|
6
11
|
import { prepareCaptureValue, truncate } from "./capture.ts";
|
|
7
12
|
import { currentClient } from "./client.ts";
|
|
@@ -29,30 +34,58 @@ export interface LogOptions {
|
|
|
29
34
|
*/
|
|
30
35
|
export function log(message: string, options?: LogOptions): void {
|
|
31
36
|
const core = currentClient().core;
|
|
37
|
+
|
|
32
38
|
if (!core) return;
|
|
39
|
+
|
|
33
40
|
try {
|
|
34
41
|
const logger = core.ensureLogger();
|
|
42
|
+
|
|
35
43
|
if (!logger) return;
|
|
36
44
|
const cfg = core.config;
|
|
45
|
+
|
|
37
46
|
const captureCfg = {
|
|
38
47
|
mask: cfg.mask,
|
|
39
48
|
maxAttributeLength: cfg.maxAttributeLength,
|
|
40
49
|
onError: cfg.onError,
|
|
41
50
|
};
|
|
51
|
+
|
|
42
52
|
const severity = SEVERITIES[options?.level ?? "info"];
|
|
43
|
-
const
|
|
53
|
+
const active = activeContext();
|
|
54
|
+
const explicitSession = options?.attributes?.["gen_ai.conversation.id"];
|
|
55
|
+
const propagatedSession = propagatedFromContext(active)?.["gen_ai.conversation.id"];
|
|
56
|
+
|
|
57
|
+
const sessionId =
|
|
58
|
+
explicitSession !== undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: typeof propagatedSession === "string"
|
|
61
|
+
? propagatedSession
|
|
62
|
+
: cfg.processSessionId;
|
|
63
|
+
|
|
64
|
+
const ctx =
|
|
65
|
+
sessionId === undefined
|
|
66
|
+
? active
|
|
67
|
+
: active.setValue(PROPAGATED_KEY, {
|
|
68
|
+
...propagatedFromContext(active),
|
|
69
|
+
"gen_ai.conversation.id": sessionId,
|
|
70
|
+
});
|
|
44
71
|
|
|
45
72
|
const attributes: Record<string, AttributeValue> = {};
|
|
73
|
+
|
|
46
74
|
for (const [key, value] of Object.entries(options?.attributes ?? {})) {
|
|
47
75
|
if (value === undefined) continue;
|
|
76
|
+
|
|
48
77
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
49
78
|
attributes[key] = value;
|
|
50
79
|
continue;
|
|
51
80
|
}
|
|
81
|
+
|
|
52
82
|
const prepared = prepareCaptureValue(key, value, captureCfg);
|
|
83
|
+
|
|
53
84
|
if (prepared !== undefined) attributes[key] = prepared;
|
|
54
85
|
}
|
|
86
|
+
|
|
55
87
|
const propagated = propagatedFromContext(ctx);
|
|
88
|
+
|
|
56
89
|
if (propagated) {
|
|
57
90
|
for (const [key, value] of Object.entries(propagated)) {
|
|
58
91
|
if (value !== undefined) {
|
package/src/noop.ts
CHANGED
package/src/observe.ts
CHANGED
|
@@ -19,11 +19,14 @@ export function observe<TThis, TArgs extends unknown[], TReturn>(
|
|
|
19
19
|
): (...args: TArgs) => TReturn {
|
|
20
20
|
const wrapped = function (this: TThis, ...args: TArgs): TReturn {
|
|
21
21
|
const core = currentClient().core;
|
|
22
|
+
|
|
22
23
|
if (!core) return fn.apply(this, args);
|
|
23
24
|
|
|
24
25
|
let handle: SpanHandle;
|
|
26
|
+
|
|
25
27
|
try {
|
|
26
28
|
const name = options?.name ?? (fn.name || "anonymous");
|
|
29
|
+
|
|
27
30
|
const input =
|
|
28
31
|
options?.input !== undefined
|
|
29
32
|
? options.input
|
|
@@ -32,18 +35,22 @@ export function observe<TThis, TArgs extends unknown[], TReturn>(
|
|
|
32
35
|
: args.length === 1
|
|
33
36
|
? args[0]
|
|
34
37
|
: args;
|
|
38
|
+
|
|
35
39
|
handle = createSpanHandle(core, name, { ...options, input });
|
|
36
40
|
} catch (error) {
|
|
37
41
|
reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
|
|
42
|
+
|
|
38
43
|
return fn.apply(this, args);
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
try {
|
|
42
47
|
const result = withContext(handle.context, () => fn.apply(this, args));
|
|
48
|
+
|
|
43
49
|
if (isThenable(result)) {
|
|
44
50
|
return result.then(
|
|
45
51
|
(value) => {
|
|
46
52
|
handle.end({ output: value });
|
|
53
|
+
|
|
47
54
|
return value;
|
|
48
55
|
},
|
|
49
56
|
(error: Error) => {
|
|
@@ -52,13 +59,17 @@ export function observe<TThis, TArgs extends unknown[], TReturn>(
|
|
|
52
59
|
},
|
|
53
60
|
) as TReturn;
|
|
54
61
|
}
|
|
62
|
+
|
|
55
63
|
handle.end({ output: result });
|
|
64
|
+
|
|
56
65
|
return result;
|
|
57
66
|
} catch (error) {
|
|
58
67
|
handle.end({ error: error instanceof Error ? error : new Error(String(error)) });
|
|
59
68
|
throw error;
|
|
60
69
|
}
|
|
61
70
|
};
|
|
71
|
+
|
|
62
72
|
Object.defineProperty(wrapped, "name", { value: fn.name || "anonymous", configurable: true });
|
|
73
|
+
|
|
63
74
|
return wrapped;
|
|
64
75
|
}
|
package/src/span.ts
CHANGED
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
21
|
activeContext,
|
|
22
|
+
DURATION_BUCKETS,
|
|
22
23
|
omitUndefined,
|
|
24
|
+
OUTPUT_CHUNK_HISTOGRAM,
|
|
25
|
+
type OutputChunkHistogram,
|
|
23
26
|
PROPAGATED_KEY,
|
|
24
27
|
propagatedFromContext,
|
|
25
28
|
reportError,
|
|
@@ -67,6 +70,8 @@ export interface SpanHandle {
|
|
|
67
70
|
readonly traceparent: string | null;
|
|
68
71
|
readonly isRecording: boolean;
|
|
69
72
|
update(fields: SpanFields): SpanHandle;
|
|
73
|
+
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
74
|
+
recordOutputChunk(timestampMs?: number): void;
|
|
70
75
|
end(fields?: SpanFields & { endTime?: Date | number }): void;
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -104,25 +109,35 @@ function isSpanHandle(value: Exclude<ParentRef, string>): value is SpanHandle {
|
|
|
104
109
|
function withAmbientPropagated(ctx: Context): Context {
|
|
105
110
|
if (propagatedFromContext(ctx)) return ctx;
|
|
106
111
|
const ambient = propagatedFromContext(activeContext());
|
|
112
|
+
|
|
107
113
|
return ambient ? ctx.setValue(PROPAGATED_KEY, ambient) : ctx;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
function resolveParent(parent: ParentRef | undefined): Context {
|
|
111
117
|
const base = activeContext();
|
|
118
|
+
|
|
112
119
|
if (parent === undefined) return base;
|
|
120
|
+
|
|
113
121
|
if (typeof parent === "string") {
|
|
114
122
|
const parsed = parseTraceParent(parent);
|
|
123
|
+
|
|
115
124
|
if (!parsed) return base;
|
|
125
|
+
|
|
116
126
|
return trace.setSpanContext(base, { ...parsed, isRemote: true });
|
|
117
127
|
}
|
|
128
|
+
|
|
118
129
|
const ref = parent as Exclude<ParentRef, string>;
|
|
130
|
+
|
|
119
131
|
if (isSpanHandle(ref)) return withAmbientPropagated(ref.context);
|
|
132
|
+
|
|
120
133
|
if (isContext(ref)) return withAmbientPropagated(ref);
|
|
134
|
+
|
|
121
135
|
return trace.setSpanContext(base, ref);
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
function applyError(span: Span, error: unknown): void {
|
|
125
139
|
const err = error instanceof Error ? error : undefined;
|
|
140
|
+
|
|
126
141
|
// Prefer the specific subclass name (Python parity: type(error).__name__) — many SDK
|
|
127
142
|
// error classes (e.g. openai's BadRequestError) never override the inherited "Error" name.
|
|
128
143
|
const errorType =
|
|
@@ -131,6 +146,7 @@ function applyError(span: Span, error: unknown): void {
|
|
|
131
146
|
: err.name !== "Error" && err.name.length > 0
|
|
132
147
|
? err.name
|
|
133
148
|
: (err.constructor?.name ?? "Error") || "Error";
|
|
149
|
+
|
|
134
150
|
const message = err?.message ?? String(error);
|
|
135
151
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
136
152
|
span.setAttribute("error.type", errorType);
|
|
@@ -148,12 +164,15 @@ function applyError(span: Span, error: unknown): void {
|
|
|
148
164
|
function applyFields(span: Span, meta: SpanMeta, fields: SpanFields): void {
|
|
149
165
|
if (fields.name) span.updateName(fields.name);
|
|
150
166
|
const attrs = fieldsToAttributes(fields, meta.type, meta.capture);
|
|
167
|
+
|
|
151
168
|
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
169
|
+
|
|
152
170
|
if (fields.error !== undefined) applyError(span, fields.error);
|
|
153
171
|
}
|
|
154
172
|
|
|
155
173
|
function metaFor(core: ClientCore, options?: StartSpanOptions): SpanMeta {
|
|
156
174
|
const cfg = core.config;
|
|
175
|
+
|
|
157
176
|
return {
|
|
158
177
|
type: options?.type ?? "span",
|
|
159
178
|
capture: {
|
|
@@ -174,12 +193,19 @@ export function createSpanHandle(
|
|
|
174
193
|
): SpanHandle {
|
|
175
194
|
const meta = metaFor(core, options);
|
|
176
195
|
const resolved = resolveParent(options.parent);
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
const sessionId = sessionIdOf(resolved, options.attributes) ?? core.config.processSessionId;
|
|
197
|
+
|
|
198
|
+
const sessionContext =
|
|
199
|
+
sessionId === undefined
|
|
200
|
+
? resolved
|
|
201
|
+
: resolved.setValue(PROPAGATED_KEY, {
|
|
202
|
+
...propagatedFromContext(resolved),
|
|
203
|
+
"gen_ai.conversation.id": sessionId,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const parentCtx = withSessionParent(sessionContext, sessionId, core.config.apiKey);
|
|
182
207
|
const attrs = fieldsToAttributes(options, meta.type, meta.capture);
|
|
208
|
+
|
|
183
209
|
const span = core.tracer.startSpan(
|
|
184
210
|
name,
|
|
185
211
|
{
|
|
@@ -194,14 +220,20 @@ export function createSpanHandle(
|
|
|
194
220
|
},
|
|
195
221
|
parentCtx,
|
|
196
222
|
);
|
|
223
|
+
|
|
197
224
|
SPAN_META.set(span, meta);
|
|
225
|
+
|
|
198
226
|
// The processor stamps propagation on start; explicit fields still win.
|
|
199
227
|
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
228
|
+
|
|
200
229
|
if (options.name) span.updateName(options.name);
|
|
230
|
+
|
|
201
231
|
if (options.error !== undefined) applyError(span, options.error);
|
|
202
232
|
|
|
203
233
|
const spanContext = span.spanContext();
|
|
234
|
+
let previousOutputChunkTime: number | undefined;
|
|
204
235
|
const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, "0");
|
|
236
|
+
|
|
205
237
|
const handle: SpanHandle = {
|
|
206
238
|
span,
|
|
207
239
|
context: trace.setSpan(parentCtx, span),
|
|
@@ -217,28 +249,69 @@ export function createSpanHandle(
|
|
|
217
249
|
} catch (error) {
|
|
218
250
|
reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
|
|
219
251
|
}
|
|
252
|
+
|
|
220
253
|
return handle;
|
|
221
254
|
},
|
|
255
|
+
recordOutputChunk(timestampMs) {
|
|
256
|
+
const now = timestampMs ?? performance.now();
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
!span.isRecording() ||
|
|
260
|
+
!Number.isFinite(now) ||
|
|
261
|
+
(previousOutputChunkTime !== undefined && now < previousOutputChunkTime)
|
|
262
|
+
)
|
|
263
|
+
return;
|
|
264
|
+
|
|
265
|
+
if (previousOutputChunkTime !== undefined) {
|
|
266
|
+
const seconds = Math.max(0, now - previousOutputChunkTime) / 1000;
|
|
267
|
+
|
|
268
|
+
const target = span as Span & {
|
|
269
|
+
[OUTPUT_CHUNK_HISTOGRAM]?: OutputChunkHistogram;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const histogram = (target[OUTPUT_CHUNK_HISTOGRAM] ??= {
|
|
273
|
+
count: 0,
|
|
274
|
+
sum: 0,
|
|
275
|
+
min: seconds,
|
|
276
|
+
max: seconds,
|
|
277
|
+
bucketCounts: Array.from({ length: DURATION_BUCKETS.length + 1 }, () => 0),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
histogram.count++;
|
|
281
|
+
histogram.sum += seconds;
|
|
282
|
+
histogram.min = Math.min(histogram.min, seconds);
|
|
283
|
+
histogram.max = Math.max(histogram.max, seconds);
|
|
284
|
+
const bucket = DURATION_BUCKETS.findIndex((boundary) => seconds <= boundary);
|
|
285
|
+
histogram.bucketCounts[bucket < 0 ? DURATION_BUCKETS.length : bucket]++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
previousOutputChunkTime = now;
|
|
289
|
+
},
|
|
222
290
|
end(fields) {
|
|
223
291
|
try {
|
|
224
292
|
if (fields) applyFields(span, meta, fields);
|
|
225
293
|
} catch (error) {
|
|
226
294
|
reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
|
|
227
295
|
}
|
|
296
|
+
|
|
228
297
|
span.end(fields?.endTime);
|
|
229
298
|
},
|
|
230
299
|
};
|
|
300
|
+
|
|
231
301
|
return handle;
|
|
232
302
|
}
|
|
233
303
|
|
|
234
304
|
/** Start a span WITHOUT activating context; the caller must call .end(). */
|
|
235
305
|
export function startSpan(name: string, options?: StartSpanOptions): SpanHandle {
|
|
236
306
|
const core = currentClient().core;
|
|
307
|
+
|
|
237
308
|
if (!core) return NOOP_SPAN_HANDLE;
|
|
309
|
+
|
|
238
310
|
try {
|
|
239
311
|
return createSpanHandle(core, name, options);
|
|
240
312
|
} catch (error) {
|
|
241
313
|
reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
|
|
314
|
+
|
|
242
315
|
return NOOP_SPAN_HANDLE;
|
|
243
316
|
}
|
|
244
317
|
}
|
|
@@ -246,10 +319,12 @@ export function startSpan(name: string, options?: StartSpanOptions): SpanHandle
|
|
|
246
319
|
export function runWithHandle<T>(handle: SpanHandle, fn: (span: SpanHandle) => T): T {
|
|
247
320
|
try {
|
|
248
321
|
const result = withContext(handle.context, () => fn(handle));
|
|
322
|
+
|
|
249
323
|
if (isThenable(result)) {
|
|
250
324
|
return result.then(
|
|
251
325
|
(value) => {
|
|
252
326
|
handle.end();
|
|
327
|
+
|
|
253
328
|
return value;
|
|
254
329
|
},
|
|
255
330
|
(error: unknown) => {
|
|
@@ -258,7 +333,9 @@ export function runWithHandle<T>(handle: SpanHandle, fn: (span: SpanHandle) => T
|
|
|
258
333
|
},
|
|
259
334
|
) as T;
|
|
260
335
|
}
|
|
336
|
+
|
|
261
337
|
handle.end();
|
|
338
|
+
|
|
262
339
|
return result;
|
|
263
340
|
} catch (error) {
|
|
264
341
|
handle.end({ error });
|
|
@@ -281,16 +358,21 @@ export function startActiveSpan<T>(
|
|
|
281
358
|
const fn = isFn ? optionsOrFn : maybeFn!;
|
|
282
359
|
const options = isFn ? undefined : optionsOrFn;
|
|
283
360
|
const handle = startSpan(name, options);
|
|
361
|
+
|
|
284
362
|
if (handle === NOOP_SPAN_HANDLE) return fn(handle);
|
|
363
|
+
|
|
285
364
|
return runWithHandle(handle, fn);
|
|
286
365
|
}
|
|
287
366
|
|
|
288
367
|
/** Update the innermost active span; no-op when none is recording. */
|
|
289
368
|
export function updateActiveSpan(fields: SpanFields): void {
|
|
290
369
|
const core = currentClient().core;
|
|
370
|
+
|
|
291
371
|
if (!core) return;
|
|
372
|
+
|
|
292
373
|
try {
|
|
293
374
|
const span = trace.getSpan(activeContext());
|
|
375
|
+
|
|
294
376
|
if (!span?.isRecording()) return;
|
|
295
377
|
const meta = SPAN_META.get(span) ?? metaFor(core);
|
|
296
378
|
applyFields(span, meta, fields);
|
|
@@ -300,6 +382,7 @@ export function updateActiveSpan(fields: SpanFields): void {
|
|
|
300
382
|
}
|
|
301
383
|
|
|
302
384
|
const TRACE_CONTEXT_PROPAGATOR = new W3CTraceContextPropagator();
|
|
385
|
+
|
|
303
386
|
const W3C_PROPAGATOR = new CompositePropagator({
|
|
304
387
|
propagators: [TRACE_CONTEXT_PROPAGATOR, new W3CBaggagePropagator()],
|
|
305
388
|
});
|
|
@@ -312,6 +395,7 @@ export function extractW3cContext(
|
|
|
312
395
|
options: { includeBaggage?: boolean } = {},
|
|
313
396
|
): Context {
|
|
314
397
|
const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
|
|
398
|
+
|
|
315
399
|
return propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
|
|
316
400
|
}
|
|
317
401
|
|
|
@@ -322,12 +406,17 @@ export function injectW3cContext(
|
|
|
322
406
|
options: { includeBaggage?: boolean } = {},
|
|
323
407
|
): void {
|
|
324
408
|
const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
|
|
409
|
+
|
|
325
410
|
for (const key of Object.keys(carrier)) {
|
|
326
411
|
const normalized = key.toLowerCase();
|
|
412
|
+
|
|
327
413
|
if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") {
|
|
328
|
-
|
|
414
|
+
if (!Reflect.deleteProperty(carrier, key)) {
|
|
415
|
+
throw new TypeError(`Cannot delete stale W3C carrier property: ${key}`);
|
|
416
|
+
}
|
|
329
417
|
}
|
|
330
418
|
}
|
|
419
|
+
|
|
331
420
|
propagator.inject(context, carrier, defaultTextMapSetter);
|
|
332
421
|
}
|
|
333
422
|
|
|
@@ -335,5 +424,6 @@ export function injectW3cContext(
|
|
|
335
424
|
export function getTraceparent(): string | null {
|
|
336
425
|
const carrier = { traceparent: undefined };
|
|
337
426
|
TRACE_CONTEXT_PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
|
|
427
|
+
|
|
338
428
|
return carrier.traceparent ?? null;
|
|
339
429
|
}
|