fleet-memory/hindsight-integrations/openclaw/src/logger.ts
akhater d441ab814d
feat(openclaw): configurable logging with structured output (#739)
* feat(openclaw): configurable logging with structured output

Replace raw console.log/warn/error spam with a structured logger.
New plugin settings: logLevel, logSummaryIntervalMs, logCompact.
Bank mission log demoted to verbose-only. Retain/recall batched
into periodic summaries. Each recall now shows memory count injected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* use api.logger for framework-consistent output, show autoRecall/autoRetain on init

Route all log output through OpenClaw's api.logger instead of raw console
calls. Matches mem0 plugin style. Startup now shows mode + feature flags.
Dropped logCompact setting (framework handles formatting). Added subtle
slate-blue color to hindsight prefix for visual differentiation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add bank name to init and summary logs, fix singular/plural consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* rename log levels to standard: off, error, warning, info, debug

Per review feedback — use standard level names instead of custom ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: billy <billy@oclaw.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nicolò Boschi <boschi1997@gmail.com>
2026-03-30 10:38:02 +02:00

147 lines
4.7 KiB
TypeScript

/**
* Hindsight OpenClaw plugin logger.
*
* Routes output through OpenClaw's api.logger for consistent formatting
* with other plugins (same colors/timestamps as mem0, etc.).
*
* Features:
* - Configurable log level: 'off' | 'error' | 'warning' | 'info' | 'debug'
* - Batched retain/recall summaries instead of per-event spam
*/
export type LogLevel = 'off' | 'error' | 'warning' | 'info' | 'debug';
export interface LoggerConfig {
/** Minimum severity to print. Default: 'info' */
logLevel?: LogLevel;
/** Interval in ms to print batched retain/recall summaries. 0 = print every event. Default: 300000 (5 min) */
logSummaryIntervalMs?: number;
}
// Muted blue (38;5;103 = slate/dusty blue from 256-color palette)
const PREFIX = '\x1b[38;5;103mhindsight:\x1b[0m';
const LEVEL_RANK: Record<LogLevel, number> = {
off: 0,
error: 1,
warning: 2,
info: 3,
debug: 4,
};
// Output backend — set via setApiLogger, falls back to console
let apiLogger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void } = {
info: (msg) => console.log(msg),
warn: (msg) => console.warn(msg),
error: (msg) => console.error(msg),
};
// Batched summary state
let retainCount = 0;
let retainMsgTotal = 0;
let recallCount = 0;
let recallMemoriesCount = 0;
const banksSeen = new Set<string>();
let lastSummaryTime = Date.now();
let summaryTimer: ReturnType<typeof setInterval> | null = null;
let currentLevel: LogLevel = 'info';
let currentSummaryIntervalMs = 300_000; // 5 min
/** Bind to OpenClaw's api.logger for consistent output formatting */
export function setApiLogger(logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void }): void {
apiLogger = logger;
}
export function configureLogger(cfg: LoggerConfig): void {
currentLevel = cfg.logLevel ?? 'info';
currentSummaryIntervalMs = cfg.logSummaryIntervalMs ?? 300_000;
// Restart summary timer
if (summaryTimer) {
clearInterval(summaryTimer);
summaryTimer = null;
}
if (currentSummaryIntervalMs > 0 && LEVEL_RANK[currentLevel] >= LEVEL_RANK['info']) {
summaryTimer = setInterval(flushSummary, currentSummaryIntervalMs);
summaryTimer.unref?.(); // don't keep process alive
}
}
function allowed(level: LogLevel): boolean {
return LEVEL_RANK[currentLevel] >= LEVEL_RANK[level];
}
/** Info-level log (requires 'info' or higher) */
export function info(msg: string): void {
if (!allowed('info')) return;
apiLogger.info(`${PREFIX} ${msg}`);
}
/** Debug log (requires 'debug') */
export function verbose(msg: string): void {
if (!allowed('debug')) return;
apiLogger.info(`${PREFIX} ${msg}`);
}
/** Warning (requires 'warning' or higher) */
export function warn(msg: string): void {
if (!allowed('warning')) return;
apiLogger.warn(`${PREFIX} ${msg}`);
}
/** Error (requires 'error' or higher) */
export function error(msg: string, err?: unknown): void {
if (!allowed('error')) return;
const detail = err instanceof Error ? err.message : (err ? String(err) : '');
apiLogger.error(`${PREFIX} ${detail ? `${msg}: ${detail}` : msg}`);
}
/** Track a retain event for batched summary */
export function trackRetain(bankId: string, messageCount: number): void {
retainCount++;
retainMsgTotal += messageCount;
banksSeen.add(bankId);
if (currentSummaryIntervalMs === 0 && allowed('info')) {
apiLogger.info(`${PREFIX} auto-retained ${messageCount} messages (bank: ${bankId})`);
}
}
/** Track a recall event for batched summary */
export function trackRecall(bankId: string, memoriesFound: number): void {
recallCount++;
recallMemoriesCount += memoriesFound;
banksSeen.add(bankId);
// per-event logging is handled by info() call at the injection site
}
/** Flush the batched summary to console */
export function flushSummary(): void {
if (!allowed('info')) return;
if (retainCount === 0 && recallCount === 0) return;
const elapsed = Math.round((Date.now() - lastSummaryTime) / 1000);
const parts: string[] = [];
if (recallCount > 0) parts.push(`${recallCount} recalls (${recallMemoriesCount} memories injected)`);
if (retainCount > 0) parts.push(`${retainCount} retains (${retainMsgTotal} messages captured)`);
const bankList = [...banksSeen];
const bankLabel = bankList.length === 1 ? 'bank' : 'banks';
const banks = bankList.length > 0 ? ` (${bankLabel}: ${bankList.join(', ')})` : '';
apiLogger.info(`${PREFIX} ${parts.join(', ')} in ${elapsed}s${banks}`);
retainCount = 0;
retainMsgTotal = 0;
recallCount = 0;
recallMemoriesCount = 0;
banksSeen.clear();
lastSummaryTime = Date.now();
}
/** Cleanup (call on plugin stop) */
export function stopLogger(): void {
flushSummary();
if (summaryTimer) {
clearInterval(summaryTimer);
summaryTimer = null;
}
}