From 294d845210a878bae58a36c3964958ed815d8923 Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Thu, 26 Dec 2024 15:42:59 +0100 Subject: [PATCH] feat(unstable): replace SpanExporter with TracerProvider This is needed to make no-config @opentelemetry/api integration work. --- cli/tsc/dts/lib.deno.unstable.d.ts | 14 +- ext/fetch/26_fetch.js | 73 ++- ext/http/00_serve.ts | 141 ++--- ext/telemetry/lib.rs | 924 +++++++++++++++++------------ ext/telemetry/telemetry.ts | 863 +++++++++++---------------- 5 files changed, 996 insertions(+), 1019 deletions(-) diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index 6759856e6add49..6f8b76f90edf92 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -1300,12 +1300,12 @@ declare namespace Deno { */ export namespace telemetry { /** - * A SpanExporter compatible with OpenTelemetry.js - * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_trace_base.SpanExporter.html + * A TracerProvider compatible with OpenTelemetry.js + * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.TracerProvider.html * @category Telemetry * @experimental */ - export class SpanExporter {} + export class TraceProvider {} /** * A ContextManager compatible with OpenTelemetry.js @@ -1315,6 +1315,14 @@ declare namespace Deno { */ export class ContextManager {} + /** + * A MeterProvider compatible with OpenTelemetry.js + * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.MeterProvider.html + * @category Telemetry + * @experimental + */ + export class MeterProvider {} + export {}; // only export exports } diff --git a/ext/fetch/26_fetch.js b/ext/fetch/26_fetch.js index 12b9c4582b7a1f..7ebef61de8d3f7 100644 --- a/ext/fetch/26_fetch.js +++ b/ext/fetch/26_fetch.js @@ -59,10 +59,9 @@ import { } from "ext:deno_fetch/23_response.js"; import * as abortSignal from "ext:deno_web/03_abort_signal.js"; import { - endSpan, + builtinTracer, enterSpan, - exitSpan, - Span, + restoreContext, TRACING_ENABLED, } from "ext:deno_telemetry/telemetry.ts"; import { @@ -176,7 +175,7 @@ async function mainFetch(req, recursive, terminator) { req.clientRid, reqBody !== null || reqRid !== null, reqBody, - reqRid, + reqRid ); function onAbort() { @@ -223,7 +222,7 @@ async function mainFetch(req, recursive, terminator) { case "error": core.close(resp.responseRid); return networkError( - "Encountered redirect while redirect mode is set to 'error'", + "Encountered redirect while redirect mode is set to 'error'" ); case "follow": core.close(resp.responseRid); @@ -241,7 +240,7 @@ async function mainFetch(req, recursive, terminator) { core.close(resp.responseRid); } else { response.body = new InnerBody( - createResponseBodyStream(resp.responseRid, terminator), + createResponseBodyStream(resp.responseRid, terminator) ); } } @@ -265,7 +264,7 @@ async function mainFetch(req, recursive, terminator) { function httpRedirectFetch(request, response, terminator) { const locationHeaders = ArrayPrototypeFilter( response.headerList, - (entry) => byteLowerCase(entry[0]) === "location", + (entry) => byteLowerCase(entry[0]) === "location" ); if (locationHeaders.length === 0) { return response; @@ -274,7 +273,7 @@ function httpRedirectFetch(request, response, terminator) { const currentURL = new URL(request.currentUrl()); const locationURL = new URL( locationHeaders[0][1], - response.url() ?? undefined, + response.url() ?? undefined ); if (locationURL.hash === "") { locationURL.hash = currentURL.hash; @@ -292,7 +291,7 @@ function httpRedirectFetch(request, response, terminator) { request.body.source === null ) { return networkError( - "Can not redeliver a streaming request body after a redirect", + "Can not redeliver a streaming request body after a redirect" ); } if ( @@ -308,7 +307,7 @@ function httpRedirectFetch(request, response, terminator) { if ( ArrayPrototypeIncludes( REQUEST_BODY_HEADER_NAMES, - byteLowerCase(request.headerList[i][0]), + byteLowerCase(request.headerList[i][0]) ) ) { ArrayPrototypeSplice(request.headerList, i, 1); @@ -320,16 +319,16 @@ function httpRedirectFetch(request, response, terminator) { // Drop confidential headers when redirecting to a less secure protocol // or to a different domain that is not a superdomain if ( - locationURL.protocol !== currentURL.protocol && - locationURL.protocol !== "https:" || - locationURL.host !== currentURL.host && - !isSubdomain(locationURL.host, currentURL.host) + (locationURL.protocol !== currentURL.protocol && + locationURL.protocol !== "https:") || + (locationURL.host !== currentURL.host && + !isSubdomain(locationURL.host, currentURL.host)) ) { for (let i = 0; i < request.headerList.length; i++) { if ( ArrayPrototypeIncludes( REDIRECT_SENSITIVE_HEADER_NAMES, - byteLowerCase(request.headerList[i][0]), + byteLowerCase(request.headerList[i][0]) ) ) { ArrayPrototypeSplice(request.headerList, i, 1); @@ -352,10 +351,11 @@ function httpRedirectFetch(request, response, terminator) { */ function fetch(input, init = { __proto__: null }) { let span; + let context; try { if (TRACING_ENABLED) { - span = new Span("fetch", { kind: 2 }); - enterSpan(span); + span = builtinTracer.startSpan("fetch", { kind: 2 }); + context = enterSpan(span); } // There is an async dispatch later that causes a stack trace disconnect. @@ -389,7 +389,7 @@ function fetch(input, init = { __proto__: null }) { function onabort() { locallyAborted = true; reject( - abortFetch(request, responseObject, requestObject.signal.reason), + abortFetch(request, responseObject, requestObject.signal.reason) ); } requestObject.signal[abortSignal.add](onabort); @@ -412,11 +412,7 @@ function fetch(input, init = { __proto__: null }) { // 12.2. if (response.aborted) { reject( - abortFetch( - request, - responseObject, - requestObject.signal.reason, - ), + abortFetch(request, responseObject, requestObject.signal.reason) ); requestObject.signal[abortSignal.remove](onabort); return; @@ -424,7 +420,7 @@ function fetch(input, init = { __proto__: null }) { // 12.3. if (response.type === "error") { const err = new TypeError( - "Fetch failed: " + (response.error ?? "unknown error"), + "Fetch failed: " + (response.error ?? "unknown error") ); reject(err); requestObject.signal[abortSignal.remove](onabort); @@ -438,12 +434,12 @@ function fetch(input, init = { __proto__: null }) { resolve(responseObject); requestObject.signal[abortSignal.remove](onabort); - }, + } ), (err) => { reject(err); requestObject.signal[abortSignal.remove](onabort); - }, + } ); }); @@ -454,9 +450,7 @@ function fetch(input, init = { __proto__: null }) { await opPromise; return result; } finally { - if (span) { - endSpan(span); - } + span?.end(); } })(); } @@ -469,19 +463,17 @@ function fetch(input, init = { __proto__: null }) { // XXX: This should always be true, otherwise `opPromise` would be present. if (op_fetch_promise_is_settled(result)) { // It's already settled. - endSpan(span); + span?.end(); } else { // Not settled yet, we can return a new wrapper promise. return SafePromisePrototypeFinally(result, () => { - endSpan(span); + span?.end(); }); } } return result; } finally { - if (span) { - exitSpan(span); - } + if (context) restoreContext(context); } } @@ -508,8 +500,11 @@ function abortFetch(request, responseObject, error) { */ function isSubdomain(subdomain, domain) { const dot = subdomain.length - domain.length - 1; - return dot > 0 && subdomain[dot] === "." && - StringPrototypeEndsWith(subdomain, domain); + return ( + dot > 0 && + subdomain[dot] === "." && + StringPrototypeEndsWith(subdomain, domain) + ); } /** @@ -529,7 +524,7 @@ function handleWasmStreaming(source, rid) { const res = webidl.converters["Response"]( source, "Failed to execute 'WebAssembly.compileStreaming'", - "Argument 1", + "Argument 1" ); // 2.3. @@ -550,7 +545,7 @@ function handleWasmStreaming(source, rid) { // 2.5. if (!res.ok) { throw new TypeError( - `Failed to receive WebAssembly content: HTTP status code ${res.status}`, + `Failed to receive WebAssembly content: HTTP status code ${res.status}` ); } @@ -573,7 +568,7 @@ function handleWasmStreaming(source, rid) { // 2.7 () => core.close(rid), // 2.8 - (err) => core.abortWasmStreaming(rid, err), + (err) => core.abortWasmStreaming(rid, err) ); } else { // 2.7 diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index 446533e91043f9..0f399b4f8aa1ce 100644 --- a/ext/http/00_serve.ts +++ b/ext/http/00_serve.ts @@ -43,10 +43,7 @@ const { Uint8Array, Promise, } = primordials; -const { - getAsyncContext, - setAsyncContext, -} = core; +const { getAsyncContext, setAsyncContext } = core; import { InnerBody } from "ext:deno_fetch/22_body.js"; import { Event } from "ext:deno_web/02_event.js"; @@ -90,9 +87,8 @@ import { import { hasTlsKeyPairOptions, listenTls } from "ext:deno_net/02_tls.js"; import { SymbolAsyncDispose } from "ext:deno_web/00_infra.js"; import { - endSpan, enterSpan, - Span, + builtinTracer, TRACING_ENABLED, } from "ext:deno_telemetry/telemetry.ts"; import { @@ -106,36 +102,17 @@ function internalServerError() { // "Internal Server Error" return new Response( new Uint8Array([ - 73, - 110, - 116, - 101, - 114, - 110, - 97, - 108, - 32, - 83, - 101, - 114, - 118, - 101, - 114, - 32, - 69, - 114, - 114, - 111, - 114, + 73, 110, 116, 101, 114, 110, 97, 108, 32, 83, 101, 114, 118, 101, 114, 32, + 69, 114, 114, 111, 114, ]), - { status: 500 }, + { status: 500 } ); } // Used to ensure that user returns a valid response (but not a different response) from handlers that are upgraded. const UPGRADE_RESPONSE_SENTINEL = fromInnerResponse( newInnerResponse(101), - "immutable", + "immutable" ); function upgradeHttpRaw(req, conn) { @@ -176,7 +153,7 @@ class InnerRequest { this.#completed.resolve(undefined); } else { this.#completed.reject( - new Interrupted("HTTP response was not sent successfully"), + new Interrupted("HTTP response was not sent successfully") ); } } @@ -212,7 +189,7 @@ class InnerRequest { const conn = new UpgradedConn( upgradeRid, underlyingConn?.remoteAddr, - underlyingConn?.localAddr, + underlyingConn?.localAddr ); return { response: UPGRADE_RESPONSE_SENTINEL, conn }; @@ -235,7 +212,7 @@ class InnerRequest { }; const wsPromise = op_http_upgrade_websocket_next( external, - response.headerList, + response.headerList ); // Start the upgrade in the background. @@ -255,9 +232,8 @@ class InnerRequest { ws[_eventLoop](); if (ws[_idleTimeoutDuration]) { - ws.addEventListener( - "close", - () => clearTimeout(ws[_idleTimeoutTimeout]), + ws.addEventListener("close", () => + clearTimeout(ws[_idleTimeoutTimeout]) ); } ws[_serverHandleIdleTimeout](); @@ -288,28 +264,28 @@ class InnerRequest { // * is valid for OPTIONS if (path === "*") { - return this.#urlValue = "*"; + return (this.#urlValue = "*"); } // If the path is empty, return the authority (valid for CONNECT) if (path == "") { - return this.#urlValue = this.#methodAndUri[1]; + return (this.#urlValue = this.#methodAndUri[1]); } // CONNECT requires an authority if (this.#methodAndUri[0] == "CONNECT") { - return this.#urlValue = this.#methodAndUri[1]; + return (this.#urlValue = this.#methodAndUri[1]); } const hostname = this.#methodAndUri[1]; if (hostname) { // Construct a URL from the scheme, the hostname, and the path - return this.#urlValue = this.#context.scheme + hostname + path; + return (this.#urlValue = this.#context.scheme + hostname + path); } // Construct a URL from the scheme, the fallback hostname, and the path - return this.#urlValue = this.#context.scheme + this.#context.fallbackHost + - path; + return (this.#urlValue = + this.#context.scheme + this.#context.fallbackHost + path); } get completed() { @@ -396,10 +372,7 @@ class InnerRequest { return; } - PromisePrototypeThen( - op_http_request_on_cancel(this.#external), - callback, - ); + PromisePrototypeThen(op_http_request_on_cancel(this.#external), callback); } } @@ -422,7 +395,7 @@ class CallbackContext { () => { op_http_cancel(this.serverRid, false); }, - { once: true }, + { once: true } ); this.abortController = new AbortController(); this.serverRid = args[0]; @@ -459,7 +432,7 @@ function fastSyncResponseOrStream( req, respBody, status, - innerRequest: InnerRequest, + innerRequest: InnerRequest ) { if (respBody === null || respBody === undefined) { // Don't set the body @@ -503,16 +476,11 @@ function fastSyncResponseOrStream( autoClose = true; } PromisePrototypeThen( - op_http_set_response_body_resource( - req, - rid, - autoClose, - status, - ), + op_http_set_response_body_resource(req, rid, autoClose, status), (success) => { innerRequest?.close(success); op_http_close_after_finish(req); - }, + } ); } @@ -538,27 +506,24 @@ function mapToCallback(context, callback, onError) { updateSpanFromRequest(span, request); } - response = await callback( - request, - new ServeHandlerInfo(innerRequest), - ); + response = await callback(request, new ServeHandlerInfo(innerRequest)); // Throwing Error if the handler return value is not a Response class if (!ObjectPrototypeIsPrototypeOf(ResponsePrototype, response)) { throw new TypeError( - "Return value from serve handler must be a response or a promise resolving to a response", + "Return value from serve handler must be a response or a promise resolving to a response" ); } if (response.type === "error") { throw new TypeError( - "Return value from serve handler must not be an error response (like Response.error())", + "Return value from serve handler must not be an error response (like Response.error())" ); } if (response.bodyUsed) { throw new TypeError( - "The body of the Response returned from the serve handler has already been consumed", + "The body of the Response returned from the serve handler has already been consumed" ); } } catch (error) { @@ -566,7 +531,7 @@ function mapToCallback(context, callback, onError) { response = await onError(error); if (!ObjectPrototypeIsPrototypeOf(ResponsePrototype, response)) { throw new TypeError( - "Return value from onError handler must be a response or a promise resolving to a response", + "Return value from onError handler must be a response or a promise resolving to a response" ); } } catch (error) { @@ -618,12 +583,12 @@ function mapToCallback(context, callback, onError) { mapped = function (req, _span) { const oldCtx = getAsyncContext(); setAsyncContext(context.asyncContext); - const span = new Span("deno.serve", { kind: 1 }); + const span = builtinTracer().startSpan("deno.serve", { kind: 1 }); try { enterSpan(span); return SafePromisePrototypeFinally( origMapped(req, span), - () => endSpan(span), + () => span.end(), ); } finally { // equiv to exitSpan. @@ -648,7 +613,7 @@ function mapToCallback(context, callback, onError) { type RawHandler = ( request: Request, - info: ServeHandlerInfo, + info: ServeHandlerInfo ) => Response | Promise; type RawServeOptions = { @@ -670,7 +635,7 @@ function formatHostName(hostname: string): string { // because browsers in Windows don't resolve "0.0.0.0". // See the discussion in https://github.com/denoland/deno_std/issues/1165 if ( - (Deno.build.os === "windows") && + Deno.build.os === "windows" && (hostname == "0.0.0.0" || hostname == "::") ) { return "localhost"; @@ -695,14 +660,14 @@ function serve(arg1, arg2) { if (handler === undefined) { if (options === undefined) { throw new TypeError( - "Cannot serve HTTP requests: either a `handler` or `options` must be specified", + "Cannot serve HTTP requests: either a `handler` or `options` must be specified" ); } handler = options.handler; } if (typeof handler !== "function") { throw new TypeError( - `Cannot serve HTTP requests: handler must be a function, received ${typeof handler}`, + `Cannot serve HTTP requests: handler must be a function, received ${typeof handler}` ); } if (options === undefined) { @@ -712,11 +677,13 @@ function serve(arg1, arg2) { const wantsHttps = hasTlsKeyPairOptions(options); const wantsUnix = ObjectHasOwn(options, "path"); const signal = options.signal; - const onError = options.onError ?? function (error) { - // deno-lint-ignore no-console - console.error(error); - return internalServerError(); - }; + const onError = + options.onError ?? + function (error) { + // deno-lint-ignore no-console + console.error(error); + return internalServerError(); + }; if (wantsUnix) { const listener = listen({ @@ -744,12 +711,12 @@ function serve(arg1, arg2) { if (options.certFile || options.keyFile) { throw new TypeError( - "Unsupported 'certFile' / 'keyFile' options provided: use 'cert' / 'key' instead.", + "Unsupported 'certFile' / 'keyFile' options provided: use 'cert' / 'key' instead." ); } if (options.alpnProtocols) { throw new TypeError( - "Unsupported 'alpnProtocols' option provided. 'h2' and 'http/1.1' are automatically supported.", + "Unsupported 'alpnProtocols' option provided. 'h2' and 'http/1.1' are automatically supported." ); } @@ -757,7 +724,7 @@ function serve(arg1, arg2) { if (wantsHttps) { if (!options.cert || !options.key) { throw new TypeError( - "Both 'cert' and 'key' must be provided to enable HTTPS", + "Both 'cert' and 'key' must be provided to enable HTTPS" ); } listenOpts.cert = options.cert; @@ -793,7 +760,7 @@ function serveHttpOnListener(listener, signal, handler, onError, onListen) { const context = new CallbackContext( signal, op_http_serve(listener[internalRidSymbol]), - listener, + listener ); const callback = mapToCallback(context, handler, onError); @@ -809,7 +776,7 @@ function serveHttpOnConnection(connection, signal, handler, onError, onListen) { const context = new CallbackContext( signal, op_http_serve_on(connection[internalRidSymbol]), - null, + null ); const callback = mapToCallback(context, handler, onError); @@ -825,10 +792,7 @@ function serveHttpOn(context, addr, callback) { const promiseErrorHandler = (error) => { // Abnormal exit // deno-lint-ignore no-console - console.error( - "Terminating Deno.serve loop due to unexpected error", - error, - ); + console.error("Terminating Deno.serve loop due to unexpected error", error); context.close(); }; @@ -938,20 +902,19 @@ function registerDeclarativeServer(exports) { if (ObjectHasOwn(exports, "fetch")) { if (typeof exports.fetch !== "function") { throw new TypeError( - "Invalid type for fetch: must be a function with a single or no parameter", + "Invalid type for fetch: must be a function with a single or no parameter" ); } return ({ servePort, serveHost, serveIsMain, serveWorkerCount }) => { Deno.serve({ port: servePort, hostname: serveHost, - [kLoadBalanced]: (serveIsMain && serveWorkerCount > 1) || - (serveWorkerCount !== null), + [kLoadBalanced]: + (serveIsMain && serveWorkerCount > 1) || serveWorkerCount !== null, onListen: ({ port, hostname }) => { if (serveIsMain) { - const nThreads = serveWorkerCount > 1 - ? ` with ${serveWorkerCount} threads` - : ""; + const nThreads = + serveWorkerCount > 1 ? ` with ${serveWorkerCount} threads` : ""; const host = formatHostName(hostname); // deno-lint-ignore no-console @@ -960,7 +923,7 @@ function registerDeclarativeServer(exports) { "color: green", "color: inherit", "color: yellow", - "color: inherit", + "color: inherit" ); } }, diff --git a/ext/telemetry/lib.rs b/ext/telemetry/lib.rs index 8018843dc47d5a..1b8cbf2051be8a 100644 --- a/ext/telemetry/lib.rs +++ b/ext/telemetry/lib.rs @@ -2,6 +2,8 @@ use deno_core::anyhow; use deno_core::anyhow::anyhow; +use deno_core::error::type_error; +use deno_core::error::AnyError; use deno_core::futures::channel::mpsc; use deno_core::futures::channel::mpsc::UnboundedSender; use deno_core::futures::future::BoxFuture; @@ -20,7 +22,7 @@ use opentelemetry::logs::LogRecord as LogRecordTrait; use opentelemetry::logs::Severity; use opentelemetry::metrics::AsyncInstrumentBuilder; use opentelemetry::metrics::InstrumentBuilder; -use opentelemetry::metrics::MeterProvider; +use opentelemetry::metrics::MeterProvider as _; use opentelemetry::otel_debug; use opentelemetry::otel_error; use opentelemetry::trace::SpanContext; @@ -29,6 +31,8 @@ use opentelemetry::trace::SpanKind; use opentelemetry::trace::Status as SpanStatus; use opentelemetry::trace::TraceFlags; use opentelemetry::trace::TraceId; +use opentelemetry::trace::TraceState; +use opentelemetry::InstrumentationScope; use opentelemetry::Key; use opentelemetry::KeyValue; use opentelemetry::StringValue; @@ -48,7 +52,11 @@ use opentelemetry_sdk::metrics::MetricResult; use opentelemetry_sdk::metrics::SdkMeterProvider; use opentelemetry_sdk::metrics::Temporality; use opentelemetry_sdk::trace::BatchSpanProcessor; -use opentelemetry_sdk::trace::SpanProcessor; +use opentelemetry_sdk::trace::IdGenerator; +use opentelemetry_sdk::trace::RandomIdGenerator; +use opentelemetry_sdk::trace::SpanEvents; +use opentelemetry_sdk::trace::SpanLinks; +use opentelemetry_sdk::trace::SpanProcessor as _; use opentelemetry_sdk::Resource; use opentelemetry_semantic_conventions::resource::PROCESS_RUNTIME_NAME; use opentelemetry_semantic_conventions::resource::PROCESS_RUNTIME_VERSION; @@ -78,23 +86,11 @@ deno_core::extension!( deno_telemetry, 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_log_foreign, + op_otel_span_attribute1, op_otel_span_attribute2, op_otel_span_attribute3, - op_otel_span_set_dropped, - op_otel_span_flush, - op_otel_metric_create_counter, - op_otel_metric_create_up_down_counter, - op_otel_metric_create_gauge, - op_otel_metric_create_histogram, - op_otel_metric_create_observable_counter, - op_otel_metric_create_observable_gauge, - op_otel_metric_create_observable_up_down_counter, + op_otel_span_update_name, op_otel_metric_attribute3, op_otel_metric_record0, op_otel_metric_record1, @@ -107,6 +103,7 @@ deno_core::extension!( op_otel_metric_wait_to_observe, op_otel_metric_observation_done, ], + objects = [OtelTracer, OtelMeter, OtelSpan], esm = ["telemetry.ts", "util.ts"], ); @@ -668,8 +665,8 @@ pub fn init(rt_config: OtelRuntimeConfig) -> anyhow::Result<()> { OTEL_PROCESSORS .set(Processors { - spans: span_processor, logs: log_processor, + spans: span_processor, meter_provider, }) .map_err(|_| anyhow!("failed to init otel"))?; @@ -849,6 +846,9 @@ macro_rules! attr_raw { } else if let Ok(bigint) = $value.try_cast::() { let (i64_value, _lossless) = bigint.i64_value(); Some(Value::I64(i64_value)) + } else if let Ok(_array) = $value.try_cast::() { + // TODO: implement array attributes + None } else { None }; @@ -874,48 +874,63 @@ macro_rules! attr { }; } -#[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] schema_url: Option, -) -> 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, +fn op_otel_log<'s>( + scope: &mut v8::HandleScope<'s>, + message: v8::Local<'s, v8::Value>, + #[smi] level: i32, + span: v8::Local<'s, v8::Value>, ) { - state.put(scope.clone()); -} + let Some(Processors { logs, .. }) = OTEL_PROCESSORS.get() else { + return; + }; + let Some(instrumentation_scope) = BUILT_IN_INSTRUMENTATION_SCOPE.get() else { + return; + }; -#[op2(fast)] -fn op_otel_instrumentation_scope_enter_builtin(state: &mut OpState) { - if let Some(scope) = BUILT_IN_INSTRUMENTATION_SCOPE.get() { - state.put(InstrumentationScope(scope.clone())); + // Convert the integer log level that ext/console uses to the corresponding + // OpenTelemetry log severity. + let severity = match level { + ..=0 => Severity::Debug, + 1 => Severity::Info, + 2 => Severity::Warn, + 3.. => Severity::Error, + }; + + let mut log_record = LogRecord::default(); + log_record.set_observed_timestamp(SystemTime::now()); + let Ok(message) = message.try_cast() else { + return; + }; + log_record.set_body(owned_string(scope, message).into()); + log_record.set_severity_number(severity); + log_record.set_severity_text(severity.name()); + if let Some(span) = + deno_core::_ops::try_unwrap_cppgc_object::(scope, span) + { + let state = span.0.borrow(); + match &*state { + OtelSpanState::Recording(span) => { + log_record.set_trace_context( + span.span_context.trace_id(), + span.span_context.span_id(), + Some(span.span_context.trace_flags()), + ); + } + OtelSpanState::Done(span_context) => { + log_record.set_trace_context( + span_context.trace_id(), + span_context.span_id(), + Some(span_context.trace_flags()), + ); + } + } } + logs.emit(&mut log_record, instrumentation_scope); } #[op2(fast)] -fn op_otel_log( +fn op_otel_log_foreign( scope: &mut v8::HandleScope<'_>, #[string] message: String, #[smi] level: i32, @@ -972,136 +987,325 @@ fn owned_string<'s>( } } -struct TemporarySpan(SpanData); +struct OtelTracer(InstrumentationScope); -#[allow(clippy::too_many_arguments)] -#[op2(fast)] -fn op_otel_span_start<'s>( - scope: &mut v8::HandleScope<'s>, - state: &mut OpState, - trace_id: v8::Local<'s, v8::Value>, - span_id: v8::Local<'s, v8::Value>, - parent_span_id: v8::Local<'s, v8::Value>, - #[smi] span_kind: u8, - name: v8::Local<'s, v8::Value>, - start_time: f64, - end_time: f64, -) -> Result<(), anyhow::Error> { - if let Some(temporary_span) = state.try_take::() { - let Some(Processors { spans, .. }) = OTEL_PROCESSORS.get() else { - return Ok(()); - }; - spans.on_end(temporary_span.0); - }; - - let Some(InstrumentationScope(instrumentation_scope)) = - state.try_borrow::() - else { - return Err(anyhow!("instrumentation scope not available")); - }; +impl deno_core::GarbageCollected for OtelTracer {} - let trace_id = parse_trace_id(scope, trace_id); - if trace_id == TraceId::INVALID { - return Err(anyhow!("invalid trace_id")); +#[op2] +impl OtelTracer { + #[constructor] + #[cppgc] + fn new( + #[string] name: String, + #[string] version: Option, + #[string] schema_url: Option, + ) -> OtelTracer { + 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 = builder.build(); + OtelTracer(scope) } - let span_id = parse_span_id(scope, span_id); - if span_id == SpanId::INVALID { - return Err(anyhow!("invalid span_id")); + #[static_method] + #[cppgc] + fn builtin() -> OtelTracer { + OtelTracer(BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap().clone()) } - let parent_span_id = parse_span_id(scope, parent_span_id); - - let name = owned_string(scope, name.try_cast()?); + #[reentrant] + #[cppgc] + fn start_span<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + #[cppgc] parent: Option<&OtelSpan>, + name: v8::Local<'s, v8::Value>, + #[smi] span_kind: u8, + start_time: Option, + #[smi] attribute_count: usize, + ) -> Result { + let span_context; + let parent_span_id; + match parent { + Some(parent) => { + let parent = parent.0.borrow(); + let parent_span_context = match &*parent { + OtelSpanState::Recording(span) => &span.span_context, + OtelSpanState::Done(span_context) => span_context, + }; + span_context = SpanContext::new( + parent_span_context.trace_id(), + RandomIdGenerator::default().new_span_id(), + TraceFlags::SAMPLED, + false, + parent_span_context.trace_state().clone(), + ); + parent_span_id = parent_span_context.span_id(); + } + None => { + span_context = SpanContext::new( + RandomIdGenerator::default().new_trace_id(), + RandomIdGenerator::default().new_span_id(), + TraceFlags::SAMPLED, + false, + TraceState::NONE, + ); + parent_span_id = SpanId::INVALID; + } + } + let name = owned_string(scope, name.try_cast()?); + let span_kind = match span_kind { + 0 => SpanKind::Internal, + 1 => SpanKind::Server, + 2 => SpanKind::Client, + 3 => SpanKind::Producer, + 4 => SpanKind::Consumer, + _ => return Err(anyhow!("invalid span kind")), + }; + let start_time = start_time + .map(|start_time| { + SystemTime::UNIX_EPOCH + .checked_add(std::time::Duration::from_secs_f64(start_time)) + .ok_or_else(|| anyhow!("invalid start time")) + }) + .unwrap_or_else(|| Ok(SystemTime::now()))?; + let span_data = SpanData { + span_context, + parent_span_id, + span_kind, + name: Cow::Owned(name), + start_time, + end_time: SystemTime::UNIX_EPOCH, + attributes: Vec::with_capacity(attribute_count), + dropped_attributes_count: 0, + status: SpanStatus::Unset, + events: SpanEvents::default(), + links: SpanLinks::default(), + instrumentation_scope: self.0.clone(), + }; + Ok(OtelSpan(RefCell::new(OtelSpanState::Recording(span_data)))) + } - let temporary_span = TemporarySpan(SpanData { - span_context: SpanContext::new( - trace_id, - span_id, + #[reentrant] + #[cppgc] + fn start_span_foreign<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + parent_trace_id: v8::Local<'s, v8::Value>, + parent_span_id: v8::Local<'s, v8::Value>, + name: v8::Local<'s, v8::Value>, + #[smi] span_kind: u8, + start_time: Option, + #[smi] attribute_count: usize, + ) -> Result { + let parent_trace_id = parse_trace_id(scope, parent_trace_id); + if parent_trace_id == TraceId::INVALID { + return Err(anyhow!("invalid trace id")); + }; + let parent_span_id = parse_span_id(scope, parent_span_id); + if parent_span_id == SpanId::INVALID { + return Err(anyhow!("invalid span id")); + }; + let span_context = SpanContext::new( + parent_trace_id, + RandomIdGenerator::default().new_span_id(), TraceFlags::SAMPLED, false, - Default::default(), - ), - parent_span_id, - span_kind: match span_kind { + TraceState::NONE, + ); + let name = owned_string(scope, name.try_cast()?); + let span_kind = match span_kind { 0 => SpanKind::Internal, 1 => SpanKind::Server, 2 => SpanKind::Client, 3 => SpanKind::Producer, 4 => SpanKind::Consumer, _ => return Err(anyhow!("invalid span kind")), - }, - name: Cow::Owned(name), - start_time: SystemTime::UNIX_EPOCH - .checked_add(std::time::Duration::from_secs_f64(start_time)) - .ok_or_else(|| anyhow!("invalid start time"))?, - end_time: SystemTime::UNIX_EPOCH - .checked_add(std::time::Duration::from_secs_f64(end_time)) - .ok_or_else(|| anyhow!("invalid start time"))?, - attributes: Vec::new(), - dropped_attributes_count: 0, - events: Default::default(), - links: Default::default(), - status: SpanStatus::Unset, - instrumentation_scope: instrumentation_scope.clone(), - }); - state.put(temporary_span); + }; + let start_time = start_time + .map(|start_time| { + SystemTime::UNIX_EPOCH + .checked_add(std::time::Duration::from_secs_f64(start_time)) + .ok_or_else(|| anyhow!("invalid start time")) + }) + .unwrap_or_else(|| Ok(SystemTime::now()))?; + let span_data = SpanData { + span_context, + parent_span_id, + span_kind, + name: Cow::Owned(name), + start_time, + end_time: SystemTime::UNIX_EPOCH, + attributes: Vec::with_capacity(attribute_count), + dropped_attributes_count: 0, + status: SpanStatus::Unset, + events: SpanEvents::default(), + links: SpanLinks::default(), + instrumentation_scope: self.0.clone(), + }; + Ok(OtelSpan(RefCell::new(OtelSpanState::Recording(span_data)))) + } +} - Ok(()) +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct JsSpanContext { + trace_id: Box, + span_id: Box, + trace_flags: u8, } -#[op2(fast)] -fn op_otel_span_continue( - state: &mut OpState, - #[smi] status: u8, - #[string] error_description: Cow<'_, str>, -) { - if let Some(temporary_span) = state.try_borrow_mut::() { - temporary_span.0.status = match status { +struct OtelSpan(RefCell); + +enum OtelSpanState { + Recording(SpanData), + Done(SpanContext), +} + +impl deno_core::GarbageCollected for OtelSpan {} + +#[op2] +impl OtelSpan { + #[constructor] + #[cppgc] + fn new() -> Result { + Err(type_error("OtelSpan can not be constructed.")) + } + + #[serde] + fn span_context(&self) -> JsSpanContext { + let state = self.0.borrow(); + let span_context = match &*state { + OtelSpanState::Recording(span) => &span.span_context, + OtelSpanState::Done(span_context) => span_context, + }; + JsSpanContext { + trace_id: format!("{:?}", span_context.trace_id()).into(), + span_id: format!("{:?}", span_context.span_id()).into(), + trace_flags: span_context.trace_flags().to_u8(), + } + } + + #[fast] + fn set_status<'s>( + &self, + #[smi] status: u8, + #[string] error_description: String, + ) -> Result<(), AnyError> { + let mut state = self.0.borrow_mut(); + let OtelSpanState::Recording(span) = &mut *state else { + return Ok(()); + }; + span.status = match status { 0 => SpanStatus::Unset, 1 => SpanStatus::Ok, 2 => SpanStatus::Error { - description: Cow::Owned(error_description.into_owned()), + description: Cow::Owned(error_description), }, - _ => return, + _ => return Err(type_error("invalid span status code")), }; + Ok(()) + } + + #[fast] + fn drop_event(&self) { + let mut state = self.0.borrow_mut(); + match &mut *state { + OtelSpanState::Recording(span) => { + span.events.dropped_count += 1; + } + OtelSpanState::Done(_) => {} + } + } + + #[fast] + fn drop_link(&self) { + let mut state = self.0.borrow_mut(); + match &mut *state { + OtelSpanState::Recording(span) => { + span.links.dropped_count += 1; + } + OtelSpanState::Done(_) => {} + } + } + + #[fast] + fn end(&self, end_time: f64) { + let end_time = if end_time.is_nan() { + SystemTime::now() + } else { + SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs_f64(end_time)) + .unwrap() + }; + + let mut state = self.0.borrow_mut(); + match &mut *state { + OtelSpanState::Recording(span) => { + let span_context = span.span_context.clone(); + match std::mem::replace(&mut *state, OtelSpanState::Done(span_context)) + { + OtelSpanState::Recording(mut span) => { + span.end_time = end_time; + let Some(Processors { spans, .. }) = OTEL_PROCESSORS.get() else { + return; + }; + spans.on_end(span); + } + _ => {} + } + } + _ => {} + } } } #[op2(fast)] -fn op_otel_span_attribute<'s>( +fn op_otel_span_attribute1<'s>( scope: &mut v8::HandleScope<'s>, - state: &mut OpState, - #[smi] capacity: u32, + span: v8::Local<'_, v8::Value>, key: v8::Local<'s, v8::Value>, value: v8::Local<'s, v8::Value>, ) { - if let Some(temporary_span) = state.try_borrow_mut::() { - temporary_span.0.attributes.reserve_exact( - (capacity as usize) - .saturating_sub(temporary_span.0.attributes.capacity()), - ); - attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key, value); + let Some(span) = + deno_core::_ops::try_unwrap_cppgc_object::(scope, span) + else { + return; + }; + let mut state = span.0.borrow_mut(); + match &mut *state { + OtelSpanState::Recording(span) => { + attr!(scope, span.attributes => span.dropped_attributes_count, key, value); + } + _ => {} } } #[op2(fast)] fn op_otel_span_attribute2<'s>( scope: &mut v8::HandleScope<'s>, - state: &mut OpState, - #[smi] capacity: u32, + span: v8::Local<'_, v8::Value>, key1: v8::Local<'s, v8::Value>, value1: v8::Local<'s, v8::Value>, key2: v8::Local<'s, v8::Value>, value2: v8::Local<'s, v8::Value>, ) { - if let Some(temporary_span) = state.try_borrow_mut::() { - temporary_span.0.attributes.reserve_exact( - (capacity as usize) - .saturating_sub(temporary_span.0.attributes.capacity()), - ); - 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); + let Some(span) = + deno_core::_ops::try_unwrap_cppgc_object::(scope, span) + else { + return; + }; + let mut state = span.0.borrow_mut(); + match &mut *state { + OtelSpanState::Recording(span) => { + attr!(scope, span.attributes => span.dropped_attributes_count, key1, value1); + attr!(scope, span.attributes => span.dropped_attributes_count, key2, value2); + } + _ => {} } } @@ -1109,8 +1313,7 @@ fn op_otel_span_attribute2<'s>( #[op2(fast)] fn op_otel_span_attribute3<'s>( scope: &mut v8::HandleScope<'s>, - state: &mut OpState, - #[smi] capacity: u32, + span: v8::Local<'_, v8::Value>, key1: v8::Local<'s, v8::Value>, value1: v8::Local<'s, v8::Value>, key2: v8::Local<'s, v8::Value>, @@ -1118,42 +1321,212 @@ fn op_otel_span_attribute3<'s>( key3: v8::Local<'s, v8::Value>, value3: v8::Local<'s, v8::Value>, ) { - if let Some(temporary_span) = state.try_borrow_mut::() { - temporary_span.0.attributes.reserve_exact( - (capacity as usize) - .saturating_sub(temporary_span.0.attributes.capacity()), - ); - 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); + let Some(span) = + deno_core::_ops::try_unwrap_cppgc_object::(scope, span) + else { + return; + }; + let mut state = span.0.borrow_mut(); + match &mut *state { + OtelSpanState::Recording(span) => { + attr!(scope, span.attributes => span.dropped_attributes_count, key1, value1); + attr!(scope, span.attributes => span.dropped_attributes_count, key2, value2); + attr!(scope, span.attributes => span.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, +fn op_otel_span_update_name<'s>( + scope: &mut v8::HandleScope<'s>, + span: v8::Local<'s, v8::Value>, + name: v8::Local<'s, v8::Value>, ) { - if let Some(temporary_span) = state.try_borrow_mut::() { - 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; - } -} - -#[op2(fast)] -fn op_otel_span_flush(state: &mut OpState) { - let Some(temporary_span) = state.try_take::() else { + let Ok(name) = name.try_cast() else { return; }; - - let Some(Processors { spans, .. }) = OTEL_PROCESSORS.get() else { + let name = owned_string(scope, name); + let Some(span) = + deno_core::_ops::try_unwrap_cppgc_object::(scope, span) + else { return; }; + let mut state = span.0.borrow_mut(); + match &mut *state { + OtelSpanState::Recording(span) => span.name = Cow::Owned(name), + _ => {} + } +} + +struct OtelMeter(opentelemetry::metrics::Meter); + +impl deno_core::GarbageCollected for OtelMeter {} + +#[op2] +impl OtelMeter { + #[constructor] + #[cppgc] + fn new( + #[string] name: String, + #[string] version: Option, + #[string] schema_url: Option, + ) -> OtelMeter { + 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 = builder.build(); + let meter = OTEL_PROCESSORS + .get() + .unwrap() + .meter_provider + .meter_with_scope(scope); + OtelMeter(meter) + } + + #[cppgc] + fn create_counter<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + name: v8::Local<'s, v8::Value>, + description: v8::Local<'s, v8::Value>, + unit: v8::Local<'s, v8::Value>, + ) -> Result { + create_instrument( + |name| self.0.f64_counter(name), + |i| Instrument::Counter(i.build()), + scope, + name, + description, + unit, + ) + } - spans.on_end(temporary_span.0); + #[cppgc] + fn create_up_down_counter<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + name: v8::Local<'s, v8::Value>, + description: v8::Local<'s, v8::Value>, + unit: v8::Local<'s, v8::Value>, + ) -> Result { + create_instrument( + |name| self.0.f64_up_down_counter(name), + |i| Instrument::UpDownCounter(i.build()), + scope, + name, + description, + unit, + ) + } + + #[cppgc] + fn create_gauge<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + name: v8::Local<'s, v8::Value>, + description: v8::Local<'s, v8::Value>, + unit: v8::Local<'s, v8::Value>, + ) -> Result { + create_instrument( + |name| self.0.f64_gauge(name), + |i| Instrument::Gauge(i.build()), + scope, + name, + description, + unit, + ) + } + + #[cppgc] + fn create_histogram<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + name: v8::Local<'s, v8::Value>, + description: v8::Local<'s, v8::Value>, + unit: v8::Local<'s, v8::Value>, + #[serde] boundaries: Option>, + ) -> Result { + let name = owned_string(scope, name.try_cast()?); + let mut builder = self.0.f64_histogram(name); + if !description.is_null_or_undefined() { + let description = owned_string(scope, description.try_cast()?); + builder = builder.with_description(description); + }; + if !unit.is_null_or_undefined() { + let unit = owned_string(scope, unit.try_cast()?); + builder = builder.with_unit(unit); + }; + if let Some(boundaries) = boundaries { + builder = builder.with_boundaries(boundaries); + } + + Ok(Instrument::Histogram(builder.build())) + } + + #[cppgc] + fn create_observable_counter<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + name: v8::Local<'s, v8::Value>, + description: v8::Local<'s, v8::Value>, + unit: v8::Local<'s, v8::Value>, + ) -> Result { + create_async_instrument( + |name| self.0.f64_observable_counter(name), + |i| { + i.build(); + }, + scope, + name, + description, + unit, + ) + } + + #[cppgc] + fn create_observable_up_down_counter<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + name: v8::Local<'s, v8::Value>, + description: v8::Local<'s, v8::Value>, + unit: v8::Local<'s, v8::Value>, + ) -> Result { + create_async_instrument( + |name| self.0.f64_observable_up_down_counter(name), + |i| { + i.build(); + }, + scope, + name, + description, + unit, + ) + } + + #[cppgc] + fn create_observable_gauge<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + name: v8::Local<'s, v8::Value>, + description: v8::Local<'s, v8::Value>, + unit: v8::Local<'s, v8::Value>, + ) -> Result { + create_async_instrument( + |name| self.0.f64_observable_gauge(name), + |i| { + i.build(); + }, + scope, + name, + description, + unit, + ) + } } enum Instrument { @@ -1166,32 +1539,16 @@ enum Instrument { impl GarbageCollected for Instrument {} -fn create_instrument<'a, T>( - cb: impl FnOnce( - &'_ opentelemetry::metrics::Meter, - String, - ) -> InstrumentBuilder<'_, T>, - cb2: impl FnOnce(InstrumentBuilder<'_, T>) -> Instrument, - state: &mut OpState, +fn create_instrument<'a, 'b, T>( + cb: impl FnOnce(String) -> InstrumentBuilder<'b, T>, + cb2: impl FnOnce(InstrumentBuilder<'b, T>) -> Instrument, scope: &mut v8::HandleScope<'a>, name: v8::Local<'a, v8::Value>, description: v8::Local<'a, v8::Value>, unit: v8::Local<'a, v8::Value>, ) -> Result { - let Some(InstrumentationScope(instrumentation_scope)) = - state.try_borrow::() - else { - return Err(anyhow!("instrumentation scope not available")); - }; - - let meter = OTEL_PROCESSORS - .get() - .unwrap() - .meter_provider - .meter_with_scope(instrumentation_scope.clone()); - let name = owned_string(scope, name.try_cast()?); - let mut builder = cb(&meter, name); + let mut builder = cb(name); if !description.is_null_or_undefined() { let description = owned_string(scope, description.try_cast()?); builder = builder.with_description(description); @@ -1204,131 +1561,16 @@ fn create_instrument<'a, T>( Ok(cb2(builder)) } -#[op2] -#[cppgc] -fn op_otel_metric_create_counter<'s>( - state: &mut OpState, - scope: &mut v8::HandleScope<'s>, - name: v8::Local<'s, v8::Value>, - description: v8::Local<'s, v8::Value>, - unit: v8::Local<'s, v8::Value>, -) -> Result { - create_instrument( - |meter, name| meter.f64_counter(name), - |i| Instrument::Counter(i.build()), - state, - scope, - name, - description, - unit, - ) -} - -#[op2] -#[cppgc] -fn op_otel_metric_create_up_down_counter<'s>( - state: &mut OpState, - scope: &mut v8::HandleScope<'s>, - name: v8::Local<'s, v8::Value>, - description: v8::Local<'s, v8::Value>, - unit: v8::Local<'s, v8::Value>, -) -> Result { - create_instrument( - |meter, name| meter.f64_up_down_counter(name), - |i| Instrument::UpDownCounter(i.build()), - state, - scope, - name, - description, - unit, - ) -} - -#[op2] -#[cppgc] -fn op_otel_metric_create_gauge<'s>( - state: &mut OpState, - scope: &mut v8::HandleScope<'s>, - name: v8::Local<'s, v8::Value>, - description: v8::Local<'s, v8::Value>, - unit: v8::Local<'s, v8::Value>, -) -> Result { - create_instrument( - |meter, name| meter.f64_gauge(name), - |i| Instrument::Gauge(i.build()), - state, - scope, - name, - description, - unit, - ) -} - -#[op2] -#[cppgc] -fn op_otel_metric_create_histogram<'s>( - state: &mut OpState, - scope: &mut v8::HandleScope<'s>, - name: v8::Local<'s, v8::Value>, - description: v8::Local<'s, v8::Value>, - unit: v8::Local<'s, v8::Value>, - #[serde] boundaries: Option>, -) -> Result { - let Some(InstrumentationScope(instrumentation_scope)) = - state.try_borrow::() - else { - return Err(anyhow!("instrumentation scope not available")); - }; - - let meter = OTEL_PROCESSORS - .get() - .unwrap() - .meter_provider - .meter_with_scope(instrumentation_scope.clone()); - - let name = owned_string(scope, name.try_cast()?); - let mut builder = meter.f64_histogram(name); - if !description.is_null_or_undefined() { - let description = owned_string(scope, description.try_cast()?); - builder = builder.with_description(description); - }; - if !unit.is_null_or_undefined() { - let unit = owned_string(scope, unit.try_cast()?); - builder = builder.with_unit(unit); - }; - if let Some(boundaries) = boundaries { - builder = builder.with_boundaries(boundaries); - } - - Ok(Instrument::Histogram(builder.build())) -} - -fn create_async_instrument<'a, T>( - cb: impl FnOnce( - &'_ opentelemetry::metrics::Meter, - String, - ) -> AsyncInstrumentBuilder<'_, T, f64>, - cb2: impl FnOnce(AsyncInstrumentBuilder<'_, T, f64>), - state: &mut OpState, +fn create_async_instrument<'a, 'b, T>( + cb: impl FnOnce(String) -> AsyncInstrumentBuilder<'b, T, f64>, + cb2: impl FnOnce(AsyncInstrumentBuilder<'b, T, f64>), scope: &mut v8::HandleScope<'a>, name: v8::Local<'a, v8::Value>, description: v8::Local<'a, v8::Value>, unit: v8::Local<'a, v8::Value>, ) -> Result { - let Some(InstrumentationScope(instrumentation_scope)) = - state.try_borrow::() - else { - return Err(anyhow!("instrumentation scope not available")); - }; - - let meter = OTEL_PROCESSORS - .get() - .unwrap() - .meter_provider - .meter_with_scope(instrumentation_scope.clone()); - let name = owned_string(scope, name.try_cast()?); - let mut builder = cb(&meter, name); + let mut builder = cb(name); if !description.is_null_or_undefined() { let description = owned_string(scope, description.try_cast()?); builder = builder.with_description(description); @@ -1354,72 +1596,6 @@ fn create_async_instrument<'a, T>( Ok(Instrument::Observable(data_share)) } -#[op2] -#[cppgc] -fn op_otel_metric_create_observable_counter<'s>( - state: &mut OpState, - scope: &mut v8::HandleScope<'s>, - name: v8::Local<'s, v8::Value>, - description: v8::Local<'s, v8::Value>, - unit: v8::Local<'s, v8::Value>, -) -> Result { - create_async_instrument( - |meter, name| meter.f64_observable_counter(name), - |i| { - i.build(); - }, - state, - scope, - name, - description, - unit, - ) -} - -#[op2] -#[cppgc] -fn op_otel_metric_create_observable_up_down_counter<'s>( - state: &mut OpState, - scope: &mut v8::HandleScope<'s>, - name: v8::Local<'s, v8::Value>, - description: v8::Local<'s, v8::Value>, - unit: v8::Local<'s, v8::Value>, -) -> Result { - create_async_instrument( - |meter, name| meter.f64_observable_up_down_counter(name), - |i| { - i.build(); - }, - state, - scope, - name, - description, - unit, - ) -} - -#[op2] -#[cppgc] -fn op_otel_metric_create_observable_gauge<'s>( - state: &mut OpState, - scope: &mut v8::HandleScope<'s>, - name: v8::Local<'s, v8::Value>, - description: v8::Local<'s, v8::Value>, - unit: v8::Local<'s, v8::Value>, -) -> Result { - create_async_instrument( - |meter, name| meter.f64_observable_gauge(name), - |i| { - i.build(); - }, - state, - scope, - name, - description, - unit, - ) -} - struct MetricAttributes { attributes: Vec, } diff --git a/ext/telemetry/telemetry.ts b/ext/telemetry/telemetry.ts index 86b4fe059dea27..f8b895a88ead30 100644 --- a/ext/telemetry/telemetry.ts +++ b/ext/telemetry/telemetry.ts @@ -2,19 +2,9 @@ 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_log_foreign, op_otel_metric_attribute3, - op_otel_metric_create_counter, - op_otel_metric_create_gauge, - op_otel_metric_create_histogram, - op_otel_metric_create_observable_counter, - op_otel_metric_create_observable_gauge, - op_otel_metric_create_observable_up_down_counter, - op_otel_metric_create_up_down_counter, op_otel_metric_observable_record0, op_otel_metric_observable_record1, op_otel_metric_observable_record2, @@ -25,13 +15,13 @@ import { op_otel_metric_record2, op_otel_metric_record3, op_otel_metric_wait_to_observe, - op_otel_span_attribute, + op_otel_span_attribute1, 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, + op_otel_span_update_name, + OtelMeter, + OtelTracer, + OtelSpan, } from "ext:core/ops"; import { Console } from "ext:deno_console/01_console.js"; import { performance } from "ext:deno_web/15_performance.js"; @@ -40,30 +30,20 @@ const { Array, ArrayPrototypePush, Error, - ObjectAssign, ObjectDefineProperty, ObjectEntries, - ObjectPrototypeIsPrototypeOf, ReflectApply, SafeIterator, SafeMap, SafePromiseAll, SafeSet, - SafeWeakMap, - SafeWeakRef, SafeWeakSet, - String, - StringPrototypePadStart, SymbolFor, - TypedArrayPrototypeSubarray, - Uint8Array, - WeakRefPrototypeDeref, } = primordials; const { AsyncVariable, setAsyncContext } = core; export let TRACING_ENABLED = false; export let METRICS_ENABLED = false; -let DETERMINISTIC = false; // Note: These start at 0 in the JS library, // but start at 1 when serialized with JSON. @@ -103,7 +83,7 @@ interface SpanStatus { message?: string; } -export type AttributeValue = +type AttributeValue = | string | number | boolean @@ -117,9 +97,14 @@ interface Attributes { type SpanAttributes = Attributes; +type TimeInput = [number, number] | number | Date; + interface SpanOptions { - attributes?: Attributes; kind?: SpanKind; + attributes?: Attributes; + links?: Link[]; + startTime?: TimeInput; + root?: boolean; } interface Link { @@ -157,10 +142,6 @@ interface IKeyValue { key: string; value: IAnyValue; } -interface IResource { - attributes: IKeyValue[]; - droppedAttributesCount: number; -} interface InstrumentationLibrary { readonly name: string; @@ -168,463 +149,296 @@ interface InstrumentationLibrary { 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; +function hrToSecs(hr: [number, number]): number { + return (hr[0] * 1e3 + hr[1] / 1e6) / 1000; } -enum ExportResultCode { - SUCCESS = 0, - FAILED = 1, -} +const TRACE_FLAG_SAMPLED = 1 << 0; -interface ExportResult { - code: ExportResultCode; - error?: Error; +const NO_ASYNC_CONTEXT = {}; + +export function enterSpan(span: Span): Context { + if (!span.isRecording()) return; + const context = (CURRENT.get() || ROOT_CONTEXT).setValue(SPAN_KEY, span); + return CURRENT.enter(context); } -function hrToSecs(hr: [number, number]): number { - return ((hr[0] * 1e3 + hr[1] / 1e6) / 1000); +export function restoreContext(context: Context): void { + setAsyncContext(context); } -const TRACE_FLAG_SAMPLED = 1 << 0; +interface OtelTracer { + __key: "tracer"; -const instrumentationScopes = new SafeWeakMap< - InstrumentationLibrary, - { __key: "instrumentation-library" } ->(); -let activeInstrumentationLibrary: WeakRef | null = null; + // deno-lint-ignore no-misused-new + new (name: string, version?: string, schemaUrl?: string): OtelTracer; -function activateInstrumentationLibrary( - instrumentationLibrary: 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, - ); - } - } - } + startSpan( + parent: OtelSpan | undefined, + name: string, + spanKind: SpanKind, + startTime: number | undefined, + attributeCount: number + ): OtelSpan; + + startSpanForeign( + parentTraceId: string, + parentSpanId: string, + name: string, + spanKind: SpanKind, + startTime: number | undefined, + attributeCount: number + ): OtelSpan; } -function submitSpan( - 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 (!TRACING_ENABLED) return; - if (!(traceFlags & TRACE_FLAG_SAMPLED)) return; - - // TODO(@lucacasonato): `resource` is ignored for now, should we implement it? - - activateInstrumentationLibrary(span.instrumentationLibrary); - - 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; - } - } +interface OtelSpan { + spanContext(): SpanContext; + setStatus(status: SpanStatusCode, errorDescription: string): void; + dropEvent(): void; + dropLink(): void; + end(endTime: number): void; +} - // TODO(@lucacasonato): implement links - // TODO(@lucacasonato): implement events +interface TracerOptions { + schemaUrl?: string; +} - 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, - ); +class TraceProvider { + getTracer(name: string, version?: string, options?: TracerOptions): Tracer { + const tracer = new OtelTracer(name, version, options?.schemaUrl); + return new Tracer(tracer); } - - op_otel_span_flush(); } -const now = () => (performance.timeOrigin + performance.now()) / 1000; +class Tracer { + #tracer: OtelTracer; -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); + constructor(tracer: OtelTracer) { + this.#tracer = tracer; + } -const NO_ASYNC_CONTEXT = {}; + startActiveSpan unknown>( + name: string, + fn: F + ): ReturnType; + startActiveSpan unknown>( + name: string, + options: SpanOptions, + fn: F + ): ReturnType; + startActiveSpan unknown>( + name: string, + options: SpanOptions, + context: Context, + fn: F + ): ReturnType; + startActiveSpan unknown>( + name: string, + optionsOrFn: SpanOptions | F, + fnOrContext?: F | Context, + maybeFn?: F + ) { + let options; + let context; + let fn; + if (typeof optionsOrFn === "function") { + options = {}; + fn = optionsOrFn; + } else if (typeof fnOrContext === "function") { + options = optionsOrFn; + fn = fnOrContext; + } else if (typeof maybeFn === "function") { + options = optionsOrFn; + context = fnOrContext; + fn = maybeFn; + } else { + throw new Error("startActiveSpan requires a function argument"); + } + const span = this.startSpan(name, options, context); + const ctx = CURRENT.enter(context); + try { + return ReflectApply(fn, undefined, [span]); + } finally { + setAsyncContext(ctx); + } + } -let otelLog: (message: string, level: number) => void; + startSpan(name: string, options?: SpanOptions, context?: Context): Span { + if (!context && !options?.root) { + context = CURRENT.get(); + } else if (options?.root) { + context = undefined; + } -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]; + let startTime = options?.startTime; + if (startTime && Array.isArray(startTime)) { + startTime = hrToSecs(startTime); + } else if (startTime && startTime instanceof Date) { + startTime = startTime.getTime(); } - } - return table; -})(); -function bytesToHex(bytes: Uint8Array): string { - let out = ""; - for (let i = 0; i < bytes.length; i += 1) { - out += hexSliceLookupTable[bytes[i]]; + const parentSpan = context?.getValue(SPAN_KEY) as + | Span + | { spanContext(): SpanContext } + | undefined; + const attributesCount = options?.attributes + ? Object.keys(options.attributes).length + : 0; + const parentOtelSpan: OtelSpan | null | undefined = + parentSpan !== undefined + ? getOtelSpan(parentSpan) ?? undefined + : undefined; + let otelSpan: OtelSpan; + if (parentOtelSpan || !parentSpan) { + otelSpan = this.#tracer.startSpan( + parentOtelSpan, + name, + options?.kind ?? 0, + startTime, + attributesCount + ); + } else { + const spanContext = parentSpan.spanContext(); + otelSpan = this.#tracer.startSpanForeign( + spanContext.traceId, + spanContext.spanId, + name, + options?.kind ?? 0, + startTime, + attributesCount + ); + } + const span = new Span(otelSpan); + if (options?.links) span.addLinks(options?.links); + if (options?.attributes) span.setAttributes(options?.attributes); + return span; } - 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: string | Uint8Array; - #traceFlags = TRACE_FLAG_SAMPLED; - - #spanContext: SpanContext | null = null; +let getOtelSpan: (span: object) => OtelSpan | null | undefined; - #parentSpanId: string | Uint8Array | null = null; - #parentSpanIdString: string | null = null; - - #recording = TRACING_ENABLED; - - #kind: number = SpanKind.INTERNAL; - #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; +class Span { + #otelSpan: OtelSpan | null; + #spanContext: SpanContext | undefined; 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; - }; - - endSpan = (span: Span) => { - const endTime = now(); - submitSpan( - 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, - ); - }; + getOtelSpan = (span) => (#otelSpan in span ? span.#otelSpan : undefined); } - constructor( - name: string, - options?: SpanOptions, - ) { - 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 = options?.attributes ?? { __proto__: null } as never; - this.#kind = options?.kind ?? SpanKind.INTERNAL; - - const currentSpan: Span | { - spanContext(): { traceId: string; spanId: string }; - } = CURRENT.get()?.getValue(SPAN_KEY); - if (currentSpan) { - if (DETERMINISTIC) { - this.#spanId = StringPrototypePadStart(String(COUNTER++), 16, "0"); - } else { - this.#spanId = new Uint8Array(SPAN_ID_BYTES); - 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; - } - } else { - if (DETERMINISTIC) { - this.#traceId = StringPrototypePadStart(String(COUNTER++), 32, "0"); - this.#spanId = StringPrototypePadStart(String(COUNTER++), 16, "0"); - } else { - const buffer = new Uint8Array(TRACE_ID_BYTES + SPAN_ID_BYTES); - op_crypto_get_random_values(buffer); - this.#traceId = TypedArrayPrototypeSubarray(buffer, 0, TRACE_ID_BYTES); - this.#spanId = TypedArrayPrototypeSubarray(buffer, TRACE_ID_BYTES); - } - } + constructor(otelSpan: OtelSpan | null) { + this.#otelSpan = otelSpan; } 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; + if (this.#otelSpan) { + this.#spanContext = this.#otelSpan.spanContext(); } else { - this.#parentSpanIdString = bytesToHex(this.#parentSpanId); + this.#spanContext = { + traceId: "00000000000000000000000000000000", + spanId: "0000000000000000", + traceFlags: 0, + }; } } - return this.#parentSpanIdString; + return this.#spanContext; } - setAttribute(name: string, value: AttributeValue) { - if (this.#recording) this.#attributes[name] = value; + addEvent( + _name: string, + _attributesOrStartTime?: Attributes | TimeInput, + _startTime?: TimeInput + ): Span { + this.#otelSpan?.dropEvent(); return this; } - setAttributes(attributes: Attributes) { - if (this.#recording) ObjectAssign(this.#attributes, attributes); + addLink(_link: Link): Span { + this.#otelSpan?.dropLink(); 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; - } + addLinks(links: Link[]): Span { + for (const _ in links) { + this.#otelSpan?.dropLink(); } return this; } - updateName(name: string) { - if (this.#recording) this.#name = name; - return this; + end(endTime?: TimeInput): void { + if (endTime && Array.isArray(endTime)) { + endTime = hrToSecs(endTime); + } else if (endTime && endTime instanceof Date) { + endTime = endTime.getTime(); + } + this.#otelSpan?.end(endTime || NaN); } - addEvent(_name: never) { - // TODO(@lucacasonato): implement events - if (this.#recording) this.#droppedEventsCount += 1; - return this; + isRecording(): boolean { + return this.#otelSpan === undefined; } - addLink(_link: never) { - // TODO(@lucacasonato): implement links - if (this.#recording) this.#droppedLinksCount += 1; + // deno-lint-ignore no-explicit-any + recordException(_exception: any, _time?: TimeInput): Span { + this.#otelSpan?.dropEvent(); return this; } - addLinks(links: never[]) { - // TODO(@lucacasonato): implement links - if (this.#recording) this.#droppedLinksCount += links.length; + setAttribute(key: string, value: AttributeValue): Span { + if (!this.#otelSpan) return this; + op_otel_span_attribute1(this.#otelSpan, key, value); 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(); - submitSpan( - context.spanId, - context.traceId, - context.traceFlags, - span.parentSpanId ?? null, - span, - hrToSecs(span.startTime), - hrToSecs(span.endTime), + setAttributes(attributes: Attributes): Span { + if (!this.#otelSpan) return this; + const attributeKvs = ObjectEntries(attributes); + let i = 0; + while (i < attributeKvs.length) { + if (i + 2 < attributeKvs.length) { + op_otel_span_attribute3( + this.#otelSpan, + 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( + this.#otelSpan, + attributeKvs[i][0], + attributeKvs[i][1], + attributeKvs[i + 1][0], + attributeKvs[i + 1][1] + ); + i += 2; + } else { + op_otel_span_attribute1( + this.#otelSpan, + attributeKvs[i][0], + attributeKvs[i][1] + ); + i += 1; } - resultCallback({ code: 0 }); - } catch (error) { - resultCallback({ - code: 1, - error: ObjectPrototypeIsPrototypeOf(error, Error) - ? error as Error - : new Error(String(error)), - }); } + return this; } - async shutdown() {} + setStatus(status: SpanStatus): Span { + this.#otelSpan?.setStatus(status.code, status.message ?? ""); + return this; + } - async forceFlush() {} + updateName(name: string): Span { + if (!this.#otelSpan) return this; + op_otel_span_update_name(this.#otelSpan, name); + return this; + } } const CURRENT = new AsyncVariable(); @@ -677,10 +491,7 @@ class ContextManager { } // deno-lint-ignore no-explicit-any - bind any>( - context: Context, - target: T, - ): T { + bind any>(context: Context, target: T): T { return ((...args) => { const ctx = CURRENT.enter(context); try { @@ -729,9 +540,42 @@ interface MetricAdvice { explicitBucketBoundaries?: number[]; } -export class MeterProvider { +interface OtelMeter { + __key: "meter"; + createCounter(name: string, description?: string, unit?: string): Instrument; + createUpDownCounter( + name: string, + description?: string, + unit?: string + ): Instrument; + createGauge(name: string, description?: string, unit?: string): Instrument; + createHistogram( + name: string, + description?: string, + unit?: string, + explicitBucketBoundaries?: number[] + ): Instrument; + createObservableCounter( + name: string, + description?: string, + unit?: string + ): Instrument; + createObservableUpDownCounter( + name: string, + description?: string, + unit?: string + ): Instrument; + createObservableGauge( + name: string, + description?: string, + unit?: string + ): Instrument; +} + +class MeterProvider { getMeter(name: string, version?: string, options?: MeterOptions): Meter { - return new Meter({ name, version, schemaUrl: options?.schemaUrl }); + const meter = new OtelMeter(name, version, options?.schemaUrl); + return new Meter(meter); } } @@ -741,7 +585,7 @@ type Instrument = { __key: "instrument" }; let batchResultHasObservables: ( res: BatchObservableResult, - observables: Observable[], + observables: Observable[] ) => boolean; class BatchObservableResult { @@ -763,7 +607,7 @@ class BatchObservableResult { observe( metric: Observable, value: number, - attributes?: MetricAttributes, + attributes?: MetricAttributes ): void { if (!this.#observables.has(metric)) return; getObservableResult(metric).observe(value, attributes); @@ -777,142 +621,117 @@ const BATCH_CALLBACKS = new SafeMap< const INDIVIDUAL_CALLBACKS = new SafeMap>(); class Meter { - #instrumentationLibrary: InstrumentationLibrary; + #meter: OtelMeter; - constructor(instrumentationLibrary: InstrumentationLibrary) { - this.#instrumentationLibrary = instrumentationLibrary; + constructor(meter: OtelMeter) { + this.#meter = meter; } - createCounter( - name: string, - options?: MetricOptions, - ): Counter { + createCounter(name: string, options?: MetricOptions): Counter { if (options?.valueType !== undefined && options?.valueType !== 1) { throw new Error("Only valueType: DOUBLE is supported"); } if (!METRICS_ENABLED) return new Counter(null, false); - activateInstrumentationLibrary(this.#instrumentationLibrary); - const instrument = op_otel_metric_create_counter( + const instrument = this.#meter.createCounter( name, // deno-lint-ignore prefer-primordials options?.description, - options?.unit, + options?.unit ) as Instrument; return new Counter(instrument, false); } - createUpDownCounter( - name: string, - options?: MetricOptions, - ): Counter { + createUpDownCounter(name: string, options?: MetricOptions): Counter { if (options?.valueType !== undefined && options?.valueType !== 1) { throw new Error("Only valueType: DOUBLE is supported"); } if (!METRICS_ENABLED) return new Counter(null, true); - activateInstrumentationLibrary(this.#instrumentationLibrary); - const instrument = op_otel_metric_create_up_down_counter( + const instrument = this.#meter.createUpDownCounter( name, // deno-lint-ignore prefer-primordials options?.description, - options?.unit, + options?.unit ) as Instrument; return new Counter(instrument, true); } - createGauge( - name: string, - options?: MetricOptions, - ): Gauge { + createGauge(name: string, options?: MetricOptions): Gauge { if (options?.valueType !== undefined && options?.valueType !== 1) { throw new Error("Only valueType: DOUBLE is supported"); } if (!METRICS_ENABLED) return new Gauge(null); - activateInstrumentationLibrary(this.#instrumentationLibrary); - const instrument = op_otel_metric_create_gauge( + const instrument = this.#meter.createGauge( name, // deno-lint-ignore prefer-primordials options?.description, - options?.unit, + options?.unit ) as Instrument; return new Gauge(instrument); } - createHistogram( - name: string, - options?: MetricOptions, - ): Histogram { + createHistogram(name: string, options?: MetricOptions): Histogram { if (options?.valueType !== undefined && options?.valueType !== 1) { throw new Error("Only valueType: DOUBLE is supported"); } if (!METRICS_ENABLED) return new Histogram(null); - activateInstrumentationLibrary(this.#instrumentationLibrary); - const instrument = op_otel_metric_create_histogram( + const instrument = this.#meter.createHistogram( name, // deno-lint-ignore prefer-primordials options?.description, options?.unit, - options?.advice?.explicitBucketBoundaries, + options?.advice?.explicitBucketBoundaries ) as Instrument; return new Histogram(instrument); } - createObservableCounter( - name: string, - options?: MetricOptions, - ): Observable { + createObservableCounter(name: string, options?: MetricOptions): Observable { if (options?.valueType !== undefined && options?.valueType !== 1) { throw new Error("Only valueType: DOUBLE is supported"); } if (!METRICS_ENABLED) new Observable(new ObservableResult(null, true)); - activateInstrumentationLibrary(this.#instrumentationLibrary); - const instrument = op_otel_metric_create_observable_counter( + const instrument = this.#meter.createObservableCounter( name, // deno-lint-ignore prefer-primordials options?.description, - options?.unit, + options?.unit ) as Instrument; return new Observable(new ObservableResult(instrument, true)); } - createObservableGauge( + createObservableUpDownCounter( name: string, - options?: MetricOptions, + options?: MetricOptions ): Observable { if (options?.valueType !== undefined && options?.valueType !== 1) { throw new Error("Only valueType: DOUBLE is supported"); } if (!METRICS_ENABLED) new Observable(new ObservableResult(null, false)); - activateInstrumentationLibrary(this.#instrumentationLibrary); - const instrument = op_otel_metric_create_observable_gauge( + const instrument = this.#meter.createObservableUpDownCounter( name, // deno-lint-ignore prefer-primordials options?.description, - options?.unit, + options?.unit ) as Instrument; return new Observable(new ObservableResult(instrument, false)); } - createObservableUpDownCounter( - name: string, - options?: MetricOptions, - ): Observable { + createObservableGauge(name: string, options?: MetricOptions): Observable { if (options?.valueType !== undefined && options?.valueType !== 1) { throw new Error("Only valueType: DOUBLE is supported"); } if (!METRICS_ENABLED) new Observable(new ObservableResult(null, false)); - activateInstrumentationLibrary(this.#instrumentationLibrary); - const instrument = op_otel_metric_create_observable_up_down_counter( + const instrument = this.#meter.createObservableGauge( name, // deno-lint-ignore prefer-primordials options?.description, - options?.unit, + options?.unit ) as Instrument; return new Observable(new ObservableResult(instrument, false)); } addBatchObservableCallback( callback: BatchObservableCallback, - observables: Observable[], + observables: Observable[] ): void { if (!METRICS_ENABLED) return; const result = new BatchObservableResult(new SafeWeakSet(observables)); @@ -922,7 +741,7 @@ class Meter { removeBatchObservableCallback( callback: BatchObservableCallback, - observables: Observable[], + observables: Observable[] ): void { if (!METRICS_ENABLED) return; const result = BATCH_CALLBACKS.get(callback); @@ -933,13 +752,13 @@ class Meter { } type BatchObservableCallback = ( - observableResult: BatchObservableResult, + observableResult: BatchObservableResult ) => void | Promise; function record( instrument: Instrument | null, value: number, - attributes?: MetricAttributes, + attributes?: MetricAttributes ) { if (instrument === null) return; if (attributes === undefined) { @@ -961,7 +780,7 @@ function record( attrs[i + 1][0], attrs[i + 1][1], attrs[i + 2][0], - attrs[i + 2][1], + attrs[i + 2][1] ); i += 3; } else if (remaining === 3) { @@ -973,7 +792,7 @@ function record( attrs[i + 1][0], attrs[i + 1][1], attrs[i + 2][0], - attrs[i + 2][1], + attrs[i + 2][1] ); i += 3; } else if (remaining === 2) { @@ -983,16 +802,11 @@ function record( attrs[i][0], attrs[i][1], attrs[i + 1][0], - attrs[i + 1][1], + attrs[i + 1][1] ); i += 2; } else if (remaining === 1) { - op_otel_metric_record1( - instrument, - value, - attrs[i][0], - attrs[i][1], - ); + op_otel_metric_record1(instrument, value, attrs[i][0], attrs[i][1]); i += 1; } } @@ -1002,7 +816,7 @@ function record( function recordObservable( instrument: Instrument | null, value: number, - attributes?: MetricAttributes, + attributes?: MetricAttributes ) { if (instrument === null) return; if (attributes === undefined) { @@ -1024,7 +838,7 @@ function recordObservable( attrs[i + 1][0], attrs[i + 1][1], attrs[i + 2][0], - attrs[i + 2][1], + attrs[i + 2][1] ); i += 3; } else if (remaining === 3) { @@ -1036,7 +850,7 @@ function recordObservable( attrs[i + 1][0], attrs[i + 1][1], attrs[i + 2][0], - attrs[i + 2][1], + attrs[i + 2][1] ); i += 3; } else if (remaining === 2) { @@ -1046,7 +860,7 @@ function recordObservable( attrs[i][0], attrs[i][1], attrs[i + 1][0], - attrs[i + 1][1], + attrs[i + 1][1] ); i += 2; } else if (remaining === 1) { @@ -1054,7 +868,7 @@ function recordObservable( instrument, value, attrs[i][0], - attrs[i][1], + attrs[i][1] ); i += 1; } @@ -1089,7 +903,7 @@ class Gauge { record( value: number, attributes?: MetricAttributes, - _context?: Context, + _context?: Context ): void { record(this.#instrument, value, attributes); } @@ -1105,14 +919,14 @@ class Histogram { record( value: number, attributes?: MetricAttributes, - _context?: Context, + _context?: Context ): void { record(this.#instrument, value, attributes); } } type ObservableCallback = ( - observableResult: ObservableResult, + observableResult: ObservableResult ) => void | Promise; let getObservableResult: (observable: Observable) => ObservableResult; @@ -1154,7 +968,7 @@ class ObservableResult { observe( this: ObservableResult, value: number, - attributes?: MetricAttributes, + attributes?: MetricAttributes ): void { if (this.#isRegularCounter) { if (value < 0) { @@ -1212,24 +1026,45 @@ const otelConsoleConfig = { replace: 2, }; +function otelLog(message: string, level: number) { + const currentSpan = CURRENT.get()?.getValue(SPAN_KEY); + const otelSpan = + currentSpan !== undefined ? getOtelSpan(currentSpan) : undefined; + if (otelSpan || currentSpan === undefined) { + op_otel_log(message, level, otelSpan); + } else { + let spanContext = currentSpan.spanContext(); + op_otel_log_foreign( + message, + level, + spanContext.traceId, + spanContext.spanId, + spanContext.traceFlags + ); + } +} + +let builtinTracerCache: Tracer; + +export function builtinTracer(): Tracer { + if (!builtinTracerCache) { + builtinTracerCache = new Tracer(OtelTracer.builtin()); + } + return builtinTracerCache; +} + export function bootstrap( config: [ 0 | 1, 0 | 1, - typeof otelConsoleConfig[keyof typeof otelConsoleConfig], - 0 | 1, - ], + (typeof otelConsoleConfig)[keyof typeof otelConsoleConfig], + 0 | 1 + ] ): void { - const { - 0: tracingEnabled, - 1: metricsEnabled, - 2: consoleConfig, - 3: deterministic, - } = config; + const { 0: tracingEnabled, 1: metricsEnabled, 2: consoleConfig } = config; TRACING_ENABLED = tracingEnabled === 1; METRICS_ENABLED = metricsEnabled === 1; - DETERMINISTIC = deterministic === 1; switch (consoleConfig) { case otelConsoleConfig.capture: @@ -1239,7 +1074,7 @@ export function bootstrap( ObjectDefineProperty( globalThis, "console", - core.propNonEnumerable(new Console(otelLog)), + core.propNonEnumerable(new Console(otelLog)) ); break; default: @@ -1248,7 +1083,7 @@ export function bootstrap( } export const telemetry = { - SpanExporter, + TraceProvider, ContextManager, MeterProvider, };