autotel
Version:
Write Once, Observe Anywhere
221 lines (175 loc) • 6.6 kB
text/typescript
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]);
});
});