feat(runtime): remove public OTEL trace API (#26854)

This PR removes the public Deno.tracing.Span API.
We are not confident we can ship an API that is
better than the `@opentelemetry/api` API, because
V8 CPED does not support us using `using` to
manage span context. If this changes, we can
revisit this decision. For now, users wanting
custom spans can instrument their code using
the `@opentelemetry/api` API and `@deno/otel`.

This PR also speeds up the OTEL trace generation
by a 30% by using Uint8Array instead of
strings for the trace ID and span ID.
This commit is contained in:
Luca Casonato 2024-11-19 00:55:22 +01:00 committed by GitHub
parent 106d47a013
commit 594a99817c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1079 additions and 604 deletions

View File

@ -1252,80 +1252,53 @@ declare namespace Deno {
}
/**
* **UNSTABLE**: New API, yet to be vetted.
*
* APIs for working with the OpenTelemetry observability framework. Deno can
* export traces, metrics, and logs to OpenTelemetry compatible backends via
* the OTLP protocol.
*
* Deno automatically instruments the runtime with OpenTelemetry traces and
* metrics. This data is exported via OTLP to OpenTelemetry compatible
* backends. User logs from the `console` API are exported as OpenTelemetry
* logs via OTLP.
*
* User code can also create custom traces, metrics, and logs using the
* OpenTelemetry API. This is done using the official OpenTelemetry package
* for JavaScript:
* [`npm:@opentelemetry/api`](https://opentelemetry.io/docs/languages/js/).
* Deno integrates with this package to provide trace context propagation
* between native Deno APIs (like `Deno.serve` or `fetch`) and custom user
* code. Deno also provides APIs that allow exporting custom telemetry data
* via the same OTLP channel used by the Deno runtime. This is done using the
* [`jsr:@deno/otel`](https://jsr.io/@deno/otel) package.
*
* @example Using OpenTelemetry API to create custom traces
* ```ts,ignore
* import { trace } from "npm:@opentelemetry/api@1";
* import "jsr:@deno/otel@0.0.2/register";
*
* const tracer = trace.getTracer("example-tracer");
*
* async function doWork() {
* return tracer.startActiveSpan("doWork", async (span) => {
* span.setAttribute("key", "value");
* await new Promise((resolve) => setTimeout(resolve, 1000));
* span.end();
* });
* }
*
* Deno.serve(async (req) => {
* await doWork();
* const resp = await fetch("https://example.com");
* return resp;
* });
* ```
*
* @category Telemetry
* @experimental
*/
export namespace tracing {
/**
* Whether tracing is enabled.
* @category Telemetry
* @experimental
*/
export const enabled: boolean;
/**
* Allowed attribute type.
* @category Telemetry
* @experimental
*/
export type AttributeValue = string | number | boolean | bigint;
/**
* A tracing span.
* @category Telemetry
* @experimental
*/
export class Span implements Disposable {
readonly traceId: string;
readonly spanId: string;
readonly parentSpanId: string;
readonly kind: string;
readonly name: string;
readonly startTime: number;
readonly endTime: number;
readonly status: null | { code: 1 } | { code: 2; message: string };
readonly attributes: Record<string, AttributeValue>;
readonly traceFlags: number;
/**
* Construct a new Span and enter it as the "current" span.
*/
constructor(
name: string,
kind?: "internal" | "server" | "client" | "producer" | "consumer",
);
/**
* Set an attribute on this span.
*/
setAttribute(
name: string,
value: AttributeValue,
): void;
/**
* Enter this span as the "current" span.
*/
enter(): void;
/**
* Exit this span as the "current" span and restore the previous one.
*/
exit(): void;
/**
* End this span, and exit it as the "current" span.
*/
end(): void;
[Symbol.dispose](): void;
/**
* Get the "current" span, if one exists.
*/
static current(): Span | undefined | null;
}
export namespace telemetry {
/**
* A SpanExporter compatible with OpenTelemetry.js
* https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_trace_base.SpanExporter.html
@ -1345,14 +1318,6 @@ declare namespace Deno {
export {}; // only export exports
}
/**
* @category Telemetry
* @experimental
*/
export namespace metrics {
export {}; // only export exports
}
export {}; // only export exports
}

View File

@ -9,4 +9,7 @@ declare module "ext:deno_console/01_console.js" {
keys: (keyof TObject)[];
evaluate: boolean;
}): Record<string, unknown>;
class Console {
}
}

View File

