Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/instrumentation/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
NodeTracerProvider,
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-node';
import { GetDatasourceReleasesSpanProcessor } from '../modules/datasource/span-processor.ts';
import { GitOperationSpanProcessor } from '../util/git/span-processor.ts';
import {
disableInstrumentations,
Expand Down Expand Up @@ -57,13 +58,14 @@ describe('instrumentation/index', () => {
_activeSpanProcessor: {
_spanProcessors: [
new GitOperationSpanProcessor(),
new GetDatasourceReleasesSpanProcessor(),
expect.any(SimpleSpanProcessor),
],
},
});
});

it('registers GitOperationSpanProcessor regardless of tracing being enabled', () => {
it('registers GitOperationSpanProcessor, GetDatasourceReleasesSpanProcessor regardless of tracing being enabled', () => {
// intentionally don't set it
delete process.env.RENOVATE_TRACING_CONSOLE_EXPORTER;
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
Expand All @@ -77,6 +79,7 @@ describe('instrumentation/index', () => {
_activeSpanProcessor: {
_spanProcessors: expect.arrayContaining([
new GitOperationSpanProcessor(),
new GetDatasourceReleasesSpanProcessor(),
]),
},
});
Expand All @@ -96,6 +99,7 @@ describe('instrumentation/index', () => {
_activeSpanProcessor: {
_spanProcessors: [
new GitOperationSpanProcessor(),
new GetDatasourceReleasesSpanProcessor(),
{
_exporter: {
_delegate: {
Expand Down Expand Up @@ -129,6 +133,7 @@ describe('instrumentation/index', () => {
_activeSpanProcessor: {
_spanProcessors: [
new GitOperationSpanProcessor(),
new GetDatasourceReleasesSpanProcessor(),
{ _exporter: {} },
{
_exporter: {
Expand Down
6 changes: 5 additions & 1 deletion lib/instrumentation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} 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 type { RenovateSpanOptions } from './types.ts';
Expand All @@ -39,7 +40,10 @@ import {
let instrumentations: Instrumentation[] = [];

export function init(): void {
const spanProcessors: SpanProcessor[] = [new GitOperationSpanProcessor()];
const spanProcessors: SpanProcessor[] = [
new GitOperationSpanProcessor(),
new GetDatasourceReleasesSpanProcessor(),
];

if (!isTracingEnabled()) {
const traceProvider = new NodeTracerProvider({ spanProcessors });
Expand Down
20 changes: 20 additions & 0 deletions lib/instrumentation/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Attributes, SpanKind, SpanOptions } from '@opentelemetry/api';
import type { ATTR_CODE_FUNCTION_NAME } from '@opentelemetry/semantic-conventions';
import type { RenovateSplit } from '../config/types.ts';
import type { BunyanRecord } from '../logger/types.ts';
import type { PackageFile } from '../modules/manager/types.ts';
Expand All @@ -12,6 +13,10 @@ export type RenovateSpanOptions = {
export type RenovateSpanAttributes = {
[ATTR_RENOVATE_SPLIT]?: RenovateSplit;
[ATTR_VCS_GIT_OPERATION_TYPE]?: GitOperationType;
[ATTR_CODE_FUNCTION_NAME]?: string;
[ATTR_RENOVATE_DATASOURCE]?: string;
[ATTR_RENOVATE_REGISTRY_URL]?: string;
[ATTR_RENOVATE_PACKAGE_NAME]?: string;
} & Attributes;

/**
Expand Down Expand Up @@ -68,6 +73,21 @@ export interface DependencyStatus {

export const ATTR_RENOVATE_SPLIT = 'renovate.split';

/**
* The name of a Renovate datasource (ex: `github-tags`, `npm`, `docker`, etc).
*/
export const ATTR_RENOVATE_DATASOURCE = 'renovate.datasource';

/**
* The registry URL of a registry URL as might be used with a datasource and package name.
*/
export const ATTR_RENOVATE_REGISTRY_URL = 'renovate.registryUrl';

/**
* The package name of a package.
*/
export const ATTR_RENOVATE_PACKAGE_NAME = 'renovate.packageName';

/**
* the Git Version Control System (VCS)'s Operation Type
*
Expand Down
33 changes: 31 additions & 2 deletions lib/modules/datasource/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { ATTR_CODE_FUNCTION_NAME } from '@opentelemetry/semantic-conventions';
import { isFunction, isNonEmptyArray, isString } from '@sindresorhus/is';
import { dequal } from 'dequal';
import { GlobalConfig } from '../../config/global.ts';
import { HOST_DISABLED } from '../../constants/error-messages.ts';
import { instrument } from '../../instrumentation/index.ts';
import {
ATTR_RENOVATE_DATASOURCE,
ATTR_RENOVATE_PACKAGE_NAME,
ATTR_RENOVATE_REGISTRY_URL,
} from '../../instrumentation/types.ts';
import { logger } from '../../logger/index.ts';
import { ExternalHostError } from '../../types/errors/external-host-error.ts';
import { coerceArray } from '../../util/array.ts';
Expand Down Expand Up @@ -86,7 +93,18 @@ async function getRegistryReleases(
DatasourceCacheStats.miss(datasource.id, registryUrl, config.packageName);
}

const res = await datasource.getReleases({ ...config, registryUrl });
const res = await instrument(
'getReleases',
Comment thread
jamietanna marked this conversation as resolved.
() => datasource.getReleases({ ...config, registryUrl }),
{
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
[ATTR_RENOVATE_DATASOURCE]: datasource.id,
[ATTR_RENOVATE_REGISTRY_URL]: registryUrl,
[ATTR_RENOVATE_PACKAGE_NAME]: config.packageName,
},
},
);
if (res?.releases.length) {
res.registryUrl ??= registryUrl;
}
Expand Down Expand Up @@ -371,7 +389,18 @@ async function fetchReleases(
dep = await mergeRegistries(config, datasource, registryUrls);
}
} else {
dep = await datasource.getReleases(config);
dep = await instrument(
'getReleases',
() => datasource.getReleases(config),
{
attributes: {
[ATTR_CODE_FUNCTION_NAME]: 'getReleases',
[ATTR_RENOVATE_DATASOURCE]: datasource.id,
[ATTR_RENOVATE_REGISTRY_URL]: config.registryUrl ?? '',
[ATTR_RENOVATE_PACKAGE_NAME]: config.packageName,
},
},
);
}
} catch (err) {
if (err.message === HOST_DISABLED || err.err?.message === HOST_DISABLED) {
Expand Down
138 changes: 138 additions & 0 deletions lib/modules/datasource/span-processor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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();
},
);
});
});
56 changes: 56 additions & 0 deletions lib/modules/datasource/span-processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Context } from '@opentelemetry/api';
import type {
ReadableSpan,
Span,
SpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import { ATTR_CODE_FUNCTION_NAME } from '@opentelemetry/semantic-conventions';
import {
ATTR_RENOVATE_DATASOURCE,
ATTR_RENOVATE_PACKAGE_NAME,
ATTR_RENOVATE_REGISTRY_URL,
} from '../../instrumentation/types.ts';
import { GetDatasourceReleasesStats } from '../../util/stats.ts';

export class GetDatasourceReleasesSpanProcessor implements SpanProcessor {
forceFlush(): Promise<void> {
return Promise.resolve();
}

onStart(_span: Span, _parentContext: Context): void {
// no implementation
}

onEnd(span: ReadableSpan): void {
if (!span.ended) {
return;
}

if (span.attributes[ATTR_CODE_FUNCTION_NAME] !== 'getReleases') {
return;
}

const datasource = span.attributes[ATTR_RENOVATE_DATASOURCE] as string;
const registryUrl = (span.attributes[ATTR_RENOVATE_REGISTRY_URL] ??
'') as string;
const packageName = span.attributes[ATTR_RENOVATE_PACKAGE_NAME] as string;

if (!datasource || !packageName) {
return;
}

// duration[0] is seconds, duration[1] is nanoseconds.
const durationMs = span.duration[0] * 1000 + span.duration[1] / 1_000_000;

GetDatasourceReleasesStats.write(
datasource,
registryUrl,
packageName,
durationMs,
);
}

shutdown(): Promise<void> {
return Promise.resolve();
}
}
Loading
Loading