diff --git a/packages/apprentice/src/daemon/adapters/discord.ts b/packages/apprentice/src/daemon/adapters/discord.ts index a504b40..1256c9c 100644 --- a/packages/apprentice/src/daemon/adapters/discord.ts +++ b/packages/apprentice/src/daemon/adapters/discord.ts @@ -9,6 +9,7 @@ import { ChannelType, Partials, } from 'discord.js'; +import { createLogger } from '../../utils/logger.js'; import { type PlatformAdapter, type PlatformConfig, @@ -20,6 +21,8 @@ import { type DiscordConfig, } from '../types.js'; +const log = createLogger({ namespace: 'Discord' }); + export class DiscordAdapter implements PlatformAdapter { public readonly platform = 'discord' as const; @@ -51,7 +54,7 @@ export class DiscordAdapter implements PlatformAdapter { return new Promise((resolve, reject) => { this.client.once(Events.ClientReady, () => { this.ready = true; - console.log(`Discord connected as ${this.client.user?.tag}`); + log.info('Connected', { tag: this.client.user?.tag }); resolve(); }); @@ -102,7 +105,7 @@ export class DiscordAdapter implements PlatformAdapter { try { await this.onMessage(incomingMsg); } catch (error) { - console.error('Error handling Discord message:', error); + log.error('Error handling message', { error }); } }); @@ -126,7 +129,7 @@ export class DiscordAdapter implements PlatformAdapter { added: true, }); } catch (error) { - console.error('Error handling Discord reaction:', error); + log.error('Error handling reaction', { error }); } }); @@ -146,7 +149,7 @@ export class DiscordAdapter implements PlatformAdapter { added: false, }); } catch (error) { - console.error('Error handling Discord reaction remove:', error); + log.error('Error handling reaction remove', { error }); } }); } @@ -259,13 +262,11 @@ export class DiscordAdapter implements PlatformAdapter { messageRef: MessageRef, content: MessageContent, ): Promise { - console.log( - `[Discord] Editing message ${messageRef.messageId} in channel ${messageRef.channelId}`, - ); + log.debug('Editing message', { messageId: messageRef.messageId, channelId: messageRef.channelId }); const channel = await this.resolveChannel(messageRef); - console.log(`[Discord] Resolved channel, fetching message...`); + log.debug('Resolved channel, fetching message'); const message = await channel.messages.fetch(messageRef.messageId); - console.log(`[Discord] Message fetched successfully`); + log.debug('Message fetched successfully'); const editOptions: any = {}; @@ -274,7 +275,7 @@ export class DiscordAdapter implements PlatformAdapter { } if (content.image) { - console.log(`[Discord] Attaching image (${content.image.length} bytes)`); + log.debug('Attaching image', { size: content.image.length }); const attachment = new AttachmentBuilder(content.image, { name: 'progress.png', }); @@ -299,9 +300,9 @@ export class DiscordAdapter implements PlatformAdapter { ]; } - console.log(`[Discord] Applying message edit...`); + log.debug('Applying message edit'); await message.edit(editOptions); - console.log(`[Discord] Message edit complete`); + log.debug('Message edit complete'); } public async deleteMessage(messageRef: MessageRef): Promise { diff --git a/packages/apprentice/src/daemon/agent-runner.ts b/packages/apprentice/src/daemon/agent-runner.ts index efea612..41abb9c 100644 --- a/packages/apprentice/src/daemon/agent-runner.ts +++ b/packages/apprentice/src/daemon/agent-runner.ts @@ -3,11 +3,15 @@ import { EventEmitter } from 'events'; import { writeFileSync } from 'fs'; import { join, dirname } from 'path'; import { promisify } from 'util'; +import { createLogger } from '../utils/logger.js'; import { ProgressFileWriter, getProgressFilePath } from './progress-file.js'; import { type AgentConfig, type ProgressConfig } from './types.js'; const execAsync = promisify(exec); +const log = createLogger({ namespace: 'AgentRunner' }); +const agentLog = createLogger({ namespace: 'Agent' }); + export interface AgentProcess extends EventEmitter { readonly id: string; readonly pid: number | undefined; @@ -73,7 +77,7 @@ export class AgentRunner { // Start asynchronously but catch any errors process.start().catch((err) => { - console.error(`[AgentRunner] Failed to start process:`, err); + log.error('Failed to start process', { error: err }); }); return process; } @@ -150,46 +154,42 @@ class AgentProcessImpl extends EventEmitter implements AgentProcess { this.process = this.spawnAgent(); - console.log( - `[AgentRunner] Process spawned with PID: ${this.process.pid}`, - ); + log.info('Process spawned', { pid: this.process.pid }); await this.progressWriter.addLogEntry( `Process spawned (PID: ${this.process.pid})`, ); // Debug lifecycle events this.process.on('spawn', () => { - console.log(`[AgentRunner] Process spawn event fired`); + log.debug('Process spawn event fired'); }); this.process.on('disconnect', () => { - console.log(`[AgentRunner] Process disconnected`); + log.debug('Process disconnected'); }); this.process.on('exit', (code, signal) => { - console.log( - `[AgentRunner] Process exit: code=${code}, signal=${signal}`, - ); + log.debug('Process exit', { code, signal }); }); this.process.stdout?.on('data', (data) => { const text = data.toString(); - console.log(`[Agent] stdout: ${text.trim()}`); + agentLog.debug('stdout', { data: text.trim() }); this.handleOutput(text); }); this.process.stdout?.on('end', () => { - console.log(`[AgentRunner] stdout stream ended`); + log.debug('stdout stream ended'); }); this.process.stderr?.on('data', (data) => { const text = data.toString(); - console.log(`[Agent] stderr: ${text.trim()}`); + agentLog.debug('stderr', { data: text.trim() }); this.handleOutput(text); }); this.process.on('close', (code) => { - console.log(`[AgentRunner] Process closed with code: ${code}`); + log.info('Process closed', { code }); this.handleClose(code); }); this.process.on('error', (err) => { - console.error(`[AgentRunner] Process error:`, err); + log.error('Process error', { error: err }); this.handleError(err); }); @@ -220,7 +220,7 @@ class AgentProcessImpl extends EventEmitter implements AgentProcess { ); const promptContent = this.buildProgressInstructions(progressFilePath); writeFileSync(promptFilePath, promptContent); - console.log(`[AgentRunner] Wrote prompt file: ${promptFilePath}`); + log.info('Wrote prompt file', { path: promptFilePath }); // Reference the prompt file at the start, then the task const fullTask = `First, read ${promptFilePath} for important instructions.\n\n${this.options.task}`; @@ -231,9 +231,9 @@ class AgentProcessImpl extends EventEmitter implements AgentProcess { const workspaceArg = workspace ? `--workspace '${workspace}'` : ''; const command = `cursor agent -p --approve-mcps --output-format stream-json ${workspaceArg} '${escapedTask}'`; - console.log(`[AgentRunner] Spawning command: ${command.slice(0, 200)}...`); - console.log(`[AgentRunner] Working directory: ${workspace}`); - console.log(`[AgentRunner] Progress file: ${progressFilePath}`); + log.info('Spawning command', { command: command.slice(0, 200) }); + log.info('Working directory', { cwd: workspace }); + log.info('Progress file', { path: progressFilePath }); // Use stdio: "inherit" so output goes directly to terminal for debugging // Progress updates come from the file monitor, not stdout parsing @@ -312,7 +312,7 @@ Rules: cwd: this.options.workingDirectory || process.cwd(), }); } catch (error) { - console.error(`Failed to remove worktree: ${error}`); + log.error('Failed to remove worktree', { error }); } } @@ -335,7 +335,7 @@ Rules: try { const event = JSON.parse(line); // Log full event for debugging - console.log(`[Agent] JSON event:`, JSON.stringify(event, null, 2)); + agentLog.debug('JSON event', { event }); this.handleEvent(event); return; } catch { @@ -348,9 +348,7 @@ Rules: } private handleEvent(event: any): void { - console.log( - `[Agent] Event type: ${event.type}, subtype: ${event.subtype || 'none'}`, - ); + agentLog.debug('Event', { type: event.type, subtype: event.subtype || 'none' }); // Only handle result events - the spawned agent handles progress updates if (event.type === 'result') { diff --git a/packages/apprentice/src/daemon/index.ts b/packages/apprentice/src/daemon/index.ts index 3f915a0..5bf9d47 100644 --- a/packages/apprentice/src/daemon/index.ts +++ b/packages/apprentice/src/daemon/index.ts @@ -1,5 +1,6 @@ import { fastComplete } from '../ai/client'; import { isAIAvailable } from '../ai/config'; +import { createLogger } from '../utils/logger.js'; import { AgentRunner, type AgentQuestion } from './agent-runner.js'; import { loadConfig } from './config.js'; import { deleteProgressFile, readProgressFile } from './progress-file.js'; @@ -9,6 +10,8 @@ import { SessionManager } from './session-manager.js'; import { type Session, type SessionProgressFile } from './session.js'; import { type DaemonConfig, type PlatformAdapter, type IncomingMessage } from './types.js'; +const log = createLogger({ namespace: 'Daemon' }); + export class AgentDaemon { private adapters: Map = new Map(); private sessions: SessionManager; @@ -57,10 +60,7 @@ export class AgentDaemon { await this.initializeAdapters(); - console.log( - 'Agent daemon started, listening on:', - [...this.adapters.keys()].join(', '), - ); + log.info('Agent daemon started', { adapters: [...this.adapters.keys()].join(', ') }); process.on('SIGINT', () => this.shutdown()); process.on('SIGTERM', () => this.shutdown()); @@ -73,9 +73,7 @@ export class AgentDaemon { const session = this.sessions.getSession(sessionId); if (!session) return; - console.log( - `[Daemon] Progress update for ${sessionId}: ${progress.stage} - ${progress.tasks.estimatedPercentComplete}%`, - ); + log.debug('Progress update', { sessionId, stage: progress.stage, percent: progress.tasks.estimatedPercentComplete }); // Handle waiting state (agent needs input) if (progress.stage === 'waiting') { @@ -99,9 +97,7 @@ export class AgentDaemon { const session = this.sessions.getSession(sessionId); if (!session) return; - console.log( - `[Daemon] Session ${sessionId} completed: ${progress.stage}`, - ); + log.info('Session completed', { sessionId, stage: progress.stage }); // Stop UI updates this.stopUIUpdates(sessionId); @@ -155,11 +151,11 @@ export class AgentDaemon { } if (this.config.slack?.enabled) { - console.log('Slack adapter not yet implemented'); + log.warn('Slack adapter not yet implemented'); } if (this.config.teams?.enabled) { - console.log('Teams adapter not yet implemented'); + log.warn('Teams adapter not yet implemented'); } } @@ -216,12 +212,12 @@ export class AgentDaemon { return; } - console.log('[Daemon] Generating thread name...'); + log.debug('Generating thread name'); const threadName = await this.generateThreadName(msg.content); - console.log(`[Daemon] Thread name: "${threadName}"`); - console.log('[Daemon] Creating thread...'); + log.debug('Thread name generated', { name: threadName }); + log.debug('Creating thread'); const thread = await adapter.createThread(msg.channel, threadName); - console.log(`[Daemon] Thread created: ${thread.threadId}`); + log.info('Thread created', { threadId: thread.threadId }); const repository = this.inferRepository(msg) || @@ -236,7 +232,7 @@ export class AgentDaemon { .replace(/\s+/g, ' ') .trim(); - console.log('[Daemon] Creating session...'); + log.debug('Creating session'); const session = this.sessions.createSession({ userId: msg.userId, platform: msg.platform, @@ -244,10 +240,9 @@ export class AgentDaemon { task: cleanedTask, repository, }); - console.log(`[Daemon] Session created: ${session.id}`); + log.info('Session created', { sessionId: session.id }); - // Render initial progress image (use a placeholder until the file is created) - console.log('[Daemon] Rendering initial progress image...'); + log.debug('Rendering initial progress image'); const initialProgress: SessionProgressFile = { sessionId: session.id, stage: 'starting', @@ -258,8 +253,8 @@ export class AgentDaemon { updatedAt: new Date().toISOString(), }; const initialImage = await this.renderer.render(initialProgress, 0); - console.log(`[Daemon] Initial image size: ${initialImage.length} bytes`); - console.log('[Daemon] Sending initial progress message...'); + log.debug('Initial image rendered', { size: initialImage.length }); + log.debug('Sending initial progress message'); const progressMsg = await adapter.sendMessage(thread, { image: initialImage, }); @@ -267,28 +262,26 @@ export class AgentDaemon { this.sessions.updateSession(session.id, { progressMessageRef: progressMsg, }); - console.log(`[Daemon] Progress message sent: ${progressMsg.messageId}`); + log.debug('Progress message sent', { messageId: progressMsg.messageId }); - console.log('[Daemon] Spawning agent process...'); + log.info('Spawning agent process'); const agent = this.runner.spawn({ sessionId: session.id, task: session.task, repository: session.repository, branch: session.branch, }); - console.log(`[Daemon] Agent spawned with session ID: ${agent.id}`); + log.info('Agent spawned', { sessionId: agent.id }); - // Start monitoring the progress file (uses session ID) - console.log('[Daemon] Starting progress file monitor...'); + log.debug('Starting progress file monitor'); this.progressMonitor.startMonitoring(session.id); - // Start UI update loop - console.log('[Daemon] Starting UI update loop...'); + log.debug('Starting UI update loop'); this.startUIUpdates(session, adapter); // Listen for agent events (questions need immediate handling) agent.on('question', async (q: AgentQuestion) => { - console.log(`[Daemon] Agent asked question: ${q.question}`); + log.info('Agent asked question', { question: q.question }); await adapter.sendMessage(thread, { text: `❓ **Agent needs input:**\n${q.question}${ q.options ? `\n\nOptions: ${q.options.join(', ')}` : '' @@ -297,13 +290,11 @@ export class AgentDaemon { }); agent.on('complete', async () => { - console.log(`[Daemon] Agent completed`); - // Progress monitor will handle the rest via file monitoring + log.info('Agent completed'); }); agent.on('error', async (error: Error) => { - console.error(`[Daemon] Agent error: ${error.message}`); - // Progress monitor will handle the rest via file monitoring + log.error('Agent error', { error: error.message }); }); } @@ -321,7 +312,7 @@ export class AgentDaemon { await this.updateProgressUI(currentSession, progress, adapter); } catch (error) { - console.error('[Daemon] Failed to update progress UI:', error); + log.error('Failed to update progress UI', { error }); } }, this.config.progress.updateIntervalMs); @@ -340,15 +331,13 @@ export class AgentDaemon { ); try { - console.log( - `[Daemon] Updating progress UI (${progress.tasks.estimatedPercentComplete}% complete, ${progress.tasks.completed}/${progress.tasks.total} tasks)`, - ); + log.debug('Updating progress UI', { percent: progress.tasks.estimatedPercentComplete, completed: progress.tasks.completed, total: progress.tasks.total }); const image = await this.renderer.render(progress, elapsedSeconds); - console.log(`[Daemon] Rendered image size: ${image.length} bytes`); + log.debug('Rendered image', { size: image.length }); await adapter.editMessage(session.progressMessageRef, { image }); - console.log(`[Daemon] Progress image updated successfully`); + log.debug('Progress image updated'); } catch (error) { - console.error('[Daemon] Failed to update progress:', error); + log.error('Failed to update progress', { error }); } } @@ -364,21 +353,14 @@ export class AgentDaemon { session: Session, response: string, ): Promise { - console.log( - `[Daemon] User response in session ${session.id}: ${response.slice( - 0, - 50, - )}...`, - ); + log.debug('User response received', { sessionId: session.id, response: response.slice(0, 50) }); const agent = this.runner.getProcess(session.agentProcessId!); if (!agent) { - console.error( - `[Daemon] No agent process found for session ${session.id}`, - ); + log.error('No agent process found for session', { sessionId: session.id }); return; } - console.log(`[Daemon] Sending input to agent ${session.agentProcessId}`); + log.debug('Sending input to agent', { agentProcessId: session.agentProcessId }); await agent.sendInput(response); } @@ -397,38 +379,36 @@ export class AgentDaemon { .replace(/\s+/g, ' ') .trim(); - console.log( - `[Daemon] Cleaned content for thread name: "${cleaned.slice(0, 100)}"`, - ); + log.debug('Cleaned content for thread name', { content: cleaned.slice(0, 100) }); if (!isAIAvailable()) { - console.log('[Daemon] AI not available, using fallback thread name'); + log.debug('AI not available, using fallback thread name'); return `Agent: ${cleaned.slice(0, 50)}`; } try { - console.log('[Daemon] Requesting AI-generated thread name...'); + log.debug('Requesting AI-generated thread name'); const result = await fastComplete( `Task: "${cleaned}"\n\nGenerate a SHORT thread title (max 40 chars). Use title case. Be specific. Examples:\n- "Fix Login Bug"\n- "Add Dark Mode"\n- "Update Dependencies"\n\nTitle:`, "You create concise thread titles for coding tasks. Use 2-5 words maximum. No sentences. Title case. Be specific about what's being done.", ); - console.log(`[Daemon] AI response: "${result.text}"`); + log.debug('AI response received', { text: result.text }); const generated = result.text .trim() .replace(/^["']|["']$/g, '') .replace(/^Title:\s*/i, '') .slice(0, 40); - console.log(`[Daemon] Generated thread name: "${generated}"`); + log.debug('Generated thread name', { name: generated }); return generated || `Agent: ${cleaned.slice(0, 35)}`; } catch (error) { - console.error(`[Daemon] Failed to generate thread name:`, error); + log.error('Failed to generate thread name', { error }); return `Agent: ${cleaned.slice(0, 35)}`; } } public async shutdown(): Promise { - console.log('Shutting down agent daemon...'); + log.info('Shutting down agent daemon'); // Stop progress monitoring this.progressMonitor.stopAll(); @@ -448,7 +428,7 @@ export class AgentDaemon { await this.runner.cancelAll(); for (const [name, adapter] of this.adapters) { - console.log(`Disconnecting ${name}...`); + log.info('Disconnecting adapter', { name }); await adapter.disconnect(); } diff --git a/packages/apprentice/src/daemon/progress-monitor.ts b/packages/apprentice/src/daemon/progress-monitor.ts index 7c71208..66f1163 100644 --- a/packages/apprentice/src/daemon/progress-monitor.ts +++ b/packages/apprentice/src/daemon/progress-monitor.ts @@ -1,8 +1,11 @@ import { EventEmitter } from 'events'; +import { createLogger } from '../utils/logger.js'; import { readProgressFile } from './progress-file.js'; import { type SessionProgressFile } from './session.js'; import { type ProgressConfig } from './types.js'; +const log = createLogger({ namespace: 'ProgressMonitor' }); + export interface ProgressMonitorEvents { update: (sessionId: string, progress: SessionProgressFile) => void; complete: (sessionId: string, progress: SessionProgressFile) => void; @@ -32,7 +35,7 @@ export class ProgressFileMonitor extends EventEmitter { return; } - console.log(`[ProgressMonitor] Starting to monitor session: ${sessionId}`); + log.info('Starting to monitor session', { sessionId }); const interval = setInterval(async () => { await this.checkProgress(sessionId); @@ -55,7 +58,7 @@ export class ProgressFileMonitor extends EventEmitter { if (session) { clearInterval(session.interval); this.monitoredSessions.delete(sessionId); - console.log(`[ProgressMonitor] Stopped monitoring session: ${sessionId}`); + log.info('Stopped monitoring session', { sessionId }); } } @@ -93,10 +96,7 @@ export class ProgressFileMonitor extends EventEmitter { } } } catch (error) { - console.error( - `[ProgressMonitor] Error reading progress for ${sessionId}:`, - error, - ); + log.error('Error reading progress', { sessionId, error }); this.emit('error', sessionId, error as Error); } } diff --git a/packages/apprentice/src/utils/dotenv.ts b/packages/apprentice/src/utils/dotenv.ts index a9c8ece..e5edb17 100644 --- a/packages/apprentice/src/utils/dotenv.ts +++ b/packages/apprentice/src/utils/dotenv.ts @@ -38,6 +38,7 @@ import crypto from 'crypto'; import fs from 'fs'; import os from 'os'; import path from 'path'; +import { createLogger } from './logger.js'; type EnvRecord = Record; type ProcessEnv = NodeJS.ProcessEnv | EnvRecord; @@ -77,9 +78,7 @@ const supportsAnsi = (): boolean => process.stdout.isTTY; const dim = (text: string): string => supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text; -const log = (msg: string): void => console.log(`[dotenv] ${msg}`); -const debug = (msg: string): void => console.debug(`[dotenv:debug] ${msg}`); -const warn = (msg: string): void => console.warn(`[dotenv:warn] ${msg}`); +const log = createLogger({ namespace: 'dotenv' }); const resolveHome = (envPath: string): string => envPath.startsWith('~') ? path.join(os.homedir(), envPath.slice(1)) : envPath; @@ -167,12 +166,12 @@ const populate = ( const exists = Object.prototype.hasOwnProperty.call(target, key); if (exists && !override) { - if (showDebug) debug(`"${key}" already defined, not overwritten`); + if (showDebug) log.debug(`"${key}" already defined, not overwritten`); continue; } if (exists && showDebug) { - debug(`"${key}" already defined, overwritten`); + log.debug(`"${key}" already defined, overwritten`); } target[key] = value; @@ -287,7 +286,7 @@ const configVault = (options: DotenvOptions): DotenvResult => { const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET ?? options.quiet); if (showDebug || !quiet) { - log('Loading env from encrypted .env.vault'); + log.info('Loading env from encrypted .env.vault'); } const parsed = parseVault(options); @@ -324,7 +323,7 @@ const configDotenv = (options?: DotenvOptions): DotenvResult => { populate(parsedAll, parsed, options); } catch (e) { if (showDebug) - debug(`Failed to load ${filePath}: ${(e as Error).message}`); + log.debug(`Failed to load ${filePath}: ${(e as Error).message}`); lastError = e as Error; } } @@ -346,7 +345,7 @@ const configDotenv = (options?: DotenvOptions): DotenvResult => { } }) .join(', '); - log( + log.info( `injected ${count} env var(s) from ${shortPaths} ${dim( '-- tip: set DOTENV_CONFIG_QUIET=true to silence', )}`, @@ -368,7 +367,7 @@ export const config = (options?: DotenvOptions): DotenvResult => { const vaultPath = findVaultPath(options); if (!vaultPath) { - warn( + log.warn( `DOTENV_KEY is set but .env.vault file not found. Did you forget to build it?`, ); return configDotenv(options); diff --git a/packages/apprentice/src/utils/logger.ts b/packages/apprentice/src/utils/logger.ts new file mode 100644 index 0000000..194d3e3 --- /dev/null +++ b/packages/apprentice/src/utils/logger.ts @@ -0,0 +1,85 @@ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +interface LogContext { + [key: string]: unknown; +} + +interface LoggerOptions { + namespace: string; + level?: LogLevel; +} + +const LOG_LEVELS: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, +}; + +const DEFAULT_LEVEL: LogLevel = process.env.NODE_ENV === 'production' ? 'info' : 'debug'; + +function shouldLog(currentLevel: LogLevel, messageLevel: LogLevel): boolean { + return LOG_LEVELS[messageLevel] >= LOG_LEVELS[currentLevel]; +} + +function formatMessage( + namespace: string, + level: LogLevel, + message: string, + context?: LogContext, +): string { + const timestamp = new Date().toISOString(); + const contextStr = context ? ` ${JSON.stringify(context)}` : ''; + return `[${timestamp}] [${level.toUpperCase()}] [${namespace}] ${message}${contextStr}`; +} + +export interface Logger { + readonly namespace: string; + debug(message: string, context?: LogContext): void; + info(message: string, context?: LogContext): void; + warn(message: string, context?: LogContext): void; + error(message: string, context?: LogContext): void; +} + +export function createLogger(options: LoggerOptions): Logger { + const namespace = options.namespace; + const level = options.level ?? DEFAULT_LEVEL; + + const log = (messageLevel: LogLevel, message: string, context?: LogContext) => { + if (!shouldLog(level, messageLevel)) return; + + const formatted = formatMessage(namespace, messageLevel, message, context); + + switch (messageLevel) { + case 'debug': + console.debug(formatted); + break; + case 'info': + console.log(formatted); + break; + case 'warn': + console.warn(formatted); + break; + case 'error': + console.error(formatted); + break; + } + }; + + return { + namespace, + debug: (message, context) => log('debug', message, context), + info: (message, context) => log('info', message, context), + warn: (message, context) => log('warn', message, context), + error: (message, context) => log('error', message, context), + }; +} + +export const logger = createLogger({ namespace: 'app' }); + +export function createChildLogger(parentLogger: Logger, childNamespace: string): Logger { + return createLogger({ + namespace: `${parentLogger.namespace}:${childNamespace}`, + level: DEFAULT_LEVEL, + }); +}