mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 20:38:55 +00:00
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:
parent
106d47a013
commit
594a99817c
123
cli/tsc/dts/lib.deno.unstable.d.ts
vendored
123
cli/tsc/dts/lib.deno.unstable.d.ts
vendored
@ -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
|
* @category Telemetry
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
export namespace tracing {
|
export namespace telemetry {
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A SpanExporter compatible with OpenTelemetry.js
|
* A SpanExporter compatible with OpenTelemetry.js
|
||||||
* https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_trace_base.SpanExporter.html
|
* 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
|
export {}; // only export exports
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @category Telemetry
|
|
||||||
* @experimental
|
|
||||||
*/
|
|
||||||
export namespace metrics {
|
|
||||||
export {}; // only export exports
|
|
||||||
}
|
|
||||||
|
|
||||||
export {}; // only export exports
|
export {}; // only export exports
|
||||||
}
|
}
|
||||||
|
|
||||||
|
3
ext/console/internal.d.ts
vendored
3
ext/console/internal.d.ts
vendored
@ -9,4 +9,7 @@ declare module "ext:deno_console/01_console.js" {
|
|||||||
keys: (keyof TObject)[];
|
keys: (keyof TObject)[];
|
||||||
evaluate: boolean;
|
evaluate: boolean;
|
||||||
}): Record<string, unknown>;
|
}): Record<string, unknown>;
|
||||||
|
|
||||||
|
class Console {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 kv from "ext:deno_kv/01_db.ts";
|
||||||
import * as cron from "ext:deno_cron/01_cron.ts";
|
import * as cron from "ext:deno_cron/01_cron.ts";
|
||||||
import * as webgpuSurface from "ext:deno_webgpu/02_surface.js";
|
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 = {
|
const denoNs = {
|
||||||
Process: process.Process,
|
Process: process.Process,
|
||||||
@ -185,8 +185,7 @@ denoNsUnstableById[unstableIds.webgpu] = {
|
|||||||
// denoNsUnstableById[unstableIds.workerOptions] = { __proto__: null }
|
// denoNsUnstableById[unstableIds.workerOptions] = { __proto__: null }
|
||||||
|
|
||||||
denoNsUnstableById[unstableIds.otel] = {
|
denoNsUnstableById[unstableIds.otel] = {
|
||||||
tracing: telemetry.tracing,
|
telemetry: telemetry.telemetry,
|
||||||
metrics: telemetry.metrics,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { denoNs, denoNsUnstableById, unstableIds };
|
export { denoNs, denoNsUnstableById, unstableIds };
|
||||||
|
@ -86,7 +86,7 @@ import {
|
|||||||
workerRuntimeGlobalProperties,
|
workerRuntimeGlobalProperties,
|
||||||
} from "ext:runtime/98_global_scope_worker.js";
|
} from "ext:runtime/98_global_scope_worker.js";
|
||||||
import { SymbolDispose, SymbolMetadata } from "ext:deno_web/00_infra.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
|
// deno-lint-ignore prefer-primordials
|
||||||
if (Symbol.metadata) {
|
if (Symbol.metadata) {
|
||||||
|
@ -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
720
runtime/js/telemetry.ts
Normal 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 };
|
@ -1,8 +1,8 @@
|
|||||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
use crate::tokio_util::create_basic_runtime;
|
use crate::tokio_util::create_basic_runtime;
|
||||||
|
use deno_core::anyhow;
|
||||||
use deno_core::anyhow::anyhow;
|
use deno_core::anyhow::anyhow;
|
||||||
use deno_core::anyhow::{self};
|
|
||||||
use deno_core::futures::channel::mpsc;
|
use deno_core::futures::channel::mpsc;
|
||||||
use deno_core::futures::channel::mpsc::UnboundedSender;
|
use deno_core::futures::channel::mpsc::UnboundedSender;
|
||||||
use deno_core::futures::future::BoxFuture;
|
use deno_core::futures::future::BoxFuture;
|
||||||
@ -23,7 +23,6 @@ use opentelemetry::trace::SpanKind;
|
|||||||
use opentelemetry::trace::Status as SpanStatus;
|
use opentelemetry::trace::Status as SpanStatus;
|
||||||
use opentelemetry::trace::TraceFlags;
|
use opentelemetry::trace::TraceFlags;
|
||||||
use opentelemetry::trace::TraceId;
|
use opentelemetry::trace::TraceId;
|
||||||
use opentelemetry::InstrumentationScope;
|
|
||||||
use opentelemetry::Key;
|
use opentelemetry::Key;
|
||||||
use opentelemetry::KeyValue;
|
use opentelemetry::KeyValue;
|
||||||
use opentelemetry::StringValue;
|
use opentelemetry::StringValue;
|
||||||
@ -63,11 +62,15 @@ deno_core::extension!(
|
|||||||
deno_otel,
|
deno_otel,
|
||||||
ops = [
|
ops = [
|
||||||
op_otel_log,
|
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_start,
|
||||||
op_otel_span_continue,
|
op_otel_span_continue,
|
||||||
op_otel_span_attribute,
|
op_otel_span_attribute,
|
||||||
op_otel_span_attribute2,
|
op_otel_span_attribute2,
|
||||||
op_otel_span_attribute3,
|
op_otel_span_attribute3,
|
||||||
|
op_otel_span_set_dropped,
|
||||||
op_otel_span_flush,
|
op_otel_span_flush,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -303,6 +306,10 @@ mod hyper_client {
|
|||||||
static OTEL_PROCESSORS: OnceCell<(SpanProcessor, LogProcessor)> =
|
static OTEL_PROCESSORS: OnceCell<(SpanProcessor, LogProcessor)> =
|
||||||
OnceCell::new();
|
OnceCell::new();
|
||||||
|
|
||||||
|
static BUILT_IN_INSTRUMENTATION_SCOPE: OnceCell<
|
||||||
|
opentelemetry::InstrumentationScope,
|
||||||
|
> = OnceCell::new();
|
||||||
|
|
||||||
pub fn init(config: OtelConfig) -> anyhow::Result<()> {
|
pub fn init(config: OtelConfig) -> anyhow::Result<()> {
|
||||||
// Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_*
|
// Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_*
|
||||||
// crates don't do this automatically.
|
// crates don't do this automatically.
|
||||||
@ -390,6 +397,14 @@ pub fn init(config: OtelConfig) -> anyhow::Result<()> {
|
|||||||
.set((span_processor, log_processor))
|
.set((span_processor, log_processor))
|
||||||
.map_err(|_| anyhow!("failed to init otel"))?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,16 +473,160 @@ pub fn handle_log(record: &log::Record) {
|
|||||||
|
|
||||||
log_processor.emit(
|
log_processor.emit(
|
||||||
&mut log_record,
|
&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)]
|
#[op2(fast)]
|
||||||
fn op_otel_log(
|
fn op_otel_log(
|
||||||
|
scope: &mut v8::HandleScope<'_>,
|
||||||
#[string] message: String,
|
#[string] message: String,
|
||||||
#[smi] level: i32,
|
#[smi] level: i32,
|
||||||
#[string] trace_id: &str,
|
trace_id: v8::Local<'_, v8::Value>,
|
||||||
#[string] span_id: &str,
|
span_id: v8::Local<'_, v8::Value>,
|
||||||
#[smi] trace_flags: u8,
|
#[smi] trace_flags: u8,
|
||||||
) {
|
) {
|
||||||
let Some((_, log_processor)) = OTEL_PROCESSORS.get() else {
|
let Some((_, log_processor)) = OTEL_PROCESSORS.get() else {
|
||||||
@ -483,15 +642,16 @@ fn op_otel_log(
|
|||||||
3.. => Severity::Error,
|
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();
|
let mut log_record = LogRecord::default();
|
||||||
|
|
||||||
log_record.set_observed_timestamp(SystemTime::now());
|
log_record.set_observed_timestamp(SystemTime::now());
|
||||||
log_record.set_body(message.into());
|
log_record.set_body(message.into());
|
||||||
log_record.set_severity_number(severity);
|
log_record.set_severity_number(severity);
|
||||||
log_record.set_severity_text(severity.name());
|
log_record.set_severity_text(severity.name());
|
||||||
if let (Ok(trace_id), Ok(span_id)) =
|
if trace_id != TraceId::INVALID && span_id != SpanId::INVALID {
|
||||||
(TraceId::from_hex(trace_id), SpanId::from_hex(span_id))
|
|
||||||
{
|
|
||||||
log_record.set_trace_context(
|
log_record.set_trace_context(
|
||||||
trace_id,
|
trace_id,
|
||||||
span_id,
|
span_id,
|
||||||
@ -501,7 +661,7 @@ fn op_otel_log(
|
|||||||
|
|
||||||
log_processor.emit(
|
log_processor.emit(
|
||||||
&mut log_record,
|
&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);
|
span_processor.on_end(temporary_span.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let trace_id = {
|
let Some(InstrumentationScope(instrumentation_scope)) =
|
||||||
let x = v8::ValueView::new(scope, trace_id.try_cast()?);
|
state.try_borrow::<InstrumentationScope>()
|
||||||
match x.data() {
|
else {
|
||||||
v8::ValueViewData::OneByte(bytes) => {
|
return Err(anyhow!("instrumentation scope not available"));
|
||||||
TraceId::from_hex(&String::from_utf8_lossy(bytes))?
|
|
||||||
}
|
|
||||||
_ => return Err(anyhow!("invalid trace_id")),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let span_id = {
|
let trace_id = parse_trace_id(scope, trace_id);
|
||||||
let x = v8::ValueView::new(scope, span_id.try_cast()?);
|
if trace_id == TraceId::INVALID {
|
||||||
match x.data() {
|
return Err(anyhow!("invalid trace_id"));
|
||||||
v8::ValueViewData::OneByte(bytes) => {
|
}
|
||||||
SpanId::from_hex(&String::from_utf8_lossy(bytes))?
|
|
||||||
}
|
|
||||||
_ => return Err(anyhow!("invalid span_id")),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let parent_span_id = {
|
let span_id = parse_span_id(scope, span_id);
|
||||||
let x = v8::ValueView::new(scope, parent_span_id.try_cast()?);
|
if span_id == SpanId::INVALID {
|
||||||
match x.data() {
|
return Err(anyhow!("invalid span_id"));
|
||||||
v8::ValueViewData::OneByte(bytes) => {
|
}
|
||||||
let s = String::from_utf8_lossy(bytes);
|
|
||||||
if s.is_empty() {
|
let parent_span_id = parse_span_id(scope, parent_span_id);
|
||||||
SpanId::INVALID
|
|
||||||
} else {
|
|
||||||
SpanId::from_hex(&s)?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return Err(anyhow!("invalid parent_span_id")),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = {
|
let name = {
|
||||||
let x = v8::ValueView::new(scope, name.try_cast()?);
|
let x = v8::ValueView::new(scope, name.try_cast()?);
|
||||||
@ -601,7 +744,7 @@ fn op_otel_span_start<'s>(
|
|||||||
events: Default::default(),
|
events: Default::default(),
|
||||||
links: Default::default(),
|
links: Default::default(),
|
||||||
status: SpanStatus::Unset,
|
status: SpanStatus::Unset,
|
||||||
instrumentation_scope: InstrumentationScope::builder("deno").build(),
|
instrumentation_scope: instrumentation_scope.clone(),
|
||||||
});
|
});
|
||||||
state.put(temporary_span);
|
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)]
|
#[op2(fast)]
|
||||||
fn op_otel_span_attribute<'s>(
|
fn op_otel_span_attribute<'s>(
|
||||||
scope: &mut v8::HandleScope<'s>,
|
scope: &mut v8::HandleScope<'s>,
|
||||||
@ -684,7 +781,7 @@ fn op_otel_span_attribute<'s>(
|
|||||||
temporary_span.0.attributes.reserve_exact(
|
temporary_span.0.attributes.reserve_exact(
|
||||||
(capacity as usize) - temporary_span.0.attributes.capacity(),
|
(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(
|
temporary_span.0.attributes.reserve_exact(
|
||||||
(capacity as usize) - temporary_span.0.attributes.capacity(),
|
(capacity as usize) - temporary_span.0.attributes.capacity(),
|
||||||
);
|
);
|
||||||
attr!(scope, temporary_span, key1, value1);
|
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key1, value1);
|
||||||
attr!(scope, temporary_span, key2, value2);
|
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(
|
temporary_span.0.attributes.reserve_exact(
|
||||||
(capacity as usize) - temporary_span.0.attributes.capacity(),
|
(capacity as usize) - temporary_span.0.attributes.capacity(),
|
||||||
);
|
);
|
||||||
attr!(scope, temporary_span, key1, value1);
|
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key1, value1);
|
||||||
attr!(scope, temporary_span, key2, value2);
|
attr!(scope, temporary_span.0.attributes => temporary_span.0.dropped_attributes_count, key2, value2);
|
||||||
attr!(scope, temporary_span, key3, value3);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ extension!(runtime,
|
|||||||
"40_signals.js",
|
"40_signals.js",
|
||||||
"40_tty.js",
|
"40_tty.js",
|
||||||
"41_prompt.js",
|
"41_prompt.js",
|
||||||
"telemetry.js",
|
"telemetry.ts",
|
||||||
"90_deno_ns.js",
|
"90_deno_ns.js",
|
||||||
"98_global_scope_shared.js",
|
"98_global_scope_shared.js",
|
||||||
"98_global_scope_window.js",
|
"98_global_scope_window.js",
|
||||||
|
14
tests/registry/jsr/@deno/otel/0.0.2/deno.json
Normal file
14
tests/registry/jsr/@deno/otel/0.0.2/deno.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
38
tests/registry/jsr/@deno/otel/0.0.2/src/index.ts
Normal file
38
tests/registry/jsr/@deno/otel/0.0.2/src/index.ts
Normal 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();
|
||||||
|
}
|
5
tests/registry/jsr/@deno/otel/0.0.2/src/register.ts
Normal file
5
tests/registry/jsr/@deno/otel/0.0.2/src/register.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Copyright 2024-2024 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
import { register } from "./index.ts";
|
||||||
|
|
||||||
|
register();
|
6
tests/registry/jsr/@deno/otel/0.0.2_meta.json
Normal file
6
tests/registry/jsr/@deno/otel/0.0.2_meta.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./register": "./src/register.ts"
|
||||||
|
}
|
||||||
|
}
|
8
tests/registry/jsr/@deno/otel/meta.json
Normal file
8
tests/registry/jsr/@deno/otel/meta.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"scope": "deno",
|
||||||
|
"name": "otel",
|
||||||
|
"latest": "0.0.2",
|
||||||
|
"versions": {
|
||||||
|
"0.0.2": {}
|
||||||
|
}
|
||||||
|
}
|
1
tests/registry/npm/@opentelemetry/api/registry.json
Normal file
1
tests/registry/npm/@opentelemetry/api/registry.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/registry/npm/@opentelemetry/core/registry.json
Normal file
1
tests/registry/npm/@opentelemetry/core/registry.json
Normal file
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
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"spans": [
|
"spans": [
|
||||||
{
|
{
|
||||||
"traceId": "00000000000000000000000000000002",
|
"traceId": "10000000000000000000000000000002",
|
||||||
"spanId": "0000000000000003",
|
"spanId": "1000000000000003",
|
||||||
"traceState": "",
|
"traceState": "",
|
||||||
"parentSpanId": "0000000000000001",
|
"parentSpanId": "1000000000000001",
|
||||||
"flags": 1,
|
"flags": 1,
|
||||||
"name": "inner span",
|
"name": "inner span",
|
||||||
"kind": 1,
|
"kind": 1,
|
||||||
@ -22,8 +22,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"traceId": "00000000000000000000000000000002",
|
"traceId": "10000000000000000000000000000002",
|
||||||
"spanId": "0000000000000001",
|
"spanId": "1000000000000001",
|
||||||
"traceState": "",
|
"traceState": "",
|
||||||
"parentSpanId": "",
|
"parentSpanId": "",
|
||||||
"flags": 1,
|
"flags": 1,
|
||||||
@ -55,8 +55,8 @@
|
|||||||
"attributes": [],
|
"attributes": [],
|
||||||
"droppedAttributesCount": 0,
|
"droppedAttributesCount": 0,
|
||||||
"flags": 1,
|
"flags": 1,
|
||||||
"traceId": "00000000000000000000000000000002",
|
"traceId": "10000000000000000000000000000002",
|
||||||
"spanId": "0000000000000003"
|
"spanId": "1000000000000003"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"timeUnixNano": "0",
|
"timeUnixNano": "0",
|
||||||
@ -69,8 +69,8 @@
|
|||||||
"attributes": [],
|
"attributes": [],
|
||||||
"droppedAttributesCount": 0,
|
"droppedAttributesCount": 0,
|
||||||
"flags": 1,
|
"flags": 1,
|
||||||
"traceId": "00000000000000000000000000000002",
|
"traceId": "10000000000000000000000000000002",
|
||||||
"spanId": "0000000000000003"
|
"spanId": "1000000000000003"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
// 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() {
|
async function inner() {
|
||||||
using _span = new Deno.tracing.Span("inner span");
|
await tracer.startActiveSpan("inner span", async (span) => {
|
||||||
console.log("log 1");
|
console.log("log 1");
|
||||||
await 1;
|
await 1;
|
||||||
console.log("log 2");
|
console.log("log 2");
|
||||||
|
span.end();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = Deno.serve({
|
const server = Deno.serve({
|
||||||
@ -17,8 +24,11 @@ const server = Deno.serve({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (_req) => {
|
handler: async (_req) => {
|
||||||
using _span = new Deno.tracing.Span("outer span");
|
return await tracer.startActiveSpan("outer span", async (span) => {
|
||||||
await inner();
|
await inner();
|
||||||
return new Response(null, { status: 200 });
|
const resp = new Response(null, { status: 200 });
|
||||||
|
span.end();
|
||||||
|
return resp;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ const server = Deno.serve(
|
|||||||
port: 0,
|
port: 0,
|
||||||
onListen({ port }) {
|
onListen({ port }) {
|
||||||
const command = new Deno.Command(Deno.execPath(), {
|
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: {
|
env: {
|
||||||
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: `http://localhost:${port}`,
|
OTEL_EXPORTER_OTLP_ENDPOINT: `http://localhost:${port}`,
|
||||||
|
@ -247,7 +247,7 @@
|
|||||||
"ext:runtime/41_prompt.js": "../runtime/js/41_prompt.js",
|
"ext:runtime/41_prompt.js": "../runtime/js/41_prompt.js",
|
||||||
"ext:runtime/90_deno_ns.js": "../runtime/js/90_deno_ns.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/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",
|
"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": "../tests/util/std/archive/mod.ts",
|
||||||
"@std/archive/tar": "../tests/util/std/archive/tar.ts",
|
"@std/archive/tar": "../tests/util/std/archive/tar.ts",
|
||||||
|
Loading…
Reference in New Issue
Block a user