Skip to content

Commit b28f5be

Browse files
feat: use only log4js methods in CLI logger
Refactor the logic in CLI's logger to use log4js methods only - currently we have some methods that use `console.log`, others use `process.stdout`, `process.stderr`, etc. Also, there are some methods that apply console colorization for the message, but in fact CLI may be used as a library or in CI, where these colorization does not make sense. To resolve this, introduce a new appender and new layout that CLI will pass to `log4js`. The appender (called `cli-appender`) will control if the message will be shown on `stdout` or `stderr`. The layout (called `cli-layout`) will control the presentation of the message on the terminal, i.e. if we should add new line at the end of the message, if we should wrap it in borders (with `*` symbols, etc.) and the colorization of the message. The layout will be applied on all methods, so you can use it with any log4js method, i.e. you can do: ```TypeScript this.$logger.warn("This is my message", { useStderr: true }); this.$logger.info("This is my message", { useStderr: true }); ``` Before passing the data to `log4js`, CLI will set required properties in the context of the logging event, so the appender and the layout can read them. This way, in case CLI is used as a library, the custom properties and CLI specific layout, will not be shown. Also delete some methods from logger API, so it is streamlined. Also change logger.error to print on stderr instead of stdout. Move logger.ts to logger directory, so all logger logic will be on a single place.
1 parent 449e8a7 commit b28f5be

File tree

12 files changed

+171
-107
lines changed

12 files changed

+171
-107
lines changed

lib/bootstrap.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require("./common/bootstrap");
2-
$injector.requirePublicClass("logger", "./common/logger");
2+
$injector.requirePublicClass("logger", "./common/logger/logger");
33
$injector.require("config", "./config");
44
$injector.require("options", "./options");
55
// note: order above is important!

lib/common/definitions/logger.d.ts

