Files
claude-mem/src/utils/logger.ts
T
Alex Newman fc5c2d5e07 Refactor settings management to use ~/.claude-mem/settings.json
- Updated paths in troubleshooting documentation to reflect new settings file location.
- Modified diagnostics and reference files to read from ~/.claude-mem/settings.json.
- Introduced getWorkerPort utility for cleaner worker port retrieval.
- Enhanced ChromaSync and SDKAgent to load Python version and Claude path from settings.
- Updated SettingsRoutes to validate new settings: CLAUDE_MEM_LOG_LEVEL and CLAUDE_MEM_PYTHON_VERSION.
- Added early-settings module to load settings for logger and other early-stage modules.
- Adjusted logger to use early-loaded log level setting.
- Refactored paths to utilize early-loaded data directory setting.
2025-12-09 12:23:33 -05:00

237 lines
6.7 KiB
TypeScript

/**
* Structured Logger for claude-mem Worker Service
* Provides readable, traceable logging with correlation IDs and data flow tracking
*/
import { loadEarlySetting } from '../shared/early-settings.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;
private useColor: boolean;
constructor() {
// Parse log level from environment
const envLevel = loadEarlySetting('CLAUDE_MEM_LOG_LEVEL', 'INFO').toUpperCase();
this.level = LogLevel[envLevel as keyof typeof LogLevel] ?? LogLevel.INFO;
// Disable colors when output is not a TTY (e.g., PM2 logs)
this.useColor = process.stdout.isTTY ?? false;
}
/**
* 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.level === 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.level) 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.level === 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();