From 4b35ba6b13c8fb33629707797962898a138e4140 Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Mon, 6 Jan 2025 14:28:29 +0100 Subject: [PATCH] feat(unstable): replace SpanExporter with TracerProvider (#27473) --- cli/main.rs | 5 +- cli/mainrt.rs | 5 +- cli/tsc/dts/lib.deno.unstable.d.ts | 31 +- ext/fetch/26_fetch.js | 37 +- ext/http/00_serve.ts | 61 +- ext/telemetry/lib.rs | 1063 ++++++++++------- ext/telemetry/telemetry.ts | 921 ++++++-------- .../jsr/@deno/otel/0.0.2/src/index.ts | 31 +- tests/specs/cli/otel_basic/basic.out | 26 +- tests/specs/cli/otel_basic/context.ts | 4 +- tests/specs/cli/otel_basic/main.ts | 9 +- tests/specs/cli/otel_basic/metric.ts | 2 +- 12 files changed, 1142 insertions(+), 1053 deletions(-) diff --git a/cli/main.rs b/cli/main.rs index 2e55d9d286..7db471932d 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -447,8 +447,9 @@ fn resolve_flags_and_init( } }; - deno_telemetry::init(crate::args::otel_runtime_config())?; - util::logger::init(flags.log_level, Some(flags.otel_config())); + let otel_config = flags.otel_config(); + deno_telemetry::init(crate::args::otel_runtime_config(), &otel_config)?; + util::logger::init(flags.log_level, Some(otel_config)); // TODO(bartlomieju): remove in Deno v2.5 and hard error then. if flags.unstable_config.legacy_flag_enabled { diff --git a/cli/mainrt.rs b/cli/mainrt.rs index ce6fddaee9..1279554514 100644 --- a/cli/mainrt.rs +++ b/cli/mainrt.rs @@ -89,7 +89,10 @@ fn main() { let future = async move { match standalone { Ok(Some(data)) => { - deno_telemetry::init(crate::args::otel_runtime_config())?; + deno_telemetry::init( + crate::args::otel_runtime_config(), + &data.metadata.otel_config, + )?; util::logger::init( data.metadata.log_level, Some(data.metadata.otel_config.clone()), diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index dbe4bace0c..3de9845fc8 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -1301,20 +1301,43 @@ 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 + * + * This is a singleton object that implements the OpenTelemetry + * TracerProvider interface. + * * @category Telemetry * @experimental */ - export class SpanExporter {} + // deno-lint-ignore no-explicit-any + export const tracerProvider: any; /** * A ContextManager compatible with OpenTelemetry.js * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.ContextManager.html + * + * This is a singleton object that implements the OpenTelemetry + * ContextManager interface. + * * @category Telemetry * @experimental */ - export class ContextManager {} + // deno-lint-ignore no-explicit-any + export const contextManager: any; + + /** + * A MeterProvider compatible with OpenTelemetry.js + * https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api.MeterProvider.html + * + * This is a singleton object that implements the OpenTelemetry + * MeterProvider interface. + * + * @category Telemetry + * @experimental + */ + // deno-lint-ignore no-explicit-any + export const meterProvider: any; export {}; // only export exports } diff --git a/ext/fetch/26_fetch.js b/ext/fetch/26_fetch.js index f26e2e3fb9..7d5f5ea2f7 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 { @@ -320,10 +319,10 @@ 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 ( @@ -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. @@ -454,9 +454,7 @@ function fetch(input, init = { __proto__: null }) { await opPromise; return result; } finally { - if (span) { - endSpan(span); - } + span?.end(); } })(); } @@ -469,19 +467,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 +504,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) + ); } /** diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index 0ef06d1902..5ce0a4bf7f 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, + builtinTracer, enterSpan, - Span, TRACING_ENABLED, } from "ext:deno_telemetry/telemetry.ts"; import { @@ -288,28 +284,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() { @@ -414,10 +410,7 @@ class InnerRequest { return; } - PromisePrototypeThen( - op_http_request_on_cancel(this.#external), - callback, - ); + PromisePrototypeThen(op_http_request_on_cancel(this.#external), callback); } } @@ -521,12 +514,7 @@ 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); @@ -556,10 +544,7 @@ 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)) { @@ -636,12 +621,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. @@ -688,7 +673,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"; @@ -730,11 +715,12 @@ 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({ @@ -843,10 +829,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(); }; @@ -964,7 +947,7 @@ function registerDeclarativeServer(exports) { port: servePort, hostname: serveHost, [kLoadBalanced]: (serveIsMain && serveWorkerCount > 1) || - (serveWorkerCount !== null), + serveWorkerCount !== null, onListen: ({ port, hostname }) => { if (serveIsMain) { const nThreads = serveWorkerCount > 1 diff --git a/ext/telemetry/lib.rs b/ext/telemetry/lib.rs index eeff09ed88..261e93124d 100644 --- a/ext/telemetry/lib.rs +++ b/ext/telemetry/lib.rs @@ -1,5 +1,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. +#![allow(clippy::too_many_arguments)] + use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; @@ -7,6 +9,7 @@ use std::env; use std::fmt::Debug; use std::pin::Pin; use std::rc::Rc; +use std::sync::atomic::AtomicU64; use std::sync::Arc; use std::sync::Mutex; use std::task::Context; @@ -17,6 +20,8 @@ use std::time::SystemTime; 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; @@ -35,7 +40,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; @@ -44,6 +49,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; @@ -63,7 +70,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; @@ -79,23 +90,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, @@ -108,6 +107,7 @@ deno_core::extension!( op_otel_metric_wait_to_observe, op_otel_metric_observation_done, ], + objects = [OtelTracer, OtelMeter, OtelSpan], esm = ["telemetry.ts", "util.ts"], ); @@ -550,19 +550,20 @@ mod hyper_client { } } -struct Processors { - spans: BatchSpanProcessor, - logs: BatchLogProcessor, +struct OtelGlobals { + span_processor: BatchSpanProcessor, + log_processor: BatchLogProcessor, + id_generator: DenoIdGenerator, meter_provider: SdkMeterProvider, + builtin_instrumentation_scope: InstrumentationScope, } -static OTEL_PROCESSORS: OnceCell = OnceCell::new(); +static OTEL_GLOBALS: OnceCell = OnceCell::new(); -static BUILT_IN_INSTRUMENTATION_SCOPE: OnceCell< - opentelemetry::InstrumentationScope, -> = OnceCell::new(); - -pub fn init(rt_config: OtelRuntimeConfig) -> anyhow::Result<()> { +pub fn init( + rt_config: OtelRuntimeConfig, + config: &OtelConfig, +) -> anyhow::Result<()> { // Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_* // crates don't do this automatically. // TODO(piscisaureus): enable GRPC support. @@ -668,21 +669,26 @@ pub fn init(rt_config: OtelRuntimeConfig) -> anyhow::Result<()> { BatchLogProcessor::builder(log_exporter, OtelSharedRuntime).build(); log_processor.set_resource(&resource); - OTEL_PROCESSORS - .set(Processors { - spans: span_processor, - logs: log_processor, - meter_provider, - }) - .map_err(|_| anyhow!("failed to init otel"))?; - let builtin_instrumentation_scope = opentelemetry::InstrumentationScope::builder("deno") .with_version(rt_config.runtime_version.clone()) .build(); - BUILT_IN_INSTRUMENTATION_SCOPE - .set(builtin_instrumentation_scope) - .map_err(|_| anyhow!("failed to init otel"))?; + + let id_generator = if config.deterministic { + DenoIdGenerator::deterministic() + } else { + DenoIdGenerator::random() + }; + + OTEL_GLOBALS + .set(OtelGlobals { + log_processor, + span_processor, + id_generator, + meter_provider, + builtin_instrumentation_scope, + }) + .map_err(|_| anyhow!("failed to set otel globals"))?; Ok(()) } @@ -691,11 +697,12 @@ pub fn init(rt_config: OtelRuntimeConfig) -> anyhow::Result<()> { /// `process::exit()`, to ensure that all OpenTelemetry logs are properly /// flushed before the process terminates. pub fn flush() { - if let Some(Processors { - spans, - logs, + if let Some(OtelGlobals { + span_processor: spans, + log_processor: logs, meter_provider, - }) = OTEL_PROCESSORS.get() + .. + }) = OTEL_GLOBALS.get() { let _ = spans.force_flush(); let _ = logs.force_flush(); @@ -706,7 +713,12 @@ pub fn flush() { pub fn handle_log(record: &log::Record) { use log::Level; - let Some(Processors { logs, .. }) = OTEL_PROCESSORS.get() else { + let Some(OtelGlobals { + log_processor: logs, + builtin_instrumentation_scope, + .. + }) = OTEL_GLOBALS.get() + else { return; }; @@ -756,10 +768,58 @@ pub fn handle_log(record: &log::Record) { let _ = record.key_values().visit(&mut Visitor(&mut log_record)); - logs.emit( - &mut log_record, - BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap(), - ); + logs.emit(&mut log_record, builtin_instrumentation_scope); +} + +#[derive(Debug)] +enum DenoIdGenerator { + Random(RandomIdGenerator), + Deterministic { + next_trace_id: AtomicU64, + next_span_id: AtomicU64, + }, +} + +impl IdGenerator for DenoIdGenerator { + fn new_trace_id(&self) -> TraceId { + match self { + Self::Random(generator) => generator.new_trace_id(), + Self::Deterministic { next_trace_id, .. } => { + let id = + next_trace_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let bytes = id.to_be_bytes(); + let bytes = [ + 0, 0, 0, 0, 0, 0, 0, 0, bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + ]; + TraceId::from_bytes(bytes) + } + } + } + + fn new_span_id(&self) -> SpanId { + match self { + Self::Random(generator) => generator.new_span_id(), + Self::Deterministic { next_span_id, .. } => { + let id = + next_span_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + SpanId::from_bytes(id.to_be_bytes()) + } + } + } +} + +impl DenoIdGenerator { + fn random() -> Self { + Self::Random(RandomIdGenerator::default()) + } + + fn deterministic() -> Self { + Self::Deterministic { + next_trace_id: AtomicU64::new(1), + next_span_id: AtomicU64::new(1), + } + } } fn parse_trace_id( @@ -851,6 +911,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 }; @@ -876,48 +939,66 @@ 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(OtelGlobals { + log_processor, + builtin_instrumentation_scope, + .. + }) = OTEL_GLOBALS.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()), + ); + } + } } + + log_processor.emit(&mut log_record, builtin_instrumentation_scope); } #[op2(fast)] -fn op_otel_log( +fn op_otel_log_foreign( scope: &mut v8::HandleScope<'_>, #[string] message: String, #[smi] level: i32, @@ -925,10 +1006,12 @@ fn op_otel_log( span_id: v8::Local<'_, v8::Value>, #[smi] trace_flags: u8, ) { - let Some(Processors { logs, .. }) = OTEL_PROCESSORS.get() else { - return; - }; - let Some(instrumentation_scope) = BUILT_IN_INSTRUMENTATION_SCOPE.get() else { + let Some(OtelGlobals { + log_processor, + builtin_instrumentation_scope, + .. + }) = OTEL_GLOBALS.get() + else { return; }; @@ -958,7 +1041,7 @@ fn op_otel_log( ); } - logs.emit(&mut log_record, instrumentation_scope); + log_processor.emit(&mut log_record, builtin_instrumentation_scope); } fn owned_string<'s>( @@ -974,136 +1057,328 @@ 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); - }; +impl deno_core::GarbageCollected for OtelTracer {} - let Some(InstrumentationScope(instrumentation_scope)) = - state.try_borrow::() - else { - return Err(anyhow!("instrumentation scope not available")); - }; - - 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 { + let OtelGlobals { + builtin_instrumentation_scope, + .. + } = OTEL_GLOBALS.get().unwrap(); + OtelTracer(builtin_instrumentation_scope.clone()) } - let parent_span_id = parse_span_id(scope, parent_span_id); - - let name = owned_string(scope, name.try_cast()?); - - let temporary_span = TemporarySpan(SpanData { - span_context: SpanContext::new( - trace_id, - span_id, - TraceFlags::SAMPLED, - false, - Default::default(), - ), - parent_span_id, - span_kind: match span_kind { + #[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 OtelGlobals { id_generator, .. } = OTEL_GLOBALS.get().unwrap(); + 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(), + id_generator.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( + id_generator.new_trace_id(), + id_generator.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")), - }, - 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(Box::new(OtelSpanState::Recording( + span_data, + ))))) + } - Ok(()) + #[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 OtelGlobals { id_generator, .. } = OTEL_GLOBALS.get().unwrap(); + let span_context = SpanContext::new( + parent_trace_id, + id_generator.new_span_id(), + TraceFlags::SAMPLED, + false, + 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")), + }; + 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(Box::new(OtelSpanState::Recording( + span_data, + ))))) + } } -#[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 { +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct JsSpanContext { + trace_id: Box, + span_id: Box, + trace_flags: u8, +} + +// boxed because of https://github.com/denoland/rusty_v8/issues/1676 +#[derive(Debug)] +struct OtelSpan(RefCell>); + +#[derive(Debug)] +#[allow(clippy::large_enum_variant)] +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(); + if let OtelSpanState::Recording(span) = &mut **state { + let span_context = span.span_context.clone(); + if let OtelSpanState::Recording(mut span) = *std::mem::replace( + &mut *state, + Box::new(OtelSpanState::Done(span_context)), + ) { + span.end_time = end_time; + let Some(OtelGlobals { span_processor, .. }) = OTEL_GLOBALS.get() + else { + return; + }; + span_processor.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(); + if let OtelSpanState::Recording(span) = &mut **state { + 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(); + if let OtelSpanState::Recording(span) = &mut **state { + attr!(scope, span.attributes => span.dropped_attributes_count, key1, value1); + attr!(scope, span.attributes => span.dropped_attributes_count, key2, value2); } } @@ -1111,8 +1386,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>, @@ -1120,42 +1394,208 @@ 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(); + if let OtelSpanState::Recording(span) = &mut **state { + 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; + let Ok(name) = name.try_cast() else { + return; + }; + 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(); + if let OtelSpanState::Recording(span) = &mut **state { + span.name = Cow::Owned(name) } } -#[op2(fast)] -fn op_otel_span_flush(state: &mut OpState) { - let Some(temporary_span) = state.try_take::() else { - return; - }; +struct OtelMeter(opentelemetry::metrics::Meter); - let Some(Processors { spans, .. }) = OTEL_PROCESSORS.get() else { - return; - }; +impl deno_core::GarbageCollected for OtelMeter {} - spans.on_end(temporary_span.0); +#[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_GLOBALS + .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, + ) + } + + #[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 { @@ -1168,32 +1608,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); @@ -1206,131 +1630,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); @@ -1356,72 +1665,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 31e052c28b..f4277c3722 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,45 +15,39 @@ 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, + OtelSpan, + OtelTracer, } from "ext:core/ops"; import { Console } from "ext:deno_console/01_console.js"; -import { performance } from "ext:deno_web/15_performance.js"; const { - Array, + ArrayIsArray, ArrayPrototypePush, + DatePrototype, + DatePrototypeGetTime, Error, - ObjectAssign, ObjectDefineProperty, ObjectEntries, + ObjectKeys, ObjectPrototypeIsPrototypeOf, ReflectApply, SafeIterator, SafeMap, SafePromiseAll, SafeSet, - SafeWeakMap, - SafeWeakRef, SafeWeakSet, - String, - StringPrototypePadStart, SymbolFor, - TypedArrayPrototypeSubarray, - Uint8Array, - WeakRefPrototypeDeref, + TypeError, } = 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. @@ -90,8 +74,6 @@ interface SpanContext { traceState?: TraceState; } -type HrTime = [number, number]; - enum SpanStatusCode { UNSET = 0, OK = 1, @@ -103,7 +85,7 @@ interface SpanStatus { message?: string; } -export type AttributeValue = +type AttributeValue = | string | number | boolean @@ -117,9 +99,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 { @@ -128,13 +115,6 @@ interface Link { droppedAttributesCount?: number; } -interface TimedEvent { - time: HrTime; - name: string; - attributes?: SpanAttributes; - droppedAttributesCount?: number; -} - interface IArrayValue { values: IAnyValue[]; } @@ -157,482 +137,322 @@ 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); + return (hr[0] * 1e3 + hr[1] / 1e6) / 1000; } -const TRACE_FLAG_SAMPLED = 1 << 0; +export function enterSpan(span: Span): Context | undefined { + if (!span.isRecording()) return undefined; + const context = (CURRENT.get() || ROOT_CONTEXT).setValue(SPAN_KEY, span); + return CURRENT.enter(context); +} -const instrumentationScopes = new SafeWeakMap< - InstrumentationLibrary, - { __key: "instrumentation-library" } ->(); -let activeInstrumentationLibrary: WeakRef | null = null; +export function restoreContext(context: Context): void { + setAsyncContext(context); +} -function activateInstrumentationLibrary( - instrumentationLibrary: InstrumentationLibrary, -) { - if ( - !activeInstrumentationLibrary || - WeakRefPrototypeDeref(activeInstrumentationLibrary) !== - instrumentationLibrary +function isDate(value: unknown): value is Date { + return ObjectPrototypeIsPrototypeOf(value, DatePrototype); +} + +interface OtelTracer { + __key: "tracer"; + + // deno-lint-ignore no-misused-new + new (name: string, version?: string, schemaUrl?: string): OtelTracer; + + 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; +} + +interface OtelSpan { + __key: "span"; + + spanContext(): SpanContext; + setStatus(status: SpanStatusCode, errorDescription: string): void; + dropEvent(): void; + dropLink(): void; + end(endTime: number): void; +} + +interface TracerOptions { + schemaUrl?: string; +} + +class TracerProvider { + constructor() { + throw new TypeError("TracerProvider can not be constructed"); + } + + static getTracer( + name: string, + version?: string, + options?: TracerOptions, + ): Tracer { + const tracer = new OtelTracer(name, version, options?.schemaUrl); + return new Tracer(tracer); + } +} + +class Tracer { + #tracer: OtelTracer; + + constructor(tracer: OtelTracer) { + this.#tracer = tracer; + } + + 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, ) { - activeInstrumentationLibrary = new SafeWeakRef(instrumentationLibrary); - if (instrumentationLibrary === BUILTIN_INSTRUMENTATION_LIBRARY) { - op_otel_instrumentation_scope_enter_builtin(); + let options; + let context; + let fn; + if (typeof optionsOrFn === "function") { + options = undefined; + fn = optionsOrFn; + } else if (typeof fnOrContext === "function") { + options = optionsOrFn; + fn = fnOrContext; + } else if (typeof maybeFn === "function") { + options = optionsOrFn; + context = fnOrContext; + fn = maybeFn; } 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, - ); - } + throw new Error("startActiveSpan requires a function argument"); } - } -} - -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; + if (options?.root) { + context = undefined; } else { - op_otel_span_attribute( - attributeKvs.length, - attributeKvs[i][0], - attributeKvs[i][1], + context = context ?? CURRENT.get(); + } + const span = this.startSpan(name, options, context); + const ctx = CURRENT.enter(context.setValue(SPAN_KEY, span)); + try { + return ReflectApply(fn, undefined, [span]); + } finally { + setAsyncContext(ctx); + } + } + + startSpan(name: string, options?: SpanOptions, context?: Context): Span { + if (options?.root) { + context = undefined; + } else { + context = context ?? CURRENT.get(); + } + + let startTime = options?.startTime; + if (startTime && ArrayIsArray(startTime)) { + startTime = hrToSecs(startTime); + } else if (startTime && isDate(startTime)) { + startTime = DatePrototypeGetTime(startTime); + } + + const parentSpan = context?.getValue(SPAN_KEY) as + | Span + | { spanContext(): SpanContext } + | undefined; + const attributesCount = options?.attributes + ? ObjectKeys(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, ); - i += 1; } + const span = new Span(otelSpan); + if (options?.links) span.addLinks(options?.links); + if (options?.attributes) span.setAttributes(options?.attributes); + return span; } - - // 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 getOtelSpan: (span: object) => OtelSpan | null | undefined; -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; - - #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, - ); - }; + // deno-lint-ignore prefer-primordials + 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, - }; + if (this.#otelSpan) { + this.#spanContext = this.#otelSpan.spanContext(); + } else { + this.#spanContext = { + traceId: "00000000000000000000000000000000", + spanId: "0000000000000000", + traceFlags: 0, + }; + } } 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; + 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 (let i = 0; i < links.length; i++) { + this.#otelSpan?.dropLink(); } return this; } - updateName(name: string) { - if (this.#recording) this.#name = name; + end(endTime?: TimeInput): void { + if (endTime && ArrayIsArray(endTime)) { + endTime = hrToSecs(endTime); + } else if (endTime && isDate(endTime)) { + endTime = DatePrototypeGetTime(endTime); + } + this.#otelSpan?.end(endTime || NaN); + } + + isRecording(): boolean { + return this.#otelSpan !== undefined; + } + + // deno-lint-ignore no-explicit-any + recordException(_exception: any, _time?: TimeInput): Span { + this.#otelSpan?.dropEvent(); return this; } - addEvent(_name: never) { - // TODO(@lucacasonato): implement events - if (this.#recording) this.#droppedEventsCount += 1; + setAttribute(key: string, value: AttributeValue): Span { + if (!this.#otelSpan) return this; + op_otel_span_attribute1(this.#otelSpan, key, value); 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(); - 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(); class Context { + // @ts-ignore __proto__ is not supported in TypeScript #data: Record = { __proto__: null }; constructor(data?: Record | null | undefined) { + // @ts-ignore __proto__ is not supported in TypeScript this.#data = { __proto__: null, ...data }; } @@ -658,11 +478,15 @@ const ROOT_CONTEXT = new Context(); // Context manager for opentelemetry js library class ContextManager { - active(): Context { + constructor() { + throw new TypeError("ContextManager can not be constructed"); + } + + static active(): Context { return CURRENT.get() ?? ROOT_CONTEXT; } - with ReturnType>( + static with ReturnType>( context: Context, fn: F, thisArg?: ThisParameterType, @@ -677,7 +501,7 @@ class ContextManager { } // deno-lint-ignore no-explicit-any - bind any>( + static bind any>( context: Context, target: T, ): T { @@ -691,11 +515,11 @@ class ContextManager { }) as T; } - enable() { + static enable() { return this; } - disable() { + static disable() { return this; } } @@ -729,9 +553,50 @@ interface MetricAdvice { explicitBucketBoundaries?: number[]; } -export class MeterProvider { - getMeter(name: string, version?: string, options?: MeterOptions): Meter { - return new Meter({ name, version, schemaUrl: options?.schemaUrl }); +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 { + constructor() { + throw new TypeError("MeterProvider can not be constructed"); + } + + static getMeter( + name: string, + version?: string, + options?: MeterOptions, + ): Meter { + const meter = new OtelMeter(name, version, options?.schemaUrl); + return new Meter(meter); } } @@ -777,22 +642,18 @@ 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, @@ -801,16 +662,12 @@ class Meter { 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, @@ -819,16 +676,12 @@ class Meter { 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, @@ -837,16 +690,12 @@ class Meter { 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, @@ -856,16 +705,12 @@ class Meter { 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, @@ -874,24 +719,6 @@ class Meter { return new Observable(new ObservableResult(instrument, true)); } - 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_gauge( - name, - // deno-lint-ignore prefer-primordials - options?.description, - options?.unit, - ) as Instrument; - return new Observable(new ObservableResult(instrument, false)); - } - createObservableUpDownCounter( name: string, options?: MetricOptions, @@ -900,8 +727,21 @@ class Meter { 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.createObservableUpDownCounter( + name, + // deno-lint-ignore prefer-primordials + options?.description, + options?.unit, + ) as Instrument; + return new Observable(new ObservableResult(instrument, false)); + } + + 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)); + const instrument = this.#meter.createObservableGauge( name, // deno-lint-ignore prefer-primordials options?.description, @@ -987,12 +827,7 @@ function record( ); 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; } } @@ -1212,24 +1047,46 @@ 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 { + const 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], + (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: @@ -1248,7 +1105,7 @@ export function bootstrap( } export const telemetry = { - SpanExporter, - ContextManager, - MeterProvider, + tracerProvider: TracerProvider, + contextManager: ContextManager, + meterProvider: MeterProvider, }; diff --git a/tests/registry/jsr/@deno/otel/0.0.2/src/index.ts b/tests/registry/jsr/@deno/otel/0.0.2/src/index.ts index 9c44457832..da72260d42 100644 --- a/tests/registry/jsr/@deno/otel/0.0.2/src/index.ts +++ b/tests/registry/jsr/@deno/otel/0.0.2/src/index.ts @@ -1,38 +1,15 @@ // Copyright 2024-2024 the Deno authors. All rights reserved. MIT license. -import { context } from "npm:@opentelemetry/api@1"; -import { - BasicTracerProvider, - SimpleSpanProcessor, -} from "npm:@opentelemetry/sdk-trace-base@1"; +import { context, trace, metrics } from "npm:@opentelemetry/api@1"; // @ts-ignore Deno.telemetry is not typed yet const telemetry = Deno.telemetry ?? Deno.tracing; -let COUNTER = 1; - /** * Register `Deno.telemetry` with the OpenTelemetry library. */ export function register() { - context.setGlobalContextManager( - new telemetry.ContextManager() ?? telemetry.ContextManager(), - ); - - const provider = new BasicTracerProvider({ - idGenerator: Deno.env.get("DENO_UNSTABLE_OTEL_DETERMINISTIC") === "1" ? { - generateSpanId() { - return "1" + String(COUNTER++).padStart(15, "0"); - }, - generateTraceId() { - return "1" + String(COUNTER++).padStart(31, "0"); - } - } : undefined - }); - - // @ts-ignore Deno.tracing is not typed yet - const exporter = new telemetry.SpanExporter(); - provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); - - provider.register(); + context.setGlobalContextManager(telemetry.contextManager); + trace.setGlobalTracerProvider(telemetry.tracerProvider); + metrics.setGlobalMeterProvider(telemetry.meterProvider); } diff --git a/tests/specs/cli/otel_basic/basic.out b/tests/specs/cli/otel_basic/basic.out index 1e82ba59b3..c16f57a8fc 100644 --- a/tests/specs/cli/otel_basic/basic.out +++ b/tests/specs/cli/otel_basic/basic.out @@ -2,7 +2,7 @@ "spans": [ { "traceId": "00000000000000000000000000000001", - "spanId": "0000000000000002", + "spanId": "0000000000000001", "traceState": "", "parentSpanId": "", "flags": 1, @@ -59,8 +59,8 @@ } }, { - "traceId": "00000000000000000000000000000003", - "spanId": "0000000000000004", + "traceId": "00000000000000000000000000000002", + "spanId": "0000000000000002", "traceState": "", "parentSpanId": "", "flags": 1, @@ -117,10 +117,10 @@ } }, { - "traceId": "00000000000000000000000000000003", - "spanId": "1000000000000001", + "traceId": "00000000000000000000000000000002", + "spanId": "0000000000000003", "traceState": "", - "parentSpanId": "0000000000000004", + "parentSpanId": "0000000000000002", "flags": 1, "name": "outer span", "kind": 1, @@ -138,10 +138,10 @@ } }, { - "traceId": "00000000000000000000000000000003", - "spanId": "1000000000000002", + "traceId": "00000000000000000000000000000002", + "spanId": "0000000000000004", "traceState": "", - "parentSpanId": "1000000000000001", + "parentSpanId": "0000000000000003", "flags": 1, "name": "inner span", "kind": 1, @@ -171,8 +171,8 @@ "attributes": [], "droppedAttributesCount": 0, "flags": 1, - "traceId": "00000000000000000000000000000003", - "spanId": "1000000000000002" + "traceId": "00000000000000000000000000000002", + "spanId": "0000000000000004" }, { "timeUnixNano": "0", @@ -185,8 +185,8 @@ "attributes": [], "droppedAttributesCount": 0, "flags": 1, - "traceId": "00000000000000000000000000000003", - "spanId": "1000000000000002" + "traceId": "00000000000000000000000000000002", + "spanId": "0000000000000004" } ], "metrics": [] diff --git a/tests/specs/cli/otel_basic/context.ts b/tests/specs/cli/otel_basic/context.ts index cef0dbd81a..16b08835ff 100644 --- a/tests/specs/cli/otel_basic/context.ts +++ b/tests/specs/cli/otel_basic/context.ts @@ -2,9 +2,7 @@ import { assertEquals } from "@std/assert"; const { ContextManager } = Deno.telemetry; -const cm = new ContextManager(); - -const a = cm.active(); +const a = ContextManager.active(); const b = a.setValue("b", 1); const c = b.setValue("c", 2); diff --git a/tests/specs/cli/otel_basic/main.ts b/tests/specs/cli/otel_basic/main.ts index 608d1d3341..921c39911b 100644 --- a/tests/specs/cli/otel_basic/main.ts +++ b/tests/specs/cli/otel_basic/main.ts @@ -21,8 +21,13 @@ const server = Deno.serve( stdout: "null", }); const child = command.spawn(); - child.output() - .then(() => server.shutdown()) + child.status + .then((status) => { + if (status.signal) { + throw new Error("child process failed: " + JSON.stringify(status)); + } + return server.shutdown(); + }) .then(() => { data.logs.sort((a, b) => Number( diff --git a/tests/specs/cli/otel_basic/metric.ts b/tests/specs/cli/otel_basic/metric.ts index 2b472a6fb8..f95c0cb802 100644 --- a/tests/specs/cli/otel_basic/metric.ts +++ b/tests/specs/cli/otel_basic/metric.ts @@ -1,6 +1,6 @@ import { metrics } from "npm:@opentelemetry/api@1"; -metrics.setGlobalMeterProvider(new Deno.telemetry.MeterProvider()); +metrics.setGlobalMeterProvider(Deno.telemetry.meterProvider); const meter = metrics.getMeter("m");