mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 09:41:46 +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>
237 lines
7.9 KiB
TypeScript
237 lines
7.9 KiB
TypeScript
import { ClientRequest, ServerResponse } from 'node:http';
|
|
import type { Context, Span, Tracer, TracerProvider } from '@opentelemetry/api';
|
|
import * as api from '@opentelemetry/api';
|
|
import { ProxyTracerProvider, SpanStatusCode } from '@opentelemetry/api';
|
|
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
|
|
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 type { Instrumentation } from '@opentelemetry/instrumentation';
|
|
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
import { BunyanInstrumentation } from '@opentelemetry/instrumentation-bunyan';
|
|
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis';
|
|
import {
|
|
detectResources,
|
|
resourceFromAttributes,
|
|
} from '@opentelemetry/resources';
|
|
import {
|
|
BatchSpanProcessor,
|
|
ConsoleSpanExporter,
|
|
SimpleSpanProcessor,
|
|
type SpanExporter,
|
|
type SpanProcessor,
|
|
} from '@opentelemetry/sdk-trace-base';
|
|
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
import {
|
|
ATTR_SERVICE_NAME,
|
|
ATTR_SERVICE_VERSION,
|
|
} from '@opentelemetry/semantic-conventions';
|
|
import { isPromise } from '@sindresorhus/is';
|
|
import { pkg } from '../expose.ts';
|
|
import { GetDatasourceReleasesSpanProcessor } from '../modules/datasource/span-processor.ts';
|
|
import { GitOperationSpanProcessor } from '../util/git/span-processor.ts';
|
|
import { getResourceDetectors } from './detectors.ts';
|
|
import { FileSpanExporter } from './file-exporter.ts';
|
|
import type { RenovateSpanOptions } from './types.ts';
|
|
import {
|
|
getFileExporterPath,
|
|
getOtlpProtocol,
|
|
isFileExporterEnabled,
|
|
isTraceDebuggingEnabled,
|
|
isTraceSendingEnabled,
|
|
isTracingEnabled,
|
|
massageThrowable,
|
|
} from './utils.ts';
|
|
|
|
let instrumentations: Instrumentation[] = [];
|
|
|
|
function createOtlpExporter(): SpanExporter {
|
|
const protocol = getOtlpProtocol();
|
|
if (protocol === 'grpc') {
|
|
return new OTLPTraceExporterGrpc();
|
|
}
|
|
if (protocol === 'http/protobuf') {
|
|
return new OTLPTraceExporterProto();
|
|
}
|
|
return new OTLPTraceExporterHttp();
|
|
}
|
|
|
|
export function init(): void {
|
|
const spanProcessors: SpanProcessor[] = [
|
|
new GitOperationSpanProcessor(),
|
|
new GetDatasourceReleasesSpanProcessor(),
|
|
];
|
|
|
|
if (!isTracingEnabled()) {
|
|
const traceProvider = new NodeTracerProvider({ spanProcessors });
|
|
traceProvider.register({
|
|
contextManager: new AsyncLocalStorageContextManager(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
// v8 ignore if -- TODO add tests
|
|
if (process.env.OTEL_LOG_LEVEL) {
|
|
api.diag.setLogger(
|
|
new api.DiagConsoleLogger(),
|
|
api.DiagLogLevel[
|
|
process.env.OTEL_LOG_LEVEL.toUpperCase() as keyof typeof api.DiagLogLevel
|
|
],
|
|
);
|
|
}
|
|
|
|
// add processors
|
|
if (isTraceDebuggingEnabled()) {
|
|
spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));
|
|
}
|
|
|
|
// OTEL specification environment variable
|
|
if (isTraceSendingEnabled()) {
|
|
spanProcessors.push(new BatchSpanProcessor(createOtlpExporter()));
|
|
}
|
|
|
|
if (isFileExporterEnabled()) {
|
|
spanProcessors.push(
|
|
new BatchSpanProcessor(new FileSpanExporter(getFileExporterPath())),
|
|
);
|
|
}
|
|
|
|
const env = process.env; // don't use getEnv() here to avoid circular dependency with env variables used in the resource detectors
|
|
const baseResource = resourceFromAttributes({
|
|
// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#semantic-attributes-with-sdk-provided-default-value
|
|
[ATTR_SERVICE_NAME]: env.OTEL_SERVICE_NAME ?? 'renovate',
|
|
// https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
|
|
// https://github.com/open-telemetry/opentelemetry-js/blob/e9d3c71918635d490b6a9ac9f8259265b38394d0/semantic-conventions/src/experimental_attributes.ts#L7688
|
|
['service.namespace']: env.OTEL_SERVICE_NAMESPACE ?? 'renovatebot.com',
|
|
[ATTR_SERVICE_VERSION]: env.OTEL_SERVICE_VERSION ?? pkg.version,
|
|
});
|
|
|
|
const detectedResource = detectResources({
|
|
detectors: getResourceDetectors(env),
|
|
});
|
|
|
|
const traceProvider = new NodeTracerProvider({
|
|
resource: baseResource.merge(detectedResource),
|
|
spanProcessors,
|
|
});
|
|
|
|
const contextManager = new AsyncLocalStorageContextManager();
|
|
traceProvider.register({
|
|
contextManager,
|
|
});
|
|
|
|
instrumentations = [
|
|
new HttpInstrumentation({
|
|
/* v8 ignore start -- not easily testable */
|
|
applyCustomAttributesOnSpan: (span, request, response) => {
|
|
// ignore 404 errors when the branch protection of Github could not be found. This is expected if no rules are configured
|
|
if (
|
|
request instanceof ClientRequest &&
|
|
request.host === `api.github.com` &&
|
|
request.path.endsWith(`/protection`) &&
|
|
response.statusCode === 404
|
|
) {
|
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
} else if (
|
|
request instanceof ClientRequest &&
|
|
request.path === '/v2/' &&
|
|
request.method === 'GET' &&
|
|
!request.getHeader('authorization') &&
|
|
response instanceof ServerResponse &&
|
|
response.getHeader('www-authenticate') &&
|
|
response.getHeader('docker-distribution-api-version') &&
|
|
response.statusCode === 401
|
|
) {
|
|
// Docker API test expects 401 with `www-authenticate` header when registry requires authentication, so ignore this error
|
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
}
|
|
},
|
|
/* v8 ignore stop -- not easily testable */
|
|
}),
|
|
new BunyanInstrumentation(),
|
|
new RedisInstrumentation(),
|
|
];
|
|
registerInstrumentations({
|
|
instrumentations,
|
|
});
|
|
}
|
|
|
|
// https://github.com/open-telemetry/opentelemetry-js-api/issues/34
|
|
/* v8 ignore next -- not easily testable */
|
|
export async function shutdown(): Promise<void> {
|
|
const traceProvider = getTracerProvider();
|
|
if (traceProvider instanceof NodeTracerProvider) {
|
|
await traceProvider.shutdown();
|
|
} else if (traceProvider instanceof ProxyTracerProvider) {
|
|
const delegateProvider = traceProvider.getDelegate();
|
|
if (delegateProvider instanceof NodeTracerProvider) {
|
|
await delegateProvider.shutdown();
|
|
}
|
|
}
|
|
}
|
|
|
|
export function disableInstrumentations(): void {
|
|
for (const instrumentation of instrumentations) {
|
|
instrumentation.disable();
|
|
}
|
|
}
|
|
|
|
export function getTracerProvider(): TracerProvider {
|
|
return api.trace.getTracerProvider();
|
|
}
|
|
|
|
function getTracer(): Tracer {
|
|
return getTracerProvider().getTracer('renovate');
|
|
}
|
|
|
|
export function instrument<F extends () => ReturnType<F>>(
|
|
name: string,
|
|
fn: F,
|
|
): ReturnType<F>;
|
|
export function instrument<F extends () => ReturnType<F>>(
|
|
name: string,
|
|
fn: F,
|
|
options: RenovateSpanOptions,
|
|
): ReturnType<F>;
|
|
export function instrument<F extends () => ReturnType<F>>(
|
|
name: string,
|
|
fn: F,
|
|
options: RenovateSpanOptions,
|
|
context: Context,
|
|
): ReturnType<F>;
|
|
export function instrument<F extends () => ReturnType<F>>(
|
|
name: string,
|
|
fn: F,
|
|
options: RenovateSpanOptions = {},
|
|
context: Context = api.context.active(),
|
|
): ReturnType<F> {
|
|
return getTracer().startActiveSpan(name, options, context, (span: Span) => {
|
|
try {
|
|
const ret = fn();
|
|
if (isPromise(ret)) {
|
|
return ret
|
|
.catch((e) => {
|
|
span.recordException(e);
|
|
span.setStatus({
|
|
code: SpanStatusCode.ERROR,
|
|
message: massageThrowable(e),
|
|
});
|
|
throw e;
|
|
})
|
|
.finally(() => span.end()) as ReturnType<F>;
|
|
}
|
|
span.end();
|
|
return ret;
|
|
} catch (e) {
|
|
span.recordException(e);
|
|
span.setStatus({
|
|
code: SpanStatusCode.ERROR,
|
|
message: massageThrowable(e),
|
|
});
|
|
span.end();
|
|
throw e;
|
|
}
|
|
});
|
|
}
|