diff options
Diffstat (limited to 'runtime')
-rw-r--r-- | runtime/js/90_deno_ns.js | 5 | ||||
-rw-r--r-- | runtime/js/99_main.js | 2 | ||||
-rw-r--r-- | runtime/js/telemetry.js | 409 | ||||
-rw-r--r-- | runtime/js/telemetry.ts | 720 | ||||
-rw-r--r-- | runtime/ops/otel.rs | 297 | ||||
-rw-r--r-- | runtime/shared.rs | 2 |
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", |