try {
const result = await api.context.with(api.trace.setSpan(ctx, span), () =>
fn(span)
);
span.setStatus({ code: api.SpanStatusCode.OK });
return result;
} catch (error) {
span.setStatus({
code: api.SpanStatusCode.ERROR,
message: error?.message,
});
span.recordException(error);
throw error;
} finally {
span.end();
}
}
// ─── Built-in metrics ────────────────────────────────────────────────────────
let _metrics = null;
/**
* Get (or lazily create) the built-in metrics instruments.
*/
export function getMetrics() {
if (_metrics) return _metrics;
const meter = getMeter();
if (meter === NOOP_METER) return null;
_metrics = {
httpRequestDuration: meter.createHistogram("http.server.request.duration", {
description: "Duration of HTTP requests",
unit: "ms",
}),
httpActiveRequests: meter.createUpDownCounter(
"http.server.active_requests",
{
description: "Number of active HTTP requests",
}
),
rscRenderDuration: meter.createHistogram(
"react_server.rsc.render.duration",
{
description: "Duration of RSC rendering",
unit: "ms",
}
),
domRenderDuration: meter.createHistogram(
"react_server.dom.render.duration",
{
description: "Duration of SSR DOM rendering",
unit: "ms",
}
),
actionDuration: meter.createHistogram(
"react_server.server_function.duration",
{
description: "Duration of server function execution",
unit: "ms",
}
),
cacheHits: meter.createCounter("react_server.cache.hits", {
description: "Number of cache hits",
}),
cacheMisses: meter.createCounter("react_server.cache.misses", {
description: "Number of cache misses",
}),
};
return _metrics;
}
// ─── SDK Initialization ──────────────────────────────────────────────────────
/**
* Resolve the telemetry config. Returns null when telemetry should be disabled.
*
* Telemetry is enabled when:
* 1. `config.telemetry.enabled` is explicitly `true`, OR
* 2. The `OTEL_EXPORTER_OTLP_ENDPOINT` env var is set, OR
* 3. The `REACT_SERVER_TELEMETRY` env var is "true"
*/
export function resolveTelemetryConfig(config) {
const env = typeof process !== "undefined" ? process.env : {};
const telemetryConfig = config?.telemetry ?? {};
// Explicit opt-out via env var (e.g. set in test runner config)
if (env.REACT_SERVER_TELEMETRY === "false") {
return null;
}
const enabled =
telemetryConfig.enabled === true ||
!!env.OTEL_EXPORTER_OTLP_ENDPOINT ||
env.REACT_SERVER_TELEMETRY === "true";
if (!enabled) return null;
const isDev = env.NODE_ENV === "development" || env.NODE_ENV === undefined;
return {
serviceName:
telemetryConfig.serviceName ??
env.OTEL_SERVICE_NAME ??
config?.name ??
"@lazarv/react-server",
endpoint:
telemetryConfig.endpoint ??
env.OTEL_EXPORTER_OTLP_ENDPOINT ??
"http://localhost:4318",
exporter:
telemetryConfig.exporter ??
(env.OTEL_EXPORTER_OTLP_ENDPOINT
? "otlp"
: isDev
? "dev-console"
: "otlp"),
sampleRate: telemetryConfig.sampleRate ?? 1.0,
propagators: telemetryConfig.propagators ?? ["w3c"],
metrics: {
enabled: telemetryConfig.metrics?.enabled !== false,
interval: telemetryConfig.metrics?.interval ?? 30000,
},
};
}
/**
* Initialize the OpenTelemetry SDK for Node.js runtime.
* Stores the tracer and meter in RuntimeContextStorage.
*
* @param {object} telemetryConfig - Resolved config from `resolveTelemetryConfig()`
* @returns {Promise<object|null>} The SDK instance, or null if not initialized
*/
export async function initTelemetry(telemetryConfig) {
if (!telemetryConfig) return null;
const logger = getRuntime(LOGGER_CONTEXT);
const isDevConsole =
telemetryConfig.exporter === "dev-console" ||
telemetryConfig.exporter === "console";
try {
// For dev-console / console exporters, use BasicTracerProvider with
// SimpleSpanProcessor so spans are exported immediately (not batched).
// This avoids importing OTLP exporters that try to connect to a collector.
if (isDevConsole) {
const [
api,
{ BasicTracerProvider, SimpleSpanProcessor, ConsoleSpanExporter },
{ resourceFromAttributes },
{ ATTR_SERVICE_NAME },
] = await Promise.all([
import("@opentelemetry/api"),
import("@opentelemetry/sdk-trace-base"),
import("@opentelemetry/resources"),
import("@opentelemetry/semantic-conventions"),
]);
// Cache the API module so synchronous helpers (makeSpanContext) work
_api = api;
runtime$(OTEL_API, api);
const resource = resourceFromAttributes({
[ATTR_SERVICE_NAME]: telemetryConfig.serviceName,
"react_server.runtime": "node",
});
let exporter;
if (telemetryConfig.exporter === "dev-console") {
const { DevConsoleSpanExporter } =
await import("./dev-trace-exporter.mjs");
exporter = new DevConsoleSpanExporter();
} else {
exporter = new ConsoleSpanExporter();
}
const provider = new BasicTracerProvider({
resource,
spanProcessors: [new SimpleSpanProcessor(exporter)],
});
api.trace.setGlobalTracerProvider(provider);
const tracer = api.trace.getTracer("@lazarv/react-server", "0.0.0");
runtime$(OTEL_TRACER, tracer);
runtime$(OTEL_SDK, provider);
const log = logger?.info?.bind(logger) ?? console.log;
log("[telemetry] OpenTelemetry initialized");
log(
`[telemetry] service=${telemetryConfig.serviceName} exporter=${telemetryConfig.exporter}`
);
return provider;
}
// For OTLP exporter, use the full NodeSDK with batch processing.
const [
{ NodeSDK },