diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 0efc6ec42e15012e113507bd8a20f278e461ad91..a84fe56d306eb3cf728e2e7a1a1dbead303b2e0e 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -1796,6 +1796,21 @@ You can configure a different maximum value in seconds using `maxRetryAfter`: } ``` +### newLogLevel + +For log level remapping, `newLogLevel` will set for the particular log message: + +```json +{ + "logLevelRemap": [ + { + "matchMessage": "/Error executing maven wrapper update command/", + "newLogLevel": "warn" + } + ] +} +``` + ### dnsCache Enable got [dnsCache](https://github.com/sindresorhus/got/blob/v11.5.2/readme.md#dnsCache) support. @@ -2155,6 +2170,27 @@ To enable `lockFileMaintenance` add this to your configuration: To reduce "noise" in the repository, Renovate performs `lockFileMaintenance` `"before 4am on monday"`, i.e. to achieve once-per-week semantics. Depending on its running schedule, Renovate may run a few times within that time window - even possibly updating the lock file more than once - but it hopefully leaves enough time for tests to run and automerge to apply, if configured. +## logLevelRemap + +This option allows you to remap log levels for specific messages. + +Be careful with remapping `warn` or `error` messages to lower log levels, as it may hide important information. + +```json +{ + "logLevelRemap": [ + { + "matchMessage": "/^pip-compile:/", + "newLogLevel": "info" + }, + { + "matchMessage": "Package lookup error", + "newLogLevel": "warn" + } + ] +} +``` + ## major Add to this object if you wish to define rules that apply only to major updates. @@ -2588,6 +2624,26 @@ Use this field to restrict rules to a particular package manager. e.g. For the full list of available managers, see the [Supported Managers](modules/manager/index.md#supported-managers) documentation. +### matchMessage + +For log level remapping, use this field to match against the particular log messages. +You can match based on any of the following: + +- an exact match string (e.g. `This is the string`) +- a minimatch pattern (e.g. `This*`) +- a regex pattern (e.g. `/^This/`) + +```json +{ + "logLevelRemap": [ + { + "matchMessage": "Manager explicitly enabled*", + "newLogLevel": "warn" + } + ] +} +``` + ### matchDatasources Use this field to restrict rules to a particular datasource. e.g. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 214d2ec7ca335aeeded6453038b6b51573f4deb6..0c3203e4401f08cd607cfc924c977df6c3f2cd62 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -2818,6 +2818,32 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'logLevelRemap', + description: 'Remap log levels to different levels.', + type: 'array', + subType: 'object', + stage: 'repository', + cli: false, + env: false, + }, + { + name: 'matchMessage', + description: 'Regex/minimatch expression to match against log message.', + type: 'string', + parents: ['logLevelRemap'], + cli: false, + env: false, + }, + { + name: 'newLogLevel', + description: 'New log level to use if matchMessage matches.', + type: 'string', + allowedValues: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], + parents: ['logLevelRemap'], + cli: false, + env: false, + }, ]; export function getOptions(): RenovateOptions[] { diff --git a/lib/config/types.ts b/lib/config/types.ts index c18f8ace772fd44f252caee4f7673345120b84c9..dc9a690db41b7935fcaa070cac51b53e9035fefd 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -1,5 +1,6 @@ import type { LogLevel } from 'bunyan'; import type { PlatformId } from '../constants'; +import type { LogLevelRemap } from '../logger/types'; import type { CustomManager } from '../modules/manager/custom/types'; import type { HostRule } from '../types'; import type { GitNoVerifyOption } from '../util/git/types'; @@ -275,6 +276,8 @@ export interface RenovateConfig customizeDashboard?: Record<string, string>; statusCheckNames?: Record<StatusCheckKey, string | null>; + + logLevelRemap?: LogLevelRemap[]; } const CustomDatasourceFormats = ['json', 'plain', 'yaml', 'html'] as const; @@ -374,7 +377,8 @@ export type AllowedParents = | 'customDatasources' | 'hostRules' | 'postUpgradeTasks' - | 'packageRules'; + | 'packageRules' + | 'logLevelRemap'; export interface RenovateOptionBase { /** * If true, the option can only be configured by people with access to the Renovate instance. diff --git a/lib/logger/index.ts b/lib/logger/index.ts index 7e90ab4d841cea3b38f121ff5756cd8952e73055..54cb4ebf4e497adcecd8ff8df08abdafd86e6ad1 100644 --- a/lib/logger/index.ts +++ b/lib/logger/index.ts @@ -6,6 +6,7 @@ import configSerializer from './config-serializer'; import errSerializer from './err-serializer'; import { once, reset as onceReset } from './once'; import { RenovateStream } from './pretty-stdout'; +import { getRemappedLevel } from './remap'; import type { BunyanRecord, Logger } from './types'; import { ProblemStream, validateLogLevel, withSanitizer } from './utils'; @@ -61,20 +62,39 @@ const bunyanLogger = bunyan.createLogger({ ].map(withSanitizer), }); -const logFactory = - (level: bunyan.LogLevelString) => - (p1: any, p2: any): void => { +const logFactory = ( + _level: bunyan.LogLevelString, +): ((p1: unknown, p2: unknown) => void) => { + return (p1: any, p2: any): void => { + let level = _level; if (p2) { // meta and msg provided - bunyanLogger[level]({ logContext, ...curMeta, ...p1 }, p2); + const msg = p2; + const meta: Record<string, unknown> = { logContext, ...curMeta, ...p1 }; + const remappedLevel = getRemappedLevel(msg); + // istanbul ignore if: not testable + if (remappedLevel) { + meta.oldLevel = level; + level = remappedLevel; + } + bunyanLogger[level](meta, msg); } else if (is.string(p1)) { // only message provided - bunyanLogger[level]({ logContext, ...curMeta }, p1); + const msg = p1; + const meta: Record<string, unknown> = { logContext, ...curMeta }; + const remappedLevel = getRemappedLevel(msg); + // istanbul ignore if: not testable + if (remappedLevel) { + meta.oldLevel = level; + level = remappedLevel; + } + bunyanLogger[level](meta, msg); } else { // only meta provided bunyanLogger[level]({ logContext, ...curMeta, ...p1 }); } }; +}; const loggerLevels: bunyan.LogLevelString[] = [ 'trace', diff --git a/lib/logger/remap.spec.ts b/lib/logger/remap.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3e7608971e27c748d1441f81458486d60499a057 --- /dev/null +++ b/lib/logger/remap.spec.ts @@ -0,0 +1,72 @@ +import { + getRemappedLevel, + resetGlobalLogLevelRemaps, + resetRepositoryLogLevelRemaps, + setGlobalLogLevelRemaps, + setRepositoryLogLevelRemaps, +} from './remap'; + +describe('logger/remap', () => { + afterEach(() => { + resetRepositoryLogLevelRemaps(); + resetGlobalLogLevelRemaps(); + }); + + it('returns null if no remaps are set', () => { + setGlobalLogLevelRemaps(undefined); + setRepositoryLogLevelRemaps(undefined); + + const res = getRemappedLevel('foo'); + + expect(res).toBeNull(); + }); + + it('performs global remaps', () => { + setGlobalLogLevelRemaps([{ matchMessage: '*foo*', newLogLevel: 'error' }]); + setRepositoryLogLevelRemaps(undefined); + + const res = getRemappedLevel('foo'); + + expect(res).toBe('error'); + }); + + it('performs repository-level remaps', () => { + setGlobalLogLevelRemaps(undefined); + setRepositoryLogLevelRemaps([ + { matchMessage: '*bar*', newLogLevel: 'error' }, + ]); + + const res = getRemappedLevel('bar'); + + expect(res).toBe('error'); + }); + + it('prioritizes repository-level remaps over global remaps', () => { + setGlobalLogLevelRemaps([{ matchMessage: '*foo*', newLogLevel: 'error' }]); + setRepositoryLogLevelRemaps([ + { matchMessage: '*bar*', newLogLevel: 'warn' }, + ]); + + const res = getRemappedLevel('foobar'); + + expect(res).toBe('warn'); + }); + + it('supports regex patterns', () => { + setGlobalLogLevelRemaps([{ matchMessage: '/foo/', newLogLevel: 'error' }]); + setRepositoryLogLevelRemaps(undefined); + + const res = getRemappedLevel('foo'); + + expect(res).toBe('error'); + }); + + it('does not match against invalid regex patterns', () => { + setGlobalLogLevelRemaps([{ matchMessage: '/(/', newLogLevel: 'error' }]); + setRepositoryLogLevelRemaps(undefined); + + const res = getRemappedLevel('()'); + + expect(res).toBeNull(); + }); +}); diff --git a/lib/logger/remap.ts b/lib/logger/remap.ts new file mode 100644 index 0000000000000000000000000000000000000000..57920e2677ea003b6d57feaddf832466344b834c --- /dev/null +++ b/lib/logger/remap.ts @@ -0,0 +1,68 @@ +import type { LogLevelString } from 'bunyan'; +import { + StringMatchPredicate, + makeRegexOrMinimatchPredicate, +} from '../util/string-match'; +import type { LogLevelRemap } from './types'; + +let globalRemaps: LogLevelRemap[] | undefined; +let repositoryRemaps: LogLevelRemap[] | undefined; + +let matcherCache = new WeakMap<LogLevelRemap, StringMatchPredicate>(); + +function match(remap: LogLevelRemap, input: string): boolean { + const { matchMessage: pattern } = remap; + let matchFn = matcherCache.get(remap); + if (!matchFn) { + matchFn = makeRegexOrMinimatchPredicate(pattern) ?? (() => false); + matcherCache.set(remap, matchFn); + } + + return matchFn(input); +} + +export function getRemappedLevel(msg: string): LogLevelString | null { + if (repositoryRemaps) { + for (const remap of repositoryRemaps) { + if (match(remap, msg)) { + return remap.newLogLevel; + } + } + } + + if (globalRemaps) { + for (const remap of globalRemaps) { + if (match(remap, msg)) { + return remap.newLogLevel; + } + } + } + + return null; +} + +function resetMatcherCache(): void { + matcherCache = new WeakMap(); +} + +export function setGlobalLogLevelRemaps( + remaps: LogLevelRemap[] | undefined, +): void { + globalRemaps = remaps; +} + +export function resetGlobalLogLevelRemaps(): void { + globalRemaps = undefined; + resetMatcherCache(); +} + +export function setRepositoryLogLevelRemaps( + remaps: LogLevelRemap[] | undefined, +): void { + repositoryRemaps = remaps; +} + +export function resetRepositoryLogLevelRemaps(): void { + repositoryRemaps = undefined; + resetMatcherCache(); +} diff --git a/lib/logger/types.ts b/lib/logger/types.ts index 26fe16a73a814d67b8de56003eb3a5c9288c1b08..6880cc0f0c8901393a48d020c5f3752c6b12117d 100644 --- a/lib/logger/types.ts +++ b/lib/logger/types.ts @@ -1,5 +1,5 @@ import type { Stream } from 'node:stream'; -import type { LogLevel } from 'bunyan'; +import type { LogLevel, LogLevelString } from 'bunyan'; export interface LogError { level: LogLevel; @@ -40,3 +40,8 @@ export type BunyanStream = (NodeJS.WritableStream | Stream) & { cb: (err?: Error | null) => void, ) => void; }; + +export interface LogLevelRemap { + matchMessage: string; + newLogLevel: LogLevelString; +} diff --git a/lib/workers/global/index.ts b/lib/workers/global/index.ts index e2c2a83a2358af8e07e7197e030b348b42882b8c..cd285c06ee769354fda60d98fa5d1584bcc9ac4e 100644 --- a/lib/workers/global/index.ts +++ b/lib/workers/global/index.ts @@ -17,6 +17,7 @@ import { CONFIG_PRESETS_INVALID } from '../../constants/error-messages'; import { pkg } from '../../expose.cjs'; import { instrument } from '../../instrumentation'; import { getProblems, logger, setMeta } from '../../logger'; +import { setGlobalLogLevelRemaps } from '../../logger/remap'; import * as hostRules from '../../util/host-rules'; import * as queue from '../../util/http/queue'; import * as throttle from '../../util/http/throttle'; @@ -158,6 +159,8 @@ export async function start(): Promise<number> { // validate secrets. Will throw and abort if invalid validateConfigSecrets(config); + + setGlobalLogLevelRemaps(config.logLevelRemap); }); // autodiscover repositories (needs to come after platform initialization) diff --git a/lib/workers/global/initialize.ts b/lib/workers/global/initialize.ts index 41af7f9a5af9d7136f6dc1124a2473973d742abe..47c780b4ea4049e22ebb5e1b933a3b698be1be82 100644 --- a/lib/workers/global/initialize.ts +++ b/lib/workers/global/initialize.ts @@ -4,6 +4,7 @@ import upath from 'upath'; import { applySecretsToConfig } from '../../config/secrets'; import type { AllConfig, RenovateConfig } from '../../config/types'; import { logger } from '../../logger'; +import { resetGlobalLogLevelRemaps } from '../../logger/remap'; import { initPlatform } from '../../modules/platform'; import * as packageCache from '../../util/cache/package'; import { setEmojiConfig } from '../../util/emoji'; @@ -93,4 +94,5 @@ export async function globalInitialize( export async function globalFinalize(config: RenovateConfig): Promise<void> { await packageCache.cleanup(config); + resetGlobalLogLevelRemaps(); } diff --git a/lib/workers/repository/index.ts b/lib/workers/repository/index.ts index 6e775996ce0b55d1febb90fbbf2b938b36002a79..fa2294a0203b5dfd9091fcce6e1c12774c90652a 100644 --- a/lib/workers/repository/index.ts +++ b/lib/workers/repository/index.ts @@ -10,6 +10,7 @@ import { import { pkg } from '../../expose.cjs'; import { instrument } from '../../instrumentation'; import { logger, setMeta } from '../../logger'; +import { resetRepositoryLogLevelRemaps } from '../../logger/remap'; import { removeDanglingContainers } from '../../util/exec/docker'; import { deleteLocalFile, privateCacheDir } from '../../util/fs'; import { isCloned } from '../../util/git'; @@ -129,6 +130,7 @@ export async function renovateRepository( clearDnsCache(); const cloned = isCloned(); logger.info({ cloned, durationMs: splits.total }, 'Repository finished'); + resetRepositoryLogLevelRemaps(); return repoResult; } diff --git a/lib/workers/repository/init/index.ts b/lib/workers/repository/init/index.ts index 374b1ec8c81a31ad8359f67735bc1ec29828020a..81f05826c03f10bba1bacc9caf0f752d87a9f196 100644 --- a/lib/workers/repository/init/index.ts +++ b/lib/workers/repository/init/index.ts @@ -2,6 +2,7 @@ import { GlobalConfig } from '../../../config/global'; import { applySecretsToConfig } from '../../../config/secrets'; import type { RenovateConfig } from '../../../config/types'; import { logger } from '../../../logger'; +import { setRepositoryLogLevelRemaps } from '../../../logger/remap'; import { platform } from '../../../modules/platform'; import { clone } from '../../../util/clone'; import { cloneSubmodules, setUserRepoConfig } from '../../../util/git'; @@ -50,6 +51,7 @@ export async function initRepo( config = await initApis(config); await initializeCaches(config as WorkerPlatformConfig); config = await getRepoConfig(config); + setRepositoryLogLevelRemaps(config.logLevelRemap); checkIfConfigured(config); warnOnUnsupportedOptions(config); config = applySecretsToConfig(config);