@lokalise/fastify-extras
Version:
Opinionated set of fastify plugins, commonly used in Lokalise
205 lines • 7.64 kB
JavaScript
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