@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/dist/index.mjs
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { INVALID_SPAN_CONTEXT, ROOT_CONTEXT, SpanKind, SpanStatusCode, context, defaultTextMapSetter, propagation, trace } from "@opentelemetry/api";
|
|
2
|
+
import { W3CTraceContextPropagator, parseTraceParent } from "@opentelemetry/core";
|
|
3
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
|
+
import { BatchLogRecordProcessor, LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
5
|
+
import { BasicTracerProvider } from "@opentelemetry/sdk-trace-base";
|
|
6
|
+
import { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, PROPAGATED_KEY, SCOPE_NAME, SCOPE_VERSION, StampingSpanProcessor, activeContext, als, createLogExporter, createMetricExporter, createMetricsPipeline, createTraceExporter, diag, jsonAttr, omitUndefined, otlpHeaders, propagateAttributes, propagatedFromContext, reportError, resolveEnv, setLogLevel, withContext } from "@telemetry-dev/otel";
|
|
7
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
8
|
+
function resolveConfig(options = {}) {
|
|
9
|
+
const env = resolveEnv();
|
|
10
|
+
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
11
|
+
return {
|
|
12
|
+
apiKey: options.apiKey ?? env.TELEMETRY_DEV_API_KEY,
|
|
13
|
+
baseUrl,
|
|
14
|
+
environment: options.environment ?? env.TELEMETRY_DEV_ENVIRONMENT ?? "production",
|
|
15
|
+
serviceName: options.serviceName ?? env.OTEL_SERVICE_NAME ?? "unknown_service",
|
|
16
|
+
enabled: options.enabled ?? true,
|
|
17
|
+
registerGlobal: options.registerGlobal ?? false,
|
|
18
|
+
exportMode: options.exportMode ?? "batched",
|
|
19
|
+
captureInput: options.captureInput ?? true,
|
|
20
|
+
captureOutput: options.captureOutput ?? true,
|
|
21
|
+
mask: options.mask,
|
|
22
|
+
maxAttributeLength: options.maxAttributeLength ?? 65536,
|
|
23
|
+
batch: {
|
|
24
|
+
...DEFAULT_BATCH,
|
|
25
|
+
...options.batch
|
|
26
|
+
},
|
|
27
|
+
spanFilter: options.spanFilter,
|
|
28
|
+
resourceAttributes: options.resourceAttributes,
|
|
29
|
+
logLevel: options.logLevel ?? "warn",
|
|
30
|
+
fetchImpl: options.fetch ?? globalThis.fetch,
|
|
31
|
+
waitUntil: options.waitUntil,
|
|
32
|
+
onError: options.onError
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/client.ts
|
|
37
|
+
const NOOP_CLIENT = {
|
|
38
|
+
enabled: false,
|
|
39
|
+
flush: () => Promise.resolve(),
|
|
40
|
+
shutdown: () => Promise.resolve()
|
|
41
|
+
};
|
|
42
|
+
let activeClient;
|
|
43
|
+
function currentClient() {
|
|
44
|
+
return activeClient ?? NOOP_CLIENT;
|
|
45
|
+
}
|
|
46
|
+
function flush() {
|
|
47
|
+
return currentClient().flush();
|
|
48
|
+
}
|
|
49
|
+
function shutdown() {
|
|
50
|
+
return currentClient().shutdown();
|
|
51
|
+
}
|
|
52
|
+
function init(options, overrides) {
|
|
53
|
+
try {
|
|
54
|
+
return initInner(options, overrides);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
reportError(options?.onError, error);
|
|
57
|
+
activeClient = NOOP_CLIENT;
|
|
58
|
+
return NOOP_CLIENT;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function initInner(options, overrides) {
|
|
62
|
+
const config = resolveConfig(options);
|
|
63
|
+
setLogLevel(config.logLevel);
|
|
64
|
+
if (activeClient && activeClient !== NOOP_CLIENT) {
|
|
65
|
+
diag.warn("init() called again; replacing the previous client");
|
|
66
|
+
activeClient.shutdown().catch((error) => reportError(config.onError, error));
|
|
67
|
+
}
|
|
68
|
+
if (!(config.enabled && Boolean(config.apiKey ?? overrides?.spanExporter))) {
|
|
69
|
+
if (config.enabled && !config.apiKey) diag.debug("no api key (apiKey option or TELEMETRY_DEV_API_KEY); telemetry disabled");
|
|
70
|
+
activeClient = NOOP_CLIENT;
|
|
71
|
+
return NOOP_CLIENT;
|
|
72
|
+
}
|
|
73
|
+
const resource = resourceFromAttributes({
|
|
74
|
+
"service.name": config.serviceName,
|
|
75
|
+
"deployment.environment.name": config.environment,
|
|
76
|
+
...config.resourceAttributes
|
|
77
|
+
});
|
|
78
|
+
const transport = {
|
|
79
|
+
fetchImpl: config.fetchImpl,
|
|
80
|
+
onError: config.onError
|
|
81
|
+
};
|
|
82
|
+
const headers = config.apiKey ? otlpHeaders(config.apiKey) : void 0;
|
|
83
|
+
const spanExporter = overrides?.spanExporter ?? createTraceExporter({
|
|
84
|
+
url: `${config.baseUrl}/v1/traces`,
|
|
85
|
+
headers
|
|
86
|
+
}, transport);
|
|
87
|
+
const metricExporter = overrides?.metricExporter ?? (headers ? createMetricExporter({
|
|
88
|
+
url: `${config.baseUrl}/v1/metrics`,
|
|
89
|
+
headers
|
|
90
|
+
}, transport) : void 0);
|
|
91
|
+
const metrics = metricExporter ? createMetricsPipeline({
|
|
92
|
+
resource,
|
|
93
|
+
exporter: metricExporter,
|
|
94
|
+
exportIntervalMillis: config.exportMode === "batched" ? BATCHED_METRIC_INTERVAL_MS : DORMANT_INTERVAL_MS
|
|
95
|
+
}) : void 0;
|
|
96
|
+
const spanFilter = config.spanFilter ?? (config.registerGlobal ? (span) => span.instrumentationScope.name === SCOPE_NAME : void 0);
|
|
97
|
+
const provider = new BasicTracerProvider({
|
|
98
|
+
resource,
|
|
99
|
+
spanProcessors: [new StampingSpanProcessor({
|
|
100
|
+
exporter: spanExporter,
|
|
101
|
+
exportMode: config.exportMode,
|
|
102
|
+
batch: config.batch,
|
|
103
|
+
spanFilter,
|
|
104
|
+
recordMetrics: metrics ? (span) => metrics.record(span) : void 0,
|
|
105
|
+
onError: config.onError
|
|
106
|
+
})],
|
|
107
|
+
spanLimits: { attributeValueLengthLimit: config.maxAttributeLength }
|
|
108
|
+
});
|
|
109
|
+
const tracer = provider.getTracer(SCOPE_NAME, SCOPE_VERSION);
|
|
110
|
+
let registeredTrace = false;
|
|
111
|
+
let registeredContext = false;
|
|
112
|
+
let registeredPropagation = false;
|
|
113
|
+
if (config.registerGlobal) {
|
|
114
|
+
registeredTrace = trace.setGlobalTracerProvider(provider);
|
|
115
|
+
if (als) registeredContext = context.setGlobalContextManager(new AlsContextManager(als));
|
|
116
|
+
registeredPropagation = propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
117
|
+
if (!registeredTrace) diag.warn("a global TracerProvider is already registered; attach the TelemetrySpanProcessor from '@telemetry-dev/otel' to your own provider instead");
|
|
118
|
+
}
|
|
119
|
+
const logExporter = overrides?.logRecordExporter ?? (headers ? createLogExporter({
|
|
120
|
+
url: `${config.baseUrl}/v1/logs`,
|
|
121
|
+
headers
|
|
122
|
+
}, transport) : void 0);
|
|
123
|
+
let logs;
|
|
124
|
+
const ensureLogger = () => {
|
|
125
|
+
if (!logExporter) return void 0;
|
|
126
|
+
if (!logs) {
|
|
127
|
+
const logProcessor = config.exportMode === "immediate" ? new SimpleLogRecordProcessor(logExporter) : new BatchLogRecordProcessor(logExporter);
|
|
128
|
+
const loggerProvider = new LoggerProvider({
|
|
129
|
+
resource,
|
|
130
|
+
logRecordLimits: { attributeValueLengthLimit: config.maxAttributeLength },
|
|
131
|
+
processors: [logProcessor]
|
|
132
|
+
});
|
|
133
|
+
logs = {
|
|
134
|
+
logger: loggerProvider.getLogger(SCOPE_NAME, SCOPE_VERSION),
|
|
135
|
+
forceFlush: () => loggerProvider.forceFlush(),
|
|
136
|
+
shutdown: () => loggerProvider.shutdown()
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return logs.logger;
|
|
140
|
+
};
|
|
141
|
+
let torn = false;
|
|
142
|
+
const collect = (op) => {
|
|
143
|
+
const parts = [provider[op]()];
|
|
144
|
+
if (logs) parts.push(logs[op]());
|
|
145
|
+
if (metrics) parts.push(metrics[op]());
|
|
146
|
+
return Promise.all(parts).then(() => void 0).catch((error) => reportError(config.onError, error));
|
|
147
|
+
};
|
|
148
|
+
const handoff = (p) => {
|
|
149
|
+
if (config.waitUntil) {
|
|
150
|
+
config.waitUntil(p);
|
|
151
|
+
return Promise.resolve();
|
|
152
|
+
}
|
|
153
|
+
return p;
|
|
154
|
+
};
|
|
155
|
+
const handle = {
|
|
156
|
+
get enabled() {
|
|
157
|
+
return !torn;
|
|
158
|
+
},
|
|
159
|
+
flush: () => torn ? Promise.resolve() : handoff(collect("forceFlush")),
|
|
160
|
+
shutdown: () => {
|
|
161
|
+
if (torn) return Promise.resolve();
|
|
162
|
+
torn = true;
|
|
163
|
+
if (registeredTrace) trace.disable();
|
|
164
|
+
if (registeredContext) context.disable();
|
|
165
|
+
if (registeredPropagation) propagation.disable();
|
|
166
|
+
handle.core = void 0;
|
|
167
|
+
return handoff(collect("shutdown"));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
handle.core = {
|
|
171
|
+
config,
|
|
172
|
+
tracer,
|
|
173
|
+
ensureLogger
|
|
174
|
+
};
|
|
175
|
+
activeClient = handle;
|
|
176
|
+
return handle;
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/capture.ts
|
|
180
|
+
const TRUNCATION_MARKER = "...[truncated]";
|
|
181
|
+
function truncate(value, maxLength) {
|
|
182
|
+
if (value.length <= maxLength) return value;
|
|
183
|
+
return value.slice(0, Math.max(maxLength - 14, 0)) + TRUNCATION_MARKER;
|
|
184
|
+
}
|
|
185
|
+
/** The single content funnel: mask → JSON stringify → truncate. */
|
|
186
|
+
function prepareCaptureValue(key, value, cfg) {
|
|
187
|
+
let masked = value;
|
|
188
|
+
if (cfg.mask) try {
|
|
189
|
+
masked = cfg.mask(value, { key });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
reportError(cfg.onError, error);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const serialized = jsonAttr(masked);
|
|
195
|
+
if (serialized === void 0) return void 0;
|
|
196
|
+
return truncate(serialized, cfg.maxAttributeLength);
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/logs.ts
|
|
200
|
+
const SEVERITIES = {
|
|
201
|
+
debug: {
|
|
202
|
+
number: SeverityNumber.DEBUG,
|
|
203
|
+
text: "DEBUG"
|
|
204
|
+
},
|
|
205
|
+
info: {
|
|
206
|
+
number: SeverityNumber.INFO,
|
|
207
|
+
text: "INFO"
|
|
208
|
+
},
|
|
209
|
+
warn: {
|
|
210
|
+
number: SeverityNumber.WARN,
|
|
211
|
+
text: "WARN"
|
|
212
|
+
},
|
|
213
|
+
error: {
|
|
214
|
+
number: SeverityNumber.ERROR,
|
|
215
|
+
text: "ERROR"
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Emit a log record to /v1/logs, correlated to the active span (when any) and stamped with the
|
|
220
|
+
* propagated correlation attributes. Works inside and outside spans.
|
|
221
|
+
*/
|
|
222
|
+
function log(message, options) {
|
|
223
|
+
const core = currentClient().core;
|
|
224
|
+
if (!core) return;
|
|
225
|
+
try {
|
|
226
|
+
const logger = core.ensureLogger();
|
|
227
|
+
if (!logger) return;
|
|
228
|
+
const cfg = core.config;
|
|
229
|
+
const captureCfg = {
|
|
230
|
+
mask: cfg.mask,
|
|
231
|
+
maxAttributeLength: cfg.maxAttributeLength,
|
|
232
|
+
onError: cfg.onError
|
|
233
|
+
};
|
|
234
|
+
const severity = SEVERITIES[options?.level ?? "info"];
|
|
235
|
+
const ctx = activeContext();
|
|
236
|
+
const attributes = {};
|
|
237
|
+
for (const [key, value] of Object.entries(options?.attributes ?? {})) {
|
|
238
|
+
if (value === void 0) continue;
|
|
239
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
240
|
+
attributes[key] = value;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const prepared = prepareCaptureValue(key, value, captureCfg);
|
|
244
|
+
if (prepared !== void 0) attributes[key] = prepared;
|
|
245
|
+
}
|
|
246
|
+
const propagated = propagatedFromContext(ctx);
|
|
247
|
+
if (propagated) {
|
|
248
|
+
for (const [key, value] of Object.entries(propagated)) if (value !== void 0) attributes[key] = typeof value === "string" ? truncate(value, cfg.maxAttributeLength) : value;
|
|
249
|
+
}
|
|
250
|
+
logger.emit({
|
|
251
|
+
body: prepareCaptureValue("log.message", message, captureCfg) ?? "",
|
|
252
|
+
severityNumber: severity.number,
|
|
253
|
+
severityText: severity.text,
|
|
254
|
+
eventName: options?.eventName,
|
|
255
|
+
timestamp: options?.timestamp,
|
|
256
|
+
context: ctx,
|
|
257
|
+
attributes
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
reportError(core.config.onError, error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/attrs.ts
|
|
265
|
+
const SPAN_TYPE_OPERATIONS = {
|
|
266
|
+
span: "function",
|
|
267
|
+
generation: "chat",
|
|
268
|
+
tool: "execute_tool",
|
|
269
|
+
agent: "invoke_agent",
|
|
270
|
+
embedding: "embeddings"
|
|
271
|
+
};
|
|
272
|
+
function inputKeyFor(type) {
|
|
273
|
+
return type === "tool" ? "gen_ai.tool.call.arguments" : "gen_ai.input.messages";
|
|
274
|
+
}
|
|
275
|
+
function outputKeyFor(type) {
|
|
276
|
+
return type === "tool" ? "gen_ai.tool.call.result" : "gen_ai.output.messages";
|
|
277
|
+
}
|
|
278
|
+
const RESERVED_METADATA_KEYS = new Set([
|
|
279
|
+
"userId",
|
|
280
|
+
"sessionId",
|
|
281
|
+
"user_id",
|
|
282
|
+
"session_id"
|
|
283
|
+
]);
|
|
284
|
+
function fieldsToAttributes(fields, type, cfg) {
|
|
285
|
+
const attrs = omitUndefined({
|
|
286
|
+
"gen_ai.request.model": fields.model,
|
|
287
|
+
"gen_ai.provider.name": fields.provider,
|
|
288
|
+
"gen_ai.response.model": fields.responseModel,
|
|
289
|
+
"gen_ai.response.id": fields.responseId,
|
|
290
|
+
"gen_ai.usage.input_tokens": fields.usage?.inputTokens,
|
|
291
|
+
"gen_ai.usage.output_tokens": fields.usage?.outputTokens,
|
|
292
|
+
"gen_ai.usage.total_tokens": fields.usage?.totalTokens,
|
|
293
|
+
"gen_ai.usage.cache_read.input_tokens": fields.usage?.cacheReadInputTokens,
|
|
294
|
+
"gen_ai.usage.cache_creation.input_tokens": fields.usage?.cacheCreationInputTokens,
|
|
295
|
+
"gen_ai.usage.reasoning.output_tokens": fields.usage?.reasoningOutputTokens,
|
|
296
|
+
"gen_ai.usage.cost": fields.costUsd,
|
|
297
|
+
"gen_ai.response.finish_reasons": fields.finishReason ? [fields.finishReason] : void 0,
|
|
298
|
+
"gen_ai.output.type": fields.outputType,
|
|
299
|
+
"gen_ai.request.temperature": fields.temperature,
|
|
300
|
+
"gen_ai.request.top_p": fields.topP,
|
|
301
|
+
"gen_ai.request.top_k": fields.topK,
|
|
302
|
+
"gen_ai.request.max_tokens": fields.maxTokens,
|
|
303
|
+
"gen_ai.request.stop_sequences": fields.stopSequences,
|
|
304
|
+
"gen_ai.request.seed": fields.seed,
|
|
305
|
+
"gen_ai.request.frequency_penalty": fields.frequencyPenalty,
|
|
306
|
+
"gen_ai.request.presence_penalty": fields.presencePenalty,
|
|
307
|
+
"gen_ai.response.time_to_first_chunk": fields.timeToFirstChunkMs !== void 0 ? fields.timeToFirstChunkMs / 1e3 : void 0,
|
|
308
|
+
"gen_ai.tool.name": fields.toolName,
|
|
309
|
+
"gen_ai.tool.call.id": fields.toolCallId,
|
|
310
|
+
"gen_ai.tool.description": fields.toolDescription,
|
|
311
|
+
"gen_ai.agent.name": fields.agentName,
|
|
312
|
+
"gen_ai.agent.id": fields.agentId
|
|
313
|
+
});
|
|
314
|
+
if (fields.systemInstructions !== void 0 && cfg.captureInput) {
|
|
315
|
+
const value = prepareCaptureValue("gen_ai.system_instructions", fields.systemInstructions, cfg);
|
|
316
|
+
if (value !== void 0) attrs["gen_ai.system_instructions"] = value;
|
|
317
|
+
}
|
|
318
|
+
if (fields.input !== void 0 && cfg.captureInput) {
|
|
319
|
+
const key = inputKeyFor(type);
|
|
320
|
+
const value = prepareCaptureValue(key, fields.input, cfg);
|
|
321
|
+
if (value !== void 0) attrs[key] = value;
|
|
322
|
+
}
|
|
323
|
+
if (fields.output !== void 0 && cfg.captureOutput) {
|
|
324
|
+
const key = outputKeyFor(type);
|
|
325
|
+
const value = prepareCaptureValue(key, fields.output, cfg);
|
|
326
|
+
if (value !== void 0) attrs[key] = value;
|
|
327
|
+
}
|
|
328
|
+
if (fields.metadata) for (const [key, value] of Object.entries(fields.metadata)) {
|
|
329
|
+
if (RESERVED_METADATA_KEYS.has(key)) {
|
|
330
|
+
diag.debug(`metadata key "${key}" is reserved; pass it via propagateAttributes instead`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const attr = typeof value === "string" ? value : jsonAttr(value);
|
|
334
|
+
if (attr !== void 0) attrs[`td.metadata.${key}`] = truncate(attr, cfg.maxAttributeLength);
|
|
335
|
+
}
|
|
336
|
+
if (fields.attributes) Object.assign(attrs, omitUndefined(fields.attributes));
|
|
337
|
+
return attrs;
|
|
338
|
+
}
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region src/noop.ts
|
|
341
|
+
const noopSpan = trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
|
|
342
|
+
/** Shared inert handle returned by span primitives before init() or when disabled. */
|
|
343
|
+
const NOOP_SPAN_HANDLE = {
|
|
344
|
+
span: noopSpan,
|
|
345
|
+
context: trace.setSpan(ROOT_CONTEXT, noopSpan),
|
|
346
|
+
traceId: INVALID_SPAN_CONTEXT.traceId,
|
|
347
|
+
spanId: INVALID_SPAN_CONTEXT.spanId,
|
|
348
|
+
traceparent: null,
|
|
349
|
+
isRecording: false,
|
|
350
|
+
update: () => NOOP_SPAN_HANDLE,
|
|
351
|
+
end: () => {}
|
|
352
|
+
};
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/span.ts
|
|
355
|
+
const SPAN_META = /* @__PURE__ */ new WeakMap();
|
|
356
|
+
const SEVERITY_ERROR = 17;
|
|
357
|
+
function isThenable(value) {
|
|
358
|
+
return value != null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function";
|
|
359
|
+
}
|
|
360
|
+
function isContext(value) {
|
|
361
|
+
return "getValue" in value && typeof value.getValue === "function";
|
|
362
|
+
}
|
|
363
|
+
function isSpanHandle(value) {
|
|
364
|
+
return "span" in value && "context" in value && "update" in value;
|
|
365
|
+
}
|
|
366
|
+
function withAmbientPropagated(ctx) {
|
|
367
|
+
if (propagatedFromContext(ctx)) return ctx;
|
|
368
|
+
const ambient = propagatedFromContext(activeContext());
|
|
369
|
+
return ambient ? ctx.setValue(PROPAGATED_KEY, ambient) : ctx;
|
|
370
|
+
}
|
|
371
|
+
function resolveParent(parent) {
|
|
372
|
+
const base = activeContext();
|
|
373
|
+
if (parent === void 0) return base;
|
|
374
|
+
if (typeof parent === "string") {
|
|
375
|
+
const parsed = parseTraceParent(parent);
|
|
376
|
+
if (!parsed) return base;
|
|
377
|
+
return trace.setSpanContext(base, {
|
|
378
|
+
...parsed,
|
|
379
|
+
isRemote: true
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
if (isSpanHandle(parent)) return withAmbientPropagated(parent.context);
|
|
383
|
+
if (isContext(parent)) return withAmbientPropagated(parent);
|
|
384
|
+
return trace.setSpanContext(base, parent);
|
|
385
|
+
}
|
|
386
|
+
function applyError(span, error) {
|
|
387
|
+
const err = error instanceof Error ? error : void 0;
|
|
388
|
+
const errorType = err?.name ?? "Error";
|
|
389
|
+
const message = err?.message ?? String(error);
|
|
390
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
391
|
+
span.setAttribute("error.type", errorType);
|
|
392
|
+
span.addEvent("exception", omitUndefined({
|
|
393
|
+
"exception.type": errorType,
|
|
394
|
+
"exception.message": message,
|
|
395
|
+
"exception.stacktrace": err?.stack,
|
|
396
|
+
"log.severity_number": SEVERITY_ERROR
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
function applyFields(span, meta, fields) {
|
|
400
|
+
if (fields.name) span.updateName(fields.name);
|
|
401
|
+
const attrs = fieldsToAttributes(fields, meta.type, meta.capture);
|
|
402
|
+
if (Object.keys(attrs).length > 0) span.setAttributes(attrs);
|
|
403
|
+
if (fields.error !== void 0) applyError(span, fields.error);
|
|
404
|
+
}
|
|
405
|
+
function metaFor(core, options) {
|
|
406
|
+
const cfg = core.config;
|
|
407
|
+
return {
|
|
408
|
+
type: options?.type ?? "span",
|
|
409
|
+
capture: {
|
|
410
|
+
mask: cfg.mask,
|
|
411
|
+
maxAttributeLength: cfg.maxAttributeLength,
|
|
412
|
+
onError: cfg.onError,
|
|
413
|
+
captureInput: options?.captureInput ?? cfg.captureInput,
|
|
414
|
+
captureOutput: options?.captureOutput ?? cfg.captureOutput
|
|
415
|
+
},
|
|
416
|
+
onError: cfg.onError
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function createSpanHandle(core, name, options = {}) {
|
|
420
|
+
const meta = metaFor(core, options);
|
|
421
|
+
const parentCtx = resolveParent(options.parent);
|
|
422
|
+
const span = core.tracer.startSpan(name, {
|
|
423
|
+
kind: SpanKind.INTERNAL,
|
|
424
|
+
startTime: options.startTime,
|
|
425
|
+
attributes: { "gen_ai.operation.name": SPAN_TYPE_OPERATIONS[meta.type] }
|
|
426
|
+
}, parentCtx);
|
|
427
|
+
SPAN_META.set(span, meta);
|
|
428
|
+
applyFields(span, meta, options);
|
|
429
|
+
const spanContext = span.spanContext();
|
|
430
|
+
const flags = (spanContext.traceFlags & 255).toString(16).padStart(2, "0");
|
|
431
|
+
const handle = {
|
|
432
|
+
span,
|
|
433
|
+
context: trace.setSpan(parentCtx, span),
|
|
434
|
+
traceId: spanContext.traceId,
|
|
435
|
+
spanId: spanContext.spanId,
|
|
436
|
+
traceparent: `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`,
|
|
437
|
+
get isRecording() {
|
|
438
|
+
return span.isRecording();
|
|
439
|
+
},
|
|
440
|
+
update(fields) {
|
|
441
|
+
try {
|
|
442
|
+
applyFields(span, meta, fields);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
reportError(meta.onError, error);
|
|
445
|
+
}
|
|
446
|
+
return handle;
|
|
447
|
+
},
|
|
448
|
+
end(fields) {
|
|
449
|
+
try {
|
|
450
|
+
if (fields) applyFields(span, meta, fields);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
reportError(meta.onError, error);
|
|
453
|
+
}
|
|
454
|
+
span.end(fields?.endTime);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
return handle;
|
|
458
|
+
}
|
|
459
|
+
/** Start a span WITHOUT activating context; the caller must call .end(). */
|
|
460
|
+
function startSpan(name, options) {
|
|
461
|
+
const core = currentClient().core;
|
|
462
|
+
if (!core) return NOOP_SPAN_HANDLE;
|
|
463
|
+
try {
|
|
464
|
+
return createSpanHandle(core, name, options);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
reportError(core.config.onError, error);
|
|
467
|
+
return NOOP_SPAN_HANDLE;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function runWithHandle(handle, fn) {
|
|
471
|
+
try {
|
|
472
|
+
const result = withContext(handle.context, () => fn(handle));
|
|
473
|
+
if (isThenable(result)) return result.then((value) => {
|
|
474
|
+
handle.end();
|
|
475
|
+
return value;
|
|
476
|
+
}, (error) => {
|
|
477
|
+
handle.end({ error });
|
|
478
|
+
throw error;
|
|
479
|
+
});
|
|
480
|
+
handle.end();
|
|
481
|
+
return result;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
handle.end({ error });
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function startActiveSpan(name, optionsOrFn, maybeFn) {
|
|
488
|
+
const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
|
|
489
|
+
const handle = startSpan(name, typeof optionsOrFn === "function" ? void 0 : optionsOrFn);
|
|
490
|
+
if (!handle.isRecording) return fn(handle);
|
|
491
|
+
return runWithHandle(handle, fn);
|
|
492
|
+
}
|
|
493
|
+
/** Update the innermost active span; no-op when none is recording. */
|
|
494
|
+
function updateActiveSpan(fields) {
|
|
495
|
+
const core = currentClient().core;
|
|
496
|
+
if (!core) return;
|
|
497
|
+
try {
|
|
498
|
+
const span = trace.getSpan(activeContext());
|
|
499
|
+
if (!span?.isRecording()) return;
|
|
500
|
+
applyFields(span, SPAN_META.get(span) ?? metaFor(core), fields);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
reportError(core.config.onError, error);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const PROPAGATOR = new W3CTraceContextPropagator();
|
|
506
|
+
/** Serialize the active span context as a W3C traceparent; null when there is none. */
|
|
507
|
+
function getTraceparent() {
|
|
508
|
+
const carrier = {};
|
|
509
|
+
PROPAGATOR.inject(activeContext(), carrier, defaultTextMapSetter);
|
|
510
|
+
return carrier.traceparent ?? null;
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/observe.ts
|
|
514
|
+
/**
|
|
515
|
+
* Wrap a function (sync or async) so every call becomes a span: args → input, return value →
|
|
516
|
+
* output, thrown/rejected errors captured and rethrown. Activates context so nested SDK calls
|
|
517
|
+
* parent to it. Resolves the client at call time, so wrapping at module load before init() works.
|
|
518
|
+
*/
|
|
519
|
+
function observe(fn, options) {
|
|
520
|
+
const wrapped = function(...args) {
|
|
521
|
+
const core = currentClient().core;
|
|
522
|
+
if (!core) return fn.apply(this, args);
|
|
523
|
+
let handle;
|
|
524
|
+
try {
|
|
525
|
+
const name = options?.name ?? (fn.name || "anonymous");
|
|
526
|
+
const input = options?.input !== void 0 ? options.input : args.length === 0 ? void 0 : args.length === 1 ? args[0] : args;
|
|
527
|
+
handle = createSpanHandle(core, name, {
|
|
528
|
+
...options,
|
|
529
|
+
input
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
reportError(core.config.onError, error);
|
|
533
|
+
return fn.apply(this, args);
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const result = withContext(handle.context, () => fn.apply(this, args));
|
|
537
|
+
if (isThenable(result)) return result.then((value) => {
|
|
538
|
+
handle.end({ output: value });
|
|
539
|
+
return value;
|
|
540
|
+
}, (error) => {
|
|
541
|
+
handle.end({ error });
|
|
542
|
+
throw error;
|
|
543
|
+
});
|
|
544
|
+
handle.end({ output: result });
|
|
545
|
+
return result;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
handle.end({ error });
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
Object.defineProperty(wrapped, "name", {
|
|
552
|
+
value: fn.name || "anonymous",
|
|
553
|
+
configurable: true
|
|
554
|
+
});
|
|
555
|
+
return wrapped;
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
export { flush, getTraceparent, init, log, observe, propagateAttributes, shutdown, startActiveSpan, startSpan, updateActiveSpan };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@telemetry-dev/sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pure TypeScript SDK for telemetry.dev: LLM traces, logs, and GenAI metrics on OpenTelemetry, for Node and edge runtimes.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"genai",
|
|
7
|
+
"llm",
|
|
8
|
+
"observability",
|
|
9
|
+
"opentelemetry",
|
|
10
|
+
"otel",
|
|
11
|
+
"sdk",
|
|
12
|
+
"telemetry",
|
|
13
|
+
"tracing"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://telemetry.dev",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/telemetry-dev/telemetry.dev.git",
|
|
20
|
+
"directory": "packages/sdk"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"import": "./dist/index.mjs",
|
|
31
|
+
"default": "./dist/index.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@opentelemetry/api-logs": "^0.218.0",
|
|
40
|
+
"@opentelemetry/core": "^2.7.1",
|
|
41
|
+
"@opentelemetry/resources": "^2.7.1",
|
|
42
|
+
"@opentelemetry/sdk-logs": "^0.218.0",
|
|
43
|
+
"@opentelemetry/sdk-metrics": "^2.7.1",
|
|
44
|
+
"@opentelemetry/sdk-trace-base": "^2.7.1",
|
|
45
|
+
"@telemetry-dev/otel": "^0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@opentelemetry/api": "^1.9.1",
|
|
49
|
+
"@types/node": "^25.5.0",
|
|
50
|
+
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
51
|
+
"typescript": "^6.0.2",
|
|
52
|
+
"vite-plus": "0.1.20",
|
|
53
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@opentelemetry/api": ">=1.9.0 <2"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20.19.0"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "pnpm exec vp pack",
|
|
63
|
+
"dev": "pnpm exec vp pack --watch",
|
|
64
|
+
"test": "vp test",
|
|
65
|
+
"check": "vp check"
|
|
66
|
+
}
|
|
67
|
+
}
|