@ -29,7 +29,7 @@ import * as tty from "ext:runtime/40_tty.js";
import * as kv from "ext:deno_kv/01_db.ts";
import * as cron from "ext:deno_cron/01_cron.ts";
import * as webgpuSurface from "ext:deno_webgpu/02_surface.js";
import * as telemetry from "ext:runtime/telemetry.js";
import * as telemetry from "ext:runtime/telemetry.ts";
const denoNs = {
Process: process.Process,
@ -185,8 +185,7 @@ denoNsUnstableById[unstableIds.webgpu] = {
// denoNsUnstableById[unstableIds.workerOptions] = { __proto__: null }
denoNsUnstableById[unstableIds.otel] = {
tracing: telemetry.tracing,
metrics: telemetry.metrics,
telemetry: telemetry.telemetry,
};
export { denoNs, denoNsUnstableById, unstableIds };

View File

@ -86,7 +86,7 @@ import {
workerRuntimeGlobalProperties,
} from "ext:runtime/98_global_scope_worker.js";
import { SymbolDispose, SymbolMetadata } from "ext:deno_web/00_infra.js";
import { bootstrap as bootstrapOtel } from "ext:runtime/telemetry.js";
import { bootstrap as bootstrapOtel } from "ext:runtime/telemetry.ts";
// deno-lint-ignore prefer-primordials
if (Symbol.metadata) {

View File

@ -1,409 +0,0 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { core, primordials } from "ext:core/mod.js";
import {
op_otel_log,
op_otel_span_attribute,
op_otel_span_attribute2,
op_otel_span_attribute3,
op_otel_span_continue,
op_otel_span_flush,
op_otel_span_start,
} from "ext:core/ops";
import { Console } from "ext:deno_console/01_console.js";
import { performance } from "ext:deno_web/15_performance.js";
const {
SymbolDispose,
MathRandom,
Array,
ObjectEntries,
SafeMap,
ReflectApply,
SymbolFor,
Error,
NumberPrototypeToString,
StringPrototypePadStart,
} = primordials;
const { AsyncVariable, setAsyncContext } = core;
const CURRENT = new AsyncVariable();
let TRACING_ENABLED = false;
let DETERMINISTIC = false;
const SPAN_ID_BYTES = 8;
const TRACE_ID_BYTES = 16;
const TRACE_FLAG_SAMPLED = 1 << 0;
const hexSliceLookupTable = (function () {
const alphabet = "0123456789abcdef";
const table = new Array(256);
for (let i = 0; i < 16; ++i) {
const i16 = i * 16;
for (let j = 0; j < 16; ++j) {
table[i16 + j] = alphabet[i] + alphabet[j];
}
}
return table;
})();
let counter = 1;
const INVALID_SPAN_ID = "0000000000000000";
const INVALID_TRACE_ID = "00000000000000000000000000000000";
function generateId(bytes) {
if (DETERMINISTIC) {
return StringPrototypePadStart(
NumberPrototypeToString(counter++, 16),
bytes * 2,
"0",
);
}
let out = "";
for (let i = 0; i < bytes / 4; i += 1) {
const r32 = (MathRandom() * 2 ** 32) >>> 0;
out += hexSliceLookupTable[(r32 >> 24) & 0xff];
out += hexSliceLookupTable[(r32 >> 16) & 0xff];
out += hexSliceLookupTable[(r32 >> 8) & 0xff];
out += hexSliceLookupTable[r32 & 0xff];
}
return out;
}
function submit(span) {
if (!(span.traceFlags & TRACE_FLAG_SAMPLED)) return;
op_otel_span_start(
span.traceId,
span.spanId,
span.parentSpanId ?? "",
span.kind,
span.name,
span.startTime,
span.endTime,
);
if (span.status !== null && span.status.code !== 0) {
op_otel_span_continue(span.code, span.message ?? "");
}
const attributes = ObjectEntries(span.attributes);
let i = 0;
while (i < attributes.length) {
if (i + 2 < attributes.length) {
op_otel_span_attribute3(
attributes.length,
attributes[i][0],
attributes[i][1],
attributes[i + 1][0],
attributes[i + 1][1],
attributes[i + 2][0],
attributes[i + 2][1],
);
i += 3;
} else if (i + 1 < attributes.length) {
op_otel_span_attribute2(
attributes.length,
attributes[i][0],
attributes[i][1],
attributes[i + 1][0],
attributes[i + 1][1],
);
i += 2;
} else {
op_otel_span_attribute(
attributes.length,
attributes[i][0],
attributes[i][1],
);
i += 1;
}
}
op_otel_span_flush();
}
const now = () => (performance.timeOrigin + performance.now()) / 1000;
const NO_ASYNC_CONTEXT = {};
class Span {
traceId;
spanId;
parentSpanId;
kind;
name;
startTime;
endTime;
status = null;
attributes = { __proto__: null };
traceFlags = TRACE_FLAG_SAMPLED;
enabled = TRACING_ENABLED;
#asyncContext = NO_ASYNC_CONTEXT;
constructor(name, kind = "internal") {
if (!this.enabled) {
this.traceId = INVALID_TRACE_ID;
this.spanId = INVALID_SPAN_ID;
this.parentSpanId = INVALID_SPAN_ID;
return;
}
this.startTime = now();
this.spanId = generateId(SPAN_ID_BYTES);
let traceId;
let parentSpanId;
const parent = Span.current();
if (parent) {
if (parent.spanId !== undefined) {
parentSpanId = parent.spanId;
traceId = parent.traceId;
} else {
const context = parent.spanContext();
parentSpanId = context.spanId;
traceId = context.traceId;
}
}
if (
traceId && traceId !== INVALID_TRACE_ID && parentSpanId &&
parentSpanId !== INVALID_SPAN_ID
) {
this.traceId = traceId;
this.parentSpanId = parentSpanId;
} else {
this.traceId = generateId(TRACE_ID_BYTES);
this.parentSpanId = INVALID_SPAN_ID;
}
this.name = name;
switch (kind) {
case "internal":
this.kind = 0;
break;
case "server":
this.kind = 1;
break;
case "client":
this.kind = 2;
break;
case "producer":
this.kind = 3;
break;
case "consumer":
this.kind = 4;
break;
default:
throw new Error(`Invalid span kind: ${kind}`);
}
this.enter();
}
// helper function to match otel js api
spanContext() {
return {
traceId: this.traceId,
spanId: this.spanId,
traceFlags: this.traceFlags,
};
}
setAttribute(name, value) {
if (!this.enabled) return;
this.attributes[name] = value;
}
enter() {
if (!this.enabled) return;
const context = (CURRENT.get() || ROOT_CONTEXT).setValue(SPAN_KEY, this);
this.#asyncContext = CURRENT.enter(context);
}
exit() {
if (!this.enabled || this.#asyncContext === NO_ASYNC_CONTEXT) return;
setAsyncContext(this.#asyncContext);
this.#asyncContext = NO_ASYNC_CONTEXT;
}
end() {
if (!this.enabled || this.endTime !== undefined) return;
this.exit();
this.endTime = now();
submit(this);
}
[SymbolDispose]() {
this.end();
}
static current() {
return CURRENT.get()?.getValue(SPAN_KEY);
}
}
function hrToSecs(hr) {
return ((hr[0] * 1e3 + hr[1] / 1e6) / 1000);
}
// Exporter compatible with opentelemetry js library
class SpanExporter {
export(spans, resultCallback) {
try {
for (let i = 0; i < spans.length; i += 1) {
const span = spans[i];
const context = span.spanContext();
submit({
spanId: context.spanId,
traceId: context.traceId,
traceFlags: context.traceFlags,
name: span.name,
kind: span.kind,
parentSpanId: span.parentSpanId,
startTime: hrToSecs(span.startTime),
endTime: hrToSecs(span.endTime),
status: span.status,
attributes: span.attributes,
});
}
resultCallback({ code: 0 });
} catch (error) {
resultCallback({ code: 1, error });
}
}
async shutdown() {}
async forceFlush() {}
}
// SPAN_KEY matches symbol in otel-js library
const SPAN_KEY = SymbolFor("OpenTelemetry Context Key SPAN");
// Context tracker compatible with otel-js api
class Context {
#data = new SafeMap();
constructor(data) {
this.#data = data ? new SafeMap(data) : new SafeMap();
}
getValue(key) {
return this.#data.get(key);
}
setValue(key, value) {
const c = new Context(this.#data);
c.#data.set(key, value);
return c;
}
deleteValue(key) {
const c = new Context(this.#data);
c.#data.delete(key);
return c;
}
}
const ROOT_CONTEXT = new Context();
// Context manager for opentelemetry js library
class ContextManager {
active() {
return CURRENT.get() ?? ROOT_CONTEXT;
}
with(context, fn, thisArg, ...args) {
const ctx = CURRENT.enter(context);
try {
return ReflectApply(fn, thisArg, args);
} finally {
setAsyncContext(ctx);
}
}
bind(context, f) {
return (...args) => {
const ctx = CURRENT.enter(context);
try {
return ReflectApply(f, thisArg, args);
} finally {
setAsyncContext(ctx);
}
};
}
enable() {
return this;
}
disable() {
return this;
}
}
function otelLog(message, level) {
let traceId = "";
let spanId = "";
let traceFlags = 0;
const span = Span.current();
if (span) {
if (span.spanId !== undefined) {
spanId = span.spanId;
traceId = span.traceId;
traceFlags = span.traceFlags;
} else {
const context = span.spanContext();
spanId = context.spanId;
traceId = context.traceId;
traceFlags = context.traceFlags;
}
}
return op_otel_log(message, level, traceId, spanId, traceFlags);
}
const otelConsoleConfig = {
ignore: 0,
capture: 1,
replace: 2,
};
export function bootstrap(config) {
if (config.length === 0) return;
const { 0: consoleConfig, 1: deterministic } = config;
TRACING_ENABLED = true;
DETERMINISTIC = deterministic === 1;
switch (consoleConfig) {
case otelConsoleConfig.capture:
core.wrapConsole(globalThis.console, new Console(otelLog));
break;
case otelConsoleConfig.replace:
ObjectDefineProperty(
globalThis,
"console",
core.propNonEnumerable(new Console(otelLog)),
);
break;
default:
break;
}
}
export const tracing = {
get enabled() {
return TRACING_ENABLED;
},
Span,
SpanExporter,
ContextManager,
};
// TODO(devsnek): implement metrics
export const metrics = {};

720
runtime/js/telemetry.ts Normal file
View File

@ -0,0 +1,720 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { core, primordials } from "ext:core/mod.js";
import {
op_crypto_get_random_values,
op_otel_instrumentation_scope_create_and_enter,
op_otel_instrumentation_scope_enter,
op_otel_instrumentation_scope_enter_builtin,
op_otel_log,
op_otel_span_attribute,
op_otel_span_attribute2,
op_otel_span_attribute3,
op_otel_span_continue,
op_otel_span_flush,
op_otel_span_set_dropped,
op_otel_span_start,
} from "ext:core/ops";
import { Console } from "ext:deno_console/01_console.js";
import { performance } from "ext:deno_web/15_performance.js";
const {
SafeWeakMap,
Array,
ObjectEntries,
SafeMap,
ReflectApply,
SymbolFor,
Error,
Uint8Array,
TypedArrayPrototypeSubarray,
ObjectAssign,
ObjectDefineProperty,
WeakRefPrototypeDeref,
String,
ObjectPrototypeIsPrototypeOf,
DataView,
DataViewPrototypeSetUint32,
SafeWeakRef,
TypedArrayPrototypeGetBuffer,
} = primordials;
const { AsyncVariable, setAsyncContext } = core;
let TRACING_ENABLED = false;
let DETERMINISTIC = false;
enum SpanKind {
INTERNAL = 0,
SERVER = 1,
CLIENT = 2,
PRODUCER = 3,
CONSUMER = 4,
}
interface TraceState {
set(key: string, value: string): TraceState;
unset(key: string): TraceState;
get(key: string): string | undefined;
serialize(): string;
}
interface SpanContext {
traceId: string;
spanId: string;
isRemote?: boolean;
traceFlags: number;
traceState?: TraceState;
}
type HrTime = [number, number];
enum SpanStatusCode {
UNSET = 0,
OK = 1,
ERROR = 2,
}
interface SpanStatus {
code: SpanStatusCode;
message?: string;
}
export type AttributeValue =
| string
| number
| boolean
| Array<null | undefined | string>
| Array<null | undefined | number>
| Array<null | undefined | boolean>;
interface Attributes {
[attributeKey: string]: AttributeValue | undefined;
}
type SpanAttributes = Attributes;
interface Link {
context: SpanContext;
attributes?: SpanAttributes;
droppedAttributesCount?: number;
}
interface TimedEvent {
time: HrTime;
name: string;
attributes?: SpanAttributes;
droppedAttributesCount?: number;
}
interface IArrayValue {
values: IAnyValue[];
}
interface IAnyValue {
stringValue?: string | null;
boolValue?: boolean | null;
intValue?: number | null;
doubleValue?: number | null;
arrayValue?: IArrayValue;
kvlistValue?: IKeyValueList;
bytesValue?: Uint8Array;
}
interface IKeyValueList {
values: IKeyValue[];
}
interface IKeyValue {
key: string;
value: IAnyValue;
}
interface IResource {
attributes: IKeyValue[];
droppedAttributesCount: number;
}
interface InstrumentationLibrary {
readonly name: string;
readonly version?: string;
readonly schemaUrl?: string;
}
interface ReadableSpan {
readonly name: string;
readonly kind: SpanKind;
readonly spanContext: () => SpanContext;
readonly parentSpanId?: string;
readonly startTime: HrTime;
readonly endTime: HrTime;
readonly status: SpanStatus;
readonly attributes: SpanAttributes;
readonly links: Link[];
readonly events: TimedEvent[];
readonly duration: HrTime;
readonly ended: boolean;
readonly resource: IResource;
readonly instrumentationLibrary: InstrumentationLibrary;
readonly droppedAttributesCount: number;
readonly droppedEventsCount: number;
readonly droppedLinksCount: number;
}
enum ExportResultCode {
SUCCESS = 0,
FAILED = 1,
}
interface ExportResult {
code: ExportResultCode;
error?: Error;
}
function hrToSecs(hr: [number, number]): number {
return ((hr[0] * 1e3 + hr[1] / 1e6) / 1000);
}
const TRACE_FLAG_SAMPLED = 1 << 0;
const instrumentationScopes = new SafeWeakMap<
InstrumentationLibrary,
{ __key: "instrumentation-library" }
>();
let activeInstrumentationLibrary: WeakRef<InstrumentationLibrary> | null = null;
function submit(
spanId: string | Uint8Array,
traceId: string | Uint8Array,
traceFlags: number,
parentSpanId: string | Uint8Array | null,
span: Omit<
ReadableSpan,
| "spanContext"
| "startTime"
| "endTime"
| "parentSpanId"
| "duration"
| "ended"
| "resource"
>,
startTime: number,
endTime: number,
) {
if (!(traceFlags & TRACE_FLAG_SAMPLED)) return;
// TODO(@lucacasonato): `resource` is ignored for now, should we implement it?
const instrumentationLibrary = span.instrumentationLibrary;
if (
!activeInstrumentationLibrary ||
WeakRefPrototypeDeref(activeInstrumentationLibrary) !==
instrumentationLibrary
) {
activeInstrumentationLibrary = new SafeWeakRef(instrumentationLibrary);
if (instrumentationLibrary === BUILTIN_INSTRUMENTATION_LIBRARY) {
op_otel_instrumentation_scope_enter_builtin();
} else {
let instrumentationScope = instrumentationScopes
.get(instrumentationLibrary);
if (instrumentationScope === undefined) {
instrumentationScope = op_otel_instrumentation_scope_create_and_enter(
instrumentationLibrary.name,
instrumentationLibrary.version,
instrumentationLibrary.schemaUrl,
) as { __key: "instrumentation-library" };
instrumentationScopes.set(
instrumentationLibrary,
instrumentationScope,
);
} else {
op_otel_instrumentation_scope_enter(
instrumentationScope,
);
}
}
}
op_otel_span_start(
traceId,
spanId,
parentSpanId,
span.kind,
span.name,
startTime,
endTime,
);
const status = span.status;
if (status !== null && status.code !== 0) {
op_otel_span_continue(status.code, status.message ?? "");
}
const attributeKvs = ObjectEntries(span.attributes);
let i = 0;
while (i < attributeKvs.length) {
if (i + 2 < attributeKvs.length) {
op_otel_span_attribute3(
attributeKvs.length,
attributeKvs[i][0],
attributeKvs[i][1],
attributeKvs[i + 1][0],
attributeKvs[i + 1][1],
attributeKvs[i + 2][0],
attributeKvs[i + 2][1],
);
i += 3;
} else if (i + 1 < attributeKvs.length) {
op_otel_span_attribute2(
attributeKvs.length,
attributeKvs[i][0],
attributeKvs[i][1],
attributeKvs[i + 1][0],
attributeKvs[i + 1][1],
);
i += 2;
} else {
op_otel_span_attribute(
attributeKvs.length,
attributeKvs[i][0],
attributeKvs[i][1],
);
i += 1;
}
}
// TODO(@lucacasonato): implement links
// TODO(@lucacasonato): implement events
const droppedAttributesCount = span.droppedAttributesCount;
const droppedLinksCount = span.droppedLinksCount + span.links.length;
const droppedEventsCount = span.droppedEventsCount + span.events.length;
if (
droppedAttributesCount > 0 || droppedLinksCount > 0 ||
droppedEventsCount > 0
) {
op_otel_span_set_dropped(
droppedAttributesCount,
droppedLinksCount,
droppedEventsCount,
);
}
op_otel_span_flush();
}
const now = () => (performance.timeOrigin + performance.now()) / 1000;
const SPAN_ID_BYTES = 8;
const TRACE_ID_BYTES = 16;
const INVALID_TRACE_ID = new Uint8Array(TRACE_ID_BYTES);
const INVALID_SPAN_ID = new Uint8Array(SPAN_ID_BYTES);
const NO_ASYNC_CONTEXT = {};
let otelLog: (message: string, level: number) => void;
const hexSliceLookupTable = (function () {
const alphabet = "0123456789abcdef";
const table = new Array(256);
for (let i = 0; i < 16; ++i) {
const i16 = i * 16;
for (let j = 0; j < 16; ++j) {
table[i16 + j] = alphabet[i] + alphabet[j];
}
}
return table;
})();
function bytesToHex(bytes: Uint8Array): string {
let out = "";
for (let i = 0; i < bytes.length; i += 1) {
out += hexSliceLookupTable[bytes[i]];
}
return out;
}
const SPAN_KEY = SymbolFor("OpenTelemetry Context Key SPAN");
const BUILTIN_INSTRUMENTATION_LIBRARY: InstrumentationLibrary = {} as never;
let COUNTER = 1;
export let enterSpan: (span: Span) => void;
export let exitSpan: (span: Span) => void;
export let endSpan: (span: Span) => void;
export class Span {
#traceId: string | Uint8Array;
#spanId: Uint8Array;
#traceFlags = TRACE_FLAG_SAMPLED;
#spanContext: SpanContext | null = null;
#parentSpanId: string | Uint8Array | null = null;
#parentSpanIdString: string | null = null;
#recording = TRACING_ENABLED;
#kind: number = 0;
#name: string;
#startTime: number;
#status: { code: number; message?: string } | null = null;
#attributes: Attributes = { __proto__: null } as never;
#droppedEventsCount = 0;
#droppedLinksCount = 0;
#asyncContext = NO_ASYNC_CONTEXT;
static {
otelLog = function otelLog(message, level) {
let traceId = null;
let spanId = null;
let traceFlags = 0;
const span = CURRENT.get()?.getValue(SPAN_KEY);
if (span) {
// The lint is wrong, we can not use anything but `in` here because this
// is a private field.
// deno-lint-ignore prefer-primordials
if (#traceId in span) {
traceId = span.#traceId;
spanId = span.#spanId;
traceFlags = span.#traceFlags;
} else {
const context = span.spanContext();
traceId = context.traceId;
spanId = context.spanId;
traceFlags = context.traceFlags;
}
}
return op_otel_log(message, level, traceId, spanId, traceFlags);
};
enterSpan = (span: Span) => {
if (!span.#recording) return;
const context = (CURRENT.get() || ROOT_CONTEXT).setValue(SPAN_KEY, span);
span.#asyncContext = CURRENT.enter(context);
};
exitSpan = (span: Span) => {
if (!span.#recording) return;
if (span.#asyncContext === NO_ASYNC_CONTEXT) return;
setAsyncContext(span.#asyncContext);
span.#asyncContext = NO_ASYNC_CONTEXT;
};
exitSpan = (span: Span) => {
const endTime = now();
submit(
span.#spanId,
span.#traceId,
span.#traceFlags,
span.#parentSpanId,
{
name: span.#name,
kind: span.#kind,
status: span.#status ?? { code: 0 },
attributes: span.#attributes,
events: [],
links: [],
droppedAttributesCount: 0,
droppedEventsCount: span.#droppedEventsCount,
droppedLinksCount: span.#droppedLinksCount,
instrumentationLibrary: BUILTIN_INSTRUMENTATION_LIBRARY,
},
span.#startTime,
endTime,
);
};
}
constructor(
name: string,
attributes?: Attributes,
) {
if (!this.isRecording) {
this.#name = "";
this.#startTime = 0;
this.#traceId = INVALID_TRACE_ID;
this.#spanId = INVALID_SPAN_ID;
this.#traceFlags = 0;
return;
}
this.#name = name;
this.#startTime = now();
this.#attributes = attributes ?? { __proto__: null } as never;
const currentSpan: Span | {
spanContext(): { traceId: string; spanId: string };
} = CURRENT.get()?.getValue(SPAN_KEY);
if (!currentSpan) {
const buffer = new Uint8Array(TRACE_ID_BYTES + SPAN_ID_BYTES);
if (DETERMINISTIC) {
DataViewPrototypeSetUint32(
new DataView(TypedArrayPrototypeGetBuffer(buffer)),
TRACE_ID_BYTES - 4,
COUNTER,
true,
);
COUNTER += 1;
DataViewPrototypeSetUint32(
new DataView(TypedArrayPrototypeGetBuffer(buffer)),
TRACE_ID_BYTES + SPAN_ID_BYTES - 4,
COUNTER,
true,
);
COUNTER += 1;
} else {
op_crypto_get_random_values(buffer);
}
this.#traceId = TypedArrayPrototypeSubarray(buffer, 0, TRACE_ID_BYTES);
this.#spanId = TypedArrayPrototypeSubarray(buffer, TRACE_ID_BYTES);
} else {
this.#spanId = new Uint8Array(SPAN_ID_BYTES);
if (DETERMINISTIC) {
DataViewPrototypeSetUint32(
new DataView(TypedArrayPrototypeGetBuffer(this.#spanId)),
SPAN_ID_BYTES - 4,
COUNTER,
true,
);
COUNTER += 1;
} else {
op_crypto_get_random_values(this.#spanId);
}
// deno-lint-ignore prefer-primordials
if (#traceId in currentSpan) {
this.#traceId = currentSpan.#traceId;
this.#parentSpanId = currentSpan.#spanId;
} else {
const context = currentSpan.spanContext();
this.#traceId = context.traceId;
this.#parentSpanId = context.spanId;
}
}
}
spanContext() {
if (!this.#spanContext) {
this.#spanContext = {
traceId: typeof this.#traceId === "string"
? this.#traceId
: bytesToHex(this.#traceId),
spanId: typeof this.#spanId === "string"
? this.#spanId
: bytesToHex(this.#spanId),
traceFlags: this.#traceFlags,
};
}
return this.#spanContext;
}
get parentSpanId() {
if (!this.#parentSpanIdString && this.#parentSpanId) {
if (typeof this.#parentSpanId === "string") {
this.#parentSpanIdString = this.#parentSpanId;
} else {
this.#parentSpanIdString = bytesToHex(this.#parentSpanId);
}
}
return this.#parentSpanIdString;
}
setAttribute(name: string, value: AttributeValue) {
if (this.#recording) this.#attributes[name] = value;
return this;
}
setAttributes(attributes: Attributes) {
if (this.#recording) ObjectAssign(this.#attributes, attributes);
return this;
}
setStatus(status: { code: number; message?: string }) {
if (this.#recording) {
if (status.code === 0) {
this.#status = null;
} else if (status.code > 2) {
throw new Error("Invalid status code");
} else {
this.#status = status;
}
}
return this;
}
updateName(name: string) {
if (this.#recording) this.#name = name;
return this;
}
addEvent(_name: never) {
// TODO(@lucacasonato): implement events
if (this.#recording) this.#droppedEventsCount += 1;
return this;
}
addLink(_link: never) {
// TODO(@lucacasonato): implement links
if (this.#recording) this.#droppedLinksCount += 1;
return this;
}
addLinks(links: never[]) {
// TODO(@lucacasonato): implement links
if (this.#recording) this.#droppedLinksCount += links.length;
return this;
}
isRecording() {
return this.#recording;
}
}
// Exporter compatible with opentelemetry js library
class SpanExporter {
export(
spans: ReadableSpan[],
resultCallback: (result: ExportResult) => void,
) {
try {
for (let i = 0; i < spans.length; i += 1) {
const span = spans[i];
const context = span.spanContext();
submit(
context.spanId,
context.traceId,
context.traceFlags,
span.parentSpanId ?? null,
span,
hrToSecs(span.startTime),
hrToSecs(span.endTime),
);
}
resultCallback({ code: 0 });
} catch (error) {
resultCallback({
code: 1,
error: ObjectPrototypeIsPrototypeOf(error, Error)
? error as Error
: new Error(String(error)),
});
}
}
async shutdown() {}
async forceFlush() {}
}
const CURRENT = new AsyncVariable();
class Context {
#data = new SafeMap();
// deno-lint-ignore no-explicit-any
constructor(data?: Iterable<readonly [any, any]> | null | undefined) {
this.#data = data ? new SafeMap(data) : new SafeMap();
}
getValue(key: symbol): unknown {
return this.#data.get(key);
}
setValue(key: symbol, value: unknown): Context {
const c = new Context(this.#data);
c.#data.set(key, value);
return c;
}
deleteValue(key: symbol): Context {
const c = new Context(this.#data);
c.#data.delete(key);
return c;
}
}
// TODO(lucacasonato): @opentelemetry/api defines it's own ROOT_CONTEXT
const ROOT_CONTEXT = new Context();
// Context manager for opentelemetry js library
class ContextManager {
active(): Context {
return CURRENT.get() ?? ROOT_CONTEXT;
}
with<A extends unknown[], F extends (...args: A) => ReturnType<F>>(
context: Context,
fn: F,
thisArg?: ThisParameterType<F>,
...args: A
): ReturnType<F> {
const ctx = CURRENT.enter(context);
try {
return ReflectApply(fn, thisArg, args);
} finally {
setAsyncContext(ctx);
}
}
// deno-lint-ignore no-explicit-any
bind<T extends (...args: any[]) => any>(
context: Context,
target: T,
): T {
return ((...args) => {
const ctx = CURRENT.enter(context);
try {
return ReflectApply(target, this, args);
} finally {
setAsyncContext(ctx);
}
}) as T;
}
enable() {
return this;
}
disable() {
return this;
}
}
const otelConsoleConfig = {
ignore: 0,
capture: 1,
replace: 2,
};
export function bootstrap(
config: [] | [
typeof otelConsoleConfig[keyof typeof otelConsoleConfig],
number,
],
): void {
if (config.length === 0) return;
const { 0: consoleConfig, 1: deterministic } = config;
TRACING_ENABLED = true;
DETERMINISTIC = deterministic === 1;
switch (consoleConfig) {
case otelConsoleConfig.capture:
core.wrapConsole(globalThis.console, new Console(otelLog));
break;
case otelConsoleConfig.replace:
ObjectDefineProperty(
globalThis,
"console",
core.propNonEnumerable(new Console(otelLog)),
);
break;
default:
break;
}
}
export const telemetry = { SpanExporter, ContextManager };

View File

@ -1,8 +1,8 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::tokio_util::create_basic_runtime;
use deno_core::anyhow;
use deno_core::anyhow::anyhow;
use deno_core::anyhow::{self};
use deno_core::futures::channel::mpsc;
use deno_core::futures::channel::mpsc::UnboundedSender;
use deno_core::futures::future::BoxFuture;
@ -23,7 +23,6 @@ use opentelemetry::trace::SpanKind;
use opentelemetry::trace::Status as SpanStatus;
use opentelemetry::trace::TraceFlags;
use opentelemetry::trace::TraceId;
use opentelemetry::InstrumentationScope;
use opentelemetry::Key;
use opentelemetry::KeyValue;
use opentelemetry::StringValue;
@ -63,11 +62,15 @@ deno_core::extension!(
deno_otel,
ops = [
op_otel_log,
op_otel_instrumentation_scope_create_and_enter,
op_otel_instrumentation_scope_enter,
op_otel_instrumentation_scope_enter_builtin,
op_otel_span_start,
op_otel_span_continue,
op_otel_span_attribute,
op_otel_span_attribute2,
op_otel_span_attribute3,
op_otel_span_set_dropped,
op_otel_span_flush,
],
);
@ -303,6 +306,10 @@ mod hyper_client {
static OTEL_PROCESSORS: OnceCell<(SpanProcessor, LogProcessor)> =
OnceCell::new();
static BUILT_IN_INSTRUMENTATION_SCOPE: OnceCell<
opentelemetry::InstrumentationScope,
> = OnceCell::new();
pub fn init(config: OtelConfig) -> anyhow::Result<()> {
// Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_*
// crates don't do this automatically.
@ -390,6 +397,14 @@ pub fn init(config: OtelConfig) -> anyhow::Result<()> {
.set((span_processor, log_processor))
.map_err(|_| anyhow!("failed to init otel"))?;
let builtin_instrumentation_scope =
opentelemetry::InstrumentationScope::builder("deno")
.with_version(config.runtime_version.clone())
.build();
BUILT_IN_INSTRUMENTATION_SCOPE
.set(builtin_instrumentation_scope)
.map_err(|_| anyhow!("failed to init otel"))?;
Ok(())
}
@ -458,16 +473,160 @@ pub fn handle_log(record: &log::Record) {
log_processor.emit(
&mut log_record,
&InstrumentationScope::builder("deno").build(),
BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap(),
);
}
fn parse_trace_id(
scope: &mut v8::HandleScope<'_>,
trace_id: v8::Local<'_, v8::Value>,
) -> TraceId {
if let Ok(string) = trace_id.try_cast() {
let value_view = v8::ValueView::new(scope, string);
match value_view.data() {
v8::ValueViewData::OneByte(bytes) => {
TraceId::from_hex(&String::from_utf8_lossy(bytes))
.unwrap_or(TraceId::INVALID)
}
_ => TraceId::INVALID,
}
} else if let Ok(uint8array) = trace_id.try_cast::<v8::Uint8Array>() {
let data = uint8array.data();
let byte_length = uint8array.byte_length();
if byte_length != 16 {
return TraceId::INVALID;
}
// SAFETY: We have ensured that the byte length is 16, so it is safe to
// cast the data to an array of 16 bytes.
let bytes = unsafe { &*(data as *const u8 as *const [u8; 16]) };
TraceId::from_bytes(*bytes)
} else {
TraceId::INVALID
}
}
fn parse_span_id(
scope: &mut v8::HandleScope<'_>,
span_id: v8::Local<'_, v8::Value>,
) -> SpanId {
if let Ok(string) = span_id.try_cast() {
let value_view = v8::ValueView::new(scope, string);
match value_view.data() {
v8::ValueViewData::OneByte(bytes) => {
SpanId::from_hex(&String::from_utf8_lossy(bytes))
.unwrap_or(SpanId::INVALID)
}
_ => SpanId::INVALID,
}
} else if let Ok(uint8array) = span_id.try_cast::<v8::Uint8Array>() {
let data = uint8array.data();
let byte_length = uint8array.byte_length();
if byte_length != 8 {
return SpanId::INVALID;
}
// SAFETY: We have ensured that the byte length is 8, so it is safe to
// cast the data to an array of 8 bytes.
let bytes = unsafe { &*(data as *const u8 as *const [u8; 8]) };
SpanId::from_bytes(*bytes)
} else {
SpanId::INVALID
}
}
macro_rules! attr {
($scope:ident, $attributes:expr $(=> $dropped_attributes_count:expr)?, $name:expr, $value:expr) => {
let name = if let Ok(name) = $name.try_cast() {
let view = v8::ValueView::new($scope, name);
match view.data() {
v8::ValueViewData::OneByte(bytes) => {
Some(String::from_utf8_lossy(bytes).into_owned())
}
v8::ValueViewData::TwoByte(bytes) => {
Some(String::from_utf16_lossy(bytes))
}
}
} else {
None
};
let value = if let Ok(string) = $value.try_cast::<v8::String>() {
Some(Value::String(StringValue::from({
let x = v8::ValueView::new($scope, string);
match x.data() {
v8::ValueViewData::OneByte(bytes) => {
String::from_utf8_lossy(bytes).into_owned()
}
v8::ValueViewData::TwoByte(bytes) => String::from_utf16_lossy(bytes),
}
})))
} else if let Ok(number) = $value.try_cast::<v8::Number>() {
Some(Value::F64(number.value()))
} else if let Ok(boolean) = $value.try_cast::<v8::Boolean>() {
Some(Value::Bool(boolean.is_true()))
} else if let Ok(bigint) = $value.try_cast::<v8::BigInt>() {
let (i64_value, _lossless) = bigint.i64_value();
Some(Value::I64(i64_value))
} else {
None
};
if let (Some(name), Some(value)) = (name, value) {
$attributes.push(KeyValue::new(name, value));
}
$(
else {
$dropped_attributes_count += 1;
}
)?
};
}
#[derive(Debug, Clone)]
struct InstrumentationScope(opentelemetry::InstrumentationScope);
impl deno_core::GarbageCollected for InstrumentationScope {}
#[op2]
#[cppgc]
fn op_otel_instrumentation_scope_create_and_enter(
state: &mut OpState,
#[string] name: String,
#[string] version: Option<String>,
#[string] schema_url: Option<String>,
) -> InstrumentationScope {
let mut builder = opentelemetry::InstrumentationScope::builder(name);
if let Some(version) = version {
builder = builder.with_version(version);
}
if let Some(schema_url) = schema_url {
builder = builder.with_schema_url(schema_url);
}
let scope = InstrumentationScope(builder.build());
state.put(scope.clone());
scope
}
#[op2(fast)]
fn op_otel_instrumentation_scope_enter(
state: &mut OpState,
#[cppgc] scope: &InstrumentationScope,
) {
state.put(scope.clone());
}
#[op2(fast)]
fn op_otel_instrumentation_scope_enter_builtin(state: &mut OpState) {
state.put(InstrumentationScope(
BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap().clone(),
));
}
#[op2(fast)]
fn op_otel_log(
scope: &mut v8::HandleScope<'_>,
#[string] message: String,
#[smi] level: i32,
#[string] trace_id: &str,
#[string] span_id: &str,
trace_id: v8::Local<'_, v8::Value>,
span_id: v8::Local<'_, v8::Value>,
#[smi] trace_flags: u8,
) {
let Some((_, log_processor)) = OTEL_PROCESSORS.get() else {
@ -483,15 +642,16 @@ fn op_otel_log(
3.. => Severity::Error,
};
let trace_id = parse_trace_id(scope, trace_id);
let span_id = parse_span_id(scope, span_id);
let mut log_record = LogRecord::default();
log_record.set_observed_timestamp(SystemTime::now());
log_record.set_body(message.into());
log_record.set_severity_number(severity);
log_record.set_severity_text(severity.name());
if let (Ok(trace_id), Ok(span_id)) =
(TraceId::from_hex(trace_id), SpanId::from_hex(span_id))
{
if trace_id != TraceId::INVALID && span_id != SpanId::INVALID {
log_record.set_trace_context(
trace_id,
span_id,
@ -501,7 +661,7 @@ fn op_otel_log(
log_processor.emit(
&mut log_record,
&InstrumentationScope::builder("deno").build(),
BUILT_IN_INSTRUMENTATION_SCOPE.get().unwrap(),
);
}
@ -527,40 +687,23 @@ fn op_otel_span_start<'s>(
span_processor.on_end(temporary_span.0);
};
let trace_id = {
let x = v8::ValueView::new(scope, trace_id.try_cast()?);
match x.data() {
v8::ValueViewData::OneByte(bytes) => {
TraceId::from_hex(&String::from_utf8_lossy(bytes))?
}
_ => return Err(anyhow!("invalid trace_id")),
}
let Some(InstrumentationScope(instrumentation_scope)) =
state.try_borrow::<InstrumentationScope>()
else {
return Err(anyhow!("instrumentation scope not available"));
};
let span_id = {
let x = v8::ValueView::new(scope, span_id.try_cast()?);
match x.data() {
v8::ValueViewData::OneByte(bytes) => {
SpanId::from_hex(&String::from_utf8_lossy(bytes))?
let trace_id = parse_trace_id(scope, trace_id);
if trace_id == TraceId::INVALID {
return Err(anyhow!("invalid trace_id"));
}
_ => return Err(anyhow!("invalid span_id")),
}
};
let parent_span_id = {
let x = v8::ValueView::new(scope, parent_span_id.try_cast()?);
match x.data() {
v8::ValueViewData::OneByte(bytes) => {
let s = String::from_utf8_lossy(bytes);
if s.is_empty() {
SpanId::INVALID
} else {
SpanId::from_hex(&s)?
let span_id = parse_span_id(scope, span_id);
if span_id == SpanId::INVALID {
return Err(anyhow!("invalid span_id"));
}
}
_ => return Err(anyhow!("invalid parent_span_id")),
}
};
let parent_span_id = parse_span_id(scope, parent_span_id);
let name = {
let x = v8::ValueView::new(scope, name.try_cast()?);
@ -601,7 +744,7 @@ fn op_otel_span_start<'s>(
events: Default::default(),
links: Default::default(),
status: SpanStatus::Unset,
instrumentation_scope: InstrumentationScope::builder("deno").build(),
instrumentation_scope: instrumentation_scope.clone(),
});
state.put(temporary_span);
@ -626,52 +769,6 @@ fn op_otel_span_continue(
}
}
macro_rules! attr {
($scope:ident, $temporary_span:ident, $name:ident, $value:ident) => {
let name = if let Ok(name) = $name.try_cast() {
let view = v8::ValueView::new($scope, name);
match view.data() {
v8::ValueViewData::OneByte(bytes) => {
Some(String::from_utf8_lossy(bytes).into_owned())
}
v8::ValueViewData::TwoByte(bytes) => {
Some(String::from_utf16_lossy(bytes))
}
}
} else {
None
};
let value = if let Ok(string) = $value.try_cast::<v8::String>() {
Some(Value::String(StringValue::from({
let x = v8::ValueView::new($scope, string);
match x.data() {
v8::ValueViewData::OneByte(bytes) => {
String::from_utf8_lossy(bytes).into_owned()
}
v8::ValueViewData::TwoByte(bytes) => String::from_utf16_lossy(bytes),
}
})))
} else if let Ok(number) = $value.try_cast::<v8::Number>() {
Some(Value::F64(number.value()))
} else if let Ok(boolean) = $value.try_cast::<v8::Boolean>() {
Some(Value::Bool(boolean.is_true()))
} else if let Ok(bigint) = $value.try_cast::<v8::BigInt>() {
let (i64_value, _lossless) = bigint.i64_value();
Some(Value::I64(i64_value))
} else {
None
};
if let (Some(name), Some(value)) = (name, value) {
$temporary_span
.0
.attributes
.push(KeyValue::new(name, value));
} else {
$temporary_span.0.dropped_attributes_count += 1;
}
};
}
#[op2(fast)]
fn op_otel_span_attribute<'s>(
scope: &mut v8::HandleScope<'s>,
@ -684,7 +781,7 @@ fn op_otel_span_attribute<'s>(
temporary_span.0.attributes.reserve_exact(
(capacity as usize) - temporary_span.0.attributes.capacity(),
);
attr!(scope, temporary_span, key, value);
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key, value);
}
}
@ -702,8 +799,8 @@ fn op_otel_span_attribute2<'s>(
temporary_span.0.attributes.reserve_exact(
(capacity as usize) - temporary_span.0.attributes.capacity(),
);
attr!(scope, temporary_span, key1, value1);
attr!(scope, temporary_span, key2, value2);
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key1, value1);
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key2, value2);
}
}
@ -724,9 +821,23 @@ fn op_otel_span_attribute3<'s>(
temporary_span.0.attributes.reserve_exact(
(capacity as usize) - temporary_span.0.attributes.capacity(),
);
attr!(scope, temporary_span, key1, value1);
attr!(scope, temporary_span, key2, value2);
attr!(scope, temporary_span, key3, value3);
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key1, value1);
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key2, value2);
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key3, value3);
}
}
#[op2(fast)]
fn op_otel_span_set_dropped(
state: &mut OpState,
#[smi] dropped_attributes_count: u32,
#[smi] dropped_links_count: u32,
#[smi] dropped_events_count: u32,
) {
if let Some(temporary_span) = state.try_borrow_mut::<TemporarySpan>() {
temporary_span.0.dropped_attributes_count = dropped_attributes_count;
temporary_span.0.links.dropped_count = dropped_links_count;
temporary_span.0.events.dropped_count = dropped_events_count;
}
}

View File

@ -47,7 +47,7 @@ extension!(runtime,
"40_signals.js",
"40_tty.js",
"41_prompt.js",
"telemetry.js",
"telemetry.ts",
"90_deno_ns.js",
"98_global_scope_shared.js",
"98_global_scope_window.js",

View File

@ -0,0 +1,14 @@
{
"name": "@deno/otel",
"version": "0.0.2",
"exports": {
".": "./src/index.ts",
"./register": "./src/register.ts"
},
"tasks": {
"check:license": "deno run -A tools/check_license.ts",
"check:docs": "deno doc --lint src/index.ts",
"check": "deno task check:license --check",
"ok": "deno fmt --check && deno lint && deno task check"
}
}

View File

@ -0,0 +1,38 @@
// 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";
// @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();
}

View File

@ -0,0 +1,5 @@
// Copyright 2024-2024 the Deno authors. All rights reserved. MIT license.
import { register } from "./index.ts";
register();

View File

@ -0,0 +1,6 @@
{
"exports": {
".": "./src/index.ts",
"./register": "./src/register.ts"
}
}

View File

@ -0,0 +1,8 @@
{
"scope": "deno",
"name": "otel",
"latest": "0.0.2",
"versions": {
"0.0.2": {}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
{
"spans": [
{
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000003",
"traceId": "10000000000000000000000000000002",
"spanId": "1000000000000003",
"traceState": "",
"parentSpanId": "0000000000000001",
"parentSpanId": "1000000000000001",
"flags": 1,
"name": "inner span",
"kind": 1,
@ -22,8 +22,8 @@
}
},
{
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000001",
"traceId": "10000000000000000000000000000002",
"spanId": "1000000000000001",
"traceState": "",
"parentSpanId": "",
"flags": 1,
@ -55,8 +55,8 @@
"attributes": [],
"droppedAttributesCount": 0,
"flags": 1,
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000003"
"traceId": "10000000000000000000000000000002",
"spanId": "1000000000000003"
},
{
"timeUnixNano": "0",
@ -69,8 +69,8 @@
"attributes": [],
"droppedAttributesCount": 0,
"flags": 1,
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000003"
"traceId": "10000000000000000000000000000002",
"spanId": "1000000000000003"
}
]
}

View File

@ -1,10 +1,17 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { trace } from "npm:@opentelemetry/api@1.9.0";
import "jsr:@deno/otel@0.0.2/register";
const tracer = trace.getTracer("example-tracer");
async function inner() {
using _span = new Deno.tracing.Span("inner span");
await tracer.startActiveSpan("inner span", async (span) => {
console.log("log 1");
await 1;
console.log("log 2");
span.end();
});
}
const server = Deno.serve({
@ -17,8 +24,11 @@ const server = Deno.serve({
}
},
handler: async (_req) => {
using _span = new Deno.tracing.Span("outer span");
return await tracer.startActiveSpan("outer span", async (span) => {
await inner();
return new Response(null, { status: 200 });
const resp = new Response(null, { status: 200 });
span.end();
return resp;
});
},
});

View File

@ -10,7 +10,7 @@ const server = Deno.serve(
port: 0,
onListen({ port }) {
const command = new Deno.Command(Deno.execPath(), {
args: ["run", "-A", "--unstable-otel", Deno.args[0]],
args: ["run", "-A", "-q", "--unstable-otel", Deno.args[0]],
env: {
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
OTEL_EXPORTER_OTLP_ENDPOINT: `http://localhost:${port}`,

View File

@ -247,7 +247,7 @@
"ext:runtime/41_prompt.js": "../runtime/js/41_prompt.js",
"ext:runtime/90_deno_ns.js": "../runtime/js/90_deno_ns.js",
"ext:runtime/98_global_scope.js": "../runtime/js/98_global_scope.js",
"ext:runtime/telemetry.js": "../runtime/js/telemetry.js",
"ext:runtime/telemetry.ts": "../runtime/js/telemetry.ts",
"ext:deno_node/_util/std_fmt_colors.ts": "../ext/node/polyfills/_util/std_fmt_colors.ts",
"@std/archive": "../tests/util/std/archive/mod.ts",
"@std/archive/tar": "../tests/util/std/archive/tar.ts",