mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 02:00:10 +00:00
0879ce538f
As noted in #36073, having support for the different protocols for exporting OpenTelemetry data would be useful, especially if this is leading to users seeing issues when they have auto-injected configuration being set. Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
367 lines
13 KiB
TypeScript
367 lines
13 KiB
TypeScript
import * as api from '@opentelemetry/api';
|
|
import { ProxyTracerProvider } from '@opentelemetry/api';
|
|
import { OTLPTraceExporter as OTLPTraceExporterGrpc } from '@opentelemetry/exporter-trace-otlp-grpc';
|
|
import { OTLPTraceExporter as OTLPTraceExporterHttp } from '@opentelemetry/exporter-trace-otlp-http';
|
|
import { OTLPTraceExporter as OTLPTraceExporterProto } from '@opentelemetry/exporter-trace-otlp-proto';
|
|
import {
|
|
BatchSpanProcessor,
|
|
NodeTracerProvider,
|
|
SimpleSpanProcessor,
|
|
} from '@opentelemetry/sdk-trace-node';
|
|
import { type DirectoryResult, dir } from 'tmp-promise';
|
|
import upath from 'upath';
|
|
import { bunyan } from '../expose.ts';
|
|
import { GetDatasourceReleasesSpanProcessor } from '../modules/datasource/span-processor.ts';
|
|
import { GitOperationSpanProcessor } from '../util/git/span-processor.ts';
|
|
import { FileSpanExporter } from './file-exporter.ts';
|
|
import {
|
|
disableInstrumentations,
|
|
getTracerProvider,
|
|
init,
|
|
instrument,
|
|
} from './index.ts';
|
|
|
|
function getOtlpExporter(nodeProvider: NodeTracerProvider): unknown {
|
|
const spanProcessors = (nodeProvider as any)._activeSpanProcessor
|
|
._spanProcessors as { _exporter: unknown }[];
|
|
// the OTLP exporter is always the last processor registered in these tests
|
|
return spanProcessors.at(-1)!._exporter;
|
|
}
|
|
|
|
afterAll(disableInstrumentations);
|
|
|
|
describe('instrumentation/index', () => {
|
|
let tmpDir: DirectoryResult;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await dir({ unsafeCleanup: true });
|
|
|
|
api.trace.disable(); // clear global components
|
|
|
|
// remove any otel env
|
|
for (const key in process.env) {
|
|
if (key.startsWith('OTEL_')) {
|
|
vi.stubEnv(key, undefined);
|
|
}
|
|
}
|
|
vi.stubEnv('RENOVATE_TRACING_CONSOLE_EXPORTER', undefined);
|
|
vi.stubEnv('RENOVATE_TRACING_FILE_EXPORTER_PATH', undefined);
|
|
// prevent real network calls to cloud metadata endpoints (AWS/GCP/Azure) during tests
|
|
vi.stubEnv('RENOVATE_USE_CLOUD_METADATA_SERVICES', 'false');
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await tmpDir.cleanup();
|
|
});
|
|
|
|
it('should use NoopTraceProvider if not activated', () => {
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
expect(traceProvider).toBeInstanceOf(ProxyTracerProvider);
|
|
const provider = traceProvider as ProxyTracerProvider;
|
|
expect(provider.constructor.name).toBe('ProxyTracerProvider');
|
|
});
|
|
|
|
it('activate console logger', () => {
|
|
vi.stubEnv('RENOVATE_TRACING_CONSOLE_EXPORTER', 'true');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
expect(traceProvider).toBeInstanceOf(ProxyTracerProvider);
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
expect(delegateProvider).toBeInstanceOf(NodeTracerProvider);
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(nodeProvider).toMatchObject({
|
|
_activeSpanProcessor: {
|
|
_spanProcessors: [
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
expect.any(SimpleSpanProcessor),
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it('registers OpenTelemetry file exporter if enabled', () => {
|
|
vi.stubEnv(
|
|
'RENOVATE_TRACING_FILE_EXPORTER_PATH',
|
|
upath.join(tmpDir.path, 'test-traces.jsonl'),
|
|
);
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
expect(traceProvider).toBeInstanceOf(ProxyTracerProvider);
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
expect(delegateProvider).toBeInstanceOf(NodeTracerProvider);
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(nodeProvider).toMatchObject({
|
|
_activeSpanProcessor: {
|
|
_spanProcessors: [
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
expect.any(BatchSpanProcessor),
|
|
],
|
|
},
|
|
});
|
|
// Verify the SimpleSpanProcessor wraps a FileSpanExporter
|
|
const spanProcessors = (nodeProvider as any)._activeSpanProcessor
|
|
._spanProcessors;
|
|
const fileProcessor = spanProcessors[2];
|
|
expect(fileProcessor._exporter).toBeInstanceOf(FileSpanExporter);
|
|
});
|
|
|
|
it('registers GitOperationSpanProcessor, GetDatasourceReleasesSpanProcessor regardless of tracing being enabled', () => {
|
|
// intentionally don't set it
|
|
vi.stubEnv('RENOVATE_TRACING_CONSOLE_EXPORTER', undefined);
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', undefined);
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(nodeProvider).toMatchObject({
|
|
_activeSpanProcessor: {
|
|
_spanProcessors: expect.arrayContaining([
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
]),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('activate remote logger', () => {
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://collector.example.com');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
expect(traceProvider).toBeInstanceOf(ProxyTracerProvider);
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
expect(delegateProvider).toBeInstanceOf(NodeTracerProvider);
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(nodeProvider).toMatchObject({
|
|
_activeSpanProcessor: {
|
|
_spanProcessors: [
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
{
|
|
_exporter: {
|
|
_delegate: {
|
|
_transport: {
|
|
_transport: {
|
|
_parameters: {
|
|
url: 'https://collector.example.com/v1/traces',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
// defaults to the http/json exporter when no protocol is configured
|
|
expect(getOtlpExporter(nodeProvider)).toBeInstanceOf(OTLPTraceExporterHttp);
|
|
});
|
|
|
|
it('activate remote logger with grpc protocol', () => {
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://collector.example.com');
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_PROTOCOL', 'grpc');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(nodeProvider).toMatchObject({
|
|
_activeSpanProcessor: {
|
|
_spanProcessors: [
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
{
|
|
_exporter: {
|
|
_delegate: {
|
|
_transport: {
|
|
_parameters: {
|
|
address: 'collector.example.com',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(getOtlpExporter(nodeProvider)).toBeInstanceOf(OTLPTraceExporterGrpc);
|
|
});
|
|
|
|
it('activate remote logger with http/protobuf protocol', () => {
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://collector.example.com');
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/protobuf');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(nodeProvider).toMatchObject({
|
|
_activeSpanProcessor: {
|
|
_spanProcessors: [
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
{
|
|
_exporter: {
|
|
_delegate: {
|
|
_transport: {
|
|
_transport: {
|
|
_parameters: {
|
|
url: 'https://collector.example.com/v1/traces',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(getOtlpExporter(nodeProvider)).toBeInstanceOf(
|
|
OTLPTraceExporterProto,
|
|
);
|
|
});
|
|
|
|
it('activate remote logger with http/json protocol', () => {
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://collector.example.com');
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/json');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(getOtlpExporter(nodeProvider)).toBeInstanceOf(OTLPTraceExporterHttp);
|
|
});
|
|
|
|
it('takes precedence from the `_TRACES` env var, if set', () => {
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://collector.example.com');
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/json');
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_TRACES_PROTOCOL', 'http/protobuf');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(getOtlpExporter(nodeProvider)).toBeInstanceOf(
|
|
OTLPTraceExporterProto,
|
|
);
|
|
});
|
|
|
|
it('defaults to http/json protocol if the specified protocol has a typo', () => {
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://collector.example.com');
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/jayson');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(getOtlpExporter(nodeProvider)).toBeInstanceOf(OTLPTraceExporterHttp);
|
|
});
|
|
|
|
it('activate console logger and remote logger', () => {
|
|
vi.stubEnv('RENOVATE_TRACING_CONSOLE_EXPORTER', 'true');
|
|
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'https://collector.example.com');
|
|
|
|
init();
|
|
const traceProvider = getTracerProvider();
|
|
expect(traceProvider).toBeInstanceOf(ProxyTracerProvider);
|
|
const proxyProvider = traceProvider as ProxyTracerProvider;
|
|
const delegateProvider = proxyProvider.getDelegate();
|
|
expect(delegateProvider).toBeInstanceOf(NodeTracerProvider);
|
|
const nodeProvider = delegateProvider as NodeTracerProvider;
|
|
expect(nodeProvider).toMatchObject({
|
|
_activeSpanProcessor: {
|
|
_spanProcessors: [
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
{ _exporter: {} },
|
|
{
|
|
_exporter: {
|
|
_delegate: {
|
|
_transport: {
|
|
_transport: {
|
|
_parameters: {
|
|
url: 'https://collector.example.com/v1/traces',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(getOtlpExporter(nodeProvider)).toBeInstanceOf(OTLPTraceExporterHttp);
|
|
});
|
|
|
|
describe('BunyanInstrumentation', () => {
|
|
// OpenTelemetry's context propagation currently uses `AsyncLocalStorage`, which does not behave the same way in vitest worker threads as in a real Node.js process, so we cannot write a full end-to-end here to validate the `span_id`, `trace_id` and `trace_flags` are set
|
|
//
|
|
// Claude Sonnet 4.6 suggests that we instead create an (admittedly brittle) test to validate that this is marked as `__wrapped`.
|
|
it('patches bunyan Logger._emit when tracing is enabled', () => {
|
|
vi.stubEnv('RENOVATE_TRACING_CONSOLE_EXPORTER', 'true');
|
|
init();
|
|
|
|
const mod = bunyan();
|
|
|
|
// shimmer marks wrapped functions with __wrapped = true
|
|
expect(
|
|
(mod.prototype as unknown as Record<string, unknown>)._emit,
|
|
).toHaveProperty('__wrapped', true);
|
|
});
|
|
});
|
|
|
|
describe('instrument', () => {
|
|
it('should return result', () => {
|
|
const value = 'testResult';
|
|
const result = instrument('test', () => {
|
|
return value;
|
|
});
|
|
expect(result).toStrictEqual(value);
|
|
});
|
|
|
|
it('should rethrow exception', () => {
|
|
const error = new Error('testError');
|
|
expect(() =>
|
|
instrument('test', () => {
|
|
throw error;
|
|
}),
|
|
).toThrow(error);
|
|
});
|
|
|
|
it('should return result for async fn', async () => {
|
|
const value = 'testResult';
|
|
const result = await instrument('test', async () => {
|
|
return await new Promise((resolve) => {
|
|
resolve(value);
|
|
});
|
|
});
|
|
expect(result).toStrictEqual(value);
|
|
});
|
|
|
|
it('should rethrow exception for async fn', async () => {
|
|
const error = new Error('testError');
|
|
await expect(
|
|
instrument('test', async () => {
|
|
await Promise.resolve();
|
|
throw error;
|
|
}),
|
|
).rejects.toThrow(error);
|
|
});
|
|
});
|
|
});
|