UNPKG

6.74 kBPlain TextView Raw
1import { fileURLToPath, URL } from 'url';
2
3import { LogLevel } from '@stryker-mutator/api/core';
4import log4js from 'log4js';
5
6import { netUtils } from '../utils/net-utils.js';
7
8import { LoggingClientContext } from './logging-client-context.js';
9import { minLevel } from './log-utils.js';
10
11const enum AppenderName {
12 File = 'file',
13 FilterLevelFile = 'filterLevelFile',
14 FilterLog4jsCategoryFile = 'filterLog4jsCategoryFile',
15 Console = 'console',
16 FilterLevelConsole = 'filterLevelConsole',
17 StripAnsi = 'stripAnsi',
18 FilterLog4jsCategoryConsole = 'filterLog4jsCategoryConsole',
19 All = 'all',
20 Server = 'server',
21}
22
23const layouts: { color: log4js.PatternLayout; noColor: log4js.PatternLayout } = {
24 color: {
25 pattern: '%[%r (%z) %p %c%] %m',
26 type: 'pattern',
27 },
28 noColor: {
29 pattern: '%r (%z) %p %c %m',
30 type: 'pattern',
31 },
32};
33
34type AppendersConfiguration = Partial<Record<AppenderName, log4js.Appender>>;
35
36const LOG_FILE_NAME = 'stryker.log';
37export class LogConfigurator {
38 private static createMainProcessAppenders(consoleLogLevel: LogLevel, fileLogLevel: LogLevel, allowConsoleColors: boolean): AppendersConfiguration {
39 // Add the custom "multiAppender": https://log4js-node.github.io/log4js-node/appenders.html#other-appenders
40 const multiAppender = {
41 type: fileURLToPath(new URL('../cjs/logging/multi-appender.js', import.meta.url)),
42 appenders: [AppenderName.FilterLevelConsole],
43 };
44
45 const consoleLayout = allowConsoleColors ? layouts.color : layouts.noColor;
46
47 let allAppenders: AppendersConfiguration = {
48 [AppenderName.Console]: { type: 'stdout', layout: consoleLayout },
49 // Exclude messages like: "ERROR log4js A worker log process hung up unexpectedly" #1245
50 [AppenderName.FilterLog4jsCategoryConsole]: { type: 'categoryFilter', appender: AppenderName.Console, exclude: 'log4js' },
51 [AppenderName.FilterLevelConsole]: { type: 'logLevelFilter', appender: AppenderName.FilterLog4jsCategoryConsole, level: consoleLogLevel },
52 [AppenderName.All]: multiAppender,
53 };
54
55 // only add file if it is needed. Otherwise log4js will create the file directly, pretty annoying.
56 if (fileLogLevel.toUpperCase() !== LogLevel.Off.toUpperCase()) {
57 // Add the custom "multiAppender": https://log4js-node.github.io/log4js-node/appenders.html#other-appenders
58 const stripAnsiAppender = {
59 type: fileURLToPath(new URL('../cjs/logging/strip-ansi-appender.js', import.meta.url)),
60 appender: AppenderName.File,
61 };
62 const fileAppender: log4js.FileAppender = { type: 'file', filename: LOG_FILE_NAME, layout: layouts.noColor };
63 const filterLog4sCategory: log4js.CategoryFilterAppender = { type: 'categoryFilter', appender: AppenderName.StripAnsi, exclude: 'log4js' };
64 const filterFileAppender: log4js.LogLevelFilterAppender = {
65 type: 'logLevelFilter',
66 appender: AppenderName.FilterLog4jsCategoryFile,
67 level: fileLogLevel,
68 };
69
70 // Don't simply add the appenders, instead actually make sure they are ordinal "before" the others.
71 // See https://github.com/log4js-node/log4js-node/issues/746
72 allAppenders = {
73 ...allAppenders,
74 [AppenderName.File]: fileAppender,
75 [AppenderName.StripAnsi]: stripAnsiAppender,
76 [AppenderName.FilterLog4jsCategoryFile]: filterLog4sCategory,
77 [AppenderName.FilterLevelFile]: filterFileAppender,
78 };
79
80 multiAppender.appenders.push(AppenderName.FilterLevelFile);
81 }
82
83 return allAppenders;
84 }
85
86 private static createLog4jsConfig(defaultLogLevel: LogLevel, appenders: AppendersConfiguration): log4js.Configuration {
87 return {
88 appenders,
89 categories: {
90 default: {
91 appenders: [AppenderName.All],
92 level: defaultLogLevel,
93 },
94 },
95 };
96 }
97
98 /**
99 * Configure logging for the master process. Either call this method or `configureChildProcess` before any `getLogger` calls.
100 * @param consoleLogLevel The log level to configure for the console
101 * @param fileLogLevel The log level to configure for the "stryker.log" file
102 */
103 public static configureMainProcess(
104 consoleLogLevel: LogLevel = LogLevel.Information,
105 fileLogLevel: LogLevel = LogLevel.Off,
106 allowConsoleColors = true
107 ): void {
108 const appenders = this.createMainProcessAppenders(consoleLogLevel, fileLogLevel, allowConsoleColors);
109 log4js.configure(this.createLog4jsConfig(minLevel(consoleLogLevel, fileLogLevel), appenders));
110 }
111
112 /**
113 * Configure the logging for the server. Includes the master configuration.
114 * This method should only be called ONCE, as it starts the log4js server to listen for log events.
115 * It returns the logging client context that should be used to configure the child processes.
116 *
117 * @param consoleLogLevel the console log level
118 * @param fileLogLevel the file log level
119 * @returns the context
120 */
121 public static async configureLoggingServer(
122 consoleLogLevel: LogLevel,
123 fileLogLevel: LogLevel,
124 allowConsoleColors: boolean
125 ): Promise<LoggingClientContext> {
126 const loggerPort = await netUtils.getFreePort();
127
128 // Include the appenders for the main Stryker process, as log4js has only one single `configure` method.
129 const appenders = this.createMainProcessAppenders(consoleLogLevel, fileLogLevel, allowConsoleColors);
130 const multiProcessAppender: log4js.MultiprocessAppender = {
131 appender: AppenderName.All,
132 loggerPort,
133 mode: 'master',
134 type: 'multiprocess',
135 };
136 appenders[AppenderName.Server] = multiProcessAppender;
137 const defaultLogLevel = minLevel(consoleLogLevel, fileLogLevel);
138 log4js.configure(this.createLog4jsConfig(defaultLogLevel, appenders));
139
140 const context: LoggingClientContext = {
141 level: defaultLogLevel,
142 port: loggerPort,
143 };
144 return context;
145 }
146
147 /**
148 * Configures the logging for a worker process. Sends all logging to the master process.
149 * Either call this method or `configureMainProcess` before any `getLogger` calls.
150 * @param context the logging client context used to configure the logging client
151 */
152 public static configureChildProcess(context: LoggingClientContext): void {
153 const clientAppender: log4js.MultiprocessAppender = { type: 'multiprocess', mode: 'worker', loggerPort: context.port };
154 const appenders: AppendersConfiguration = { [AppenderName.All]: clientAppender };
155 log4js.configure(this.createLog4jsConfig(context.level, appenders));
156 }
157
158 public static shutdown(): Promise<void> {
159 return new Promise((res, rej) => {
160 log4js.shutdown((err) => {
161 if (err) {
162 rej(err);
163 } else {
164 res();
165 }
166 });
167 });
168 }
169}