@telemetry-dev/sdk 0.1.5 → 0.1.7
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 +26 -5
- package/dist/index.d.mts +38 -3
- package/dist/index.mjs +188 -8
- package/package.json +1 -1
- package/src/attrs.ts +18 -0
- package/src/capture.ts +243 -3
- package/src/client.ts +36 -6
- package/src/config.ts +13 -3
- package/src/index.ts +17 -1
- package/src/noop.ts +2 -0
- package/src/span.ts +16 -0
package/README.md
CHANGED
|
@@ -55,11 +55,21 @@ Without an API key every call is a silent no-op — the SDK never throws into yo
|
|
|
55
55
|
|
|
56
56
|
Other `init()` options: `enabled` (kill switch), `registerGlobal`, `exportMode: "batched" |
|
|
57
57
|
"immediate"`, `captureInput` / `captureOutput`, `mask(value, { key })` redaction hook,
|
|
58
|
-
`maxAttributeLength` (
|
|
58
|
+
`maxAttributeLength` (a non-negative safe integer or `Infinity`; default 65536; `null` and
|
|
59
|
+
`undefined` select the default, while other invalid values disable telemetry and are reported
|
|
60
|
+
through `onError`; truncated values use `...[truncated]`, or its prefix when the cap is shorter than
|
|
61
|
+
the marker; the cap also applies, with the marker, to string metadata and log attributes, and any
|
|
62
|
+
other string attribute is cut at the cap without a marker, except that a cap of `0` leaves those at
|
|
63
|
+
65536), `batch`
|
|
59
64
|
(`maxExportBatchSize` 64, `scheduledDelayMillis` 1000, `maxQueueSize` 2048,
|
|
60
65
|
`exportTimeoutMillis` 30000), `spanFilter`, `resourceAttributes`, `logLevel` (SDK diagnostics,
|
|
61
66
|
default `"warn"`), `fetch`, `waitUntil`, `onError`.
|
|
62
67
|
|
|
68
|
+
`captureInput: false` and `captureOutput: false` omit captured content. Provider integrations may
|
|
69
|
+
still record non-content metadata such as response IDs and models, finish reasons, usage, timing,
|
|
70
|
+
and errors. Capture flags do not gate stop sequences, tool descriptions and definitions (`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes,
|
|
71
|
+
or exception messages and stack traces; redact those separately when needed.
|
|
72
|
+
|
|
63
73
|
Root spans remain independent by default. `init({ sessionMode: "process" })` groups otherwise
|
|
64
74
|
uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK initialization.
|
|
65
75
|
Explicit and propagated session IDs take precedence, and valid parent trace context is preserved.
|
|
@@ -90,12 +100,19 @@ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-at
|
|
|
90
100
|
context.
|
|
91
101
|
- `extractW3cContext(carrier, options?)` / `injectW3cContext(context, carrier, options?)` — extract
|
|
92
102
|
or inject W3C trace context. Baggage is omitted unless `includeBaggage` is true.
|
|
103
|
+
- `captureEnabled("input" | "output")` — report whether the active initialized client permits that
|
|
104
|
+
capture direction. Returns `false` when no client is initialized.
|
|
105
|
+
- `boundedCapture(value, options?)` — copy a JSON-compatible value within bounded work and retained
|
|
106
|
+
size. Defaults to 48 KiB, depth 32, and 1,000 traversal items. Returns `{ value, truncated }`;
|
|
107
|
+
`value` can be partial or `undefined` when a limit, cycle, getter, or skip rule prevents capture.
|
|
93
108
|
- `flush()` / `shutdown()` — export pending data; required before serverless freeze/exit.
|
|
94
109
|
|
|
95
110
|
Span types: `span` (default) | `generation` | `tool` | `agent` | `embedding` — mapped to
|
|
96
111
|
`gen_ai.operation.name` `function` / `chat` / `execute_tool` / `invoke_agent` / `embeddings`.
|
|
97
112
|
Generation fields (`model`, `provider`, `usage`, `costUsd`, sampling params, …) map to the OTel
|
|
98
|
-
GenAI semantic conventions
|
|
113
|
+
GenAI semantic conventions. An explicit `costUsd` takes precedence; otherwise, the server estimates
|
|
114
|
+
standard token cost when reported usage and matching model pricing are available. Missing pricing leaves cost
|
|
115
|
+
unavailable, not zero, and does not prevent tracing or token accounting.
|
|
99
116
|
|
|
100
117
|
Duration and token-usage histograms (`gen_ai.client.operation.duration`,
|
|
101
118
|
`gen_ai.client.token.usage`) are recorded automatically for generation/agent/embedding/tool spans.
|
|
@@ -108,9 +125,13 @@ after the ended span passes `spanFilter`, including interrupted streams. Chunk t
|
|
|
108
125
|
histogram state and does not retain individual intervals. Pull-based timing includes consumer delay
|
|
109
126
|
between reads. An explicit timestamp uses the same monotonic millisecond clock for every chunk.
|
|
110
127
|
|
|
111
|
-
Provider integrations feature-detect
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
Provider integrations feature-detect stream APIs. With an older core SDK that lacks the stream
|
|
129
|
+
capture policy, streams are still traced with usage, model, finish reason, and timing, but output
|
|
130
|
+
retention assumes the default 65536 limit and treats a mask as configured, so incomplete output is
|
|
131
|
+
withheld. Instrumentation failures are not reported to `onError` with such a core. If only
|
|
132
|
+
`recordOutputChunk` is unavailable, tracing and reconstruction continue but chunk-interval metrics
|
|
133
|
+
are unavailable. Upgrade the core SDK with provider integrations to enable the full stream
|
|
134
|
+
telemetry.
|
|
114
135
|
|
|
115
136
|
## Serverless
|
|
116
137
|
|
package/dist/index.d.mts
CHANGED
|
@@ -34,7 +34,7 @@ interface TelemetryOptions {
|
|
|
34
34
|
captureOutput?: boolean;
|
|
35
35
|
/** Redaction hook run on captured content before stringify/truncate. */
|
|
36
36
|
mask?: MaskFn;
|
|
37
|
-
/**
|
|
37
|
+
/** Non-negative safe-integer truncation cap for content attributes; Infinity disables truncation. The cap also applies, with the marker, to string metadata and log attributes; any other string attribute is cut at the cap without a marker, except that a cap of 0 leaves those at 65536. init() reports invalid values through onError and returns a disabled client. Default 65536. */
|
|
38
38
|
maxAttributeLength?: number;
|
|
39
39
|
batch?: BatchOptions$1;
|
|
40
40
|
/** Export filter. Default when registerGlobal: only spans from this SDK's instrumentation scope. */
|
|
@@ -53,12 +53,30 @@ interface TelemetryOptions {
|
|
|
53
53
|
fetch?: typeof fetch;
|
|
54
54
|
/** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
|
|
55
55
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
56
|
-
/** Receives
|
|
56
|
+
/** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never throws into user code. */
|
|
57
57
|
onError?: (cause: Error) => void;
|
|
58
58
|
/** Header value for a custom telemetry.dev integration. */
|
|
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;
|
|
@@ -161,6 +189,11 @@ interface StartSpanOptions extends SpanFields {
|
|
|
161
189
|
/** Per-call override of the global captureOutput. */
|
|
162
190
|
captureOutput?: boolean;
|
|
163
191
|
}
|
|
192
|
+
interface SpanCapturePolicy {
|
|
193
|
+
readonly output: boolean;
|
|
194
|
+
readonly mask: boolean;
|
|
195
|
+
readonly maxAttributeLength: number;
|
|
196
|
+
}
|
|
164
197
|
interface SpanHandle {
|
|
165
198
|
/** Raw OTel span (interop escape hatch). */
|
|
166
199
|
readonly span: Span;
|
|
@@ -171,9 +204,11 @@ interface SpanHandle {
|
|
|
171
204
|
/** W3C traceparent for this span; null for the no-op handle. */
|
|
172
205
|
readonly traceparent: string | null;
|
|
173
206
|
readonly isRecording: boolean;
|
|
207
|
+
readonly capturePolicy?: SpanCapturePolicy;
|
|
174
208
|
update(fields: SpanFields): SpanHandle;
|
|
175
209
|
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
176
210
|
recordOutputChunk(timestampMs?: number): void;
|
|
211
|
+
reportError?(cause: unknown): void;
|
|
177
212
|
end(fields?: SpanFields & {
|
|
178
213
|
endTime?: Date | number;
|
|
179
214
|
}): void;
|
|
@@ -207,4 +242,4 @@ interface ObserveOptions extends StartSpanOptions {
|
|
|
207
242
|
*/
|
|
208
243
|
declare function observe<TThis, TArgs extends unknown[], TReturn>(fn: (this: TThis, ...args: TArgs) => TReturn, options?: ObserveOptions): (...args: TArgs) => TReturn;
|
|
209
244
|
//#endregion
|
|
210
|
-
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 };
|
|
245
|
+
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 SpanCapturePolicy, 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
|
@@ -5,9 +5,13 @@ import { BatchLogRecordProcessor, LoggerProvider, SimpleLogRecordProcessor } fro
|
|
|
5
5
|
import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
6
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
|
+
//#region src/config.ts
|
|
9
|
+
const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
|
|
8
10
|
function resolveConfig(options = {}) {
|
|
9
11
|
const env = resolveEnv();
|
|
10
12
|
if (options.sessionMode !== void 0 && options.sessionMode !== "explicit" && options.sessionMode !== "process") throw new TypeError(`Invalid sessionMode: ${String(options.sessionMode)}`);
|
|
13
|
+
const maxAttributeLength = options.maxAttributeLength == null ? DEFAULT_MAX_ATTRIBUTE_LENGTH : options.maxAttributeLength;
|
|
14
|
+
if (maxAttributeLength !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(maxAttributeLength) || maxAttributeLength < 0)) throw new RangeError("maxAttributeLength must be a non-negative safe integer or Infinity");
|
|
11
15
|
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
12
16
|
return {
|
|
13
17
|
apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
|
|
@@ -22,7 +26,7 @@ function resolveConfig(options = {}) {
|
|
|
22
26
|
captureInput: options.captureInput ?? true,
|
|
23
27
|
captureOutput: options.captureOutput ?? true,
|
|
24
28
|
mask: options.mask,
|
|
25
|
-
maxAttributeLength
|
|
29
|
+
maxAttributeLength,
|
|
26
30
|
batch: {
|
|
27
31
|
...DEFAULT_BATCH,
|
|
28
32
|
...options.batch
|
|
@@ -45,9 +49,20 @@ const NOOP_CLIENT = {
|
|
|
45
49
|
shutdown: () => Promise.resolve()
|
|
46
50
|
};
|
|
47
51
|
let activeClient;
|
|
52
|
+
function shutdownClient(client, onError) {
|
|
53
|
+
try {
|
|
54
|
+
client.shutdown().catch((error) => reportError(onError, error));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
reportError(onError, error instanceof Error ? error : new Error(String(error)));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
48
59
|
function currentClient() {
|
|
49
60
|
return activeClient ?? NOOP_CLIENT;
|
|
50
61
|
}
|
|
62
|
+
function captureEnabled(kind) {
|
|
63
|
+
const config = currentClient().core?.config;
|
|
64
|
+
return kind === "input" ? config?.captureInput ?? false : config?.captureOutput ?? false;
|
|
65
|
+
}
|
|
51
66
|
function flush() {
|
|
52
67
|
return currentClient().flush();
|
|
53
68
|
}
|
|
@@ -55,20 +70,22 @@ function shutdown() {
|
|
|
55
70
|
return currentClient().shutdown();
|
|
56
71
|
}
|
|
57
72
|
function init(options, overrides) {
|
|
73
|
+
const previousClient = activeClient;
|
|
58
74
|
try {
|
|
75
|
+
setLogLevel(options?.logLevel ?? "warn");
|
|
59
76
|
return initInner(options, overrides);
|
|
60
77
|
} catch (error) {
|
|
61
78
|
reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
|
|
62
|
-
|
|
79
|
+
if (previousClient && previousClient !== NOOP_CLIENT) shutdownClient(previousClient, options?.onError);
|
|
80
|
+
if (activeClient === previousClient) activeClient = NOOP_CLIENT;
|
|
63
81
|
return NOOP_CLIENT;
|
|
64
82
|
}
|
|
65
83
|
}
|
|
66
84
|
function initInner(options, overrides) {
|
|
67
85
|
const config = resolveConfig(options);
|
|
68
|
-
setLogLevel(config.logLevel);
|
|
69
86
|
if (activeClient && activeClient !== NOOP_CLIENT) {
|
|
70
87
|
diag.warn("init() called again; replacing the previous client");
|
|
71
|
-
activeClient
|
|
88
|
+
shutdownClient(activeClient, config.onError);
|
|
72
89
|
}
|
|
73
90
|
if (!(config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter))) {
|
|
74
91
|
if (config.enabled && !config.apiKey) diag.debug("no api key (apiKey option or TELEMETRY_DEV_API_KEY); telemetry disabled");
|
|
@@ -108,11 +125,12 @@ function initInner(options, overrides) {
|
|
|
108
125
|
recordMetrics: metrics ? (span) => metrics.record(span) : void 0,
|
|
109
126
|
onError: config.onError
|
|
110
127
|
});
|
|
128
|
+
const backstopLength = config.maxAttributeLength || 65536;
|
|
111
129
|
const provider = new BasicTracerProvider({
|
|
112
130
|
resource,
|
|
113
131
|
sampler: sessionSampler(config.sampler),
|
|
114
132
|
spanProcessors: [processor],
|
|
115
|
-
spanLimits: { attributeValueLengthLimit:
|
|
133
|
+
spanLimits: { attributeValueLengthLimit: backstopLength }
|
|
116
134
|
});
|
|
117
135
|
const tracer = provider.getTracer(SCOPE_NAME$1, SCOPE_VERSION);
|
|
118
136
|
let registeredTrace = false;
|
|
@@ -135,7 +153,7 @@ function initInner(options, overrides) {
|
|
|
135
153
|
const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor(logExporter) : new BatchLogRecordProcessor(logExporter);
|
|
136
154
|
const loggerProvider = new LoggerProvider({
|
|
137
155
|
resource,
|
|
138
|
-
logRecordLimits: { attributeValueLengthLimit:
|
|
156
|
+
logRecordLimits: { attributeValueLengthLimit: backstopLength },
|
|
139
157
|
processors: [logProcessor]
|
|
140
158
|
});
|
|
141
159
|
logs = {
|
|
@@ -186,9 +204,148 @@ function initInner(options, overrides) {
|
|
|
186
204
|
//#endregion
|
|
187
205
|
//#region src/capture.ts
|
|
188
206
|
const TRUNCATION_MARKER = "...[truncated]";
|
|
207
|
+
const jsonStringByteLength = (value, limit) => {
|
|
208
|
+
let bytes = 2;
|
|
209
|
+
for (let index = 0; index < value.length && bytes <= limit; index += 1) {
|
|
210
|
+
const code = value.charCodeAt(index);
|
|
211
|
+
if (code === 34 || code === 92 || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) bytes += 2;
|
|
212
|
+
else if (code < 32) bytes += 6;
|
|
213
|
+
else if (code < 128) bytes += 1;
|
|
214
|
+
else if (code < 2048) bytes += 2;
|
|
215
|
+
else if (code >= 55296 && code <= 56319) {
|
|
216
|
+
const next = value.charCodeAt(index + 1);
|
|
217
|
+
if (next >= 56320 && next <= 57343) {
|
|
218
|
+
bytes += 4;
|
|
219
|
+
index += 1;
|
|
220
|
+
} else bytes += 6;
|
|
221
|
+
} else if (code >= 56320 && code <= 57343) bytes += 6;
|
|
222
|
+
else bytes += 3;
|
|
223
|
+
}
|
|
224
|
+
return bytes;
|
|
225
|
+
};
|
|
226
|
+
const captureLimit = (name, value, fallback) => {
|
|
227
|
+
if (value === void 0) return fallback;
|
|
228
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
229
|
+
return value;
|
|
230
|
+
};
|
|
231
|
+
function boundedCaptureDetails(input, options = {}) {
|
|
232
|
+
const maxBytes = captureLimit("maxBytes", options.maxBytes, 48 * 1024);
|
|
233
|
+
const maxDepth = captureLimit("maxDepth", options.maxDepth, 32);
|
|
234
|
+
const maxItems = captureLimit("maxItems", options.maxItems, 1e3);
|
|
235
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
236
|
+
let bytes = 0;
|
|
237
|
+
let items = 0;
|
|
238
|
+
let truncated = false;
|
|
239
|
+
const visit = (value, depth, path) => {
|
|
240
|
+
if (items >= maxItems) {
|
|
241
|
+
truncated = true;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
items += 1;
|
|
245
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
|
|
246
|
+
const length = typeof value === "string" ? jsonStringByteLength(value, maxBytes - bytes) : JSON.stringify(value)?.length ?? 0;
|
|
247
|
+
if (bytes + length > maxBytes) {
|
|
248
|
+
truncated = true;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
bytes += length;
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
if (typeof value !== "object") return void 0;
|
|
255
|
+
if (depth >= maxDepth || ancestors.has(value) || bytes + 2 > maxBytes) {
|
|
256
|
+
truncated = true;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
ancestors.add(value);
|
|
260
|
+
bytes += 2;
|
|
261
|
+
try {
|
|
262
|
+
if (Array.isArray(value)) {
|
|
263
|
+
const result = [];
|
|
264
|
+
for (const item of value) {
|
|
265
|
+
if (items >= maxItems) {
|
|
266
|
+
truncated = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
const previousBytes = bytes;
|
|
270
|
+
if (result.length > 0) bytes += 1;
|
|
271
|
+
const converted = visit(item, depth + 1, path);
|
|
272
|
+
if (converted === void 0) bytes = previousBytes;
|
|
273
|
+
else result.push(converted);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
const result = {};
|
|
278
|
+
let emitted = false;
|
|
279
|
+
for (const key in value) {
|
|
280
|
+
if (items >= maxItems) {
|
|
281
|
+
truncated = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
if (!Object.hasOwn(value, key)) {
|
|
285
|
+
items += 1;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const keyBytes = jsonStringByteLength(key, maxBytes - bytes) + 1 + (emitted ? 1 : 0);
|
|
289
|
+
if (bytes + keyBytes > maxBytes) {
|
|
290
|
+
items += 1;
|
|
291
|
+
truncated = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
let item;
|
|
295
|
+
try {
|
|
296
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
297
|
+
item = descriptor?.get ? descriptor.get.call(value) : descriptor?.value;
|
|
298
|
+
if (options.skip?.(key, item, value, path)) {
|
|
299
|
+
items += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
} catch {
|
|
303
|
+
items += 1;
|
|
304
|
+
truncated = true;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const previousBytes = bytes;
|
|
308
|
+
bytes += keyBytes;
|
|
309
|
+
const converted = visit(item, depth + 1, [...path, key]);
|
|
310
|
+
if (converted === void 0) bytes = previousBytes;
|
|
311
|
+
else {
|
|
312
|
+
if (key === "__proto__") Object.defineProperty(result, key, {
|
|
313
|
+
configurable: true,
|
|
314
|
+
enumerable: true,
|
|
315
|
+
value: converted,
|
|
316
|
+
writable: true
|
|
317
|
+
});
|
|
318
|
+
else result[key] = converted;
|
|
319
|
+
emitted = true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
} catch {
|
|
324
|
+
truncated = true;
|
|
325
|
+
return;
|
|
326
|
+
} finally {
|
|
327
|
+
ancestors.delete(value);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
return {
|
|
331
|
+
value: visit(input, 0, []),
|
|
332
|
+
truncated,
|
|
333
|
+
bytes,
|
|
334
|
+
items
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function boundedCapture(input, options = {}) {
|
|
338
|
+
const { value, truncated } = boundedCaptureDetails(input, options);
|
|
339
|
+
return {
|
|
340
|
+
value,
|
|
341
|
+
truncated
|
|
342
|
+
};
|
|
343
|
+
}
|
|
189
344
|
function truncate(value, maxLength) {
|
|
190
345
|
if (value.length <= maxLength) return value;
|
|
191
|
-
|
|
346
|
+
const limit = Math.max(maxLength, 0);
|
|
347
|
+
const marker = TRUNCATION_MARKER.slice(0, limit);
|
|
348
|
+
return value.slice(0, limit - marker.length) + marker;
|
|
192
349
|
}
|
|
193
350
|
/** The single content funnel: mask → JSON stringify → truncate. */
|
|
194
351
|
function prepareCaptureValue(key, value, cfg) {
|
|
@@ -308,6 +465,15 @@ function fieldsToAttributes(fields, type, cfg) {
|
|
|
308
465
|
"gen_ai.usage.cache_read.input_tokens": fields.usage?.cacheReadInputTokens,
|
|
309
466
|
"gen_ai.usage.cache_creation.input_tokens": fields.usage?.cacheCreationInputTokens,
|
|
310
467
|
"gen_ai.usage.reasoning.output_tokens": fields.usage?.reasoningOutputTokens,
|
|
468
|
+
"gen_ai.usage.text.input_tokens": fields.usage?.textInputTokens,
|
|
469
|
+
"gen_ai.usage.text.output_tokens": fields.usage?.textOutputTokens,
|
|
470
|
+
"gen_ai.usage.text.cache_read.input_tokens": fields.usage?.textCacheReadInputTokens,
|
|
471
|
+
"gen_ai.usage.image.input_tokens": fields.usage?.imageInputTokens,
|
|
472
|
+
"gen_ai.usage.image.output_tokens": fields.usage?.imageOutputTokens,
|
|
473
|
+
"gen_ai.usage.image.cache_read.input_tokens": fields.usage?.imageCacheReadInputTokens,
|
|
474
|
+
"gen_ai.usage.audio.input_tokens": fields.usage?.audioInputTokens,
|
|
475
|
+
"gen_ai.usage.audio.output_tokens": fields.usage?.audioOutputTokens,
|
|
476
|
+
"gen_ai.usage.audio.cache_read.input_tokens": fields.usage?.audioCacheReadInputTokens,
|
|
311
477
|
"gen_ai.usage.cost": fields.costUsd,
|
|
312
478
|
"gen_ai.response.finish_reasons": fields.finishReason ? [fields.finishReason] : void 0,
|
|
313
479
|
"gen_ai.output.type": fields.outputType,
|
|
@@ -362,8 +528,14 @@ const NOOP_SPAN_HANDLE = {
|
|
|
362
528
|
spanId: INVALID_SPAN_CONTEXT.spanId,
|
|
363
529
|
traceparent: null,
|
|
364
530
|
isRecording: false,
|
|
531
|
+
capturePolicy: {
|
|
532
|
+
output: false,
|
|
533
|
+
mask: false,
|
|
534
|
+
maxAttributeLength: 0
|
|
535
|
+
},
|
|
365
536
|
update: () => NOOP_SPAN_HANDLE,
|
|
366
537
|
recordOutputChunk: () => {},
|
|
538
|
+
reportError: () => {},
|
|
367
539
|
end: () => {}
|
|
368
540
|
};
|
|
369
541
|
//#endregion
|
|
@@ -465,6 +637,11 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
465
637
|
traceId: spanContext.traceId,
|
|
466
638
|
spanId: spanContext.spanId,
|
|
467
639
|
traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`,
|
|
640
|
+
capturePolicy: {
|
|
641
|
+
output: meta.capture.captureOutput,
|
|
642
|
+
mask: meta.capture.mask !== void 0,
|
|
643
|
+
maxAttributeLength: meta.capture.maxAttributeLength
|
|
644
|
+
},
|
|
468
645
|
get isRecording() {
|
|
469
646
|
return span.isRecording();
|
|
470
647
|
},
|
|
@@ -498,6 +675,9 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
498
675
|
}
|
|
499
676
|
previousOutputChunkTime = now;
|
|
500
677
|
},
|
|
678
|
+
reportError(cause) {
|
|
679
|
+
reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
|
|
680
|
+
},
|
|
501
681
|
end(fields) {
|
|
502
682
|
try {
|
|
503
683
|
if (fields) applyFields(span, meta, fields);
|
|
@@ -625,4 +805,4 @@ function observe(fn, options) {
|
|
|
625
805
|
return wrapped;
|
|
626
806
|
}
|
|
627
807
|
//#endregion
|
|
628
|
-
export { SCOPE_NAME, TelemetrySpanProcessor, activeContext, extractW3cContext, flush, getTraceparent, init, injectW3cContext, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan, withContext };
|
|
808
|
+
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
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,
|
package/src/capture.ts
CHANGED
|
@@ -11,12 +11,252 @@ 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 = {} as 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
|
+
if (key === "__proto__") {
|
|
217
|
+
Object.defineProperty(result, key, {
|
|
218
|
+
configurable: true,
|
|
219
|
+
enumerable: true,
|
|
220
|
+
value: converted,
|
|
221
|
+
writable: true,
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
result[key] = converted;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
emitted = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return result;
|
|
232
|
+
} catch {
|
|
233
|
+
truncated = true;
|
|
234
|
+
|
|
235
|
+
return undefined;
|
|
236
|
+
} finally {
|
|
237
|
+
ancestors.delete(value);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return { value: visit(input, 0, []), truncated, bytes, items };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function boundedCapture(
|
|
245
|
+
input: unknown,
|
|
246
|
+
options: BoundedCaptureOptions = {},
|
|
247
|
+
): BoundedCaptureResult {
|
|
248
|
+
const { value, truncated } = boundedCaptureDetails(input, options);
|
|
249
|
+
|
|
250
|
+
return { value, truncated };
|
|
251
|
+
}
|
|
252
|
+
|
|
14
253
|
export function truncate(value: string, maxLength: number): string {
|
|
15
254
|
if (value.length <= maxLength) return value;
|
|
16
255
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
256
|
+
const limit = Math.max(maxLength, 0);
|
|
257
|
+
const marker = TRUNCATION_MARKER.slice(0, limit);
|
|
258
|
+
|
|
259
|
+
return value.slice(0, limit - marker.length) + marker;
|
|
20
260
|
}
|
|
21
261
|
|
|
22
262
|
/** The single content funnel: mask → JSON stringify → truncate. */
|
package/src/client.ts
CHANGED
|
@@ -36,7 +36,12 @@ import {
|
|
|
36
36
|
type Transport,
|
|
37
37
|
} from "@telemetry-dev/otel";
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_MAX_ATTRIBUTE_LENGTH,
|
|
41
|
+
type ResolvedConfig,
|
|
42
|
+
resolveConfig,
|
|
43
|
+
type TelemetryOptions,
|
|
44
|
+
} from "./config.ts";
|
|
40
45
|
|
|
41
46
|
/** Test seam: inject in-memory exporters instead of the OTLP fetch transport. */
|
|
42
47
|
export interface ClientOverrides {
|
|
@@ -71,10 +76,24 @@ const NOOP_CLIENT: ClientHandle = {
|
|
|
71
76
|
|
|
72
77
|
let activeClient: ClientHandle | undefined;
|
|
73
78
|
|
|
79
|
+
function shutdownClient(client: ClientHandle, onError?: (cause: Error) => void): void {
|
|
80
|
+
try {
|
|
81
|
+
void client.shutdown().catch((error: Error) => reportError(onError, error));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
reportError(onError, error instanceof Error ? error : new Error(String(error)));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
74
87
|
export function currentClient(): ClientHandle {
|
|
75
88
|
return activeClient ?? NOOP_CLIENT;
|
|
76
89
|
}
|
|
77
90
|
|
|
91
|
+
export function captureEnabled(kind: "input" | "output"): boolean {
|
|
92
|
+
const config = currentClient().core?.config;
|
|
93
|
+
|
|
94
|
+
return kind === "input" ? (config?.captureInput ?? false) : (config?.captureOutput ?? false);
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
export function flush(): Promise<void> {
|
|
79
98
|
return currentClient().flush();
|
|
80
99
|
}
|
|
@@ -84,11 +103,20 @@ export function shutdown(): Promise<void> {
|
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
export function init(options?: TelemetryOptions, overrides?: ClientOverrides): TelemetryClient {
|
|
106
|
+
const previousClient = activeClient;
|
|
107
|
+
|
|
87
108
|
try {
|
|
109
|
+
setLogLevel(options?.logLevel ?? "warn");
|
|
110
|
+
|
|
88
111
|
return initInner(options, overrides);
|
|
89
112
|
} catch (error) {
|
|
90
113
|
reportError(options?.onError, error instanceof Error ? error : new Error(String(error)));
|
|
91
|
-
|
|
114
|
+
|
|
115
|
+
if (previousClient && previousClient !== NOOP_CLIENT) {
|
|
116
|
+
shutdownClient(previousClient, options?.onError);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (activeClient === previousClient) activeClient = NOOP_CLIENT;
|
|
92
120
|
|
|
93
121
|
return NOOP_CLIENT;
|
|
94
122
|
}
|
|
@@ -96,12 +124,11 @@ export function init(options?: TelemetryOptions, overrides?: ClientOverrides): T
|
|
|
96
124
|
|
|
97
125
|
function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): TelemetryClient {
|
|
98
126
|
const config = resolveConfig(options);
|
|
99
|
-
setLogLevel(config.logLevel);
|
|
100
127
|
|
|
101
128
|
if (activeClient && activeClient !== NOOP_CLIENT) {
|
|
102
129
|
diag.warn("init() called again; replacing the previous client");
|
|
103
130
|
// shutdown() synchronously releases global registrations before this init claims them.
|
|
104
|
-
|
|
131
|
+
shutdownClient(activeClient, config.onError);
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
const enabled = config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter);
|
|
@@ -161,11 +188,14 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
161
188
|
onError: config.onError,
|
|
162
189
|
});
|
|
163
190
|
|
|
191
|
+
// A zero cap only drops content; OpenTelemetry rejects it as a provider limit.
|
|
192
|
+
const backstopLength = config.maxAttributeLength || DEFAULT_MAX_ATTRIBUTE_LENGTH;
|
|
193
|
+
|
|
164
194
|
const provider = new BasicTracerProvider({
|
|
165
195
|
resource,
|
|
166
196
|
sampler: sessionSampler(config.sampler),
|
|
167
197
|
spanProcessors: [processor],
|
|
168
|
-
spanLimits: { attributeValueLengthLimit:
|
|
198
|
+
spanLimits: { attributeValueLengthLimit: backstopLength },
|
|
169
199
|
});
|
|
170
200
|
|
|
171
201
|
const tracer = provider.getTracer(SCOPE_NAME, SCOPE_VERSION);
|
|
@@ -214,7 +244,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
214
244
|
|
|
215
245
|
const loggerProvider = new LoggerProvider({
|
|
216
246
|
resource,
|
|
217
|
-
logRecordLimits: { attributeValueLengthLimit:
|
|
247
|
+
logRecordLimits: { attributeValueLengthLimit: backstopLength },
|
|
218
248
|
processors: [logProcessor],
|
|
219
249
|
});
|
|
220
250
|
|
package/src/config.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface TelemetryOptions {
|
|
|
40
40
|
captureOutput?: boolean;
|
|
41
41
|
/** Redaction hook run on captured content before stringify/truncate. */
|
|
42
42
|
mask?: MaskFn;
|
|
43
|
-
/**
|
|
43
|
+
/** Non-negative safe-integer truncation cap for content attributes; Infinity disables truncation. The cap also applies, with the marker, to string metadata and log attributes; any other string attribute is cut at the cap without a marker, except that a cap of 0 leaves those at 65536. init() reports invalid values through onError and returns a disabled client. Default 65536. */
|
|
44
44
|
maxAttributeLength?: number;
|
|
45
45
|
batch?: BatchOptions;
|
|
46
46
|
/** Export filter. Default when registerGlobal: only spans from this SDK's instrumentation scope. */
|
|
@@ -59,7 +59,7 @@ export interface TelemetryOptions {
|
|
|
59
59
|
fetch?: typeof fetch;
|
|
60
60
|
/** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
|
|
61
61
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
62
|
-
/** Receives
|
|
62
|
+
/** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. The SDK never throws into user code. */
|
|
63
63
|
onError?: (cause: Error) => void;
|
|
64
64
|
/** Header value for a custom telemetry.dev integration. */
|
|
65
65
|
sdkName?: string;
|
|
@@ -104,6 +104,16 @@ export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
|
|
|
104
104
|
throw new TypeError(`Invalid sessionMode: ${String(options.sessionMode)}`);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
const maxAttributeLength =
|
|
108
|
+
options.maxAttributeLength == null ? DEFAULT_MAX_ATTRIBUTE_LENGTH : options.maxAttributeLength;
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
maxAttributeLength !== Number.POSITIVE_INFINITY &&
|
|
112
|
+
(!Number.isSafeInteger(maxAttributeLength) || maxAttributeLength < 0)
|
|
113
|
+
) {
|
|
114
|
+
throw new RangeError("maxAttributeLength must be a non-negative safe integer or Infinity");
|
|
115
|
+
}
|
|
116
|
+
|
|
107
117
|
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(
|
|
108
118
|
/\/+$/,
|
|
109
119
|
"",
|
|
@@ -122,7 +132,7 @@ export function resolveConfig(options: TelemetryOptions = {}): ResolvedConfig {
|
|
|
122
132
|
captureInput: options.captureInput ?? true,
|
|
123
133
|
captureOutput: options.captureOutput ?? true,
|
|
124
134
|
mask: options.mask,
|
|
125
|
-
maxAttributeLength
|
|
135
|
+
maxAttributeLength,
|
|
126
136
|
batch: { ...DEFAULT_BATCH, ...options.batch },
|
|
127
137
|
spanFilter: options.spanFilter,
|
|
128
138
|
sessionRootOf: options.sessionRootOf,
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,14 @@ export type {
|
|
|
7
7
|
ToolFields,
|
|
8
8
|
} from "./attrs.ts";
|
|
9
9
|
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
captureEnabled,
|
|
12
|
+
type ClientOverrides,
|
|
13
|
+
flush,
|
|
14
|
+
init,
|
|
15
|
+
shutdown,
|
|
16
|
+
type TelemetryClient,
|
|
17
|
+
} from "./client.ts";
|
|
11
18
|
|
|
12
19
|
export type {
|
|
13
20
|
BatchOptions,
|
|
@@ -18,6 +25,14 @@ export type {
|
|
|
18
25
|
TelemetryOptions,
|
|
19
26
|
} from "./config.ts";
|
|
20
27
|
|
|
28
|
+
export {
|
|
29
|
+
boundedCapture,
|
|
30
|
+
boundedCaptureDetails,
|
|
31
|
+
type BoundedCaptureDetails,
|
|
32
|
+
type BoundedCaptureOptions,
|
|
33
|
+
type BoundedCaptureResult,
|
|
34
|
+
} from "./capture.ts";
|
|
35
|
+
|
|
21
36
|
export {
|
|
22
37
|
propagateAttributes,
|
|
23
38
|
type PropagatedAttributes,
|
|
@@ -36,6 +51,7 @@ export {
|
|
|
36
51
|
getTraceparent,
|
|
37
52
|
injectW3cContext,
|
|
38
53
|
type ParentRef,
|
|
54
|
+
type SpanCapturePolicy,
|
|
39
55
|
type SpanHandle,
|
|
40
56
|
startActiveSpan,
|
|
41
57
|
startSpan,
|
package/src/noop.ts
CHANGED
|
@@ -12,7 +12,9 @@ export const NOOP_SPAN_HANDLE: SpanHandle = {
|
|
|
12
12
|
spanId: INVALID_SPAN_CONTEXT.spanId,
|
|
13
13
|
traceparent: null,
|
|
14
14
|
isRecording: false,
|
|
15
|
+
capturePolicy: { output: false, mask: false, maxAttributeLength: 0 },
|
|
15
16
|
update: () => NOOP_SPAN_HANDLE,
|
|
16
17
|
recordOutputChunk: () => {},
|
|
18
|
+
reportError: () => {},
|
|
17
19
|
end: () => {},
|
|
18
20
|
};
|
package/src/span.ts
CHANGED
|
@@ -59,6 +59,12 @@ export interface StartSpanOptions extends SpanFields {
|
|
|
59
59
|
captureOutput?: boolean;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface SpanCapturePolicy {
|
|
63
|
+
readonly output: boolean;
|
|
64
|
+
readonly mask: boolean;
|
|
65
|
+
readonly maxAttributeLength: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
export interface SpanHandle {
|
|
63
69
|
/** Raw OTel span (interop escape hatch). */
|
|
64
70
|
readonly span: Span;
|
|
@@ -69,9 +75,11 @@ export interface SpanHandle {
|
|
|
69
75
|
/** W3C traceparent for this span; null for the no-op handle. */
|
|
70
76
|
readonly traceparent: string | null;
|
|
71
77
|
readonly isRecording: boolean;
|
|
78
|
+
readonly capturePolicy?: SpanCapturePolicy;
|
|
72
79
|
update(fields: SpanFields): SpanHandle;
|
|
73
80
|
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
74
81
|
recordOutputChunk(timestampMs?: number): void;
|
|
82
|
+
reportError?(cause: unknown): void;
|
|
75
83
|
end(fields?: SpanFields & { endTime?: Date | number }): void;
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -240,6 +248,11 @@ export function createSpanHandle(
|
|
|
240
248
|
traceId: spanContext.traceId,
|
|
241
249
|
spanId: spanContext.spanId,
|
|
242
250
|
traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`,
|
|
251
|
+
capturePolicy: {
|
|
252
|
+
output: meta.capture.captureOutput,
|
|
253
|
+
mask: meta.capture.mask !== undefined,
|
|
254
|
+
maxAttributeLength: meta.capture.maxAttributeLength,
|
|
255
|
+
},
|
|
243
256
|
get isRecording() {
|
|
244
257
|
return span.isRecording();
|
|
245
258
|
},
|
|
@@ -287,6 +300,9 @@ export function createSpanHandle(
|
|
|
287
300
|
|
|
288
301
|
previousOutputChunkTime = now;
|
|
289
302
|
},
|
|
303
|
+
reportError(cause) {
|
|
304
|
+
reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
|
|
305
|
+
},
|
|
290
306
|
end(fields) {
|
|
291
307
|
try {
|
|
292
308
|
if (fields) applyFields(span, meta, fields);
|