@telemetry-dev/sdk 0.1.3 → 0.1.6
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 +22 -2
- package/dist/index.d.mts +31 -1
- package/dist/index.mjs +173 -3
- package/package.json +3 -3
- package/src/attrs.ts +28 -0
- package/src/capture.ts +235 -0
- package/src/client.ts +36 -0
- package/src/config.ts +3 -0
- package/src/index.ts +22 -1
- package/src/logs.ts +13 -0
- package/src/noop.ts +1 -0
- package/src/observe.ts +11 -0
- package/src/span.ts +87 -1
package/README.md
CHANGED
|
@@ -77,7 +77,8 @@ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-at
|
|
|
77
77
|
- `observe(fn, options?)` — wrap a sync/async function: args → input, return → output, errors
|
|
78
78
|
captured and rethrown; activates context for nesting.
|
|
79
79
|
- `startSpan(name, options?)` — manual handle; does **not** activate context. `handle.update()`,
|
|
80
|
-
`handle.end()`, `handle.traceparent`, `handle.span`
|
|
80
|
+
`handle.end()`, `handle.recordOutputChunk(timestampMs?)`, `handle.traceparent`, `handle.span`
|
|
81
|
+
(raw OTel span).
|
|
81
82
|
- `startActiveSpan(name, options?, fn)` — callback-scoped; activates context, auto-ends.
|
|
82
83
|
- `updateActiveSpan(fields)` — update the innermost active span.
|
|
83
84
|
- `propagateAttributes({ userId, sessionId, metadata }, fn)` — stamps `user.id`,
|
|
@@ -89,16 +90,35 @@ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-at
|
|
|
89
90
|
context.
|
|
90
91
|
- `extractW3cContext(carrier, options?)` / `injectW3cContext(context, carrier, options?)` — extract
|
|
91
92
|
or inject W3C trace context. Baggage is omitted unless `includeBaggage` is true.
|
|
93
|
+
- `captureEnabled("input" | "output")` — report whether the active initialized client permits that
|
|
94
|
+
capture direction. Returns `false` when no client is initialized.
|
|
95
|
+
- `boundedCapture(value, options?)` — copy a JSON-compatible value within bounded work and retained
|
|
96
|
+
size. Defaults to 48 KiB, depth 32, and 1,000 traversal items. Returns `{ value, truncated }`;
|
|
97
|
+
`value` can be partial or `undefined` when a limit, cycle, getter, or skip rule prevents capture.
|
|
92
98
|
- `flush()` / `shutdown()` — export pending data; required before serverless freeze/exit.
|
|
93
99
|
|
|
94
100
|
Span types: `span` (default) | `generation` | `tool` | `agent` | `embedding` — mapped to
|
|
95
101
|
`gen_ai.operation.name` `function` / `chat` / `execute_tool` / `invoke_agent` / `embeddings`.
|
|
96
102
|
Generation fields (`model`, `provider`, `usage`, `costUsd`, sampling params, …) map to the OTel
|
|
97
|
-
GenAI semantic conventions
|
|
103
|
+
GenAI semantic conventions. An explicit `costUsd` takes precedence; otherwise, the server estimates
|
|
104
|
+
standard token cost when reported usage and matching model pricing are available. Missing pricing leaves cost
|
|
105
|
+
unavailable, not zero, and does not prevent tracing or token accounting.
|
|
98
106
|
|
|
99
107
|
Duration and token-usage histograms (`gen_ai.client.operation.duration`,
|
|
100
108
|
`gen_ai.client.token.usage`) are recorded automatically for generation/agent/embedding/tool spans.
|
|
101
109
|
|
|
110
|
+
Generation spans also emit `gen_ai.client.operation.time_to_first_chunk` when a valid
|
|
111
|
+
`timeToFirstChunkMs` is supplied. `handle.recordOutputChunk()` records each nonempty output chunk's
|
|
112
|
+
arrival; after the first chunk, intervals contribute to
|
|
113
|
+
`gen_ai.client.operation.time_per_output_chunk`. Both histograms use seconds and are emitted only
|
|
114
|
+
after the ended span passes `spanFilter`, including interrupted streams. Chunk timing uses bounded
|
|
115
|
+
histogram state and does not retain individual intervals. Pull-based timing includes consumer delay
|
|
116
|
+
between reads. An explicit timestamp uses the same monotonic millisecond clock for every chunk.
|
|
117
|
+
|
|
118
|
+
Provider integrations feature-detect chunk recording. With an older core SDK that lacks
|
|
119
|
+
`recordOutputChunk`, streams and existing tracing continue normally, but chunk-interval metrics are
|
|
120
|
+
unavailable. Upgrade the core SDK with provider integrations to enable the new metric.
|
|
121
|
+
|
|
102
122
|
## Serverless
|
|
103
123
|
|
|
104
124
|
Use `exportMode: "immediate"` and either `await flush()` before returning or pass the platform
|
package/dist/index.d.mts
CHANGED
|
@@ -59,6 +59,24 @@ interface TelemetryOptions {
|
|
|
59
59
|
sdkName?: string;
|
|
60
60
|
}
|
|
61
61
|
//#endregion
|
|
62
|
+
//#region src/capture.d.ts
|
|
63
|
+
interface BoundedCaptureOptions {
|
|
64
|
+
maxBytes?: number;
|
|
65
|
+
maxDepth?: number;
|
|
66
|
+
maxItems?: number;
|
|
67
|
+
skip?: (key: string, value: unknown, parent: unknown, path: readonly string[]) => boolean;
|
|
68
|
+
}
|
|
69
|
+
interface BoundedCaptureResult {
|
|
70
|
+
value: unknown;
|
|
71
|
+
truncated: boolean;
|
|
72
|
+
}
|
|
73
|
+
interface BoundedCaptureDetails extends BoundedCaptureResult {
|
|
74
|
+
bytes: number;
|
|
75
|
+
items: number;
|
|
76
|
+
}
|
|
77
|
+
declare function boundedCaptureDetails(input: unknown, options?: BoundedCaptureOptions): BoundedCaptureDetails;
|
|
78
|
+
declare function boundedCapture(input: unknown, options?: BoundedCaptureOptions): BoundedCaptureResult;
|
|
79
|
+
//#endregion
|
|
62
80
|
//#region src/attrs.d.ts
|
|
63
81
|
type SpanType = "span" | "generation" | "tool" | "agent" | "embedding";
|
|
64
82
|
interface TokenUsage {
|
|
@@ -68,6 +86,15 @@ interface TokenUsage {
|
|
|
68
86
|
cacheReadInputTokens?: number;
|
|
69
87
|
cacheCreationInputTokens?: number;
|
|
70
88
|
reasoningOutputTokens?: number;
|
|
89
|
+
textInputTokens?: number;
|
|
90
|
+
textOutputTokens?: number;
|
|
91
|
+
textCacheReadInputTokens?: number;
|
|
92
|
+
imageInputTokens?: number;
|
|
93
|
+
imageOutputTokens?: number;
|
|
94
|
+
imageCacheReadInputTokens?: number;
|
|
95
|
+
audioInputTokens?: number;
|
|
96
|
+
audioOutputTokens?: number;
|
|
97
|
+
audioCacheReadInputTokens?: number;
|
|
71
98
|
}
|
|
72
99
|
interface GenerationFields {
|
|
73
100
|
model?: string;
|
|
@@ -126,6 +153,7 @@ interface TelemetryClient {
|
|
|
126
153
|
/** Flush + teardown + unregister globals; the client becomes a no-op afterwards. */
|
|
127
154
|
shutdown(): Promise<void>;
|
|
128
155
|
}
|
|
156
|
+
declare function captureEnabled(kind: "input" | "output"): boolean;
|
|
129
157
|
declare function flush(): Promise<void>;
|
|
130
158
|
declare function shutdown(): Promise<void>;
|
|
131
159
|
declare function init(options?: TelemetryOptions, overrides?: ClientOverrides): TelemetryClient;
|
|
@@ -172,6 +200,8 @@ interface SpanHandle {
|
|
|
172
200
|
readonly traceparent: string | null;
|
|
173
201
|
readonly isRecording: boolean;
|
|
174
202
|
update(fields: SpanFields): SpanHandle;
|
|
203
|
+
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
204
|
+
recordOutputChunk(timestampMs?: number): void;
|
|
175
205
|
end(fields?: SpanFields & {
|
|
176
206
|
endTime?: Date | number;
|
|
177
207
|
}): void;
|
|
@@ -205,4 +235,4 @@ interface ObserveOptions extends StartSpanOptions {
|
|
|
205
235
|
*/
|
|
206
236
|
declare function observe<TThis, TArgs extends unknown[], TReturn>(fn: (this: TThis, ...args: TArgs) => TReturn, options?: ObserveOptions): (...args: TArgs) => TReturn;
|
|
207
237
|
//#endregion
|
|
208
|
-
export { type AgentFields, type BatchOptions, type ClientOverrides, type ExportMode, type GenerationFields, type LogLevel, type LogOptions, type MaskFn, type ObserveOptions, type ParentRef, type PropagatedAttributes, SCOPE_NAME, type SdkLogLevel, type SpanFields, type SpanHandle, type SpanType, type StartSpanOptions, type TelemetryClient, type TelemetryOptions, TelemetrySpanProcessor, type TelemetrySpanProcessorOptions, type TokenUsage, type ToolFields, activeContext, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
|
|
238
|
+
export { type AgentFields, type BatchOptions, type BoundedCaptureDetails, type BoundedCaptureOptions, type BoundedCaptureResult, type ClientOverrides, type ExportMode, type GenerationFields, type LogLevel, type LogOptions, type MaskFn, type ObserveOptions, type ParentRef, type PropagatedAttributes, SCOPE_NAME, type SdkLogLevel, type SpanFields, type SpanHandle, type SpanType, type StartSpanOptions, type TelemetryClient, type TelemetryOptions, TelemetrySpanProcessor, type TelemetrySpanProcessorOptions, type TokenUsage, type ToolFields, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ 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();
|
|
@@ -48,6 +48,10 @@ let activeClient;
|
|
|
48
48
|
function currentClient() {
|
|
49
49
|
return activeClient ?? NOOP_CLIENT;
|
|
50
50
|
}
|
|
51
|
+
function captureEnabled(kind) {
|
|
52
|
+
const config = currentClient().core?.config;
|
|
53
|
+
return kind === "input" ? config?.captureInput ?? false : config?.captureOutput ?? false;
|
|
54
|
+
}
|
|
51
55
|
function flush() {
|
|
52
56
|
return currentClient().flush();
|
|
53
57
|
}
|
|
@@ -186,6 +190,137 @@ function initInner(options, overrides) {
|
|
|
186
190
|
//#endregion
|
|
187
191
|
//#region src/capture.ts
|
|
188
192
|
const TRUNCATION_MARKER = "...[truncated]";
|
|
193
|
+
const jsonStringByteLength = (value, limit) => {
|
|
194
|
+
let bytes = 2;
|
|
195
|
+
for (let index = 0; index < value.length && bytes <= limit; index += 1) {
|
|
196
|
+
const code = value.charCodeAt(index);
|
|
197
|
+
if (code === 34 || code === 92 || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) bytes += 2;
|
|
198
|
+
else if (code < 32) bytes += 6;
|
|
199
|
+
else if (code < 128) bytes += 1;
|
|
200
|
+
else if (code < 2048) bytes += 2;
|
|
201
|
+
else if (code >= 55296 && code <= 56319) {
|
|
202
|
+
const next = value.charCodeAt(index + 1);
|
|
203
|
+
if (next >= 56320 && next <= 57343) {
|
|
204
|
+
bytes += 4;
|
|
205
|
+
index += 1;
|
|
206
|
+
} else bytes += 6;
|
|
207
|
+
} else if (code >= 56320 && code <= 57343) bytes += 6;
|
|
208
|
+
else bytes += 3;
|
|
209
|
+
}
|
|
210
|
+
return bytes;
|
|
211
|
+
};
|
|
212
|
+
const captureLimit = (name, value, fallback) => {
|
|
213
|
+
if (value === void 0) return fallback;
|
|
214
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
215
|
+
return value;
|
|
216
|
+
};
|
|
217
|
+
function boundedCaptureDetails(input, options = {}) {
|
|
218
|
+
const maxBytes = captureLimit("maxBytes", options.maxBytes, 48 * 1024);
|
|
219
|
+
const maxDepth = captureLimit("maxDepth", options.maxDepth, 32);
|
|
220
|
+
const maxItems = captureLimit("maxItems", options.maxItems, 1e3);
|
|
221
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
222
|
+
let bytes = 0;
|
|
223
|
+
let items = 0;
|
|
224
|
+
let truncated = false;
|
|
225
|
+
const visit = (value, depth, path) => {
|
|
226
|
+
if (items >= maxItems) {
|
|
227
|
+
truncated = true;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
items += 1;
|
|
231
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
232
|
+
const length = typeof value === "string" ? jsonStringByteLength(value, maxBytes - bytes) : JSON.stringify(value)?.length ?? 0;
|
|
233
|
+
if (bytes + length > maxBytes) {
|
|
234
|
+
truncated = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
bytes += length;
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
if (typeof value !== "object") return void 0;
|
|
241
|
+
if (depth >= maxDepth || ancestors.has(value) || bytes + 2 > maxBytes) {
|
|
242
|
+
truncated = true;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
ancestors.add(value);
|
|
246
|
+
bytes += 2;
|
|
247
|
+
try {
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
const result = [];
|
|
250
|
+
for (const item of value) {
|
|
251
|
+
if (items >= maxItems) {
|
|
252
|
+
truncated = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
const previousBytes = bytes;
|
|
256
|
+
if (result.length > 0) bytes += 1;
|
|
257
|
+
const converted = visit(item, depth + 1, path);
|
|
258
|
+
if (converted === void 0) bytes = previousBytes;
|
|
259
|
+
else result.push(converted);
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
const result = {};
|
|
264
|
+
let emitted = false;
|
|
265
|
+
for (const key in value) {
|
|
266
|
+
if (items >= maxItems) {
|
|
267
|
+
truncated = true;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
if (!Object.hasOwn(value, key)) {
|
|
271
|
+
items += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const keyBytes = jsonStringByteLength(key, maxBytes - bytes) + 1 + (emitted ? 1 : 0);
|
|
275
|
+
if (bytes + keyBytes > maxBytes) {
|
|
276
|
+
items += 1;
|
|
277
|
+
truncated = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
let item;
|
|
281
|
+
try {
|
|
282
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
283
|
+
item = descriptor?.get ? descriptor.get.call(value) : descriptor?.value;
|
|
284
|
+
if (options.skip?.(key, item, value, path)) {
|
|
285
|
+
items += 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
items += 1;
|
|
290
|
+
truncated = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const previousBytes = bytes;
|
|
294
|
+
bytes += keyBytes;
|
|
295
|
+
const converted = visit(item, depth + 1, [...path, key]);
|
|
296
|
+
if (converted === void 0) bytes = previousBytes;
|
|
297
|
+
else {
|
|
298
|
+
result[key] = converted;
|
|
299
|
+
emitted = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
} catch {
|
|
304
|
+
truncated = true;
|
|
305
|
+
return;
|
|
306
|
+
} finally {
|
|
307
|
+
ancestors.delete(value);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
value: visit(input, 0, []),
|
|
312
|
+
truncated,
|
|
313
|
+
bytes,
|
|
314
|
+
items
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function boundedCapture(input, options = {}) {
|
|
318
|
+
const { value, truncated } = boundedCaptureDetails(input, options);
|
|
319
|
+
return {
|
|
320
|
+
value,
|
|
321
|
+
truncated
|
|
322
|
+
};
|
|
323
|
+
}
|
|
189
324
|
function truncate(value, maxLength) {
|
|
190
325
|
if (value.length <= maxLength) return value;
|
|
191
326
|
return value.slice(0, Math.max(maxLength - 14, 0)) + TRUNCATION_MARKER;
|
|
@@ -308,6 +443,15 @@ function fieldsToAttributes(fields, type, cfg) {
|
|
|
308
443
|
"gen_ai.usage.cache_read.input_tokens": fields.usage?.cacheReadInputTokens,
|
|
309
444
|
"gen_ai.usage.cache_creation.input_tokens": fields.usage?.cacheCreationInputTokens,
|
|
310
445
|
"gen_ai.usage.reasoning.output_tokens": fields.usage?.reasoningOutputTokens,
|
|
446
|
+
"gen_ai.usage.text.input_tokens": fields.usage?.textInputTokens,
|
|
447
|
+
"gen_ai.usage.text.output_tokens": fields.usage?.textOutputTokens,
|
|
448
|
+
"gen_ai.usage.text.cache_read.input_tokens": fields.usage?.textCacheReadInputTokens,
|
|
449
|
+
"gen_ai.usage.image.input_tokens": fields.usage?.imageInputTokens,
|
|
450
|
+
"gen_ai.usage.image.output_tokens": fields.usage?.imageOutputTokens,
|
|
451
|
+
"gen_ai.usage.image.cache_read.input_tokens": fields.usage?.imageCacheReadInputTokens,
|
|
452
|
+
"gen_ai.usage.audio.input_tokens": fields.usage?.audioInputTokens,
|
|
453
|
+
"gen_ai.usage.audio.output_tokens": fields.usage?.audioOutputTokens,
|
|
454
|
+
"gen_ai.usage.audio.cache_read.input_tokens": fields.usage?.audioCacheReadInputTokens,
|
|
311
455
|
"gen_ai.usage.cost": fields.costUsd,
|
|
312
456
|
"gen_ai.response.finish_reasons": fields.finishReason ? [fields.finishReason] : void 0,
|
|
313
457
|
"gen_ai.output.type": fields.outputType,
|
|
@@ -363,6 +507,7 @@ const NOOP_SPAN_HANDLE = {
|
|
|
363
507
|
traceparent: null,
|
|
364
508
|
isRecording: false,
|
|
365
509
|
update: () => NOOP_SPAN_HANDLE,
|
|
510
|
+
recordOutputChunk: () => {},
|
|
366
511
|
end: () => {}
|
|
367
512
|
};
|
|
368
513
|
//#endregion
|
|
@@ -456,6 +601,7 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
456
601
|
if (options.name) span.updateName(options.name);
|
|
457
602
|
if (options.error !== void 0) applyError(span, options.error);
|
|
458
603
|
const spanContext = span.spanContext();
|
|
604
|
+
let previousOutputChunkTime;
|
|
459
605
|
const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
|
|
460
606
|
const handle = {
|
|
461
607
|
span,
|
|
@@ -474,6 +620,28 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
474
620
|
}
|
|
475
621
|
return handle;
|
|
476
622
|
},
|
|
623
|
+
recordOutputChunk(timestampMs) {
|
|
624
|
+
const now = timestampMs ?? performance.now();
|
|
625
|
+
if (!span.isRecording() || !Number.isFinite(now) || previousOutputChunkTime !== void 0 && now < previousOutputChunkTime) return;
|
|
626
|
+
if (previousOutputChunkTime !== void 0) {
|
|
627
|
+
const seconds = Math.max(0, now - previousOutputChunkTime) / 1e3;
|
|
628
|
+
const target = span;
|
|
629
|
+
const histogram = target[OUTPUT_CHUNK_HISTOGRAM] ??= {
|
|
630
|
+
count: 0,
|
|
631
|
+
sum: 0,
|
|
632
|
+
min: seconds,
|
|
633
|
+
max: seconds,
|
|
634
|
+
bucketCounts: Array.from({ length: DURATION_BUCKETS.length + 1 }, () => 0)
|
|
635
|
+
};
|
|
636
|
+
histogram.count++;
|
|
637
|
+
histogram.sum += seconds;
|
|
638
|
+
histogram.min = Math.min(histogram.min, seconds);
|
|
639
|
+
histogram.max = Math.max(histogram.max, seconds);
|
|
640
|
+
const bucket = DURATION_BUCKETS.findIndex((boundary) => seconds <= boundary);
|
|
641
|
+
histogram.bucketCounts[bucket < 0 ? DURATION_BUCKETS.length : bucket]++;
|
|
642
|
+
}
|
|
643
|
+
previousOutputChunkTime = now;
|
|
644
|
+
},
|
|
477
645
|
end(fields) {
|
|
478
646
|
try {
|
|
479
647
|
if (fields) applyFields(span, meta, fields);
|
|
@@ -543,7 +711,9 @@ function injectW3cContext(context, carrier, options = {}) {
|
|
|
543
711
|
const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
|
|
544
712
|
for (const key of Object.keys(carrier)) {
|
|
545
713
|
const normalized = key.toLowerCase();
|
|
546
|
-
if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage")
|
|
714
|
+
if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") {
|
|
715
|
+
if (!Reflect.deleteProperty(carrier, key)) throw new TypeError(`Cannot delete stale W3C carrier property: ${key}`);
|
|
716
|
+
}
|
|
547
717
|
}
|
|
548
718
|
propagator.inject(context, carrier, defaultTextMapSetter);
|
|
549
719
|
}
|
|
@@ -599,4 +769,4 @@ function observe(fn, options) {
|
|
|
599
769
|
return wrapped;
|
|
600
770
|
}
|
|
601
771
|
//#endregion
|
|
602
|
-
export { SCOPE_NAME, TelemetrySpanProcessor, activeContext, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
|
|
772
|
+
export { SCOPE_NAME, TelemetrySpanProcessor, activeContext, boundedCapture, boundedCaptureDetails, captureEnabled, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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/
|
|
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
|
@@ -32,6 +32,15 @@ export interface TokenUsage {
|
|
|
32
32
|
cacheReadInputTokens?: number;
|
|
33
33
|
cacheCreationInputTokens?: number;
|
|
34
34
|
reasoningOutputTokens?: number;
|
|
35
|
+
textInputTokens?: number;
|
|
36
|
+
textOutputTokens?: number;
|
|
37
|
+
textCacheReadInputTokens?: number;
|
|
38
|
+
imageInputTokens?: number;
|
|
39
|
+
imageOutputTokens?: number;
|
|
40
|
+
imageCacheReadInputTokens?: number;
|
|
41
|
+
audioInputTokens?: number;
|
|
42
|
+
audioOutputTokens?: number;
|
|
43
|
+
audioCacheReadInputTokens?: number;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export interface GenerationFields {
|
|
@@ -103,6 +112,15 @@ export function fieldsToAttributes(
|
|
|
103
112
|
"gen_ai.usage.cache_read.input_tokens": fields.usage?.cacheReadInputTokens,
|
|
104
113
|
"gen_ai.usage.cache_creation.input_tokens": fields.usage?.cacheCreationInputTokens,
|
|
105
114
|
"gen_ai.usage.reasoning.output_tokens": fields.usage?.reasoningOutputTokens,
|
|
115
|
+
"gen_ai.usage.text.input_tokens": fields.usage?.textInputTokens,
|
|
116
|
+
"gen_ai.usage.text.output_tokens": fields.usage?.textOutputTokens,
|
|
117
|
+
"gen_ai.usage.text.cache_read.input_tokens": fields.usage?.textCacheReadInputTokens,
|
|
118
|
+
"gen_ai.usage.image.input_tokens": fields.usage?.imageInputTokens,
|
|
119
|
+
"gen_ai.usage.image.output_tokens": fields.usage?.imageOutputTokens,
|
|
120
|
+
"gen_ai.usage.image.cache_read.input_tokens": fields.usage?.imageCacheReadInputTokens,
|
|
121
|
+
"gen_ai.usage.audio.input_tokens": fields.usage?.audioInputTokens,
|
|
122
|
+
"gen_ai.usage.audio.output_tokens": fields.usage?.audioOutputTokens,
|
|
123
|
+
"gen_ai.usage.audio.cache_read.input_tokens": fields.usage?.audioCacheReadInputTokens,
|
|
106
124
|
"gen_ai.usage.cost": fields.costUsd,
|
|
107
125
|
"gen_ai.response.finish_reasons": fields.finishReason ? [fields.finishReason] : undefined,
|
|
108
126
|
"gen_ai.output.type": fields.outputType,
|
|
@@ -126,32 +144,42 @@ export function fieldsToAttributes(
|
|
|
126
144
|
|
|
127
145
|
if (fields.systemInstructions !== undefined && cfg.captureInput) {
|
|
128
146
|
const value = prepareCaptureValue("gen_ai.system_instructions", fields.systemInstructions, cfg);
|
|
147
|
+
|
|
129
148
|
if (value !== undefined) attrs["gen_ai.system_instructions"] = value;
|
|
130
149
|
}
|
|
150
|
+
|
|
131
151
|
if (fields.input !== undefined && cfg.captureInput) {
|
|
132
152
|
const key = inputKeyFor(type);
|
|
133
153
|
const value = prepareCaptureValue(key, fields.input, cfg);
|
|
154
|
+
|
|
134
155
|
if (value !== undefined) attrs[key] = value;
|
|
135
156
|
}
|
|
157
|
+
|
|
136
158
|
if (fields.output !== undefined && cfg.captureOutput) {
|
|
137
159
|
const key = outputKeyFor(type);
|
|
138
160
|
const value = prepareCaptureValue(key, fields.output, cfg);
|
|
161
|
+
|
|
139
162
|
if (value !== undefined) attrs[key] = value;
|
|
140
163
|
}
|
|
164
|
+
|
|
141
165
|
if (fields.metadata) {
|
|
142
166
|
for (const [key, value] of Object.entries(fields.metadata)) {
|
|
143
167
|
if (RESERVED_METADATA_KEYS.has(key)) {
|
|
144
168
|
diag.debug(`metadata key "${key}" is reserved; pass it via propagateAttributes instead`);
|
|
145
169
|
continue;
|
|
146
170
|
}
|
|
171
|
+
|
|
147
172
|
const attr = typeof value === "string" ? value : jsonAttr(value);
|
|
173
|
+
|
|
148
174
|
if (attr !== undefined) {
|
|
149
175
|
attrs[`td.metadata.${key}`] = truncate(attr, cfg.maxAttributeLength);
|
|
150
176
|
}
|
|
151
177
|
}
|
|
152
178
|
}
|
|
179
|
+
|
|
153
180
|
if (fields.attributes) {
|
|
154
181
|
Object.assign(attrs, omitUndefined(fields.attributes));
|
|
155
182
|
}
|
|
183
|
+
|
|
156
184
|
return attrs;
|
|
157
185
|
}
|
package/src/capture.ts
CHANGED
|
@@ -11,8 +11,238 @@ export interface CaptureConfig {
|
|
|
11
11
|
onError?: (cause: Error) => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface BoundedCaptureOptions {
|
|
15
|
+
maxBytes?: number;
|
|
16
|
+
maxDepth?: number;
|
|
17
|
+
maxItems?: number;
|
|
18
|
+
skip?: (key: string, value: unknown, parent: unknown, path: readonly string[]) => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BoundedCaptureResult {
|
|
22
|
+
value: unknown;
|
|
23
|
+
truncated: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BoundedCaptureDetails extends BoundedCaptureResult {
|
|
27
|
+
bytes: number;
|
|
28
|
+
items: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface CapturedObject {
|
|
32
|
+
[key: string]: CapturedValue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type CapturedValue = null | boolean | number | string | CapturedValue[] | CapturedObject;
|
|
36
|
+
|
|
37
|
+
const jsonStringByteLength = (value: string, limit: number) => {
|
|
38
|
+
let bytes = 2;
|
|
39
|
+
|
|
40
|
+
for (let index = 0; index < value.length && bytes <= limit; index += 1) {
|
|
41
|
+
const code = value.charCodeAt(index);
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
code === 0x22 ||
|
|
45
|
+
code === 0x5c ||
|
|
46
|
+
code === 0x08 ||
|
|
47
|
+
code === 0x09 ||
|
|
48
|
+
code === 0x0a ||
|
|
49
|
+
code === 0x0c ||
|
|
50
|
+
code === 0x0d
|
|
51
|
+
) {
|
|
52
|
+
bytes += 2;
|
|
53
|
+
} else if (code < 0x20) {
|
|
54
|
+
bytes += 6;
|
|
55
|
+
} else if (code < 0x80) {
|
|
56
|
+
bytes += 1;
|
|
57
|
+
} else if (code < 0x800) {
|
|
58
|
+
bytes += 2;
|
|
59
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
60
|
+
const next = value.charCodeAt(index + 1);
|
|
61
|
+
|
|
62
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
63
|
+
bytes += 4;
|
|
64
|
+
index += 1;
|
|
65
|
+
} else {
|
|
66
|
+
bytes += 6;
|
|
67
|
+
}
|
|
68
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
69
|
+
bytes += 6;
|
|
70
|
+
} else {
|
|
71
|
+
bytes += 3;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return bytes;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const captureLimit = (name: string, value: number | undefined, fallback: number) => {
|
|
79
|
+
if (value === undefined) return fallback;
|
|
80
|
+
|
|
81
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
82
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return value;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function boundedCaptureDetails(
|
|
89
|
+
input: unknown,
|
|
90
|
+
options: BoundedCaptureOptions = {},
|
|
91
|
+
): BoundedCaptureDetails {
|
|
92
|
+
const maxBytes = captureLimit("maxBytes", options.maxBytes, 48 * 1024);
|
|
93
|
+
const maxDepth = captureLimit("maxDepth", options.maxDepth, 32);
|
|
94
|
+
const maxItems = captureLimit("maxItems", options.maxItems, 1_000);
|
|
95
|
+
const ancestors = new WeakSet<object>();
|
|
96
|
+
let bytes = 0;
|
|
97
|
+
let items = 0;
|
|
98
|
+
let truncated = false;
|
|
99
|
+
|
|
100
|
+
const visit = (
|
|
101
|
+
value: unknown,
|
|
102
|
+
depth: number,
|
|
103
|
+
path: readonly string[],
|
|
104
|
+
): CapturedValue | undefined => {
|
|
105
|
+
if (items >= maxItems) {
|
|
106
|
+
truncated = true;
|
|
107
|
+
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
items += 1;
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
value === null ||
|
|
115
|
+
typeof value === "boolean" ||
|
|
116
|
+
typeof value === "number" ||
|
|
117
|
+
typeof value === "string"
|
|
118
|
+
) {
|
|
119
|
+
const length =
|
|
120
|
+
typeof value === "string"
|
|
121
|
+
? jsonStringByteLength(value, maxBytes - bytes)
|
|
122
|
+
: (JSON.stringify(value)?.length ?? 0);
|
|
123
|
+
|
|
124
|
+
if (bytes + length > maxBytes) {
|
|
125
|
+
truncated = true;
|
|
126
|
+
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
bytes += length;
|
|
131
|
+
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof value !== "object") return undefined;
|
|
136
|
+
|
|
137
|
+
if (depth >= maxDepth || ancestors.has(value) || bytes + 2 > maxBytes) {
|
|
138
|
+
truncated = true;
|
|
139
|
+
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
ancestors.add(value);
|
|
144
|
+
bytes += 2;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
const result: CapturedValue[] = [];
|
|
149
|
+
|
|
150
|
+
for (const item of value) {
|
|
151
|
+
if (items >= maxItems) {
|
|
152
|
+
truncated = true;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const previousBytes = bytes;
|
|
157
|
+
|
|
158
|
+
if (result.length > 0) bytes += 1;
|
|
159
|
+
const converted = visit(item, depth + 1, path);
|
|
160
|
+
|
|
161
|
+
if (converted === undefined) {
|
|
162
|
+
bytes = previousBytes;
|
|
163
|
+
} else {
|
|
164
|
+
result.push(converted);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result: CapturedObject = {};
|
|
172
|
+
let emitted = false;
|
|
173
|
+
|
|
174
|
+
for (const key in value) {
|
|
175
|
+
if (items >= maxItems) {
|
|
176
|
+
truncated = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!Object.hasOwn(value, key)) {
|
|
181
|
+
items += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const keyBytes = jsonStringByteLength(key, maxBytes - bytes) + 1 + (emitted ? 1 : 0);
|
|
186
|
+
|
|
187
|
+
if (bytes + keyBytes > maxBytes) {
|
|
188
|
+
items += 1;
|
|
189
|
+
truncated = true;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let item: unknown;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
197
|
+
item = descriptor?.get ? descriptor.get.call(value) : descriptor?.value;
|
|
198
|
+
|
|
199
|
+
if (options.skip?.(key, item, value, path)) {
|
|
200
|
+
items += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
items += 1;
|
|
205
|
+
truncated = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const previousBytes = bytes;
|
|
210
|
+
bytes += keyBytes;
|
|
211
|
+
const converted = visit(item, depth + 1, [...path, key]);
|
|
212
|
+
|
|
213
|
+
if (converted === undefined) {
|
|
214
|
+
bytes = previousBytes;
|
|
215
|
+
} else {
|
|
216
|
+
result[key] = converted;
|
|
217
|
+
emitted = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
} catch {
|
|
223
|
+
truncated = true;
|
|
224
|
+
|
|
225
|
+
return undefined;
|
|
226
|
+
} finally {
|
|
227
|
+
ancestors.delete(value);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return { value: visit(input, 0, []), truncated, bytes, items };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function boundedCapture(
|
|
235
|
+
input: unknown,
|
|
236
|
+
options: BoundedCaptureOptions = {},
|
|
237
|
+
): BoundedCaptureResult {
|
|
238
|
+
const { value, truncated } = boundedCaptureDetails(input, options);
|
|
239
|
+
|
|
240
|
+
return { value, truncated };
|
|
241
|
+
}
|
|
242
|
+
|
|
14
243
|
export function truncate(value: string, maxLength: number): string {
|
|
15
244
|
if (value.length <= maxLength) return value;
|
|
245
|
+
|
|
16
246
|
// Total stays within maxLength so the provider's attributeValueLengthLimit backstop
|
|
17
247
|
// (set to the same cap) never slices the marker off.
|
|
18
248
|
return value.slice(0, Math.max(maxLength - TRUNCATION_MARKER.length, 0)) + TRUNCATION_MARKER;
|
|
@@ -25,15 +255,20 @@ export function prepareCaptureValue(
|
|
|
25
255
|
cfg: CaptureConfig,
|
|
26
256
|
): string | undefined {
|
|
27
257
|
let masked = value;
|
|
258
|
+
|
|
28
259
|
if (cfg.mask) {
|
|
29
260
|
try {
|
|
30
261
|
masked = cfg.mask(value, { key });
|
|
31
262
|
} catch (error) {
|
|
32
263
|
reportError(cfg.onError, error instanceof Error ? error : new Error(String(error)));
|
|
264
|
+
|
|
33
265
|
return undefined;
|
|
34
266
|
}
|
|
35
267
|
}
|
|
268
|
+
|
|
36
269
|
const serialized = jsonAttr(masked);
|
|
270
|
+
|
|
37
271
|
if (serialized === undefined) return undefined;
|
|
272
|
+
|
|
38
273
|
return truncate(serialized, cfg.maxAttributeLength);
|
|
39
274
|
}
|
package/src/client.ts
CHANGED
|
@@ -75,6 +75,12 @@ export function currentClient(): ClientHandle {
|
|
|
75
75
|
return activeClient ?? NOOP_CLIENT;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export function captureEnabled(kind: "input" | "output"): boolean {
|
|
79
|
+
const config = currentClient().core?.config;
|
|
80
|
+
|
|
81
|
+
return kind === "input" ? (config?.captureInput ?? false) : (config?.captureOutput ?? false);
|
|
82
|
+
}
|
|
83
|
+
|
|
78
84
|
export function flush(): Promise<void> {
|
|
79
85
|
return currentClient().flush();
|
|
80
86
|
}
|
|
@@ -89,6 +95,7 @@ export function init(options?: TelemetryOptions, overrides?: ClientOverrides): T
|
|
|
89
95
|
} catch (error) {
|
|
90
96
|
reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
|
|
91
97
|
activeClient = NOOP_CLIENT;
|
|
98
|
+
|
|
92
99
|
return NOOP_CLIENT;
|
|
93
100
|
}
|
|
94
101
|
}
|
|
@@ -104,11 +111,14 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
const enabled = config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter);
|
|
114
|
+
|
|
107
115
|
if (!enabled) {
|
|
108
116
|
if (config.enabled && !config.apiKey) {
|
|
109
117
|
diag.debug("no api key (apiKey option or TELEMETRY_DEV_API_KEY); telemetry disabled");
|
|
110
118
|
}
|
|
119
|
+
|
|
111
120
|
activeClient = NOOP_CLIENT;
|
|
121
|
+
|
|
112
122
|
return NOOP_CLIENT;
|
|
113
123
|
}
|
|
114
124
|
|
|
@@ -119,6 +129,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
119
129
|
"deployment.environment.name": config.environment,
|
|
120
130
|
...config.resourceAttributes,
|
|
121
131
|
});
|
|
132
|
+
|
|
122
133
|
const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError };
|
|
123
134
|
const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
|
|
124
135
|
|
|
@@ -131,6 +142,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
131
142
|
(headers
|
|
132
143
|
? createMetricExporter({ url: `${config.baseUrl}/v1/metrics`, headers }, transport)
|
|
133
144
|
: undefined);
|
|
145
|
+
|
|
134
146
|
const metrics = metricExporter
|
|
135
147
|
? createMetricsPipeline({
|
|
136
148
|
resource,
|
|
@@ -161,21 +173,26 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
161
173
|
spanProcessors: [processor],
|
|
162
174
|
spanLimits: { attributeValueLengthLimit: config.maxAttributeLength },
|
|
163
175
|
});
|
|
176
|
+
|
|
164
177
|
const tracer = provider.getTracer(SCOPE_NAME, SCOPE_VERSION);
|
|
165
178
|
|
|
166
179
|
let registeredTrace = false;
|
|
167
180
|
let registeredContext = false;
|
|
168
181
|
let registeredPropagation = false;
|
|
182
|
+
|
|
169
183
|
if (config.registerGlobal) {
|
|
170
184
|
registeredTrace = trace.setGlobalTracerProvider(
|
|
171
185
|
config.sessionRootOf
|
|
172
186
|
? sessionRootTracerProvider(provider, config.apiKey, config.sessionRootOf, config.onError)
|
|
173
187
|
: provider,
|
|
174
188
|
);
|
|
189
|
+
|
|
175
190
|
if (als) {
|
|
176
191
|
registeredContext = apiContext.setGlobalContextManager(new AlsContextManager(als));
|
|
177
192
|
}
|
|
193
|
+
|
|
178
194
|
registeredPropagation = propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
195
|
+
|
|
179
196
|
if (!registeredTrace) {
|
|
180
197
|
diag.warn(
|
|
181
198
|
"a global TracerProvider is already registered; attach the TelemetrySpanProcessor from '@telemetry-dev/otel' to your own provider instead",
|
|
@@ -188,43 +205,56 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
188
205
|
(headers
|
|
189
206
|
? createLogExporter({ url: `${config.baseUrl}/v1/logs`, headers }, transport)
|
|
190
207
|
: undefined);
|
|
208
|
+
|
|
191
209
|
// Lazy: the log pipeline only exists once log() is first called.
|
|
192
210
|
let logs: { logger: Logger; forceFlush(): Promise<void>; shutdown(): Promise<void> } | undefined;
|
|
211
|
+
|
|
193
212
|
const ensureLogger = (): Logger | undefined => {
|
|
194
213
|
if (!logExporter) return undefined;
|
|
214
|
+
|
|
195
215
|
if (!logs) {
|
|
196
216
|
const logProcessor =
|
|
197
217
|
config.exportMode === "immediate"
|
|
198
218
|
? new SimpleLogRecordProcessor(logExporter)
|
|
199
219
|
: new BatchLogRecordProcessor(logExporter);
|
|
220
|
+
|
|
200
221
|
const loggerProvider = new LoggerProvider({
|
|
201
222
|
resource,
|
|
202
223
|
logRecordLimits: { attributeValueLengthLimit: config.maxAttributeLength },
|
|
203
224
|
processors: [logProcessor],
|
|
204
225
|
});
|
|
226
|
+
|
|
205
227
|
logs = {
|
|
206
228
|
logger: loggerProvider.getLogger(SCOPE_NAME, SCOPE_VERSION),
|
|
207
229
|
forceFlush: () => loggerProvider.forceFlush(),
|
|
208
230
|
shutdown: () => loggerProvider.shutdown(),
|
|
209
231
|
};
|
|
210
232
|
}
|
|
233
|
+
|
|
211
234
|
return logs.logger;
|
|
212
235
|
};
|
|
213
236
|
|
|
214
237
|
let torn = false;
|
|
238
|
+
|
|
215
239
|
const collect = (op: "forceFlush" | "shutdown"): Promise<void> => {
|
|
216
240
|
const parts: Promise<void>[] = [provider[op]()];
|
|
241
|
+
|
|
217
242
|
if (logs) parts.push(logs[op]());
|
|
243
|
+
|
|
218
244
|
if (metrics) parts.push(metrics[op]());
|
|
245
|
+
|
|
219
246
|
return Promise.all(parts)
|
|
220
247
|
.then(() => undefined)
|
|
221
248
|
.catch((error: Error) => reportError(config.onError, error));
|
|
222
249
|
};
|
|
250
|
+
|
|
223
251
|
const handoff = (p: Promise<void>): Promise<void> => {
|
|
224
252
|
if (config.waitUntil) {
|
|
225
253
|
config.waitUntil(p);
|
|
254
|
+
|
|
226
255
|
return Promise.resolve();
|
|
227
256
|
}
|
|
257
|
+
|
|
228
258
|
return p;
|
|
229
259
|
};
|
|
230
260
|
|
|
@@ -236,14 +266,20 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
236
266
|
shutdown: () => {
|
|
237
267
|
if (torn) return Promise.resolve();
|
|
238
268
|
torn = true;
|
|
269
|
+
|
|
239
270
|
if (registeredTrace) trace.disable();
|
|
271
|
+
|
|
240
272
|
if (registeredContext) apiContext.disable();
|
|
273
|
+
|
|
241
274
|
if (registeredPropagation) propagation.disable();
|
|
242
275
|
handle.core = undefined;
|
|
276
|
+
|
|
243
277
|
return handoff(collect("shutdown"));
|
|
244
278
|
},
|
|
245
279
|
};
|
|
280
|
+
|
|
246
281
|
handle.core = { config, tracer, ensureLogger };
|
|
247
282
|
activeClient = handle;
|
|
283
|
+
|
|
248
284
|
return handle;
|
|
249
285
|
}
|
package/src/config.ts
CHANGED
|
@@ -95,6 +95,7 @@ export const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
|
|
|
95
95
|
|
|
96
96
|
export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
|
|
97
97
|
const env = resolveEnv();
|
|
98
|
+
|
|
98
99
|
if (
|
|
99
100
|
options.sessionMode !== undefined &&
|
|
100
101
|
options.sessionMode !== "explicit" &&
|
|
@@ -102,10 +103,12 @@ export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
|
|
|
102
103
|
) {
|
|
103
104
|
throw new TypeError(`Invalid sessionMode: ${String(options.sessionMode)}`);
|
|
104
105
|
}
|
|
106
|
+
|
|
105
107
|
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(
|
|
106
108
|
/\/+$/,
|
|
107
109
|
"",
|
|
108
110
|
);
|
|
111
|
+
|
|
109
112
|
return {
|
|
110
113
|
apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
|
|
111
114
|
baseUrl,
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,16 @@ export type {
|
|
|
6
6
|
TokenUsage,
|
|
7
7
|
ToolFields,
|
|
8
8
|
} from "./attrs.ts";
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
captureEnabled,
|
|
12
|
+
type ClientOverrides,
|
|
13
|
+
flush,
|
|
14
|
+
init,
|
|
15
|
+
shutdown,
|
|
16
|
+
type TelemetryClient,
|
|
17
|
+
} from "./client.ts";
|
|
18
|
+
|
|
10
19
|
export type {
|
|
11
20
|
BatchOptions,
|
|
12
21
|
ExportMode,
|
|
@@ -15,6 +24,15 @@ export type {
|
|
|
15
24
|
SdkLogLevel,
|
|
16
25
|
TelemetryOptions,
|
|
17
26
|
} from "./config.ts";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
boundedCapture,
|
|
30
|
+
boundedCaptureDetails,
|
|
31
|
+
type BoundedCaptureDetails,
|
|
32
|
+
type BoundedCaptureOptions,
|
|
33
|
+
type BoundedCaptureResult,
|
|
34
|
+
} from "./capture.ts";
|
|
35
|
+
|
|
18
36
|
export {
|
|
19
37
|
propagateAttributes,
|
|
20
38
|
type PropagatedAttributes,
|
|
@@ -22,8 +40,11 @@ export {
|
|
|
22
40
|
TelemetrySpanProcessor,
|
|
23
41
|
type TelemetrySpanProcessorOptions,
|
|
24
42
|
} from "@telemetry-dev/otel";
|
|
43
|
+
|
|
25
44
|
export { log, type LogOptions } from "./logs.ts";
|
|
45
|
+
|
|
26
46
|
export { observe, type ObserveOptions } from "./observe.ts";
|
|
47
|
+
|
|
27
48
|
export {
|
|
28
49
|
activeContext,
|
|
29
50
|
extractW3cContext,
|
package/src/logs.ts
CHANGED
|
@@ -34,26 +34,33 @@ export interface LogOptions {
|
|
|
34
34
|
*/
|
|
35
35
|
export function log(message: string, options?: LogOptions): void {
|
|
36
36
|
const core = currentClient().core;
|
|
37
|
+
|
|
37
38
|
if (!core) return;
|
|
39
|
+
|
|
38
40
|
try {
|
|
39
41
|
const logger = core.ensureLogger();
|
|
42
|
+
|
|
40
43
|
if (!logger) return;
|
|
41
44
|
const cfg = core.config;
|
|
45
|
+
|
|
42
46
|
const captureCfg = {
|
|
43
47
|
mask: cfg.mask,
|
|
44
48
|
maxAttributeLength: cfg.maxAttributeLength,
|
|
45
49
|
onError: cfg.onError,
|
|
46
50
|
};
|
|
51
|
+
|
|
47
52
|
const severity = SEVERITIES[options?.level ?? "info"];
|
|
48
53
|
const active = activeContext();
|
|
49
54
|
const explicitSession = options?.attributes?.["gen_ai.conversation.id"];
|
|
50
55
|
const propagatedSession = propagatedFromContext(active)?.["gen_ai.conversation.id"];
|
|
56
|
+
|
|
51
57
|
const sessionId =
|
|
52
58
|
explicitSession !== undefined
|
|
53
59
|
? undefined
|
|
54
60
|
: typeof propagatedSession === "string"
|
|
55
61
|
? propagatedSession
|
|
56
62
|
: cfg.processSessionId;
|
|
63
|
+
|
|
57
64
|
const ctx =
|
|
58
65
|
sessionId === undefined
|
|
59
66
|
? active
|
|
@@ -63,16 +70,22 @@ export function log(message: string, options?: LogOptions): void {
|
|
|
63
70
|
});
|
|
64
71
|
|
|
65
72
|
const attributes: Record<string, AttributeValue> = {};
|
|
73
|
+
|
|
66
74
|
for (const [key, value] of Object.entries(options?.attributes ?? {})) {
|
|
67
75
|
if (value === undefined) continue;
|
|
76
|
+
|
|
68
77
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
69
78
|
attributes[key] = value;
|
|
70
79
|
continue;
|
|
71
80
|
}
|
|
81
|
+
|
|
72
82
|
const prepared = prepareCaptureValue(key, value, captureCfg);
|
|
83
|
+
|
|
73
84
|
if (prepared !== undefined) attributes[key] = prepared;
|
|
74
85
|
}
|
|
86
|
+
|
|
75
87
|
const propagated = propagatedFromContext(ctx);
|
|
88
|
+
|
|
76
89
|
if (propagated) {
|
|
77
90
|
for (const [key, value] of Object.entries(propagated)) {
|
|
78
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: {
|
|
@@ -175,6 +194,7 @@ export function createSpanHandle(
|
|
|
175
194
|
const meta = metaFor(core, options);
|
|
176
195
|
const resolved = resolveParent(options.parent);
|
|
177
196
|
const sessionId = sessionIdOf(resolved, options.attributes) ?? core.config.processSessionId;
|
|
197
|
+
|
|
178
198
|
const sessionContext =
|
|
179
199
|
sessionId === undefined
|
|
180
200
|
? resolved
|
|
@@ -182,8 +202,10 @@ export function createSpanHandle(
|
|
|
182
202
|
...propagatedFromContext(resolved),
|
|
183
203
|
"gen_ai.conversation.id": sessionId,
|
|
184
204
|
});
|
|
205
|
+
|
|
185
206
|
const parentCtx = withSessionParent(sessionContext, sessionId, core.config.apiKey);
|
|
186
207
|
const attrs = fieldsToAttributes(options, meta.type, meta.capture);
|
|
208
|
+
|
|
187
209
|
const span = core.tracer.startSpan(
|
|
188
210
|
name,
|
|
189
211
|
{
|
|
@@ -198,14 +220,20 @@ export function createSpanHandle(
|
|
|
198
220
|
},
|
|
199
221
|
parentCtx,
|
|
200
222
|
);
|
|
223
|
+
|
|
201
224
|
SPAN_META.set(span, meta);
|
|
225
|
+
|
|
202
226
|
// The processor stamps propagation on start; explicit fields still win.
|
|
203
227
|
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
228
|
+
|
|
204
229
|
if (options.name) span.updateName(options.name);
|
|
230
|
+
|
|
205
231
|
if (options.error !== undefined) applyError(span, options.error);
|
|
206
232
|
|
|
207
233
|
const spanContext = span.spanContext();
|
|
234
|
+
let previousOutputChunkTime: number | undefined;
|
|
208
235
|
const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, "0");
|
|
236
|
+
|
|
209
237
|
const handle: SpanHandle = {
|
|
210
238
|
span,
|
|
211
239
|
context: trace.setSpan(parentCtx, span),
|
|
@@ -221,28 +249,69 @@ export function createSpanHandle(
|
|
|
221
249
|
} catch (error) {
|
|
222
250
|
reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
|
|
223
251
|
}
|
|
252
|
+
|
|
224
253
|
return handle;
|
|
225
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
|
+
},
|
|
226
290
|
end(fields) {
|
|
227
291
|
try {
|
|
228
292
|
if (fields) applyFields(span, meta, fields);
|
|
229
293
|
} catch (error) {
|
|
230
294
|
reportError(meta.onError, error instanceof Error ? error : new Error(String(error)));
|
|
231
295
|
}
|
|
296
|
+
|
|
232
297
|
span.end(fields?.endTime);
|
|
233
298
|
},
|
|
234
299
|
};
|
|
300
|
+
|
|
235
301
|
return handle;
|
|
236
302
|
}
|
|
237
303
|
|
|
238
304
|
/** Start a span WITHOUT activating context; the caller must call .end(). */
|
|
239
305
|
export function startSpan(name: string, options?: StartSpanOptions): SpanHandle {
|
|
240
306
|
const core = currentClient().core;
|
|
307
|
+
|
|
241
308
|
if (!core) return NOOP_SPAN_HANDLE;
|
|
309
|
+
|
|
242
310
|
try {
|
|
243
311
|
return createSpanHandle(core, name, options);
|
|
244
312
|
} catch (error) {
|
|
245
313
|
reportError(core.config.onError, error instanceof Error ? error : new Error(String(error)));
|
|
314
|
+
|
|
246
315
|
return NOOP_SPAN_HANDLE;
|
|
247
316
|
}
|
|
248
317
|
}
|
|
@@ -250,10 +319,12 @@ export function startSpan(name: string, options?: StartSpanOptions): SpanHandle
|
|
|
250
319
|
export function runWithHandle<T>(handle: SpanHandle, fn: (span: SpanHandle) => T): T {
|
|
251
320
|
try {
|
|
252
321
|
const result = withContext(handle.context, () => fn(handle));
|
|
322
|
+
|
|
253
323
|
if (isThenable(result)) {
|
|
254
324
|
return result.then(
|
|
255
325
|
(value) => {
|
|
256
326
|
handle.end();
|
|
327
|
+
|
|
257
328
|
return value;
|
|
258
329
|
},
|
|
259
330
|
(error: unknown) => {
|
|
@@ -262,7 +333,9 @@ export function runWithHandle<T>(handle: SpanHandle, fn: (span: SpanHandle) => T
|
|
|
262
333
|
},
|
|
263
334
|
) as T;
|
|
264
335
|
}
|
|
336
|
+
|
|
265
337
|
handle.end();
|
|
338
|
+
|
|
266
339
|
return result;
|
|
267
340
|
} catch (error) {
|
|
268
341
|
handle.end({ error });
|
|
@@ -285,16 +358,21 @@ export function startActiveSpan<T>(
|
|
|
285
358
|
const fn = isFn ? optionsOrFn : maybeFn!;
|
|
286
359
|
const options = isFn ? undefined : optionsOrFn;
|
|
287
360
|
const handle = startSpan(name, options);
|
|
361
|
+
|
|
288
362
|
if (handle === NOOP_SPAN_HANDLE) return fn(handle);
|
|
363
|
+
|
|
289
364
|
return runWithHandle(handle, fn);
|
|
290
365
|
}
|
|
291
366
|
|
|
292
367
|
/** Update the innermost active span; no-op when none is recording. */
|
|
293
368
|
export function updateActiveSpan(fields: SpanFields): void {
|
|
294
369
|
const core = currentClient().core;
|
|
370
|
+
|
|
295
371
|
if (!core) return;
|
|
372
|
+
|
|
296
373
|
try {
|
|
297
374
|
const span = trace.getSpan(activeContext());
|
|
375
|
+
|
|
298
376
|
if (!span?.isRecording()) return;
|
|
299
377
|
const meta = SPAN_META.get(span) ?? metaFor(core);
|
|
300
378
|
applyFields(span, meta, fields);
|
|
@@ -304,6 +382,7 @@ export function updateActiveSpan(fields: SpanFields): void {
|
|
|
304
382
|
}
|
|
305
383
|
|
|
306
384
|
const TRACE_CONTEXT_PROPAGATOR = new W3CTraceContextPropagator();
|
|
385
|
+
|
|
307
386
|
const W3C_PROPAGATOR = new CompositePropagator({
|
|
308
387
|
propagators: [TRACE_CONTEXT_PROPAGATOR, new W3CBaggagePropagator()],
|
|
309
388
|
});
|
|
@@ -316,6 +395,7 @@ export function extractW3cContext(
|
|
|
316
395
|
options: { includeBaggage?: boolean } = {},
|
|
317
396
|
): Context {
|
|
318
397
|
const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
|
|
398
|
+
|
|
319
399
|
return propagator.extract(ROOT_CONTEXT, carrier, defaultTextMapGetter);
|
|
320
400
|
}
|
|
321
401
|
|
|
@@ -326,12 +406,17 @@ export function injectW3cContext(
|
|
|
326
406
|
options: { includeBaggage?: boolean } = {},
|
|
327
407
|
): void {
|
|
328
408
|
const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
|
|
409
|
+
|
|
329
410
|
for (const key of Object.keys(carrier)) {
|
|
330
411
|
const normalized = key.toLowerCase();
|
|
412
|
+
|
|
331
413
|
if (normalized === "traceparent" || normalized === "tracestate" || normalized === "baggage") {
|
|
332
|
-
|
|
414
|
+
if (!Reflect.deleteProperty(carrier, key)) {
|
|
415
|
+
throw new TypeError(`Cannot delete stale W3C carrier property: ${key}`);
|
|
416
|
+
}
|
|
333
417
|
}
|
|
334
418
|
}
|
|
419
|
+
|
|
335
420
|
propagator.inject(context, carrier, defaultTextMapSetter);
|
|
336
421
|
}
|
|
337
422
|
|
|
@@ -339,5 +424,6 @@ export function injectW3cContext(
|
|
|
339
424
|
export function getTraceparent(): string | null {
|
|
340
425
|
const carrier = { traceparent: undefined };
|
|
341
426
|
TRACE_CONTEXT_PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
|
|
427
|
+
|
|
342
428
|
return carrier.traceparent ?? null;
|
|
343
429
|
}
|