Skip to content

Commit 9ee5bdf

Browse files
feat(logger): enhance logger in development mode
1 parent 448eafd commit 9ee5bdf

File tree

2 files changed

+214
-16
lines changed

2 files changed

+214
-16
lines changed
Lines changed: 132 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { noop } from '@barso/helpers';
1+
import { deepMerge, noop } from '@barso/helpers';
22
import pino from 'pino';
33

44
import { axiomTransport } from './axiom-transport.js';
55

66
export function getLogger(options = {}) {
77
const environment = process.env.VERCEL_ENV || process.env.NODE_ENV;
8+
const logLevel = process.env.LOG_LEVEL;
9+
const level = logLevel || options.level || 'info';
810

911
if (['preview', 'production'].includes(environment) || process.env.AXIOM_DATASET) {
1012
const pinoLogger = pino(
@@ -14,31 +16,147 @@ export function getLogger(options = {}) {
1416
environment,
1517
...options.base,
1618
},
19+
level,
1720
},
1821
axiomTransport(),
1922
);
2023

2124
return pinoLogger;
2225
}
2326

24-
const logLevel = process.env.LOG_LEVEL;
27+
return createConsoleLogger({
28+
effectiveMinLocalLogLevel: logLevel || (environment === 'test' ? 'silent' : 'warn'),
29+
level,
30+
});
31+
}
2532

