UNPKG

@lokalise/fastify-extras

Version:

Opinionated set of fastify plugins, commonly used in Lokalise

205 lines 7.64 kB
import { SpanStatusCode, context, trace } from '@opentelemetry/api'; import fp from 'fastify-plugin'; import { FifoMap } from 'toad-cache'; /** * A FIFO map that automatically ends spans when they are evicted due to capacity limits. * This prevents span leaks when the map reaches its maximum size. */ class EvictingSpanMap { map; maxSize; constructor(maxSize) { this.maxSize = maxSize; this.map = new FifoMap(maxSize); } set(key, span) { // Check if key already exists (update case - end existing span to prevent leaks) const existingSpan = this.map.get(key); if (existingSpan !== undefined) { existingSpan.setStatus({ code: SpanStatusCode.OK, message: 'Span replaced with new span for same key', }); existingSpan.end(); this.map.set(key, span); return; } // Check if we're at capacity and will evict if (this.map.size >= this.maxSize) { // FifoMap evicts the oldest entry when at capacity // We need to end that span before it gets evicted const oldestKey = this.getOldestKey(); if (oldestKey) { const evictedSpan = this.map.get(oldestKey); if (evictedSpan) { // End the span with an error status to indicate it was evicted evictedSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'Span evicted due to capacity limits', }); evictedSpan.end(); } } } this.map.set(key, span); } get(key) { return this.map.get(key); } delete(key) { this.map.delete(key); } /** * Get the oldest key in the map (first to be evicted). * FifoMap stores keys in insertion order, so we iterate to find the first one. */ getOldestKey() { // FifoMap internally uses a linked list structure with first/last pointers // We access the internal structure to find the oldest key // biome-ignore lint/suspicious/noExplicitAny: Accessing internal FifoMap structure const internal = this.map; if (internal.first) { return internal.first.key; } return undefined; } } export class OpenTelemetryTransactionManager { isEnabled; tracer; spanMap; /** * Creates a new OpenTelemetryTransactionManager. * * @param isEnabled - Whether tracing is enabled * @param tracerName - The instrumentation scope name for the tracer. This identifies * the instrumentation library, not the service. Service identification should be * configured via OpenTelemetry SDK resource attributes (e.g., OTEL_SERVICE_NAME). * @param tracerVersion - The instrumentation scope version for the tracer. * @param maxConcurrentSpans - Maximum number of concurrent spans to track before eviction. */ constructor(isEnabled, tracerName = 'unknown-tracer', tracerVersion = '1.0.0', maxConcurrentSpans = 2000) { this.isEnabled = isEnabled; this.tracer = trace.getTracer(tracerName, tracerVersion); this.spanMap = new EvictingSpanMap(maxConcurrentSpans); } static createDisabled() { return new OpenTelemetryTransactionManager(false); } /** * @param transactionName - used for grouping similar transactions together * @param uniqueTransactionKey - used for identifying specific ongoing transaction. Must be reasonably unique to reduce possibility of collisions */ start(transactionName, uniqueTransactionKey) { if (!this.isEnabled) return; const span = this.tracer.startSpan(transactionName, { attributes: { 'transaction.type': 'background', }, }); this.spanMap.set(uniqueTransactionKey, span); } /** * @param transactionName - used for grouping similar transactions together * @param uniqueTransactionKey - used for identifying specific ongoing transaction. Must be reasonably unique to reduce possibility of collisions * @param transactionGroup - group is used for grouping related transactions with different names */ startWithGroup(transactionName, uniqueTransactionKey, transactionGroup) { if (!this.isEnabled) return; const span = this.tracer.startSpan(transactionName, { attributes: { 'transaction.type': 'background', 'transaction.group': transactionGroup, }, }); this.spanMap.set(uniqueTransactionKey, span); } stop(uniqueTransactionKey, wasSuccessful = true) { if (!this.isEnabled) return; const span = this.spanMap.get(uniqueTransactionKey) ?? null; if (!span) return; if (!wasSuccessful) { span.setStatus({ code: SpanStatusCode.ERROR }); } else { span.setStatus({ code: SpanStatusCode.OK }); } span.end(); this.spanMap.delete(uniqueTransactionKey); } addCustomAttribute(attrName, attrValue) { if (!this.isEnabled) return; const activeSpan = trace.getActiveSpan(); if (activeSpan) { activeSpan.setAttribute(attrName, attrValue); } } addCustomAttributes(uniqueTransactionKey, atts) { if (!this.isEnabled) return; const span = this.spanMap.get(uniqueTransactionKey); if (!span) return; for (const [key, value] of Object.entries(atts)) { span.setAttribute(key, value); } } setUserID(userId) { if (!this.isEnabled) return; const activeSpan = trace.getActiveSpan(); if (activeSpan) { activeSpan.setAttribute('enduser.id', userId); } } setControllerName(name, action) { if (!this.isEnabled) return; const activeSpan = trace.getActiveSpan(); if (activeSpan) { activeSpan.setAttribute('code.namespace', name); activeSpan.setAttribute('code.function', action); } } /** * Get a span by its unique transaction key. Useful for advanced use cases * where direct span manipulation is needed. */ getSpan(uniqueTransactionKey) { if (!this.isEnabled) return null; return this.spanMap.get(uniqueTransactionKey) ?? null; } /** * Get the underlying tracer for advanced use cases. */ getTracer() { return this.tracer; } /** * Run a function within the context of a specific span. * Useful when you need child spans to be automatically linked to a parent. */ runInSpanContext(uniqueTransactionKey, fn) { if (!this.isEnabled) return fn(); const span = this.spanMap.get(uniqueTransactionKey); if (!span) return fn(); return context.with(trace.setSpan(context.active(), span), fn); } } function plugin(fastify, opts) { const manager = new OpenTelemetryTransactionManager(opts.isEnabled, opts.tracerName, opts.tracerVersion, opts.maxConcurrentSpans); fastify.decorate('openTelemetryTransactionManager', manager); } export const openTelemetryTransactionManagerPlugin = fp(plugin, { fastify: '5.x', name: 'opentelemetry-transaction-manager-plugin', }); //# sourceMappingURL=openTelemetryTransactionManagerPlugin.js.map