summaryrefslogtreecommitdiff
path: root/runtime/js
diff options
context:
space:
mode:
authorLuca Casonato <hello@lcas.dev>2024-11-19 00:55:22 +0100
committerGitHub <noreply@github.com>2024-11-18 23:55:22 +0000
commit594a99817cbe44553b2c288578fbba8e1e9c1907 (patch)
tree1ea268742bc626482005fac460a189b4a03f0a53 /runtime/js
parent106d47a0136c04ca219a81c3f91505116e13855e (diff)
feat(runtime): remove public OTEL trace API (#26854)
This PR removes the public Deno.tracing.Span API. We are not confident we can ship an API that is better than the `@opentelemetry/api` API, because V8 CPED does not support us using `using` to manage span context. If this changes, we can revisit this decision. For now, users wanting custom spans can instrument their code using the `@opentelemetry/api` API and `@deno/otel`. This PR also speeds up the OTEL trace generation by a 30% by using Uint8Array instead of strings for the trace ID and span ID.
Diffstat (limited to 'runtime/js')
-rw-r--r--runtime/js/90_deno_ns.js5
-rw-r--r--runtime/js/99_main.js2
-rw-r--r--runtime/js/telemetry.js409
-rw-r--r--runtime/js/telemetry.ts720
4 files changed, 723 insertions, 413 deletions
diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js
index 079338510..6300f599d 100644
--- a/runtime/js/90_deno_ns.js
+++ b/runtime/js/90_deno_ns.js
@@ -29,7 +29,7 @@ import * as tty from "ext:runtime/40_tty.js";
import * as kv from "ext:deno_kv/01_db.ts";
import * as cron from "ext:deno_cron/01_cron.ts";
import * as webgpuSurface from "ext:deno_webgpu/02_surface.js";
-import * as telemetry from "ext:runtime/telemetry.js";
+import * as telemetry from "ext:runtime/telemetry.ts";
const denoNs = {
Process: process.Process,
@@ -185,8 +185,7 @@ denoNsUnstableById[unstableIds.webgpu] = {
// denoNsUnstableById[unstableIds.workerOptions] = { __proto__: null }
denoNsUnstableById[unstableIds.otel] = {
- tracing: telemetry.tracing,
- metrics: telemetry.metrics,
+ telemetry: telemetry.telemetry,
};
export { denoNs, denoNsUnstableById, unstableIds };
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index b21575b8f..eedca3396 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -86,7 +86,7 @@ import {
workerRuntimeGlobalProperties,
} from "ext:runtime/98_global_scope_worker.js";
import { SymbolDispose, SymbolMetadata } from "ext:deno_web/00_infra.js";
-import { bootstrap as bootstrapOtel } from "ext:runtime/telemetry.js";
+import { bootstrap as bootstrapOtel } from "ext:runtime/telemetry.ts";
// deno-lint-ignore prefer-primordials
if (Symbol.metadata) {
diff --git a/runtime/js/telemetry.js b/runtime/js/telemetry.js
deleted file mode 100644
index 195839fb1..000000000
--- a/runtime/js/telemetry.js
+++ /dev/null
@@ -1,409 +0,0 @@
-// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-
-import { core, primordials } from "ext:core/mod.js";
-import {
- op_otel_log,
- op_otel_span_attribute,
- op_otel_span_attribute2,
- op_otel_span_attribute3,
- op_otel_span_continue,
- op_otel_span_flush,
- op_otel_span_start,
-} from "ext:core/ops";
-import { Console } from "ext:deno_console/01_console.js";
-import { performance } from "ext:deno_web/15_performance.js";
-
-const {
- SymbolDispose,
- MathRandom,
- Array,
- ObjectEntries,
- SafeMap,
- ReflectApply,
- SymbolFor,
- Error,
- NumberPrototypeToString,
- StringPrototypePadStart,
-} = primordials;
-const { AsyncVariable, setAsyncContext } = core;
-
-const CURRENT = new AsyncVariable();
-let TRACING_ENABLED = false;
-let DETERMINISTIC = false;
-
-const SPAN_ID_BYTES = 8;
-const TRACE_ID_BYTES = 16;
-
-const TRACE_FLAG_SAMPLED = 1 << 0;
-
-const hexSliceLookupTable = (function () {
- const alphabet = "0123456789abcdef";
- const table = new Array(256);
- for (let i = 0; i < 16; ++i) {
- const i16 = i * 16;
- for (let j = 0; j < 16; ++j) {
- table[i16 + j] = alphabet[i] + alphabet[j];
- }
- }
- return table;
-})();
-
-let counter = 1;
-
-const INVALID_SPAN_ID = "0000000000000000";
-const INVALID_TRACE_ID = "00000000000000000000000000000000";
-
-function generateId(bytes) {
- if (DETERMINISTIC) {
- return StringPrototypePadStart(
- NumberPrototypeToString(counter++, 16),
- bytes * 2,
- "0",
- );
- }
- let out = "";
- for (let i = 0; i < bytes / 4; i += 1) {
- const r32 = (MathRandom() * 2 ** 32) >>> 0;
- out += hexSliceLookupTable[(r32 >> 24) & 0xff];
- out += hexSliceLookupTable[(r32 >> 16) & 0xff];
- out += hexSliceLookupTable[(r32 >> 8) & 0xff];
- out += hexSliceLookupTable[r32 & 0xff];
- }
- return out;
-}
-
-function submit(span) {
- if (!(span.traceFlags & TRACE_FLAG_SAMPLED)) return;
-
- op_otel_span_start(
- span.traceId,
- span.spanId,
- span.parentSpanId ?? "",
- span.kind,
- span.name,
- span.startTime,
- span.endTime,
- );
-
- if (span.status !== null && span.status.code !== 0) {
- op_otel_span_continue(span.code, span.message ?? "");
- }
-
- const attributes = ObjectEntries(span.attributes);
- let i = 0;
- while (i < attributes.length) {
- if (i + 2 < attributes.length) {
- op_otel_span_attribute3(
- attributes.length,
- attributes[i][0],
- attributes[i][1],
- attributes[i + 1][0],
- attributes[i + 1][1],
- attributes[i + 2][0],
- attributes[i + 2][1],
- );
- i += 3;
- } else if (i + 1 < attributes.length) {
- op_otel_span_attribute2(
- attributes.length,
- attributes[i][0],
- attributes[i][1],
- attributes[i + 1][0],
- attributes[i + 1][1],
- );
- i += 2;
- } else {
- op_otel_span_attribute(
- attributes.length,
- attributes[i][0],
- attributes[i][1],
- );
- i += 1;
- }
- }
-
- op_otel_span_flush();
-}
-
-const now = () => (performance.timeOrigin + performance.now()) / 1000;
-
-const NO_ASYNC_CONTEXT = {};
-
-class Span {
- traceId;
- spanId;
- parentSpanId;
- kind;
- name;
- startTime;
- endTime;
- status = null;
- attributes = { __proto__: null };
- traceFlags = TRACE_FLAG_SAMPLED;
-
- enabled = TRACING_ENABLED;
- #asyncContext = NO_ASYNC_CONTEXT;
-
- constructor(name, kind = "internal") {
- if (!this.enabled) {
- this.traceId = INVALID_TRACE_ID;
- this.spanId = INVALID_SPAN_ID;
- this.parentSpanId = INVALID_SPAN_ID;
- return;
- }
-
- this.startTime = now();
-
- this.spanId = generateId(SPAN_ID_BYTES);
-
- let traceId;
- let parentSpanId;
- const parent = Span.current();
- if (parent) {
- if (parent.spanId !== undefined) {
- parentSpanId = parent.spanId;
- traceId = parent.traceId;
- } else {
- const context = parent.spanContext();
- parentSpanId = context.spanId;
- traceId = context.traceId;
- }
- }
- if (
- traceId && traceId !== INVALID_TRACE_ID && parentSpanId &&
- parentSpanId !== INVALID_SPAN_ID
- ) {
- this.traceId = traceId;
- this.parentSpanId = parentSpanId;
- } else {
- this.traceId = generateId(TRACE_ID_BYTES);
- this.parentSpanId = INVALID_SPAN_ID;
- }
-
- this.name = name;
-
- switch (kind) {
- case "internal":
- this.kind = 0;
- break;
- case "server":
- this.kind = 1;
- break;
- case "client":
- this.kind = 2;
- break;
- case "producer":
- this.kind = 3;
- break;
- case "consumer":
- this.kind = 4;
- break;
- default:
- throw new Error(`Invalid span kind: ${kind}`);
- }
-
- this.enter();
- }
-
- // helper function to match otel js api
- spanContext() {
- return {
- traceId: this.traceId,
- spanId: this.spanId,
- traceFlags: this.traceFlags,
- };
- }
-
- setAttribute(name, value) {
- if (!this.enabled) return;
- this.attributes[name] = value;
- }
-
- enter() {
- if (!this.enabled) return;
- const context = (CURRENT.get() || ROOT_CONTEXT).setValue(SPAN_KEY, this);
- this.#asyncContext = CURRENT.enter(context);
- }
-
- exit() {
- if (!this.enabled || this.#asyncContext === NO_ASYNC_CONTEXT) return;
- setAsyncContext(this.#asyncContext);
- this.#asyncContext = NO_ASYNC_CONTEXT;
- }
-
- end() {
- if (!this.enabled || this.endTime !== undefined) return;
- this.exit();
- this.endTime = now();
- submit(this);
- }
-
- [SymbolDispose]() {
- this.end();
- }
-
- static current() {
- return CURRENT.get()?.getValue(SPAN_KEY);
- }
-}
-
-function hrToSecs(hr) {
- return ((hr[0] * 1e3 + hr[1] / 1e6) / 1000);
-}
-
-// Exporter compatible with opentelemetry js library
-class SpanExporter {
- export(spans, resultCallback) {
- try {
- for (let i = 0; i < spans.length; i += 1) {
- const span = spans[i];
- const context = span.spanContext();
- submit({
- spanId: context.spanId,
- traceId: context.traceId,
- traceFlags: context.traceFlags,
- name: span.name,
- kind: span.kind,
- parentSpanId: span.parentSpanId,
- startTime: hrToSecs(span.startTime),
- endTime: hrToSecs(span.endTime),
- status: span.status,
- attributes: span.attributes,
- });
- }
- resultCallback({ code: 0 });
- } catch (error) {
- resultCallback({ code: 1, error });
- }
- }
-
- async shutdown() {}
-
- async forceFlush() {}
-}
-
-// SPAN_KEY matches symbol in otel-js library
-const SPAN_KEY = SymbolFor("OpenTelemetry Context Key SPAN");
-
-// Context tracker compatible with otel-js api
-class Context {
- #data = new SafeMap();
-
- constructor(data) {
- this.#data = data ? new SafeMap(data) : new SafeMap();
- }
-
- getValue(key) {
- return this.#data.get(key);
- }
-
- setValue(key, value) {
- const c = new Context(this.#data);
- c.#data.set(key, value);
- return c;
- }
-
- deleteValue(key) {
- const c = new Context(this.#data);
- c.#data.delete(key);
- return c;
- }
-}
-
-const ROOT_CONTEXT = new Context();
-
-// Context manager for opentelemetry js library
-class ContextManager {
- active() {
- return CURRENT.get() ?? ROOT_CONTEXT;
- }
-
- with(context, fn, thisArg, ...args) {
- const ctx = CURRENT.enter(context);
- try {
- return ReflectApply(fn, thisArg, args);
- } finally {
- setAsyncContext(ctx);
- }
- }
-
- bind(context, f) {
- return (...args) => {
- const ctx = CURRENT.enter(context);
- try {
- return ReflectApply(f, thisArg, args);
- } finally {
- setAsyncContext(ctx);
- }
- };
- }
-
- enable() {
- return this;
- }
-
- disable() {
- return this;
- }
-}
-
-function otelLog(message, level) {
- let traceId = "";
- let spanId = "";
- let traceFlags = 0;
- const span = Span.current();
- if (span) {
- if (span.spanId !== undefined) {
- spanId = span.spanId;
- traceId = span.traceId;
- traceFlags = span.traceFlags;
- } else {
- const context = span.spanContext();
- spanId = context.spanId;
- traceId = context.traceId;
- traceFlags = context.traceFlags;
- }
- }
- return op_otel_log(message, level, traceId, spanId, traceFlags);
-}
-
-const otelConsoleConfig = {
- ignore: 0,
- capture: 1,
- replace: 2,
-};
-
-export function bootstrap(config) {
- if (config.length === 0) return;
- const { 0: consoleConfig, 1: deterministic } = config;
-
- TRACING_ENABLED = true;
- DETERMINISTIC = deterministic === 1;
-
- switch (consoleConfig) {
- case otelConsoleConfig.capture:
- core.wrapConsole(globalThis.console, new Console(otelLog));
- break;
- case otelConsoleConfig.replace:
- ObjectDefineProperty(
- globalThis,
- "console",
- core.propNonEnumerable(new Console(otelLog)),
- );
- break;
- default:
- break;
- }
-}
-
-export const tracing = {
- get enabled() {
- return TRACING_ENABLED;
- },
- Span,
- SpanExporter,
- ContextManager,
-};
-
-// TODO(devsnek): implement metrics
-export const metrics = {};
diff --git a/runtime/js/telemetry.ts b/runtime/js/telemetry.ts
new file mode 100644
index 000000000..ecef3b5e6
--- /dev/null
+++ b/runtime/js/telemetry.ts
@@ -0,0 +1,720 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { core, primordials } from "ext:core/mod.js";
+import {
+ op_crypto_get_random_values,
+ op_otel_instrumentation_scope_create_and_enter,
+ op_otel_instrumentation_scope_enter,
+ op_otel_instrumentation_scope_enter_builtin,
+ op_otel_log,
+ op_otel_span_attribute,
+ op_otel_span_attribute2,
+ op_otel_span_attribute3,
+ op_otel_span_continue,
+ op_otel_span_flush,
+ op_otel_span_set_dropped,
+ op_otel_span_start,
+} from "ext:core/ops";
+import { Console } from "ext:deno_console/01_console.js";
+import { performance } from "ext:deno_web/15_performance.js";
+
+const {
+ SafeWeakMap,
+ Array,
+ ObjectEntries,
+ SafeMap,
+ ReflectApply,
+ SymbolFor,
+ Error,
+ Uint8Array,
+ TypedArrayPrototypeSubarray,
+ ObjectAssign,
+ ObjectDefineProperty,
+ WeakRefPrototypeDeref,
+ String,
+ ObjectPrototypeIsPrototypeOf,
+ DataView,
+ DataViewPrototypeSetUint32,
+ SafeWeakRef,
+ TypedArrayPrototypeGetBuffer,
+} = primordials;
+const { AsyncVariable, setAsyncContext } = core;
+
+let TRACING_ENABLED = false;
+let DETERMINISTIC = false;
+
+enum SpanKind {
+ INTERNAL = 0,
+ SERVER = 1,
+ CLIENT = 2,
+ PRODUCER = 3,
+ CONSUMER = 4,
+}
+
+interface TraceState {
+ set(key: string, value: string): TraceState;
+ unset(key: string): TraceState;
+ get(key: string): string | undefined;
+ serialize(): string;
+}
+
+interface SpanContext {
+ traceId: string;
+ spanId: string;
+ isRemote?: boolean;
+ traceFlags: number;
+ traceState?: TraceState;
+}
+
+type HrTime = [number, number];
+
+enum SpanStatusCode {
+ UNSET = 0,
+ OK = 1,
+ ERROR = 2,
+}
+
+interface SpanStatus {
+ code: SpanStatusCode;
+ message?: string;
+}
+
+export type AttributeValue =
+ | string
+ | number
+ | boolean
+ | Array<null | undefined | string>
+ | Array<null | undefined | number>
+ | Array<null | undefined | boolean>;
+
+interface Attributes {
+ [attributeKey: string]: AttributeValue | undefined;
+}
+
+type SpanAttributes = Attributes;
+
+interface Link {
+ context: SpanContext;
+ attributes?: SpanAttributes;
+ droppedAttributesCount?: number;
+}
+
+interface TimedEvent {
+ time: HrTime;
+ name: string;
+ attributes?: SpanAttributes;
+ droppedAttributesCount?: number;
+}
+
+interface IArrayValue {
+ values: IAnyValue[];
+}
+
+interface IAnyValue {
+ stringValue?: string | null;
+ boolValue?: boolean | null;
+ intValue?: number | null;
+ doubleValue?: number | null;
+ arrayValue?: IArrayValue;
+ kvlistValue?: IKeyValueList;
+ bytesValue?: Uint8Array;
+}
+
+interface IKeyValueList {
+ values: IKeyValue[];
+}
+
+interface IKeyValue {
+ key: string;
+ value: IAnyValue;
+}
+interface IResource {
+ attributes: IKeyValue[];
+ droppedAttributesCount: number;
+}
+
+interface InstrumentationLibrary {
+ readonly name: string;
+ readonly version?: string;
+ readonly schemaUrl?: string;
+}
+
+interface ReadableSpan {
+ readonly name: string;
+ readonly kind: SpanKind;
+ readonly spanContext: () => SpanContext;
+ readonly parentSpanId?: string;
+ readonly startTime: HrTime;
+ readonly endTime: HrTime;
+ readonly status: SpanStatus;
+ readonly attributes: SpanAttributes;
+ readonly links: Link[];
+ readonly events: TimedEvent[];
+ readonly duration: HrTime;
+ readonly ended: boolean;
+ readonly resource: IResource;
+ readonly instrumentationLibrary: InstrumentationLibrary;
+ readonly droppedAttributesCount: number;
+ readonly droppedEventsCount: number;
+ readonly droppedLinksCount: number;
+}
+
+enum ExportResultCode {
+ SUCCESS = 0,
+ FAILED = 1,
+}
+
+interface ExportResult {
+ code: ExportResultCode;
+ error?: Error;
+}
+
+function hrToSecs(hr: [number, number]): number {
+ return ((hr[0] * 1e3 + hr[1] / 1e6) / 1000);
+}
+
+const TRACE_FLAG_SAMPLED = 1 << 0;
+
+const instrumentationScopes = new SafeWeakMap<
+ InstrumentationLibrary,
+ { __key: "instrumentation-library" }
+>();
+let activeInstrumentationLibrary: WeakRef<InstrumentationLibrary> | null = null;
+
+function submit(
+ spanId: string | Uint8Array,
+ traceId: string | Uint8Array,
+ traceFlags: number,
+ parentSpanId: string | Uint8Array | null,
+ span: Omit<
+ ReadableSpan,
+ | "spanContext"
+ | "startTime"
+ | "endTime"
+ | "parentSpanId"
+ | "duration"
+ | "ended"
+ | "resource"
+ >,
+ startTime: number,
+ endTime: number,
+) {
+ if (!(traceFlags & TRACE_FLAG_SAMPLED)) return;
+
+ // TODO(@lucacasonato): `resource` is ignored for now, should we implement it?
+
+ const instrumentationLibrary = span.instrumentationLibrary;
+ if (
+ !activeInstrumentationLibrary ||
+ WeakRefPrototypeDeref(activeInstrumentationLibrary) !==
+ instrumentationLibrary
+ ) {
+ activeInstrumentationLibrary = new SafeWeakRef(instrumentationLibrary);
+ if (instrumentationLibrary === BUILTIN_INSTRUMENTATION_LIBRARY) {
+ op_otel_instrumentation_scope_enter_builtin();
+ } else {
+ let instrumentationScope = instrumentationScopes
+ .get(instrumentationLibrary);
+
+ if (instrumentationScope === undefined) {
+ instrumentationScope = op_otel_instrumentation_scope_create_and_enter(
+ instrumentationLibrary.name,
+ instrumentationLibrary.version,
+ instrumentationLibrary.schemaUrl,
+ ) as { __key: "instrumentation-library" };
+ instrumentationScopes.set(
+ instrumentationLibrary,
+ instrumentationScope,
+ );
+ } else {
+ op_otel_instrumentation_scope_enter(
+ instrumentationScope,
+ );
+ }
+ }
+ }
+
+ op_otel_span_start(
+ traceId,
+ spanId,
+ parentSpanId,
+ span.kind,
+ span.name,
+ startTime,
+ endTime,
+ );
+
+ const status = span.status;
+ if (status !== null && status.code !== 0) {
+ op_otel_span_continue(status.code, status.message ?? "");
+ }
+
+ const attributeKvs = ObjectEntries(span.attributes);
+ let i = 0;
+ while (i < attributeKvs.length) {
+ if (i + 2 < attributeKvs.length) {
+ op_otel_span_attribute3(
+ attributeKvs.length,
+ attributeKvs[i][0],
+ attributeKvs[i][1],
+ attributeKvs[i + 1][0],
+ attributeKvs[i + 1][1],
+ attributeKvs[i + 2][0],
+ attributeKvs[i + 2][1],
+ );
+ i += 3;
+ } else if (i + 1 < attributeKvs.length) {
+ op_otel_span_attribute2(
+ attributeKvs.length,
+ attributeKvs[i][0],
+ attributeKvs[i][1],
+ attributeKvs[i + 1][0],
+ attributeKvs[i + 1][1],
+ );
+ i += 2;
+ } else {
+ op_otel_span_attribute(
+ attributeKvs.length,
+ attributeKvs[i][0],
+ attributeKvs[i][1],
+ );
+ i += 1;
+ }
+ }
+
+ // TODO(@lucacasonato): implement links
+ // TODO(@lucacasonato): implement events
+
+ const droppedAttributesCount = span.droppedAttributesCount;
+ const droppedLinksCount = span.droppedLinksCount + span.links.length;
+ const droppedEventsCount = span.droppedEventsCount + span.events.length;
+ if (
+ droppedAttributesCount > 0 || droppedLinksCount > 0 ||
+ droppedEventsCount > 0
+ ) {
+ op_otel_span_set_dropped(
+ droppedAttributesCount,
+ droppedLinksCount,
+ droppedEventsCount,
+ );
+ }
+
+ op_otel_span_flush();
+}
+
+const now = () => (performance.timeOrigin + performance.now()) / 1000;
+
+const SPAN_ID_BYTES = 8;
+const TRACE_ID_BYTES = 16;
+
+const INVALID_TRACE_ID = new Uint8Array(TRACE_ID_BYTES);
+const INVALID_SPAN_ID = new Uint8Array(SPAN_ID_BYTES);
+
+const NO_ASYNC_CONTEXT = {};
+
+let otelLog: (message: string, level: number) => void;
+
+const hexSliceLookupTable = (function () {
+ const alphabet = "0123456789abcdef";
+ const table = new Array(256);
+ for (let i = 0; i < 16; ++i) {
+ const i16 = i * 16;
+ for (let j = 0; j < 16; ++j) {
+ table[i16 + j] = alphabet[i] + alphabet[j];
+ }
+ }
+ return table;
+})();
+
+function bytesToHex(bytes: Uint8Array): string {
+ let out = "";
+ for (let i = 0; i < bytes.length; i += 1) {
+ out += hexSliceLookupTable[bytes[i]];
+ }
+ return out;
+}
+
+const SPAN_KEY = SymbolFor("OpenTelemetry Context Key SPAN");
+
+const BUILTIN_INSTRUMENTATION_LIBRARY: InstrumentationLibrary = {} as never;
+
+let COUNTER = 1;
+
+export let enterSpan: (span: Span) => void;
+export let exitSpan: (span: Span) => void;
+export let endSpan: (span: Span) => void;
+
+export class Span {
+ #traceId: string | Uint8Array;
+ #spanId: Uint8Array;
+ #traceFlags = TRACE_FLAG_SAMPLED;
+
+ #spanContext: SpanContext | null = null;
+
+ #parentSpanId: string | Uint8Array | null = null;
+ #parentSpanIdString: string | null = null;
+
+ #recording = TRACING_ENABLED;
+
+ #kind: number = 0;
+ #name: string;
+ #startTime: number;
+ #status: { code: number; message?: string } | null = null;
+ #attributes: Attributes = { __proto__: null } as never;
+
+ #droppedEventsCount = 0;
+ #droppedLinksCount = 0;
+
+ #asyncContext = NO_ASYNC_CONTEXT;
+
+ static {
+ otelLog = function otelLog(message, level) {
+ let traceId = null;
+ let spanId = null;
+ let traceFlags = 0;
+ const span = CURRENT.get()?.getValue(SPAN_KEY);
+ if (span) {
+ // The lint is wrong, we can not use anything but `in` here because this
+ // is a private field.
+ // deno-lint-ignore prefer-primordials
+ if (#traceId in span) {
+ traceId = span.#traceId;
+ spanId = span.#spanId;
+ traceFlags = span.#traceFlags;
+ } else {
+ const context = span.spanContext();
+ traceId = context.traceId;
+ spanId = context.spanId;
+ traceFlags = context.traceFlags;
+ }
+ }
+ return op_otel_log(message, level, traceId, spanId, traceFlags);
+ };
+
+ enterSpan = (span: Span) => {
+ if (!span.#recording) return;
+ const context = (CURRENT.get() || ROOT_CONTEXT).setValue(SPAN_KEY, span);
+ span.#asyncContext = CURRENT.enter(context);
+ };
+
+ exitSpan = (span: Span) => {
+ if (!span.#recording) return;
+ if (span.#asyncContext === NO_ASYNC_CONTEXT) return;
+ setAsyncContext(span.#asyncContext);
+ span.#asyncContext = NO_ASYNC_CONTEXT;
+ };
+
+ exitSpan = (span: Span) => {
+ const endTime = now();
+ submit(
+ span.#spanId,
+ span.#traceId,
+ span.#traceFlags,
+ span.#parentSpanId,
+ {
+ name: span.#name,
+ kind: span.#kind,
+ status: span.#status ?? { code: 0 },
+ attributes: span.#attributes,
+ events: [],
+ links: [],
+ droppedAttributesCount: 0,
+ droppedEventsCount: span.#droppedEventsCount,
+ droppedLinksCount: span.#droppedLinksCount,
+ instrumentationLibrary: BUILTIN_INSTRUMENTATION_LIBRARY,
+ },
+ span.#startTime,
+ endTime,
+ );
+ };
+ }
+
+ constructor(
+ name: string,
+ attributes?: Attributes,
+ ) {
+ if (!this.isRecording) {
+ this.#name = "";
+ this.#startTime = 0;
+ this.#traceId = INVALID_TRACE_ID;
+ this.#spanId = INVALID_SPAN_ID;
+ this.#traceFlags = 0;
+ return;
+ }
+
+ this.#name = name;
+ this.#startTime = now();
+ this.#attributes = attributes ?? { __proto__: null } as never;
+
+ const currentSpan: Span | {
+ spanContext(): { traceId: string; spanId: string };
+ } = CURRENT.get()?.getValue(SPAN_KEY);
+ if (!currentSpan) {
+ const buffer = new Uint8Array(TRACE_ID_BYTES + SPAN_ID_BYTES);
+ if (DETERMINISTIC) {
+ DataViewPrototypeSetUint32(
+ new DataView(TypedArrayPrototypeGetBuffer(buffer)),
+ TRACE_ID_BYTES - 4,
+ COUNTER,
+ true,
+ );
+ COUNTER += 1;
+ DataViewPrototypeSetUint32(
+ new DataView(TypedArrayPrototypeGetBuffer(buffer)),
+ TRACE_ID_BYTES + SPAN_ID_BYTES - 4,
+ COUNTER,
+ true,
+ );
+ COUNTER += 1;
+ } else {
+ op_crypto_get_random_values(buffer);
+ }
+ this.#traceId = TypedArrayPrototypeSubarray(buffer, 0, TRACE_ID_BYTES);
+ this.#spanId = TypedArrayPrototypeSubarray(buffer, TRACE_ID_BYTES);
+ } else {
+ this.#spanId = new Uint8Array(SPAN_ID_BYTES);
+ if (DETERMINISTIC) {
+ DataViewPrototypeSetUint32(
+ new DataView(TypedArrayPrototypeGetBuffer(this.#spanId)),
+ SPAN_ID_BYTES - 4,
+ COUNTER,
+ true,
+ );
+ COUNTER += 1;
+ } else {
+ op_crypto_get_random_values(this.#spanId);
+ }
+ // deno-lint-ignore prefer-primordials
+ if (#traceId in currentSpan) {
+ this.#traceId = currentSpan.#traceId;
+ this.#parentSpanId = currentSpan.#spanId;
+ } else {
+ const context = currentSpan.spanContext();
+ this.#traceId = context.traceId;
+ this.#parentSpanId = context.spanId;
+ }
+ }
+ }
+
+ spanContext() {
+ if (!this.#spanContext) {
+ this.#spanContext = {
+ traceId: typeof this.#traceId === "string"
+ ? this.#traceId
+ : bytesToHex(this.#traceId),
+ spanId: typeof this.#spanId === "string"
+ ? this.#spanId
+ : bytesToHex(this.#spanId),
+ traceFlags: this.#traceFlags,
+ };
+ }
+ return this.#spanContext;
+ }
+
+ get parentSpanId() {
+ if (!this.#parentSpanIdString && this.#parentSpanId) {
+ if (typeof this.#parentSpanId === "string") {
+ this.#parentSpanIdString = this.#parentSpanId;
+ } else {
+ this.#parentSpanIdString = bytesToHex(this.#parentSpanId);
+ }
+ }
+ return this.#parentSpanIdString;
+ }
+
+ setAttribute(name: string, value: AttributeValue) {
+ if (this.#recording) this.#attributes[name] = value;
+ return this;
+ }
+
+ setAttributes(attributes: Attributes) {
+ if (this.#recording) ObjectAssign(this.#attributes, attributes);
+ return this;
+ }
+
+ setStatus(status: { code: number; message?: string }) {
+ if (this.#recording) {
+ if (status.code === 0) {
+ this.#status = null;
+ } else if (status.code > 2) {
+ throw new Error("Invalid status code");
+ } else {
+ this.#status = status;
+ }
+ }
+ return this;
+ }
+
+ updateName(name: string) {
+ if (this.#recording) this.#name = name;
+ return this;
+ }
+
+ addEvent(_name: never) {
+ // TODO(@lucacasonato): implement events
+ if (this.#recording) this.#droppedEventsCount += 1;
+ return this;
+ }
+
+ addLink(_link: never) {
+ // TODO(@lucacasonato): implement links
+ if (this.#recording) this.#droppedLinksCount += 1;
+ return this;
+ }
+
+ addLinks(links: never[]) {
+ // TODO(@lucacasonato): implement links
+ if (this.#recording) this.#droppedLinksCount += links.length;
+ return this;
+ }
+
+ isRecording() {
+ return this.#recording;
+ }
+}
+
+// Exporter compatible with opentelemetry js library
+class SpanExporter {
+ export(
+ spans: ReadableSpan[],
+ resultCallback: (result: ExportResult) => void,
+ ) {
+ try {
+ for (let i = 0; i < spans.length; i += 1) {
+ const span = spans[i];
+ const context = span.spanContext();
+ submit(
+ context.spanId,
+ context.traceId,
+ context.traceFlags,
+ span.parentSpanId ?? null,
+ span,
+ hrToSecs(span.startTime),
+ hrToSecs(span.endTime),
+ );
+ }
+ resultCallback({ code: 0 });
+ } catch (error) {
+ resultCallback({
+ code: 1,
+ error: ObjectPrototypeIsPrototypeOf(error, Error)
+ ? error as Error
+ : new Error(String(error)),
+ });
+ }
+ }
+
+ async shutdown() {}
+
+ async forceFlush() {}
+}
+
+const CURRENT = new AsyncVariable();
+
+class Context {
+ #data = new SafeMap();
+
+ // deno-lint-ignore no-explicit-any
+ constructor(data?: Iterable<readonly [any, any]> | null | undefined) {
+ this.#data = data ? new SafeMap(data) : new SafeMap();
+ }
+
+ getValue(key: symbol): unknown {
+ return this.#data.get(key);
+ }
+
+ setValue(key: symbol, value: unknown): Context {
+ const c = new Context(this.#data);
+ c.#data.set(key, value);
+ return c;
+ }
+
+ deleteValue(key: symbol): Context {
+ const c = new Context(this.#data);
+ c.#data.delete(key);
+ return c;
+ }
+}
+
+// TODO(lucacasonato): @opentelemetry/api defines it's own ROOT_CONTEXT
+const ROOT_CONTEXT = new Context();
+
+// Context manager for opentelemetry js library
+class ContextManager {
+ active(): Context {
+ return CURRENT.get() ?? ROOT_CONTEXT;
+ }
+
+ with<A extends unknown[], F extends (...args: A) => ReturnType<F>>(
+ context: Context,
+ fn: F,
+ thisArg?: ThisParameterType<F>,
+ ...args: A
+ ): ReturnType<F> {
+ const ctx = CURRENT.enter(context);
+ try {
+ return ReflectApply(fn, thisArg, args);
+ } finally {
+ setAsyncContext(ctx);
+ }
+ }
+
+ // deno-lint-ignore no-explicit-any
+ bind<T extends (...args: any[]) => any>(
+ context: Context,
+ target: T,
+ ): T {
+ return ((...args) => {
+ const ctx = CURRENT.enter(context);
+ try {
+ return ReflectApply(target, this, args);
+ } finally {
+ setAsyncContext(ctx);
+ }
+ }) as T;
+ }
+
+ enable() {
+ return this;
+ }
+
+ disable() {
+ return this;
+ }
+}
+
+const otelConsoleConfig = {
+ ignore: 0,
+ capture: 1,
+ replace: 2,
+};
+
+export function bootstrap(
+ config: [] | [
+ typeof otelConsoleConfig[keyof typeof otelConsoleConfig],
+ number,
+ ],
+): void {
+ if (config.length === 0) return;
+ const { 0: consoleConfig, 1: deterministic } = config;
+
+ TRACING_ENABLED = true;
+ DETERMINISTIC = deterministic === 1;
+
+ switch (consoleConfig) {
+ case otelConsoleConfig.capture:
+ core.wrapConsole(globalThis.console, new Console(otelLog));
+ break;
+ case otelConsoleConfig.replace:
+ ObjectDefineProperty(
+ globalThis,
+ "console",
+ core.propNonEnumerable(new Console(otelLog)),
+ );
+ break;
+ default:
+ break;
+ }
+}
+
+export const telemetry = { SpanExporter, ContextManager };