From 38426c51d96a87e346b3ab6153232eaf585623c1 Mon Sep 17 00:00:00 2001 From: Orioye Blessing Esther <210155349+ayaoba24@users.noreply.github.com> Date: Tue, 30 Jun 2026 10:43:54 +0100 Subject: [PATCH 1/2] Add overview for body size limit configuration Added overview section for body size limit configuration. --- docs/BODY-SIZE-LIMITS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/BODY-SIZE-LIMITS.md b/docs/BODY-SIZE-LIMITS.md index a08ec4e..c3bece9 100644 --- a/docs/BODY-SIZE-LIMITS.md +++ b/docs/BODY-SIZE-LIMITS.md @@ -1,5 +1,6 @@ # Body Size Limit Configuration - Implementation Summary + ## Overview Implemented per-route body size limit configuration for the GrantFox campaign, allowing different size limits for different API route categories: From 3ce56fd31db924656c78677b7c990117014632c6 Mon Sep 17 00:00:00 2001 From: ayaoba24 Date: Thu, 2 Jul 2026 12:45:05 +0100 Subject: [PATCH 2/2] feat: add opentelemetry tracing across api + lib --- app/instrumentation.ts | 5 +++ lib/otel.test.ts | 85 ++++++++++++++++++++++++++++++++++++++++++ lib/otel.ts | 46 +++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 app/instrumentation.ts create mode 100644 lib/otel.test.ts create mode 100644 lib/otel.ts diff --git a/app/instrumentation.ts b/app/instrumentation.ts new file mode 100644 index 0000000..5f8fd44 --- /dev/null +++ b/app/instrumentation.ts @@ -0,0 +1,5 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + await import('../lib/otel'); + } +} diff --git a/lib/otel.test.ts b/lib/otel.test.ts new file mode 100644 index 0000000..283d724 --- /dev/null +++ b/lib/otel.test.ts @@ -0,0 +1,85 @@ +import { getCorrelationId, logger, sdk } from './otel'; +import { trace, context } from '@opentelemetry/api'; + +jest.mock('@opentelemetry/api', () => ({ + trace: { + getSpan: jest.fn(), + }, + context: { + active: jest.fn(), + }, +})); + +jest.mock('@opentelemetry/sdk-node', () => { + return { + NodeSDK: jest.fn().mockImplementation(() => ({ + start: jest.fn(), + shutdown: jest.fn().mockResolvedValue(undefined), + })), + }; +}); + +jest.mock('@opentelemetry/auto-instrumentations-node', () => ({ + getNodeAutoInstrumentations: jest.fn(), +})); + +jest.mock('@opentelemetry/exporter-trace-otlp-http', () => ({ + OTLPTraceExporter: jest.fn(), +})); + +describe('OpenTelemetry Initialization', () => { + let consoleLogSpy: jest.SpyInstance; + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should initialize NodeSDK without error', () => { + expect(sdk).toBeDefined(); + }); + + it('should get correlation id when span is active', () => { + const mockTraceId = '1234567890abcdef'; + (trace.getSpan as jest.Mock).mockReturnValue({ + spanContext: () => ({ traceId: mockTraceId }), + }); + + const correlationId = getCorrelationId(); + expect(correlationId).toBe(mockTraceId); + }); + + it('should return undefined correlation id when no span is active', () => { + (trace.getSpan as jest.Mock).mockReturnValue(undefined); + + const correlationId = getCorrelationId(); + expect(correlationId).toBeUndefined(); + }); + + it('should log info with structured format', () => { + (trace.getSpan as jest.Mock).mockReturnValue(undefined); + logger.info('test info', { userId: 123 }); + + expect(consoleLogSpy).toHaveBeenCalledWith( + JSON.stringify({ level: 'info', message: 'test info', correlationId: undefined, userId: 123 }) + ); + }); + + it('should log error with structured format', () => { + const mockTraceId = '1234567890abcdef'; + (trace.getSpan as jest.Mock).mockReturnValue({ + spanContext: () => ({ traceId: mockTraceId }), + }); + + logger.error('test error', { errorCode: 500 }); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + JSON.stringify({ level: 'error', message: 'test error', correlationId: mockTraceId, errorCode: 500 }) + ); + }); +}); diff --git a/lib/otel.ts b/lib/otel.ts new file mode 100644 index 0000000..75a5bc0 --- /dev/null +++ b/lib/otel.ts @@ -0,0 +1,46 @@ +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { Resource } from '@opentelemetry/resources'; +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; +import { trace, context } from '@opentelemetry/api'; + +const exporterOptions = { + url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces', +}; + +const traceExporter = new OTLPTraceExporter(exporterOptions); + +export const sdk = new NodeSDK({ + resource: new Resource({ + [ATTR_SERVICE_NAME]: 'streampay-frontend', + [ATTR_SERVICE_VERSION]: '1.0.0', + }), + traceExporter, + instrumentations: [getNodeAutoInstrumentations()] +}); + +if (process.env.NODE_ENV !== 'test') { + sdk.start(); + + process.on('SIGTERM', () => { + sdk.shutdown() + .then(() => console.log('Tracing terminated')) + .catch((error) => console.log('Error terminating tracing', error)) + .finally(() => process.exit(0)); + }); +} + +export const getCorrelationId = (): string | undefined => { + const span = trace.getSpan(context.active()); + return span ? span.spanContext().traceId : undefined; +}; + +export const logger = { + info: (message: string, meta?: Record) => { + console.log(JSON.stringify({ level: 'info', message, correlationId: getCorrelationId(), ...meta })); + }, + error: (message: string, meta?: Record) => { + console.error(JSON.stringify({ level: 'error', message, correlationId: getCorrelationId(), ...meta })); + } +};