+1-7
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,16 @@ declare global {
1414

1515
interface ILogger {
1616
initialize(opts?: ILoggerOptions): void;
17+
initializeCliLogger(): void;
1718
getLevel(): string;
1819
fatal(formatStr?: any, ...args: any[]): void;
1920
error(formatStr?: any, ...args: any[]): void;
2021
warn(formatStr?: any, ...args: any[]): void;
21-
warnWithLabel(formatStr?: any, ...args: any[]): void;
2222
info(formatStr?: any, ...args: any[]): void;
2323
debug(formatStr?: any, ...args: any[]): void;
2424
trace(formatStr?: any, ...args: any[]): void;
2525
printMarkdown(...args: any[]): void;
26-
27-
out(formatStr?: any, ...args: any[]): void;
28-
write(...args: any[]): void;
29-
3026
prepare(item: any): string;
31-
printInfoMessageOnSameLine(message: string): void;
32-
printOnStderr(formatStr?: any, ...args: any[]): void;
3327
}
3428

3529

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { LoggingEvent } from "log4js";
2+
import { LoggerConfigData } from "../../../constants";
3+
4+
function cliAppender(layout: Function) {
5+
const appender = (loggingEvent: LoggingEvent) => {
6+
if (loggingEvent && loggingEvent.data) {
7+
const stream = loggingEvent.context[LoggerConfigData.useStderr] ? process.stderr : process.stdout;
8+
const preparedData = layout(loggingEvent);
9+
stream.write(preparedData);
10+
}
11+
};
12+
13+
return appender;
14+
}
15+
16+
function configure(config: Log4JSEmitAppenderConfiguration, layouts: any) {
17+
// the default layout for the appender
18+
let layout = layouts.messagePassThroughLayout;
19+
20+
// check if there is another layout specified
21+
if (config.layout) {
22+
layout = layouts.layout(config.layout.type, config.layout);
23+
}
24+
25+
// create a new appender instance
26+
return cliAppender(layout);
27+
}
28+
29+
exports.configure = configure;

lib/common/logger-appenders/emit-appender.ts renamed to lib/common/logger/appenders/emit-appender.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { LoggingEvent } from "log4js";
22
import { EventEmitter } from "events";
3+
import { EmitAppenderEventName } from "../../../constants";
34

4-
const logDataEventName = "logData";
55
function emitAppender(layout: Function, emitter: EventEmitter) {
66
const appender = (loggingEvent: LoggingEvent) => {
7-
emitter.emit(logDataEventName, { loggingEvent, formattedMessage: layout(loggingEvent) });
7+
emitter.emit(EmitAppenderEventName, { loggingEvent, formattedMessage: layout(loggingEvent) });
88
};
99

1010
appender.shutdown = () => {
11-
emitter.removeAllListeners(logDataEventName);
11+
emitter.removeAllListeners(EmitAppenderEventName);
1212
};
1313

1414
return appender;
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { format } from "util";
2+
import { getMessageWithBorders } from "../../helpers";
3+
import { LoggingEvent } from "log4js";
4+
import { LoggerConfigData, LoggerLevel } from "../../../constants";
5+
import { EOL } from "os";
6+
7+
export function layout(config: any) {
8+
return function (logEvent: LoggingEvent): string {
9+
let msg = format.apply(null, logEvent.data);
10+
11+
if (logEvent.context[LoggerConfigData.wrapMessageWithBorders]) {
12+
msg = getMessageWithBorders(msg);
13+
}
14+
15+
if (!logEvent.context[LoggerConfigData.skipNewLine]) {
16+
msg += EOL;
17+
}
18+
19+
if (logEvent.level.isEqualTo(LoggerLevel.INFO)) {
20+
return msg;
21+
}
22+
23+
if (logEvent.level.isEqualTo(LoggerLevel.ERROR)) {
24+
return msg.red.bold;
25+
}
26+
27+
if (logEvent.level.isEqualTo(LoggerLevel.WARN)) {
28+
return msg.yellow;
29+
}
30+
31+
return msg;
32+
};
33+
}

lib/common/logger.ts renamed to lib/common/logger/logger.ts

+69-53
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,16 @@ import * as log4js from "log4js";
22
import * as util from "util";
33
import * as stream from "stream";
44
import * as marked from "marked";
5-
import { cache } from "./decorators";
5+
import { cache } from "../decorators";
6+
import { layout } from "./layouts/cli-layout";
7+
import { LoggerConfigData, LoggerLevel, LoggerAppenders } from "../../constants";
68
const TerminalRenderer = require("marked-terminal");
79
const chalk = require("chalk");
810

911
export class Logger implements ILogger {
1012
private log4jsLogger: log4js.Logger = null;
1113
private passwordRegex = /(password=).*?(['&,]|$)|(password["']?\s*:\s*["']).*?(["'])/i;
1214
private passwordReplacement = "$1$3*******$2$4";
13-
private static LABEL = "[WARNING]:";
1415

1516
constructor(private $config: Config.IConfig,
1617
private $options: IOptions) {
@@ -48,68 +49,49 @@ export class Logger implements ILogger {
4849
this.log4jsLogger = log4js.getLogger();
4950
}
5051

51-
getLevel(): string {
52-
this.initialize();
52+
public initializeCliLogger(): void {
53+
log4js.addLayout("cli", layout);
5354

54-
return this.log4jsLogger.level.toString();
55+
this.initialize({
56+
appenderOptions: { type: LoggerAppenders.cliAppender, layout: { type: "cli" } },
57+
level: <any>this.$options.log
58+
});
5559
}
5660

57-
fatal(...args: string[]): void {
61+
getLevel(): string {
5862
this.initialize();
5963

60-
this.log4jsLogger.fatal.apply(this.log4jsLogger, args);
64+
return this.log4jsLogger.level.toString();
6165
}
6266

63-
error(...args: string[]): void {
64-
this.initialize();
65-
66-
const message = util.format.apply(null, args);
67-
const colorizedMessage = message.red;
68-
69-
this.log4jsLogger.error.apply(this.log4jsLogger, [colorizedMessage]);
67+
fatal(...args: any[]): void {
68+
this.logMessage(args, LoggerLevel.FATAL);
7069
}
7170

72-
warn(...args: string[]): void {
73-
this.initialize();
74-
75-
const message = util.format.apply(null, args);
76-
const colorizedMessage = message.yellow;
77-
78-
this.log4jsLogger.warn.apply(this.log4jsLogger, [colorizedMessage]);
71+
error(...args: any[]): void {
72+
args.push({ [LoggerConfigData.useStderr]: true });
73+
this.logMessage(args, LoggerLevel.ERROR);
7974
}
8075

81-
warnWithLabel(...args: string[]): void {
82-
this.initialize();
83-
84-
const message = util.format.apply(null, args);
85-
this.warn(`${Logger.LABEL} ${message}`);
76+
warn(...args: any[]): void {
77+
this.logMessage(args, LoggerLevel.WARN);
8678
}
8779

88-
info(...args: string[]): void {
89-
this.initialize();
90-
91-
this.log4jsLogger.info.apply(this.log4jsLogger, args);
80+
info(...args: any[]): void {
81+
this.logMessage(args, LoggerLevel.INFO);
9282
}
9383

94-
debug(...args: string[]): void {
95-
this.initialize();
96-
84+
debug(...args: any[]): void {
9785
const encodedArgs: string[] = this.getPasswordEncodedArguments(args);
98-
this.log4jsLogger.debug.apply(this.log4jsLogger, encodedArgs);
86+
this.logMessage(encodedArgs, LoggerLevel.DEBUG);
9987
}
10088

101-
trace(...args: string[]): void {
102-
this.initialize();
103-
89+
trace(...args: any[]): void {
10490
const encodedArgs: string[] = this.getPasswordEncodedArguments(args);
105-
this.log4jsLogger.trace.apply(this.log4jsLogger, encodedArgs);
106-
}
107-
108-
out(...args: string[]): void {
109-
console.log(util.format.apply(null, args));
91+
this.logMessage(encodedArgs, LoggerLevel.TRACE);
11092
}
11193

112-
write(...args: string[]): void {
94+
write(...args: any[]): void {
11395
process.stdout.write(util.format.apply(null, args));
11496
}
11597

@@ -133,12 +115,6 @@ export class Logger implements ILogger {
133115
return JSON.stringify(item);
134116
}
135117

136-
public printInfoMessageOnSameLine(message: string): void {
137-
if (!this.$options.log || this.$options.log === "info") {
138-
this.write(message);
139-
}
140-
}
141-
142118
public printMarkdown(...args: string[]): void {
143119
const opts = {
144120
unescape: true,
@@ -160,13 +136,53 @@ export class Logger implements ILogger {
160136
marked.setOptions({ renderer: new TerminalRenderer(opts) });
161137

162138
const formattedMessage = marked(util.format.apply(null, args));
163-
this.write(formattedMessage);
139+
this.info(formattedMessage, { [LoggerConfigData.skipNewLine]: true });
164140
}
165141

166-
public printOnStderr(...args: string[]): void {
167-
if (process.stderr) {
168-
process.stderr.write(util.format.apply(null, args));
142+
private logMessage(inputData: any[], logMethod: string): void {
143+
this.initialize();
144+
145+
const logOpts = this.getLogOptionsForMessage(inputData);
146+
const data = logOpts.data;
147+
delete logOpts.data;
148+
149+
for (const prop in logOpts) {
150+
this.log4jsLogger.addContext(prop, logOpts[prop]);
169151
}
152+
153+
(<IDictionary<any>>this.log4jsLogger)[logMethod.toLowerCase()].apply(this.log4jsLogger, data);
154+
155+
for (const prop in logOpts) {
156+
this.log4jsLogger.removeContext(prop);
157+
}
158+
}
159+
160+
private getLogOptionsForMessage(data: any[]): { data: any[], [key: string]: any } {
161+
const opts = _.keys(LoggerConfigData);
162+
163+
const result: any = {};
164+
const cleanedData = _.cloneDeep(data);
165+
166+
const dataToCheck = data.filter(el => typeof el === "object");
167+
168+
for (const element of dataToCheck) {
169+
if (opts.length === 0) {
170+
break;
171+
}
172+
173+
const remainingOpts = _.cloneDeep(opts);
174+
for (const prop of remainingOpts) {
175+
const hasProp = element && element.hasOwnProperty(prop);
176+
if (hasProp) {
177+
opts.splice(opts.indexOf(prop), 1);
178+
result[prop] = element[prop];
179+
cleanedData.splice(cleanedData.indexOf(element), 1);
180+
}
181+
}
182+
}
183+
184+
result.data = cleanedData;
185+
return result;
170186
}
171187

172188
private getPasswordEncodedArguments(args: string[]): string[] {

lib/common/test/unit-tests/logger.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Yok } from "../../yok";
2-
import { Logger } from "../../logger";
2+
import { Logger } from "../../logger/logger";
33
import * as path from "path";
44
import { assert } from "chai";
55
import * as fileSystemFile from "../../file-system";

lib/common/test/unit-tests/stubs.ts

+11-22
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import * as util from "util";
44
import { EventEmitter } from "events";
5+
import { LoggerConfigData } from "../../../constants";
56

67
export class LockServiceStub implements ILockService {
78
public async lock(lockFilePath?: string, lockOpts?: ILockOptions): Promise<() => void> {
@@ -19,43 +20,31 @@ export class LockServiceStub implements ILockService {
1920

2021
export class CommonLoggerStub implements ILogger {
2122
initialize(opts?: ILoggerOptions): void { }
23+
initializeCliLogger(): void { }
2224
getLevel(): string { return undefined; }
23-
fatal(...args: string[]): void { }
24-
error(...args: string[]): void { }
25-
warn(...args: string[]): void {
26-
this.out.apply(this, args);
25+
fatal(...args: any[]): void { }
26+
error(...args: any[]): void { }
27+
warn(...args: any[]): void {
28+
this.output += util.format.apply(null, args) + "\n";
2729
}
28-
warnWithLabel(...args: string[]): void { }
29-
info(...args: string[]): void {
30-
this.out.apply(this, args);
30+
info(...args: any[]): void {
31+
this.output += util.format.apply(null, args) + "\n";
3132
}
32-
debug(...args: string[]): void { }
33-
trace(...args: string[]): void {
33+
debug(...args: any[]): void { }
34+
trace(...args: any[]): void {
3435
this.traceOutput += util.format.apply(null, args) + "\n";
3536
}
3637

3738
public output = "";
3839
public traceOutput = "";
3940

40-
out(...args: string[]): void {
41-
this.output += util.format.apply(null, args) + "\n";
42-
}
43-
44-
write(...args: string[]): void { }
45-
4641
prepare(item: any): string {
4742
return "";
4843
}
4944

50-
printInfoMessageOnSameLine(message: string): void { }
51-
5245
printMarkdown(message: string): void {
5346
this.output += message;
5447
}
55-
56-
printOnStderr(...args: string[]): void {
57-
// nothing to do here
58-
}
5948
}
6049

6150
export class ErrorsStub implements IErrors {
@@ -168,7 +157,7 @@ export class DeviceLogProviderStub extends EventEmitter implements Mobile.IDevic
168157
public currentDeviceProjectNames: IStringDictionary = {};
169158

170159
logData(line: string, platform: string, deviceIdentifier: string): void {
171-
this.logger.write(line, platform, deviceIdentifier);
160+
this.logger.info(line, platform, deviceIdentifier, { [LoggerConfigData.skipNewLine]: true });
172161
}
173162

174163
setLogLevel(level: string, deviceIdentifier?: string): void {

lib/constants.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,10 @@ export enum IOSNativeTargetTypes {
301301
appExtension = "app_extension"
302302
}
303303

304+
const pathToLoggerAppendersDir = join(__dirname, "common", "logger", "appenders");
304305
export const LoggerAppenders = {
305-
emitAppender: join(__dirname, "common", "logger-appenders", "emit-appender"),
306+
emitAppender: join(pathToLoggerAppendersDir, "emit-appender"),
307+
cliAppender: join(pathToLoggerAppendersDir, "cli-appender")
306308
};
307309

308310
export enum LoggerLevel {
@@ -362,3 +364,11 @@ export enum LoggerLevel {
362364
*/
363365
OFF = "OFF"
364366
}
367+
368+
export enum LoggerConfigData {
369+
useStderr = "useStderr",
370+
wrapMessageWithBorders = "wrapMessageWithBorders",
371+
skipNewLine = "skipNewLine"
372+
}
373+
374+
export const EMIT_APPENDER_EVENT_NAME = "logData";

0 commit comments

Comments
 (0)