Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/otel-nestjs-instrumentation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './otel-context-guard';
export * from './otel-nestjs-event';
export * from './otel-nestjs-instrumentation.module';
export * from './otel.interceptor';
export * from './start-otel-instrumentation-if-absent';
39 changes: 8 additions & 31 deletions libs/otel-nestjs-instrumentation/src/otel-context-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import {
Injectable,
} from '@nestjs/common';
import { EventEmitter } from 'stream';
import {
emitterSymbol,
getTransactionName,
InternalContext,
otelInstrumentation,
} from './internal';
import { emitterSymbol, InternalContext } from './internal';
import { startOtelInstrumentationIfAbsent } from './start-otel-instrumentation-if-absent';

/**
* NestJS guard that sets up OpenTelemetry span context for requests.
Expand Down Expand Up @@ -85,12 +81,11 @@ export class OtelContextGuard implements CanActivate {
/**
* Guard method that sets up OpenTelemetry span context for the request.
*
* This method:
* 1. Generates a descriptive span name from the execution context
* 2. Attempts to extract existing span context or creates a new span
* 3. Sets up async local storage with span information
* 4. Emits appropriate events for monitoring
* 5. Always returns true to allow request processing
* This method delegates to `startOtelInstrumentationIfAbsent` which handles:
* 1. Generating a descriptive span name from the execution context
* 2. Attempting to extract existing span context or creating a new span
* 3. Setting up async local storage with span information
* 4. Emitting appropriate events for monitoring
*
* The guard never blocks requests - if OpenTelemetry setup fails,
* the request continues without instrumentation.
Expand All @@ -99,25 +94,7 @@ export class OtelContextGuard implements CanActivate {
* @returns Always returns true to allow request processing
*/
canActivate(context: ExecutionContext) {
const transactionName = getTransactionName(context);

try {
let traceId = otelInstrumentation.getCurrentTransactionId();

if (traceId) return true;

traceId = otelInstrumentation.create(transactionName, context);

if (traceId) {
this.context.customTransactionId = traceId;
// New span was created
this.emitter.emit('spanStarted', traceId, context);
}
} catch (error) {
this.emitter.emit('spanStartFailed', error);
}

// Always return true - we never want to block requests due to instrumentation issues
startOtelInstrumentationIfAbsent(context, this.context, this.emitter);
return true;
}
}
15 changes: 10 additions & 5 deletions libs/otel-nestjs-instrumentation/src/otel.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './internal';
import EventEmitter from 'events';
import otel, { Span } from '@opentelemetry/api';
import { startOtelInstrumentationIfAbsent } from './start-otel-instrumentation-if-absent';

/**
* NestJS interceptor that manages OpenTelemetry span lifecycle.
Expand All @@ -31,6 +32,7 @@ import otel, { Span } from '@opentelemetry/api';
* - Handles both successful responses and errors
* - Records exceptions in spans for error tracking
* - Prevents duplicate span ending for custom spans
* - Serves as a fallback for RPC contexts where the guard may not run
*
* This is particularly important for:
* - SQS and Kafka consumers where span timing matters
Expand Down Expand Up @@ -88,11 +90,13 @@ export class OtelInterceptor implements NestInterceptor {
* Intercept method that manages the span lifecycle.
*
* This method:
* 1. Sets up span completion handling for both success and error cases
* 2. Records exceptions in spans when errors occur
* 3. Calls span finalizers to properly end spans
* 4. Emits appropriate events for monitoring
* 5. Ensures spans are marked with correct status codes
* 1. Calls `startOtelInstrumentationIfAbsent` as a fallback for RPC contexts
* (guards only work for HTTP requests, not RPC/microservice calls)
* 2. Sets up span completion handling for both success and error cases
* 3. Records exceptions in spans when errors occur
* 4. Calls span finalizers to properly end spans
* 5. Emits appropriate events for monitoring
* 6. Ensures spans are marked with correct status codes
*
* The interceptor works with the async context established by the guard
* to access span information and manage its lifecycle.
Expand All @@ -102,6 +106,7 @@ export class OtelInterceptor implements NestInterceptor {
* @returns Observable that completes when the request is finished
*/
intercept(context: ExecutionContext, next: CallHandler) {
startOtelInstrumentationIfAbsent(context, this.context, this.emitter);
const span = otel.trace.getActiveSpan();
if (!span) return next.handle();
const traceId = span.spanContext().traceId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ExecutionContext } from '@nestjs/common';
import { EventEmitter } from 'stream';

Check warning on line 2 in libs/otel-nestjs-instrumentation/src/start-otel-instrumentation-if-absent.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `node:stream` over `stream`.

See more on https://sonarcloud.io/project/issues?id=codibre_nestjs-context&issues=AZ_sKzCMfpbeqx5_SMvC&open=AZ_sKzCMfpbeqx5_SMvC&pullRequest=9
import {
getTransactionName,
InternalContext,
otelInstrumentation,
} from './internal';

/**
* Start a new OpenTelemetry span if one is not already active.
* @param context ExecutionContext to use
* @param internalContext The async local storage context
* @param emitter Event emitter for monitoring
*/
export function startOtelInstrumentationIfAbsent(
context: ExecutionContext,
internalContext: InternalContext,
emitter: EventEmitter,
): void {
// If a span is already active, don't create another one
const existingTraceId = otelInstrumentation.getCurrentTransactionId();
if (existingTraceId) return;

let traceId: string | undefined;
const transactionName = getTransactionName(context);

try {
traceId = otelInstrumentation.create(transactionName, context);
} catch (error) {
emitter.emit('spanStartFailed', error);
return;
}

if (!traceId) return;

internalContext.customTransactionId = traceId;
emitter.emit('spanStarted', traceId, context);
}
Loading
Loading