summaryrefslogtreecommitdiff
path: root/runtime/js
diff options
context:
space:
mode:
authorsnek <snek@deno.com>2024-11-13 11:38:46 +0100
committerGitHub <noreply@github.com>2024-11-13 10:38:46 +0000
commitaa546189be730163ee5370029e4dfdb3b454ab96 (patch)
tree4407643e6908f82c9ac31d9ae5faf04b3ab8d413 /runtime/js
parent7becd83a3828b35331d0fcb82c64146e520f154b (diff)
feat: OpenTelemetry Tracing API and Exporting (#26710)
Initial import of OTEL code supporting tracing. Metrics soon to come. Implements APIs for https://jsr.io/@deno/otel so that code using OpenTelemetry.js just works tm. There is still a lot of work to do with configuration and adding built-in tracing to core APIs, which will come in followup PRs. --------- Co-authored-by: Luca Casonato <hello@lcas.dev>
Diffstat (limited to 'runtime/js')
-rw-r--r--runtime/js/90_deno_ns.js19
-rw-r--r--runtime/js/99_main.js14
-rw-r--r--runtime/js/telemetry.js395
3 files changed, 418 insertions, 10 deletions
diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js
index fd2ac00f2..11f618ce2 100644
--- a/runtime/js/90_deno_ns.js
+++ b/runtime/js/90_deno_ns.js
@@ -29,6 +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";
const denoNs = {
Process: process.Process,
@@ -134,7 +135,7 @@ const denoNs = {
createHttpClient: httpClient.createHttpClient,
};
-// NOTE(bartlomieju): keep IDs in sync with `cli/main.rs`
+// NOTE(bartlomieju): keep IDs in sync with `runtime/lib.rs`
const unstableIds = {
broadcastChannel: 1,
cron: 2,
@@ -143,11 +144,12 @@ const unstableIds = {
http: 5,
kv: 6,
net: 7,
- process: 8,
- temporal: 9,
- unsafeProto: 10,
- webgpu: 11,
- workerOptions: 12,
+ otel: 8,
+ process: 9,
+ temporal: 10,
+ unsafeProto: 11,
+ webgpu: 12,
+ workerOptions: 13,
};
const denoNsUnstableById = { __proto__: null };
@@ -181,4 +183,9 @@ denoNsUnstableById[unstableIds.webgpu] = {
// denoNsUnstableById[unstableIds.workerOptions] = { __proto__: null }
+denoNsUnstableById[unstableIds.otel] = {
+ tracing: telemetry.tracing,
+ metrics: telemetry.metrics,
+};
+
export { denoNs, denoNsUnstableById, unstableIds };
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index 6ddaa1335..2da5c5398 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -86,6 +86,8 @@ 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";
+
// deno-lint-ignore prefer-primordials
if (Symbol.metadata) {
throw "V8 supports Symbol.metadata now, no need to shim it";
@@ -573,6 +575,7 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) {
10: serveHost,
11: serveIsMain,
12: serveWorkerCount,
+ 13: otelConfig,
} = runtimeOptions;
if (mode === executionModes.serve) {
@@ -673,9 +676,10 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) {
});
ObjectSetPrototypeOf(globalThis, Window.prototype);
+ bootstrapOtel(otelConfig);
+
if (inspectFlag) {
- const consoleFromDeno = globalThis.console;
- core.wrapConsole(consoleFromDeno, core.v8Console);
+ core.wrapConsole(globalThis.console, core.v8Console);
}
event.defineEventHandler(globalThis, "error");
@@ -855,6 +859,7 @@ function bootstrapWorkerRuntime(
5: hasNodeModulesDir,
6: argv0,
7: nodeDebug,
+ 13: otelConfig,
} = runtimeOptions;
performance.setTimeOrigin();
@@ -882,8 +887,9 @@ function bootstrapWorkerRuntime(
}
ObjectSetPrototypeOf(globalThis, DedicatedWorkerGlobalScope.prototype);
- const consoleFromDeno = globalThis.console;
- core.wrapConsole(consoleFromDeno, core.v8Console);
+ bootstrapOtel(otelConfig);
+
+ core.wrapConsole(globalThis.console, core.v8Console);
event.defineEventHandler(self, "message");
event.defineEventHandler(self, "error", undefined, true);
diff --git a/runtime/js/telemetry.js b/runtime/js/telemetry.js
new file mode 100644
index 000000000..e9eb51f7c
--- /dev/null
+++ b/runtime/js/telemetry.js
@@ -0,0 +1,395 @@
+// 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,
+} = primordials;
+const { AsyncVariable, setAsyncContext } = core;
+
+const CURRENT = new AsyncVariable();
+let TRACING_ENABLED = 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;
+})();
+
+function generateId(bytes) {
+ 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 INVALID_SPAN_ID = "0000000000000000";
+const INVALID_TRACE_ID = "00000000000000000000000000000000";
+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 } = config;
+
+ TRACING_ENABLED = true;
+
+ 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 = {};