Files
renovate/lib/modules/datasource/span-processor.spec.ts
T
Mike Ryan f635ae29c4 chore(instrumentation): add timing statistics for getReleases (#42523)
* feat: timing statistics for getReleases

Adds collection and reporting of timing statistics for
`datasource.getReleases()` calls. Each call records the datasource, registryUrl,
packageName, and duration. The intent is to visibility into where
renovate is retrieving release information and how long that might take.

The report consists of a nested data-structure that provides increasing
detail as we descend datasource, registryUrl, and packageName.

The full report  is logged at trace level
`getReleases statistics with packages`.

A summary report including only datasources and registryUrls is logged
at debug level `getReleases statistics summary`.

* add GetReleasesSpanProcessor

- Re-instrument getReleases calls using open telemetry.
- Adds GetReleasesSpanProcessor, which can adapt
  `getReleases` spans to GetReleasesStats.

* fixup instrumentation tests

* pr feedback

* pr feedback: naming

* pr feedback: comments
2026-04-14 17:36:37 +00:00

139 lines
4.5 KiB
TypeScript

import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import { ATTR_CODE_FUNCTION_NAME } from '@opentelemetry/semantic-conventions';
import { partial } from '~test/util.ts';
import {
ATTR_RENOVATE_DATASOURCE,
ATTR_RENOVATE_PACKAGE_NAME,
ATTR_RENOVATE_REGISTRY_URL,
} from '../../instrumentation/types.ts';
import * as stats from '../../util/stats.ts';
import { GetDatasourceReleasesSpanProcessor } from './span-processor.ts';
vi.mock('../../util/stats.ts');
describe('modules/datasource/span-processor', () => {
describe('GetDatasourceReleasesSpanProcessor', () => {
it('creates an instance', async () => {
const processor = new GetDatasourceReleasesSpanProcessor();
expect(processor).toBeInstanceOf(GetDatasourceReleasesSpanProcessor);
await expect(processor.forceFlush()).resolves.toBeUndefined();
expect(processor.onStart(partial(), partial())).toBeUndefined();
await expect(processor.shutdown()).resolves.toBeUndefined();
});
it('writes span datapoints to GetDatasourceReleasesStats', () => {
const writeMock = vi.mocked(stats.GetDatasourceReleasesStats.write);
const processor = new GetDatasourceReleasesSpanProcessor();
processor.onEnd(
partial<ReadableSpan>({
ended: true,
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
[ATTR_RENOVATE_DATASOURCE]: 'npm',
[ATTR_RENOVATE_REGISTRY_URL]: 'https://registry.npmjs.org',
[ATTR_RENOVATE_PACKAGE_NAME]: 'lodash',
},
duration: [1, 500_123_000], // 1.500123 seconds = 1500.123ms
}),
);
expect(writeMock).toHaveBeenCalledOnce();
expect(writeMock).toHaveBeenCalledWith(
'npm',
'https://registry.npmjs.org',
'lodash',
1500.123,
);
});
it('defaults registryUrl to an empty string if not provided', () => {
const writeMock = vi.mocked(stats.GetDatasourceReleasesStats.write);
const processor = new GetDatasourceReleasesSpanProcessor();
processor.onEnd(
partial<ReadableSpan>({
ended: true,
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
[ATTR_RENOVATE_DATASOURCE]: 'npm',
[ATTR_RENOVATE_PACKAGE_NAME]: 'lodash',
},
duration: [1, 0],
}),
);
expect(writeMock).toHaveBeenCalledOnce();
expect(writeMock).toHaveBeenCalledWith('npm', '', 'lodash', 1000);
});
interface NoWriteTestCase {
name: string;
span: Partial<ReadableSpan>;
}
const noWriteTestCases: NoWriteTestCase[] = [
{
name: 'span is not ended',
span: {
ended: false,
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
[ATTR_RENOVATE_DATASOURCE]: 'npm',
[ATTR_RENOVATE_REGISTRY_URL]: 'https://registry.npmjs.org',
[ATTR_RENOVATE_PACKAGE_NAME]: 'lodash',
},
duration: [1, 0],
},
},
{
name: 'function name is not getReleases',
span: {
ended: true,
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'somethingElse',
[ATTR_RENOVATE_DATASOURCE]: 'npm',
[ATTR_RENOVATE_REGISTRY_URL]: 'https://registry.npmjs.org',
[ATTR_RENOVATE_PACKAGE_NAME]: 'lodash',
},
duration: [1, 0],
},
},
{
name: 'datasource is not provided',
span: {
ended: true,
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
[ATTR_RENOVATE_REGISTRY_URL]: 'https://registry.npmjs.org',
[ATTR_RENOVATE_PACKAGE_NAME]: 'lodash',
},
duration: [1, 0],
},
},
{
name: 'package name is not provided',
span: {
ended: true,
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
[ATTR_RENOVATE_DATASOURCE]: 'npm',
[ATTR_RENOVATE_REGISTRY_URL]: 'https://registry.npmjs.org',
},
duration: [1, 0],
},
},
];
test.each(noWriteTestCases)(
'does not write span datapoints to GetDatasourceReleasesStats if $name',
({ span }) => {
const writeMock = vi.mocked(stats.GetDatasourceReleasesStats.write);
const processor = new GetDatasourceReleasesSpanProcessor();
processor.onEnd(partial<ReadableSpan>(span));
expect(writeMock).not.toHaveBeenCalled();
},
);
});
});