Skip to content

Commit dfb9c5d

Browse files
committed
feat: add logger options to file, separate http logs and app logs
1 parent a3bbaa9 commit dfb9c5d

File tree

6 files changed

+214
-18
lines changed

6 files changed

+214
-18
lines changed

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Full-featured slim webserver for microservices with extremely low overhead and r
1616
## Requirements
1717

1818
```bash
19-
Node.js >= 14.8
19+
Node.js >= 22
2020
```
2121

2222
## Quick start
@@ -147,6 +147,23 @@ Default `false`
147147
148148
Enable request/response logging, format varies from `production` or `development` environments, though to change use e.g. `NODE_ENV=production`
149149
150+
#### `logger?` (Object)
151+
152+
Configure log outputs for app logs and http request logs.
153+
154+
```typescript
155+
const app = new BareHttp({
156+
logging: true,
157+
logger: {
158+
app: { file: './logs/app.log' },
159+
http: { file: './logs/http.log' },
160+
level: 'debug',
161+
},
162+
});
163+
```
164+
165+
`app.file` and `http.file` accept a path string or `{ path, sync }`, and `http.file` is used only when `logging: true`. `console` (default `true`) keeps stdout logging, and `pretty` (default `true` outside production) controls colored console output. `level` sets the base level and can be overridden per target with `app.level` or `http.level`.
166+
150167
#### `ws?` (Boolean)
151168
152169
Default `false`
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import axios from 'axios';
2+
3+
import { mkdtempSync, readFileSync, rmSync } from 'fs';
4+
import { tmpdir } from 'os';
5+
import { join } from 'path';
6+
7+
import { envs } from '../../env.js';
8+
import { BareHttp } from '../../server.js';
9+
import { logMe } from '../../logger/index.js';
10+
11+
test('Writes http and app logs into separate files', async () => {
12+
const tempDir = mkdtempSync(join(tmpdir(), 'barehttp-logger-'));
13+
const appLogPath = join(tempDir, 'app.log');
14+
const httpLogPath = join(tempDir, 'http.log');
15+
const prevIsTest = envs.isTest;
16+
17+
try {
18+
envs.isTest = false;
19+
const app = new BareHttp({
20+
setRandomPort: true,
21+
logging: true,
22+
logger: {
23+
console: false,
24+
pretty: false,
25+
app: { file: { path: appLogPath, sync: true } },
26+
http: { file: { path: httpLogPath, sync: true } },
27+
},
28+
});
29+
30+
app.route.get({
31+
route: '/logger-test',
32+
handler: () => {
33+
logMe.info('app-log-works');
34+
return { ok: true };
35+
},
36+
});
37+
38+
await app.start();
39+
await axios.get(`http://localhost:${app.getServerPort()}/logger-test`);
40+
await app.stop();
41+
42+
const appLogContents = readFileSync(appLogPath, 'utf8');
43+
const httpLogContents = readFileSync(httpLogPath, 'utf8');
44+
45+
expect(appLogContents).toContain('app-log-works');
46+
expect(httpLogContents).toContain('GET /logger-test');
47+
expect(appLogContents).not.toContain('GET /logger-test');
48+
expect(httpLogContents).not.toContain('app-log-works');
49+
} finally {
50+
envs.isTest = prevIsTest;
51+
rmSync(tempDir, { recursive: true, force: true });
52+
}
53+
});

src/examples/bare-http.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
import { BareHttp } from '../index.js';
1+
import { BareHttp, logMe } from '../index.js';
22

