@valkey/valkey-glide
Version:
General Language Independent Driver for the Enterprise (GLIDE) for Valkey
287 lines (286 loc) • 12.6 kB
JavaScript
"use strict";
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenTelemetry = void 0;
const _1 = require(".");
/**
* ⚠️ OpenTelemetry can only be initialized once per process. Calling `OpenTelemetry.init()` more than once will be ignored.
* If you need to change configuration, restart the process with new settings.
* ### OpenTelemetry
*
* - **openTelemetryConfig**: Use {@link GlideOpenTelemetryConfig} to configure OpenTelemetry exporters and options.
* - **traces**: (optional) Configure trace exporting.
* - **endpoint**: The collector endpoint for traces. Supported protocols:
* - `http://` or `https://` for HTTP/HTTPS
* - `grpc://` for gRPC
* - `file://` for local file export (see below)
* - **samplePercentage**: (optional) The percentage of requests to sample and create a span for, used to measure command duration. Must be between 0 and 100. Defaults to 1 if not specified.
* Note: There is a tradeoff between sampling percentage and performance. Higher sampling percentages will provide more detailed telemetry data but will impact performance.
* It is recommended to keep this number low (1-5%) in production environments unless you have specific needs for higher sampling rates.
* - **metrics**: (optional) Configure metrics exporting.
* - **endpoint**: The collector endpoint for metrics. Same protocol rules as above.
* - **flushIntervalMs**: (optional) Interval in milliseconds for flushing data to the collector. Must be a positive integer. Defaults to 5000ms if not specified.
* - **parentSpanContextProvider**: (optional) Callback returning the active parent span context. See {@link GlideOpenTelemetryConfig.parentSpanContextProvider}.
*
* #### File Exporter Details
* - For `file://` endpoints:
* - The path must start with `file://` (e.g., `file:///tmp/otel` or `file:///tmp/otel/traces.json`).
* - If the path is a directory or lacks a file extension, data is written to `signals.json` in that directory.
* - If the path includes a filename with an extension, that file is used as-is.
* - The parent directory must already exist; otherwise, initialization will fail with an InvalidInput error.
* - If the target file exists, new data is appended (not overwritten).
*
* #### Validation Rules
* - `flushIntervalMs` must be a positive integer.
* - `samplePercentage` must be between 0 and 100.
* - File exporter paths must start with `file://` and have an existing parent directory.
* - Invalid configuration will throw an error synchronously when calling `OpenTelemetry.init()`.
*/
class OpenTelemetry {
static _instance = null;
static openTelemetryConfig = null;
static spanContextFn = null;
static TRACE_ID_REGEX = /^[0-9a-f]{32}$/;
static SPAN_ID_REGEX = /^[0-9a-f]{16}$/;
/**
* Singleton class for managing OpenTelemetry configuration and operations.
* This class provides a centralized way to initialize OpenTelemetry and control
* sampling behavior at runtime.
*
* Example usage:
* ```typescript
* import { OpenTelemetry, GlideOpenTelemetryConfig } from "@valkey/valkey-glide";
* import { trace } from "@opentelemetry/api";
*
* const config: GlideOpenTelemetryConfig = {
* traces: {
* endpoint: "http://localhost:4318/v1/traces",
* samplePercentage: 10,
* },
* metrics: {
* endpoint: "http://localhost:4318/v1/metrics",
* },
* flushIntervalMs: 1000,
* parentSpanContextProvider: () => {
* const span = trace.getActiveSpan();
* if (!span) return undefined;
* const ctx = span.spanContext();
* return { traceId: ctx.traceId, spanId: ctx.spanId, traceFlags: ctx.traceFlags };
* },
* };
* OpenTelemetry.init(config);
* ```
*
* @remarks
* OpenTelemetry can only be initialized once per process. Subsequent calls to
* init() will be ignored. This is by design, as OpenTelemetry is a global
* resource that should be configured once at application startup.
*
* Initialize the OpenTelemetry instance
* @param openTelemetryConfig - The OpenTelemetry configuration
*/
static init(openTelemetryConfig) {
if (!this._instance) {
const { parentSpanContextProvider, ...nativeConfig } = openTelemetryConfig;
this.internalInit(nativeConfig, parentSpanContextProvider);
_1.Logger.log("info", "GlideOpenTelemetry", "OpenTelemetry initialized with config: " +
JSON.stringify(nativeConfig) +
(parentSpanContextProvider
? " (parentSpanContextProvider: set)"
: ""));
return;
}
_1.Logger.log("warn", "GlideOpenTelemetry", "OpenTelemetry already initialized - ignoring new configuration");
}
static internalInit(nativeConfig, parentSpanContextProvider) {
this.openTelemetryConfig = nativeConfig;
if (parentSpanContextProvider) {
this.spanContextFn = parentSpanContextProvider;
}
(0, _1.InitOpenTelemetry)(nativeConfig);
this._instance = new OpenTelemetry();
}
/**
* Check if the OpenTelemetry instance is initialized
* @returns True if the OpenTelemetry instance is initialized, false otherwise
*/
static isInitialized() {
return this._instance != null;
}
/**
* Get the sample percentage for traces
* @returns The sample percentage for traces only if OpenTelemetry is initialized and the traces config is set, otherwise undefined.
*/
static getSamplePercentage() {
return this.openTelemetryConfig?.traces?.samplePercentage;
}
/**
* Determines if the current request should be sampled for OpenTelemetry tracing.
* Uses the configured sample percentage to randomly decide whether to create a span for this request.
* @returns true if the request should be sampled, false otherwise
*/
static shouldSample() {
const percentage = this.getSamplePercentage();
return (this.isInitialized() &&
percentage !== undefined &&
Math.random() * 100 < percentage);
}
/**
* Set the percentage of requests to be sampled and traced. Must be a value between 0 and 100.
* This setting only affects traces, not metrics.
* @param percentage - The sample percentage 0-100
* @throws Error if OpenTelemetry is not initialized or traces config is not set
* @remarks
* This method can be called at runtime to change the sampling percentage without reinitializing OpenTelemetry.
*/
static setSamplePercentage(percentage) {
if (!this.openTelemetryConfig || !this.openTelemetryConfig.traces) {
throw new _1.ConfigurationError("OpenTelemetry config traces not initialized");
}
if (percentage < 0 || percentage > 100) {
throw new _1.ConfigurationError("Sample percentage must be between 0 and 100");
}
this.openTelemetryConfig.traces.samplePercentage = percentage;
}
/**
* Register or replace the callback that returns the active parent span context.
*
* This allows changing the provider at runtime (e.g., switching tracing contexts
* in a multi-tenant application). The initial provider can also be set via
* {@link GlideOpenTelemetryConfig.parentSpanContextProvider} in `init()`.
*
* @param fn - A function returning a `GlideSpanContext` or `undefined`, or `null` to clear.
*
* @example
* ```typescript
* import { trace } from "@opentelemetry/api";
*
* OpenTelemetry.setParentSpanContextProvider(() => {
* const span = trace.getActiveSpan();
* if (!span) return undefined;
* const ctx = span.spanContext();
* return {
* traceId: ctx.traceId,
* spanId: ctx.spanId,
* traceFlags: ctx.traceFlags,
* traceState: ctx.traceState?.toString(),
* };
* });
* ```
*/
static setParentSpanContextProvider(fn) {
this.spanContextFn = fn;
}
/**
* Retrieve the current parent span context by invoking the registered callback.
*
* @returns The `GlideSpanContext` from the registered callback, or `undefined` if no callback
* is set or the callback returns `undefined`.
* @internal
*/
static getParentSpanContext() {
let ctx;
try {
ctx = this.spanContextFn?.();
}
catch (e) {
_1.Logger.log("warn", "GlideOpenTelemetry", `parentSpanContextProvider threw: ${e}. Falling back to standalone span.`);
return undefined;
}
if (ctx === undefined) {
return undefined;
}
if (!this.TRACE_ID_REGEX.test(ctx.traceId)) {
_1.Logger.log("warn", "GlideOpenTelemetry", `Invalid traceId "${ctx.traceId}" — expected 32 lowercase hex chars. Falling back to standalone span.`);
return undefined;
}
if (!this.SPAN_ID_REGEX.test(ctx.spanId)) {
_1.Logger.log("warn", "GlideOpenTelemetry", `Invalid spanId "${ctx.spanId}" — expected 16 lowercase hex chars. Falling back to standalone span.`);
return undefined;
}
if (!Number.isInteger(ctx.traceFlags) ||
ctx.traceFlags < 0 ||
ctx.traceFlags > 255) {
_1.Logger.log("warn", "GlideOpenTelemetry", `Invalid traceFlags "${ctx.traceFlags}" — expected integer 0-255. Falling back to standalone span.`);
return undefined;
}
if (ctx.traceState !== undefined &&
!OpenTelemetry.validTraceState(ctx.traceState)) {
_1.Logger.log("warn", "GlideOpenTelemetry", `Invalid traceState "${ctx.traceState}" — expected W3C tracestate format. Falling back to standalone span.`);
return undefined;
}
return ctx;
}
/**
* Validate a W3C tracestate key.
* See https://www.w3.org/TR/trace-context/#key
* @internal
*/
static validTraceStateKey(key) {
if (key.length === 0 || key.length > 256)
return false;
const ALLOWED_SPECIAL = new Set([
"_".charCodeAt(0),
"-".charCodeAt(0),
"*".charCodeAt(0),
"/".charCodeAt(0),
]);
let vendorStart = null;
for (let i = 0; i < key.length; i++) {
const c = key.charCodeAt(i);
const isLower = c >= 0x61 && c <= 0x7a; // a-z
const isDigit = c >= 0x30 && c <= 0x39; // 0-9
const isAt = c === 0x40; // @
if (!(isLower || isDigit || ALLOWED_SPECIAL.has(c) || isAt)) {
return false;
}
if (i === 0 && !isLower && !isDigit)
return false;
if (isAt) {
if (vendorStart !== null || i + 14 < key.length)
return false;
vendorStart = i;
}
else if (vendorStart !== null && i === vendorStart + 1) {
if (!isLower && !isDigit)
return false;
}
}
return true;
}
/**
* Validate a W3C tracestate value.
* See https://www.w3.org/TR/trace-context/#value
* @internal
*/
static validTraceStateValue(value) {
if (value.length > 256)
return false;
return !value.includes(",") && !value.includes("=");
}
/**
* Validate a W3C tracestate string (comma-separated key=value pairs).
* Mirrors the validation in opentelemetry-rust TraceState::from_str.
* @internal
*/
static validTraceState(traceState) {
if (traceState === "")
return true;
const entries = traceState.split(",");
for (const entry of entries) {
const eqIndex = entry.indexOf("=");
if (eqIndex === -1)
return false;
const key = entry.substring(0, eqIndex);
const value = entry.substring(eqIndex + 1);
if (!OpenTelemetry.validTraceStateKey(key) ||
!OpenTelemetry.validTraceStateValue(value)) {
return false;
}
}
return true;
}
}
exports.OpenTelemetry = OpenTelemetry;