mirror of
https://github.com/denoland/deno.git
synced 2025-09-21 18:10:02 +00:00
1821 lines
46 KiB
TypeScript
1821 lines
46 KiB
TypeScript
// Copyright 2018-2025 the Deno authors. MIT license.
|
|
|
|
import { core, primordials } from "ext:core/mod.js";
|
|
import {
|
|
op_otel_collect_isolate_metrics,
|
|
op_otel_enable_isolate_metrics,
|
|
op_otel_log,
|
|
op_otel_log_foreign,
|
|
op_otel_metric_attribute3,
|
|
op_otel_metric_observable_record0,
|
|
op_otel_metric_observable_record1,
|
|
op_otel_metric_observable_record2,
|
|
op_otel_metric_observable_record3,
|
|
op_otel_metric_observation_done,
|
|
op_otel_metric_record0,
|
|
op_otel_metric_record1,
|
|
op_otel_metric_record2,
|
|
op_otel_metric_record3,
|
|
op_otel_metric_wait_to_observe,
|
|
op_otel_span_add_link,
|
|
op_otel_span_attribute1,
|
|
op_otel_span_attribute2,
|
|
op_otel_span_attribute3,
|
|
op_otel_span_update_name,
|
|
OtelMeter,
|
|
OtelSpan,
|
|
OtelTracer,
|
|
} from "ext:core/ops";
|
|
import { Console } from "ext:deno_console/01_console.js";
|
|
|
|
const {
|
|
ArrayFrom,
|
|
ArrayIsArray,
|
|
ArrayPrototypeFilter,
|
|
ArrayPrototypeForEach,
|
|
ArrayPrototypeJoin,
|
|
ArrayPrototypeMap,
|
|
ArrayPrototypePush,
|
|
ArrayPrototypeReduce,
|
|
ArrayPrototypeReverse,
|
|
ArrayPrototypeShift,
|
|
ArrayPrototypeSlice,
|
|
DatePrototype,
|
|
DatePrototypeGetTime,
|
|
Error,
|
|
MapPrototypeEntries,
|
|
MapPrototypeKeys,
|
|
Number,
|
|
NumberParseInt,
|
|
NumberPrototypeToString,
|
|
ObjectAssign,
|
|
ObjectDefineProperty,
|
|
ObjectEntries,
|
|
ObjectKeys,
|
|
ObjectPrototypeIsPrototypeOf,
|
|
ObjectValues,
|
|
ReflectApply,
|
|
SafeArrayIterator,
|
|
SafeIterator,
|
|
SafeMap,
|
|
SafePromiseAll,
|
|
SafeRegExp,
|
|
SafeSet,
|
|
SafeWeakSet,
|
|
StringPrototypeIndexOf,
|
|
StringPrototypeSlice,
|
|
StringPrototypeSplit,
|
|
StringPrototypeSubstring,
|
|
StringPrototypeTrim,
|
|
SymbolFor,
|
|
TypeError,
|
|
decodeURIComponent,
|
|
encodeURIComponent,
|
|
} = primordials;
|
|
const { AsyncVariable, getAsyncContext, setAsyncContext } = core;
|
|
|
|
export let TRACING_ENABLED = false;
|
|
export let METRICS_ENABLED = false;
|
|
export let PROPAGATORS: TextMapPropagator[] = [];
|
|
let ISOLATE_METRICS = false;
|
|
|
|
// Note: These start at 0 in the JS library,
|
|
// but start at 1 when serialized with JSON.
|
|
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;
|
|
}
|
|
|
|
enum SpanStatusCode {
|
|
UNSET = 0,
|
|
OK = 1,
|
|
ERROR = 2,
|
|
}
|
|
|
|
interface SpanStatus {
|
|
code: SpanStatusCode;
|
|
message?: string;
|
|
}
|
|
|
|
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 Exception {
|
|
code?: string | number;
|
|
message?: string;
|
|
name?: string;
|
|
stack?: string;
|
|
}
|
|
|
|
type TimeInput = [number, number] | number | Date;
|
|
|
|
interface SpanOptions {
|
|
kind?: SpanKind;
|
|
attributes?: Attributes;
|
|
links?: Link[];
|
|
startTime?: TimeInput;
|
|
root?: boolean;
|
|
}
|
|
|
|
interface Link {
|
|
context: SpanContext;
|
|
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;
|
|
}
|
|
|
|
function hrToMs(hr: [number, number]): number {
|
|
return (hr[0] * 1e3 + hr[1] / 1e6);
|
|
}
|
|
|
|
function isTimeInput(input: unknown): input is TimeInput {
|
|
return typeof input === "number" ||
|
|
(input && (ArrayIsArray(input) || isDate(input)));
|
|
}
|
|
|
|
function timeInputToMs(input?: TimeInput): number | undefined {
|
|
if (input === undefined) return;
|
|
if (ArrayIsArray(input)) {
|
|
return hrToMs(input);
|
|
} else if (isDate(input)) {
|
|
return DatePrototypeGetTime(input);
|
|
}
|
|
return input;
|
|
}
|
|
|
|
function countAttributes(attributes?: Attributes): number {
|
|
return attributes ? ObjectKeys(attributes).length : 0;
|
|
}
|
|
|
|
interface AsyncContextSnapshot {
|
|
__brand: "AsyncContextSnapshot";
|
|
}
|
|
|
|
export function enterSpan(span: Span): AsyncContextSnapshot | undefined {
|
|
if (!span.isRecording()) return undefined;
|
|
const context = (CURRENT.get() ?? ROOT_CONTEXT).setValue(SPAN_KEY, span);
|
|
return CURRENT.enter(context);
|
|
}
|
|
|
|
export const currentSnapshot = getAsyncContext;
|
|
export const restoreSnapshot = setAsyncContext;
|
|
|
|
function isDate(value: unknown): value is Date {
|
|
return ObjectPrototypeIsPrototypeOf(DatePrototype, value);
|
|
}
|
|
|
|
interface OtelTracer {
|
|
__key: "tracer";
|
|
|
|
// deno-lint-ignore no-misused-new
|
|
new (name: string, version?: string, schemaUrl?: string): OtelTracer;
|
|
|
|
startSpan(
|
|
parent: OtelSpan | undefined,
|
|
name: string,
|
|
spanKind: SpanKind,
|
|
startTime: number | undefined,
|
|
attributeCount: number,
|
|
): OtelSpan;
|
|
|
|
startSpanForeign(
|
|
parentTraceId: string,
|
|
parentSpanId: string,
|
|
name: string,
|
|
spanKind: SpanKind,
|
|
startTime: number | undefined,
|
|
attributeCount: number,
|
|
): OtelSpan;
|
|
}
|
|
|
|
interface OtelSpan {
|
|
__key: "span";
|
|
|
|
spanContext(): SpanContext;
|
|
setStatus(status: SpanStatusCode, errorDescription: string): void;
|
|
addEvent(
|
|
name: string,
|
|
startTime: number,
|
|
): void;
|
|
dropEvent(): void;
|
|
end(endTime: number): void;
|
|
}
|
|
|
|
enum SpanAttributesLocation {
|
|
SELF = 0,
|
|
LAST_EVENT = 1,
|
|
LAST_LINK = 2,
|
|
}
|
|
|
|
function spanAddAttributes(
|
|
span: OtelSpan,
|
|
attributesLocation: SpanAttributesLocation,
|
|
attributes: Attributes,
|
|
) {
|
|
const attributeKvs = ObjectEntries(attributes);
|
|
let i = 0;
|
|
while (i < attributeKvs.length) {
|
|
if (i + 2 < attributeKvs.length) {
|
|
op_otel_span_attribute3(
|
|
span,
|
|
attributesLocation,
|
|
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(
|
|
span,
|
|
attributesLocation,
|
|
attributeKvs[i][0],
|
|
attributeKvs[i][1],
|
|
attributeKvs[i + 1][0],
|
|
attributeKvs[i + 1][1],
|
|
);
|
|
i += 2;
|
|
} else {
|
|
op_otel_span_attribute1(
|
|
span,
|
|
attributesLocation,
|
|
attributeKvs[i][0],
|
|
attributeKvs[i][1],
|
|
);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
interface TracerOptions {
|
|
schemaUrl?: string;
|
|
}
|
|
|
|
class TracerProvider {
|
|
constructor() {
|
|
throw new TypeError("TracerProvider can not be constructed");
|
|
}
|
|
|
|
static getTracer(
|
|
name: string,
|
|
version?: string,
|
|
options?: TracerOptions,
|
|
): Tracer {
|
|
const tracer = new OtelTracer(name, version, options?.schemaUrl);
|
|
return new Tracer(tracer);
|
|
}
|
|
}
|
|
|
|
class Tracer {
|
|
#tracer: OtelTracer;
|
|
|
|
constructor(tracer: OtelTracer) {
|
|
this.#tracer = tracer;
|
|
}
|
|
|
|
startActiveSpan<F extends (span: Span) => unknown>(
|
|
name: string,
|
|
fn: F,
|
|
): ReturnType<F>;
|
|
startActiveSpan<F extends (span: Span) => unknown>(
|
|
name: string,
|
|
options: SpanOptions,
|
|
fn: F,
|
|
): ReturnType<F>;
|
|
startActiveSpan<F extends (span: Span) => unknown>(
|
|
name: string,
|
|
options: SpanOptions,
|
|
context: Context,
|
|
fn: F,
|
|
): ReturnType<F>;
|
|
startActiveSpan<F extends (span: Span) => unknown>(
|
|
name: string,
|
|
optionsOrFn: SpanOptions | F,
|
|
fnOrContext?: F | Context,
|
|
maybeFn?: F,
|
|
) {
|
|
let options;
|
|
let context;
|
|
let fn;
|
|
if (typeof optionsOrFn === "function") {
|
|
options = undefined;
|
|
fn = optionsOrFn;
|
|
} else if (typeof fnOrContext === "function") {
|
|
options = optionsOrFn;
|
|
fn = fnOrContext;
|
|
} else if (typeof maybeFn === "function") {
|
|
options = optionsOrFn;
|
|
context = fnOrContext;
|
|
fn = maybeFn;
|
|
} else {
|
|
throw new Error("startActiveSpan requires a function argument");
|
|
}
|
|
if (options?.root) {
|
|
context = ROOT_CONTEXT;
|
|
} else {
|
|
context = context ?? CURRENT.get() ?? ROOT_CONTEXT;
|
|
}
|
|
const span = this.startSpan(name, options, context);
|
|
const ctx = CURRENT.enter(context.setValue(SPAN_KEY, span));
|
|
try {
|
|
return ReflectApply(fn, undefined, [span]);
|
|
} finally {
|
|
setAsyncContext(ctx);
|
|
}
|
|
}
|
|
|
|
startSpan(name: string, options?: SpanOptions, context?: Context): Span {
|
|
if (options?.root) {
|
|
context = undefined;
|
|
} else {
|
|
context = context ?? CURRENT.get();
|
|
}
|
|
|
|
const startTime = timeInputToMs(options?.startTime);
|
|
|
|
const parentSpan = context?.getValue(SPAN_KEY) as
|
|
| Span
|
|
| { spanContext(): SpanContext }
|
|
| undefined;
|
|
const attributesCount = countAttributes(options?.attributes);
|
|
const parentOtelSpan: OtelSpan | null | undefined = parentSpan !== undefined
|
|
? getOtelSpan(parentSpan) ?? undefined
|
|
: undefined;
|
|
let otelSpan: OtelSpan;
|
|
if (parentOtelSpan || !parentSpan) {
|
|
otelSpan = this.#tracer.startSpan(
|
|
parentOtelSpan,
|
|
name,
|
|
options?.kind ?? 0,
|
|
startTime,
|
|
attributesCount,
|
|
);
|
|
} else {
|
|
const spanContext = parentSpan.spanContext();
|
|
otelSpan = this.#tracer.startSpanForeign(
|
|
spanContext.traceId,
|
|
spanContext.spanId,
|
|
name,
|
|
options?.kind ?? 0,
|
|
startTime,
|
|
attributesCount,
|
|
);
|
|
}
|
|
const span = new Span(otelSpan);
|
|
if (options?.links) span.addLinks(options?.links);
|
|
if (options?.attributes) span.setAttributes(options?.attributes);
|
|
return span;
|
|
}
|
|
}
|
|
|
|
const SPAN_KEY = SymbolFor("OpenTelemetry Context Key SPAN");
|
|
|
|
let getOtelSpan: (span: object) => OtelSpan | null | undefined;
|
|
|
|
class Span {
|
|
#otelSpan: OtelSpan | null;
|
|
#spanContext: SpanContext | undefined;
|
|
|
|
static {
|
|
// deno-lint-ignore prefer-primordials
|
|
getOtelSpan = (span) => (#otelSpan in span ? span.#otelSpan : undefined);
|
|
}
|
|
|
|
constructor(otelSpan: OtelSpan | null) {
|
|
this.#otelSpan = otelSpan;
|
|
}
|
|
|
|
spanContext() {
|
|
if (!this.#spanContext) {
|
|
if (this.#otelSpan) {
|
|
this.#spanContext = this.#otelSpan.spanContext();
|
|
} else {
|
|
this.#spanContext = {
|
|
traceId: "00000000000000000000000000000000",
|
|
spanId: "0000000000000000",
|
|
traceFlags: 0,
|
|
};
|
|
}
|
|
}
|
|
return this.#spanContext;
|
|
}
|
|
|
|
addEvent(
|
|
name: string,
|
|
attributesOrStartTime?: Attributes | TimeInput,
|
|
startTime?: TimeInput,
|
|
): this {
|
|
if (!this.#otelSpan) return this;
|
|
let attributes: Attributes | undefined;
|
|
if (isTimeInput(attributesOrStartTime)) {
|
|
startTime = attributesOrStartTime;
|
|
} else {
|
|
attributes = attributesOrStartTime;
|
|
}
|
|
const startTimeMs = timeInputToMs(startTime);
|
|
|
|
this.#otelSpan.addEvent(
|
|
name,
|
|
startTimeMs ?? NaN,
|
|
);
|
|
if (attributes) {
|
|
spanAddAttributes(
|
|
this.#otelSpan,
|
|
SpanAttributesLocation.LAST_EVENT,
|
|
attributes,
|
|
);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
addLink(link: Link): this {
|
|
if (!this.#otelSpan) return this;
|
|
const valid = op_otel_span_add_link(
|
|
this.#otelSpan,
|
|
link.context.traceId,
|
|
link.context.spanId,
|
|
link.context.traceFlags,
|
|
link.context.isRemote ?? false,
|
|
link.droppedAttributesCount ?? 0,
|
|
);
|
|
if (link.attributes) {
|
|
spanAddAttributes(
|
|
this.#otelSpan,
|
|
SpanAttributesLocation.LAST_LINK,
|
|
link.attributes,
|
|
);
|
|
}
|
|
if (!valid) return this;
|
|
return this;
|
|
}
|
|
|
|
addLinks(links: Link[]): this {
|
|
for (let i = 0; i < links.length; i++) {
|
|
this.addLink(links[i]);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
end(endTime?: TimeInput): void {
|
|
this.#otelSpan?.end(timeInputToMs(endTime) || NaN);
|
|
}
|
|
|
|
isRecording(): boolean {
|
|
return this.#otelSpan !== undefined;
|
|
}
|
|
|
|
recordException(exception: string | Exception, time?: TimeInput): void {
|
|
if (typeof exception === "string") {
|
|
this.addEvent("exception", {
|
|
"exception.message": exception,
|
|
}, time);
|
|
return;
|
|
}
|
|
const attributes: Attributes = {};
|
|
|
|
if (exception.code) {
|
|
if (typeof exception.code === "number") {
|
|
attributes["exception.type"] = NumberPrototypeToString(exception.code);
|
|
} else {
|
|
attributes["exception.type"] = exception.code;
|
|
}
|
|
} else if (exception.name) {
|
|
attributes["exception.type"] = exception.name;
|
|
}
|
|
|
|
if (exception.message) {
|
|
attributes["exception.message"] = exception.message;
|
|
}
|
|
if (exception.stack) {
|
|
attributes["exception.stacktrace"] = exception.stack;
|
|
}
|
|
|
|
this.addEvent("exception", attributes, time);
|
|
}
|
|
|
|
setAttribute(key: string, value: AttributeValue): this {
|
|
if (!this.#otelSpan) return this;
|
|
op_otel_span_attribute1(
|
|
this.#otelSpan,
|
|
SpanAttributesLocation.SELF,
|
|
key,
|
|
value,
|
|
);
|
|
return this;
|
|
}
|
|
|
|
setAttributes(attributes: Attributes): this {
|
|
if (!this.#otelSpan) return this;
|
|
spanAddAttributes(this.#otelSpan, SpanAttributesLocation.SELF, attributes);
|
|
return this;
|
|
}
|
|
|
|
setStatus(status: SpanStatus): this {
|
|
this.#otelSpan?.setStatus(status.code, status.message ?? "");
|
|
return this;
|
|
}
|
|
|
|
updateName(name: string): this {
|
|
if (!this.#otelSpan) return this;
|
|
op_otel_span_update_name(this.#otelSpan, name);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
const CURRENT = new AsyncVariable();
|
|
|
|
class Context {
|
|
// @ts-ignore __proto__ is not supported in TypeScript
|
|
#data: Record<symbol, unknown> = { __proto__: null };
|
|
|
|
constructor(data?: Record<symbol, unknown> | null | undefined) {
|
|
// @ts-ignore __proto__ is not supported in TypeScript
|
|
this.#data = { __proto__: null, ...data };
|
|
}
|
|
|
|
getValue(key: symbol): unknown {
|
|
return this.#data[key];
|
|
}
|
|
|
|
setValue(key: symbol, value: unknown): Context {
|
|
const c = new Context(this.#data);
|
|
c.#data[key] = value;
|
|
return c;
|
|
}
|
|
|
|
deleteValue(key: symbol): Context {
|
|
const c = new Context(this.#data);
|
|
delete c.#data[key];
|
|
return c;
|
|
}
|
|
}
|
|
|
|
// TODO(lucacasonato): @opentelemetry/api defines it's own ROOT_CONTEXT
|
|
const ROOT_CONTEXT = new Context();
|
|
|
|
// Context manager for opentelemetry js library
|
|
export class ContextManager {
|
|
constructor() {
|
|
throw new TypeError("ContextManager can not be constructed");
|
|
}
|
|
|
|
static active(): Context {
|
|
return CURRENT.get() ?? ROOT_CONTEXT;
|
|
}
|
|
|
|
static 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
|
|
static 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;
|
|
}
|
|
|
|
static enable() {
|
|
return this;
|
|
}
|
|
|
|
static disable() {
|
|
return this;
|
|
}
|
|
}
|
|
|
|
// metrics
|
|
|
|
interface MeterOptions {
|
|
schemaUrl?: string;
|
|
}
|
|
|
|
interface MetricOptions {
|
|
description?: string;
|
|
|
|
unit?: string;
|
|
|
|
valueType?: ValueType;
|
|
|
|
advice?: MetricAdvice;
|
|
}
|
|
|
|
enum ValueType {
|
|
INT = 0,
|
|
DOUBLE = 1,
|
|
}
|
|
|
|
interface MetricAdvice {
|
|
/**
|
|
* Hint the explicit bucket boundaries for SDK if the metric is been
|
|
* aggregated with a HistogramAggregator.
|
|
*/
|
|
explicitBucketBoundaries?: number[];
|
|
}
|
|
|
|
interface OtelMeter {
|
|
__key: "meter";
|
|
createCounter(name: string, description?: string, unit?: string): Instrument;
|
|
createUpDownCounter(
|
|
name: string,
|
|
description?: string,
|
|
unit?: string,
|
|
): Instrument;
|
|
createGauge(name: string, description?: string, unit?: string): Instrument;
|
|
createHistogram(
|
|
name: string,
|
|
description?: string,
|
|
unit?: string,
|
|
explicitBucketBoundaries?: number[],
|
|
): Instrument;
|
|
createObservableCounter(
|
|
name: string,
|
|
description?: string,
|
|
unit?: string,
|
|
): Instrument;
|
|
createObservableUpDownCounter(
|
|
name: string,
|
|
description?: string,
|
|
unit?: string,
|
|
): Instrument;
|
|
createObservableGauge(
|
|
name: string,
|
|
description?: string,
|
|
unit?: string,
|
|
): Instrument;
|
|
}
|
|
|
|
class MeterProvider {
|
|
constructor() {
|
|
throw new TypeError("MeterProvider can not be constructed");
|
|
}
|
|
|
|
static getMeter(
|
|
name: string,
|
|
version?: string,
|
|
options?: MeterOptions,
|
|
): Meter {
|
|
const meter = new OtelMeter(name, version, options?.schemaUrl);
|
|
return new Meter(meter);
|
|
}
|
|
}
|
|
|
|
type MetricAttributes = Attributes;
|
|
|
|
type Instrument = { __key: "instrument" };
|
|
|
|
let batchResultHasObservables: (
|
|
res: BatchObservableResult,
|
|
observables: Observable[],
|
|
) => boolean;
|
|
|
|
class BatchObservableResult {
|
|
#observables: WeakSet<Observable>;
|
|
|
|
constructor(observables: WeakSet<Observable>) {
|
|
this.#observables = observables;
|
|
}
|
|
|
|
static {
|
|
batchResultHasObservables = (cb, observables) => {
|
|
for (const observable of new SafeIterator(observables)) {
|
|
if (!cb.#observables.has(observable)) return false;
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
observe(
|
|
metric: Observable,
|
|
value: number,
|
|
attributes?: MetricAttributes,
|
|
): void {
|
|
if (!this.#observables.has(metric)) return;
|
|
getObservableResult(metric).observe(value, attributes);
|
|
}
|
|
}
|
|
|
|
const BATCH_CALLBACKS = new SafeMap<
|
|
BatchObservableCallback,
|
|
BatchObservableResult
|
|
>();
|
|
const INDIVIDUAL_CALLBACKS = new SafeMap<Observable, Set<ObservableCallback>>();
|
|
|
|
class Meter {
|
|
#meter: OtelMeter;
|
|
|
|
constructor(meter: OtelMeter) {
|
|
this.#meter = meter;
|
|
}
|
|
|
|
createCounter(name: string, options?: MetricOptions): Counter {
|
|
if (options?.valueType !== undefined && options?.valueType !== 1) {
|
|
throw new Error("Only valueType: DOUBLE is supported");
|
|
}
|
|
if (!METRICS_ENABLED) return new Counter(null, false);
|
|
const instrument = this.#meter.createCounter(
|
|
name,
|
|
// deno-lint-ignore prefer-primordials
|
|
options?.description,
|
|
options?.unit,
|
|
) as Instrument;
|
|
return new Counter(instrument, false);
|
|
}
|
|
|
|
createUpDownCounter(name: string, options?: MetricOptions): Counter {
|
|
if (options?.valueType !== undefined && options?.valueType !== 1) {
|
|
throw new Error("Only valueType: DOUBLE is supported");
|
|
}
|
|
if (!METRICS_ENABLED) return new Counter(null, true);
|
|
const instrument = this.#meter.createUpDownCounter(
|
|
name,
|
|
// deno-lint-ignore prefer-primordials
|
|
options?.description,
|
|
options?.unit,
|
|
) as Instrument;
|
|
return new Counter(instrument, true);
|
|
}
|
|
|
|
createGauge(name: string, options?: MetricOptions): Gauge {
|
|
if (options?.valueType !== undefined && options?.valueType !== 1) {
|
|
throw new Error("Only valueType: DOUBLE is supported");
|
|
}
|
|
if (!METRICS_ENABLED) return new Gauge(null);
|
|
const instrument = this.#meter.createGauge(
|
|
name,
|
|
// deno-lint-ignore prefer-primordials
|
|
options?.description,
|
|
options?.unit,
|
|
) as Instrument;
|
|
return new Gauge(instrument);
|
|
}
|
|
|
|
createHistogram(name: string, options?: MetricOptions): Histogram {
|
|
if (options?.valueType !== undefined && options?.valueType !== 1) {
|
|
throw new Error("Only valueType: DOUBLE is supported");
|
|
}
|
|
if (!METRICS_ENABLED) return new Histogram(null);
|
|
const instrument = this.#meter.createHistogram(
|
|
name,
|
|
// deno-lint-ignore prefer-primordials
|
|
options?.description,
|
|
options?.unit,
|
|
options?.advice?.explicitBucketBoundaries,
|
|
) as Instrument;
|
|
return new Histogram(instrument);
|
|
}
|
|
|
|
createObservableCounter(name: string, options?: MetricOptions): Observable {
|
|
if (options?.valueType !== undefined && options?.valueType !== 1) {
|
|
throw new Error("Only valueType: DOUBLE is supported");
|
|
}
|
|
if (!METRICS_ENABLED) new Observable(new ObservableResult(null, true));
|
|
const instrument = this.#meter.createObservableCounter(
|
|
name,
|
|
// deno-lint-ignore prefer-primordials
|
|
options?.description,
|
|
options?.unit,
|
|
) as Instrument;
|
|
return new Observable(new ObservableResult(instrument, true));
|
|
}
|
|
|
|
createObservableUpDownCounter(
|
|
name: string,
|
|
options?: MetricOptions,
|
|
): Observable {
|
|
if (options?.valueType !== undefined && options?.valueType !== 1) {
|
|
throw new Error("Only valueType: DOUBLE is supported");
|
|
}
|
|
if (!METRICS_ENABLED) new Observable(new ObservableResult(null, false));
|
|
const instrument = this.#meter.createObservableUpDownCounter(
|
|
name,
|
|
// deno-lint-ignore prefer-primordials
|
|
options?.description,
|
|
options?.unit,
|
|
) as Instrument;
|
|
return new Observable(new ObservableResult(instrument, false));
|
|
}
|
|
|
|
createObservableGauge(name: string, options?: MetricOptions): Observable {
|
|
if (options?.valueType !== undefined && options?.valueType !== 1) {
|
|
throw new Error("Only valueType: DOUBLE is supported");
|
|
}
|
|
if (!METRICS_ENABLED) new Observable(new ObservableResult(null, false));
|
|
const instrument = this.#meter.createObservableGauge(
|
|
name,
|
|
// deno-lint-ignore prefer-primordials
|
|
options?.description,
|
|
options?.unit,
|
|
) as Instrument;
|
|
return new Observable(new ObservableResult(instrument, false));
|
|
}
|
|
|
|
addBatchObservableCallback(
|
|
callback: BatchObservableCallback,
|
|
observables: Observable[],
|
|
): void {
|
|
if (!METRICS_ENABLED) return;
|
|
const result = new BatchObservableResult(new SafeWeakSet(observables));
|
|
startObserving();
|
|
BATCH_CALLBACKS.set(callback, result);
|
|
}
|
|
|
|
removeBatchObservableCallback(
|
|
callback: BatchObservableCallback,
|
|
observables: Observable[],
|
|
): void {
|
|
if (!METRICS_ENABLED) return;
|
|
const result = BATCH_CALLBACKS.get(callback);
|
|
if (result && batchResultHasObservables(result, observables)) {
|
|
BATCH_CALLBACKS.delete(callback);
|
|
}
|
|
}
|
|
}
|
|
|
|
type BatchObservableCallback = (
|
|
observableResult: BatchObservableResult,
|
|
) => void | Promise<void>;
|
|
|
|
function record(
|
|
instrument: Instrument | null,
|
|
value: number,
|
|
attributes?: MetricAttributes,
|
|
) {
|
|
if (instrument === null) return;
|
|
if (attributes === undefined) {
|
|
op_otel_metric_record0(instrument, value);
|
|
} else {
|
|
const attrs = ObjectEntries(attributes);
|
|
if (attrs.length === 0) {
|
|
op_otel_metric_record0(instrument, value);
|
|
}
|
|
let i = 0;
|
|
while (i < attrs.length) {
|
|
const remaining = attrs.length - i;
|
|
if (remaining > 3) {
|
|
op_otel_metric_attribute3(
|
|
attrs.length,
|
|
attrs[i][0],
|
|
attrs[i][1],
|
|
attrs[i + 1][0],
|
|
attrs[i + 1][1],
|
|
attrs[i + 2][0],
|
|
attrs[i + 2][1],
|
|
);
|
|
i += 3;
|
|
} else if (remaining === 3) {
|
|
op_otel_metric_record3(
|
|
instrument,
|
|
value,
|
|
attrs[i][0],
|
|
attrs[i][1],
|
|
attrs[i + 1][0],
|
|
attrs[i + 1][1],
|
|
attrs[i + 2][0],
|
|
attrs[i + 2][1],
|
|
);
|
|
i += 3;
|
|
} else if (remaining === 2) {
|
|
op_otel_metric_record2(
|
|
instrument,
|
|
value,
|
|
attrs[i][0],
|
|
attrs[i][1],
|
|
attrs[i + 1][0],
|
|
attrs[i + 1][1],
|
|
);
|
|
i += 2;
|
|
} else if (remaining === 1) {
|
|
op_otel_metric_record1(instrument, value, attrs[i][0], attrs[i][1]);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function recordObservable(
|
|
instrument: Instrument | null,
|
|
value: number,
|
|
attributes?: MetricAttributes,
|
|
) {
|
|
if (instrument === null) return;
|
|
if (attributes === undefined) {
|
|
op_otel_metric_observable_record0(instrument, value);
|
|
} else {
|
|
const attrs = ObjectEntries(attributes);
|
|
if (attrs.length === 0) {
|
|
op_otel_metric_observable_record0(instrument, value);
|
|
}
|
|
let i = 0;
|
|
while (i < attrs.length) {
|
|
const remaining = attrs.length - i;
|
|
if (remaining > 3) {
|
|
op_otel_metric_attribute3(
|
|
attrs.length,
|
|
attrs[i][0],
|
|
attrs[i][1],
|
|
attrs[i + 1][0],
|
|
attrs[i + 1][1],
|
|
attrs[i + 2][0],
|
|
attrs[i + 2][1],
|
|
);
|
|
i += 3;
|
|
} else if (remaining === 3) {
|
|
op_otel_metric_observable_record3(
|
|
instrument,
|
|
value,
|
|
attrs[i][0],
|
|
attrs[i][1],
|
|
attrs[i + 1][0],
|
|
attrs[i + 1][1],
|
|
attrs[i + 2][0],
|
|
attrs[i + 2][1],
|
|
);
|
|
i += 3;
|
|
} else if (remaining === 2) {
|
|
op_otel_metric_observable_record2(
|
|
instrument,
|
|
value,
|
|
attrs[i][0],
|
|
attrs[i][1],
|
|
attrs[i + 1][0],
|
|
attrs[i + 1][1],
|
|
);
|
|
i += 2;
|
|
} else if (remaining === 1) {
|
|
op_otel_metric_observable_record1(
|
|
instrument,
|
|
value,
|
|
attrs[i][0],
|
|
attrs[i][1],
|
|
);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class Counter {
|
|
#instrument: Instrument | null;
|
|
#upDown: boolean;
|
|
|
|
constructor(instrument: Instrument | null, upDown: boolean) {
|
|
this.#instrument = instrument;
|
|
this.#upDown = upDown;
|
|
}
|
|
|
|
add(value: number, attributes?: MetricAttributes, _context?: Context): void {
|
|
if (value < 0 && !this.#upDown) {
|
|
throw new Error("Counter can only be incremented");
|
|
}
|
|
record(this.#instrument, value, attributes);
|
|
}
|
|
}
|
|
|
|
class Gauge {
|
|
#instrument: Instrument | null;
|
|
|
|
constructor(instrument: Instrument | null) {
|
|
this.#instrument = instrument;
|
|
}
|
|
|
|
record(
|
|
value: number,
|
|
attributes?: MetricAttributes,
|
|
_context?: Context,
|
|
): void {
|
|
record(this.#instrument, value, attributes);
|
|
}
|
|
}
|
|
|
|
class Histogram {
|
|
#instrument: Instrument | null;
|
|
|
|
constructor(instrument: Instrument | null) {
|
|
this.#instrument = instrument;
|
|
}
|
|
|
|
record(
|
|
value: number,
|
|
attributes?: MetricAttributes,
|
|
_context?: Context,
|
|
): void {
|
|
record(this.#instrument, value, attributes);
|
|
}
|
|
}
|
|
|
|
type ObservableCallback = (
|
|
observableResult: ObservableResult,
|
|
) => void | Promise<void>;
|
|
|
|
let getObservableResult: (observable: Observable) => ObservableResult;
|
|
|
|
class Observable {
|
|
#result: ObservableResult;
|
|
|
|
constructor(result: ObservableResult) {
|
|
this.#result = result;
|
|
}
|
|
|
|
static {
|
|
getObservableResult = (observable) => observable.#result;
|
|
}
|
|
|
|
addCallback(callback: ObservableCallback): void {
|
|
const res = INDIVIDUAL_CALLBACKS.get(this);
|
|
if (res) res.add(callback);
|
|
else INDIVIDUAL_CALLBACKS.set(this, new SafeSet([callback]));
|
|
startObserving();
|
|
}
|
|
|
|
removeCallback(callback: ObservableCallback): void {
|
|
const res = INDIVIDUAL_CALLBACKS.get(this);
|
|
if (res) res.delete(callback);
|
|
if (res?.size === 0) INDIVIDUAL_CALLBACKS.delete(this);
|
|
}
|
|
}
|
|
|
|
class ObservableResult {
|
|
#instrument: Instrument | null;
|
|
#isRegularCounter: boolean;
|
|
|
|
constructor(instrument: Instrument | null, isRegularCounter: boolean) {
|
|
this.#instrument = instrument;
|
|
this.#isRegularCounter = isRegularCounter;
|
|
}
|
|
|
|
observe(
|
|
this: ObservableResult,
|
|
value: number,
|
|
attributes?: MetricAttributes,
|
|
): void {
|
|
if (this.#isRegularCounter) {
|
|
if (value < 0) {
|
|
throw new Error("Observable counters can only be incremented");
|
|
}
|
|
}
|
|
recordObservable(this.#instrument, value, attributes);
|
|
}
|
|
}
|
|
|
|
async function observe(): Promise<void> {
|
|
if (ISOLATE_METRICS) {
|
|
op_otel_collect_isolate_metrics();
|
|
}
|
|
|
|
const promises: Promise<void>[] = [];
|
|
// Primordials are not needed, because this is a SafeMap.
|
|
// deno-lint-ignore prefer-primordials
|
|
for (const { 0: observable, 1: callbacks } of INDIVIDUAL_CALLBACKS) {
|
|
const result = getObservableResult(observable);
|
|
// Primordials are not needed, because this is a SafeSet.
|
|
// deno-lint-ignore prefer-primordials
|
|
for (const callback of callbacks) {
|
|
// PromiseTry is not in primordials?
|
|
// deno-lint-ignore prefer-primordials
|
|
ArrayPrototypePush(promises, Promise.try(callback, result));
|
|
}
|
|
}
|
|
// Primordials are not needed, because this is a SafeMap.
|
|
// deno-lint-ignore prefer-primordials
|
|
for (const { 0: callback, 1: result } of BATCH_CALLBACKS) {
|
|
// PromiseTry is not in primordials?
|
|
// deno-lint-ignore prefer-primordials
|
|
ArrayPrototypePush(promises, Promise.try(callback, result));
|
|
}
|
|
await SafePromiseAll(promises);
|
|
}
|
|
|
|
let isObserving = false;
|
|
function startObserving() {
|
|
if (!isObserving) {
|
|
isObserving = true;
|
|
(async () => {
|
|
while (true) {
|
|
const promise = op_otel_metric_wait_to_observe();
|
|
core.unrefOpPromise(promise);
|
|
const ok = await promise;
|
|
if (!ok) break;
|
|
await observe();
|
|
op_otel_metric_observation_done();
|
|
}
|
|
})();
|
|
}
|
|
}
|
|
|
|
const otelConsoleConfig = {
|
|
ignore: 0,
|
|
capture: 1,
|
|
replace: 2,
|
|
};
|
|
|
|
function otelLog(message: string, level: number) {
|
|
const currentSpan = CURRENT.get()?.getValue(SPAN_KEY);
|
|
const otelSpan = currentSpan !== undefined
|
|
? getOtelSpan(currentSpan)
|
|
: undefined;
|
|
if (otelSpan || currentSpan === undefined) {
|
|
op_otel_log(message, level, otelSpan);
|
|
} else {
|
|
const spanContext = currentSpan.spanContext();
|
|
op_otel_log_foreign(
|
|
message,
|
|
level,
|
|
spanContext.traceId,
|
|
spanContext.spanId,
|
|
spanContext.traceFlags,
|
|
);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Copyright The OpenTelemetry Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
const VERSION = "00";
|
|
const VERSION_PART = "(?!ff)[\\da-f]{2}";
|
|
const TRACE_ID_PART = "(?![0]{32})[\\da-f]{32}";
|
|
const PARENT_ID_PART = "(?![0]{16})[\\da-f]{16}";
|
|
const FLAGS_PART = "[\\da-f]{2}";
|
|
const TRACE_PARENT_REGEX = new SafeRegExp(
|
|
`^\\s?(${VERSION_PART})-(${TRACE_ID_PART})-(${PARENT_ID_PART})-(${FLAGS_PART})(-.*)?\\s?$`,
|
|
);
|
|
const VALID_TRACEID_REGEX = new SafeRegExp("^([0-9a-f]{32})$", "i");
|
|
const VALID_SPANID_REGEX = new SafeRegExp("^[0-9a-f]{16}$", "i");
|
|
const MAX_TRACE_STATE_ITEMS = 32;
|
|
const MAX_TRACE_STATE_LEN = 512;
|
|
const LIST_MEMBERS_SEPARATOR = ",";
|
|
const LIST_MEMBER_KEY_VALUE_SPLITTER = "=";
|
|
const VALID_KEY_CHAR_RANGE = "[_0-9a-z-*/]";
|
|
const VALID_KEY = `[a-z]${VALID_KEY_CHAR_RANGE}{0,255}`;
|
|
const VALID_VENDOR_KEY =
|
|
`[a-z0-9]${VALID_KEY_CHAR_RANGE}{0,240}@[a-z]${VALID_KEY_CHAR_RANGE}{0,13}`;
|
|
const VALID_KEY_REGEX = new SafeRegExp(
|
|
`^(?:${VALID_KEY}|${VALID_VENDOR_KEY})$`,
|
|
);
|
|
const VALID_VALUE_BASE_REGEX = new SafeRegExp("^[ -~]{0,255}[!-~]$");
|
|
const INVALID_VALUE_COMMA_EQUAL_REGEX = new SafeRegExp(",|=");
|
|
|
|
const TRACE_PARENT_HEADER = "traceparent";
|
|
const TRACE_STATE_HEADER = "tracestate";
|
|
const INVALID_TRACEID = "00000000000000000000000000000000";
|
|
const INVALID_SPANID = "0000000000000000";
|
|
const INVALID_SPAN_CONTEXT: SpanContext = {
|
|
traceId: INVALID_TRACEID,
|
|
spanId: INVALID_SPANID,
|
|
traceFlags: 0,
|
|
};
|
|
const BAGGAGE_KEY_PAIR_SEPARATOR = "=";
|
|
const BAGGAGE_PROPERTIES_SEPARATOR = ";";
|
|
const BAGGAGE_ITEMS_SEPARATOR = ",";
|
|
const BAGGAGE_HEADER = "baggage";
|
|
const BAGGAGE_MAX_NAME_VALUE_PAIRS = 180;
|
|
const BAGGAGE_MAX_PER_NAME_VALUE_PAIRS = 4096;
|
|
const BAGGAGE_MAX_TOTAL_LENGTH = 8192;
|
|
|
|
class NonRecordingSpan implements Span {
|
|
constructor(
|
|
private readonly _spanContext: SpanContext = INVALID_SPAN_CONTEXT,
|
|
) {}
|
|
|
|
spanContext(): SpanContext {
|
|
return this._spanContext;
|
|
}
|
|
|
|
setAttribute(_key: string, _value: unknown): this {
|
|
return this;
|
|
}
|
|
|
|
setAttributes(_attributes: SpanAttributes): this {
|
|
return this;
|
|
}
|
|
|
|
addEvent(_name: string, _attributes?: SpanAttributes): this {
|
|
return this;
|
|
}
|
|
|
|
addLink(_link: Link): this {
|
|
return this;
|
|
}
|
|
|
|
addLinks(_links: Link[]): this {
|
|
return this;
|
|
}
|
|
|
|
setStatus(_status: SpanStatus): this {
|
|
return this;
|
|
}
|
|
|
|
updateName(_name: string): this {
|
|
return this;
|
|
}
|
|
|
|
end(_endTime?: TimeInput): void {}
|
|
|
|
isRecording(): boolean {
|
|
return false;
|
|
}
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
recordException(_exception: any, _time?: TimeInput): void {}
|
|
}
|
|
|
|
const otelPropagators = {
|
|
traceContext: 0,
|
|
baggage: 1,
|
|
none: 2,
|
|
};
|
|
|
|
function parseTraceParent(traceParent: string): SpanContext | null {
|
|
const match = TRACE_PARENT_REGEX.exec(traceParent);
|
|
if (!match) return null;
|
|
|
|
// According to the specification the implementation should be compatible
|
|
// with future versions. If there are more parts, we only reject it if it's using version 00
|
|
// See https://www.w3.org/TR/trace-context/#versioning-of-traceparent
|
|
if (match[1] === "00" && match[5]) return null;
|
|
|
|
return {
|
|
traceId: match[2],
|
|
spanId: match[3],
|
|
traceFlags: NumberParseInt(match[4], 16),
|
|
};
|
|
}
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
interface TextMapSetter<Carrier = any> {
|
|
set(carrier: Carrier, key: string, value: string): void;
|
|
}
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
interface TextMapPropagator<Carrier = any> {
|
|
inject(
|
|
context: Context,
|
|
carrier: Carrier,
|
|
setter: TextMapSetter<Carrier>,
|
|
): void;
|
|
extract(
|
|
context: Context,
|
|
carrier: Carrier,
|
|
getter: TextMapGetter<Carrier>,
|
|
): Context;
|
|
fields(): string[];
|
|
}
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
interface TextMapGetter<Carrier = any> {
|
|
keys(carrier: Carrier): string[];
|
|
get(carrier: Carrier, key: string): undefined | string | string[];
|
|
}
|
|
|
|
function isTracingSuppressed(context: Context): boolean {
|
|
return context.getValue(
|
|
SymbolFor("OpenTelemetry SDK Context Key SUPPRESS_TRACING"),
|
|
) === true;
|
|
}
|
|
|
|
function isValidTraceId(traceId: string): boolean {
|
|
return VALID_TRACEID_REGEX.test(traceId) && traceId !== INVALID_TRACEID;
|
|
}
|
|
|
|
function isValidSpanId(spanId: string): boolean {
|
|
return VALID_SPANID_REGEX.test(spanId) && spanId !== INVALID_SPANID;
|
|
}
|
|
|
|
function isSpanContextValid(spanContext: SpanContext): boolean {
|
|
return (
|
|
isValidTraceId(spanContext.traceId) && isValidSpanId(spanContext.spanId)
|
|
);
|
|
}
|
|
|
|
function validateKey(key: string): boolean {
|
|
return VALID_KEY_REGEX.test(key);
|
|
}
|
|
|
|
function validateValue(value: string): boolean {
|
|
return (
|
|
VALID_VALUE_BASE_REGEX.test(value) &&
|
|
!INVALID_VALUE_COMMA_EQUAL_REGEX.test(value)
|
|
);
|
|
}
|
|
|
|
class TraceStateClass implements TraceState {
|
|
private _internalState: Map<string, string> = new SafeMap();
|
|
|
|
constructor(rawTraceState?: string) {
|
|
if (rawTraceState) this._parse(rawTraceState);
|
|
}
|
|
|
|
set(key: string, value: string): TraceStateClass {
|
|
const traceState = this._clone();
|
|
if (traceState._internalState.has(key)) {
|
|
traceState._internalState.delete(key);
|
|
}
|
|
traceState._internalState.set(key, value);
|
|
return traceState;
|
|
}
|
|
|
|
unset(key: string): TraceStateClass {
|
|
const traceState = this._clone();
|
|
traceState._internalState.delete(key);
|
|
return traceState;
|
|
}
|
|
|
|
get(key: string): string | undefined {
|
|
return this._internalState.get(key);
|
|
}
|
|
|
|
serialize(): string {
|
|
return ArrayPrototypeJoin(
|
|
ArrayPrototypeReduce(this._keys(), (agg: string[], key) => {
|
|
ArrayPrototypePush(
|
|
agg,
|
|
key + LIST_MEMBER_KEY_VALUE_SPLITTER + this.get(key),
|
|
);
|
|
return agg;
|
|
}, []),
|
|
LIST_MEMBERS_SEPARATOR,
|
|
);
|
|
}
|
|
|
|
private _parse(rawTraceState: string) {
|
|
if (rawTraceState.length > MAX_TRACE_STATE_LEN) return;
|
|
this._internalState = ArrayPrototypeReduce(
|
|
ArrayPrototypeReverse(
|
|
StringPrototypeSplit(rawTraceState, LIST_MEMBERS_SEPARATOR),
|
|
),
|
|
(agg: Map<string, string>, part: string) => {
|
|
const listMember = StringPrototypeTrim(part); // Optional Whitespace (OWS) handling
|
|
const i = StringPrototypeIndexOf(
|
|
listMember,
|
|
LIST_MEMBER_KEY_VALUE_SPLITTER,
|
|
);
|
|
if (i !== -1) {
|
|
const key = StringPrototypeSlice(listMember, 0, i);
|
|
const value = StringPrototypeSlice(listMember, i + 1, part.length);
|
|
if (validateKey(key) && validateValue(value)) {
|
|
agg.set(key, value);
|
|
}
|
|
}
|
|
return agg;
|
|
},
|
|
new SafeMap(),
|
|
);
|
|
|
|
// Because of the reverse() requirement, trunc must be done after map is created
|
|
if (this._internalState.size > MAX_TRACE_STATE_ITEMS) {
|
|
this._internalState = new SafeMap(
|
|
ArrayPrototypeSlice(
|
|
ArrayPrototypeReverse(
|
|
ArrayFrom(MapPrototypeEntries(this._internalState)),
|
|
),
|
|
0,
|
|
MAX_TRACE_STATE_ITEMS,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
private _keys(): string[] {
|
|
return ArrayPrototypeReverse(
|
|
ArrayFrom(MapPrototypeKeys(this._internalState)),
|
|
);
|
|
}
|
|
|
|
private _clone(): TraceStateClass {
|
|
const traceState = new TraceStateClass();
|
|
traceState._internalState = new SafeMap(this._internalState);
|
|
return traceState;
|
|
}
|
|
}
|
|
|
|
class W3CTraceContextPropagator implements TextMapPropagator {
|
|
inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
|
|
const spanContext = (context.getValue(SPAN_KEY) as Span | undefined)
|
|
?.spanContext();
|
|
if (
|
|
!spanContext ||
|
|
isTracingSuppressed(context) ||
|
|
!isSpanContextValid(spanContext)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const traceParent =
|
|
`${VERSION}-${spanContext.traceId}-${spanContext.spanId}-0${
|
|
NumberPrototypeToString(Number(spanContext.traceFlags || 0), 16)
|
|
}`;
|
|
|
|
setter.set(carrier, TRACE_PARENT_HEADER, traceParent);
|
|
if (spanContext.traceState) {
|
|
setter.set(
|
|
carrier,
|
|
TRACE_STATE_HEADER,
|
|
spanContext.traceState.serialize(),
|
|
);
|
|
}
|
|
}
|
|
|
|
extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
|
|
const traceParentHeader = getter.get(carrier, TRACE_PARENT_HEADER);
|
|
if (!traceParentHeader) return context;
|
|
const traceParent = ArrayIsArray(traceParentHeader)
|
|
? traceParentHeader[0]
|
|
: traceParentHeader;
|
|
if (typeof traceParent !== "string") return context;
|
|
const spanContext = parseTraceParent(traceParent);
|
|
if (!spanContext) return context;
|
|
|
|
spanContext.isRemote = true;
|
|
|
|
const traceStateHeader = getter.get(carrier, TRACE_STATE_HEADER);
|
|
if (traceStateHeader) {
|
|
// If more than one `tracestate` header is found, we merge them into a
|
|
// single header.
|
|
const state = ArrayIsArray(traceStateHeader)
|
|
? ArrayPrototypeJoin(traceStateHeader, ",")
|
|
: traceStateHeader;
|
|
spanContext.traceState = new TraceStateClass(
|
|
typeof state === "string" ? state : undefined,
|
|
);
|
|
}
|
|
return context.setValue(SPAN_KEY, new NonRecordingSpan(spanContext));
|
|
}
|
|
|
|
fields(): string[] {
|
|
return [TRACE_PARENT_HEADER, TRACE_STATE_HEADER];
|
|
}
|
|
}
|
|
|
|
const baggageEntryMetadataSymbol = SymbolFor("BaggageEntryMetadata");
|
|
|
|
type BaggageEntryMetadata = { toString(): string } & {
|
|
__TYPE__: typeof baggageEntryMetadataSymbol;
|
|
};
|
|
|
|
interface BaggageEntry {
|
|
value: string;
|
|
metadata?: BaggageEntryMetadata;
|
|
}
|
|
|
|
interface ParsedBaggageKeyValue {
|
|
key: string;
|
|
value: string;
|
|
metadata: BaggageEntryMetadata | undefined;
|
|
}
|
|
|
|
interface Baggage {
|
|
getEntry(key: string): BaggageEntry | undefined;
|
|
getAllEntries(): [string, BaggageEntry][];
|
|
setEntry(key: string, entry: BaggageEntry): Baggage;
|
|
removeEntry(key: string): Baggage;
|
|
removeEntries(...key: string[]): Baggage;
|
|
clear(): Baggage;
|
|
}
|
|
|
|
export function baggageEntryMetadataFromString(
|
|
str: string,
|
|
): BaggageEntryMetadata {
|
|
if (typeof str !== "string") {
|
|
str = "";
|
|
}
|
|
|
|
return {
|
|
__TYPE__: baggageEntryMetadataSymbol,
|
|
toString() {
|
|
return str;
|
|
},
|
|
};
|
|
}
|
|
|
|
function serializeKeyPairs(keyPairs: string[]): string {
|
|
return ArrayPrototypeReduce(keyPairs, (hValue: string, current: string) => {
|
|
const value = `${hValue}${
|
|
hValue !== "" ? BAGGAGE_ITEMS_SEPARATOR : ""
|
|
}${current}`;
|
|
return value.length > BAGGAGE_MAX_TOTAL_LENGTH ? hValue : value;
|
|
}, "");
|
|
}
|
|
|
|
function getKeyPairs(baggage: Baggage): string[] {
|
|
return ArrayPrototypeMap(baggage.getAllEntries(), (baggageEntry) => {
|
|
let entry = `${encodeURIComponent(baggageEntry[0])}=${
|
|
encodeURIComponent(baggageEntry[1].value)
|
|
}`;
|
|
|
|
// include opaque metadata if provided
|
|
// NOTE: we intentionally don't URI-encode the metadata - that responsibility falls on the metadata implementation
|
|
if (baggageEntry[1].metadata !== undefined) {
|
|
entry += BAGGAGE_PROPERTIES_SEPARATOR +
|
|
// deno-lint-ignore prefer-primordials
|
|
baggageEntry[1].metadata.toString();
|
|
}
|
|
|
|
return entry;
|
|
});
|
|
}
|
|
|
|
function parsePairKeyValue(
|
|
entry: string,
|
|
): ParsedBaggageKeyValue | undefined {
|
|
const valueProps = StringPrototypeSplit(entry, BAGGAGE_PROPERTIES_SEPARATOR);
|
|
if (valueProps.length <= 0) return;
|
|
const keyPairPart = ArrayPrototypeShift(valueProps);
|
|
if (!keyPairPart) return;
|
|
const separatorIndex = StringPrototypeIndexOf(
|
|
keyPairPart,
|
|
BAGGAGE_KEY_PAIR_SEPARATOR,
|
|
);
|
|
if (separatorIndex <= 0) return;
|
|
const key = decodeURIComponent(
|
|
StringPrototypeTrim(
|
|
StringPrototypeSubstring(keyPairPart, 0, separatorIndex),
|
|
),
|
|
);
|
|
const value = decodeURIComponent(
|
|
StringPrototypeTrim(
|
|
StringPrototypeSubstring(keyPairPart, separatorIndex + 1),
|
|
),
|
|
);
|
|
let metadata;
|
|
if (valueProps.length > 0) {
|
|
metadata = baggageEntryMetadataFromString(
|
|
ArrayPrototypeJoin(valueProps, BAGGAGE_PROPERTIES_SEPARATOR),
|
|
);
|
|
}
|
|
return { key, value, metadata };
|
|
}
|
|
|
|
class BaggageImpl implements Baggage {
|
|
#entries: Map<string, BaggageEntry>;
|
|
|
|
constructor(entries?: Map<string, BaggageEntry>) {
|
|
this.#entries = entries ? new SafeMap(entries) : new SafeMap();
|
|
}
|
|
|
|
getEntry(key: string): BaggageEntry | undefined {
|
|
const entry = this.#entries.get(key);
|
|
if (!entry) {
|
|
return undefined;
|
|
}
|
|
|
|
return ObjectAssign({}, entry);
|
|
}
|
|
|
|
getAllEntries(): [string, BaggageEntry][] {
|
|
return ArrayPrototypeMap(
|
|
ArrayFrom(MapPrototypeEntries(this.#entries)),
|
|
(entry) => [entry[0], entry[1]],
|
|
);
|
|
}
|
|
|
|
setEntry(key: string, entry: BaggageEntry): BaggageImpl {
|
|
const newBaggage = new BaggageImpl(this.#entries);
|
|
newBaggage.#entries.set(key, entry);
|
|
return newBaggage;
|
|
}
|
|
|
|
removeEntry(key: string): BaggageImpl {
|
|
const newBaggage = new BaggageImpl(this.#entries);
|
|
newBaggage.#entries.delete(key);
|
|
return newBaggage;
|
|
}
|
|
|
|
removeEntries(...keys: string[]): BaggageImpl {
|
|
const newBaggage = new BaggageImpl(this.#entries);
|
|
for (const key of new SafeArrayIterator(keys)) {
|
|
newBaggage.#entries.delete(key);
|
|
}
|
|
return newBaggage;
|
|
}
|
|
|
|
clear(): BaggageImpl {
|
|
return new BaggageImpl();
|
|
}
|
|
}
|
|
|
|
export class W3CBaggagePropagator implements TextMapPropagator {
|
|
inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
|
|
const baggage = context.getValue(baggageEntryMetadataSymbol) as
|
|
| Baggage
|
|
| undefined;
|
|
if (!baggage || isTracingSuppressed(context)) return;
|
|
const keyPairs = ArrayPrototypeSlice(
|
|
ArrayPrototypeFilter(getKeyPairs(baggage), (pair: string) => {
|
|
return pair.length <= BAGGAGE_MAX_PER_NAME_VALUE_PAIRS;
|
|
}),
|
|
0,
|
|
BAGGAGE_MAX_NAME_VALUE_PAIRS,
|
|
);
|
|
const headerValue = serializeKeyPairs(keyPairs);
|
|
if (headerValue.length > 0) {
|
|
setter.set(carrier, BAGGAGE_HEADER, headerValue);
|
|
}
|
|
}
|
|
|
|
extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
|
|
const headerValue = getter.get(carrier, BAGGAGE_HEADER);
|
|
const baggageString = ArrayIsArray(headerValue)
|
|
? ArrayPrototypeJoin(headerValue, BAGGAGE_ITEMS_SEPARATOR)
|
|
: headerValue;
|
|
if (!baggageString) return context;
|
|
const baggage: Record<string, BaggageEntry> = {};
|
|
if (baggageString.length === 0) {
|
|
return context;
|
|
}
|
|
const pairs = StringPrototypeSplit(baggageString, BAGGAGE_ITEMS_SEPARATOR);
|
|
ArrayPrototypeForEach(pairs, (entry) => {
|
|
const keyPair = parsePairKeyValue(entry);
|
|
if (keyPair) {
|
|
const baggageEntry: BaggageEntry = { value: keyPair.value };
|
|
if (keyPair.metadata) {
|
|
baggageEntry.metadata = keyPair.metadata;
|
|
}
|
|
baggage[keyPair.key] = baggageEntry;
|
|
}
|
|
});
|
|
if (ObjectEntries(baggage).length === 0) {
|
|
return context;
|
|
}
|
|
|
|
return context.setValue(
|
|
baggageEntryMetadataSymbol,
|
|
new BaggageImpl(new SafeMap(ObjectEntries(baggage))),
|
|
);
|
|
}
|
|
|
|
fields(): string[] {
|
|
return [BAGGAGE_HEADER];
|
|
}
|
|
}
|
|
|
|
let builtinTracerCache: Tracer;
|
|
|
|
export function builtinTracer(): Tracer {
|
|
if (!builtinTracerCache) {
|
|
builtinTracerCache = new Tracer(OtelTracer.builtin());
|
|
}
|
|
return builtinTracerCache;
|
|
}
|
|
|
|
function enableIsolateMetrics() {
|
|
op_otel_enable_isolate_metrics();
|
|
ISOLATE_METRICS = true;
|
|
startObserving();
|
|
}
|
|
|
|
// We specify a very high version number, to allow any `@opentelemetry/api`
|
|
// version to load this module. This does cause @opentelemetry/api to not be
|
|
// able to register anything itself with the global registration methods.
|
|
const OTEL_API_COMPAT_VERSION = "1.999.999";
|
|
|
|
export function bootstrap(
|
|
config: [
|
|
0 | 1,
|
|
0 | 1,
|
|
(typeof otelConsoleConfig)[keyof typeof otelConsoleConfig],
|
|
...Array<(typeof otelPropagators)[keyof typeof otelPropagators]>,
|
|
],
|
|
): void {
|
|
const {
|
|
0: tracingEnabled,
|
|
1: metricsEnabled,
|
|
2: consoleConfig,
|
|
...propagators
|
|
} = config;
|
|
|
|
TRACING_ENABLED = tracingEnabled === 1;
|
|
METRICS_ENABLED = metricsEnabled === 1;
|
|
|
|
PROPAGATORS = ArrayPrototypeMap(
|
|
ArrayPrototypeFilter(
|
|
ObjectValues(propagators),
|
|
(propagator) => propagator !== otelPropagators.none,
|
|
),
|
|
(propagator) => {
|
|
switch (propagator) {
|
|
case otelPropagators.traceContext:
|
|
return new W3CTraceContextPropagator();
|
|
case otelPropagators.baggage:
|
|
return new W3CBaggagePropagator();
|
|
}
|
|
},
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
if (TRACING_ENABLED || METRICS_ENABLED) {
|
|
const otel = globalThis[SymbolFor("opentelemetry.js.api.1")] ??= {
|
|
version: OTEL_API_COMPAT_VERSION,
|
|
};
|
|
if (TRACING_ENABLED) {
|
|
otel.trace = TracerProvider;
|
|
otel.context = ContextManager;
|
|
}
|
|
if (METRICS_ENABLED) {
|
|
otel.metrics = MeterProvider;
|
|
enableIsolateMetrics();
|
|
}
|
|
}
|
|
}
|
|
|
|
export const telemetry = {
|
|
tracerProvider: TracerProvider,
|
|
contextManager: ContextManager,
|
|
meterProvider: MeterProvider,
|
|
};
|