src — telemetry
src — telemetry
The src/telemetry/otel-tracer.ts module provides a lightweight, dependency-free implementation of an OpenTelemetry (OTel) tracer. Its primary purpose is to capture and export trace spans to an OTLP-compatible endpoint using HTTP JSON, without requiring the full @opentelemetry/* SDKs.
This module is designed for scenarios where:
- Minimizing dependencies is crucial.
- Fine-grained control over span creation and export is desired.
- The tracing needs are primarily focused on spans (not metrics or logs).
- Integration with existing OTLP collectors (like Jaeger, Tempo, or custom solutions) is required.
Core Concepts
The module implements a subset of OpenTelemetry tracing concepts:
- Spans: Represent a single operation within a trace. Each span has a name, start/end times, attributes (key-value pairs), and a status (OK, Error, Unset). Spans are identified by a
spanIdand belong to atraceId. - OTLP (OpenTelemetry Protocol): The standard for transmitting telemetry data. This tracer specifically uses the OTLP/HTTP JSON format for exporting trace spans.
OtelTracer Class
The OtelTracer class is the central component of this module, responsible for creating, buffering, and exporting spans.
Initialization and Configuration
An OtelTracer instance is configured via the OtelTracerConfig interface, environment variables, or CLI flags.
interface OtelTracerConfig {
/** OTLP HTTP endpoint (e.g., http:class="hl-cmt">//localhost:4318/v1/traces) */
endpoint?: string;
/** Service name reported in telemetry */
serviceName?: string;
/** Enable/disable the tracer */
enabled?: boolean;
/** Flush interval in milliseconds (default: 30000) */
flushIntervalMs?: number;
/** Maximum buffer size before auto-flush (default: 100) */
maxBufferSize?: number;
}
The constructor prioritizes configuration in the following order: config object > OTEL_ENDPOINT environment variable > default values.
import { OtelTracer, getOtelTracer } from 39;./telemetry/otel-tracer.ts39;;
class="hl-cmt">// Via constructor config
const tracer = new OtelTracer({
endpoint: 39;http:class="hl-cmt">//localhost:4318/v1/traces39;,
serviceName: 39;my-app39;,
flushIntervalMs: 10000,
});
class="hl-cmt">// Via singleton (recommended for app-wide use)
const appTracer = getOtelTracer({
endpoint: process.env.OTEL_ENDPOINT || 39;http:class="hl-cmt">//localhost:4318/v1/traces39;,
serviceName: 39;codebuddy39;,
});
If an endpoint is provided and the tracer is enabled, a setInterval timer is started to periodically call flush() at the specified flushIntervalMs. This timer is unref()'d to prevent it from keeping the Node.js process alive.
Span Lifecycle
The OtelTracer manages the creation, completion, and buffering of spans.
startSpan(name, attributes?):
- Creates a new
OtelSpanobject. - Assigns a unique
spanIdand the currenttraceId. - Records
startTimeUnixNano. - Initializes
kindtoINTERNAL(0) andstatustoUNSET(0). - Returns the
OtelSpanobject. The caller is responsible for holding onto this object and callingendSpanwhen the operation completes.
endSpan(span, status?):
- Records
endTimeUnixNanofor the providedspan. - Updates the
span.statusif provided. - If the tracer is enabled, the completed
spanis added to an internalbuffer. - If the
buffer.lengthreachesmaxBufferSize, an automaticflush()is triggered.
import { getOtelTracer } from 39;./telemetry/otel-tracer.ts39;;
const tracer = getOtelTracer();
async function performOperation() {
const span = tracer.startSpan(39;my.operation39;, { 39;operation.type39;: 39;async39; });
try {
class="hl-cmt">// Simulate work
await new Promise(resolve => setTimeout(resolve, 100));
tracer.endSpan(span, { code: 1 }); class="hl-cmt">// OK
} catch (error) {
tracer.endSpan(span, { code: 2, message: String(error) }); class="hl-cmt">// ERROR
throw error;
}
}
Data Export (flush)
The flush() method is responsible for sending buffered spans to the configured OTLP endpoint.
sequenceDiagram
participant App as Application Code
participant Tracer as OtelTracer Instance
participant Buffer as Internal Buffer
participant OTLP as OTLP Endpoint
App->>Tracer: startSpan("operation")
Tracer->>App: OtelSpan object
App->>Tracer: endSpan(OtelSpan, status)
Tracer->>Buffer: Add OtelSpan
alt Buffer full OR Flush Interval
Tracer->>Tracer: flush()
Tracer->>Buffer: Retrieve spans
Tracer->>OTLP: POST /v1/traces (JSON payload)
OTLP-->>Tracer: HTTP 200 OK
Tracer->>Buffer: Clear sent spans
else Export Failed
OTLP--xTracer: HTTP Error / Network Error
Tracer->>Buffer: Re-add spans (with limits)
end
App->>Tracer: dispose()
Tracer->>Buffer: Flush remaining spans
Tracer->>OTLP: POST /v1/traces
- It constructs an OTLP-compliant JSON payload containing all buffered spans, along with
service.nameandscopeattributes. - It uses
fetchto send the payload via HTTP POST. - Error handling: If the
fetchrequest fails (network error or non-2xx HTTP status), the spans are re-added to the buffer for a potential retry. To prevent unbounded memory growth, the buffer size is capped atmaxBufferSize * 2during retries. - The
flush()method is called periodically by asetIntervaltimer (if enabled) and automatically when the buffer reachesmaxBufferSize. It can also be called manually.
Convenience Tracing Methods
The OtelTracer provides specialized methods for common tracing patterns, which create, populate, and immediately buffer a span:
traceApiCall(model, tokens, duration): Records an LLM API call.- Sets
nametollm.api_call. - Adds attributes:
llm.model,llm.tokens,llm.duration_ms. - Sets
kindtoCLIENT(2) andstatustoOK(1). traceToolExecution(toolName, duration, success): Records a tool execution.- Sets
nametotool.execute. - Adds attributes:
tool.name,tool.duration_ms,tool.success. - Sets
statusbased onsuccess(OK or ERROR). traceConversation(sessionId, messageCount): Records a conversation turn.- Sets
nametoconversation.turn. - Adds attributes:
session.id,conversation.message_count. - Sets
statustoOK(1).
These methods simplify tracing by handling startSpan, attribute setting, endTimeUnixNano, and endSpan (buffering) in a single call.
Trace Management
newTrace(): Generates a newtraceIdand sets it as thecurrentTraceIdfor subsequent spans. This effectively starts a new trace.
Properties
pendingSpans: A getter that returns the current number of spans in the internal buffer.isEnabled: A getter that indicates whether the tracer is currently enabled.
Disposal
dispose(): Clears the periodicflushTimerand performs a finalflush()to ensure all remaining buffered spans are sent before the application exits or the tracer is no longer needed.
Helper Functions
The module includes several internal helper functions:
generateId(bytes): Generates cryptographically strong random hexadecimal strings, used fortraceId(16 bytes) andspanId(8 bytes).nowNano(): Returns the current time in nanoseconds since the Unix epoch. It combinesDate.now()(milliseconds) withprocess.hrtime()(nanoseconds) for high precision.toAttribute(key, value): Converts a JavaScript primitive (string,number,boolean) into theOtelAttributeformat required for OTLP.
Singleton Access
To ensure consistent tracing across an application, the module provides a singleton pattern:
getOtelTracer(config?): Returns the single, application-wideOtelTracerinstance. If no instance exists, it creates one with the providedconfig. Subsequent calls return the same instance.resetOtelTracer(): Primarily for testing or scenarios requiring a fresh tracer instance. It callsdispose()on the current instance and clears the singleton reference, allowinggetOtelTracer()to create a new one.
Example Usage
import { getOtelTracer, OtelSpanStatus } from 39;./telemetry/otel-tracer.ts39;;
class="hl-cmt">// Get the singleton tracer instance
const tracer = getOtelTracer({
endpoint: 39;http:class="hl-cmt">//localhost:4318/v1/traces39;,
serviceName: 39;my-codebuddy-app39;,
});
async function processRequest(requestId: string) {
class="hl-cmt">// Start a new trace for this request
tracer.newTrace();
class="hl-cmt">// Trace a high-level operation
const requestSpan = tracer.startSpan(39;request.process39;, { 39;request.id39;: requestId });
try {
class="hl-cmt">// Trace an LLM API call using the convenience method
tracer.traceApiCall(39;gpt-439;, 1500, 2500); class="hl-cmt">// 1500 tokens, 2500ms duration
class="hl-cmt">// Trace a tool execution
const toolSuccess = Math.random() > 0.1; class="hl-cmt">// 90% success rate
tracer.traceToolExecution(39;search_tool39;, 500, toolSuccess);
class="hl-cmt">// Trace a conversation turn
tracer.traceConversation(39;session-abc39;, 5);
class="hl-cmt">// End the main request span
tracer.endSpan(requestSpan, { code: 1 }); class="hl-cmt">// OK
} catch (error) {
tracer.endSpan(requestSpan, { code: 2, message: String(error) }); class="hl-cmt">// ERROR
}
}
class="hl-cmt">// Example call
processRequest(39;req-12339;).catch(console.error);
class="hl-cmt">// In a shutdown hook or before process exit, ensure all spans are flushed
process.on(39;beforeExit39;, async () => {
console.log(`Flushing ${tracer.pendingSpans} pending spans...`);
await tracer.dispose();
console.log(39;Tracer disposed.39;);
});