From e51175c4a29b44cd32af2b6f901c0e6d8a2e850a Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 13 Mar 2026 18:04:37 +0100 Subject: [PATCH] feat(telemetry): add decorator based tracing utils Signed-off-by: William Phetsinorath --- .../telemetry/telemetry.constants.ts | 1 + .../telemetry/telemetry.decorator.ts | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.constants.ts create mode 100644 apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.decorator.ts 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 new file mode 100644 index 000000000..cc4af6f36 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.constants.ts @@ -0,0 +1 @@ +export const TELEMETRY_TRACER_NAME = 'cloud-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 000000000..88a81f158 --- /dev/null +++ b/apps/server-nestjs/src/cpin-module/infrastructure/telemetry/telemetry.decorator.ts @@ -0,0 +1,65 @@ +import { SpanStatusCode, trace } from '@opentelemetry/api' +import type { Attributes, SpanOptions, Span as OpenTelemetrySpan } from '@opentelemetry/api' +import { TELEMETRY_TRACER_NAME } from './telemetry.constants' + +export interface TelemetrySpanMetadata { + options?: SpanOptions + attributes?: Attributes +} + +export function Span(name?: string, metadata: TelemetrySpanMetadata = {}): MethodDecorator { + return ( + _target: object, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, + ): TypedPropertyDescriptor => { + const original = descriptor.value + if (typeof original !== 'function') return descriptor + + descriptor.value = function (this: unknown, ...args: unknown[]): unknown { + const tracer = trace.getTracer(TELEMETRY_TRACER_NAME) + const className = (this as any)?.constructor?.name ?? 'Unknown' + const spanName = name ?? `${className}.${String(propertyKey)}` + + return tracer.startActiveSpan(spanName, metadata.options ?? {}, (span) => { + if (metadata.attributes) span.setAttributes(metadata.attributes) + + try { + const result = original.apply(this, args) + if (result instanceof Promise) { + return handlePromiseResult(span, result) + } + span.end() + return result + } catch (error) { + recordError(span, error) + span.end() + throw error + } + }) + } as T + + return descriptor + } +} + +async function handlePromiseResult(span: OpenTelemetrySpan, promise: Promise): Promise { + try { + return await promise + } catch (error) { + recordError(span, error) + throw error + } finally { + span.end() + } +} + +function recordError(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 }) +}