33
const app = new BareHttp({
4-
logging: false,
4+
logging: true,
5+
requestTimeFormat: 'ms',
6+
logger: {
7+
console: true,
8+
pretty: true,
9+
level: 'trace',
10+
app: { file: './logs/example-app.log', level: 'trace' },
11+
http: { file: './logs/example-http.log', level: 'trace' },
12+
},
513
enableSchemaValidation: false,
614
declaredRoutesPaths: ['examples/bare-http'],
715
});
@@ -51,6 +59,7 @@ app.route.get({
5159
// if (flow.remoteIp) {
5260
// return { special: 'return' };
5361
// }
62+
logMe.info('Handling /route GET request');
5463
return returning;
5564
// flow.send(returning);
5665
},

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export { BareHttp } from './server.js';
22
export type { BareHttpType } from './server.js';
33
export type { BareRequest } from './request.js';
44
export { context } from './context/index.js';
5-
export { logMe } from './logger/index.js';
5+
export { configureLogger, logMe } from './logger/index.js';
6+
export type { LoggerConfig } from './logger/index.js';

src/logger/index.ts

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,130 @@
1-
import pino, { destination } from 'pino';
1+
import pino, {
2+
destination,
3+
type LevelWithSilent,
4+
type Logger as PinoLogger,
5+
type LoggerOptions as PinoLoggerOptions,
6+
} from 'pino';
27

3-
import { serializeLog, serializeHttp } from './serializers.js';
8+
import { mkdirSync } from 'fs';
9+
import { dirname } from 'path';
410

11+
import { serializeLog, serializeHttp } from './serializers.js';
512
import { envs } from '../env.js';
613

7-
const asyncDest = envs.isProd ? [destination({ sync: false })] : [];
14+
type LoggerFileConfig = {
15+
path: string;
16+
sync?: boolean;
17+
};
18+
19+
type LoggerTargetConfig = {
20+
file?: string | LoggerFileConfig;
21+
level?: LevelWithSilent;
22+
};
23+
24+
export type LoggerConfig = {
25+
console?: boolean;
26+
pretty?: boolean;
27+
level?: LevelWithSilent;
28+
app?: LoggerTargetConfig;
29+
http?: LoggerTargetConfig;
30+
};
831

9-
const pinoCommonOptions = {
32+
const defaultPretty = !envs.isProd;
33+
const defaultFileSync = envs.isProd ? false : true;
34+
35+
const pinoCommonOptions: PinoLoggerOptions = {
1036
timestamp: () => `,"time":"${new Date()[envs.isProd ? 'toISOString' : 'toLocaleTimeString']()}"`,
1137
formatters: {
1238
level: (label: string) => ({ level: label }),
1339
},
1440
messageKey: 'message',
15-
transport: envs.isProd ? undefined : { target: 'pino-pretty', options: { colorize: true } },
1641
};
1742

18-
const logger = pino(pinoCommonOptions, asyncDest[0]);
43+
const normalizeFileConfig = (file?: string | LoggerFileConfig) => {
44+
if (!file) return undefined;
45+
if (typeof file === 'string') return { path: file, sync: defaultFileSync };
46+
return { path: file.path, sync: file.sync ?? defaultFileSync };
47+
};
48+
49+
const ensureLogDir = (filePath: string) => {
50+
const dir = dirname(filePath);
51+
if (dir && dir !== '.') {
52+
mkdirSync(dir, { recursive: true });
53+
}
54+
};
55+
56+
const buildLogger = (config: LoggerConfig, target?: LoggerTargetConfig): PinoLogger => {
57+
const consoleEnabled = config.console ?? true;
58+
const prettyEnabled = config.pretty ?? defaultPretty;
59+
const fileConfig = normalizeFileConfig(target?.file);
60+
const level = target?.level ?? config.level;
61+
62+
const options: PinoLoggerOptions = { ...pinoCommonOptions };
63+
if (level) options.level = level;
64+
65+
if (consoleEnabled && !prettyEnabled && !fileConfig) {
66+
const stream = envs.isProd ? destination({ sync: false }) : undefined;
67+
return pino(options, stream);
68+
}
69+
70+
if (!consoleEnabled && fileConfig) {
71+
ensureLogDir(fileConfig.path);
72+
return pino(
73+
options,
74+
destination({ dest: fileConfig.path, sync: fileConfig.sync ?? defaultFileSync }),
75+
);
76+
}
77+
78+
const targets: Array<{
79+
target: string;
80+
level?: LevelWithSilent;
81+
options?: Record<string, unknown>;
82+
}> = [];
83+
84+
if (consoleEnabled) {
85+
if (prettyEnabled) {
86+
targets.push({ target: 'pino-pretty', level, options: { colorize: true } });
87+
} else {
88+
targets.push({ target: 'pino/file', level, options: { destination: 1 } });
89+
}
90+
}
91+
92+
if (fileConfig) {
93+
ensureLogDir(fileConfig.path);
94+
targets.push({
95+
target: 'pino/file',
96+
level,
97+
options: {
98+
destination: fileConfig.path,
99+
mkdir: true,
100+
sync: fileConfig.sync ?? defaultFileSync,
101+
},
102+
});
103+
}
104+
105+
if (targets.length === 0) {
106+
return pino({ ...options, level: 'silent' });
107+
}
108+
109+
const transport = pino.transport({ targets });
110+
return pino(options, transport);
111+
};
112+
113+
let appLogger: PinoLogger;
114+
let httpLogger: PinoLogger;
115+
116+
export const configureLogger = (config: LoggerConfig = {}) => {
117+
if (config.app || config.http) {
118+
appLogger = buildLogger(config, config.app);
119+
httpLogger = buildLogger(config, config.http);
120+
} else {
121+
const shared = buildLogger(config);
122+
appLogger = shared;
123+
httpLogger = shared;
124+
}
125+
};
126+
127+
configureLogger();
19128

20129
interface LogMeFn {
21130
(obj: unknown, ...args: []): void;
@@ -33,15 +142,15 @@ type LogMe = {
33142

34143
export const logHttp = (...params: Parameters<typeof serializeHttp>) => {
35144
const { level, logObject } = serializeHttp(...params);
36-
logger[level](logObject);
145+
httpLogger[level](logObject);
37146
};
38147

39148
// TODO: remove the test condition
40149
export const logMe: LogMe = {
41-
debug: (...args) => !envs.isTest && logger.debug(serializeLog(...args)),
42-
info: (...args) => !envs.isTest && logger.info(serializeLog(...args)),
43-
warn: (...args) => !envs.isTest && logger.warn(serializeLog(...args)),
44-
error: (...args) => !envs.isTest && logger.error(serializeLog(...args)),
45-
fatal: (...args) => !envs.isTest && logger.fatal(serializeLog(...args)),
46-
trace: (...args) => !envs.isTest && logger.trace(serializeLog(...args)),
150+
debug: (...args) => !envs.isTest && appLogger.debug(serializeLog(...args)),
151+
info: (...args) => !envs.isTest && appLogger.info(serializeLog(...args)),
152+
warn: (...args) => !envs.isTest && appLogger.warn(serializeLog(...args)),
153+
error: (...args) => !envs.isTest && appLogger.error(serializeLog(...args)),
154+
fatal: (...args) => !envs.isTest && appLogger.fatal(serializeLog(...args)),
155+
trace: (...args) => !envs.isTest && appLogger.trace(serializeLog(...args)),
47156
};

src/server.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { ServerOptions } from 'ws';
33
import { Ajv, ValidateFunction } from 'ajv';
44

55
import { BareRequest, CacheOpts } from './request.js';
6-
import { logMe } from './logger/index.js';
6+
import { configureLogger, logMe } from './logger/index.js';
7+
import type { LoggerConfig } from './logger/index.js';
78
import { context, enableContext, newContext } from './context/index.js';
89
import { CookiesManagerOptions } from './middlewares/cookies/cookie-manager.js';
910
import {
@@ -77,6 +78,10 @@ type BareOptions<A extends IP> = {
7778
* Default `false`
7879
*/
7980
logging?: boolean;
81+
/**
82+
* Logger configuration. Controls outputs for app and http logs.
83+
*/
84+
logger?: LoggerConfig;
8085
errorHandlerMiddleware?: ErrorHandler;
8186
/**
8287
* Request time format in `seconds` or `milliseconds`
@@ -213,6 +218,8 @@ export class BareServer<A extends IP> {
213218
private applyLaunchOptions = () => {
214219
const { bareOptions: bo } = this;
215220

221+
if (bo.logger) configureLogger(bo.logger);
222+
216223
if (bo.setRandomPort) {
217224
this.#port = undefined as any;
218225
} else {

0 commit comments

Comments
 (0)