diff --git a/ext/telemetry/lib.rs b/ext/telemetry/lib.rs index aac743e0fb..cccef09399 100644 --- a/ext/telemetry/lib.rs +++ b/ext/telemetry/lib.rs @@ -45,6 +45,7 @@ pub use opentelemetry::metrics::MeterProvider; pub use opentelemetry::metrics::UpDownCounter; use opentelemetry::otel_debug; use opentelemetry::otel_error; +use opentelemetry::trace::Event; use opentelemetry::trace::Link; use opentelemetry::trace::SpanContext; use opentelemetry::trace::SpanId; @@ -1381,6 +1382,32 @@ impl OtelSpan { Ok(()) } + #[fast] + fn add_event( + &self, + #[string] name: String, + start_time: f64, + #[smi] dropped_attributes_count: u32, + ) { + let start_time = if start_time.is_nan() { + SystemTime::now() + } else { + SystemTime::UNIX_EPOCH + .checked_add(Duration::from_secs_f64(start_time / 1000.0)) + .unwrap() + }; + let mut state = self.0.borrow_mut(); + let OtelSpanState::Recording(span) = &mut **state else { + return; + }; + span.events.events.push(Event::new( + name, + start_time, + vec![], + dropped_attributes_count, + )); + } + #[fast] fn drop_event(&self) { let mut state = self.0.borrow_mut(); diff --git a/ext/telemetry/telemetry.ts b/ext/telemetry/telemetry.ts index 119e089cfd..703e659fb9 100644 --- a/ext/telemetry/telemetry.ts +++ b/ext/telemetry/telemetry.ts @@ -169,6 +169,25 @@ function hrToMs(hr: [number, number]): number { return (hr[0] * 1e3 + hr[1] / 1e6); } +function isTimeInput(input: unknown): input is TimeInput { + return typeof input === "number" || + (input && (ArrayIsArray(input) || isDate(input))); +} + +function timeInputToMs(input?: TimeInput): number | undefined { + if (input === undefined) return; + if (ArrayIsArray(input)) { + return hrToMs(input); + } else if (isDate(input)) { + return DatePrototypeGetTime(input); + } + return input; +} + +function countAttributes(attributes?: Attributes): number { + return attributes ? ObjectKeys(attributes).length : 0; +} + interface AsyncContextSnapshot { __brand: "AsyncContextSnapshot"; } @@ -183,7 +202,7 @@ export const currentSnapshot = getAsyncContext; export const restoreSnapshot = setAsyncContext; function isDate(value: unknown): value is Date { - return ObjectPrototypeIsPrototypeOf(value, DatePrototype); + return ObjectPrototypeIsPrototypeOf(DatePrototype, value); } interface OtelTracer { @@ -215,6 +234,11 @@ interface OtelSpan { spanContext(): SpanContext; setStatus(status: SpanStatusCode, errorDescription: string): void; + addEvent( + name: string, + startTime: number, + droppedAttributeCount: number, + ): void; dropEvent(): void; end(endTime: number): void; } @@ -303,20 +327,13 @@ class Tracer { context = context ?? CURRENT.get(); } - let startTime = options?.startTime; - if (startTime && ArrayIsArray(startTime)) { - startTime = hrToMs(startTime); - } else if (startTime && isDate(startTime)) { - startTime = DatePrototypeGetTime(startTime); - } + const startTime = timeInputToMs(options?.startTime); const parentSpan = context?.getValue(SPAN_KEY) as | Span | { spanContext(): SpanContext } | undefined; - const attributesCount = options?.attributes - ? ObjectKeys(options.attributes).length - : 0; + const attributesCount = countAttributes(options?.attributes); const parentOtelSpan: OtelSpan | null | undefined = parentSpan !== undefined ? getOtelSpan(parentSpan) ?? undefined : undefined; @@ -380,17 +397,27 @@ class Span { } addEvent( - _name: string, - _attributesOrStartTime?: Attributes | TimeInput, - _startTime?: TimeInput, + name: string, + attributesOrStartTime?: Attributes | TimeInput, + startTime?: TimeInput, ): this { - this.#otelSpan?.dropEvent(); + if (isTimeInput(attributesOrStartTime)) { + startTime = attributesOrStartTime; + attributesOrStartTime = undefined; + } + const startTimeMs = timeInputToMs(startTime); + + this.#otelSpan?.addEvent( + name, + startTimeMs ?? NaN, + countAttributes(attributesOrStartTime), + ); return this; } addLink(link: Link): this { const droppedAttributeCount = (link.droppedAttributesCount ?? 0) + - (link.attributes ? ObjectKeys(link.attributes).length : 0); + countAttributes(link.attributes); const valid = op_otel_span_add_link( this.#otelSpan, link.context.traceId, @@ -411,12 +438,7 @@ class Span { } end(endTime?: TimeInput): void { - if (endTime && ArrayIsArray(endTime)) { - endTime = hrToMs(endTime); - } else if (endTime && isDate(endTime)) { - endTime = DatePrototypeGetTime(endTime); - } - this.#otelSpan?.end(endTime || NaN); + this.#otelSpan?.end(timeInputToMs(endTime) || NaN); } isRecording(): boolean { diff --git a/tests/specs/cli/otel_basic/__test__.jsonc b/tests/specs/cli/otel_basic/__test__.jsonc index e54f663993..401c9a669e 100644 --- a/tests/specs/cli/otel_basic/__test__.jsonc +++ b/tests/specs/cli/otel_basic/__test__.jsonc @@ -56,6 +56,10 @@ "node_http_request": { "args": "run -A main.ts node_http_request.ts", "output": "node_http_request.out" + }, + "events": { + "args": "run -A main.ts events.ts", + "output": "events.out" } } } diff --git a/tests/specs/cli/otel_basic/events.out b/tests/specs/cli/otel_basic/events.out new file mode 100644 index 0000000000..644ae9ab1a --- /dev/null +++ b/tests/specs/cli/otel_basic/events.out @@ -0,0 +1,90 @@ +{ + "spans": [ + { + "traceId": "00000000000000000000000000000001", + "spanId": "0000000000000001", + "traceState": "", + "parentSpanId": "", + "flags": 1, + "name": "example span", + "kind": 1, + "startTimeUnixNano": "[WILDCARD]", + "endTimeUnixNano": "[WILDCARD]", + "attributes": [], + "droppedAttributesCount": 0, + "events": [ + { + "timeUnixNano": "[WILDCARD]", + "name": "example event", + "attributes": [], + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "links": [], + "droppedLinksCount": 0, + "status": { + "message": "", + "code": 0 + } + }, + { + "traceId": "00000000000000000000000000000002", + "spanId": "0000000000000002", + "traceState": "", + "parentSpanId": "", + "flags": 1, + "name": "example span", + "kind": 1, + "startTimeUnixNano": "[WILDCARD]", + "endTimeUnixNano": "[WILDCARD]", + "attributes": [], + "droppedAttributesCount": 0, + "events": [ + { + "timeUnixNano": "[WILDCARD]", + "name": "example event", + "attributes": [], + "droppedAttributesCount": 1 + } + ], + "droppedEventsCount": 0, + "links": [], + "droppedLinksCount": 0, + "status": { + "message": "", + "code": 0 + } + }, + { + "traceId": "00000000000000000000000000000003", + "spanId": "0000000000000003", + "traceState": "", + "parentSpanId": "", + "flags": 1, + "name": "example span", + "kind": 1, + "startTimeUnixNano": "[WILDCARD]", + "endTimeUnixNano": "[WILDCARD]", + "attributes": [], + "droppedAttributesCount": 0, + "events": [ + { + "timeUnixNano": "[WILDCARD]", + "name": "example event", + "attributes": [], + "droppedAttributesCount": 1 + } + ], + "droppedEventsCount": 0, + "links": [], + "droppedLinksCount": 0, + "status": { + "message": "", + "code": 0 + } + } + ], + "logs": [], + "metrics": [] +} diff --git a/tests/specs/cli/otel_basic/events.ts b/tests/specs/cli/otel_basic/events.ts new file mode 100644 index 0000000000..8e467284cb --- /dev/null +++ b/tests/specs/cli/otel_basic/events.ts @@ -0,0 +1,21 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +import { trace } from "npm:@opentelemetry/api@1.9.0"; + +const tracer = trace.getTracer("example-tracer"); + +const span1 = tracer.startSpan("example span"); +span1.addEvent("example event"); +span1.end(); + +const span2 = tracer.startSpan("example span"); +span2.addEvent("example event", { + key: "value", +}); +span2.end(); + +const span3 = tracer.startSpan("example span"); +span3.addEvent("example event", { + key: "value", +}, new Date()); +span3.end();