c2015c4dfc
**Problem:** Worker service crashed on startup with: TypeError: Cannot read properties of undefined (reading 'get') at new Wd (.../worker-service.cjs:52:131469) **Root Cause:** Circular dependency between SettingsDefaultsManager and logger: 1. SettingsDefaultsManager imports logger 2. logger imports SettingsDefaultsManager 3. logger constructor calls SettingsDefaultsManager.get() at init time 4. When CommonJS resolves the cycle, SettingsDefaultsManager is undefined **Solution:** Break the circular dependency by making logger lazy-load its configuration: - Change logger.level from initialized in constructor to lazy-loaded - Add getLevel() method that loads on first access - Update all level checks to use getLevel() This allows SettingsDefaultsManager to import logger without triggering the circular dependency, since logger no longer accesses SettingsDefaultsManager during module initialization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
244 lines
7.0 KiB
TypeScript
244 lines
7.0 KiB
TypeScript
/**
|
|
* Structured Logger for claude-mem Worker Service
|
|
* Provides readable, traceable logging with correlation IDs and data flow tracking
|
|
*/
|
|
|
|
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
|
|
|
export enum LogLevel {
|
|
DEBUG = 0,
|
|
INFO = 1,
|
|
WARN = 2,
|
|
ERROR = 3,
|
|
SILENT = 4
|
|
}
|
|
|
|
export type Component = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM' | 'HTTP' | 'SESSION' | 'CHROMA';
|
|
|
|
interface LogContext {
|
|
sessionId?: number;
|
|
sdkSessionId?: string;
|
|
correlationId?: string;
|
|
[key: string]: any;
|
|
}
|
|
|
|
class Logger {
|
|
private level: LogLevel | null = null;
|
|
private useColor: boolean;
|
|
|
|
constructor() {
|
|
// Disable colors when output is not a TTY (e.g., PM2 logs)
|
|
this.useColor = process.stdout.isTTY ?? false;
|
|
}
|
|
|
|
/**
|
|
* Lazy-load log level from settings (breaks circular dependency with SettingsDefaultsManager)
|
|
*/
|
|
private getLevel(): LogLevel {
|
|
if (this.level === null) {
|
|
const envLevel = SettingsDefaultsManager.get('CLAUDE_MEM_LOG_LEVEL').toUpperCase();
|
|
this.level = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
|
|
}
|
|
return this.level;
|
|
}
|
|
|
|
/**
|
|
* Create correlation ID for tracking an observation through the pipeline
|
|
*/
|
|
correlationId(sessionId: number, observationNum: number): string {
|
|
return `obs-${sessionId}-${observationNum}`;
|
|
}
|
|
|
|
/**
|
|
* Create session correlation ID
|
|
*/
|
|
sessionId(sessionId: number): string {
|
|
return `session-${sessionId}`;
|
|
}
|
|
|
|
/**
|
|
* Format data for logging - create compact summaries instead of full dumps
|
|
*/
|
|
private formatData(data: any): string {
|
|
if (data === null || data === undefined) return '';
|
|
if (typeof data === 'string') return data;
|
|
if (typeof data === 'number') return data.toString();
|
|
if (typeof data === 'boolean') return data.toString();
|
|
|
|
// For objects, create compact summaries
|
|
if (typeof data === 'object') {
|
|
// If it's an error, show message and stack in debug mode
|
|
if (data instanceof Error) {
|
|
return this.getLevel() === LogLevel.DEBUG
|
|
? `${data.message}\n${data.stack}`
|
|
: data.message;
|
|
}
|
|
|
|
// For arrays, show count
|
|
if (Array.isArray(data)) {
|
|
return `[${data.length} items]`;
|
|
}
|
|
|
|
// For objects, show key count
|
|
const keys = Object.keys(data);
|
|
if (keys.length === 0) return '{}';
|
|
if (keys.length <= 3) {
|
|
// Show small objects inline
|
|
return JSON.stringify(data);
|
|
}
|
|
return `{${keys.length} keys: ${keys.slice(0, 3).join(', ')}...}`;
|
|
}
|
|
|
|
return String(data);
|
|
}
|
|
|
|
/**
|
|
* Format a tool name and input for compact display
|
|
*/
|
|
formatTool(toolName: string, toolInput?: any): string {
|
|
if (!toolInput) return toolName;
|
|
|
|
try {
|
|
const input = typeof toolInput === 'string' ? JSON.parse(toolInput) : toolInput;
|
|
|
|
// Special formatting for common tools
|
|
if (toolName === 'Bash' && input.command) {
|
|
const cmd = input.command.length > 50
|
|
? input.command.substring(0, 50) + '...'
|
|
: input.command;
|
|
return `${toolName}(${cmd})`;
|
|
}
|
|
|
|
if (toolName === 'Read' && input.file_path) {
|
|
const path = input.file_path.split('/').pop() || input.file_path;
|
|
return `${toolName}(${path})`;
|
|
}
|
|
|
|
if (toolName === 'Edit' && input.file_path) {
|
|
const path = input.file_path.split('/').pop() || input.file_path;
|
|
return `${toolName}(${path})`;
|
|
}
|
|
|
|
if (toolName === 'Write' && input.file_path) {
|
|
const path = input.file_path.split('/').pop() || input.file_path;
|
|
return `${toolName}(${path})`;
|
|
}
|
|
|
|
// Default: just show tool name
|
|
return toolName;
|
|
} catch {
|
|
return toolName;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Core logging method
|
|
*/
|
|
private log(
|
|
level: LogLevel,
|
|
component: Component,
|
|
message: string,
|
|
context?: LogContext,
|
|
data?: any
|
|
): void {
|
|
if (level < this.getLevel()) return;
|
|
|
|
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 23);
|
|
const levelStr = LogLevel[level].padEnd(5);
|
|
const componentStr = component.padEnd(6);
|
|
|
|
// Build correlation ID part
|
|
let correlationStr = '';
|
|
if (context?.correlationId) {
|
|
correlationStr = `[${context.correlationId}] `;
|
|
} else if (context?.sessionId) {
|
|
correlationStr = `[session-${context.sessionId}] `;
|
|
}
|
|
|
|
// Build data part
|
|
let dataStr = '';
|
|
if (data !== undefined && data !== null) {
|
|
if (this.getLevel() === LogLevel.DEBUG && typeof data === 'object') {
|
|
// In debug mode, show full JSON for objects
|
|
dataStr = '\n' + JSON.stringify(data, null, 2);
|
|
} else {
|
|
dataStr = ' ' + this.formatData(data);
|
|
}
|
|
}
|
|
|
|
// Build additional context
|
|
let contextStr = '';
|
|
if (context) {
|
|
const { sessionId, sdkSessionId, correlationId, ...rest } = context;
|
|
if (Object.keys(rest).length > 0) {
|
|
const pairs = Object.entries(rest).map(([k, v]) => `${k}=${v}`);
|
|
contextStr = ` {${pairs.join(', ')}}`;
|
|
}
|
|
}
|
|
|
|
const logLine = `[${timestamp}] [${levelStr}] [${componentStr}] ${correlationStr}${message}${contextStr}${dataStr}`;
|
|
|
|
// Output to appropriate stream
|
|
if (level === LogLevel.ERROR) {
|
|
console.error(logLine);
|
|
} else {
|
|
console.log(logLine);
|
|
}
|
|
}
|
|
|
|
// Public logging methods
|
|
debug(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.log(LogLevel.DEBUG, component, message, context, data);
|
|
}
|
|
|
|
info(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.log(LogLevel.INFO, component, message, context, data);
|
|
}
|
|
|
|
warn(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.log(LogLevel.WARN, component, message, context, data);
|
|
}
|
|
|
|
error(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.log(LogLevel.ERROR, component, message, context, data);
|
|
}
|
|
|
|
/**
|
|
* Log data flow: input → processing
|
|
*/
|
|
dataIn(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.info(component, `→ ${message}`, context, data);
|
|
}
|
|
|
|
/**
|
|
* Log data flow: processing → output
|
|
*/
|
|
dataOut(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.info(component, `← ${message}`, context, data);
|
|
}
|
|
|
|
/**
|
|
* Log successful completion
|
|
*/
|
|
success(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.info(component, `✓ ${message}`, context, data);
|
|
}
|
|
|
|
/**
|
|
* Log failure
|
|
*/
|
|
failure(component: Component, message: string, context?: LogContext, data?: any): void {
|
|
this.error(component, `✗ ${message}`, context, data);
|
|
}
|
|
|
|
/**
|
|
* Log timing information
|
|
*/
|
|
timing(component: Component, message: string, durationMs: number, context?: LogContext): void {
|
|
this.info(component, `⏱ ${message}`, context, { duration: `${durationMs}ms` });
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const logger = new Logger();
|