diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.constants.ts b/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.constants.ts index ac3e98eda8..3056b5bcb1 100644 --- a/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.constants.ts +++ b/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.constants.ts @@ -1 +1,2 @@ +export const TRACER_NAME = '@cpn-console/server-nestjs' export const SERVICE_NAME = 'console-pi-native-console' diff --git a/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.decorator.ts b/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.decorator.ts new file mode 100644 index 0000000000..98a1eff00c --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.decorator.ts @@ -0,0 +1,74 @@ +import { SpanStatusCode, trace } from '@opentelemetry/api' +import type { SpanOptions, Span as OpenTelemetrySpan } from '@opentelemetry/api' +import { TRACER_NAME } from './telemetry.constants' + +export type TypedMethodDecorator = any>( + target: object, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, +) => void + +export function StartActiveSpan(options?: SpanOptions): TypedMethodDecorator { + return any>( + _target: object, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, + ): void => { + const original = descriptor.value + if (!original) return + + descriptor.value = function (this: ThisParameterType, ...args: Parameters): ReturnType { + const tracer = trace.getTracer(TRACER_NAME) + const className = this?.constructor?.name ?? 'Unknown' + const spanName = `${className}.${String(propertyKey)}` + + const runInActiveSpan = (span: OpenTelemetrySpan) => { + try { + const result = original.apply(this, args) + + if (isPromiseLike(result)) { + return handlePromiseResult(span, result) as ReturnType + } + + span.end() + return result + } catch (error) { + recordException(span, error) + span.end() + throw error + } + } + + if (options) { + return tracer.startActiveSpan(spanName, options, runInActiveSpan) as ReturnType + } + return tracer.startActiveSpan(spanName, runInActiveSpan) as ReturnType + } as T + } +} + +function isPromiseLike(value: unknown): value is Promise { + if (!value) return false + return typeof (value as Promise).then === 'function' +} + +async function handlePromiseResult(span: OpenTelemetrySpan, promise: Promise): Promise { + try { + return await promise + } catch (error) { + recordException(span, error) + throw error + } finally { + span.end() + } +} + +function recordException(span: OpenTelemetrySpan, error: unknown): void { + // If it's an actual Error object, OpenTelemetry captures the stack trace automatically + if (error instanceof Error) { + span.recordException(error) + } else { + span.recordException(String(error)) + } + span.setStatus({ code: SpanStatusCode.ERROR }) +}