UNPKG

autotel

Version:
221 lines (175 loc) 6.6 kB
import { afterEach, describe, expect, it, vi } from 'vitest'; import type { MetricReader } from '@opentelemetry/sdk-metrics'; import type { NodeSDK } from '@opentelemetry/sdk-node'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { mock, mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; type SdkRecord = { options: Record<string, unknown>; instance: DeepMockProxy<NodeSDK>; }; async function loadInitWithMocks() { const sdkInstances: SdkRecord[] = []; const traceExporterOptions: Record<string, unknown>[] = []; const metricExporterOptions: Record<string, unknown>[] = []; const metricReaderOptions: Record<string, unknown>[] = []; class MockNodeSDK { constructor(options: Record<string, unknown>) { const instance = mockDeep<NodeSDK>(); instance.start.mockImplementation(() => {}); instance.shutdown.mockResolvedValue(); sdkInstances.push({ options, instance }); return instance; } } class MockOTLPTraceExporter { options: Record<string, unknown>; constructor(options: Record<string, unknown>) { this.options = options; traceExporterOptions.push(options); } } class MockOTLPMetricExporter { options: Record<string, unknown>; constructor(options: Record<string, unknown>) { this.options = options; metricExporterOptions.push(options); } } class MockPeriodicExportingMetricReader { options: Record<string, unknown>; constructor(options: Record<string, unknown>) { this.options = options; metricReaderOptions.push(options); } } // Reset modules immediately before mocking to ensure clean state vi.resetModules(); vi.doMock('@opentelemetry/sdk-node', () => ({ NodeSDK: MockNodeSDK, })); vi.doMock('@opentelemetry/exporter-trace-otlp-http', () => ({ OTLPTraceExporter: MockOTLPTraceExporter, })); vi.doMock('@opentelemetry/exporter-metrics-otlp-http', () => ({ OTLPMetricExporter: MockOTLPMetricExporter, })); vi.doMock('@opentelemetry/sdk-metrics', () => ({ PeriodicExportingMetricReader: MockPeriodicExportingMetricReader, })); const mod = await import('./init'); return { init: mod.init, getConfig: mod.getConfig, sdkInstances, traceExporterOptions, metricExporterOptions, metricReaderOptions, }; } describe('init() customization', () => { afterEach(() => { vi.restoreAllMocks(); delete process.env.AUTOTEL_METRICS; delete process.env.NODE_ENV; }); it( 'passes custom instrumentations to the NodeSDK', { timeout: 10_000 }, async () => { const { init, sdkInstances } = await loadInitWithMocks(); const instrumentation = { name: 'http' } as any; init({ service: 'instrumented-app', instrumentations: [instrumentation], }); const options = sdkInstances.at(-1)?.options as Record<string, unknown>; expect(options.instrumentations).toBeDefined(); expect(options.instrumentations).toContain(instrumentation); }, ); it('merges resource attributes with defaults', async () => { const { init, getConfig, sdkInstances } = await loadInitWithMocks(); init({ service: 'resource-app', resourceAttributes: { 'cloud.region': 'eu-central-1' }, }); const resource = sdkInstances.at(-1)?.options.resource as | { attributes?: Record<string, unknown>; } | undefined; if (resource?.attributes) { expect(resource.attributes['cloud.region']).toBe('eu-central-1'); expect(resource.attributes['service.name']).toBe('resource-app'); return; } const config = getConfig(); expect(config.service).toBe('resource-app'); expect(config.resourceAttributes).toMatchObject({ 'cloud.region': 'eu-central-1', }); }); it('creates a default OTLP metric reader when metrics enabled', async () => { const { init, metricReaderOptions, metricExporterOptions } = await loadInitWithMocks(); init({ service: 'metrics-app', endpoint: 'http://localhost:4318' }); expect(metricReaderOptions).toHaveLength(1); expect(metricExporterOptions).toHaveLength(1); }); it('skips default metric reader when metrics disabled', async () => { const { init, metricReaderOptions } = await loadInitWithMocks(); init({ service: 'no-metrics', metrics: false }); expect(metricReaderOptions).toHaveLength(0); }); it('respects custom metric readers', async () => { const { init, sdkInstances, metricReaderOptions } = await loadInitWithMocks(); const customMetricReader = mock<MetricReader>(); init({ service: 'custom-metrics', metricReaders: [customMetricReader] }); expect(sdkInstances).toHaveLength(1); const options = sdkInstances.at(-1)!.options as Record<string, unknown>; expect(options.metricReaders).toEqual([customMetricReader]); expect(metricReaderOptions).toHaveLength(0); }); it('applies OTLP headers for default exporters', async () => { const { init, traceExporterOptions, metricExporterOptions } = await loadInitWithMocks(); init({ service: 'headers-app', endpoint: 'http://localhost:4318', headers: 'Authorization=Basic abc123', }); expect(traceExporterOptions[0]).toMatchObject({ headers: { Authorization: 'Basic abc123' }, }); expect(metricExporterOptions[0]).toMatchObject({ headers: { Authorization: 'Basic abc123' }, }); }); it('supports sdkFactory overrides', async () => { const { init, sdkInstances } = await loadInitWithMocks(); const customSdk = mockDeep<NodeSDK>(); customSdk.start.mockImplementation(() => {}); customSdk.shutdown.mockResolvedValue(); init({ service: 'custom-sdk', endpoint: 'http://localhost:4318', metrics: false, sdkFactory: (defaults) => { expect(defaults.spanProcessors).toBeDefined(); return customSdk; }, }); expect(sdkInstances).toHaveLength(0); expect(customSdk.start).toHaveBeenCalled(); }); it('uses provided spanProcessors when supplied', async () => { const { init, sdkInstances } = await loadInitWithMocks(); const customProcessor = mock<SpanProcessor>(); customProcessor.shutdown.mockResolvedValue(); customProcessor.forceFlush.mockResolvedValue(); init({ service: 'custom-span', spanProcessors: [customProcessor] }); const options = sdkInstances.at(-1)?.options as Record<string, unknown>; expect(options.spanProcessors).toEqual([customProcessor]); }); });