UNPKG

autotel

Version:
290 lines (233 loc) 8.19 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { NodeSDK } from '@opentelemetry/sdk-node'; import { mock, mockDeep, type DeepMockProxy } from 'vitest-mock-extended'; type SdkRecord = { options: Record<string, unknown>; instance: DeepMockProxy<NodeSDK>; }; const mockedModules = [ '@opentelemetry/sdk-node', '@opentelemetry/exporter-trace-otlp-http', '@opentelemetry/exporter-metrics-otlp-http', '@opentelemetry/sdk-metrics', './node-require', './node-require.ts', ]; // Track traceloop initialize calls globally const traceloopInitializeCalls: Array<Record<string, unknown>> = []; // Store mock initialize function globally let globalMockInitialize: ReturnType<typeof vi.fn> | null = null; async function loadInitWithMocks() { const sdkInstances: SdkRecord[] = []; class MockNodeSDK { constructor(options: Record<string, unknown>) { const instance = mockDeep<NodeSDK>(); instance.start.mockImplementation(() => {}); instance.shutdown.mockResolvedValue(); (instance as any).getTracerProvider = vi.fn().mockReturnValue(mock()); sdkInstances.push({ options, instance }); return instance; } } class MockOTLPTraceExporter { options: Record<string, unknown>; constructor(options: Record<string, unknown>) { this.options = options; } } class MockOTLPMetricExporter { options: Record<string, unknown>; constructor(options: Record<string, unknown>) { this.options = options; } } class MockPeriodicExportingMetricReader { options: Record<string, unknown>; constructor(options: Record<string, unknown>) { this.options = options; } } // Clear module cache first vi.resetModules(); // Create mock function that captures calls and store it globally globalMockInitialize = vi.fn((options?: Record<string, unknown>) => { traceloopInitializeCalls.push(options || {}); }); const mockTraceloop = { initialize: globalMockInitialize, instrumentations: [{ name: 'openai' }, { name: 'langchain' }], }; // Mock node-require to intercept safeRequire('@traceloop/node-server-sdk'). // vi.doMock on the traceloop module itself doesn't work because safeRequire // uses native require() which bypasses vitest's module interception. const nodeRequireMock = () => ({ safeRequire: vi.fn((id: string) => { if (id === '@traceloop/node-server-sdk') { return mockTraceloop; } return undefined; }), requireModule: vi.fn((id: string) => { const err = new Error(`Cannot find module '${id}'`); (err as NodeJS.ErrnoException).code = 'MODULE_NOT_FOUND'; throw err; }), nodeRequire: vi.fn(), }); vi.doMock('./node-require', nodeRequireMock); vi.doMock('./node-require.ts', nodeRequireMock); 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, traceloopInitializeCalls, mockTraceloop, }; } describe('init() OpenLLMetry integration', () => { beforeEach(() => { vi.resetModules(); traceloopInitializeCalls.length = 0; globalMockInitialize = null; }); afterEach(() => { for (const mod of mockedModules) { vi.doUnmock(mod); } vi.clearAllMocks(); delete process.env.AUTOTEL_METRICS; delete process.env.NODE_ENV; delete process.env.TRACELOOP_API_KEY; }); it('should not initialize OpenLLMetry when disabled', async () => { const { init, traceloopInitializeCalls } = await loadInitWithMocks(); init({ service: 'test-app' }); expect(traceloopInitializeCalls).toHaveLength(0); }); it('should initialize OpenLLMetry when enabled', async () => { const { init, traceloopInitializeCalls } = await loadInitWithMocks(); init({ service: 'test-app', openllmetry: { enabled: true }, }); expect(traceloopInitializeCalls).toHaveLength(1); expect(traceloopInitializeCalls[0]).toBeDefined(); }); it('should pass OpenLLMetry options to initialize', async () => { const { init, traceloopInitializeCalls } = await loadInitWithMocks(); init({ service: 'test-app', openllmetry: { enabled: true, options: { disableBatch: true, apiKey: 'test-key', }, }, }); expect(traceloopInitializeCalls).toHaveLength(1); expect(traceloopInitializeCalls[0]).toMatchObject({ disableBatch: true, apiKey: 'test-key', }); }); it('should reuse autotel tracer provider when OpenLLMetry is enabled', async () => { const { init, traceloopInitializeCalls, sdkInstances } = await loadInitWithMocks(); init({ service: 'test-app', openllmetry: { enabled: true }, }); expect(sdkInstances).toHaveLength(1); const sdkInstance = sdkInstances[0].instance; expect(traceloopInitializeCalls).toHaveLength(1); const callOptions = traceloopInitializeCalls[0]; expect(callOptions).toBeDefined(); expect((sdkInstance as any).getTracerProvider).toHaveBeenCalled(); }); it('should add OpenLLMetry instrumentations when selectiveInstrumentation is false', async () => { const { init, sdkInstances, mockTraceloop } = await loadInitWithMocks(); init({ service: 'test-app', openllmetry: { enabled: true }, autoInstrumentations: false, }); const options = sdkInstances.at(-1)?.options as Record<string, unknown>; const instrumentations = options.instrumentations as unknown[]; expect(instrumentations).toBeDefined(); expect(mockTraceloop.instrumentations).toBeDefined(); }); it('should handle missing @traceloop/node-server-sdk gracefully', async () => { vi.resetModules(); // Mock node-require to return undefined for traceloop (simulating not installed) const nodeRequireMock = () => ({ safeRequire: vi.fn(() => undefined), requireModule: vi.fn((id: string) => { const err = new Error(`Cannot find module '${id}'`); (err as NodeJS.ErrnoException).code = 'MODULE_NOT_FOUND'; throw err; }), nodeRequire: vi.fn(), }); vi.doMock('./node-require', nodeRequireMock); vi.doMock('./node-require.ts', nodeRequireMock); vi.doMock('@opentelemetry/sdk-node', () => ({ NodeSDK: class { constructor() { const instance = mockDeep<NodeSDK>(); instance.start.mockImplementation(() => {}); instance.shutdown.mockResolvedValue(); return instance; } }, })); vi.doMock('@opentelemetry/exporter-trace-otlp-http', () => ({ OTLPTraceExporter: class { constructor() {} }, })); vi.doMock('@opentelemetry/exporter-metrics-otlp-http', () => ({ OTLPMetricExporter: class { constructor() {} }, })); vi.doMock('@opentelemetry/sdk-metrics', () => ({ PeriodicExportingMetricReader: class { constructor() {} }, })); const { init } = await import('./init'); expect(() => { init({ service: 'test-app', openllmetry: { enabled: true }, }); }).not.toThrow(); }); it('should initialize OpenLLMetry after SDK start', async () => { const { init, sdkInstances, traceloopInitializeCalls } = await loadInitWithMocks(); init({ service: 'test-app', openllmetry: { enabled: true }, }); // Verify SDK started (synchronously in init) expect(sdkInstances).toHaveLength(1); expect(sdkInstances[0].instance.start).toHaveBeenCalled(); // Verify OpenLLMetry was initialized (synchronously via safeRequire) expect(traceloopInitializeCalls).toHaveLength(1); }); });