deno/runtime/js/telemetry.js
snek 4e899d48cf
fix: otel resiliency (#26857)
Improving the breadth of collected data, and ensuring that the collected
data is more likely to be successfully reported.

- Use `log` crate in more places
- Hook up `log` crate to otel
- Switch to process-wide otel processors
- Handle places that use `process::exit`

Also adds a more robust testing framework, with a deterministic tracing
setting.

Refs: https://github.com/denoland/deno/issues/26852
2024-11-14 12:16:28 +00:00

410 lines
8.8 KiB
JavaScript

// 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 = {};