Files
Sebastian Poxhofer c41b46bac3 feat(logging): do not use dynamic fields in log messages with severity WARN or higher (#44560)
* ci(linter): add renovate/logger-static-message rule

Adds a custom oxlint rule that enforces a static msg component for
logger.warn/error/fatal calls (including logger.once variants) so the
messages can be grouped in metrics and error catching services, per
docs/development/best-practices.md. It also flags metadata properties
named 'error' that hold error-ish values, because the bunyan serializer
only handles the 'err' key.

The rule is enabled as an error for lib/ production code and disabled
for spec files. All 33 pre-existing violations are fixed by moving
interpolated values into the metadata object, making messages static,
and renaming 'error' metadata keys to 'err'. Affected log-message
assertions in co-located specs are updated accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: ignore validator output

* fixup new merged in log messages

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 06:53:37 +00:00

102 lines
2.7 KiB
TypeScript

import { isNullOrUndefined } from '@sindresorhus/is';
import { logger } from '../../../logger/index.ts';
import { getExpression } from '../../../util/jsonata.ts';
import { Datasource } from '../datasource.ts';
import type {
DigestConfig,
GetReleasesConfig,
ReleaseResult,
} from '../types.ts';
import { fetchers } from './formats/index.ts';
import { ReleaseResultZod } from './schema.ts';
import { getCustomConfig } from './utils.ts';
export class CustomDatasource extends Datasource {
static readonly id = 'custom';
override customRegistrySupport = true;
constructor() {
super(CustomDatasource.id);
}
async getReleases(
getReleasesConfig: GetReleasesConfig,
): Promise<ReleaseResult | null> {
const config = getCustomConfig(getReleasesConfig);
if (isNullOrUndefined(config)) {
return null;
}
const { defaultRegistryUrlTemplate, transformTemplates, format } = config;
const fetcher = fetchers[format];
const isLocalRegistry = defaultRegistryUrlTemplate.startsWith('file://');
let data: unknown;
try {
if (isLocalRegistry) {
data = await fetcher.readFile(
defaultRegistryUrlTemplate.replace('file://', ''),
);
} else {
data = await fetcher.fetch(this.http, defaultRegistryUrlTemplate);
}
} catch (e) {
this.handleHttpErrors(e);
return null;
}
logger.trace(
{ data },
`Custom datasource API fetcher '${format}' received data. Starting transformation.`,
);
for (const transformTemplate of transformTemplates) {
const expression = getExpression(transformTemplate);
if (expression instanceof Error) {
logger.once.warn(
{ errorMessage: expression.message, transformTemplate },
'Invalid JSONata expression',
);
return null;
}
try {
const modifiedData = await expression.evaluate(data);
logger.trace(
{ before: data, after: modifiedData },
`Custom datasource transformed data.`,
);
data = modifiedData;
} catch (err) {
logger.once.warn(
{ err, transformTemplate },
'Error while evaluating JSONata expression',
);
return null;
}
}
try {
const parsed = ReleaseResultZod.parse(data);
return structuredClone(parsed);
} catch (err) {
logger.debug({ err }, `Response has failed validation`);
logger.trace({ data }, 'Response that has failed validation');
return null;
}
}
override getDigest(
_cfg: DigestConfig,
_newValue?: string,
): Promise<string | null> {
// Return null here to support setting a digest: value can be provided digest in getReleases
return Promise.resolve(null);
}
}