Files
paperclip/server/src/middleware/logger.ts
Dotta 1420b86aa7 fix(server): attach raw Error to res.err and avoid pino err key collision
Extract attachErrorContext helper to DRY up the error handler, attach the
original Error object to res.err so pino can serialize stack traces, and
rename the log context key from err to errorContext so it doesn't clash
with pino's built-in err serializer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:19:03 -06:00

90 lines
2.6 KiB
TypeScript

import path from "node:path";
import fs from "node:fs";
import pino from "pino";
import { pinoHttp } from "pino-http";
import { readConfigFile } from "../config-file.js";
import { resolveDefaultLogsDir, resolveHomeAwarePath } from "../home-paths.js";
function resolveServerLogDir(): string {
const envOverride = process.env.PAPERCLIP_LOG_DIR?.trim();
if (envOverride) return resolveHomeAwarePath(envOverride);
const fileLogDir = readConfigFile()?.logging.logDir?.trim();
if (fileLogDir) return resolveHomeAwarePath(fileLogDir);
return resolveDefaultLogsDir();
}
const logDir = resolveServerLogDir();
fs.mkdirSync(logDir, { recursive: true });
const logFile = path.join(logDir, "server.log");
const sharedOpts = {
translateTime: "HH:MM:ss",
ignore: "pid,hostname",
};
export const logger = pino({
level: "debug",
}, pino.transport({
targets: [
{
target: "pino-pretty",
options: { ...sharedOpts, ignore: "pid,hostname,req,res,responseTime", colorize: true, destination: 1 },
level: "info",
},
{
target: "pino-pretty",
options: { ...sharedOpts, colorize: false, destination: logFile, mkdir: true },
level: "debug",
},
],
}));
export const httpLogger = pinoHttp({
logger,
customLogLevel(_req, res, err) {
if (err || res.statusCode >= 500) return "error";
if (res.statusCode >= 400) return "warn";
return "info";
},
customSuccessMessage(req, res) {
return `${req.method} ${req.url} ${res.statusCode}`;
},
customErrorMessage(req, res, err) {
const ctx = (res as any).__errorContext;
const errMsg = ctx?.error?.message || err?.message || (res as any).err?.message || "unknown error";
return `${req.method} ${req.url} ${res.statusCode}${errMsg}`;
},
customProps(req, res) {
if (res.statusCode >= 400) {
const ctx = (res as any).__errorContext;
if (ctx) {
return {
errorContext: ctx.error,
reqBody: ctx.reqBody,
reqParams: ctx.reqParams,
reqQuery: ctx.reqQuery,
};
}
const props: Record<string, unknown> = {};
const { body, params, query } = req as any;
if (body && typeof body === "object" && Object.keys(body).length > 0) {
props.reqBody = body;
}
if (params && typeof params === "object" && Object.keys(params).length > 0) {
props.reqParams = params;
}
if (query && typeof query === "object" && Object.keys(query).length > 0) {
props.reqQuery = query;
}
if ((req as any).route?.path) {
props.routePath = (req as any).route.path;
}
return props;
}
return {};
},
});