@telemetry-dev/sdk 0.1.3 → 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 +14 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +28 -2
- package/package.json +3 -3
- package/src/attrs.ts +10 -0
- package/src/capture.ts +6 -0
- package/src/client.ts +30 -0
- package/src/config.ts +3 -0
- package/src/index.ts +6 -0
- 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`,
|
|
@@ -99,6 +100,18 @@ GenAI semantic conventions; cost is computed server-side from usage unless `cost
|
|
|
99
100
|
Duration and token-usage histograms (`gen_ai.client.operation.duration`,
|
|
100
101
|
`gen_ai.client.token.usage`) are recorded automatically for generation/agent/embedding/tool spans.
|
|
101
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
|
+
|
|
102
115
|
## Serverless
|
|
103
116
|
|
|
104
117
|
Use `exportMode: "immediate"` and either `await flush()` before returning or pass the platform
|
package/dist/index.d.mts
CHANGED
|
@@ -172,6 +172,8 @@ interface SpanHandle {
|
|
|
172
172
|
readonly traceparent: string | null;
|
|
173
173
|
readonly isRecording: boolean;
|
|
174
174
|
update(fields: SpanFields): SpanHandle;
|
|
175
|
+
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
176
|
+
recordOutputChunk(timestampMs?: number): void;
|
|
175
177
|
end(fields?: SpanFields & {
|
|
176
178
|
endTime?: Date | number;
|
|
177
179
|
}): void;
|
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();
|
|
@@ -363,6 +363,7 @@ const NOOP_SPAN_HANDLE = {
|
|
|
363
363
|
traceparent: null,
|
|
364
364
|
isRecording: false,
|
|
365
365
|
update: () => NOOP_SPAN_HANDLE,
|
|
366
|
+
recordOutputChunk: () => {},
|
|
366
367
|
end: () => {}
|
|
367
368
|
};
|
|
368
369
|
//#endregion
|
|
@@ -456,6 +457,7 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
456
457
|
if (options.name) span.updateName(options.name);
|
|
457
458
|
if (options.error !== void 0) applyError(span, options.error);
|
|
458
459
|
const spanContext = span.spanContext();
|
|
460
|
+
let previousOutputChunkTime;
|
|
459
461
|
const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
|
|
460
462
|
const handle = {
|
|
461
463
|
span,
|
|
@@ -474,6 +476,28 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
474
476
|
}
|
|
475
477
|
return handle;
|
|
476
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
|
+
},
|
|
477
501
|
end(fields) {
|
|
478
502
|
try {
|
|
479
503
|
if (fields) applyFields(span, meta, fields);
|
|
@@ -543,7 +567,9 @@ function injectW3cContext(context, carrier, options = {}) {
|
|
|
543
567
|
const propagator = options.includeBaggage === true ? W3C_PROPAGATOR : TRACE_CONTEXT_PROPAGATOR;
|
|
544
568
|
for (const key of Object.keys(carrier)) {
|
|
545
569
|
const normalized = key.toLowerCase();
|
|
546
|
-
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
|
+
}
|
|
547
573
|
}
|
|
548
574
|
propagator.inject(context, carrier, defaultTextMapSetter);
|
|
549
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/
|
|
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,11 +105,14 @@ 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,6 +123,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
119
123
|
"deployment.environment.name": config.environment,
|
|
120
124
|
...config.resourceAttributes,
|
|
121
125
|
});
|
|
126
|
+
|
|
122
127
|
const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError };
|
|
123
128
|
const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
|
|
124
129
|
|
|
@@ -131,6 +136,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
131
136
|
(headers
|
|
132
137
|
? createMetricExporter({ url: `${config.baseUrl}/v1/metrics`, headers }, transport)
|
|
133
138
|
: undefined);
|
|
139
|
+
|
|
134
140
|
const metrics = metricExporter
|
|
135
141
|
? createMetricsPipeline({
|
|
136
142
|
resource,
|
|
@@ -161,21 +167,26 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
161
167
|
spanProcessors: [processor],
|
|
162
168
|
spanLimits: { attributeValueLengthLimit: config.maxAttributeLength },
|
|
163
169
|
});
|
|
170
|
+
|
|
164
171
|
const tracer = provider.getTracer(SCOPE_NAME, SCOPE_VERSION);
|
|
165
172
|
|
|
166
173
|
let registeredTrace = false;
|
|
167
174
|
let registeredContext = false;
|
|
168
175
|
let registeredPropagation = false;
|
|
176
|
+
|
|
169
177
|
if (config.registerGlobal) {
|
|
170
178
|
registeredTrace = trace.setGlobalTracerProvider(
|
|
171
179
|
config.sessionRootOf
|
|
172
180
|
? sessionRootTracerProvider(provider, config.apiKey, config.sessionRootOf, config.onError)
|
|
173
181
|
: provider,
|
|
174
182
|
);
|
|
183
|
+
|
|
175
184
|
if (als) {
|
|
176
185
|
registeredContext = apiContext.setGlobalContextManager(new AlsContextManager(als));
|
|
177
186
|
}
|
|
187
|
+
|
|
178
188
|
registeredPropagation = propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
189
|
+
|
|
179
190
|
if (!registeredTrace) {
|
|
180
191
|
diag.warn(
|
|
181
192
|
"a global TracerProvider is already registered; attach the TelemetrySpanProcessor from '@telemetry-dev/otel' to your own provider instead",
|
|
@@ -188,43 +199,56 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
188
199
|
(headers
|
|
189
200
|
? createLogExporter({ url: `${config.baseUrl}/v1/logs`, headers }, transport)
|
|
190
201
|
: undefined);
|
|
202
|
+
|
|
191
203
|
// Lazy: the log pipeline only exists once log() is first called.
|
|
192
204
|
let logs: { logger: Logger; forceFlush(): Promise<void>; shutdown(): Promise<void> } | undefined;
|
|
205
|
+
|
|
193
206
|
const ensureLogger = (): Logger | undefined => {
|
|
194
207
|
if (!logExporter) return undefined;
|
|
208
|
+
|
|
195
209
|
if (!logs) {
|
|
196
210
|
const logProcessor =
|
|
197
211
|
config.exportMode === "immediate"
|
|
198
212
|
? new SimpleLogRecordProcessor(logExporter)
|
|
199
213
|
: new BatchLogRecordProcessor(logExporter);
|
|
214
|
+
|
|
200
215
|
const loggerProvider = new LoggerProvider({
|
|
201
216
|
resource,
|
|
202
217
|
logRecordLimits: { attributeValueLengthLimit: config.maxAttributeLength },
|
|
203
218
|
processors: [logProcessor],
|
|
204
219
|
});
|
|
220
|
+
|
|
205
221
|
logs = {
|
|
206
222
|
logger: loggerProvider.getLogger(SCOPE_NAME, SCOPE_VERSION),
|
|
207
223
|
forceFlush: () => loggerProvider.forceFlush(),
|
|
208
224
|
shutdown: () => loggerProvider.shutdown(),
|
|
209
225
|
};
|
|
210
226
|
}
|
|
227
|
+
|
|
211
228
|
return logs.logger;
|
|
212
229
|
};
|
|
213
230
|
|
|
214
231
|
let torn = false;
|
|
232
|
+
|
|
215
233
|
const collect = (op: "forceFlush" | "shutdown"): Promise<void> => {
|
|
216
234
|
const parts: Promise<void>[] = [provider[op]()];
|
|
235
|
+
|
|
217
236
|
if (logs) parts.push(logs[op]());
|
|
237
|
+
|
|
218
238
|
if (metrics) parts.push(metrics[op]());
|
|
239
|
+
|
|
219
240
|
return Promise.all(parts)
|
|
220
241
|
.then(() => undefined)
|
|
221
242
|
.catch((error: Error) => reportError(config.onError, error));
|
|
222
243
|
};
|
|
244
|
+
|
|
223
245
|
const handoff = (p: Promise<void>): Promise<void> => {
|
|
224
246
|
if (config.waitUntil) {
|
|
225
247
|
config.waitUntil(p);
|
|
248
|
+
|
|
226
249
|
return Promise.resolve();
|
|
227
250
|
}
|
|
251
|
+
|
|
228
252
|
return p;
|
|
229
253
|
};
|
|
230
254
|
|
|
@@ -236,14 +260,20 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
236
260
|
shutdown: () => {
|
|
237
261
|
if (torn) return Promise.resolve();
|
|
238
262
|
torn = true;
|
|
263
|
+
|
|
239
264
|
if (registeredTrace) trace.disable();
|
|
265
|
+
|
|
240
266
|
if (registeredContext) apiContext.disable();
|
|
267
|
+
|
|
241
268
|
if (registeredPropagation) propagation.disable();
|
|
242
269
|
handle.core = undefined;
|
|
270
|
+
|
|
243
271
|
return handoff(collect("shutdown"));
|
|
244
272
|
},
|
|
245
273
|
};
|
|
274
|
+
|
|
246
275
|
handle.core = { config, tracer, ensureLogger };
|
|
247
276
|
activeClient = handle;
|
|
277
|
+
|
|
248
278
|
return handle;
|
|
249
279
|
}
|
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,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
|
@@ -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
|
}
|