diff --git a/app/utils/log.ts b/app/utils/log.ts index 9704143f..13a72361 100644 --- a/app/utils/log.ts +++ b/app/utils/log.ts @@ -3,6 +3,8 @@ // is static and logger is later modified in `app/server/index.ts` to // disable debug logging if the `HEADPLANE_DEBUG_LOG` specifies as such. +import pino from "pino"; + const levels = ["info", "warn", "error", "debug"] as const; type Category = "server" | "config" | "agent" | "api" | "auth" | "sse"; @@ -13,32 +15,38 @@ export interface Logger extends Record< debugEnabled: boolean; } -const logLevels = getLogLevels(); -export default { - debugEnabled: logLevels.includes("debug"), - debug: (..._: Parameters) => {}, - ...Object.fromEntries( - logLevels.map((level) => [ - level, - (category: Category, message: string, ...args: unknown[]) => { - const date = new Date().toISOString(); - console.log(`${date} [${category}] ${level.toUpperCase()}: ${message}`, ...args); - }, - ]), - ), -} as Logger; - -function getLogLevels() { +function isDebugEnabled() { const debugLog = process.env.HEADPLANE_DEBUG_LOG; - if (debugLog == null) { - return ["info", "warn", "error"]; - } - + if (debugLog == null) return false; const normalized = debugLog.trim().toLowerCase(); - const truthyValues = ["1", "true", "yes", "on"]; - if (!truthyValues.includes(normalized)) { - return ["info", "warn", "error"]; - } - - return ["info", "warn", "error", "debug"]; + return ["1", "true", "yes", "on"].includes(normalized); } + +const debugEnabled = isDebugEnabled(); + +const pinoLogger = pino({ + level: debugEnabled ? "debug" : "info", + formatters: { + level: (label) => { + return { level: label }; + }, + }, +}); + +export default { + debugEnabled, + debug: (...args: Parameters) => { + if (!debugEnabled) return; + const [category, message, ...rest] = args; + pinoLogger.debug({ category }, message, ...rest); + }, + info: (category: Category, message: string, ...args: unknown[]) => { + pinoLogger.info({ category }, message, ...args); + }, + warn: (category: Category, message: string, ...args: unknown[]) => { + pinoLogger.warn({ category }, message, ...args); + }, + error: (category: Category, message: string, ...args: unknown[]) => { + pinoLogger.error({ category }, message, ...args); + }, +} as Logger; diff --git a/nix/package.nix b/nix/package.nix index 9a4774b5..965f3594 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -33,7 +33,7 @@ in inherit (finalAttrs) pname version src; fetcherVersion = 3; pnpm = pnpm_10; - hash = "sha256-NGIeboj/2kXuWsmTVl1fv4LgU1VYRdO+qSnNLVuneC8="; + hash = "sha256-QHGsNb4A9hctK0EDqXe5fzTndc8r+X38Sb4aJA3/ZOc="; }; buildPhase = '' diff --git a/package.json b/package.json index 058317bd..f7022584 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "lucide-react": "^1.8.0", "mime": "^4.1.0", "openapi-types": "^12.1.3", + "pino": "^10.3.1", "react": "19.2.5", "react-codemirror-merge": "4.25.9", "react-dom": "19.2.5", @@ -69,6 +70,7 @@ "oxfmt": "^0.44.0", "oxlint": "^1.59.0", "oxlint-tsgolint": "^0.20.0", + "pino-pretty": "^13.1.3", "tailwindcss": "^4.2.2", "tailwindcss-animate": "^1.0.7", "testcontainers": "^11.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a6a48a0..8eb74024 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: openapi-types: specifier: ^12.1.3 version: 12.1.3 + pino: + specifier: ^10.3.1 + version: 10.3.1 react: specifier: 19.2.5 version: 19.2.5 @@ -153,6 +156,9 @@ importers: oxlint-tsgolint: specifier: ^0.20.0 version: 0.20.0 + pino-pretty: + specifier: ^13.1.3 + version: 13.1.3 tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -1412,6 +1418,9 @@ packages: cpu: [x64] os: [win32] + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2179,6 +2188,10 @@ packages: asynckit@0.4.0: 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: resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} peerDependencies: @@ -2380,6 +2393,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2448,6 +2464,9 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2738,12 +2757,18 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + fast-copy@4.0.3: + resolution: {integrity: sha512-58apWr0GUiDFM8+3afrO6eYwJBn9ZAhDOzG3L+/9llab/haCARS2UIfffmOurYLwbgDRs8n0rfr6qAAPEAuAQw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -2848,6 +2873,9 @@ packages: hast-util-whitespace@3.0.0: 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: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -2938,6 +2966,10 @@ packages: jose@6.2.2: 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: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} @@ -3309,6 +3341,10 @@ packages: obug@2.1.1: 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: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3378,6 +3414,20 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} 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: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} @@ -3399,6 +3449,9 @@ packages: process-nextick-args@2.0.1: 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: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -3426,6 +3479,9 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -3494,6 +3550,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -3551,12 +3611,19 @@ packages: safe-buffer@5.2.1: 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: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} scheduler@0.27.0: 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: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3608,6 +3675,9 @@ packages: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} 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: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3629,6 +3699,10 @@ packages: split-ca@1.0.1: resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} @@ -3684,6 +3758,10 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} 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: resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} @@ -3765,6 +3843,10 @@ packages: peerDependencies: typescript: ^5 + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3873,6 +3955,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true valibot@1.3.1: @@ -5214,6 +5297,8 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.59.0': optional: true + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5951,6 +6036,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + b4a@1.7.3: {} b4a@1.8.0: {} @@ -6151,6 +6238,8 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -6217,6 +6306,8 @@ snapshots: data-uri-to-buffer@4.0.1: optional: true + dateformat@4.6.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -6459,10 +6550,14 @@ snapshots: exsolve@1.0.8: {} + fast-copy@4.0.3: {} + fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} + fast-safe-stringify@2.1.1: {} + fast-uri@3.1.0: {} fdir@6.5.0(picomatch@4.0.4): @@ -6583,6 +6678,8 @@ snapshots: dependencies: '@types/hast': 3.0.4 + help-me@5.0.0: {} + hookable@5.5.3: {} hpagent@1.2.0: {} @@ -6656,6 +6753,8 @@ snapshots: jose@6.2.2: {} + joycon@3.1.1: {} + js-base64@3.7.8: optional: true @@ -6915,8 +7014,7 @@ snapshots: dependencies: brace-expansion: 2.0.3 - minimist@1.2.8: - optional: true + minimist@1.2.8: {} minipass@7.1.3: {} @@ -6982,6 +7080,8 @@ snapshots: obug@2.1.1: {} + on-exit-leak-free@2.1.2: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -7085,6 +7185,42 @@ snapshots: 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.0.0 + pkg-types@2.3.0: dependencies: confbox: 0.2.4 @@ -7117,6 +7253,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@5.0.0: {} + process@0.11.10: {} promise-limit@2.7.0: @@ -7162,6 +7300,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + quick-format-unescaped@4.0.4: {} + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -7244,6 +7384,8 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -7327,10 +7469,14 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -7385,6 +7531,10 @@ snapshots: ip-address: 10.1.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-support@0.5.21: @@ -7402,6 +7552,8 @@ snapshots: split-ca@1.0.1: {} + split2@4.2.0: {} + sprintf-js@1.1.3: {} ssh-remote-port-forward@1.0.4: @@ -7477,6 +7629,8 @@ snapshots: strip-json-comments@2.0.1: optional: true + strip-json-comments@5.0.3: {} + style-mod@4.1.3: {} superjson@2.2.2: @@ -7644,6 +7798,10 @@ snapshots: dependencies: typescript: 6.0.2 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@1.1.1: {} diff --git a/tests/unit/utils/log.test.ts b/tests/unit/utils/log.test.ts new file mode 100644 index 00000000..4240e209 --- /dev/null +++ b/tests/unit/utils/log.test.ts @@ -0,0 +1,85 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +describe("Pino Structured Logging", () => { + const originalEnv = process.env.HEADPLANE_DEBUG_LOG; + let stdoutSpy: any; + + beforeEach(() => { + vi.clearAllMocks(); + stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + vi.resetModules(); + }); + + afterEach(() => { + process.env.HEADPLANE_DEBUG_LOG = originalEnv; + stdoutSpy.mockRestore(); + }); + + // Helper to get the logged object + const getParsedLog = () => { + const call = stdoutSpy.mock.calls[stdoutSpy.mock.calls.length - 1]; + if (!call) return null; + return JSON.parse(call[0].toString()); + }; + + test("should expose primary logging methods", async () => { + const log = (await import(`~/utils/log?update=${Date.now()}`)).default; + expect(typeof log.info).toBe("function"); + expect(typeof log.warn).toBe("function"); + expect(typeof log.error).toBe("function"); + expect(typeof log.debug).toBe("function"); + }); + + test("should correctly structure JSON payloads via info method", async () => { + const log = (await import(`~/utils/log?update=${Date.now()}`)).default; + log.info("server", "test message", "extra arg"); + + const payload = getParsedLog(); + expect(payload).toBeDefined(); + expect(payload.level).toBe("info"); + expect(payload.category).toBe("server"); + expect(payload.msg).toBe("test message"); + // pino formats with extra args if format directives are passed in msg. + }); + + test("should correctly format warn and error payloads", async () => { + const log = (await import(`~/utils/log?update=${Date.now()}`)).default; + + log.warn("api", "warning text"); + const warnPayload = getParsedLog(); + expect(warnPayload.level).toBe("warn"); + expect(warnPayload.category).toBe("api"); + expect(warnPayload.msg).toBe("warning text"); + + log.error("auth", "error text"); + const errorPayload = getParsedLog(); + expect(errorPayload.level).toBe("error"); + expect(errorPayload.category).toBe("auth"); + expect(errorPayload.msg).toBe("error text"); + }); + + test("should respect HEADPLANE_DEBUG_LOG toggle when disabled", async () => { + process.env.HEADPLANE_DEBUG_LOG = "false"; + const freshLog = (await import(`~/utils/log?update=${Date.now()}`)).default; + + expect(freshLog.debugEnabled).toBe(false); + + freshLog.debug("api", "should not log"); + // Should not have made a new call + expect(stdoutSpy).not.toHaveBeenCalled(); + }); + + test("should respect HEADPLANE_DEBUG_LOG toggle when enabled", async () => { + process.env.HEADPLANE_DEBUG_LOG = "1"; + const freshLog = (await import(`~/utils/log?update=${Date.now()}`)).default; + + expect(freshLog.debugEnabled).toBe(true); + + freshLog.debug("config", "should log now"); + + const debugPayload = getParsedLog(); + expect(debugPayload.level).toBe("debug"); + expect(debugPayload.category).toBe("config"); + expect(debugPayload.msg).toBe("should log now"); + }); +});