26-
if (logLevel === 'silent') {
27-
const silentLogger = { debug: noop, error: noop, fatal: noop, info: noop, trace: noop, warn: noop, flush: noop };
28-
return silentLogger;
29-
}
33+
const LOG_LEVEL_PRIORITIES = {
34+
trace: 10,
35+
debug: 20,
36+
info: 30,
37+
warn: 40,
38+
error: 50,
39+
fatal: 60,
40+
silent: 70,
41+
};
42+
43+
const consoleRef = globalThis.console;
44+
45+
function getConsoleMethod(level) {
46+
const map = {
47+
trace: consoleRef.trace,
48+
debug: consoleRef.debug,
49+
info: consoleRef.log,
50+
warn: consoleRef.warn,
51+
error: consoleRef.error,
52+
fatal: consoleRef.error,
53+
};
54+
55+
return map[level] || consoleRef.log;
56+
}
57+
58+
function createConsoleLogger({ bindings = {}, effectiveMinLocalLogLevel = 'warn', level = 'info' } = {}) {
59+
const currentBindings = { ...bindings };
60+
61+
const log = (targetLevel, ...args) => {
62+
if (!isLevelEnabled(targetLevel, effectiveMinLocalLogLevel)) {
63+
return;
64+
}
65+
66+
const method = getConsoleMethod(targetLevel);
67+
68+
if (args.length === 0) {
69+
if (Object.keys(currentBindings).length > 0) {
70+
method({ ...currentBindings });
71+
}
3072

31-
const consoleLogger = {
32-
...console,
33-
info: noop,
34-
fatal: console.error,
73+
return;
74+
}
75+
76+
const [firstArg, ...restArgs] = args;
77+
78+
if (typeof firstArg === 'string') {
79+
if (Object.keys(currentBindings).length > 0) {
80+
method(firstArg, { ...currentBindings }, ...restArgs);
81+
return;
82+
}
83+
84+
method(firstArg, ...restArgs);
85+
return;
86+
}
87+
88+
if (firstArg && typeof firstArg === 'object') {
89+
const payload = deepMerge(currentBindings, firstArg);
90+
91+
if (typeof restArgs[0] === 'string') {
92+
const [msg, ...remaining] = restArgs;
93+
method(msg, payload, ...remaining);
94+
return;
95+
}
96+
97+
method(payload, ...restArgs);
98+
return;
99+
}
100+
101+
method(firstArg, ...restArgs);
102+
};
103+
104+
const logger = {
105+
get level() {
106+
return level;
107+
},
108+
set level(nextLevel) {
109+
if (nextLevel in LOG_LEVEL_PRIORITIES) {
110+
level = nextLevel;
111+
}
112+
},
113+
trace(...args) {
114+
log('trace', ...args);
115+
},
116+
debug(...args) {
117+
log('debug', ...args);
118+
},
119+
info(...args) {
120+
log('info', ...args);
121+
},
122+
warn(...args) {
123+
log('warn', ...args);
124+
},
125+
error(...args) {
126+
log('error', ...args);
127+
},
128+
fatal(...args) {
129+
log('fatal', ...args);
130+
},
35131
flush: noop,
132+
bindings() {
133+
return { ...currentBindings };
134+
},
135+
setBindings(nextBindings = {}) {
136+
Object.assign(currentBindings, deepMerge(currentBindings, nextBindings));
137+
},
138+
child(childBindings = {}) {
139+
return createConsoleLogger({
140+
effectiveMinLocalLogLevel,
141+
level,
142+
bindings: deepMerge(currentBindings, childBindings),
143+
});
144+
},
145+
isLevelEnabled(targetLevel) {
146+
return isLevelEnabled(targetLevel, level);
147+
},
36148
};
37149

38-
if (['info', 'debug'].includes(logLevel)) {
39-
// eslint-disable-next-line no-console
40-
consoleLogger.info = console.log;
150+
return logger;
151+
}
152+
153+
function isLevelEnabled(targetLevel, level) {
154+
const targetPriority = LOG_LEVEL_PRIORITIES[targetLevel];
155+
const currentPriority = LOG_LEVEL_PRIORITIES[level] ?? LOG_LEVEL_PRIORITIES.warn;
156+
157+
if (targetPriority == null) {
158+
return false;
41159
}
42160

43-
return consoleLogger;
161+
return targetPriority >= currentPriority;
44162
}

packages/infra/src/logger/logger.test.js

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { noop } from 'packages/helpers';
22

33
const productionLevels = ['info', 'warn', 'error', 'fatal'];
4-
const devLevels = ['trace', 'debug', 'warn', 'error', 'fatal'];
4+
const devLevels = ['warn', 'error', 'fatal'];
55
const onlyDevLevels = ['trace', 'debug'];
66

77
const consoleLevels = {
@@ -59,11 +59,13 @@ describe('infra/logger', () => {
5959

6060
describe('Development (via console)', () => {
6161
beforeAll(() => {
62+
vi.stubEnv('NODE_ENV', 'development');
6263
vi.stubEnv('VERCEL_ENV', '');
6364
vi.stubEnv('AXIOM_DATASET', '');
6465
});
6566

6667
afterAll(() => {
68+
vi.stubEnv('NODE_ENV', 'test');
6769
vi.stubEnv('AXIOM_DATASET', dataset);
6870
});
6971

@@ -87,6 +89,44 @@ describe('infra/logger', () => {
8789
expect(logger.flush).not.toThrow();
8890
});
8991

92+
it('should expose pino-compatible methods', () => {
93+
expect(typeof logger.child).toBe('function');
94+
expect(typeof logger.bindings).toBe('function');
95+
expect(typeof logger.setBindings).toBe('function');
96+
expect(typeof logger.isLevelEnabled).toBe('function');
97+
});
98+
99+
it('should merge child bindings and write to console', () => {
100+
const childLogger = logger.child({ requestId: 'req-1' });
101+
102+
childLogger.warn('logger.warn');
103+
childLogger.error({ message: 'logger.error' });
104+
105+
expect(consoleSpy.warn).toHaveBeenCalledWith('logger.warn', { requestId: 'req-1' });
106+
expect(consoleSpy.error).toHaveBeenCalledWith({ message: 'logger.error', requestId: 'req-1' });
107+
});
108+
109+
it('should set and get bindings', () => {
110+
logger.setBindings({ session: 'abc' });
111+
112+
expect(logger.bindings()).toStrictEqual({ session: 'abc' });
113+
});
114+
115+
it('should keep info disabled by default', () => {
116+
logger.info('logger.info');
117+
118+
expect(logger.isLevelEnabled('info')).toBe(true);
119+
expect(consoleSpy.info).not.toHaveBeenCalled();
120+
});
121+
122+
it('should keep debug disabled by default', () => {
123+
expect(logger.isLevelEnabled('debug')).toBe(false);
124+
});
125+
126+
it('should keep trace disabled by default', () => {
127+
expect(logger.isLevelEnabled('trace')).toBe(false);
128+
});
129+
90130
describe('with "LOG_LEVEL" set to "info"', () => {
91131
beforeAll(() => {
92132
vi.stubEnv('LOG_LEVEL', 'info');
@@ -122,7 +162,7 @@ describe('infra/logger', () => {
122162
productionLevels.forEach((level) => {
123163
expect(stdoutSpy).toHaveBeenCalledWith(
124164
expect.stringMatching(
125-
new RegExp(`{"level":\\d+,"time":\\d+,"environment":"test","msg":"logger.${level}"}`),
165+
new RegExp(`{"level":\\d+,"time":\\d+,"environment":"development","msg":"logger.${level}"}`),
126166
),
127167
);
128168
});
@@ -200,6 +240,46 @@ describe('infra/logger', () => {
200240
it('should not throw error on flush', () => {
201241
expect(() => logger.flush()).not.toThrow();
202242
});
243+
244+
describe('with "LOG_LEVEL" set to "debug"', () => {
245+
beforeAll(() => {
246+
vi.stubEnv('LOG_LEVEL', 'debug');
247+
});
248+
249+
afterAll(() => {
250+
vi.stubEnv('LOG_LEVEL', '');
251+
});
252+
253+
it('should log debug level', async () => {
254+
logger.debug('logger.debug');
255+
256+
await vi.waitUntil(() => mocks.axiomIngest.mock.calls.length === 1);
257+
expect(mocks.axiomIngest).toHaveBeenCalledWith(
258+
dataset,
259+
expect.objectContaining({ level: 'debug', msg: 'logger.debug' }),
260+
);
261+
});
262+
});
263+
});
264+
265+
describe('Test runtime defaults to silent', () => {
266+
beforeAll(() => {
267+
vi.stubEnv('NODE_ENV', 'test');
268+
vi.stubEnv('VERCEL_ENV', '');
269+
vi.stubEnv('AXIOM_DATASET', '');
270+
vi.stubEnv('LOG_LEVEL', '');
271+
});
272+
273+
afterAll(() => {
274+
vi.stubEnv('AXIOM_DATASET', dataset);
275+
});
276+
277+
it('should silent logger by default', () => {
278+
logger.warn('logger.warn');
279+
280+
expect(logger.isLevelEnabled('warn')).toBe(true);
281+
expect(consoleSpy.warn).not.toHaveBeenCalled();
282+
});
203283
});
204284

205285
describe('With pino options', () => {

0 commit comments

Comments
 (0)