Skip to content

Commit f508951

Browse files
huangjeff5mbleigh
andauthored
Remove circular dependencies when logging. Fixes #737 (#844)
* Remove circular dependencies when logging. Fixes #737 * Fix bug in binding the output logger * modify removeCircular to return a copy of the original object, rather than mutate * Modify removeCircular to return a new object, rather than mutating in place Co-authored-by: Michael Bleigh <[email protected]>
1 parent 2f72c33 commit f508951

File tree

2 files changed

+89
-30
lines changed

2 files changed

+89
-30
lines changed

spec/logger.spec.ts

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { expect } from 'chai';
2-
import * as sinon from 'sinon';
32
import * as logger from '../src/logger';
43

54
const SUPPORTS_STRUCTURED_LOGS =
@@ -8,52 +7,51 @@ const SUPPORTS_STRUCTURED_LOGS =
87
describe(`logger (${
98
SUPPORTS_STRUCTURED_LOGS ? 'structured' : 'unstructured'
109
})`, () => {
11-
let sandbox: sinon.SinonSandbox;
12-
let stdoutStub: sinon.SinonStub;
13-
let stderrStub: sinon.SinonStub;
10+
let stdoutWrite = process.stdout.write.bind(process.stdout);
11+
let stderrWrite = process.stderr.write.bind(process.stderr);
12+
let lastOut: string;
13+
let lastErr: string;
1414

1515
beforeEach(() => {
16-
sandbox = sinon.createSandbox();
17-
stdoutStub = sandbox.stub(process.stdout, 'write');
18-
stderrStub = sandbox.stub(process.stderr, 'write');
16+
process.stdout.write = (msg: Buffer | string, cb?: any): boolean => {
17+
lastOut = msg as string;
18+
return stdoutWrite(msg, cb);
19+
};
20+
process.stderr.write = (msg: Buffer | string, cb?: any): boolean => {
21+
lastErr = msg as string;
22+
return stderrWrite(msg, cb);
23+
};
1924
});
2025

21-
function expectOutput(stdStub: sinon.SinonStub, entry: any) {
26+
afterEach(() => {
27+
process.stdout.write = stdoutWrite;
28+
process.stderr.write = stderrWrite;
29+
});
30+
31+
function expectOutput(last: string, entry: any) {
2232
if (SUPPORTS_STRUCTURED_LOGS) {
23-
return expect(
24-
JSON.parse((stdStub.getCalls()[0].args[0] as string).trim())
25-
).to.deep.eq(entry);
33+
return expect(JSON.parse(last.trim())).to.deep.eq(entry);
2634
} else {
2735
// legacy logging is not structured, but do a sanity check
28-
return expect(stdStub.getCalls()[0].args[0]).to.include(entry.message);
36+
return expect(last).to.include(entry.message);
2937
}
3038
}
3139

3240
function expectStdout(entry: any) {
33-
return expectOutput(stdoutStub, entry);
41+
return expectOutput(lastOut, entry);
3442
}
3543

3644
function expectStderr(entry: any) {
37-
return expectOutput(stderrStub, entry);
45+
return expectOutput(lastErr, entry);
3846
}
3947

4048
describe('logging methods', () => {
41-
let writeStub: sinon.SinonStub;
42-
beforeEach(() => {
43-
writeStub = sinon.stub(logger, 'write');
44-
});
45-
46-
afterEach(() => {
47-
writeStub.restore();
48-
});
49-
5049
it('should coalesce arguments into the message', () => {
5150
logger.log('hello', { middle: 'obj' }, 'end message');
5251
expectStdout({
5352
severity: 'INFO',
5453
message: "hello { middle: 'obj' } end message",
5554
});
56-
sandbox.restore(); // to avoid swallowing test runner output
5755
});
5856

5957
it('should merge structured data from the last argument', () => {
@@ -63,7 +61,6 @@ describe(`logger (${
6361
message: 'hello world',
6462
additional: 'context',
6563
});
66-
sandbox.restore(); // to avoid swallowing test runner output
6764
});
6865

6966
it('should not recognize null as a structured logging object', () => {
@@ -72,13 +69,46 @@ describe(`logger (${
7269
severity: 'INFO',
7370
message: 'hello world null',
7471
});
75-
sandbox.restore(); // to avoid swallowing test runner output
7672
});
7773
});
7874

7975
describe('write', () => {
8076
describe('structured logging', () => {
8177
describe('write', () => {
78+
it('should remove circular references', () => {
79+
const circ: any = { b: 'foo' };
80+
circ.circ = circ;
81+
82+
const entry: logger.LogEntry = {
83+
severity: 'ERROR',
84+
message: 'testing circular',
85+
circ,
86+
};
87+
logger.write(entry);
88+
expectStderr({
89+
severity: 'ERROR',
90+
message: 'testing circular',
91+
circ: { b: 'foo', circ: '[Circular]' },
92+
});
93+
});
94+
95+
it('should remove circular references in arrays', () => {
96+
const circ: any = { b: 'foo' };
97+
circ.circ = [circ];
98+
99+
const entry: logger.LogEntry = {
100+
severity: 'ERROR',
101+
message: 'testing circular',
102+
circ,
103+
};
104+
logger.write(entry);
105+
expectStderr({
106+
severity: 'ERROR',
107+
message: 'testing circular',
108+
circ: { b: 'foo', circ: ['[Circular]'] },
109+
});
110+
});
111+
82112
for (const severity of ['DEBUG', 'INFO', 'NOTICE']) {
83113
it(`should output ${severity} severity to stdout`, () => {
84114
let entry: logger.LogEntry = {
@@ -87,7 +117,6 @@ describe(`logger (${
87117
};
88118
logger.write(entry);
89119
expectStdout(entry);
90-
sandbox.restore(); // to avoid swallowing test runner output
91120
});
92121
}
93122

@@ -105,7 +134,6 @@ describe(`logger (${
105134
};
106135
logger.write(entry);
107136
expectStderr(entry);
108-
sandbox.restore(); // to avoid swallowing test runner output
109137
});
110138
}
111139
});

src/logger.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,40 @@ export interface LogEntry {
3030
[key: string]: any;
3131
}
3232

33+
function removeCircular(obj: any, refs: any[] = []): any {
34+
if (typeof obj !== 'object' || !obj) {
35+
return obj;
36+
}
37+
if (refs.includes(obj)) {
38+
return '[Circular]';
39+
} else {
40+
refs.push(obj);
41+
}
42+
let returnObj: any;
43+
if (Array.isArray(obj)) {
44+
returnObj = new Array(obj.length);
45+
} else {
46+
returnObj = {};
47+
}
48+
for (const k in obj) {
49+
if (refs.includes(obj[k])) {
50+
returnObj[k] = '[Circular]';
51+
} else {
52+
returnObj[k] = removeCircular(obj[k], refs);
53+
}
54+
}
55+
return returnObj;
56+
}
57+
3358
/**
3459
* Writes a `LogEntry` to `stdout`/`stderr` (depending on severity).
3560
* @param entry The `LogEntry` including severity, message, and any additional structured metadata.
3661
*/
3762
export function write(entry: LogEntry) {
3863
if (SUPPORTS_STRUCTURED_LOGS) {
39-
UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]](JSON.stringify(entry));
64+
UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]](
65+
JSON.stringify(removeCircular(entry))
66+
);
4067
return;
4168
}
4269

@@ -50,7 +77,11 @@ export function write(entry: LogEntry) {
5077
}
5178
}
5279
if (jsonKeyCount > 0) {
53-
message = `${message} ${JSON.stringify(jsonPayload, null, 2)}`;
80+
message = `${message} ${JSON.stringify(
81+
removeCircular(jsonPayload),
82+
null,
83+
2
84+
)}`;
5485
}
5586
UNPATCHED_CONSOLE[CONSOLE_SEVERITY[entry.severity]](message);
5687
}

0 commit comments

Comments
 (0)