diff --git a/apps/rest-api-server/src/morgan.ts b/apps/rest-api-server/src/morgan.ts index c5f98c29e..bb6e7c725 100644 --- a/apps/rest-api-server/src/morgan.ts +++ b/apps/rest-api-server/src/morgan.ts @@ -3,7 +3,56 @@ import morgan from "morgan"; import { logger } from "@blobscan/logger"; const stream = { - write: (message: string) => logger.http(message), + write: (message: string) => { + try { + const parsed = JSON.parse(message); + logger.http("HTTP request handled", parsed); + } catch { + logger.http(message.trim()); + } + }, }; -export const morganMiddleware = morgan("short", { stream }); +export const morganMiddleware = morgan( + (tokens, req, res) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const get = (name: string, ...args: any[]) => + tokens[name]?.(req, res, ...args); + + const data: Record = {}; + + const ip = get("remote-addr"); + const method = get("method"); + const url = get("url"); + const status = get("status"); + const responseTime = get("response-time"); + const contentLength = get("res", "content-length"); + + if (ip) { + data.ip = ip; + } + + if (method) { + data.method = method; + } + + if (url) { + data.url = url; + } + + if (status) { + data.status = status; + } + + if (responseTime) { + data.res_time = `${responseTime}ms`; + } + + if (contentLength) { + data.res_length = contentLength; + } + + return JSON.stringify(data); + }, + { stream } +); diff --git a/packages/logger/package.json b/packages/logger/package.json index 28e699c7b..646d53f8c 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -13,8 +13,12 @@ }, "dependencies": { "@blobscan/zod": "workspace:^0.1.0", + "logfmt": "^1.4.0", "winston": "^3.18.3" }, + "devDependencies": { + "@types/logfmt": "^1.2.1" + }, "eslintConfig": { "root": true, "extends": [ diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 38a18ee22..2585ce46f 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,3 +1,4 @@ +import logfmt from "logfmt"; import winston from "winston"; import { z } from "@blobscan/zod"; @@ -6,19 +7,6 @@ export const logLevelEnum = z.enum(["error", "warn", "info", "http", "debug"]); export type LoggerLevel = z.output; -function buildErrorCause(err: Error) { - let msg = `\n - Cause: ${err.message}`; - - const cause = err.cause; - if (cause instanceof Error || typeof cause === "string") { - const errorCause = typeof cause === "string" ? new Error(cause) : cause; - - msg += buildErrorCause(errorCause); - } - - return msg; -} - const LOG_LEVELS = { error: 0, warn: 1, @@ -38,29 +26,51 @@ const colorFormat = winston.format.colorize({ }, }); -const format = winston.format.combine( +// Logfmt formatter for Winston +const logfmtFormat = winston.format.combine( winston.format.errors({ cause: true, stack: true }), winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), winston.format.printf((info) => { - const { timestamp, level, message, cause, service } = info; - - const formattedLevel = colorFormat.colorize(level, level.toUpperCase()); - const formattedService = - typeof service === "string" - ? colorFormat.colorize("service", service ?? "app") - : ""; - const formattedMessage = - typeof message === "string" - ? colorFormat.colorize(level, message) - : message; - - let msg = `${timestamp} ${formattedLevel} ${formattedService}: ${formattedMessage}`; + const { timestamp, level, message, service, cause, ...meta } = info; + + const logData: Record = { + ts: String(timestamp), + level: colorFormat.colorize(level, level), + message: colorFormat.colorize( + level, + typeof message === "string" ? message : JSON.stringify(message) + ), + }; + + if (typeof service === "string") { + logData.service = colorFormat.colorize("service", service); + } + // Add any additional metadata + Object.keys(meta).forEach((key) => { + const value = meta[key]; + if (value !== undefined && key !== "cause") { + if ( + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { + logData[key] = value; + } else { + logData[key] = JSON.stringify(value); + } + } + }); + + // Handle error causes if (cause instanceof Error) { - msg += colorFormat.colorize(level, buildErrorCause(cause)); + logData.error_cause = cause.message; + if (cause.stack) { + logData.error_stack = cause.stack; + } } - return msg; + return logfmt.stringify(logData); }) ); @@ -68,7 +78,7 @@ export function createLogger(name?: string, opts: winston.LoggerOptions = {}) { return winston.createLogger({ level: process.env.LOG_LEVEL ?? "info", levels: LOG_LEVELS, - format, + format: logfmtFormat, transports: [new winston.transports.Console()], silent: process.env.MODE === "test", ...opts, @@ -79,7 +89,7 @@ export function createLogger(name?: string, opts: winston.LoggerOptions = {}) { }); } -export const logger = createLogger(); +export const logger = createLogger("rest-api-server"); export async function perfOperation( operation: () => T diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c00ae782c..d08c1d239 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,7 +115,7 @@ importers: version: 0.5.13(tailwindcss@3.4.6(ts-node@10.9.2(@types/node@24.7.2)(typescript@5.5.3))) autoprefixer: specifier: ^10.4.14 - version: 10.4.19(postcss@8.4.39) + version: 10.4.19(postcss@8.4.38) clsx: specifier: ^2.1.0 version: 2.1.1 @@ -666,9 +666,16 @@ importers: '@blobscan/zod': specifier: workspace:^0.1.0 version: link:../zod + logfmt: + specifier: ^1.4.0 + version: 1.4.0 winston: specifier: ^3.18.3 version: 3.18.3 + devDependencies: + '@types/logfmt': + specifier: ^1.2.1 + version: 1.2.6 packages/network-blob-config: {} @@ -3805,6 +3812,9 @@ packages: '@types/linkify-it@3.0.5': resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + '@types/logfmt@1.2.6': + resolution: {integrity: sha512-9/L27oLOjVlhMEHJs4vuvgEFNsAnPISQMi4ploHhidLwv2NX1S7aDmpWuSmaC2S+mXWD/Zx7qUP0mWFO7G0zvw==} + '@types/markdown-it@12.2.3': resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} @@ -6063,6 +6073,10 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + logfmt@1.4.0: + resolution: {integrity: sha512-p1Ow0C2dDJYaQBhRHt+HVMP6ELuBm4jYSYNHPMfz0J5wJ9qA6/7oBOlBZBfT1InqguTYcvJzNea5FItDxTcbyw==} + hasBin: true + logform@2.7.0: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} @@ -7253,6 +7267,9 @@ packages: spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} + split@0.2.10: + resolution: {integrity: sha512-e0pKq+UUH2Xq/sXbYpZBZc3BawsfDZ7dgv+JtRTUPNcvF5CMR4Y9cvJqkMY0MoxWzTHvZuz1beg6pNEKlszPiQ==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -7518,6 +7535,9 @@ packages: engines: {node: '>= 0.10.x'} hasBin: true + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinybench@2.8.0: resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} @@ -11767,6 +11787,10 @@ snapshots: '@types/linkify-it@3.0.5': optional: true + '@types/logfmt@1.2.6': + dependencies: + '@types/node': 22.15.18 + '@types/markdown-it@12.2.3': dependencies: '@types/linkify-it': 3.0.5 @@ -12356,6 +12380,16 @@ snapshots: asynckit@0.4.0: {} + autoprefixer@10.4.19(postcss@8.4.38): + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001614 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + autoprefixer@10.4.19(postcss@8.4.39): dependencies: browserslist: 4.23.0 @@ -14485,6 +14519,11 @@ snapshots: chalk: 5.3.0 is-unicode-supported: 1.3.0 + logfmt@1.4.0: + dependencies: + split: 0.2.10 + through: 2.3.8 + logform@2.7.0: dependencies: '@colors/colors': 1.6.0 @@ -15753,6 +15792,10 @@ snapshots: cross-spawn: 5.1.0 signal-exit: 3.0.7 + split@0.2.10: + dependencies: + through: 2.3.8 + sprintf-js@1.0.3: {} stack-trace@0.0.10: {} @@ -16052,6 +16095,8 @@ snapshots: error: 7.0.2 long: 2.4.0 + through@2.3.8: {} + tinybench@2.8.0: {} tinypool@0.7.0: {}