@telemetry-dev/otel 0.1.1 → 0.1.2
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 +37 -1
- package/dist/index.d.mts +42 -5
- package/dist/index.mjs +455 -84
- package/package.json +1 -1
- package/src/config.ts +1 -1
- package/src/index.ts +9 -0
- package/src/otel.ts +7 -1
- package/src/session.ts +260 -0
- package/src/transport.ts +256 -87
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { ROOT_CONTEXT, context, createContextKey } from "@opentelemetry/api";
|
|
1
|
+
import { ROOT_CONTEXT, TraceFlags, context, createContextKey, diag as diag$1, isSpanContextValid, trace } from "@opentelemetry/api";
|
|
2
2
|
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
3
|
-
import { ExportResultCode } from "@opentelemetry/core";
|
|
3
|
+
import { ExportResultCode, getNumberFromEnv, getStringFromEnv } from "@opentelemetry/core";
|
|
4
4
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
5
|
-
import { BatchSpanProcessor, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
5
|
+
import { AlwaysOffSampler, AlwaysOnSampler, BatchSpanProcessor, ParentBasedSampler, SimpleSpanProcessor, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
|
|
6
6
|
import { ProtobufLogsSerializer, ProtobufMetricsSerializer, ProtobufTraceSerializer } from "@opentelemetry/otlp-transformer";
|
|
7
7
|
//#region src/attrs.ts
|
|
8
8
|
const SCOPE_NAME = "@telemetry-dev/sdk";
|
|
@@ -301,6 +301,8 @@ var StampingSpanProcessor = class {
|
|
|
301
301
|
//#endregion
|
|
302
302
|
//#region src/transport.ts
|
|
303
303
|
const RETRY_DELAYS_MS = [100, 500];
|
|
304
|
+
const MAX_RETRY_AFTER_MS = 2e3;
|
|
305
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
304
306
|
const RETRYABLE_STATUSES = new Set([
|
|
305
307
|
429,
|
|
306
308
|
502,
|
|
@@ -308,21 +310,105 @@ const RETRYABLE_STATUSES = new Set([
|
|
|
308
310
|
504
|
|
309
311
|
]);
|
|
310
312
|
const isRetryableStatus = (status) => RETRYABLE_STATUSES.has(status);
|
|
311
|
-
const
|
|
313
|
+
const HTTP_MONTHS = [
|
|
314
|
+
"Jan",
|
|
315
|
+
"Feb",
|
|
316
|
+
"Mar",
|
|
317
|
+
"Apr",
|
|
318
|
+
"May",
|
|
319
|
+
"Jun",
|
|
320
|
+
"Jul",
|
|
321
|
+
"Aug",
|
|
322
|
+
"Sep",
|
|
323
|
+
"Oct",
|
|
324
|
+
"Nov",
|
|
325
|
+
"Dec"
|
|
326
|
+
];
|
|
327
|
+
const HTTP_WEEKDAYS = [
|
|
328
|
+
"Sun",
|
|
329
|
+
"Mon",
|
|
330
|
+
"Tue",
|
|
331
|
+
"Wed",
|
|
332
|
+
"Thu",
|
|
333
|
+
"Fri",
|
|
334
|
+
"Sat"
|
|
335
|
+
];
|
|
336
|
+
const HTTP_WEEKDAYS_LONG = [
|
|
337
|
+
"Sunday",
|
|
338
|
+
"Monday",
|
|
339
|
+
"Tuesday",
|
|
340
|
+
"Wednesday",
|
|
341
|
+
"Thursday",
|
|
342
|
+
"Friday",
|
|
343
|
+
"Saturday"
|
|
344
|
+
];
|
|
345
|
+
const httpDateTimestamp = (weekday, dayText, monthText, yearText, hourText, minuteText, secondText, weekdays) => {
|
|
346
|
+
const day = Number(dayText);
|
|
347
|
+
const month = HTTP_MONTHS.indexOf(monthText);
|
|
348
|
+
const year = Number(yearText);
|
|
349
|
+
const hour = Number(hourText);
|
|
350
|
+
const minute = Number(minuteText);
|
|
351
|
+
const second = Number(secondText);
|
|
352
|
+
if (hour > 23 || minute > 59 || second > 59) return void 0;
|
|
353
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
354
|
+
date.setUTCFullYear(year, month, day);
|
|
355
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
356
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month || date.getUTCDate() !== day || weekdays[date.getUTCDay()] !== weekday) return;
|
|
357
|
+
return date.getTime();
|
|
358
|
+
};
|
|
359
|
+
const parseHttpDate = (value) => {
|
|
360
|
+
const imf = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/.exec(value);
|
|
361
|
+
if (imf) return httpDateTimestamp(...imf.slice(1), HTTP_WEEKDAYS);
|
|
362
|
+
const rfc850 = /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/.exec(value);
|
|
363
|
+
if (rfc850) {
|
|
364
|
+
const fields = rfc850.slice(1);
|
|
365
|
+
const currentYear = (/* @__PURE__ */ new Date()).getUTCFullYear();
|
|
366
|
+
let year = Math.floor(currentYear / 100) * 100 + Number(fields[3]);
|
|
367
|
+
if (year > currentYear + 50) year -= 100;
|
|
368
|
+
fields[3] = String(year);
|
|
369
|
+
return httpDateTimestamp(...fields, HTTP_WEEKDAYS_LONG);
|
|
370
|
+
}
|
|
371
|
+
const asctime = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( [1-9]|[12]\d|3[01]) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/.exec(value);
|
|
372
|
+
if (!asctime) return void 0;
|
|
373
|
+
const [, weekday, month, day, hour, minute, second, year] = asctime;
|
|
374
|
+
return httpDateTimestamp(weekday, day.trim(), month, year, hour, minute, second, HTTP_WEEKDAYS);
|
|
375
|
+
};
|
|
376
|
+
const retryAfterMs = (res) => {
|
|
377
|
+
const header = res.headers.get("retry-after")?.trim();
|
|
378
|
+
if (header === void 0) return void 0;
|
|
379
|
+
if (/^\d+$/.test(header)) return Number(header) * 1e3;
|
|
380
|
+
const at = parseHttpDate(header);
|
|
381
|
+
return at === void 0 ? void 0 : Math.max(0, at - Date.now());
|
|
382
|
+
};
|
|
383
|
+
const delay = (ms, signal) => new Promise((resolve, reject) => {
|
|
384
|
+
const onAbort = () => {
|
|
385
|
+
clearTimeout(timer);
|
|
386
|
+
reject(signal?.reason ?? /* @__PURE__ */ new Error("telemetry.dev export aborted"));
|
|
387
|
+
};
|
|
388
|
+
const timer = setTimeout(() => {
|
|
389
|
+
signal?.removeEventListener("abort", onAbort);
|
|
390
|
+
resolve();
|
|
391
|
+
}, ms);
|
|
392
|
+
if (signal?.aborted) onAbort();
|
|
393
|
+
else signal?.addEventListener("abort", onAbort, { once: true });
|
|
394
|
+
});
|
|
312
395
|
const cancelBody = async (res) => {
|
|
313
396
|
try {
|
|
314
397
|
await res.body?.cancel();
|
|
315
398
|
} catch {}
|
|
316
399
|
};
|
|
317
|
-
const postOtlp = async ({ fetchImpl, url, headers, body }) => {
|
|
400
|
+
const postOtlp = async ({ fetchImpl, url, headers, body, signal }) => {
|
|
318
401
|
for (let attempt = 0;; attempt += 1) {
|
|
402
|
+
let retryAfter;
|
|
319
403
|
try {
|
|
320
404
|
const res = await fetchImpl(url, {
|
|
321
405
|
method: "POST",
|
|
322
406
|
headers,
|
|
323
|
-
body
|
|
407
|
+
body,
|
|
408
|
+
signal
|
|
324
409
|
});
|
|
325
|
-
|
|
410
|
+
retryAfter = retryAfterMs(res);
|
|
411
|
+
if (res.ok || !isRetryableStatus(res.status) || attempt === RETRY_DELAYS_MS.length || retryAfter !== void 0 && retryAfter > MAX_RETRY_AFTER_MS) {
|
|
326
412
|
await cancelBody(res);
|
|
327
413
|
return res;
|
|
328
414
|
}
|
|
@@ -330,7 +416,7 @@ const postOtlp = async ({ fetchImpl, url, headers, body }) => {
|
|
|
330
416
|
} catch (error) {
|
|
331
417
|
if (attempt === RETRY_DELAYS_MS.length) throw error;
|
|
332
418
|
}
|
|
333
|
-
await delay(RETRY_DELAYS_MS[attempt]);
|
|
419
|
+
await delay(retryAfter ?? RETRY_DELAYS_MS[attempt], signal);
|
|
334
420
|
}
|
|
335
421
|
};
|
|
336
422
|
const GZIP_THRESHOLD_BYTES = 1024;
|
|
@@ -346,113 +432,133 @@ async function maybeGzip(body) {
|
|
|
346
432
|
return { body };
|
|
347
433
|
}
|
|
348
434
|
}
|
|
435
|
+
async function postSerialized(body, target, transport, label, post = postOtlp, signal) {
|
|
436
|
+
const { body: finalBody, contentEncoding } = await maybeGzip(body);
|
|
437
|
+
const headers = contentEncoding ? {
|
|
438
|
+
...target.headers,
|
|
439
|
+
"content-encoding": contentEncoding
|
|
440
|
+
} : target.headers;
|
|
441
|
+
const res = await post({
|
|
442
|
+
fetchImpl: transport.fetchImpl,
|
|
443
|
+
url: target.url,
|
|
444
|
+
headers,
|
|
445
|
+
body: finalBody,
|
|
446
|
+
signal
|
|
447
|
+
});
|
|
448
|
+
if (!res.ok) {
|
|
449
|
+
const retryAfter = retryAfterMs(res);
|
|
450
|
+
const retryAfterHeader = res.headers.get("retry-after")?.trim();
|
|
451
|
+
const retryDetail = retryAfter !== void 0 && retryAfter > MAX_RETRY_AFTER_MS && retryAfterHeader !== void 0 ? `; retry-after ${retryAfterHeader} exceeds ${MAX_RETRY_AFTER_MS / 1e3}s retry cap` : "";
|
|
452
|
+
throw new Error(`telemetry.dev ${label} ingest failed: ${res.status}${retryDetail}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const createExportLifecycle = (transport) => {
|
|
456
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
457
|
+
let shutDown = false;
|
|
458
|
+
const configuredTimeoutMillis = transport.exportTimeoutMillis ?? DEFAULT_BATCH.exportTimeoutMillis;
|
|
459
|
+
const timeoutMillis = Number.isFinite(configuredTimeoutMillis) && configuredTimeoutMillis >= 0 && configuredTimeoutMillis <= MAX_TIMER_DELAY_MS ? configuredTimeoutMillis : DEFAULT_BATCH.exportTimeoutMillis;
|
|
460
|
+
const callback = (resultCallback, result) => {
|
|
461
|
+
try {
|
|
462
|
+
resultCallback(result);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
reportError(transport.onError, error);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
const exportBatch = (send, resultCallback) => {
|
|
468
|
+
if (shutDown) {
|
|
469
|
+
const error = /* @__PURE__ */ new Error("telemetry.dev exporter is shut down");
|
|
470
|
+
callback(resultCallback, {
|
|
471
|
+
code: ExportResultCode.FAILED,
|
|
472
|
+
error
|
|
473
|
+
});
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const controller = new AbortController();
|
|
477
|
+
let timer;
|
|
478
|
+
const timeout = new Promise((_, reject) => {
|
|
479
|
+
timer = setTimeout(() => {
|
|
480
|
+
controller.abort();
|
|
481
|
+
reject(/* @__PURE__ */ new Error(`telemetry.dev export timed out after ${timeoutMillis}ms`));
|
|
482
|
+
}, timeoutMillis);
|
|
483
|
+
});
|
|
484
|
+
let tracked;
|
|
485
|
+
tracked = Promise.race([Promise.resolve().then(() => send(controller.signal)), timeout]).then(() => callback(resultCallback, { code: ExportResultCode.SUCCESS }), (cause) => {
|
|
486
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
487
|
+
reportError(transport.onError, error);
|
|
488
|
+
callback(resultCallback, {
|
|
489
|
+
code: ExportResultCode.FAILED,
|
|
490
|
+
error
|
|
491
|
+
});
|
|
492
|
+
}).finally(() => {
|
|
493
|
+
clearTimeout(timer);
|
|
494
|
+
inFlight.delete(tracked);
|
|
495
|
+
});
|
|
496
|
+
inFlight.add(tracked);
|
|
497
|
+
};
|
|
498
|
+
const forceFlush = () => Promise.all(inFlight).then(() => void 0);
|
|
499
|
+
const shutdown = () => {
|
|
500
|
+
shutDown = true;
|
|
501
|
+
return forceFlush();
|
|
502
|
+
};
|
|
503
|
+
return {
|
|
504
|
+
exportBatch,
|
|
505
|
+
forceFlush,
|
|
506
|
+
shutdown
|
|
507
|
+
};
|
|
508
|
+
};
|
|
349
509
|
const MAX_BODY_BYTES = 35e5;
|
|
350
510
|
function createOtlpBatchSender(serializer, target, transport, label) {
|
|
351
|
-
const send = async (items) => {
|
|
511
|
+
const send = async (items, signal) => {
|
|
352
512
|
const body = serializer.serializeRequest(items);
|
|
353
513
|
if (!body || body.byteLength === 0) return;
|
|
354
514
|
if (body.byteLength > MAX_BODY_BYTES) {
|
|
355
515
|
if (items.length > 1) {
|
|
356
516
|
const mid = Math.ceil(items.length / 2);
|
|
357
|
-
await send(items.slice(0, mid));
|
|
358
|
-
await send(items.slice(mid));
|
|
517
|
+
await send(items.slice(0, mid), signal);
|
|
518
|
+
await send(items.slice(mid), signal);
|
|
359
519
|
return;
|
|
360
520
|
}
|
|
361
521
|
reportError(transport.onError, /* @__PURE__ */ new Error(`telemetry.dev: ${label} record exceeds max export size, dropped`));
|
|
362
522
|
return;
|
|
363
523
|
}
|
|
364
|
-
|
|
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}`);
|
|
524
|
+
await postSerialized(body, target, transport, label, postOtlp, signal);
|
|
376
525
|
};
|
|
377
526
|
return send;
|
|
378
527
|
}
|
|
379
528
|
function createTraceExporter(target, transport) {
|
|
380
529
|
const send = createOtlpBatchSender(ProtobufTraceSerializer, target, transport, "trace");
|
|
530
|
+
const lifecycle = createExportLifecycle(transport);
|
|
381
531
|
return {
|
|
382
532
|
export(spans, resultCallback) {
|
|
383
|
-
|
|
384
|
-
reportError(transport.onError, error);
|
|
385
|
-
resultCallback({
|
|
386
|
-
code: ExportResultCode.FAILED,
|
|
387
|
-
error: error instanceof Error ? error : void 0
|
|
388
|
-
});
|
|
389
|
-
});
|
|
533
|
+
lifecycle.exportBatch((signal) => send(spans, signal), resultCallback);
|
|
390
534
|
},
|
|
391
|
-
forceFlush:
|
|
392
|
-
shutdown:
|
|
535
|
+
forceFlush: lifecycle.forceFlush,
|
|
536
|
+
shutdown: lifecycle.shutdown
|
|
393
537
|
};
|
|
394
538
|
}
|
|
395
539
|
function createLogExporter(target, transport) {
|
|
396
540
|
const send = createOtlpBatchSender(ProtobufLogsSerializer, target, transport, "log");
|
|
541
|
+
const lifecycle = createExportLifecycle(transport);
|
|
397
542
|
return {
|
|
398
543
|
export(logs, resultCallback) {
|
|
399
|
-
|
|
400
|
-
reportError(transport.onError, error);
|
|
401
|
-
resultCallback({
|
|
402
|
-
code: ExportResultCode.FAILED,
|
|
403
|
-
error: error instanceof Error ? error : void 0
|
|
404
|
-
});
|
|
405
|
-
});
|
|
544
|
+
lifecycle.exportBatch((signal) => send(logs, signal), resultCallback);
|
|
406
545
|
},
|
|
407
|
-
forceFlush:
|
|
408
|
-
shutdown:
|
|
546
|
+
forceFlush: lifecycle.forceFlush,
|
|
547
|
+
shutdown: lifecycle.shutdown
|
|
409
548
|
};
|
|
410
549
|
}
|
|
411
550
|
function createMetricExporter(target, transport) {
|
|
412
|
-
const
|
|
551
|
+
const lifecycle = createExportLifecycle(transport);
|
|
413
552
|
return {
|
|
414
553
|
export(resourceMetrics, resultCallback) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
});
|
|
554
|
+
lifecycle.exportBatch(async (signal) => {
|
|
555
|
+
const body = resourceMetrics.scopeMetrics.some((scope) => scope.metrics.some((metric) => metric.dataPoints.length > 0)) ? ProtobufMetricsSerializer.serializeRequest(resourceMetrics) : void 0;
|
|
556
|
+
if (body !== void 0 && body.byteLength > 0) await postSerialized(body, target, transport, "metric", postOtlp, signal);
|
|
557
|
+
}, resultCallback);
|
|
452
558
|
},
|
|
453
559
|
selectAggregationTemporality: () => AggregationTemporality.DELTA,
|
|
454
|
-
forceFlush:
|
|
455
|
-
shutdown:
|
|
560
|
+
forceFlush: lifecycle.forceFlush,
|
|
561
|
+
shutdown: lifecycle.shutdown
|
|
456
562
|
};
|
|
457
563
|
}
|
|
458
564
|
function otlpHeaders(apiKey, sdkName = "@telemetry-dev/otel") {
|
|
@@ -478,7 +584,8 @@ var TelemetrySpanProcessor = class {
|
|
|
478
584
|
const baseUrl = (options.baseUrl ?? env.TELEMETRY_DEV_BASE_URL ?? "https://ingest.telemetry.dev").replace(/\/+$/, "");
|
|
479
585
|
const transport = {
|
|
480
586
|
fetchImpl: options.fetch ?? globalThis.fetch,
|
|
481
|
-
onError: options.onError
|
|
587
|
+
onError: options.onError,
|
|
588
|
+
exportTimeoutMillis: options.batch?.exportTimeoutMillis
|
|
482
589
|
};
|
|
483
590
|
const exporter = options.spanExporter ?? (apiKey ? createTraceExporter({
|
|
484
591
|
url: `${baseUrl}/v1/traces`,
|
|
@@ -547,8 +654,272 @@ function createTelemetrySpanExporter(options = {}) {
|
|
|
547
654
|
headers: otlpHeaders(apiKey)
|
|
548
655
|
}, {
|
|
549
656
|
fetchImpl: options.fetch ?? globalThis.fetch,
|
|
550
|
-
onError: options.onError
|
|
657
|
+
onError: options.onError,
|
|
658
|
+
exportTimeoutMillis: options.exportTimeoutMillis
|
|
551
659
|
});
|
|
552
660
|
}
|
|
553
661
|
//#endregion
|
|
554
|
-
|
|
662
|
+
//#region src/session.ts
|
|
663
|
+
const SESSION_PARENTS = /* @__PURE__ */ new WeakMap();
|
|
664
|
+
/**
|
|
665
|
+
* Install on the provider used with withSessionParent or sessionRootTracerProvider.
|
|
666
|
+
* Pass the provider's configured sampler explicitly; OTel cannot read it back from a provider.
|
|
667
|
+
* Without an argument, uses OTEL_TRACES_SAMPLER / OTEL_TRACES_SAMPLER_ARG defaults.
|
|
668
|
+
*/
|
|
669
|
+
function sessionSampler(inner = samplerFromEnv()) {
|
|
670
|
+
return {
|
|
671
|
+
shouldSample(ctx, traceId, name, kind, attributes, links) {
|
|
672
|
+
const parent = trace.getSpan(ctx);
|
|
673
|
+
const original = parent && SESSION_PARENTS.get(parent);
|
|
674
|
+
if (original) {
|
|
675
|
+
const span = trace.getSpan(original);
|
|
676
|
+
ctx = span ? trace.setSpan(ctx, span) : trace.deleteSpan(ctx);
|
|
677
|
+
}
|
|
678
|
+
return inner.shouldSample(ctx, traceId, name, kind, attributes, links);
|
|
679
|
+
},
|
|
680
|
+
toString: () => `SessionSampler{${inner.toString()}}`
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function samplerFromEnv() {
|
|
684
|
+
const name = getStringFromEnv("OTEL_TRACES_SAMPLER") ?? "parentbased_always_on";
|
|
685
|
+
let root;
|
|
686
|
+
switch (name) {
|
|
687
|
+
case "always_off":
|
|
688
|
+
case "parentbased_always_off":
|
|
689
|
+
root = new AlwaysOffSampler();
|
|
690
|
+
break;
|
|
691
|
+
case "traceidratio":
|
|
692
|
+
case "parentbased_traceidratio": {
|
|
693
|
+
const ratio = getNumberFromEnv("OTEL_TRACES_SAMPLER_ARG");
|
|
694
|
+
const valid = ratio !== void 0 && Number.isFinite(ratio) && ratio >= 0 && ratio <= 1;
|
|
695
|
+
if (!valid) diag$1.error("Invalid OTEL_TRACES_SAMPLER_ARG; using 1.");
|
|
696
|
+
root = new TraceIdRatioBasedSampler(valid ? ratio : 1);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case "always_on":
|
|
700
|
+
case "parentbased_always_on":
|
|
701
|
+
root = new AlwaysOnSampler();
|
|
702
|
+
break;
|
|
703
|
+
default:
|
|
704
|
+
diag$1.error(`Invalid OTEL_TRACES_SAMPLER "${name}"; using parentbased_always_on.`);
|
|
705
|
+
return new ParentBasedSampler({ root: new AlwaysOnSampler() });
|
|
706
|
+
}
|
|
707
|
+
return name.startsWith("parentbased_") ? new ParentBasedSampler({ root }) : root;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Deterministic remote parent for a session: every root span of the session joins one trace
|
|
711
|
+
* (`traceId = SHA-256(apiKey ‖ 0x00 ‖ sessionId)[0:16]`, parent `spanId = digest[16:24]`).
|
|
712
|
+
* The API key is part of the input so two projects with the same session id never share a trace
|
|
713
|
+
* id. No span is ever emitted for the parent. Its flags are not a sampling decision:
|
|
714
|
+
* use withSessionParent and install sessionSampler on the provider. Python mirrors these IDs.
|
|
715
|
+
*/
|
|
716
|
+
function sessionSpanContext(apiKey, sessionId) {
|
|
717
|
+
const digest = sha256(new TextEncoder().encode(`${apiKey ?? ""}\0${sessionId}`));
|
|
718
|
+
return {
|
|
719
|
+
traceId: hex(digest.subarray(0, 16)),
|
|
720
|
+
spanId: hex(digest.subarray(16, 24)),
|
|
721
|
+
traceFlags: TraceFlags.NONE,
|
|
722
|
+
isRemote: true
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Wraps a provider so that spans `sessionRootOf` recognizes are parented under the session
|
|
727
|
+
* parent even when a parent is active. Frameworks that run each turn inside their own
|
|
728
|
+
* engine span (eve's "workflow" scope) otherwise give every turn its own trace.
|
|
729
|
+
* The inner provider MUST use sessionSampler(configuredSampler) to retain sampling policy.
|
|
730
|
+
*/
|
|
731
|
+
function sessionRootTracerProvider(inner, apiKey, sessionRootOf, onError) {
|
|
732
|
+
const reparent = (name, options, ctx) => {
|
|
733
|
+
if (!apiKey) return ctx;
|
|
734
|
+
try {
|
|
735
|
+
const sessionId = sessionRootOf(name, options?.attributes ?? {});
|
|
736
|
+
return sessionId ? setSessionParent(options?.root ? trace.deleteSpan(ctx) : ctx, sessionId, apiKey) : ctx;
|
|
737
|
+
} catch (error) {
|
|
738
|
+
reportError(onError, error);
|
|
739
|
+
return ctx;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
return { getTracer(name, version, options) {
|
|
743
|
+
const tracer = inner.getTracer(name, version, options);
|
|
744
|
+
return {
|
|
745
|
+
startSpan: (spanName, spanOptions, ctx = context.active()) => {
|
|
746
|
+
const parent = reparent(spanName, spanOptions, ctx);
|
|
747
|
+
return tracer.startSpan(spanName, parent !== ctx && spanOptions?.root ? {
|
|
748
|
+
...spanOptions,
|
|
749
|
+
root: false
|
|
750
|
+
} : spanOptions, parent);
|
|
751
|
+
},
|
|
752
|
+
startActiveSpan(spanName, ...args) {
|
|
753
|
+
const fn = args.at(-1);
|
|
754
|
+
const spanOptions = args.length > 1 ? args[0] : void 0;
|
|
755
|
+
const ctx = args.length > 2 ? args[1] : context.active();
|
|
756
|
+
const parent = reparent(spanName, spanOptions, ctx);
|
|
757
|
+
return tracer.startActiveSpan(spanName, parent !== ctx && spanOptions?.root ? {
|
|
758
|
+
...spanOptions,
|
|
759
|
+
root: false
|
|
760
|
+
} : spanOptions ?? {}, parent, fn);
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
} };
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Parent a would-be root under the session; nested spans keep their real parent.
|
|
767
|
+
* Requires sessionSampler(configuredSampler) on the provider that starts the span.
|
|
768
|
+
*/
|
|
769
|
+
function withSessionParent(ctx, sessionId, apiKey) {
|
|
770
|
+
if (!apiKey || !sessionId) return ctx;
|
|
771
|
+
const current = trace.getSpanContext(ctx);
|
|
772
|
+
if (current && isSpanContextValid(current)) return ctx;
|
|
773
|
+
return setSessionParent(ctx, sessionId, apiKey);
|
|
774
|
+
}
|
|
775
|
+
function setSessionParent(ctx, sessionId, apiKey) {
|
|
776
|
+
const parent = trace.wrapSpanContext({
|
|
777
|
+
...sessionSpanContext(apiKey, sessionId),
|
|
778
|
+
traceState: trace.getSpanContext(ctx)?.traceState
|
|
779
|
+
});
|
|
780
|
+
SESSION_PARENTS.set(parent, ctx);
|
|
781
|
+
return trace.setSpan(ctx, parent);
|
|
782
|
+
}
|
|
783
|
+
function sessionIdOf(ctx, attributes) {
|
|
784
|
+
const explicit = attributes?.["gen_ai.conversation.id"];
|
|
785
|
+
if (typeof explicit === "string") return explicit;
|
|
786
|
+
const propagated = propagatedFromContext(ctx)?.["gen_ai.conversation.id"];
|
|
787
|
+
return typeof propagated === "string" ? propagated : void 0;
|
|
788
|
+
}
|
|
789
|
+
function hex(bytes) {
|
|
790
|
+
let s = "";
|
|
791
|
+
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
792
|
+
return s;
|
|
793
|
+
}
|
|
794
|
+
const K = new Uint32Array([
|
|
795
|
+
1116352408,
|
|
796
|
+
1899447441,
|
|
797
|
+
3049323471,
|
|
798
|
+
3921009573,
|
|
799
|
+
961987163,
|
|
800
|
+
1508970993,
|
|
801
|
+
2453635748,
|
|
802
|
+
2870763221,
|
|
803
|
+
3624381080,
|
|
804
|
+
310598401,
|
|
805
|
+
607225278,
|
|
806
|
+
1426881987,
|
|
807
|
+
1925078388,
|
|
808
|
+
2162078206,
|
|
809
|
+
2614888103,
|
|
810
|
+
3248222580,
|
|
811
|
+
3835390401,
|
|
812
|
+
4022224774,
|
|
813
|
+
264347078,
|
|
814
|
+
604807628,
|
|
815
|
+
770255983,
|
|
816
|
+
1249150122,
|
|
817
|
+
1555081692,
|
|
818
|
+
1996064986,
|
|
819
|
+
2554220882,
|
|
820
|
+
2821834349,
|
|
821
|
+
2952996808,
|
|
822
|
+
3210313671,
|
|
823
|
+
3336571891,
|
|
824
|
+
3584528711,
|
|
825
|
+
113926993,
|
|
826
|
+
338241895,
|
|
827
|
+
666307205,
|
|
828
|
+
773529912,
|
|
829
|
+
1294757372,
|
|
830
|
+
1396182291,
|
|
831
|
+
1695183700,
|
|
832
|
+
1986661051,
|
|
833
|
+
2177026350,
|
|
834
|
+
2456956037,
|
|
835
|
+
2730485921,
|
|
836
|
+
2820302411,
|
|
837
|
+
3259730800,
|
|
838
|
+
3345764771,
|
|
839
|
+
3516065817,
|
|
840
|
+
3600352804,
|
|
841
|
+
4094571909,
|
|
842
|
+
275423344,
|
|
843
|
+
430227734,
|
|
844
|
+
506948616,
|
|
845
|
+
659060556,
|
|
846
|
+
883997877,
|
|
847
|
+
958139571,
|
|
848
|
+
1322822218,
|
|
849
|
+
1537002063,
|
|
850
|
+
1747873779,
|
|
851
|
+
1955562222,
|
|
852
|
+
2024104815,
|
|
853
|
+
2227730452,
|
|
854
|
+
2361852424,
|
|
855
|
+
2428436474,
|
|
856
|
+
2756734187,
|
|
857
|
+
3204031479,
|
|
858
|
+
3329325298
|
|
859
|
+
]);
|
|
860
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
861
|
+
/** FIPS 180-4 SHA-256 in plain JS: the package must stay synchronous and runtime-neutral. */
|
|
862
|
+
function sha256(bytes) {
|
|
863
|
+
const h = new Uint32Array([
|
|
864
|
+
1779033703,
|
|
865
|
+
3144134277,
|
|
866
|
+
1013904242,
|
|
867
|
+
2773480762,
|
|
868
|
+
1359893119,
|
|
869
|
+
2600822924,
|
|
870
|
+
528734635,
|
|
871
|
+
1541459225
|
|
872
|
+
]);
|
|
873
|
+
const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
|
|
874
|
+
padded.set(bytes);
|
|
875
|
+
padded[bytes.length] = 128;
|
|
876
|
+
const view = new DataView(padded.buffer);
|
|
877
|
+
const bitLen = bytes.length * 8;
|
|
878
|
+
view.setUint32(padded.length - 8, Math.floor(bitLen / 4294967296));
|
|
879
|
+
view.setUint32(padded.length - 4, bitLen >>> 0);
|
|
880
|
+
const w = new Uint32Array(64);
|
|
881
|
+
for (let off = 0; off < padded.length; off += 64) {
|
|
882
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4);
|
|
883
|
+
for (let i = 16; i < 64; i++) {
|
|
884
|
+
const x = w[i - 15];
|
|
885
|
+
const y = w[i - 2];
|
|
886
|
+
const s0 = rotr(x, 7) ^ rotr(x, 18) ^ x >>> 3;
|
|
887
|
+
const s1 = rotr(y, 17) ^ rotr(y, 19) ^ y >>> 10;
|
|
888
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
889
|
+
}
|
|
890
|
+
let a = h[0];
|
|
891
|
+
let b = h[1];
|
|
892
|
+
let c = h[2];
|
|
893
|
+
let d = h[3];
|
|
894
|
+
let e = h[4];
|
|
895
|
+
let f = h[5];
|
|
896
|
+
let g = h[6];
|
|
897
|
+
let hh = h[7];
|
|
898
|
+
for (let i = 0; i < 64; i++) {
|
|
899
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] >>> 0;
|
|
900
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
901
|
+
hh = g;
|
|
902
|
+
g = f;
|
|
903
|
+
f = e;
|
|
904
|
+
e = d + t1 >>> 0;
|
|
905
|
+
d = c;
|
|
906
|
+
c = b;
|
|
907
|
+
b = a;
|
|
908
|
+
a = t1 + t2 >>> 0;
|
|
909
|
+
}
|
|
910
|
+
h[0] = h[0] + a >>> 0;
|
|
911
|
+
h[1] = h[1] + b >>> 0;
|
|
912
|
+
h[2] = h[2] + c >>> 0;
|
|
913
|
+
h[3] = h[3] + d >>> 0;
|
|
914
|
+
h[4] = h[4] + e >>> 0;
|
|
915
|
+
h[5] = h[5] + f >>> 0;
|
|
916
|
+
h[6] = h[6] + g >>> 0;
|
|
917
|
+
h[7] = h[7] + hh >>> 0;
|
|
918
|
+
}
|
|
919
|
+
const out = new Uint8Array(32);
|
|
920
|
+
const outView = new DataView(out.buffer);
|
|
921
|
+
for (let i = 0; i < 8; i++) outView.setUint32(i * 4, h[i]);
|
|
922
|
+
return out;
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
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, sessionIdOf, sessionRootTracerProvider, sessionSampler, sessionSpanContext, setLogLevel, sha256, withContext, withSessionParent };
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const DEFAULT_BATCH: Required<BatchOptions> = {
|
|
|
22
22
|
exportTimeoutMillis: 30000,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export function resolveEnv() {
|
|
25
|
+
export function resolveEnv(): Record<string, string | undefined> {
|
|
26
26
|
if (globalThis.process !== undefined && process.env) return process.env;
|
|
27
27
|
return {};
|
|
28
28
|
}
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,15 @@ export {
|
|
|
34
34
|
type TelemetrySpanProcessorOptions,
|
|
35
35
|
} from "./otel.ts";
|
|
36
36
|
export { StampingSpanProcessor, type StampingProcessorOptions } from "./processor.ts";
|
|
37
|
+
export {
|
|
38
|
+
sessionIdOf,
|
|
39
|
+
type SessionRootOf,
|
|
40
|
+
sessionRootTracerProvider,
|
|
41
|
+
sessionSampler,
|
|
42
|
+
sessionSpanContext,
|
|
43
|
+
sha256,
|
|
44
|
+
withSessionParent,
|
|
45
|
+
} from "./session.ts";
|
|
37
46
|
export {
|
|
38
47
|
createLogExporter,
|
|
39
48
|
createMetricExporter,
|
package/src/otel.ts
CHANGED
|
@@ -71,6 +71,7 @@ export class TelemetrySpanProcessor implements SpanProcessor {
|
|
|
71
71
|
const transport: Transport = {
|
|
72
72
|
fetchImpl: options.fetch ?? globalThis.fetch,
|
|
73
73
|
onError: options.onError,
|
|
74
|
+
exportTimeoutMillis: options.batch?.exportTimeoutMillis,
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
const exporter =
|
|
@@ -143,6 +144,7 @@ export function createTelemetrySpanExporter(
|
|
|
143
144
|
baseUrl?: string;
|
|
144
145
|
fetch?: typeof fetch;
|
|
145
146
|
onError?: (error: Error) => void;
|
|
147
|
+
exportTimeoutMillis?: number;
|
|
146
148
|
} = {},
|
|
147
149
|
): SpanExporter {
|
|
148
150
|
const env = resolveEnv();
|
|
@@ -161,6 +163,10 @@ export function createTelemetrySpanExporter(
|
|
|
161
163
|
}
|
|
162
164
|
return createTraceExporter(
|
|
163
165
|
{ url: `${baseUrl}/v1/traces`, headers: otlpHeaders(apiKey) },
|
|
164
|
-
{
|
|
166
|
+
{
|
|
167
|
+
fetchImpl: options.fetch ?? globalThis.fetch,
|
|
168
|
+
onError: options.onError,
|
|
169
|
+
exportTimeoutMillis: options.exportTimeoutMillis,
|
|
170
|
+
},
|
|
165
171
|
);
|
|
166
172
|
}
|