Skip to content

Commit 6256958

Browse files
fix: logger fails to print null objects
In case you try printing the following: ``` const obj1 = null; this.$logger.info("value %s", obj1); ``` Our implementation fails with error: `Cannot read property hasOwnProperty of null`. The problem is in the way we check for passed logger options in the args passed to `logger` methods. The `typeof null` returns object, so next parts of the code fail. Also passed logger options are handled by priority, i.e. the first passed option should be used. This allows callers of methods that specify some logger options internaly, to overwrite their values. Add unit tests for this behavior. Fix the case when same logger option is passed multiple times in different objects - currently only one of the instances is removed and the other one is included in the message visible to the user. Now all such objects are removed. Add unit tests for all of the described cases.
1 parent 812d980 commit 6256958

File tree

2 files changed

+102
-12
lines changed

2 files changed

+102
-12
lines changed

lib/common/logger/logger.ts

+20-11
Original file line numberDiff line numberDiff line change
@@ -154,31 +154,40 @@ export class Logger implements ILogger {
154154
}
155155

156156
private getLogOptionsForMessage(data: any[]): { data: any[], [key: string]: any } {
157-
const opts = _.keys(LoggerConfigData);
157+
const loggerOptionKeys = _.keys(LoggerConfigData);
158+
const dataToCheck = data.filter(el => {
159+
// objects created with Object.create(null) do not have `hasOwnProperty` function
160+
if (!!el && typeof el === "object" && el.hasOwnProperty && typeof el.hasOwnProperty === "function") {
161+
for (const key of loggerOptionKeys) {
162+
if (el.hasOwnProperty(key)) {
163+
// include only the elements which have one of the keys we've specified as logger options
164+
return true;
165+
}
166+
}
167+
}
158168

159-
const result: any = {};
160-
const cleanedData = _.cloneDeep(data);
169+
return false;
170+
});
161171

162-
// objects created with Object.create(null) do not have `hasOwnProperty` function
163-
const dataToCheck = data.filter(el => typeof el === "object" && el.hasOwnProperty && typeof el.hasOwnProperty === "function");
172+
const result: any = {
173+
data: _.difference(data, dataToCheck)
174+
};
164175

165176
for (const element of dataToCheck) {
166-
if (opts.length === 0) {
177+
if (loggerOptionKeys.length === 0) {
167178
break;
168179
}
169180

170-
const remainingOpts = _.cloneDeep(opts);
181+
const remainingOpts = _.cloneDeep(loggerOptionKeys);
171182
for (const prop of remainingOpts) {
172183
const hasProp = element && element.hasOwnProperty(prop);
173184
if (hasProp) {
174-
opts.splice(opts.indexOf(prop), 1);
185+
loggerOptionKeys.splice(loggerOptionKeys.indexOf(prop), 1);
175186
result[prop] = element[prop];
176-
cleanedData.splice(cleanedData.indexOf(element), 1);
177187
}
178188
}
179189
}
180190

181-
result.data = cleanedData;
182191
return result;
183192
}
184193

@@ -195,7 +204,7 @@ export class Logger implements ILogger {
195204
/*******************************************************************************************
196205
* Metods below are deprecated. Delete them in 6.0.0 release: *
197206
* Present only for backwards compatibility as some plugins (nativescript-plugin-firebase) *
198-
* use these methods in their hooks *
207+
* use these methods in their hooks *
199208
*******************************************************************************************/
200209

201210
out(...args: any[]): void {

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

+82-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Yok } from "../../yok";
22
import { Logger } from "../../logger/logger";
33
import * as path from "path";
4+
import * as util from "util";
45
import { assert } from "chai";
56
import * as fileSystemFile from "../../file-system";
7+
import { LoggerConfigData } from "../../../constants";
68

79
const passwordReplacement = "*******";
810
const debugTrace = ["debug", "trace"];
@@ -33,7 +35,11 @@ describe("logger", () => {
3335
fs = testInjector.resolve("fs");
3436
outputs = {
3537
debug: "",
36-
trace: ""
38+
trace: "",
39+
info: "",
40+
error: "",
41+
context: {},
42+
removedContext: {}
3743
};
3844

3945
const log4jsLogger = {
@@ -42,6 +48,18 @@ describe("logger", () => {
4248
},
4349
trace: (...args: string[]) => {
4450
outputs.trace += args.join("");
51+
},
52+
info: (...args: string[]) => {
53+
outputs.info += util.format.apply(null, args);
54+
},
55+
error: (...args: string[]) => {
56+
outputs.error += util.format.apply(null, args);
57+
},
58+
addContext(key: string, value: any): void {
59+
outputs.context[key] = value;
60+
},
61+
removeContext(key: string): void {
62+
outputs.removedContext[key] = true;
4563
}
4664
};
4765

@@ -139,4 +157,67 @@ describe("logger", () => {
139157
assert.deepEqual(outputs.trace, `${request}${requestBody}`, "logger.trace should not obfuscate body of request unless it is towards api/itmstransporter");
140158
});
141159
});
160+
161+
describe("info", () => {
162+
[undefined, null, false, "string value", 42, { obj: 1 }, ["string value 1", "string value 2"]].forEach(value => {
163+
it(`handles formatted message with '${value}' value in one of the args`, () => {
164+
logger.info("test %s", value);
165+
assert.equal(outputs.info, `test ${value}`);
166+
assert.deepEqual(outputs.context, {}, "Nothing should be added to logger context.");
167+
assert.deepEqual(outputs.removedContext, {}, "Removed context should be empty.");
168+
});
169+
170+
it(`handles formatted message with '${value}' value in one of the args and additional values passed to context`, () => {
171+
logger.info("test %s", value, { [LoggerConfigData.skipNewLine]: true });
172+
assert.equal(outputs.info, `test ${value}`);
173+
assert.deepEqual(outputs.context, { [LoggerConfigData.skipNewLine]: true }, `${LoggerConfigData.skipNewLine} should be set with value true.`);
174+
assert.deepEqual(outputs.removedContext, { [LoggerConfigData.skipNewLine]: true }, `Removed context should contain ${LoggerConfigData.skipNewLine}`);
175+
});
176+
});
177+
178+
it("sets correct context when multiple logger options are passed in one object", () => {
179+
logger.info("test", { [LoggerConfigData.skipNewLine]: true, [LoggerConfigData.useStderr]: true });
180+
assert.equal(outputs.info, "test");
181+
assert.deepEqual(outputs.context, { [LoggerConfigData.skipNewLine]: true, [LoggerConfigData.useStderr]: true });
182+
assert.deepEqual(outputs.removedContext, { [LoggerConfigData.skipNewLine]: true, [LoggerConfigData.useStderr]: true });
183+
});
184+
185+
it("sets correct context when multiple logger options are passed in multiple objects", () => {
186+
logger.info({ [LoggerConfigData.useStderr]: true }, "test", { [LoggerConfigData.skipNewLine]: true });
187+
assert.equal(outputs.info, "test");
188+
assert.deepEqual(outputs.context, { [LoggerConfigData.skipNewLine]: true, [LoggerConfigData.useStderr]: true });
189+
assert.deepEqual(outputs.removedContext, { [LoggerConfigData.skipNewLine]: true, [LoggerConfigData.useStderr]: true });
190+
});
191+
});
192+
193+
describe("printMarkdown", () => {
194+
it(`passes markdown formatted text to log4js.info method`, () => {
195+
logger.printMarkdown("header text\n==");
196+
assert.isTrue(outputs.info.indexOf("# header text") !== -1);
197+
});
198+
199+
it(`passes skipNewLine to log4js context`, () => {
200+
logger.printMarkdown("`coloured` text");
201+
assert.isTrue(outputs.info.indexOf("coloured") !== -1);
202+
assert.deepEqual(outputs.context, { [LoggerConfigData.skipNewLine]: true }, `${LoggerConfigData.skipNewLine} should be set with value true.`);
203+
assert.deepEqual(outputs.removedContext, { [LoggerConfigData.skipNewLine]: true }, `Removed context should contain ${LoggerConfigData.skipNewLine}`);
204+
});
205+
});
206+
207+
describe("error", () => {
208+
const errorMessage = "this is error message";
209+
it(`passes useStderr to log4js context`, () => {
210+
logger.error(errorMessage);
211+
assert.equal(outputs.error, errorMessage);
212+
assert.deepEqual(outputs.context, { [LoggerConfigData.useStderr]: true }, `${LoggerConfigData.useStderr} should be set with value true.`);
213+
assert.deepEqual(outputs.removedContext, { [LoggerConfigData.useStderr]: true }, `Removed context should contain ${LoggerConfigData.useStderr}`);
214+
});
215+
216+
it(`allows overwrite of useStderr`, () => {
217+
logger.error(errorMessage, { [LoggerConfigData.useStderr]: false });
218+
assert.equal(outputs.error, errorMessage);
219+
assert.deepEqual(outputs.context, { [LoggerConfigData.useStderr]: false }, `${LoggerConfigData.useStderr} should be set with value false.`);
220+
assert.deepEqual(outputs.removedContext, { [LoggerConfigData.useStderr]: true }, `Removed context should contain ${LoggerConfigData.useStderr}`);
221+
});
222+
});
142223
});

0 commit comments

Comments
 (0)