@telemetry-dev/otel 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/dist/index.mjs ADDED
@@ -0,0 +1,553 @@
1
+ import { ROOT_CONTEXT, context, createContextKey } from "@opentelemetry/api";
2
+ import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
3
+ import { ExportResultCode } from "@opentelemetry/core";
4
+ import { resourceFromAttributes } from "@opentelemetry/resources";
5
+ import { BatchSpanProcessor, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
6
+ import { ProtobufLogsSerializer, ProtobufMetricsSerializer, ProtobufTraceSerializer } from "@opentelemetry/otlp-transformer";
7
+ //#region src/attrs.ts
8
+ const SCOPE_NAME = "@telemetry-dev/sdk";
9
+ const SCOPE_VERSION = "0.0.0";
10
+ function omitUndefined(attributes) {
11
+ const out = {};
12
+ for (const key of Object.keys(attributes)) {
13
+ const value = attributes[key];
14
+ if (value !== void 0) out[key] = value;
15
+ }
16
+ return out;
17
+ }
18
+ function jsonAttr(value) {
19
+ if (value === void 0) return void 0;
20
+ if (typeof value === "string") return value;
21
+ try {
22
+ return JSON.stringify(value);
23
+ } catch {
24
+ return;
25
+ }
26
+ }
27
+ //#endregion
28
+ //#region src/config.ts
29
+ const DEFAULT_BASE_URL = "https://ingest.telemetry.dev";
30
+ const DEFAULT_BATCH = {
31
+ maxExportBatchSize: 64,
32
+ scheduledDelayMillis: 1e3,
33
+ maxQueueSize: 2048,
34
+ exportTimeoutMillis: 3e4
35
+ };
36
+ function resolveEnv() {
37
+ if (typeof process !== "undefined" && process.env) return process.env;
38
+ return {};
39
+ }
40
+ //#endregion
41
+ //#region src/debug.ts
42
+ const ORDER = {
43
+ debug: 0,
44
+ info: 1,
45
+ warn: 2,
46
+ error: 3,
47
+ silent: 4
48
+ };
49
+ let currentLevel = "warn";
50
+ function setLogLevel(level) {
51
+ currentLevel = level;
52
+ }
53
+ function emit(level, args) {
54
+ if (ORDER[level] < ORDER[currentLevel]) return;
55
+ console[level]("[telemetry.dev]", ...args);
56
+ }
57
+ const diag = {
58
+ debug: (...args) => emit("debug", args),
59
+ info: (...args) => emit("info", args),
60
+ warn: (...args) => emit("warn", args),
61
+ error: (...args) => emit("error", args)
62
+ };
63
+ /** Fail-open guard: SDK internals report through onError + diagnostics, never into user code. */
64
+ function reportError(onError, error) {
65
+ try {
66
+ onError?.(error);
67
+ } catch {}
68
+ diag.error(error);
69
+ }
70
+ //#endregion
71
+ //#region src/context.ts
72
+ function loadAls() {
73
+ const g = globalThis;
74
+ if (g.AsyncLocalStorage) return g.AsyncLocalStorage;
75
+ try {
76
+ return g.process?.getBuiltinModule?.("node:async_hooks")?.AsyncLocalStorage;
77
+ } catch {}
78
+ }
79
+ const AlsCtor = loadAls();
80
+ const als = AlsCtor ? new AlsCtor() : void 0;
81
+ /**
82
+ * The active context: our AsyncLocalStorage when available, falling back to the OTel global
83
+ * context (which joins a host app's own OTel setup when one is registered).
84
+ */
85
+ function activeContext() {
86
+ return als?.getStore() ?? context.active();
87
+ }
88
+ /** Run `fn` with `ctx` active in both our ALS and the global OTel context manager. */
89
+ function withContext(ctx, fn) {
90
+ const run = () => context.with(ctx, fn);
91
+ return als ? als.run(ctx, run) : run();
92
+ }
93
+ const PROPAGATED_KEY = createContextKey("telemetry.dev propagated attributes");
94
+ const RESERVED_METADATA_KEYS = new Set([
95
+ "userId",
96
+ "sessionId",
97
+ "user_id",
98
+ "session_id"
99
+ ]);
100
+ function buildPropagatedAttributes(attrs) {
101
+ const out = {};
102
+ if (attrs.userId !== void 0) out["user.id"] = attrs.userId;
103
+ if (attrs.sessionId !== void 0) out["gen_ai.conversation.id"] = attrs.sessionId;
104
+ if (attrs.metadata) for (const [key, value] of Object.entries(attrs.metadata)) {
105
+ if (RESERVED_METADATA_KEYS.has(key)) {
106
+ diag.debug(`metadata key "${key}" is reserved; use the userId/sessionId fields of propagateAttributes`);
107
+ continue;
108
+ }
109
+ const attr = typeof value === "string" ? value : jsonAttr(value);
110
+ if (attr !== void 0) out[`td.metadata.${key}`] = attr;
111
+ }
112
+ return out;
113
+ }
114
+ function propagatedFromContext(ctx) {
115
+ return ctx.getValue(PROPAGATED_KEY);
116
+ }
117
+ /**
118
+ * Stamp correlation attributes (user, session/conversation, metadata) on every span and log
119
+ * record created inside `fn`. Inner scopes merge over outer ones per key. Works before init().
120
+ */
121
+ function propagateAttributes(attributes, fn) {
122
+ const base = activeContext();
123
+ const merged = {
124
+ ...propagatedFromContext(base),
125
+ ...buildPropagatedAttributes(attributes)
126
+ };
127
+ return withContext(base.setValue(PROPAGATED_KEY, merged), fn);
128
+ }
129
+ /** Minimal ContextManager over our AsyncLocalStorage, registered only for registerGlobal. */
130
+ var AlsContextManager = class {
131
+ constructor(storage) {
132
+ this.storage = storage;
133
+ }
134
+ active() {
135
+ return this.storage.getStore() ?? ROOT_CONTEXT;
136
+ }
137
+ with(context, fn, thisArg, ...args) {
138
+ const cb = thisArg == null ? fn : fn.bind(thisArg);
139
+ return this.storage.run(context, cb, ...args);
140
+ }
141
+ bind(context, target) {
142
+ if (typeof target === "function") {
143
+ const storage = this.storage;
144
+ const bound = function(...args) {
145
+ return storage.run(context, () => target.apply(this, args));
146
+ };
147
+ return bound;
148
+ }
149
+ return target;
150
+ }
151
+ enable() {
152
+ return this;
153
+ }
154
+ disable() {
155
+ this.storage.disable();
156
+ return this;
157
+ }
158
+ };
159
+ //#endregion
160
+ //#region src/metrics.ts
161
+ const DURATION_BUCKETS = [
162
+ .01,
163
+ .02,
164
+ .04,
165
+ .08,
166
+ .16,
167
+ .32,
168
+ .64,
169
+ 1.28,
170
+ 2.56,
171
+ 5.12,
172
+ 10.24,
173
+ 20.48,
174
+ 40.96,
175
+ 81.92
176
+ ];
177
+ const TOKEN_BUCKETS = [
178
+ 1,
179
+ 4,
180
+ 16,
181
+ 64,
182
+ 256,
183
+ 1024,
184
+ 4096,
185
+ 16384,
186
+ 65536,
187
+ 262144,
188
+ 1048576,
189
+ 4194304,
190
+ 16777216,
191
+ 67108864
192
+ ];
193
+ const DURATION_OPERATIONS = new Set([
194
+ "chat",
195
+ "invoke_agent",
196
+ "embeddings",
197
+ "execute_tool"
198
+ ]);
199
+ const TOKEN_OPERATIONS = new Set([
200
+ "chat",
201
+ "invoke_agent",
202
+ "embeddings"
203
+ ]);
204
+ const DORMANT_INTERVAL_MS = 2 ** 31 - 1;
205
+ const BATCHED_METRIC_INTERVAL_MS = 6e4;
206
+ function stringAttr(value) {
207
+ return typeof value === "string" ? value : void 0;
208
+ }
209
+ function createMetricsPipeline({ resource, exporter, exportIntervalMillis }) {
210
+ const reader = new PeriodicExportingMetricReader({
211
+ exporter,
212
+ exportIntervalMillis
213
+ });
214
+ const meterProvider = new MeterProvider({
215
+ resource,
216
+ readers: [reader]
217
+ });
218
+ const meter = meterProvider.getMeter(SCOPE_NAME, SCOPE_VERSION);
219
+ const durationHistogram = meter.createHistogram("gen_ai.client.operation.duration", {
220
+ unit: "s",
221
+ advice: { explicitBucketBoundaries: DURATION_BUCKETS }
222
+ });
223
+ const tokenHistogram = meter.createHistogram("gen_ai.client.token.usage", {
224
+ unit: "{token}",
225
+ advice: { explicitBucketBoundaries: TOKEN_BUCKETS }
226
+ });
227
+ const record = (span) => {
228
+ const operation = span.attributes["gen_ai.operation.name"];
229
+ if (typeof operation !== "string" || !DURATION_OPERATIONS.has(operation)) return;
230
+ const attrs = omitUndefined({
231
+ "gen_ai.operation.name": operation,
232
+ "gen_ai.provider.name": stringAttr(span.attributes["gen_ai.provider.name"]),
233
+ "gen_ai.request.model": stringAttr(span.attributes["gen_ai.request.model"]),
234
+ "gen_ai.response.model": stringAttr(span.attributes["gen_ai.response.model"])
235
+ });
236
+ const durationSec = span.duration[0] + span.duration[1] / 1e9;
237
+ const errorType = stringAttr(span.attributes["error.type"]);
238
+ durationHistogram.record(durationSec, errorType !== void 0 ? {
239
+ ...attrs,
240
+ "error.type": errorType
241
+ } : attrs);
242
+ if (!TOKEN_OPERATIONS.has(operation)) return;
243
+ const inputTokens = span.attributes["gen_ai.usage.input_tokens"];
244
+ if (typeof inputTokens === "number") tokenHistogram.record(inputTokens, {
245
+ ...attrs,
246
+ "gen_ai.token.type": "input"
247
+ });
248
+ const outputTokens = span.attributes["gen_ai.usage.output_tokens"];
249
+ if (typeof outputTokens === "number") tokenHistogram.record(outputTokens, {
250
+ ...attrs,
251
+ "gen_ai.token.type": "output"
252
+ });
253
+ };
254
+ return {
255
+ record,
256
+ forceFlush: () => reader.forceFlush(),
257
+ shutdown: () => meterProvider.shutdown()
258
+ };
259
+ }
260
+ //#endregion
261
+ //#region src/processor.ts
262
+ /**
263
+ * The vendor span processor: stamps propagated correlation attributes onto every span at start,
264
+ * then filters, records auto-metrics, and delegates to a Batch/SimpleSpanProcessor at end.
265
+ */
266
+ var StampingSpanProcessor = class {
267
+ inner;
268
+ constructor(options) {
269
+ this.options = options;
270
+ this.inner = options.exportMode === "immediate" ? new SimpleSpanProcessor(options.exporter) : new BatchSpanProcessor(options.exporter, options.batch);
271
+ }
272
+ onStart(span, parentContext) {
273
+ try {
274
+ const propagated = parentContext.getValue(PROPAGATED_KEY) ?? propagatedFromContext(activeContext());
275
+ if (propagated) span.setAttributes(propagated);
276
+ } catch (error) {
277
+ reportError(this.options.onError, error);
278
+ }
279
+ this.inner.onStart(span, parentContext);
280
+ }
281
+ onEnd(span) {
282
+ try {
283
+ if (this.options.spanFilter && !this.options.spanFilter(span)) return;
284
+ } catch (error) {
285
+ reportError(this.options.onError, error);
286
+ }
287
+ try {
288
+ this.options.recordMetrics?.(span);
289
+ } catch (error) {
290
+ reportError(this.options.onError, error);
291
+ }
292
+ this.inner.onEnd(span);
293
+ }
294
+ forceFlush() {
295
+ return this.inner.forceFlush();
296
+ }
297
+ shutdown() {
298
+ return this.inner.shutdown();
299
+ }
300
+ };
301
+ //#endregion
302
+ //#region src/transport.ts
303
+ const RETRY_DELAYS_MS = [100, 500];
304
+ const RETRYABLE_STATUSES = new Set([
305
+ 429,
306
+ 502,
307
+ 503,
308
+ 504
309
+ ]);
310
+ const isRetryableStatus = (status) => RETRYABLE_STATUSES.has(status);
311
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
312
+ const cancelBody = async (res) => {
313
+ try {
314
+ await res.body?.cancel();
315
+ } catch {}
316
+ };
317
+ const postOtlp = async ({ fetchImpl, url, headers, body }) => {
318
+ for (let attempt = 0;; attempt += 1) {
319
+ try {
320
+ const res = await fetchImpl(url, {
321
+ method: "POST",
322
+ headers,
323
+ body
324
+ });
325
+ if (res.ok || !isRetryableStatus(res.status) || attempt === RETRY_DELAYS_MS.length) {
326
+ await cancelBody(res);
327
+ return res;
328
+ }
329
+ await cancelBody(res);
330
+ } catch (error) {
331
+ if (attempt === RETRY_DELAYS_MS.length) throw error;
332
+ }
333
+ await delay(RETRY_DELAYS_MS[attempt]);
334
+ }
335
+ };
336
+ const GZIP_THRESHOLD_BYTES = 1024;
337
+ async function maybeGzip(body) {
338
+ if (body.byteLength <= GZIP_THRESHOLD_BYTES || typeof CompressionStream === "undefined") return { body };
339
+ try {
340
+ const stream = new Blob([body]).stream().pipeThrough(new CompressionStream("gzip"));
341
+ return {
342
+ body: new Uint8Array(await new Response(stream).arrayBuffer()),
343
+ contentEncoding: "gzip"
344
+ };
345
+ } catch {
346
+ return { body };
347
+ }
348
+ }
349
+ const MAX_BODY_BYTES = 35e5;
350
+ function createOtlpBatchSender(serializer, target, transport, label) {
351
+ const send = async (items) => {
352
+ const body = serializer.serializeRequest(items);
353
+ if (!body || body.byteLength === 0) return;
354
+ if (body.byteLength > MAX_BODY_BYTES) {
355
+ if (items.length > 1) {
356
+ const mid = Math.ceil(items.length / 2);
357
+ await send(items.slice(0, mid));
358
+ await send(items.slice(mid));
359
+ return;
360
+ }
361
+ reportError(transport.onError, /* @__PURE__ */ new Error(`telemetry.dev: ${label} record exceeds max export size, dropped`));
362
+ return;
363
+ }
364
+ const { body: finalBody, contentEncoding } = await maybeGzip(body);
365
+ const headers = contentEncoding ? {
366
+ ...target.headers,
367
+ "content-encoding": contentEncoding
368
+ } : target.headers;
369
+ const res = await postOtlp({
370
+ fetchImpl: transport.fetchImpl,
371
+ url: target.url,
372
+ headers,
373
+ body: finalBody
374
+ });
375
+ if (!res.ok) throw new Error(`telemetry.dev ${label} ingest failed: ${res.status}`);
376
+ };
377
+ return send;
378
+ }
379
+ function createTraceExporter(target, transport) {
380
+ const send = createOtlpBatchSender(ProtobufTraceSerializer, target, transport, "trace");
381
+ return {
382
+ export(spans, resultCallback) {
383
+ send(spans).then(() => resultCallback({ code: ExportResultCode.SUCCESS }), (error) => {
384
+ reportError(transport.onError, error);
385
+ resultCallback({
386
+ code: ExportResultCode.FAILED,
387
+ error: error instanceof Error ? error : void 0
388
+ });
389
+ });
390
+ },
391
+ forceFlush: () => Promise.resolve(),
392
+ shutdown: () => Promise.resolve()
393
+ };
394
+ }
395
+ function createLogExporter(target, transport) {
396
+ const send = createOtlpBatchSender(ProtobufLogsSerializer, target, transport, "log");
397
+ return {
398
+ export(logs, resultCallback) {
399
+ send(logs).then(() => resultCallback({ code: ExportResultCode.SUCCESS }), (error) => {
400
+ reportError(transport.onError, error);
401
+ resultCallback({
402
+ code: ExportResultCode.FAILED,
403
+ error: error instanceof Error ? error : void 0
404
+ });
405
+ });
406
+ },
407
+ forceFlush: () => Promise.resolve(),
408
+ shutdown: () => Promise.resolve()
409
+ };
410
+ }
411
+ function createMetricExporter(target, transport) {
412
+ const { fetchImpl, onError } = transport;
413
+ return {
414
+ export(resourceMetrics, resultCallback) {
415
+ if (!resourceMetrics.scopeMetrics.some((scope) => scope.metrics.some((metric) => metric.dataPoints.length > 0))) {
416
+ resultCallback({ code: 0 });
417
+ return;
418
+ }
419
+ const body = ProtobufMetricsSerializer.serializeRequest(resourceMetrics);
420
+ if (!body || body.byteLength === 0) {
421
+ resultCallback({ code: 0 });
422
+ return;
423
+ }
424
+ maybeGzip(body).then(({ body: finalBody, contentEncoding }) => {
425
+ const headers = contentEncoding ? {
426
+ ...target.headers,
427
+ "content-encoding": contentEncoding
428
+ } : target.headers;
429
+ return fetchImpl(target.url, {
430
+ method: "POST",
431
+ headers,
432
+ body: finalBody
433
+ });
434
+ }).then((res) => {
435
+ if (!res.ok) {
436
+ const error = /* @__PURE__ */ new Error(`telemetry.dev metric ingest failed: ${res.status}`);
437
+ reportError(onError, error);
438
+ resultCallback({
439
+ code: 1,
440
+ error
441
+ });
442
+ return;
443
+ }
444
+ resultCallback({ code: 0 });
445
+ }).catch((error) => {
446
+ reportError(onError, error);
447
+ resultCallback({
448
+ code: 1,
449
+ error: error instanceof Error ? error : void 0
450
+ });
451
+ });
452
+ },
453
+ selectAggregationTemporality: () => AggregationTemporality.DELTA,
454
+ forceFlush: () => Promise.resolve(),
455
+ shutdown: () => Promise.resolve()
456
+ };
457
+ }
458
+ function otlpHeaders(apiKey) {
459
+ return {
460
+ "content-type": "application/x-protobuf",
461
+ authorization: `Bearer ${apiKey}`
462
+ };
463
+ }
464
+ //#endregion
465
+ //#region src/otel.ts
466
+ /**
467
+ * BYO-OpenTelemetry surface: add this processor to your own TracerProvider (NodeSDK,
468
+ * registerOTel, …) to ship its spans to telemetry.dev. Also stamps propagateAttributes
469
+ * correlation attributes onto every span it sees.
470
+ */
471
+ var TelemetrySpanProcessor = class {
472
+ inner;
473
+ metrics;
474
+ constructor(options = {}) {
475
+ const env = resolveEnv();
476
+ const apiKey = options.apiKey ?? env.TELEMETRY_DEV_API_KEY;
477
+ const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? "https://ingest.telemetry.dev").replace(/\/+$/, "");
478
+ const transport = {
479
+ fetchImpl: options.fetch ?? globalThis.fetch,
480
+ onError: options.onError
481
+ };
482
+ const exporter = options.spanExporter ?? (apiKey ? createTraceExporter({
483
+ url: `${baseUrl}/v1/traces`,
484
+ headers: otlpHeaders(apiKey)
485
+ }, transport) : void 0);
486
+ if (!exporter) {
487
+ diag.debug("no api key (apiKey option or TELEMETRY_DEV_API_KEY); TelemetrySpanProcessor is a no-op");
488
+ return;
489
+ }
490
+ const exportMode = options.exportMode ?? "batched";
491
+ if (options.metrics !== false && apiKey) {
492
+ const resource = resourceFromAttributes({
493
+ "service.name": options.serviceName ?? env.OTEL_SERVICE_NAME ?? "unknown_service",
494
+ "deployment.environment.name": options.environment ?? env.TELEMETRY_DEV_ENVIRONMENT ?? "production"
495
+ });
496
+ this.metrics = createMetricsPipeline({
497
+ resource,
498
+ exporter: createMetricExporter({
499
+ url: `${baseUrl}/v1/metrics`,
500
+ headers: otlpHeaders(apiKey)
501
+ }, transport),
502
+ exportIntervalMillis: exportMode === "batched" ? BATCHED_METRIC_INTERVAL_MS : DORMANT_INTERVAL_MS
503
+ });
504
+ }
505
+ const metrics = this.metrics;
506
+ this.inner = new StampingSpanProcessor({
507
+ exporter,
508
+ exportMode,
509
+ batch: {
510
+ ...DEFAULT_BATCH,
511
+ ...options.batch
512
+ },
513
+ spanFilter: options.spanFilter,
514
+ recordMetrics: metrics ? (span) => metrics.record(span) : void 0,
515
+ onError: options.onError
516
+ });
517
+ }
518
+ onStart(span, parentContext) {
519
+ this.inner?.onStart(span, parentContext);
520
+ }
521
+ onEnd(span) {
522
+ this.inner?.onEnd(span);
523
+ }
524
+ forceFlush() {
525
+ return Promise.all([this.inner?.forceFlush(), this.metrics?.forceFlush()]).then(() => void 0);
526
+ }
527
+ shutdown() {
528
+ return Promise.all([this.inner?.shutdown(), this.metrics?.shutdown()]).then(() => void 0);
529
+ }
530
+ };
531
+ /** Raw OTLP/protobuf fetch exporter for users wiring their own BatchSpanProcessor. */
532
+ function createTelemetrySpanExporter(options = {}) {
533
+ const env = resolveEnv();
534
+ const apiKey = options.apiKey ?? env.TELEMETRY_DEV_API_KEY;
535
+ const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? "https://ingest.telemetry.dev").replace(/\/+$/, "");
536
+ if (!apiKey) {
537
+ diag.debug("no api key (apiKey option or TELEMETRY_DEV_API_KEY); exporter is a no-op");
538
+ return {
539
+ export: (_spans, resultCallback) => resultCallback({ code: ExportResultCode.SUCCESS }),
540
+ forceFlush: () => Promise.resolve(),
541
+ shutdown: () => Promise.resolve()
542
+ };
543
+ }
544
+ return createTraceExporter({
545
+ url: `${baseUrl}/v1/traces`,
546
+ headers: otlpHeaders(apiKey)
547
+ }, {
548
+ fetchImpl: options.fetch ?? globalThis.fetch,
549
+ onError: options.onError
550
+ });
551
+ }
552
+ //#endregion
553
+ export { AlsContextManager, BATCHED_METRIC_INTERVAL_MS, DEFAULT_BASE_URL, DEFAULT_BATCH, DORMANT_INTERVAL_MS, DURATION_BUCKETS, PROPAGATED_KEY, SCOPE_NAME, SCOPE_VERSION, StampingSpanProcessor, TOKEN_BUCKETS, TelemetrySpanProcessor, activeContext, als, buildPropagatedAttributes, createLogExporter, createMetricExporter, createMetricsPipeline, createTelemetrySpanExporter, createTraceExporter, diag, jsonAttr, maybeGzip, omitUndefined, otlpHeaders, postOtlp, propagateAttributes, propagatedFromContext, reportError, resolveEnv, setLogLevel, withContext };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@telemetry-dev/otel",
3
+ "version": "0.1.0",
4
+ "description": "Bring-your-own OpenTelemetry base layer for telemetry.dev: span processor and OTLP/protobuf trace exporter.",
5
+ "keywords": [
6
+ "genai",
7
+ "llm",
8
+ "observability",
9
+ "opentelemetry",
10
+ "otel",
11
+ "telemetry",
12
+ "tracing"
13
+ ],
14
+ "homepage": "https://telemetry.dev",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/telemetry-dev/telemetry.dev.git",
19
+ "directory": "packages/otel"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "src"
24
+ ],
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.mts",
29
+ "import": "./dist/index.mjs",
30
+ "default": "./dist/index.mjs"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@opentelemetry/core": "^2.7.1",
39
+ "@opentelemetry/otlp-transformer": "^0.218.0",
40
+ "@opentelemetry/resources": "^2.7.1",
41
+ "@opentelemetry/sdk-logs": "^0.218.0",
42
+ "@opentelemetry/sdk-metrics": "^2.7.1",
43
+ "@opentelemetry/sdk-trace-base": "^2.7.1"
44
+ },
45
+ "devDependencies": {
46
+ "@opentelemetry/api": "^1.9.1",
47
+ "@types/node": "^25.5.0",
48
+ "@typescript/native-preview": "7.0.0-dev.20260328.1",
49
+ "typescript": "^6.0.2",
50
+ "vite-plus": "0.1.20",
51
+ "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20"
52
+ },
53
+ "peerDependencies": {
54
+ "@opentelemetry/api": ">=1.9.0 <2"
55
+ },
56
+ "engines": {
57
+ "node": ">=20.19.0"
58
+ },
59
+ "scripts": {
60
+ "build": "pnpm exec vp pack",
61
+ "dev": "pnpm exec vp pack --watch",
62
+ "test": "vp test",
63
+ "check": "vp check"
64
+ }
65
+ }
package/src/attrs.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Attributes } from "@opentelemetry/api";
2
+
3
+ // Pinned by docs/sdk-conformance.md ("instrumentation scopes @telemetry-dev/sdk and
4
+ // telemetry_dev") and asserted by the ingest e2e (C11). Lives here so the metrics
5
+ // pipeline and BYO processor emit the identical scope; do NOT rename to the package name.
6
+ export const SCOPE_NAME = "@telemetry-dev/sdk";
7
+ export const SCOPE_VERSION = "0.0.0";
8
+
9
+ export function omitUndefined(attributes: Attributes): Attributes {
10
+ const out: Attributes = {};
11
+ for (const key of Object.keys(attributes)) {
12
+ const value = attributes[key];
13
+ if (value !== undefined) {
14
+ out[key] = value;
15
+ }
16
+ }
17
+ return out;
18
+ }
19
+
20
+ // Stringify structured content (messages / tool args) for the gen_ai.* string attributes the
21
+ // ingest parses back into JSON. Returns undefined so omitUndefined drops absent content.
22
+ export function jsonAttr(value: unknown): string | undefined {
23
+ if (value === undefined) return undefined;
24
+ if (typeof value === "string") return value;
25
+ try {
26
+ return JSON.stringify(value);
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
package/src/config.ts ADDED
@@ -0,0 +1,29 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export type SdkLogLevel = LogLevel | "silent";
3
+ export type ExportMode = "batched" | "immediate";
4
+
5
+ export interface BatchOptions {
6
+ /** Spans per export batch. LLM spans are large; keep batches small. Default 64. */
7
+ maxExportBatchSize?: number;
8
+ /** Delay between batch exports in milliseconds. Default 1000. */
9
+ scheduledDelayMillis?: number;
10
+ /** Maximum spans buffered before drops. Default 2048. */
11
+ maxQueueSize?: number;
12
+ /** Per-export timeout in milliseconds. Default 30000. */
13
+ exportTimeoutMillis?: number;
14
+ }
15
+
16
+ export const DEFAULT_BASE_URL = "https://ingest.telemetry.dev";
17
+
18
+ export const DEFAULT_BATCH: Required<BatchOptions> = {
19
+ maxExportBatchSize: 64,
20
+ scheduledDelayMillis: 1000,
21
+ maxQueueSize: 2048,
22
+ exportTimeoutMillis: 30000,
23
+ };
24
+
25
+ export function resolveEnv(): Record<string, string | undefined> {
26
+ if (typeof process !== "undefined" && process.env) return process.env;
27
+ const emptyEnv: Record<string, string | undefined> = {};
28
+ return emptyEnv;
29
+ }