@telemetry-dev/sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +123 -0
- package/dist/index.d.mts +184 -0
- package/dist/index.mjs +558 -0
- package/package.json +67 -0
- package/src/attrs.ts +157 -0
- package/src/capture.ts +39 -0
- package/src/client.ts +240 -0
- package/src/config.ts +104 -0
- package/src/index.ts +29 -0
- package/src/logs.ts +77 -0
- package/src/noop.ts +17 -0
- package/src/observe.ts +64 -0
- package/src/span.ts +272 -0
package/src/observe.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { reportError, withContext } from "@telemetry-dev/otel";
|
|
2
|
+
|
|
3
|
+
import { currentClient } from "./client.ts";
|
|
4
|
+
import { createSpanHandle, isThenable, type SpanHandle, type StartSpanOptions } from "./span.ts";
|
|
5
|
+
|
|
6
|
+
export interface ObserveOptions extends StartSpanOptions {
|
|
7
|
+
/** Span name; defaults to fn.name or "anonymous". */
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wrap a function (sync or async) so every call becomes a span: args → input, return value →
|
|
13
|
+
* output, thrown/rejected errors captured and rethrown. Activates context so nested SDK calls
|
|
14
|
+
* parent to it. Resolves the client at call time, so wrapping at module load before init() works.
|
|
15
|
+
*/
|
|
16
|
+
export function observe<TArgs extends unknown[], TReturn>(
|
|
17
|
+
fn: (...args: TArgs) => TReturn,
|
|
18
|
+
options?: ObserveOptions,
|
|
19
|
+
): (...args: TArgs) => TReturn {
|
|
20
|
+
const wrapped = function (this: unknown, ...args: TArgs): TReturn {
|
|
21
|
+
const core = currentClient().core;
|
|
22
|
+
if (!core) return fn.apply(this, args);
|
|
23
|
+
|
|
24
|
+
let handle: SpanHandle;
|
|
25
|
+
try {
|
|
26
|
+
const name = options?.name ?? (fn.name || "anonymous");
|
|
27
|
+
const input =
|
|
28
|
+
options?.input !== undefined
|
|
29
|
+
? options.input
|
|
30
|
+
: args.length === 0
|
|
31
|
+
? undefined
|
|
32
|
+
: args.length === 1
|
|
33
|
+
? args[0]
|
|
34
|
+
: args;
|
|
35
|
+
handle = createSpanHandle(core, name, { ...options, input });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
reportError(core.config.onError, error);
|
|
38
|
+
return fn.apply(this, args);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const result = withContext(handle.context, () => fn.apply(this, args));
|
|
43
|
+
if (isThenable(result)) {
|
|
44
|
+
return (result as unknown as Promise<unknown>).then(
|
|
45
|
+
(value) => {
|
|
46
|
+
handle.end({ output: value });
|
|
47
|
+
return value;
|
|
48
|
+
},
|
|
49
|
+
(error: unknown) => {
|
|
50
|
+
handle.end({ error });
|
|
51
|
+
throw error;
|
|
52
|
+
},
|
|
53
|
+
) as TReturn;
|
|
54
|
+
}
|
|
55
|
+
handle.end({ output: result });
|
|
56
|
+
return result;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
handle.end({ error });
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
Object.defineProperty(wrapped, "name", { value: fn.name || "anonymous", configurable: true });
|
|
63
|
+
return wrapped;
|
|
64
|
+
}
|
package/src/span.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Context,
|
|
3
|
+
defaultTextMapSetter,
|
|
4
|
+
type Span,
|
|
5
|
+
type SpanContext,
|
|
6
|
+
SpanKind,
|
|
7
|
+
SpanStatusCode,
|
|
8
|
+
trace,
|
|
9
|
+
} from "@opentelemetry/api";
|
|
10
|
+
import { parseTraceParent, W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
activeContext,
|
|
14
|
+
omitUndefined,
|
|
15
|
+
PROPAGATED_KEY,
|
|
16
|
+
propagatedFromContext,
|
|
17
|
+
reportError,
|
|
18
|
+
withContext,
|
|
19
|
+
} from "@telemetry-dev/otel";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
type FieldCaptureConfig,
|
|
23
|
+
fieldsToAttributes,
|
|
24
|
+
SPAN_TYPE_OPERATIONS,
|
|
25
|
+
type SpanFields,
|
|
26
|
+
type SpanType,
|
|
27
|
+
} from "./attrs.ts";
|
|
28
|
+
import { type ClientCore, currentClient } from "./client.ts";
|
|
29
|
+
import { NOOP_SPAN_HANDLE } from "./noop.ts";
|
|
30
|
+
|
|
31
|
+
export type ParentRef = string /* W3C traceparent */ | SpanHandle | SpanContext | Context;
|
|
32
|
+
|
|
33
|
+
export interface StartSpanOptions extends SpanFields {
|
|
34
|
+
/** Default "span" → operation "function". */
|
|
35
|
+
type?: SpanType;
|
|
36
|
+
/** Parent override; defaults to the active context. */
|
|
37
|
+
parent?: ParentRef;
|
|
38
|
+
startTime?: Date | number;
|
|
39
|
+
/** Per-call override of the global captureInput. */
|
|
40
|
+
captureInput?: boolean;
|
|
41
|
+
/** Per-call override of the global captureOutput. */
|
|
42
|
+
captureOutput?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SpanHandle {
|
|
46
|
+
/** Raw OTel span (interop escape hatch). */
|
|
47
|
+
readonly span: Span;
|
|
48
|
+
/** Context with this span active, for manual parenting. */
|
|
49
|
+
readonly context: Context;
|
|
50
|
+
readonly traceId: string;
|
|
51
|
+
readonly spanId: string;
|
|
52
|
+
/** W3C traceparent for this span; null for the no-op handle. */
|
|
53
|
+
readonly traceparent: string | null;
|
|
54
|
+
readonly isRecording: boolean;
|
|
55
|
+
update(fields: SpanFields): SpanHandle;
|
|
56
|
+
end(fields?: SpanFields & { endTime?: Date | number }): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SpanMeta {
|
|
60
|
+
type: SpanType;
|
|
61
|
+
capture: FieldCaptureConfig;
|
|
62
|
+
onError?: (error: unknown) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Remembers each span's type + capture flags so update()/end()/updateActiveSpan map
|
|
66
|
+
// input/output to the right gen_ai.* keys.
|
|
67
|
+
const SPAN_META = new WeakMap<Span, SpanMeta>();
|
|
68
|
+
|
|
69
|
+
const SEVERITY_ERROR = 17;
|
|
70
|
+
|
|
71
|
+
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
72
|
+
return (
|
|
73
|
+
value != null &&
|
|
74
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
75
|
+
"then" in value &&
|
|
76
|
+
typeof value.then === "function"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isContext(value: object): value is Context {
|
|
81
|
+
return "getValue" in value && typeof value.getValue === "function";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSpanHandle(value: object): value is SpanHandle {
|
|
85
|
+
return "span" in value && "context" in value && "update" in value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Ambient propagated correlation attrs survive explicit parent overrides: contexts that lack
|
|
89
|
+
// PROPAGATED_KEY inherit it from the active context.
|
|
90
|
+
function withAmbientPropagated(ctx: Context): Context {
|
|
91
|
+
if (propagatedFromContext(ctx)) return ctx;
|
|
92
|
+
const ambient = propagatedFromContext(activeContext());
|
|
93
|
+
return ambient ? ctx.setValue(PROPAGATED_KEY, ambient) : ctx;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveParent(parent: ParentRef | undefined): Context {
|
|
97
|
+
const base = activeContext();
|
|
98
|
+
if (parent === undefined) return base;
|
|
99
|
+
if (typeof parent === "string") {
|
|
100
|
+
const parsed = parseTraceParent(parent);
|
|
101
|
+
if (!parsed) return base;
|
|
102
|
+
return trace.setSpanContext(base, { ...parsed, isRemote: true });
|
|
103
|
+
}
|
|
104
|
+
if (isSpanHandle(parent)) return withAmbientPropagated(parent.context);
|
|
105
|
+
if (isContext(parent)) return withAmbientPropagated(parent);
|
|
106
|
+
return trace.setSpanContext(base, parent);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function applyError(span: Span, error: unknown): void {
|
|
110
|
+
const err = error instanceof Error ? error : undefined;
|
|
111
|
+
const errorType = err?.name ?? "Error";
|
|
112
|
+
const message = err?.message ?? String(error);
|
|
113
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
114
|
+
span.setAttribute("error.type", errorType);
|
|
115
|
+
span.addEvent(
|
|
116
|
+
"exception",
|
|
117
|
+
omitUndefined({
|
|
118
|
+
"exception.type": errorType,
|
|
119
|
+
"exception.message": message,
|
|
120
|
+
"exception.stacktrace": err?.stack,
|
|
121
|
+
"log.severity_number": SEVERITY_ERROR,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function applyFields(span: Span, meta: SpanMeta, fields: SpanFields): void {
|
|
127
|
+
if (fields.name) span.updateName(fields.name);
|
|
128
|
+
const attrs = fieldsToAttributes(fields, meta.type, meta.capture);
|
|
129
|
+
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
130
|
+
if (fields.error !== undefined) applyError(span, fields.error);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function metaFor(core: ClientCore, options?: StartSpanOptions): SpanMeta {
|
|
134
|
+
const cfg = core.config;
|
|
135
|
+
return {
|
|
136
|
+
type: options?.type ?? "span",
|
|
137
|
+
capture: {
|
|
138
|
+
mask: cfg.mask,
|
|
139
|
+
maxAttributeLength: cfg.maxAttributeLength,
|
|
140
|
+
onError: cfg.onError,
|
|
141
|
+
captureInput: options?.captureInput ?? cfg.captureInput,
|
|
142
|
+
captureOutput: options?.captureOutput ?? cfg.captureOutput,
|
|
143
|
+
},
|
|
144
|
+
onError: cfg.onError,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createSpanHandle(
|
|
149
|
+
core: ClientCore,
|
|
150
|
+
name: string,
|
|
151
|
+
options: StartSpanOptions = {},
|
|
152
|
+
): SpanHandle {
|
|
153
|
+
const meta = metaFor(core, options);
|
|
154
|
+
const parentCtx = resolveParent(options.parent);
|
|
155
|
+
const span = core.tracer.startSpan(
|
|
156
|
+
name,
|
|
157
|
+
{
|
|
158
|
+
kind: SpanKind.INTERNAL,
|
|
159
|
+
startTime: options.startTime,
|
|
160
|
+
attributes: { "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type] },
|
|
161
|
+
},
|
|
162
|
+
parentCtx,
|
|
163
|
+
);
|
|
164
|
+
SPAN_META.set(span, meta);
|
|
165
|
+
applyFields(span, meta, options);
|
|
166
|
+
|
|
167
|
+
const spanContext = span.spanContext();
|
|
168
|
+
const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, "0");
|
|
169
|
+
const handle: SpanHandle = {
|
|
170
|
+
span,
|
|
171
|
+
context: trace.setSpan(parentCtx, span),
|
|
172
|
+
traceId: spanContext.traceId,
|
|
173
|
+
spanId: spanContext.spanId,
|
|
174
|
+
traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`,
|
|
175
|
+
get isRecording() {
|
|
176
|
+
return span.isRecording();
|
|
177
|
+
},
|
|
178
|
+
update(fields) {
|
|
179
|
+
try {
|
|
180
|
+
applyFields(span, meta, fields);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
reportError(meta.onError, error);
|
|
183
|
+
}
|
|
184
|
+
return handle;
|
|
185
|
+
},
|
|
186
|
+
end(fields) {
|
|
187
|
+
try {
|
|
188
|
+
if (fields) applyFields(span, meta, fields);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
reportError(meta.onError, error);
|
|
191
|
+
}
|
|
192
|
+
span.end(fields?.endTime);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
return handle;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Start a span WITHOUT activating context; the caller must call .end(). */
|
|
199
|
+
export function startSpan(name: string, options?: StartSpanOptions): SpanHandle {
|
|
200
|
+
const core = currentClient().core;
|
|
201
|
+
if (!core) return NOOP_SPAN_HANDLE;
|
|
202
|
+
try {
|
|
203
|
+
return createSpanHandle(core, name, options);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
reportError(core.config.onError, error);
|
|
206
|
+
return NOOP_SPAN_HANDLE;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function runWithHandle<T>(handle: SpanHandle, fn: (span: SpanHandle) => T): T {
|
|
211
|
+
try {
|
|
212
|
+
const result = withContext(handle.context, () => fn(handle));
|
|
213
|
+
if (isThenable(result)) {
|
|
214
|
+
return (result as unknown as Promise<unknown>).then(
|
|
215
|
+
(value) => {
|
|
216
|
+
handle.end();
|
|
217
|
+
return value;
|
|
218
|
+
},
|
|
219
|
+
(error: unknown) => {
|
|
220
|
+
handle.end({ error });
|
|
221
|
+
throw error;
|
|
222
|
+
},
|
|
223
|
+
) as T;
|
|
224
|
+
}
|
|
225
|
+
handle.end();
|
|
226
|
+
return result;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
handle.end({ error });
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function startActiveSpan<T>(name: string, fn: (span: SpanHandle) => T): T;
|
|
234
|
+
export function startActiveSpan<T>(
|
|
235
|
+
name: string,
|
|
236
|
+
options: StartSpanOptions,
|
|
237
|
+
fn: (span: SpanHandle) => T,
|
|
238
|
+
): T;
|
|
239
|
+
export function startActiveSpan<T>(
|
|
240
|
+
name: string,
|
|
241
|
+
optionsOrFn: StartSpanOptions | ((span: SpanHandle) => T),
|
|
242
|
+
maybeFn?: (span: SpanHandle) => T,
|
|
243
|
+
): T {
|
|
244
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn!;
|
|
245
|
+
const options = typeof optionsOrFn === "function" ? undefined : optionsOrFn;
|
|
246
|
+
const handle = startSpan(name, options);
|
|
247
|
+
if (!handle.isRecording) return fn(handle);
|
|
248
|
+
return runWithHandle(handle, fn);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Update the innermost active span; no-op when none is recording. */
|
|
252
|
+
export function updateActiveSpan(fields: SpanFields): void {
|
|
253
|
+
const core = currentClient().core;
|
|
254
|
+
if (!core) return;
|
|
255
|
+
try {
|
|
256
|
+
const span = trace.getSpan(activeContext());
|
|
257
|
+
if (!span?.isRecording()) return;
|
|
258
|
+
const meta = SPAN_META.get(span) ?? metaFor(core);
|
|
259
|
+
applyFields(span, meta, fields);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
reportError(core.config.onError, error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const PROPAGATOR = new W3CTraceContextPropagator();
|
|
266
|
+
|
|
267
|
+
/** Serialize the active span context as a W3C traceparent; null when there is none. */
|
|
268
|
+
export function getTraceparent(): string | null {
|
|
269
|
+
const carrier: { traceparent?: string } = {};
|
|
270
|
+
PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
|
|
271
|
+
return carrier.traceparent ?? null;
|
|
272
|
+
}
|