@telemetry-dev/sdk 0.1.8 → 0.1.10
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 +59 -11
- package/dist/index.d.mts +17 -4
- package/dist/index.mjs +18 -12
- package/package.json +8 -8
- package/src/capture.ts +3 -2
- package/src/client.ts +8 -4
- package/src/config.ts +8 -4
- package/src/noop.ts +1 -0
- package/src/span.ts +25 -6
package/README.md
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
Pure TypeScript SDK for [telemetry.dev](https://telemetry.dev): add LLM traces, logs, and GenAI
|
|
4
4
|
metrics to any app. Built on OpenTelemetry — spans use the `gen_ai.*` semantic conventions and
|
|
5
5
|
ship as OTLP protobuf to `https://ingest.telemetry.dev`. Works on Node >= 20.19 and edge runtimes
|
|
6
|
-
(Vercel Edge, Cloudflare Workers with
|
|
7
|
-
|
|
6
|
+
(Vercel Edge, and Cloudflare Workers with Node.js compatibility, which is on by default for
|
|
7
|
+
compatibility dates of 2026-08-04 or later; earlier dates need the `nodejs_compat` flag). ESM-only;
|
|
8
|
+
CJS consumers can `require()` it on Node >= 20.19.
|
|
9
|
+
|
|
10
|
+
Docs: https://telemetry.dev/docs/sdk/typescript
|
|
8
11
|
|
|
9
12
|
```sh
|
|
10
13
|
npm install @telemetry-dev/sdk
|
|
@@ -12,10 +15,20 @@ npm install @telemetry-dev/sdk
|
|
|
12
15
|
|
|
13
16
|
## Quickstart
|
|
14
17
|
|
|
18
|
+
Set `TELEMETRY_DEV_API_KEY` to your project API key. `init()` reads it from the environment.
|
|
19
|
+
|
|
15
20
|
```ts
|
|
16
21
|
import { init, observe, startSpan, propagateAttributes, log, flush } from "@telemetry-dev/sdk";
|
|
17
22
|
|
|
18
|
-
init(
|
|
23
|
+
init();
|
|
24
|
+
|
|
25
|
+
// Stand-in for your model client, so the example runs on its own. Replace it with a real call.
|
|
26
|
+
async function callModel(question: string) {
|
|
27
|
+
return {
|
|
28
|
+
message: { role: "assistant", content: `You asked: ${question}` },
|
|
29
|
+
usage: { input: 12, output: 9 },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
19
32
|
|
|
20
33
|
const answer = observe(
|
|
21
34
|
async function answer(question: string) {
|
|
@@ -25,13 +38,13 @@ const answer = observe(
|
|
|
25
38
|
provider: "openai",
|
|
26
39
|
input: [{ role: "user", content: question }],
|
|
27
40
|
});
|
|
28
|
-
const res = await
|
|
41
|
+
const res = await callModel(question);
|
|
29
42
|
generation.end({
|
|
30
|
-
output: res.message,
|
|
43
|
+
output: [res.message],
|
|
31
44
|
usage: { inputTokens: res.usage.input, outputTokens: res.usage.output },
|
|
32
45
|
});
|
|
33
46
|
log("generation finished", { attributes: { cached: false } });
|
|
34
|
-
return res.message;
|
|
47
|
+
return res.message.content;
|
|
35
48
|
},
|
|
36
49
|
{ type: "agent" },
|
|
37
50
|
);
|
|
@@ -65,10 +78,27 @@ other string attribute is cut at the cap without a marker, except that a cap of
|
|
|
65
78
|
`exportTimeoutMillis` 30000), `spanFilter`, `resourceAttributes`, `logLevel` (SDK diagnostics,
|
|
66
79
|
default `"warn"`), `fetch`, `waitUntil`, `onError`.
|
|
67
80
|
|
|
81
|
+
Core SDK error recording keeps failure status, error type, and an exception event. It includes
|
|
82
|
+
`exception.message` and `exception.stacktrace` only when both effective `captureInput` and
|
|
83
|
+
`captureOutput` are true, including per-span overrides: exceptions can echo either inputs or
|
|
84
|
+
outputs. The policy is per span: when an error propagates to an enclosing span whose capture is on,
|
|
85
|
+
that span records its message and stack. Included fields pass through `mask` with their exception
|
|
86
|
+
attribute keys, then serialization and truncation. A throwing mask drops the affected field and reports a generic error to `onError`
|
|
87
|
+
and SDK diagnostics without the hook's original message or cause. The original application error
|
|
88
|
+
is rethrown unchanged. These controls do not scrub error type names, metadata, identifiers, resource
|
|
89
|
+
attributes, or events written directly through raw OpenTelemetry APIs.
|
|
90
|
+
|
|
68
91
|
`captureInput: false` and `captureOutput: false` omit captured content. Provider integrations may
|
|
69
92
|
still record non-content metadata such as response IDs and models, finish reasons, usage, timing,
|
|
70
|
-
and
|
|
71
|
-
|
|
93
|
+
and error types. Capture flags do not gate stop sequences, tool descriptions and definitions
|
|
94
|
+
(`gen_ai.tool.description`, `gen_ai.tool.definitions`), caller-supplied metadata or raw attributes;
|
|
95
|
+
redact those separately when needed.
|
|
96
|
+
|
|
97
|
+
`mask` receives input and output content, system instructions, log messages, log attributes
|
|
98
|
+
other than numbers and booleans, and content that integrations pass to
|
|
99
|
+
`handle.serializeContent`, including captured exception messages and stack traces. `ctx.key` names
|
|
100
|
+
the attribute being written. It does not receive span or propagated `metadata` or raw `attributes`;
|
|
101
|
+
redact those separately. Log masking is independent of span capture flags.
|
|
72
102
|
|
|
73
103
|
Root spans remain independent by default. `init({ sessionMode: "process" })` groups otherwise
|
|
74
104
|
uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK initialization.
|
|
@@ -79,7 +109,7 @@ new ID. It is not persisted across processes.
|
|
|
79
109
|
`sampler` accepts an OpenTelemetry `Sampler` and overrides `OTEL_TRACES_SAMPLER` / `OTEL_TRACES_SAMPLER_ARG`.
|
|
80
110
|
The default is parent-based always-on. Session roots keep the configured root policy.
|
|
81
111
|
Nested spans and explicitly parented spans keep the real parent's policy.
|
|
82
|
-
The SDK installs the [session sampling wrapper](
|
|
112
|
+
The SDK installs the [session sampling wrapper](https://github.com/telemetry-dev/telemetry/blob/main/packages/otel/README.md#correlation-attributes) on its provider. This includes the global provider that Eve uses.
|
|
83
113
|
|
|
84
114
|
## API
|
|
85
115
|
|
|
@@ -89,6 +119,13 @@ The SDK installs the [session sampling wrapper](../otel/README.md#correlation-at
|
|
|
89
119
|
- `startSpan(name, options?)` — manual handle; does **not** activate context. `handle.update()`,
|
|
90
120
|
`handle.end()`, `handle.recordOutputChunk(timestampMs?)`, `handle.traceparent`, `handle.span`
|
|
91
121
|
(raw OTel span).
|
|
122
|
+
- `handle.serializeContent(value, key)` — prepare content that an integration writes outside
|
|
123
|
+
`input` and `output`, such as an error message or a raw attribute, the way `input` and `output`
|
|
124
|
+
are prepared: the configured `mask` with `key`, JSON stringify (strings pass through), then
|
|
125
|
+
truncation to `maxAttributeLength`. Returns `undefined` when the mask throws (reported through
|
|
126
|
+
`onError`) or drops the value, or the value cannot be serialized; write nothing then. It does not
|
|
127
|
+
check `captureInput` / `captureOutput`. Integrations that support older cores should treat a
|
|
128
|
+
missing method as a configured mask.
|
|
92
129
|
- `startActiveSpan(name, options?, fn)` — callback-scoped; activates context, auto-ends.
|
|
93
130
|
- `updateActiveSpan(fields)` — update the innermost active span.
|
|
94
131
|
- `propagateAttributes({ userId, sessionId, metadata }, fn)` — stamps `user.id`,
|
|
@@ -165,10 +202,21 @@ that mode only this SDK's spans are exported by default.
|
|
|
165
202
|
Any vanilla OTel app can also skip the SDK entirely: point a stock OTLP/HTTP protobuf exporter at
|
|
166
203
|
`https://ingest.telemetry.dev/v1/traces` with an `Authorization: Bearer td_live_...` header.
|
|
167
204
|
|
|
205
|
+
### Native Claude SDK spans
|
|
206
|
+
|
|
207
|
+
`@anthropic-ai/sdk` 0.134.0 and later emits OpenTelemetry spans natively through the global tracer provider.
|
|
208
|
+
To send these spans to telemetry.dev, refer to [Claude SDK](https://telemetry.dev/docs/integrations/claude-sdk).
|
|
209
|
+
If you use `init({ registerGlobal: true })`, also set `spanFilter: () => true`. The default filter of that mode exports only the spans of this SDK.
|
|
210
|
+
Do not use the native spans and `@telemetry-dev/anthropic` together. If you use the two, each request has two spans.
|
|
211
|
+
|
|
168
212
|
## Notes
|
|
169
213
|
|
|
170
|
-
- AsyncLocalStorage powers context propagation. On runtimes without it (Workers without
|
|
171
|
-
|
|
214
|
+
- AsyncLocalStorage powers context propagation. On runtimes without it (Cloudflare Workers without
|
|
215
|
+
Node.js compatibility) automatic nesting is unavailable entirely unless the host app registered its
|
|
172
216
|
own global OTel context manager — pass `parent` explicitly there.
|
|
173
217
|
- `@opentelemetry/api` is a peer dependency (installed automatically by npm/pnpm) so the SDK
|
|
174
218
|
shares the host app's OTel API singleton.
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT. See [LICENSE](https://github.com/telemetry-dev/telemetry/blob/main/packages/sdk/LICENSE).
|
package/dist/index.d.mts
CHANGED
|
@@ -5,7 +5,11 @@ import { BatchOptions, BatchOptions as BatchOptions$1, ExportMode, ExportMode as
|
|
|
5
5
|
import { PushMetricExporter } from "@opentelemetry/sdk-metrics";
|
|
6
6
|
|
|
7
7
|
//#region src/config.d.ts
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Redaction hook applied to captured content (input/output, log messages and attributes, and
|
|
10
|
+
* content passed to `SpanHandle.serializeContent`), including exception messages/stacks, before
|
|
11
|
+
* JSON stringification. Not applied to metadata or raw attributes.
|
|
12
|
+
*/
|
|
9
13
|
type MaskFn<T = unknown> = (value: T, ctx: {
|
|
10
14
|
key: string;
|
|
11
15
|
}) => T;
|
|
@@ -28,9 +32,9 @@ interface TelemetryOptions {
|
|
|
28
32
|
exportMode?: ExportMode$1;
|
|
29
33
|
/** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
|
|
30
34
|
sampler?: Sampler;
|
|
31
|
-
/** Capture span inputs (args, prompts)
|
|
35
|
+
/** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
|
|
32
36
|
captureInput?: boolean;
|
|
33
|
-
/** Capture span outputs (results, completions)
|
|
37
|
+
/** Capture span outputs (results, completions). False also omits exception messages/stacks. Default true. */
|
|
34
38
|
captureOutput?: boolean;
|
|
35
39
|
/** Redaction hook run on captured content before stringify/truncate. */
|
|
36
40
|
mask?: MaskFn;
|
|
@@ -53,7 +57,7 @@ interface TelemetryOptions {
|
|
|
53
57
|
fetch?: typeof fetch;
|
|
54
58
|
/** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
|
|
55
59
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
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. */
|
|
60
|
+
/** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. A series of failed exports is reported once, and the next failure after an export reaches the ingest is reported again. The SDK never throws into user code. */
|
|
57
61
|
onError?: (cause: Error) => void;
|
|
58
62
|
/** Header value for a custom telemetry.dev integration. */
|
|
59
63
|
sdkName?: string;
|
|
@@ -211,6 +215,15 @@ interface SpanHandle {
|
|
|
211
215
|
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
212
216
|
recordOutputChunk(timestampMs?: number): void;
|
|
213
217
|
reportError?(cause: unknown): void;
|
|
218
|
+
/**
|
|
219
|
+
* Prepares content that the span writes outside `input` and `output` the way those are
|
|
220
|
+
* prepared: the configured mask with `key` (the attribute the result is written to), JSON
|
|
221
|
+
* stringify (strings pass through), then truncation to `maxAttributeLength`. Returns undefined
|
|
222
|
+
* when the mask throws (reported to `onError`) or drops the value, or the value cannot be
|
|
223
|
+
* serialized. Capture flags are not checked. Absent on cores that predate it; treat a missing
|
|
224
|
+
* method as masked.
|
|
225
|
+
*/
|
|
226
|
+
serializeContent?(value: unknown, key: string): string | undefined;
|
|
214
227
|
end(fields?: SpanFields & {
|
|
215
228
|
endTime?: Date | number;
|
|
216
229
|
}): 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, 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";
|
|
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, createExportFailures, 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
|
//#region src/config.ts
|
|
9
9
|
const DEFAULT_MAX_ATTRIBUTE_LENGTH = 65536;
|
|
@@ -102,9 +102,11 @@ function initInner(options, overrides) {
|
|
|
102
102
|
"deployment.environment.name": config.environment,
|
|
103
103
|
...config.resourceAttributes
|
|
104
104
|
});
|
|
105
|
+
const failures = createExportFailures(config.onError);
|
|
105
106
|
const transport = {
|
|
106
107
|
fetchImpl: config.fetchImpl,
|
|
107
|
-
onError: config.onError
|
|
108
|
+
onError: config.onError,
|
|
109
|
+
failures
|
|
108
110
|
};
|
|
109
111
|
const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : void 0;
|
|
110
112
|
const spanExporter = overrides?.spanExporter ?? createTraceExporter({
|
|
@@ -154,7 +156,7 @@ function initInner(options, overrides) {
|
|
|
154
156
|
const ensureLogger = () => {
|
|
155
157
|
if (!logExporter) return void 0;
|
|
156
158
|
if (!logs) {
|
|
157
|
-
const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor(logExporter) : new BatchLogRecordProcessor(logExporter);
|
|
159
|
+
const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor({ exporter: logExporter }) : new BatchLogRecordProcessor({ exporter: logExporter });
|
|
158
160
|
const loggerProvider = new LoggerProvider({
|
|
159
161
|
resource,
|
|
160
162
|
logRecordLimits: { attributeValueLengthLimit: backstopLength },
|
|
@@ -173,7 +175,7 @@ function initInner(options, overrides) {
|
|
|
173
175
|
const parts = [provider[op]()];
|
|
174
176
|
if (logs) parts.push(logs[op]());
|
|
175
177
|
if (metrics) parts.push(metrics[op]());
|
|
176
|
-
return Promise.all(parts).then(() => void 0).catch((error) =>
|
|
178
|
+
return Promise.all(parts).then(() => void 0).catch((error) => failures.report(error));
|
|
177
179
|
};
|
|
178
180
|
const handoff = (p) => {
|
|
179
181
|
if (config.waitUntil) {
|
|
@@ -356,8 +358,8 @@ function prepareCaptureValue(key, value, cfg) {
|
|
|
356
358
|
let masked = value;
|
|
357
359
|
if (cfg.mask) try {
|
|
358
360
|
masked = cfg.mask(value, { key });
|
|
359
|
-
} catch
|
|
360
|
-
reportError(cfg.onError,
|
|
361
|
+
} catch {
|
|
362
|
+
reportError(cfg.onError, /* @__PURE__ */ new Error("mask hook raised; content dropped"));
|
|
361
363
|
return;
|
|
362
364
|
}
|
|
363
365
|
const serialized = jsonAttr(masked);
|
|
@@ -540,6 +542,7 @@ const NOOP_SPAN_HANDLE = {
|
|
|
540
542
|
update: () => NOOP_SPAN_HANDLE,
|
|
541
543
|
recordOutputChunk: () => {},
|
|
542
544
|
reportError: () => {},
|
|
545
|
+
serializeContent: () => void 0,
|
|
543
546
|
end: () => {}
|
|
544
547
|
};
|
|
545
548
|
//#endregion
|
|
@@ -576,16 +579,16 @@ function resolveParent(parent) {
|
|
|
576
579
|
if (isContext(ref)) return withAmbientPropagated(ref);
|
|
577
580
|
return trace.setSpanContext(base, ref);
|
|
578
581
|
}
|
|
579
|
-
function applyError(span, error) {
|
|
582
|
+
function applyError(span, error, capture) {
|
|
580
583
|
const err = error instanceof Error ? error : void 0;
|
|
581
584
|
const errorType = err === void 0 ? "Error" : err.name !== "Error" && err.name.length > 0 ? err.name : (err.constructor?.name ?? "Error") || "Error";
|
|
582
|
-
const message = err?.message ?? String(error);
|
|
583
585
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
584
586
|
span.setAttribute("error.type", errorType);
|
|
587
|
+
const captureContent = capture.captureInput && capture.captureOutput;
|
|
585
588
|
span.addEvent("exception", omitUndefined({
|
|
586
589
|
"exception.type": errorType,
|
|
587
|
-
"exception.message": message,
|
|
588
|
-
"exception.stacktrace": err?.stack,
|
|
590
|
+
"exception.message": captureContent ? prepareCaptureValue("exception.message", err?.message ?? String(error), capture) : void 0,
|
|
591
|
+
"exception.stacktrace": captureContent && err?.stack !== void 0 ? prepareCaptureValue("exception.stacktrace", err.stack, capture) : void 0,
|
|
589
592
|
"log.severity_number": SEVERITY_ERROR
|
|
590
593
|
}));
|
|
591
594
|
}
|
|
@@ -593,7 +596,7 @@ function applyFields(span, meta, fields) {
|
|
|
593
596
|
if (fields.name) span.updateName(fields.name);
|
|
594
597
|
const attrs = fieldsToAttributes(fields, meta.type, meta.capture);
|
|
595
598
|
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
596
|
-
if (fields.error !== void 0) applyError(span, fields.error);
|
|
599
|
+
if (fields.error !== void 0) applyError(span, fields.error, meta.capture);
|
|
597
600
|
}
|
|
598
601
|
function metaFor(core, options) {
|
|
599
602
|
const cfg = core.config;
|
|
@@ -631,7 +634,7 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
631
634
|
SPAN_META.set(span, meta);
|
|
632
635
|
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
633
636
|
if (options.name) span.updateName(options.name);
|
|
634
|
-
if (options.error !== void 0) applyError(span, options.error);
|
|
637
|
+
if (options.error !== void 0) applyError(span, options.error, meta.capture);
|
|
635
638
|
const spanContext = span.spanContext();
|
|
636
639
|
let previousOutputChunkTime;
|
|
637
640
|
const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
|
|
@@ -682,6 +685,9 @@ function createSpanHandle(core, name, options = {}) {
|
|
|
682
685
|
reportError(cause) {
|
|
683
686
|
reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
|
|
684
687
|
},
|
|
688
|
+
serializeContent(value, key) {
|
|
689
|
+
return prepareCaptureValue(key, value, meta.capture);
|
|
690
|
+
},
|
|
685
691
|
end(fields) {
|
|
686
692
|
try {
|
|
687
693
|
if (fields) applyFields(span, meta, fields);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"access": "public"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@opentelemetry/api-logs": "^0.
|
|
40
|
-
"@opentelemetry/core": "^2.
|
|
41
|
-
"@opentelemetry/resources": "^2.
|
|
42
|
-
"@opentelemetry/sdk-logs": "^0.
|
|
43
|
-
"@opentelemetry/sdk-metrics": "^2.
|
|
44
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
45
|
-
"@telemetry-dev/otel": "^0.1.
|
|
39
|
+
"@opentelemetry/api-logs": "^0.223.0",
|
|
40
|
+
"@opentelemetry/core": "^2.12.0",
|
|
41
|
+
"@opentelemetry/resources": "^2.12.0",
|
|
42
|
+
"@opentelemetry/sdk-logs": "^0.223.0",
|
|
43
|
+
"@opentelemetry/sdk-metrics": "^2.12.0",
|
|
44
|
+
"@opentelemetry/sdk-trace-base": "^2.12.0",
|
|
45
|
+
"@telemetry-dev/otel": "^0.1.7"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@opentelemetry/api": "^1.9.1",
|
package/src/capture.ts
CHANGED
|
@@ -270,8 +270,9 @@ export function prepareCaptureValue(
|
|
|
270
270
|
if (cfg.mask) {
|
|
271
271
|
try {
|
|
272
272
|
masked = cfg.mask(value, { key });
|
|
273
|
-
} catch
|
|
274
|
-
|
|
273
|
+
} catch {
|
|
274
|
+
// Hook errors can contain the unmasked value or chain the application exception.
|
|
275
|
+
reportError(cfg.onError, new Error("mask hook raised; content dropped"));
|
|
275
276
|
|
|
276
277
|
return undefined;
|
|
277
278
|
}
|
package/src/client.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
als,
|
|
20
20
|
AlsContextManager,
|
|
21
21
|
BATCHED_METRIC_INTERVAL_MS,
|
|
22
|
+
createExportFailures,
|
|
22
23
|
createLogExporter,
|
|
23
24
|
createMetricExporter,
|
|
24
25
|
createMetricsPipeline,
|
|
@@ -156,7 +157,10 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
156
157
|
...config.resourceAttributes,
|
|
157
158
|
});
|
|
158
159
|
|
|
159
|
-
|
|
160
|
+
// A failed flush repeats the failure of its export, so the flush and the exporters share one
|
|
161
|
+
// failure state.
|
|
162
|
+
const failures = createExportFailures(config.onError);
|
|
163
|
+
const transport: Transport = { fetchImpl: config.fetchImpl, onError: config.onError, failures };
|
|
160
164
|
const headers = config.apiKey ? otlpHeaders(config.apiKey, config.sdkName) : undefined;
|
|
161
165
|
|
|
162
166
|
const spanExporter =
|
|
@@ -244,8 +248,8 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
244
248
|
if (!logs) {
|
|
245
249
|
const logProcessor =
|
|
246
250
|
config.exportMode === "immediate"
|
|
247
|
-
? new SimpleLogRecordProcessor(logExporter)
|
|
248
|
-
: new BatchLogRecordProcessor(logExporter);
|
|
251
|
+
? new SimpleLogRecordProcessor({ exporter: logExporter })
|
|
252
|
+
: new BatchLogRecordProcessor({ exporter: logExporter });
|
|
249
253
|
|
|
250
254
|
const loggerProvider = new LoggerProvider({
|
|
251
255
|
resource,
|
|
@@ -274,7 +278,7 @@ function initInner(options?: TelemetryOptions, overrides?: ClientOverrides): Tel
|
|
|
274
278
|
|
|
275
279
|
return Promise.all(parts)
|
|
276
280
|
.then(() => undefined)
|
|
277
|
-
.catch((error: Error) =>
|
|
281
|
+
.catch((error: Error) => failures.report(error));
|
|
278
282
|
};
|
|
279
283
|
|
|
280
284
|
const handoff = (p: Promise<void>): Promise<void> => {
|
package/src/config.ts
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export type { BatchOptions, ExportMode, LogLevel, SdkLogLevel } from "@telemetry-dev/otel";
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Redaction hook applied to captured content (input/output, log messages and attributes, and
|
|
17
|
+
* content passed to `SpanHandle.serializeContent`), including exception messages/stacks, before
|
|
18
|
+
* JSON stringification. Not applied to metadata or raw attributes.
|
|
19
|
+
*/
|
|
16
20
|
export type MaskFn<T = unknown> = (value: T, ctx: { key: string }) => T;
|
|
17
21
|
|
|
18
22
|
export interface TelemetryOptions {
|
|
@@ -34,9 +38,9 @@ export interface TelemetryOptions {
|
|
|
34
38
|
exportMode?: ExportMode;
|
|
35
39
|
/** OTel sampling policy. Defaults to OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG. */
|
|
36
40
|
sampler?: Sampler;
|
|
37
|
-
/** Capture span inputs (args, prompts)
|
|
41
|
+
/** Capture span inputs (args, prompts). False also omits exception messages/stacks. Default true. */
|
|
38
42
|
captureInput?: boolean;
|
|
39
|
-
/** Capture span outputs (results, completions)
|
|
43
|
+
/** Capture span outputs (results, completions). False also omits exception messages/stacks. Default true. */
|
|
40
44
|
captureOutput?: boolean;
|
|
41
45
|
/** Redaction hook run on captured content before stringify/truncate. */
|
|
42
46
|
mask?: MaskFn;
|
|
@@ -59,7 +63,7 @@ export interface TelemetryOptions {
|
|
|
59
63
|
fetch?: typeof fetch;
|
|
60
64
|
/** Serverless extender (e.g. Cloudflare `ctx.waitUntil`). When provided, `flush()` does not await exports. */
|
|
61
65
|
waitUntil?: (p: Promise<unknown>) => void;
|
|
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. */
|
|
66
|
+
/** Receives errors raised while emitting telemetry; repeated instrumentation failures from one stream may be coalesced into one report. A series of failed exports is reported once, and the next failure after an export reaches the ingest is reported again. The SDK never throws into user code. */
|
|
63
67
|
onError?: (cause: Error) => void;
|
|
64
68
|
/** Header value for a custom telemetry.dev integration. */
|
|
65
69
|
sdkName?: string;
|
package/src/noop.ts
CHANGED
package/src/span.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type SpanFields,
|
|
39
39
|
type SpanType,
|
|
40
40
|
} from "./attrs.ts";
|
|
41
|
+
import { prepareCaptureValue } from "./capture.ts";
|
|
41
42
|
import { type ClientCore, currentClient } from "./client.ts";
|
|
42
43
|
import { NOOP_SPAN_HANDLE } from "./noop.ts";
|
|
43
44
|
|
|
@@ -80,6 +81,15 @@ export interface SpanHandle {
|
|
|
80
81
|
/** Records an output chunk arrival. Pull-based streams include consumer delay between pulls. */
|
|
81
82
|
recordOutputChunk(timestampMs?: number): void;
|
|
82
83
|
reportError?(cause: unknown): void;
|
|
84
|
+
/**
|
|
85
|
+
* Prepares content that the span writes outside `input` and `output` the way those are
|
|
86
|
+
* prepared: the configured mask with `key` (the attribute the result is written to), JSON
|
|
87
|
+
* stringify (strings pass through), then truncation to `maxAttributeLength`. Returns undefined
|
|
88
|
+
* when the mask throws (reported to `onError`) or drops the value, or the value cannot be
|
|
89
|
+
* serialized. Capture flags are not checked. Absent on cores that predate it; treat a missing
|
|
90
|
+
* method as masked.
|
|
91
|
+
*/
|
|
92
|
+
serializeContent?(value: unknown, key: string): string | undefined;
|
|
83
93
|
end(fields?: SpanFields & { endTime?: Date | number }): void;
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -143,7 +153,7 @@ function resolveParent(parent: ParentRef | undefined): Context {
|
|
|
143
153
|
return trace.setSpanContext(base, ref);
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
function applyError(span: Span, error: unknown): void {
|
|
156
|
+
function applyError(span: Span, error: unknown, capture: FieldCaptureConfig): void {
|
|
147
157
|
const err = error instanceof Error ? error : undefined;
|
|
148
158
|
|
|
149
159
|
// Prefer the specific subclass name (Python parity: type(error).__name__) — many SDK
|
|
@@ -155,15 +165,21 @@ function applyError(span: Span, error: unknown): void {
|
|
|
155
165
|
? err.name
|
|
156
166
|
: (err.constructor?.name ?? "Error") || "Error";
|
|
157
167
|
|
|
158
|
-
const message = err?.message ?? String(error);
|
|
159
168
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
160
169
|
span.setAttribute("error.type", errorType);
|
|
170
|
+
// Exceptions can echo either inputs or outputs; both must allow content capture.
|
|
171
|
+
const captureContent = capture.captureInput && capture.captureOutput;
|
|
161
172
|
span.addEvent(
|
|
162
173
|
"exception",
|
|
163
174
|
omitUndefined({
|
|
164
175
|
"exception.type": errorType,
|
|
165
|
-
"exception.message":
|
|
166
|
-
|
|
176
|
+
"exception.message": captureContent
|
|
177
|
+
? prepareCaptureValue("exception.message", err?.message ?? String(error), capture)
|
|
178
|
+
: undefined,
|
|
179
|
+
"exception.stacktrace":
|
|
180
|
+
captureContent && err?.stack !== undefined
|
|
181
|
+
? prepareCaptureValue("exception.stacktrace", err.stack, capture)
|
|
182
|
+
: undefined,
|
|
167
183
|
"log.severity_number": SEVERITY_ERROR,
|
|
168
184
|
}),
|
|
169
185
|
);
|
|
@@ -175,7 +191,7 @@ function applyFields(span: Span, meta: SpanMeta, fields: SpanFields): void {
|
|
|
175
191
|
|
|
176
192
|
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
177
193
|
|
|
178
|
-
if (fields.error !== undefined) applyError(span, fields.error);
|
|
194
|
+
if (fields.error !== undefined) applyError(span, fields.error, meta.capture);
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
function metaFor(core: ClientCore, options?: StartSpanOptions): SpanMeta {
|
|
@@ -236,7 +252,7 @@ export function createSpanHandle(
|
|
|
236
252
|
|
|
237
253
|
if (options.name) span.updateName(options.name);
|
|
238
254
|
|
|
239
|
-
if (options.error !== undefined) applyError(span, options.error);
|
|
255
|
+
if (options.error !== undefined) applyError(span, options.error, meta.capture);
|
|
240
256
|
|
|
241
257
|
const spanContext = span.spanContext();
|
|
242
258
|
let previousOutputChunkTime: number | undefined;
|
|
@@ -303,6 +319,9 @@ export function createSpanHandle(
|
|
|
303
319
|
reportError(cause) {
|
|
304
320
|
reportError(meta.onError, cause instanceof Error ? cause : new Error(String(cause)));
|
|
305
321
|
},
|
|
322
|
+
serializeContent(value, key) {
|
|
323
|
+
return prepareCaptureValue(key, value, meta.capture);
|
|
324
|
+
},
|
|
306
325
|
end(fields) {
|
|
307
326
|
try {
|
|
308
327
|
if (fields) applyFields(span, meta, fields);
|