fc5c2d5e07
- 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.
237 lines
6.7 KiB
TypeScript
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();
|