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
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@
*
* @param transactionName - The name for the span
* @param context - The NestJS execution context
* @returns Object containing trace ID, span ID, and optional finalizer function
* @param effectiveType When provided, overrides automatic type detection
* (e.g., 'rpc' from interceptor fallback)
* @returns Trace ID or undefined if tracer is unavailable
*/
create(transactionName: string, context: ExecutionContext) {
create(
transactionName: string,
context: ExecutionContext,
effectiveType?: 'http' | 'rpc',
) {
// Create a new span since none exists
const tracer = otel.trace.getTracer(tracerName);
if (!tracer) return undefined;
Expand All @@ -54,11 +60,13 @@
// Use default context if extraction fails
}

// Determine span kind and attributes based on context type
// Determine span kind and attributes based on effective type
let spanKind = otel.SpanKind.INTERNAL; // default for unknown contexts
const attributes: Record<string, string> = {};

if (context.getType() === 'http') {
effectiveType ??= context.getType() as 'http' | 'rpc';

Check failure on line 67 in libs/otel-nestjs-instrumentation/src/internal/otel-instrumentation.ts

View workflow job for this annotation

GitHub Actions / CI - Build and Test

This assertion is unnecessary since it does not change the type of the expression

if (effectiveType === 'http') {
spanKind = otel.SpanKind.SERVER;
try {
const request = context.switchToHttp().getRequest<{
Expand All @@ -74,10 +82,10 @@
} catch {
// Ignore request extraction errors
}
} else if (context.getType() === 'rpc') {
} else if (effectiveType === 'rpc') {
spanKind = otel.SpanKind.SERVER;
try {
attributes['rpc.method'] = context.getHandler()?.name || 'Call';
attributes['rpc.method'] = context.getHandler()?.name ?? 'Call';
} catch {
// Ignore RPC context extraction errors
}
Expand Down
9 changes: 8 additions & 1 deletion libs/otel-nestjs-instrumentation/src/otel.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,14 @@ 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);
// If no guard ran (e.g. gRPC/microservice), force RPC since HTTP spans
// would have been started by the guard already.
startOtelInstrumentationIfAbsent(
context,
this.context,
this.emitter,
'rpc',
);
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
Expand Up @@ -11,11 +11,14 @@ import {
* @param context ExecutionContext to use
* @param internalContext The async local storage context
* @param emitter Event emitter for monitoring
* @param effectiveType When provided, overrides automatic type detection
* (e.g., 'rpc' when called from interceptor as fallback for non-HTTP transports)
*/
export function startOtelInstrumentationIfAbsent(
context: ExecutionContext,
internalContext: InternalContext,
emitter: EventEmitter,
effectiveType?: 'http' | 'rpc',
): void {
// If a span is already active, don't create another one
const existingTraceId = otelInstrumentation.getCurrentTransactionId();
Expand All @@ -25,7 +28,11 @@ export function startOtelInstrumentationIfAbsent(
const transactionName = getTransactionName(context);

try {
traceId = otelInstrumentation.create(transactionName, context);
traceId = otelInstrumentation.create(
transactionName,
context,
effectiveType,
);
} catch (error) {
emitter.emit('spanStartFailed', error);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ describe('OtelInstrumentation', () => {
{
kind: mockOtelApi.SpanKind.SERVER,
attributes: {
'rpc.method': 'Call', // Fallback when no name
'rpc.method': '', // Empty string preserved by ?? (not null/undefined)
'nestjs.controller': 'RpcController',
'nestjs.handler': 'unknown', // Falls back to 'unknown' when name is empty
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ describe('OtelContextGuard', () => {
expect(mockOtelInstrumentation.create).toHaveBeenCalledWith(
expect.any(String),
mockExecutionContext,
undefined,
);
});

Expand Down Expand Up @@ -182,6 +183,7 @@ describe('OtelContextGuard', () => {
expect(mockOtelInstrumentation.create).toHaveBeenCalledWith(
expect.stringContaining('TestController.testMethod'),
mockContext,
undefined,
);
});

Expand All @@ -200,6 +202,7 @@ describe('OtelContextGuard', () => {
expect(mockOtelInstrumentation.create).toHaveBeenCalledWith(
expect.stringContaining('TestController.processMessage'),
mockContext,
undefined,
);
});

Expand All @@ -216,6 +219,7 @@ describe('OtelContextGuard', () => {
expect(mockOtelInstrumentation.create).toHaveBeenCalledWith(
expect.stringContaining('TestController.testMethod'),
mockContext,
undefined,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ describe('OtelInterceptor', () => {
expect(mockOtelInstrumentation.create).toHaveBeenCalledWith(
expect.stringContaining('TestController'),
mockExecutionContext,
'rpc', // interceptor fallback forces RPC type
);
expect(mockEmitter.emit).toHaveBeenCalledWith(
'spanStarted',
Expand Down
Loading