feat: switch logging to pino

Closes HP-279
This commit is contained in:
Aarnav Tale
2026-06-17 15:58:48 -04:00
parent 57c8046f99
commit 3252482e0b
8 changed files with 292 additions and 27 deletions
+3 -1
View File
@@ -7,6 +7,8 @@ import { renderToPipeableStream } from "react-dom/server";
import type { AppLoadContext, EntryContext } from "react-router"; import type { AppLoadContext, EntryContext } from "react-router";
import { ServerRouter } from "react-router"; import { ServerRouter } from "react-router";
import log from "~/utils/log";
export const streamTimeout = 5_000; export const streamTimeout = 5_000;
export default function handleRequest( export default function handleRequest(
request: Request, request: Request,
@@ -52,7 +54,7 @@ export default function handleRequest(
// errors encountered during initial shell rendering since they'll // errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest. // reject and get logged in handleDocumentRequest.
if (shellRendered) { if (shellRendered) {
console.error(error); log.error("server", "Streaming render error: %o", error);
} }
}, },
}, },
+7
View File
@@ -57,15 +57,22 @@ const listenFile = listenFilePath
} }
: undefined; : undefined;
const runtimeLogger = {
info: (message: string, ...args: unknown[]) => log.info("server", message, ...args),
error: (message: string, ...args: unknown[]) => log.error("server", message, ...args),
};
startHttpServer({ startHttpServer({
host: config.server.host, host: config.server.host,
port: config.server.port, port: config.server.port,
tls, tls,
logger: runtimeLogger,
listenFile, listenFile,
listener: composeListener({ listener: composeListener({
basename: __PREFIX__, basename: __PREFIX__,
staticRoot: clientDir, staticRoot: clientDir,
immutableAssets: true, immutableAssets: true,
logger: runtimeLogger,
requestListener, requestListener,
}), }),
onShutdown: dispose, onShutdown: dispose,
+34 -20
View File
@@ -1,44 +1,58 @@
// MARK: Side-Effects // MARK: Side-Effects
// This module contains a side-effect because everything running here // This module contains a side-effect because log levels are read from
// is static and logger is later modified in `app/server/index.ts` to // the environment once at module initialization.
// disable debug logging if the `HEADPLANE_DEBUG_LOG` specifies as such.
import pino from "pino";
const levels = ["info", "warn", "error", "debug"] as const; const levels = ["info", "warn", "error", "debug"] as const;
type Category = "server" | "config" | "agent" | "api" | "auth" | "sse"; type Category = "server" | "config" | "agent" | "api" | "auth" | "sse";
type Level = (typeof levels)[number];
export interface Logger extends Record< export interface Logger extends Record<
(typeof levels)[number], Level,
(category: Category, message: string, ...args: unknown[]) => void (category: Category, message: string, ...args: unknown[]) => void
> { > {
debugEnabled: boolean; debugEnabled: boolean;
} }
const logLevels = getLogLevels(); const rootLogger = createRootLogger();
export default { export default {
debugEnabled: logLevels.includes("debug"), debugEnabled: rootLogger.isLevelEnabled("debug"),
debug: (..._: Parameters<Logger["debug"]>) => {}, info: (category, msg, ...args) => rootLogger.info({ component: category }, msg, ...args),
...Object.fromEntries( warn: (category, msg, ...args) => rootLogger.warn({ component: category }, msg, ...args),
logLevels.map((level) => [ error: (category, msg, ...args) => rootLogger.error({ component: category }, msg, ...args),
level, debug: (category, msg, ...args) => rootLogger.debug({ component: category }, msg, ...args),
(category: Category, message: string, ...args: unknown[]) => {
const date = new Date().toISOString();
console.log(`${date} [${category}] ${level.toUpperCase()}: ${message}`, ...args);
},
]),
),
} as Logger; } as Logger;
function getLogLevels() { function createRootLogger() {
const options = {
level: getLogLevel(),
timestamp: () => `,"timestamp":"${new Date().toISOString()}"`,
formatters: {
level: (label: string) => ({ level: label }),
},
} satisfies pino.LoggerOptions;
if (process.env.NODE_ENV === "test") {
return pino(options);
}
const destination = pino.destination({ dest: 1, sync: false });
process.on("exit", () => destination.flushSync());
return pino(options, destination);
}
function getLogLevel(): Level {
const debugLog = process.env.HEADPLANE_DEBUG_LOG; const debugLog = process.env.HEADPLANE_DEBUG_LOG;
if (debugLog == null) { if (debugLog == null) {
return ["info", "warn", "error"]; return "info";
} }
const normalized = debugLog.trim().toLowerCase(); const normalized = debugLog.trim().toLowerCase();
const truthyValues = ["1", "true", "yes", "on"]; const truthyValues = ["1", "true", "yes", "on"];
if (!truthyValues.includes(normalized)) { if (!truthyValues.includes(normalized)) {
return ["info", "warn", "error"]; return "info";
} }
return ["info", "warn", "error", "debug"]; return "debug";
} }
+4
View File
@@ -82,6 +82,10 @@ The path-based secret loading mechanism also works with environment variables. F
## Debugging ## Debugging
Headplane writes server logs as newline-delimited JSON using Pino. Each entry
includes a timestamp, level, component, and message (`msg`) so log aggregation
tools can filter and index it directly.
To enable debug logging, set the **`HEADPLANE_DEBUG_LOG=true`** environment variable. To enable debug logging, set the **`HEADPLANE_DEBUG_LOG=true`** environment variable.
This will enable all debug logs for Headplane, which could fill up log space very quickly. This will enable all debug logs for Headplane, which could fill up log space very quickly.
This is not recommended in production environments. This is not recommended in production environments.
+3 -1
View File
@@ -7,7 +7,7 @@
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"build": "react-router build", "build": "react-router build",
"dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev", "dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml react-router dev | pino-pretty -c",
"start": "node build/server/index.js", "start": "node build/server/index.js",
"typecheck": "react-router typegen && tsgo", "typecheck": "react-router typegen && tsgo",
"test:unit": "vitest run --project unit", "test:unit": "vitest run --project unit",
@@ -40,6 +40,7 @@
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"mime": "^4.1.0", "mime": "^4.1.0",
"pino": "^10.3.1",
"react": "19.2.5", "react": "19.2.5",
"react-codemirror-merge": "4.25.9", "react-codemirror-merge": "4.25.9",
"react-dom": "19.2.5", "react-dom": "19.2.5",
@@ -66,6 +67,7 @@
"oxfmt": "^0.44.0", "oxfmt": "^0.44.0",
"oxlint": "^1.59.0", "oxlint": "^1.59.0",
"oxlint-tsgolint": "^0.20.0", "oxlint-tsgolint": "^0.20.0",
"pino-pretty": "^13.1.3",
"tailwindcss": "^4.2.2", "tailwindcss": "^4.2.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"testcontainers": "^11.14.0", "testcontainers": "^11.14.0",
+164 -2
View File
@@ -74,6 +74,9 @@ importers:
mime: mime:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
pino:
specifier: ^10.3.1
version: 10.3.1
react: react:
specifier: 19.2.5 specifier: 19.2.5
version: 19.2.5 version: 19.2.5
@@ -147,6 +150,9 @@ importers:
oxlint-tsgolint: oxlint-tsgolint:
specifier: ^0.20.0 specifier: ^0.20.0
version: 0.20.0 version: 0.20.0
pino-pretty:
specifier: ^13.1.3
version: 13.1.3
tailwindcss: tailwindcss:
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2 version: 4.2.2
@@ -1396,6 +1402,9 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -2133,6 +2142,10 @@ packages:
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
b4a@1.7.3: b4a@1.7.3:
resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==}
peerDependencies: peerDependencies:
@@ -2334,6 +2347,9 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
combined-stream@1.0.8: combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -2402,6 +2418,9 @@ packages:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
debug@4.4.3: debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@@ -2692,9 +2711,15 @@ packages:
exsolve@1.0.8: exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
fast-copy@4.0.3:
resolution: {integrity: sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==}
fast-fifo@1.3.2: fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -2796,6 +2821,9 @@ packages:
hast-util-whitespace@3.0.0: hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
hookable@5.5.3: hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -2886,6 +2914,10 @@ packages:
jose@6.2.2: jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-base64@3.7.8: js-base64@3.7.8:
resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
@@ -3246,6 +3278,10 @@ packages:
obug@2.1.1: obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
once@1.4.0: once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -3312,6 +3348,20 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'} engines: {node: '>=12'}
pino-abstract-transport@3.0.0:
resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
pino-pretty@13.1.3:
resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==}
hasBin: true
pino-std-serializers@7.1.0:
resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
pino@10.3.1:
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
hasBin: true
pkg-types@2.3.0: pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
@@ -3333,6 +3383,9 @@ packages:
process-nextick-args@2.0.1: process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
process@0.11.10: process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
@@ -3360,6 +3413,9 @@ packages:
pump@3.0.4: pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
rc@1.2.8: rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true hasBin: true
@@ -3428,6 +3484,13 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'} engines: {node: '>= 14.18.0'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
real-require@1.0.0:
resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==}
regex-recursion@6.0.2: regex-recursion@6.0.2:
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
@@ -3481,12 +3544,19 @@ packages:
safe-buffer@5.2.1: safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
safer-buffer@2.1.2: safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
secure-json-parse@4.1.0:
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
semver@6.3.1: semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
@@ -3538,6 +3608,9 @@ packages:
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3559,6 +3632,10 @@ packages:
split-ca@1.0.1: split-ca@1.0.1:
resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
sprintf-js@1.1.3: sprintf-js@1.1.3:
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
@@ -3614,6 +3691,10 @@ packages:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
strip-json-comments@5.0.3:
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
engines: {node: '>=14.16'}
style-mod@4.1.3: style-mod@4.1.3:
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
@@ -3695,6 +3776,10 @@ packages:
peerDependencies: peerDependencies:
typescript: ^5 typescript: ^5
thread-stream@4.2.0:
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
engines: {node: '>=20'}
tinybench@2.9.0: tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -5138,6 +5223,8 @@ snapshots:
'@oxlint/binding-win32-x64-msvc@1.59.0': '@oxlint/binding-win32-x64-msvc@1.59.0':
optional: true optional: true
'@pinojs/redact@0.4.0': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@@ -5840,6 +5927,8 @@ snapshots:
asynckit@0.4.0: {} asynckit@0.4.0: {}
atomic-sleep@1.0.0: {}
b4a@1.7.3: {} b4a@1.7.3: {}
b4a@1.8.0: {} b4a@1.8.0: {}
@@ -6040,6 +6129,8 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
colorette@2.0.20: {}
combined-stream@1.0.8: combined-stream@1.0.8:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
@@ -6106,6 +6197,8 @@ snapshots:
data-uri-to-buffer@4.0.1: data-uri-to-buffer@4.0.1:
optional: true optional: true
dateformat@4.6.3: {}
debug@4.4.3: debug@4.4.3:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -6348,8 +6441,12 @@ snapshots:
exsolve@1.0.8: {} exsolve@1.0.8: {}
fast-copy@4.0.3: {}
fast-fifo@1.3.2: {} fast-fifo@1.3.2: {}
fast-safe-stringify@2.1.1: {}
fdir@6.5.0(picomatch@4.0.4): fdir@6.5.0(picomatch@4.0.4):
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
@@ -6468,6 +6565,8 @@ snapshots:
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
help-me@5.0.0: {}
hookable@5.5.3: {} hookable@5.5.3: {}
hpagent@1.2.0: {} hpagent@1.2.0: {}
@@ -6541,6 +6640,8 @@ snapshots:
jose@6.2.2: {} jose@6.2.2: {}
joycon@3.1.1: {}
js-base64@3.7.8: js-base64@3.7.8:
optional: true optional: true
@@ -6794,8 +6895,7 @@ snapshots:
dependencies: dependencies:
brace-expansion: 2.0.3 brace-expansion: 2.0.3
minimist@1.2.8: minimist@1.2.8: {}
optional: true
minipass@7.1.3: {} minipass@7.1.3: {}
@@ -6861,6 +6961,8 @@ snapshots:
obug@2.1.1: {} obug@2.1.1: {}
on-exit-leak-free@2.1.2: {}
once@1.4.0: once@1.4.0:
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
@@ -6962,6 +7064,42 @@ snapshots:
picomatch@4.0.4: {} picomatch@4.0.4: {}
pino-abstract-transport@3.0.0:
dependencies:
split2: 4.2.0
pino-pretty@13.1.3:
dependencies:
colorette: 2.0.20
dateformat: 4.6.3
fast-copy: 4.0.3
fast-safe-stringify: 2.1.1
help-me: 5.0.0
joycon: 3.1.1
minimist: 1.2.8
on-exit-leak-free: 2.1.2
pino-abstract-transport: 3.0.0
pump: 3.0.4
secure-json-parse: 4.1.0
sonic-boom: 4.2.1
strip-json-comments: 5.0.3
pino-std-serializers@7.1.0: {}
pino@10.3.1:
dependencies:
'@pinojs/redact': 0.4.0
atomic-sleep: 1.0.0
on-exit-leak-free: 2.1.2
pino-abstract-transport: 3.0.0
pino-std-serializers: 7.1.0
process-warning: 5.0.0
quick-format-unescaped: 4.0.4
real-require: 0.2.0
safe-stable-stringify: 2.5.0
sonic-boom: 4.2.1
thread-stream: 4.2.0
pkg-types@2.3.0: pkg-types@2.3.0:
dependencies: dependencies:
confbox: 0.2.4 confbox: 0.2.4
@@ -6994,6 +7132,8 @@ snapshots:
process-nextick-args@2.0.1: {} process-nextick-args@2.0.1: {}
process-warning@5.0.0: {}
process@0.11.10: {} process@0.11.10: {}
promise-limit@2.7.0: promise-limit@2.7.0:
@@ -7039,6 +7179,8 @@ snapshots:
end-of-stream: 1.4.5 end-of-stream: 1.4.5
once: 1.4.0 once: 1.4.0
quick-format-unescaped@4.0.4: {}
rc@1.2.8: rc@1.2.8:
dependencies: dependencies:
deep-extend: 0.6.0 deep-extend: 0.6.0
@@ -7121,6 +7263,10 @@ snapshots:
readdirp@4.1.2: {} readdirp@4.1.2: {}
real-require@0.2.0: {}
real-require@1.0.0: {}
regex-recursion@6.0.2: regex-recursion@6.0.2:
dependencies: dependencies:
regex-utilities: 2.3.0 regex-utilities: 2.3.0
@@ -7202,10 +7348,14 @@ snapshots:
safe-buffer@5.2.1: {} safe-buffer@5.2.1: {}
safe-stable-stringify@2.5.0: {}
safer-buffer@2.1.2: {} safer-buffer@2.1.2: {}
scheduler@0.27.0: {} scheduler@0.27.0: {}
secure-json-parse@4.1.0: {}
semver@6.3.1: {} semver@6.3.1: {}
semver@7.7.4: {} semver@7.7.4: {}
@@ -7260,6 +7410,10 @@ snapshots:
ip-address: 10.1.0 ip-address: 10.1.0
smart-buffer: 4.2.0 smart-buffer: 4.2.0
sonic-boom@4.2.1:
dependencies:
atomic-sleep: 1.0.0
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map-support@0.5.21: source-map-support@0.5.21:
@@ -7277,6 +7431,8 @@ snapshots:
split-ca@1.0.1: {} split-ca@1.0.1: {}
split2@4.2.0: {}
sprintf-js@1.1.3: {} sprintf-js@1.1.3: {}
ssh-remote-port-forward@1.0.4: ssh-remote-port-forward@1.0.4:
@@ -7352,6 +7508,8 @@ snapshots:
strip-json-comments@2.0.1: strip-json-comments@2.0.1:
optional: true optional: true
strip-json-comments@5.0.3: {}
style-mod@4.1.3: {} style-mod@4.1.3: {}
superjson@2.2.2: superjson@2.2.2:
@@ -7519,6 +7677,10 @@ snapshots:
dependencies: dependencies:
typescript: 6.0.2 typescript: 6.0.2
thread-stream@4.2.0:
dependencies:
real-require: 1.0.0
tinybench@2.9.0: {} tinybench@2.9.0: {}
tinyexec@1.1.1: {} tinyexec@1.1.1: {}
+18 -3
View File
@@ -19,16 +19,31 @@ import {
import { extname, normalize, resolve, sep } from "node:path"; import { extname, normalize, resolve, sep } from "node:path";
import mime from "mime"; import mime from "mime";
import pino from "pino";
export interface Logger { export interface Logger {
info: (msg: string, ...args: unknown[]) => void; info: (msg: string, ...args: unknown[]) => void;
error: (msg: string, ...args: unknown[]) => void; error: (msg: string, ...args: unknown[]) => void;
} }
// TODO: Replace with Pino! const runtimeDestination = pino.destination({ dest: 1, sync: false });
process.on("exit", () => runtimeDestination.flushSync());
const runtimePino = pino(
{
base: { service: "headplane" },
messageKey: "message",
timestamp: () => `,"timestamp":"${new Date().toISOString()}"`,
formatters: {
level: (label) => ({ level: label }),
},
},
runtimeDestination,
);
const defaultLogger: Logger = { const defaultLogger: Logger = {
info: (msg, ...args) => console.log(`[runtime] ${msg}`, ...args), info: (msg, ...args) => runtimePino.info({ component: "runtime" }, msg, ...args),
error: (msg, ...args) => console.error(`[runtime] ${msg}`, ...args), error: (msg, ...args) => runtimePino.error({ component: "runtime" }, msg, ...args),
}; };
export interface StaticOptions { export interface StaticOptions {
+59
View File
@@ -0,0 +1,59 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
const envSnapshot = { ...process.env };
async function loadLogger() {
vi.resetModules();
const mod = await import("~/utils/log");
return mod.default;
}
describe("structured logger", () => {
beforeEach(() => {
process.env = { ...envSnapshot };
vi.restoreAllMocks();
});
test("writes JSON log entries", async () => {
process.env.HEADPLANE_DEBUG_LOG = "false";
const spy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const log = await loadLogger();
log.info("server", "Listening on %s://%s:%s", "http", "127.0.0.1", 3000);
expect(spy).toHaveBeenCalledOnce();
const entry = JSON.parse(spy.mock.calls[0][0] as string);
expect(entry).toMatchObject({
level: "info",
component: "server",
msg: "Listening on http://127.0.0.1:3000",
});
expect(entry.timestamp).toEqual(expect.any(String));
});
test("keeps debug logs disabled by default", async () => {
delete process.env.HEADPLANE_DEBUG_LOG;
const spy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const log = await loadLogger();
log.debug("api", "GET %s", "/v1/node");
expect(log.debugEnabled).toBe(false);
expect(spy).not.toHaveBeenCalled();
});
test("writes debug entries when enabled", async () => {
process.env.HEADPLANE_DEBUG_LOG = "true";
const spy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
const log = await loadLogger();
log.debug("api", "GET %s", "/v1/node");
expect(log.debugEnabled).toBe(true);
expect(JSON.parse(spy.mock.calls[0][0] as string)).toMatchObject({
level: "debug",
component: "api",
msg: "GET /v1/node",
});
});
});