From 1cafaabe2ad1a8db7591cd9524ca11994d13283f Mon Sep 17 00:00:00 2001 From: Nick Tabeling Date: Thu, 9 Apr 2026 17:09:37 +0200 Subject: [PATCH] feat(DIS-101): introduce pino structured logger, remove console.* Replace all console.* calls in src/ with pino child-loggers. Add rootLogger + childLogger(component) in src/runtime/logger.ts. pino-pretty used as dev transport only. Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 247 ++++++++++++++++++++++ package.json | 7 +- src/bot.ts | 19 +- src/config/loader.ts | 10 +- src/index.ts | 15 +- src/runtime/logger.ts | 16 ++ tests/unit/logger.test.ts | 31 +++ tests/unit/workspace-root-resolve.test.ts | 17 +- 8 files changed, 334 insertions(+), 28 deletions(-) create mode 100644 src/runtime/logger.ts create mode 100644 tests/unit/logger.test.ts diff --git a/package-lock.json b/package-lock.json index 359ed8d..9464b0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,15 +12,18 @@ "cross-spawn": "^7.0.6", "discord.js": "^14.16.0", "dotenv": "^16.4.0", + "pino": "^10.3.1", "yaml": "^2.6.0" }, "devDependencies": { "@types/better-sqlite3": "^7.6.0", "@types/cross-spawn": "^6.0.6", "@types/node": "^22.0.0", + "@types/pino": "^7.0.4", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.57.0", + "pino-pretty": "^13.1.3", "typescript": "^5.7.0", "vitest": "^4.1.4" }, @@ -831,6 +834,12 @@ "node": ">= 8" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -1275,6 +1284,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pino": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-7.0.4.tgz", + "integrity": "sha512-yKw1UbZOTe7vP1xMQT+oz3FexwgIpBTrM+AC62vWgAkNRULgLTJWfYX+H5/sKPm8VXFbIcXkC3VZPyuaNioZFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pino": "*" + } + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1730,6 +1749,15 @@ "node": ">=12" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1891,6 +1919,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1919,6 +1954,16 @@ "node": ">= 8" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2330,6 +2375,13 @@ "node": ">=12.0.0" } }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2350,6 +2402,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -2569,6 +2628,13 @@ "node": ">=8" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2689,6 +2755,16 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2903,6 +2979,15 @@ ], "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3031,6 +3116,81 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.9", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", @@ -3097,6 +3257,22 @@ "node": ">= 0.8.0" } }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -3138,6 +3314,12 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3167,6 +3349,15 @@ "node": ">= 6" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3294,6 +3485,32 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -3379,6 +3596,15 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3389,6 +3615,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3482,6 +3717,18 @@ "dev": true, "license": "MIT" }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/package.json b/package.json index 30b93c9..b82d398 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "lint": "eslint src --ext .ts" }, "vitest": { - "include": ["tests/**/*.test.ts"], + "include": [ + "tests/**/*.test.ts" + ], "environment": "node" }, "dependencies": { @@ -19,15 +21,18 @@ "cross-spawn": "^7.0.6", "discord.js": "^14.16.0", "dotenv": "^16.4.0", + "pino": "^10.3.1", "yaml": "^2.6.0" }, "devDependencies": { "@types/better-sqlite3": "^7.6.0", "@types/cross-spawn": "^6.0.6", "@types/node": "^22.0.0", + "@types/pino": "^7.0.4", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.57.0", + "pino-pretty": "^13.1.3", "typescript": "^5.7.0", "vitest": "^4.1.4" }, diff --git a/src/bot.ts b/src/bot.ts index c7e03e5..fff85d0 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -11,6 +11,9 @@ import { DisclawDatabase } from "./db/database"; import { DisclawConfig, EnvConfig } from "./config/loader"; import { newAgentCommand, handleNewAgent } from "./commands/new-agent"; import { routeMessage } from "./router"; +import { childLogger } from "./runtime/logger"; + +const log = childLogger("bot"); export async function startBot( envConfig: EnvConfig, @@ -31,18 +34,18 @@ export async function startBot( // --- Event: Ready --- client.once("ready", async () => { if (!client.user) return; - console.log(`Bot online as ${client.user.tag}`); + log.info({ tag: client.user.tag }, "Bot online"); const guild = envConfig.DISCORD_GUILD_ID ? client.guilds.cache.get(envConfig.DISCORD_GUILD_ID) : client.guilds.cache.first(); if (!guild) { - console.error("No guild found. Is the bot added to a server?"); + log.error("No guild found. Is the bot added to a server?"); process.exit(1); } - console.log(`Connected to guild: ${guild.name} (${guild.id})`); + log.info({ guildName: guild.name, guildId: guild.id }, "Connected to guild"); // --- Ensure management channel exists --- const managementName = disclawConfig.management_channel; @@ -55,7 +58,7 @@ export async function startBot( ); if (existingChannel) { managementChannelId = existingWorkspace.channel_id; - console.log(`Management channel found: #${managementName}`); + log.info({ channelName: managementName }, "Management channel found"); } else { // Channel was deleted from Discord, recreate it existingWorkspace = undefined; @@ -80,7 +83,7 @@ export async function startBot( ); managementChannelId = found.id; mgmtChannel = found; - console.log(`Adopted existing channel #${managementName}`); + log.info({ channelName: managementName }, "Adopted existing channel"); } else { const newChannel = await guild.channels.create({ name: managementName, @@ -95,7 +98,7 @@ export async function startBot( ); managementChannelId = newChannel.id; mgmtChannel = newChannel; - console.log(`Created management channel #${managementName}`); + log.info({ channelName: managementName }, "Created management channel"); } // Send welcome message using the direct channel reference @@ -118,9 +121,9 @@ export async function startBot( body: [newAgentCommand.toJSON()], } ); - console.log("Slash commands registered."); + log.info("Slash commands registered."); } catch (err) { - console.error("Failed to register slash commands:", err); + log.error({ err }, "Failed to register slash commands"); } }); diff --git a/src/config/loader.ts b/src/config/loader.ts index 4b58ff7..346d260 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -3,6 +3,9 @@ import * as os from "os"; import * as path from "path"; import * as dotenv from "dotenv"; import * as YAML from "yaml"; +import { childLogger } from "../runtime/logger"; + +const log = childLogger("config"); export interface DisclawConfig { workspaces_root: string; @@ -37,10 +40,9 @@ export function expandWorkspacesRoot(raw: string, repoRoot: string): string { const isInsideRepo = !rel.startsWith("..") && !path.isAbsolute(rel); if (isInsideRepo) { - console.warn( - "[DisClaw] WARNING: workspaces_root lies inside the DisClaw repository (" + - resolved + - "). " + + log.warn( + { workspacesRoot: resolved }, + "workspaces_root lies inside the DisClaw repository. " + "Claude Code walks parent directories and concatenates every CLAUDE.md it finds. " + "A workspace under the repo root will inherit the project CLAUDE.md, " + "leaking DisClaw system instructions into every agent context. " + diff --git a/src/index.ts b/src/index.ts index 49fd4eb..fff9de0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,17 +3,20 @@ import { loadEnv, loadDisclawConfig } from "./config/loader"; import { DisclawDatabase } from "./db/database"; import { startBot } from "./bot"; import { acquireLock } from "./runtime/lockfile"; +import { childLogger } from "./runtime/logger"; +const log = childLogger("main"); const ROOT_DIR = path.resolve(__dirname, ".."); async function main(): Promise { - console.log("DisClaw starting..."); + log.info("DisClaw starting..."); // 1. Load configuration const envConfig = loadEnv(ROOT_DIR); const disclawConfig = loadDisclawConfig(ROOT_DIR); - console.log( - `Config loaded. Workspaces root: ${disclawConfig.workspaces_root}` + log.info( + { workspacesRoot: disclawConfig.workspaces_root }, + "Config loaded" ); // 2. Acquire startup lock (before DB init) @@ -24,14 +27,14 @@ async function main(): Promise { // 3. Initialize database const dbPath = path.join(ROOT_DIR, "data", "disclaw.db"); const db = new DisclawDatabase(dbPath); - console.log(`Database initialized at ${dbPath}`); + log.info({ dbPath }, "Database initialized"); // 4. Start Discord bot await startBot(envConfig, disclawConfig, db); // Graceful shutdown const shutdown = (): void => { - console.log("\nShutting down..."); + log.info("Shutting down..."); db.close(); lockRelease(); process.exit(0); @@ -42,6 +45,6 @@ async function main(): Promise { } main().catch((err) => { - console.error("Fatal error:", err); + log.error({ err }, "Fatal error"); process.exit(1); }); diff --git a/src/runtime/logger.ts b/src/runtime/logger.ts new file mode 100644 index 0000000..2b89208 --- /dev/null +++ b/src/runtime/logger.ts @@ -0,0 +1,16 @@ +import pino from "pino"; + +export const rootLogger = pino({ + level: process.env.LOG_LEVEL ?? "info", + transport: + process.env.NODE_ENV !== "production" + ? { target: "pino-pretty" } + : undefined, +}); + +/** + * Returns a child logger that includes `component` in every log entry. + */ +export function childLogger(component: string): pino.Logger { + return rootLogger.child({ component }); +} diff --git a/tests/unit/logger.test.ts b/tests/unit/logger.test.ts new file mode 100644 index 0000000..4a83156 --- /dev/null +++ b/tests/unit/logger.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { childLogger, rootLogger } from "../../src/runtime/logger"; +import pino from "pino"; + +describe("childLogger", () => { + it("returns a pino logger instance", () => { + const logger = childLogger("runner"); + expect(logger).toBeDefined(); + expect(typeof logger.info).toBe("function"); + expect(typeof logger.error).toBe("function"); + }); + + it("sets component binding on child logger", () => { + const logger = childLogger("runner"); + // pino child loggers expose their bindings via .bindings() + const bindings = (logger as pino.Logger).bindings(); + expect(bindings.component).toBe("runner"); + }); + + it("rootLogger is a pino logger", () => { + expect(typeof rootLogger.info).toBe("function"); + expect(typeof rootLogger.child).toBe("function"); + }); + + it("different component names produce distinct bindings", () => { + const runnerLog = childLogger("runner"); + const botLog = childLogger("bot"); + expect(runnerLog.bindings().component).toBe("runner"); + expect(botLog.bindings().component).toBe("bot"); + }); +}); diff --git a/tests/unit/workspace-root-resolve.test.ts b/tests/unit/workspace-root-resolve.test.ts index 233ba59..19973c5 100644 --- a/tests/unit/workspace-root-resolve.test.ts +++ b/tests/unit/workspace-root-resolve.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import * as os from "os"; import * as path from "path"; import { expandWorkspacesRoot } from "../../src/config/loader"; +import { rootLogger } from "../../src/runtime/logger"; const HOME = os.homedir(); // Use an absolute path that is portable across Linux, macOS, and Windows. @@ -10,7 +11,7 @@ const REPO_ROOT = path.join(os.tmpdir(), "fake-disclaw-repo"); describe("expandWorkspacesRoot", () => { beforeEach(() => { - vi.spyOn(console, "warn").mockImplementation(() => undefined); + vi.spyOn(rootLogger, "warn").mockImplementation(() => undefined); }); afterEach(() => { @@ -40,24 +41,22 @@ describe("expandWorkspacesRoot", () => { expect(path.isAbsolute(result)).toBe(true); }); - it("(d) triggers console.warn when workspaces_root is inside the repo", () => { + it("(d) triggers logger.warn when workspaces_root is inside the repo", () => { expandWorkspacesRoot("workspaces", REPO_ROOT); - expect(console.warn).toHaveBeenCalledOnce(); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining("[DisClaw] WARNING") - ); - expect(console.warn).toHaveBeenCalledWith( + expect(rootLogger.warn).toHaveBeenCalledOnce(); + expect(rootLogger.warn).toHaveBeenCalledWith( + expect.objectContaining({ workspacesRoot: expect.stringContaining(REPO_ROOT) }), expect.stringContaining("CLAUDE.md") ); }); it("(d) does NOT warn when workspaces_root is outside the repo", () => { expandWorkspacesRoot("~/.disclaw/workspaces", REPO_ROOT); - expect(console.warn).not.toHaveBeenCalled(); + expect(rootLogger.warn).not.toHaveBeenCalled(); }); it("(d) does NOT warn when an absolute path outside the repo is given", () => { expandWorkspacesRoot("/tmp/disclaw-workspaces", REPO_ROOT); - expect(console.warn).not.toHaveBeenCalled(); + expect(rootLogger.warn).not.toHaveBeenCalled(); }); }); -- 2.45.2