Skip to content

Commit 60ef8ea

Browse files
committed
[node-tracing] Replace winston with a small built-in logger
1 parent 0cb46de commit 60ef8ea

4 files changed

Lines changed: 187 additions & 27 deletions

File tree

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525
"author": "blackfire.io",
2626
"license": "MIT",
2727
"dependencies": {
28-
"dd-trace": "^5.92.0",
29-
"winston": "^3.9.0"
28+
"dd-trace": "^5.92.0"
3029
},
3130
"devDependencies": {
3231
"@stylistic/eslint-plugin": "^2.9.0",

src/index.js

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,9 @@
11
const os = require('os');
2-
const winston = require('winston');
32
const ddtrace = require('dd-trace');
3+
const { createLogger } = require('./logger');
44
const { version } = require('../package.json');
55

6-
const DEFAULT_LOG_LEVEL = 1;
7-
const logLevels = {
8-
4: 'debug',
9-
3: 'info',
10-
2: 'warn',
11-
1: 'error',
12-
};
13-
14-
// initialize logger
15-
const logger = winston.createLogger({
16-
format: winston.format.combine(
17-
winston.format.timestamp(),
18-
winston.format.splat(),
19-
winston.format.simple(),
20-
),
21-
level: logLevels[process.env.BLACKFIRE_LOG_LEVEL || DEFAULT_LOG_LEVEL],
22-
});
23-
24-
const logFile = process.env.BLACKFIRE_LOG_FILE;
25-
if (logFile && logFile !== 'stderr') {
26-
logger.add(new winston.transports.File({ filename: logFile }));
27-
} else {
28-
logger.add(new winston.transports.Console());
29-
}
6+
const logger = createLogger();
307

318
const currentProfilingSession = {
329
stop: undefined,

src/logger.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const fs = require('fs');
2+
const util = require('util');
3+
4+
const DEFAULT_LOG_LEVEL = 1;
5+
const LOG_LEVELS = {
6+
4: 'debug',
7+
3: 'info',
8+
2: 'warn',
9+
1: 'error',
10+
};
11+
12+
function createLogger(env = process.env) {
13+
const requested = Number(env.BLACKFIRE_LOG_LEVEL);
14+
// Clamp to a known level so the gate and the `level` string always agree, even for '0'/'5'/'foo'.
15+
const numericLevel = requested in LOG_LEVELS ? requested : DEFAULT_LOG_LEVEL;
16+
const level = LOG_LEVELS[numericLevel];
17+
18+
const logFile = env.BLACKFIRE_LOG_FILE;
19+
20+
let stream = null;
21+
if (logFile && logFile !== 'stderr') {
22+
// A bad BLACKFIRE_LOG_FILE must never crash the host app: warn once, then fall back to stderr.
23+
const degradeToStderr = (err) => {
24+
stream = null;
25+
process.stderr.write(`blackfire: cannot write log file ${logFile}, falling back to stderr: ${err.message}\n`);
26+
};
27+
try {
28+
// Open synchronously so a bad path fails fast and loses no early lines; writes stay non-blocking.
29+
stream = fs.createWriteStream(logFile, { fd: fs.openSync(logFile, 'a') });
30+
stream.on('error', degradeToStderr);
31+
} catch (err) {
32+
degradeToStderr(err);
33+
}
34+
}
35+
36+
const write = (line) => {
37+
if (stream) {
38+
stream.write(`${line}\n`);
39+
} else {
40+
process.stderr.write(`${line}\n`);
41+
}
42+
};
43+
44+
const log = (severity, name, message, args) => {
45+
if (severity > numericLevel) return;
46+
write(`${new Date().toISOString()} ${name}: ${util.format(message, ...args)}`);
47+
};
48+
49+
return {
50+
level,
51+
error: (message, ...args) => log(1, 'error', message, args),
52+
warn: (message, ...args) => log(2, 'warn', message, args),
53+
info: (message, ...args) => log(3, 'info', message, args),
54+
debug: (message, ...args) => log(4, 'debug', message, args),
55+
};
56+
}
57+
58+
module.exports = { createLogger };

tests/logger.test.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
const fs = require('fs');
2+
const os = require('os');
3+
const path = require('path');
4+
const { EventEmitter } = require('events');
5+
const { createLogger } = require('../src/logger');
6+
7+
let stderr;
8+
9+
beforeEach(() => {
10+
stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
11+
});
12+
13+
afterEach(() => {
14+
jest.restoreAllMocks();
15+
});
16+
17+
describe('level resolution and gating', () => {
18+
test('defaults to error when BLACKFIRE_LOG_LEVEL is unset', () => {
19+
const logger = createLogger({});
20+
expect(logger.level).toBe('error');
21+
22+
logger.debug('d');
23+
logger.info('i');
24+
logger.warn('w');
25+
expect(stderr).not.toHaveBeenCalled();
26+
27+
logger.error('e');
28+
expect(stderr).toHaveBeenCalledTimes(1);
29+
});
30+
31+
test('level 4 yields debug and emits every level', () => {
32+
const logger = createLogger({ BLACKFIRE_LOG_LEVEL: '4' });
33+
expect(logger.level).toBe('debug');
34+
35+
logger.debug('d');
36+
logger.info('i');
37+
logger.warn('w');
38+
logger.error('e');
39+
expect(stderr).toHaveBeenCalledTimes(4);
40+
});
41+
42+
// Invalid/out-of-range values clamp to error so the gate and the `level` string stay consistent.
43+
test.each(['0', '5', 'foo'])('level %s clamps to error and emits only error', (value) => {
44+
const logger = createLogger({ BLACKFIRE_LOG_LEVEL: value });
45+
expect(logger.level).toBe('error');
46+
47+
logger.debug('d');
48+
logger.warn('w');
49+
expect(stderr).not.toHaveBeenCalled();
50+
51+
logger.error('e');
52+
expect(stderr).toHaveBeenCalledTimes(1);
53+
});
54+
});
55+
56+
test('interpolates printf-style placeholders', () => {
57+
const logger = createLogger({});
58+
logger.error('hi %s', 'there');
59+
60+
const line = stderr.mock.calls[0][0];
61+
expect(line).toContain('error: hi there');
62+
});
63+
64+
describe('sink selection', () => {
65+
test('writes to stderr when BLACKFIRE_LOG_FILE is "stderr"', () => {
66+
const logger = createLogger({ BLACKFIRE_LOG_FILE: 'stderr' });
67+
logger.error('e');
68+
expect(stderr).toHaveBeenCalledTimes(1);
69+
});
70+
71+
test('writes formatted lines to the configured log file with a timestamp', (done) => {
72+
const logFile = path.join(os.tmpdir(), `blackfire-logger-${process.pid}-${process.hrtime.bigint()}.log`);
73+
const realCreate = fs.createWriteStream;
74+
let stream;
75+
jest.spyOn(fs, 'createWriteStream').mockImplementation((file, opts) => {
76+
stream = realCreate(file, opts);
77+
return stream;
78+
});
79+
80+
const logger = createLogger({ BLACKFIRE_LOG_FILE: logFile });
81+
logger.error('to file %s', 'here');
82+
83+
// end() flushes the buffered write before we read the file back.
84+
stream.end(() => {
85+
const contents = fs.readFileSync(logFile, 'utf8');
86+
expect(contents).toContain('error: to file here');
87+
expect(contents).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
88+
expect(stderr).not.toHaveBeenCalled();
89+
fs.rmSync(logFile, { force: true });
90+
done();
91+
});
92+
});
93+
94+
test('falls back to stderr and warns once when the log file cannot be opened', () => {
95+
// openSync throws synchronously on a bad path, so no early lines are buffered into a dead stream.
96+
jest.spyOn(fs, 'openSync').mockImplementation(() => {
97+
throw new Error('EACCES');
98+
});
99+
100+
const logger = createLogger({ BLACKFIRE_LOG_FILE: '/nope/blackfire.log' });
101+
expect(stderr).toHaveBeenCalledTimes(1);
102+
expect(stderr.mock.calls[0][0]).toContain('cannot write log file /nope/blackfire.log');
103+
104+
// Subsequent logs route to stderr instead of the dead stream.
105+
logger.error('after failure');
106+
expect(stderr).toHaveBeenCalledTimes(2);
107+
expect(stderr.mock.calls[1][0]).toContain('error: after failure');
108+
});
109+
110+
test('falls back to stderr if the stream errors after opening', () => {
111+
jest.spyOn(fs, 'openSync').mockReturnValue(123);
112+
const stream = new EventEmitter();
113+
stream.write = jest.fn();
114+
jest.spyOn(fs, 'createWriteStream').mockReturnValue(stream);
115+
116+
const logger = createLogger({ BLACKFIRE_LOG_FILE: '/some/blackfire.log' });
117+
stream.emit('error', new Error('ENOSPC'));
118+
119+
expect(stderr).toHaveBeenCalledTimes(1);
120+
expect(stderr.mock.calls[0][0]).toContain('cannot write log file /some/blackfire.log');
121+
122+
logger.error('after failure');
123+
expect(stderr).toHaveBeenCalledTimes(2);
124+
expect(stderr.mock.calls[1][0]).toContain('error: after failure');
125+
});
126+
});

0 commit comments

Comments
 (0)