Skip to content
Open
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
5 changes: 5 additions & 0 deletions app/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('../lib/otel');
}
}
1 change: 1 addition & 0 deletions docs/BODY-SIZE-LIMITS.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
85 changes: 85 additions & 0 deletions lib/otel.test.ts
Original file line number Diff line number Diff line change
@@ -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 })
);
});
});
46 changes: 46 additions & 0 deletions lib/otel.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) => {
console.log(JSON.stringify({ level: 'info', message, correlationId: getCorrelationId(), ...meta }));
},
error: (message: string, meta?: Record<string, unknown>) => {
console.error(JSON.stringify({ level: 'error', message, correlationId: getCorrelationId(), ...meta }));
}
};