diff --git a/docs/usage/config-overview.md b/docs/usage/config-overview.md index 4f5959c036bebad18afa0cb4825c4002cc50d854..8ed23f918d566bac23114e8af5876f8892826020 100644 --- a/docs/usage/config-overview.md +++ b/docs/usage/config-overview.md @@ -108,6 +108,8 @@ Read the [Self-hosted experimental environment variables](./self-hosted-experime Finally, there are some special environment variables that are loaded _before_ configuration parsing because they are used during logging initialization: - `LOG_CONTEXT`: a unique identifier used in each log message to track context +- `LOG_FILE`: used to enable file logging and specify the log file path +- `LOG_FILE_LEVEL`: log file logging level, defaults to `debug` - `LOG_FORMAT`: defaults to a "pretty" human-readable output, but can be changed to "json" - `LOG_LEVEL`: most commonly used to change from the default `info` to `debug` logging diff --git a/docs/usage/examples/self-hosting.md b/docs/usage/examples/self-hosting.md index 07397e80345d970d18330aa9c7921641adb6ad17..a4753c126286533cca19c3857cc51d7ac3015170 100644 --- a/docs/usage/examples/self-hosting.md +++ b/docs/usage/examples/self-hosting.md @@ -248,7 +248,7 @@ module.exports = { }; ``` -Here change the `logFile` and `repositories` to something appropriate. +Here change the `repositories` to something appropriate. Also replace `gitlab-token` value with the one created during the previous step. If you're running against GitHub Enterprise Server, then change the `gitlab` values in the example to the equivalent GitHub ones. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 0ef942e29a7aebe0e06921e82746a8bce09d9526..0a523e8e03626e20276f8720c29da90a8999c36d 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -548,6 +548,8 @@ const options: RenovateOptions[] = [ stage: 'global', type: 'string', globalOnly: true, + deprecationMsg: + 'Instead of configuring log file path in the file config. Use the `LOG_FILE` environment variable instead.', }, { name: 'logFileLevel', @@ -556,6 +558,8 @@ const options: RenovateOptions[] = [ type: 'string', default: 'debug', globalOnly: true, + deprecationMsg: + 'Instead of configuring log file level in the file config. Use the `LOG_FILE_LEVEL` environment variable instead.', }, { name: 'logContext', diff --git a/lib/config/validation.spec.ts b/lib/config/validation.spec.ts index 976c86b8c80f40c742b26c315260a46cc2852e96..410e5db26c0a5683525420975cb5817496063dd0 100644 --- a/lib/config/validation.spec.ts +++ b/lib/config/validation.spec.ts @@ -1388,6 +1388,23 @@ describe('config/validation', () => { }); describe('validateConfig() -> globaOnly options', () => { + it('returns deprecation warnings', async () => { + const config = { + logFile: 'something', + }; + const { warnings } = await configValidation.validateConfig( + 'global', + config, + ); + expect(warnings).toMatchObject([ + { + message: + 'Using logFile to specify log file name is deprecated now. Please use the enviroment variable LOG_FILE instead', + topic: 'Deprecation Warning', + }, + ]); + }); + it('validates hostRules.headers', async () => { const config = { hostRules: [ @@ -1501,6 +1518,23 @@ describe('config/validation', () => { }); describe('validate globalOptions()', () => { + it('binarySource', async () => { + const config = { + binarySource: 'invalid' as never, + }; + const { warnings } = await configValidation.validateConfig( + 'global', + config, + ); + expect(warnings).toEqual([ + { + message: + 'Invalid value `invalid` for `binarySource`. The allowed values are docker, global, install, hermit.', + topic: 'Configuration Error', + }, + ]); + }); + describe('validates string type options', () => { it('binarySource', async () => { const config = { diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 62d1aa6c3cd70b8f2b55438b15be598e3a85d3a0..abdee24bffc7857e986d73d6bee5a708329bd418 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -129,6 +129,8 @@ function getDeprecationMessage(option: string): string | undefined { branchName: `Direct editing of branchName is now deprecated. Please edit branchPrefix, additionalBranchPrefix, or branchTopic instead`, commitMessage: `Direct editing of commitMessage is now deprecated. Please edit commitMessage's subcomponents instead.`, prTitle: `Direct editing of prTitle is now deprecated. Please edit commitMessage subcomponents instead as they will be passed through to prTitle.`, + logFile: `Using logFile to specify log file name is deprecated now. Please use the enviroment variable LOG_FILE instead`, + logFileLevel: `Using logFileLevel to specify log level for file logging is deprecated now. Please use the enviroment variable LOG_FILE_LEVEL instead`, }; return deprecatedOptions[option]; } @@ -937,6 +939,12 @@ async function validateGlobalConfig( currentPath: string | undefined, config: RenovateConfig, ): Promise<void> { + if (getDeprecationMessage(key)) { + warnings.push({ + topic: 'Deprecation Warning', + message: getDeprecationMessage(key)!, + }); + } if (val !== null) { if (type === 'string') { if (is.string(val)) { diff --git a/lib/logger/index.ts b/lib/logger/index.ts index 54cb4ebf4e497adcecd8ff8df08abdafd86e6ad1..504e9b1a2c97e8b738238b5153eb6a284867d314 100644 --- a/lib/logger/index.ts +++ b/lib/logger/index.ts @@ -1,6 +1,8 @@ import is from '@sindresorhus/is'; import * as bunyan from 'bunyan'; +import fs from 'fs-extra'; import { nanoid } from 'nanoid'; +import upath from 'upath'; import cmdSerializer from './cmd-serializer'; import configSerializer from './config-serializer'; import errSerializer from './err-serializer'; @@ -20,16 +22,13 @@ if (is.string(process.env.LOG_LEVEL)) { process.env.LOG_LEVEL = process.env.LOG_LEVEL.toLowerCase().trim(); } -validateLogLevel(process.env.LOG_LEVEL); const stdout: bunyan.Stream = { name: 'stdout', - level: - (process.env.LOG_LEVEL as bunyan.LogLevel) || - /* istanbul ignore next: not testable */ 'info', + level: validateLogLevel(process.env.LOG_LEVEL, 'info'), stream: process.stdout, }; -// istanbul ignore else: not testable +// istanbul ignore if: not testable if (process.env.LOG_FORMAT !== 'json') { // TODO: typings (#9615) const prettyStdOut = new RenovateStream() as any; @@ -123,6 +122,19 @@ loggerLevels.forEach((loggerLevel) => { logger.once[loggerLevel] = logOnceFn as never; }); +// istanbul ignore if: not easily testable +if (is.string(process.env.LOG_FILE)) { + // ensure log file directory exists + const directoryName = upath.dirname(process.env.LOG_FILE); + fs.ensureDirSync(directoryName); + + addStream({ + name: 'logfile', + path: process.env.LOG_FILE, + level: validateLogLevel(process.env.LOG_FILE_LEVEL, 'debug'), + }); +} + export function setContext(value: string): void { logContext = value; } diff --git a/lib/logger/utils.spec.ts b/lib/logger/utils.spec.ts index 51b3ac222616bf9f0107b2e1a01bee0d970121a7..2073782a91c1c1af7836deb605b97d1ca37ea11b 100644 --- a/lib/logger/utils.spec.ts +++ b/lib/logger/utils.spec.ts @@ -11,11 +11,13 @@ describe('logger/utils', () => { }); it('checks for valid log levels', () => { - expect(validateLogLevel(undefined)).toBeUndefined(); - expect(validateLogLevel('warn')).toBeUndefined(); - expect(validateLogLevel('debug')).toBeUndefined(); - expect(validateLogLevel('trace')).toBeUndefined(); - expect(validateLogLevel('info')).toBeUndefined(); + expect(validateLogLevel(undefined, 'info')).toBe('info'); + expect(validateLogLevel('warn', 'info')).toBe('warn'); + expect(validateLogLevel('debug', 'info')).toBe('debug'); + expect(validateLogLevel('trace', 'info')).toBe('trace'); + expect(validateLogLevel('info', 'info')).toBe('info'); + expect(validateLogLevel('error', 'info')).toBe('error'); + expect(validateLogLevel('fatal', 'info')).toBe('fatal'); }); it.each` @@ -32,7 +34,7 @@ describe('logger/utils', () => { throw new Error(`process.exit: ${number}`); }); expect(() => { - validateLogLevel(input); + validateLogLevel(input, 'info'); }).toThrow(); expect(mockExit).toHaveBeenCalledWith(1); }); diff --git a/lib/logger/utils.ts b/lib/logger/utils.ts index dc49caa7f957ea4b9d02ee17f729da95455d1dc6..5fc70628769b093fce60bfdd3d48076aef419fe4 100644 --- a/lib/logger/utils.ts +++ b/lib/logger/utils.ts @@ -277,13 +277,16 @@ export function withSanitizer(streamConfig: bunyan.Stream): bunyan.Stream { } /** - * A function that terminates exeution if the log level that was entered is + * A function that terminates execution if the log level that was entered is * not a valid value for the Bunyan logger. * @param logLevelToCheck - * @returns returns undefined when the logLevelToCheck is valid. Else it stops execution. + * @returns returns the logLevel when the logLevelToCheck is valid or the defaultLevel passed as argument when it is undefined. Else it stops execution. */ -export function validateLogLevel(logLevelToCheck: string | undefined): void { - const allowedValues: bunyan.LogLevel[] = [ +export function validateLogLevel( + logLevelToCheck: string | undefined, + defaultLevel: bunyan.LogLevelString, +): bunyan.LogLevelString { + const allowedValues: bunyan.LogLevelString[] = [ 'trace', 'debug', 'info', @@ -291,13 +294,14 @@ export function validateLogLevel(logLevelToCheck: string | undefined): void { 'error', 'fatal', ]; + if ( is.undefined(logLevelToCheck) || (is.string(logLevelToCheck) && - allowedValues.includes(logLevelToCheck as bunyan.LogLevel)) + allowedValues.includes(logLevelToCheck as bunyan.LogLevelString)) ) { // log level is in the allowed values or its undefined - return; + return (logLevelToCheck as bunyan.LogLevelString) ?? defaultLevel; } const logger = bunyan.createLogger({ diff --git a/lib/workers/global/config/parse/index.spec.ts b/lib/workers/global/config/parse/index.spec.ts index 6a4e4169525d6979387a216e704027029133eccb..d13081cc8f944055482aab7ce65547d0448cb550 100644 --- a/lib/workers/global/config/parse/index.spec.ts +++ b/lib/workers/global/config/parse/index.spec.ts @@ -1,13 +1,12 @@ import upath from 'upath'; import { mocked } from '../../../../../test/util'; -import { readSystemFile } from '../../../../util/fs'; +import { getParentDir, readSystemFile } from '../../../../util/fs'; import getArgv from './__fixtures__/argv'; import * as _hostRulesFromEnv from './host-rules-from-env'; jest.mock('../../../../modules/datasource/npm'); jest.mock('../../../../util/fs'); jest.mock('./host-rules-from-env'); -jest.mock('../../config.js', () => ({}), { virtual: true }); const { hostRulesFromEnv } = mocked(_hostRulesFromEnv); @@ -174,9 +173,38 @@ describe('workers/global/config/parse/index', () => { expect(parsed).toContainEntries([['dryRun', null]]); }); + it('initalizes file logging when logFile is set and env vars LOG_FILE is undefined', async () => { + jest.doMock( + '../../../../../config.js', + () => ({ logFile: 'somepath', logFileLevel: 'debug' }), + { + virtual: true, + }, + ); + const env: NodeJS.ProcessEnv = {}; + const parsedConfig = await configParser.parseConfigs(env, defaultArgv); + expect(parsedConfig).not.toContain([['logFile', 'someFile']]); + expect(getParentDir).toHaveBeenCalledWith('somepath'); + }); + + it('skips initializing file logging when logFile is set but env vars LOG_FILE is defined', async () => { + jest.doMock( + '../../../../../config.js', + () => ({ logFile: 'somepath', logFileLevel: 'debug' }), + { + virtual: true, + }, + ); + const env: NodeJS.ProcessEnv = {}; + process.env.LOG_FILE = 'somepath'; + const parsedConfig = await configParser.parseConfigs(env, defaultArgv); + expect(parsedConfig).not.toContain([['logFile', 'someFile']]); + expect(getParentDir).not.toHaveBeenCalled(); + }); + it('massage onboardingNoDeps when autodiscover is false', async () => { - jest.mock( - '../../config.js', + jest.doMock( + '../../../../../config.js', () => ({ onboardingNoDeps: 'auto', autodiscover: false }), { virtual: true, diff --git a/lib/workers/global/config/parse/index.ts b/lib/workers/global/config/parse/index.ts index 5ccb5105efff5fb2a8b34a183f5db0a2074cfc4e..6d51625ace451a7bdc36799c7fe18d17a830cad0 100644 --- a/lib/workers/global/config/parse/index.ts +++ b/lib/workers/global/config/parse/index.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import * as defaultsParser from '../../../../config/defaults'; import type { AllConfig } from '../../../../config/types'; import { mergeChildConfig } from '../../../../config/utils'; @@ -67,8 +68,7 @@ export async function parseConfigs( } // Add file logger - // istanbul ignore if - if (config.logFile) { + if (config.logFile && is.undefined(process.env.LOG_FILE)) { logger.debug( // TODO: types (#22198) `Enabling ${config.logFileLevel!} logging to ${config.logFile}`,