summaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
Diffstat (limited to 'runtime')
-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
-rw-r--r--runtime/ops/otel.rs297
-rw-r--r--runtime/shared.rs2
6 files changed, 928 insertions, 507 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 };
diff --git a/runtime/ops/otel.rs b/runtime/ops/otel.rs
index b32764d7f..61a7b0ef0 100644
--- a/runtime/ops/otel.rs
+++ b/runtime/ops/otel.rs
@@ -1,8 +1,8 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::tokio_util::create_basic_runtime;
+use deno_core::anyhow;
use deno_core::anyhow::anyhow;
-use deno_core::anyhow::{self};
use deno_core::futures::channel::mpsc;
use deno_core::futures::channel::mpsc::UnboundedSender;
use deno_core::futures::future::BoxFuture;
@@ -23,7 +23,6 @@ use opentelemetry::trace::SpanKind;
use opentelemetry::trace::Status as SpanStatus;
use opentelemetry::trace::TraceFlags;
use opentelemetry::trace::TraceId;
-use opentelemetry::InstrumentationScope;
use opentelemetry::Key;
use opentelemetry::KeyValue;
use opentelemetry::StringValue;
@@ -63,11 +62,15 @@ deno_core::extension!(
deno_otel,
ops = [
op_otel_log,
+ op_otel_instrumentation_scope_create_and_enter,
+ op_otel_instrumentation_scope_enter,
+ op_otel_instrumentation_scope_enter_builtin,
op_otel_span_start,
op_otel_span_continue,
op_otel_span_attribute,
op_otel_span_attribute2,
op_otel_span_attribute3,
+ op_otel_span_set_dropped,
op_otel_span_flush,
],
);
@@ -303,6 +306,10 @@ mod hyper_client {
static OTEL_PROCESSORS: OnceCell<(SpanProcessor, LogProcessor)> =
OnceCell::new();
+static BUILT_IN_INSTRUMENTATION_SCOPE: OnceCell<
+ opentelemetry::InstrumentationScope,
+> = OnceCell::new();
+
pub fn init(config: OtelConfig) -> anyhow::Result<()> {
// Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_*
// crates don't do this automatically.
@@ -390,6 +397,14 @@ pub fn init(config: OtelConfig) -> anyhow::Result<()> {
.set((span_processor, log_processor))
.map_err(|_| anyhow!("failed to init otel"))?;
+ let builtin_instrumentation_scope =
+ opentelemetry::InstrumentationScope::builder("deno")
+ .with_version(config.runtime_version.clone())
+ .build();
+ BUILT_IN_INSTRUMENTATION_SCOPE
+ .set(builtin_instrumentation_scope)
+ .map_err(|_| anyhow!("failed to init otel"))?;
+
Ok(())
}
@@ -458,16 +473,160 @@ pub fn handle_log(record: &log::Record) {
log_processor.emit(
&mut log_record,
- &InstrumentationScope::builder("deno").build(),
+ BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap(),
);
}
+fn parse_trace_id(
+ scope: &mut v8::HandleScope<'_>,
+ trace_id: v8::Local<'_, v8::Value>,
+) -> TraceId {
+ if let Ok(string) = trace_id.try_cast() {
+ let value_view = v8::ValueView::new(scope, string);
+ match value_view.data() {
+ v8::ValueViewData::OneByte(bytes) => {
+ TraceId::from_hex(&String::from_utf8_lossy(bytes))
+ .unwrap_or(TraceId::INVALID)
+ }
+
+ _ => TraceId::INVALID,
+ }
+ } else if let Ok(uint8array) = trace_id.try_cast::<v8::Uint8Array>() {
+ let data = uint8array.data();
+ let byte_length = uint8array.byte_length();
+ if byte_length != 16 {
+ return TraceId::INVALID;
+ }
+ // SAFETY: We have ensured that the byte length is 16, so it is safe to
+ // cast the data to an array of 16 bytes.
+ let bytes = unsafe { &*(data as *const u8 as *const [u8; 16]) };
+ TraceId::from_bytes(*bytes)
+ } else {
+ TraceId::INVALID
+ }
+}
+
+fn parse_span_id(
+ scope: &mut v8::HandleScope<'_>,
+ span_id: v8::Local<'_, v8::Value>,
+) -> SpanId {
+ if let Ok(string) = span_id.try_cast() {
+ let value_view = v8::ValueView::new(scope, string);
+ match value_view.data() {
+ v8::ValueViewData::OneByte(bytes) => {
+ SpanId::from_hex(&String::from_utf8_lossy(bytes))
+ .unwrap_or(SpanId::INVALID)
+ }
+ _ => SpanId::INVALID,
+ }
+ } else if let Ok(uint8array) = span_id.try_cast::<v8::Uint8Array>() {
+ let data = uint8array.data();
+ let byte_length = uint8array.byte_length();
+ if byte_length != 8 {
+ return SpanId::INVALID;
+ }
+ // SAFETY: We have ensured that the byte length is 8, so it is safe to
+ // cast the data to an array of 8 bytes.
+ let bytes = unsafe { &*(data as *const u8 as *const [u8; 8]) };
+ SpanId::from_bytes(*bytes)
+ } else {
+ SpanId::INVALID
+ }
+}
+
+macro_rules! attr {
+ ($scope:ident, $attributes:expr $(=> $dropped_attributes_count:expr)?, $name:expr, $value:expr) => {
+ let name = if let Ok(name) = $name.try_cast() {
+ let view = v8::ValueView::new($scope, name);
+ match view.data() {
+ v8::ValueViewData::OneByte(bytes) => {
+ Some(String::from_utf8_lossy(bytes).into_owned())
+ }
+ v8::ValueViewData::TwoByte(bytes) => {
+ Some(String::from_utf16_lossy(bytes))
+ }
+ }
+ } else {
+ None
+ };
+ let value = if let Ok(string) = $value.try_cast::<v8::String>() {
+ Some(Value::String(StringValue::from({
+ let x = v8::ValueView::new($scope, string);
+ match x.data() {
+ v8::ValueViewData::OneByte(bytes) => {
+ String::from_utf8_lossy(bytes).into_owned()
+ }
+ v8::ValueViewData::TwoByte(bytes) => String::from_utf16_lossy(bytes),
+ }
+ })))
+ } else if let Ok(number) = $value.try_cast::<v8::Number>() {
+ Some(Value::F64(number.value()))
+ } else if let Ok(boolean) = $value.try_cast::<v8::Boolean>() {
+ Some(Value::Bool(boolean.is_true()))
+ } else if let Ok(bigint) = $value.try_cast::<v8::BigInt>() {
+ let (i64_value, _lossless) = bigint.i64_value();
+ Some(Value::I64(i64_value))
+ } else {
+ None
+ };
+ if let (Some(name), Some(value)) = (name, value) {
+ $attributes.push(KeyValue::new(name, value));
+ }
+ $(
+ else {
+ $dropped_attributes_count += 1;
+ }
+ )?
+ };
+}
+
+#[derive(Debug, Clone)]
+struct InstrumentationScope(opentelemetry::InstrumentationScope);
+
+impl deno_core::GarbageCollected for InstrumentationScope {}
+
+#[op2]
+#[cppgc]
+fn op_otel_instrumentation_scope_create_and_enter(
+ state: &mut OpState,
+ #[string] name: String,
+ #[string] version: Option<String>,
+ #[string] schema_url: Option<String>,
+) -> InstrumentationScope {
+ let mut builder = opentelemetry::InstrumentationScope::builder(name);
+ if let Some(version) = version {
+ builder = builder.with_version(version);
+ }
+ if let Some(schema_url) = schema_url {
+ builder = builder.with_schema_url(schema_url);
+ }
+ let scope = InstrumentationScope(builder.build());
+ state.put(scope.clone());
+ scope
+}
+
+#[op2(fast)]
+fn op_otel_instrumentation_scope_enter(
+ state: &mut OpState,
+ #[cppgc] scope: &InstrumentationScope,
+) {
+ state.put(scope.clone());
+}
+
+#[op2(fast)]
+fn op_otel_instrumentation_scope_enter_builtin(state: &mut OpState) {
+ state.put(InstrumentationScope(
+ BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap().clone(),
+ ));
+}
+
#[op2(fast)]
fn op_otel_log(
+ scope: &mut v8::HandleScope<'_>,
#[string] message: String,
#[smi] level: i32,
- #[string] trace_id: &str,
- #[string] span_id: &str,
+ trace_id: v8::Local<'_, v8::Value>,
+ span_id: v8::Local<'_, v8::Value>,
#[smi] trace_flags: u8,
) {
let Some((_, log_processor)) = OTEL_PROCESSORS.get() else {
@@ -483,15 +642,16 @@ fn op_otel_log(
3.. => Severity::Error,
};
+ let trace_id = parse_trace_id(scope, trace_id);
+ let span_id = parse_span_id(scope, span_id);
+
let mut log_record = LogRecord::default();
log_record.set_observed_timestamp(SystemTime::now());
log_record.set_body(message.into());
log_record.set_severity_number(severity);
log_record.set_severity_text(severity.name());
- if let (Ok(trace_id), Ok(span_id)) =
- (TraceId::from_hex(trace_id), SpanId::from_hex(span_id))
- {
+ if trace_id != TraceId::INVALID && span_id != SpanId::INVALID {
log_record.set_trace_context(
trace_id,
span_id,
@@ -501,7 +661,7 @@ fn op_otel_log(
log_processor.emit(
&mut log_record,
- &InstrumentationScope::builder("deno").build(),
+ BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap(),
);
}
@@ -527,40 +687,23 @@ fn op_otel_span_start<'s>(
span_processor.on_end(temporary_span.0);
};
- let trace_id = {
- let x = v8::ValueView::new(scope, trace_id.try_cast()?);
- match x.data() {
- v8::ValueViewData::OneByte(bytes) => {
- TraceId::from_hex(&String::from_utf8_lossy(bytes))?
- }
- _ => return Err(anyhow!("invalid trace_id")),
- }
+ let Some(InstrumentationScope(instrumentation_scope)) =
+ state.try_borrow::<InstrumentationScope>()
+ else {
+ return Err(anyhow!("instrumentation scope not available"));
};
- let span_id = {
- let x = v8::ValueView::new(scope, span_id.try_cast()?);
- match x.data() {
- v8::ValueViewData::OneByte(bytes) => {
- SpanId::from_hex(&String::from_utf8_lossy(bytes))?
- }
- _ => return Err(anyhow!("invalid span_id")),
- }
- };
+ let trace_id = parse_trace_id(scope, trace_id);
+ if trace_id == TraceId::INVALID {
+ return Err(anyhow!("invalid trace_id"));
+ }
- let parent_span_id = {
- let x = v8::ValueView::new(scope, parent_span_id.try_cast()?);
- match x.data() {
- v8::ValueViewData::OneByte(bytes) => {
- let s = String::from_utf8_lossy(bytes);
- if s.is_empty() {
- SpanId::INVALID
- } else {
- SpanId::from_hex(&s)?
- }
- }
- _ => return Err(anyhow!("invalid parent_span_id")),
- }
- };
+ let span_id = parse_span_id(scope, span_id);
+ if span_id == SpanId::INVALID {
+ return Err(anyhow!("invalid span_id"));
+ }
+
+ let parent_span_id = parse_span_id(scope, parent_span_id);
let name = {
let x = v8::ValueView::new(scope, name.try_cast()?);
@@ -601,7 +744,7 @@ fn op_otel_span_start<'s>(
events: Default::default(),
links: Default::default(),
status: SpanStatus::Unset,
- instrumentation_scope: InstrumentationScope::builder("deno").build(),
+ instrumentation_scope: instrumentation_scope.clone(),
});
state.put(temporary_span);
@@ -626,52 +769,6 @@ fn op_otel_span_continue(
}
}
-macro_rules! attr {
- ($scope:ident, $temporary_span:ident, $name:ident, $value:ident) => {
- let name = if let Ok(name) = $name.try_cast() {
- let view = v8::ValueView::new($scope, name);
- match view.data() {
- v8::ValueViewData::OneByte(bytes) => {
- Some(String::from_utf8_lossy(bytes).into_owned())
- }
- v8::ValueViewData::TwoByte(bytes) => {
- Some(String::from_utf16_lossy(bytes))
- }
- }
- } else {
- None
- };
- let value = if let Ok(string) = $value.try_cast::<v8::String>() {
- Some(Value::String(StringValue::from({
- let x = v8::ValueView::new($scope, string);
- match x.data() {
- v8::ValueViewData::OneByte(bytes) => {
- String::from_utf8_lossy(bytes).into_owned()
- }
- v8::ValueViewData::TwoByte(bytes) => String::from_utf16_lossy(bytes),
- }
- })))
- } else if let Ok(number) = $value.try_cast::<v8::Number>() {
- Some(Value::F64(number.value()))
- } else if let Ok(boolean) = $value.try_cast::<v8::Boolean>() {
- Some(Value::Bool(boolean.is_true()))
- } else if let Ok(bigint) = $value.try_cast::<v8::BigInt>() {
- let (i64_value, _lossless) = bigint.i64_value();
- Some(Value::I64(i64_value))
- } else {
- None
- };
- if let (Some(name), Some(value)) = (name, value) {
- $temporary_span
- .0
- .attributes
- .push(KeyValue::new(name, value));
- } else {
- $temporary_span.0.dropped_attributes_count += 1;
- }
- };
-}
-
#[op2(fast)]
fn op_otel_span_attribute<'s>(
scope: &mut v8::HandleScope<'s>,
@@ -684,7 +781,7 @@ fn op_otel_span_attribute<'s>(
temporary_span.0.attributes.reserve_exact(
(capacity as usize) - temporary_span.0.attributes.capacity(),
);
- attr!(scope, temporary_span, key, value);
+ attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key, value);
}
}
@@ -702,8 +799,8 @@ fn op_otel_span_attribute2<'s>(
temporary_span.0.attributes.reserve_exact(
(capacity as usize) - temporary_span.0.attributes.capacity(),
);
- attr!(scope, temporary_span, key1, value1);
- attr!(scope, temporary_span, key2, value2);
+ attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key1, value1);
+ attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key2, value2);
}
}
@@ -724,9 +821,23 @@ fn op_otel_span_attribute3<'s>(
temporary_span.0.attributes.reserve_exact(
(capacity as usize) - temporary_span.0.attributes.capacity(),
);
- attr!(scope, temporary_span, key1, value1);
- attr!(scope, temporary_span, key2, value2);
- attr!(scope, temporary_span, key3, value3);
+ attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key1, value1);
+ attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key2, value2);
+ attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key3, value3);
+ }
+}
+
+#[op2(fast)]
+fn op_otel_span_set_dropped(
+ state: &mut OpState,
+ #[smi] dropped_attributes_count: u32,
+ #[smi] dropped_links_count: u32,
+ #[smi] dropped_events_count: u32,
+) {
+ if let Some(temporary_span) = state.try_borrow_mut::<TemporarySpan>() {
+ temporary_span.0.dropped_attributes_count = dropped_attributes_count;
+ temporary_span.0.links.dropped_count = dropped_links_count;
+ temporary_span.0.events.dropped_count = dropped_events_count;
}
}
diff --git a/runtime/shared.rs b/runtime/shared.rs
index c05f352f1..b1f383b03 100644
--- a/runtime/shared.rs
+++ b/runtime/shared.rs
@@ -47,7 +47,7 @@ extension!(runtime,
"40_signals.js",
"40_tty.js",
"41_prompt.js",
- "telemetry.js",
+ "telemetry.ts",
"90_deno_ns.js",
"98_global_scope_shared.js",
"98_global_scope_window.js",