Skip to content

Commit 6909295

Browse files
feat(api): use logfmt as logging format (#909)
* feat(api): use logfmt as logging format * feat(api): create specific logfmt logger file * chore(logger): use logfmt format in general logger * chore(rest-api-server): revert custom logger * chore(rest-api-server): format morgan request fields --------- Co-authored-by: PJColombo <[email protected]>
1 parent d9bcae3 commit 6909295

File tree

4 files changed

+142
-34
lines changed

4 files changed

+142
-34
lines changed

apps/rest-api-server/src/morgan.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,56 @@ import morgan from "morgan";
33
import { logger } from "@blobscan/logger";
44

55
const stream = {
6-
write: (message: string) => logger.http(message),
6+
write: (message: string) => {
7+
try {
8+
const parsed = JSON.parse(message);
9+
logger.http("HTTP request handled", parsed);
10+
} catch {
11+
logger.http(message.trim());
12+
}
13+
},
714
};
815

9-
export const morganMiddleware = morgan("short", { stream });
16+
export const morganMiddleware = morgan(
17+
(tokens, req, res) => {
18+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
19+
const get = (name: string, ...args: any[]) =>
20+
tokens[name]?.(req, res, ...args);
21+
22+
const data: Record<string, string> = {};
23+
24+
const ip = get("remote-addr");
25+
const method = get("method");
26+
const url = get("url");
27+
const status = get("status");
28+
const responseTime = get("response-time");
29+
const contentLength = get("res", "content-length");
30+
31+
if (ip) {
32+
data.ip = ip;
33+
}
34+
35+
if (method) {
36+
data.method = method;
37+
}
38+
39+
if (url) {
40+
data.url = url;
41+
}
42+
43+
if (status) {
44+
data.status = status;
45+
}
46+
47+
if (responseTime) {
48+
data.res_time = `${responseTime}ms`;
49+
}
50+
51+
if (contentLength) {
52+
data.res_length = contentLength;
53+
}
54+
55+
return JSON.stringify(data);
56+
},
57+
{ stream }
58+
);

packages/logger/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
},
1414
"dependencies": {
1515
"@blobscan/zod": "workspace:^0.1.0",
16+
"logfmt": "^1.4.0",
1617
"winston": "^3.18.3"
1718
},
19+
"devDependencies": {
20+
"@types/logfmt": "^1.2.1"
21+
},
1822
"eslintConfig": {
1923
"root": true,
2024
"extends": [

packages/logger/src/index.ts

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logfmt from "logfmt";
12
import winston from "winston";
23

34
import { z } from "@blobscan/zod";
@@ -6,19 +7,6 @@ export const logLevelEnum = z.enum(["error", "warn", "info", "http", "debug"]);
67

78
export type LoggerLevel = z.output<typeof logLevelEnum>;
89

9-
function buildErrorCause(err: Error) {
10-
let msg = `\n - Cause: ${err.message}`;
11-
12-
const cause = err.cause;
13-
if (cause instanceof Error || typeof cause === "string") {
14-
const errorCause = typeof cause === "string" ? new Error(cause) : cause;
15-
16-
msg += buildErrorCause(errorCause);
17-
}
18-
19-
return msg;
20-
}
21-
2210
const LOG_LEVELS = {
2311
error: 0,
2412
warn: 1,
@@ -38,37 +26,59 @@ const colorFormat = winston.format.colorize({
3826
},
3927
});
4028

41-
const format = winston.format.combine(
29+
// Logfmt formatter for Winston
30+
const logfmtFormat = winston.format.combine(
4231
winston.format.errors({ cause: true, stack: true }),
4332
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
4433
winston.format.printf((info) => {
45-
const { timestamp, level, message, cause, service } = info;
46-
47-
const formattedLevel = colorFormat.colorize(level, level.toUpperCase());
48-
const formattedService =
49-
typeof service === "string"
50-
? colorFormat.colorize("service", service ?? "app")
51-
: "";
52-
const formattedMessage =
53-
typeof message === "string"
54-
? colorFormat.colorize(level, message)
55-
: message;
56-
57-
let msg = `${timestamp} ${formattedLevel} ${formattedService}: ${formattedMessage}`;
34+
const { timestamp, level, message, service, cause, ...meta } = info;
35+
36+
const logData: Record<string, string | number | boolean> = {
37+
ts: String(timestamp),
38+
level: colorFormat.colorize(level, level),
39+
message: colorFormat.colorize(
40+
level,
41+
typeof message === "string" ? message : JSON.stringify(message)
42+
),
43+
};
44+
45+
if (typeof service === "string") {
46+
logData.service = colorFormat.colorize("service", service);
47+
}
5848

49+
// Add any additional metadata
50+
Object.keys(meta).forEach((key) => {
51+
const value = meta[key];
52+
if (value !== undefined && key !== "cause") {
53+
if (
54+
typeof value === "string" ||
55+
typeof value === "number" ||
56+
typeof value === "boolean"
57+
) {
58+
logData[key] = value;
59+
} else {
60+
logData[key] = JSON.stringify(value);
61+
}
62+
}
63+
});
64+
65+
// Handle error causes
5966
if (cause instanceof Error) {
60-
msg += colorFormat.colorize(level, buildErrorCause(cause));
67+
logData.error_cause = cause.message;
68+
if (cause.stack) {
69+
logData.error_stack = cause.stack;
70+
}
6171
}
6272

63-
return msg;
73+
return logfmt.stringify(logData);
6474
})
6575
);
6676

6777
export function createLogger(name?: string, opts: winston.LoggerOptions = {}) {
6878
return winston.createLogger({
6979
level: process.env.LOG_LEVEL ?? "info",
7080
levels: LOG_LEVELS,
71-
format,
81+
format: logfmtFormat,
7282
transports: [new winston.transports.Console()],
7383
silent: process.env.MODE === "test",
7484
...opts,
@@ -79,7 +89,7 @@ export function createLogger(name?: string, opts: winston.LoggerOptions = {}) {
7989
});
8090
}
8191

82-
export const logger = createLogger();
92+
export const logger = createLogger("rest-api-server");
8393

8494
export async function perfOperation<T>(
8595
operation: () => T

pnpm-lock.yaml

Lines changed: 46 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)