diff --git a/server/src/__tests__/http-log-policy.test.ts b/server/src/__tests__/http-log-policy.test.ts new file mode 100644 index 00000000000..98b1f121479 --- /dev/null +++ b/server/src/__tests__/http-log-policy.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { shouldSilenceHttpSuccessLog } from "../middleware/http-log-policy.js"; + +describe("shouldSilenceHttpSuccessLog", () => { + it("silences cached 304 responses", () => { + expect(shouldSilenceHttpSuccessLog("GET", "/api/issues/PAP-1383", 304)).toBe(true); + }); + + it("silences successful polling endpoints", () => { + expect(shouldSilenceHttpSuccessLog("GET", "/api/health", 200)).toBe(true); + expect( + shouldSilenceHttpSuccessLog( + "GET", + "/api/companies/5cbe79ee-acb3-4597-896e-7662742593cd/live-runs?minCount=3", + 200, + ), + ).toBe(true); + expect( + shouldSilenceHttpSuccessLog( + "HEAD", + "/api/companies/5cbe79ee-acb3-4597-896e-7662742593cd/sidebar-badges", + 200, + ), + ).toBe(true); + }); + + it("silences successful static asset requests", () => { + expect(shouldSilenceHttpSuccessLog("GET", "/@fs/Users/dotta/paperclip/ui/src/main.tsx", 200)).toBe(true); + expect(shouldSilenceHttpSuccessLog("GET", "/src/App.tsx?t=123", 200)).toBe(true); + expect(shouldSilenceHttpSuccessLog("GET", "/site.webmanifest", 200)).toBe(true); + }); + + it("keeps normal successful application requests", () => { + expect(shouldSilenceHttpSuccessLog("GET", "/api/issues/PAP-1383", 200)).toBe(false); + expect(shouldSilenceHttpSuccessLog("PATCH", "/api/issues/PAP-1383", 200)).toBe(false); + }); + + it("keeps failing requests visible", () => { + expect(shouldSilenceHttpSuccessLog("GET", "/api/health", 500)).toBe(false); + expect(shouldSilenceHttpSuccessLog("GET", "/@fs/Users/dotta/paperclip/ui/src/main.tsx", 404)).toBe(false); + }); +}); diff --git a/server/src/middleware/http-log-policy.ts b/server/src/middleware/http-log-policy.ts new file mode 100644 index 00000000000..b9d924744d6 --- /dev/null +++ b/server/src/middleware/http-log-policy.ts @@ -0,0 +1,42 @@ +const SILENCED_SUCCESS_METHODS = new Set(["GET", "HEAD"]); + +const SILENCED_SUCCESS_API_PATHS = [ + /^\/api\/health(?:\/|$)/, + /^\/api\/companies\/[^/]+\/live-runs(?:\/|$)/, + /^\/api\/companies\/[^/]+\/sidebar-badges(?:\/|$)/, +]; + +const SILENCED_SUCCESS_STATIC_PREFIXES = [ + "/@fs/", + "/@id/", + "/@react-refresh", + "/@vite/", + "/_plugins/", + "/assets/", + "/node_modules/", + "/src/", +]; + +const SILENCED_SUCCESS_STATIC_PATHS = new Set([ + "/favicon.ico", + "/site.webmanifest", +]); + +function normalizePath(url: string): string { + const trimmed = url.trim(); + if (trimmed.length === 0) return "/"; + const pathname = trimmed.split("?")[0]?.trim() ?? "/"; + return pathname.length > 0 ? pathname : "/"; +} + +export function shouldSilenceHttpSuccessLog(method: string | undefined, url: string | undefined, statusCode: number): boolean { + if (statusCode >= 400) return false; + if (statusCode === 304) return true; + if (!method || !url) return false; + if (!SILENCED_SUCCESS_METHODS.has(method.toUpperCase())) return false; + + const pathname = normalizePath(url); + if (SILENCED_SUCCESS_STATIC_PATHS.has(pathname)) return true; + if (SILENCED_SUCCESS_STATIC_PREFIXES.some((prefix) => pathname.startsWith(prefix))) return true; + return SILENCED_SUCCESS_API_PATHS.some((pattern) => pattern.test(pathname)); +} diff --git a/server/src/middleware/logger.ts b/server/src/middleware/logger.ts index 7844e567368..7eb5c2b4342 100644 --- a/server/src/middleware/logger.ts +++ b/server/src/middleware/logger.ts @@ -4,6 +4,7 @@ import pino from "pino"; import { pinoHttp } from "pino-http"; import { readConfigFile } from "../config-file.js"; import { resolveDefaultLogsDir, resolveHomeAwarePath } from "../home-paths.js"; +import { shouldSilenceHttpSuccessLog } from "./http-log-policy.js"; function resolveServerLogDir(): string { const envOverride = process.env.PAPERCLIP_LOG_DIR?.trim(); @@ -47,6 +48,9 @@ export const logger = pino({ export const httpLogger = pinoHttp({ logger, customLogLevel(_req, res, err) { + if (shouldSilenceHttpSuccessLog(_req.method, _req.url, res.statusCode)) { + return "silent"; + } if (err || res.statusCode >= 500) return "error"; if (res.statusCode >= 400) return "warn"; return "info";