feat(log): record history

This commit is contained in:
Johannes Millan 2025-07-10 12:41:54 +02:00
parent 30998b21da
commit 4cfb6c5dcf

View file

@ -8,9 +8,27 @@ export enum LogLevel {
DEBUG = 4,
}
interface LogEntry {
timestamp: number;
level: string;
context: string;
message: string;
args: unknown[];
}
// Map old numeric levels to new enum for backwards compatibility
const LOG_LEVEL = environment.production ? LogLevel.DEBUG : LogLevel.NORMAL;
// IMPORTANT: All Log class methods and context loggers (SyncLog, etc.) record logs to history
// for later export. The trade-off is that line numbers will show log.ts instead of the
// actual calling location. This is a fundamental limitation of JavaScript - any wrapper
// function breaks line number preservation.
//
// Available logging methods:
// 1. Direct console.log() - preserves line numbers but NO recording
// 2. Log.normal() etc - records to history (shows log.ts line numbers)
// 3. SyncLog.error() etc - records to history with context (shows log.ts line numbers)
/**
* Modern logging class that preserves line numbers using pre-bound console methods
*/
@ -21,6 +39,12 @@ export class Log {
/** Current context string */
private static context: string = '';
/** Array to store log entries for download */
private static logHistory: LogEntry[] = [];
/** Maximum number of log entries to keep in memory */
private static maxHistorySize: number = 1000;
/** Pre-bound console functions that preserve line numbers */
private static readonly c = console.log.bind(console); // critical
private static readonly e = (console.error || console.log).bind(console);
@ -43,31 +67,54 @@ export class Log {
return this.context.length ? `[${this.context}]` : '';
}
/** Record log entry to history */
private static recordLog(level: string, context: string, args: unknown[]): void {
const entry: LogEntry = {
timestamp: Date.now(),
level,
context,
message: args.length > 0 ? String(args[0]) : '',
args: args.slice(1),
};
this.logHistory.push(entry);
// Keep history size under control
if (this.logHistory.length > this.maxHistorySize) {
this.logHistory = this.logHistory.slice(-this.maxHistorySize);
}
}
static critical(...args: unknown[]): void {
// Critical always logs (level 0)
this.recordLog('CRITICAL', this.context, args);
this.c(this.getPrefix(), ...args);
}
static error(...args: unknown[]): void {
if (this.level >= LogLevel.ERROR) {
this.recordLog('ERROR', this.context, args);
this.e(this.getPrefix(), ...args);
}
}
static normal(...args: unknown[]): void {
if (this.level >= LogLevel.NORMAL) {
this.recordLog('NORMAL', this.context, args);
this.n(this.getPrefix(), ...args);
}
}
static verbose(...args: unknown[]): void {
if (this.level >= LogLevel.VERBOSE) {
this.recordLog('VERBOSE', this.context, args);
this.v(this.getPrefix(), ...args);
}
}
static debug(...args: unknown[]): void {
if (this.level >= LogLevel.DEBUG) {
this.recordLog('DEBUG', this.context, args);
this.d(this.getPrefix(), ...args);
}
}
@ -77,9 +124,38 @@ export class Log {
static info = Log.normal;
static warn = Log.error;
/** Get the current log history */
static getLogHistory(): LogEntry[] {
return [...this.logHistory];
}
/** Clear the log history */
static clearLogHistory(): void {
this.logHistory = [];
}
/** Export log history as JSON string for download */
static exportLogHistory(): string {
return JSON.stringify(this.logHistory, null, 2);
}
/** Export log history as formatted text for download */
static exportLogHistoryAsText(): string {
return this.logHistory
.map((entry) => {
const date = new Date(entry.timestamp).toISOString();
const context = entry.context ? `[${entry.context}] ` : '';
const argsStr =
entry.args.length > 0 ? ` ${entry.args.map(String).join(' ')}` : '';
return `${date} [${entry.level}] ${context}${entry.message}${argsStr}`;
})
.join('\n');
}
/**
* Create a new logger instance with a specific context that preserves line numbers
* This returns direct bound console functions with context prefix
* Create a new logger instance with a specific context
* All logs are recorded to history for later export
* @param context - The context string to prefix logs with
*/
static withContext(context: string): {
critical: (...args: unknown[]) => void;
@ -93,40 +169,60 @@ export class Log {
} {
const contextPrefix = `[${context}]`;
// Helper function to create conditional loggers that respect log levels
const createConditionalLogger = (
requiredLevel: LogLevel,
consoleFn: typeof console.log,
): ((...args: unknown[]) => void) => {
if (LOG_LEVEL >= requiredLevel) {
return consoleFn.bind(console, contextPrefix);
} else {
return () => {}; // No-op function when logging is disabled
}
};
// Return bound console functions that respect log levels and preserve line numbers
// Return wrapper functions that always record logs
return {
critical: createConditionalLogger(LogLevel.CRITICAL, console.log),
error: createConditionalLogger(LogLevel.ERROR, console.error || console.log),
normal: createConditionalLogger(LogLevel.NORMAL, console.log),
verbose: createConditionalLogger(LogLevel.VERBOSE, console.log),
debug: createConditionalLogger(LogLevel.DEBUG, console.debug || console.log),
critical: (...args: unknown[]) => {
this.recordLog('CRITICAL', context, args);
console.log(contextPrefix, ...args);
},
error: (...args: unknown[]) => {
if (LOG_LEVEL >= LogLevel.ERROR) {
this.recordLog('ERROR', context, args);
console.error(contextPrefix, ...args);
}
},
normal: (...args: unknown[]) => {
if (LOG_LEVEL >= LogLevel.NORMAL) {
this.recordLog('NORMAL', context, args);
console.log(contextPrefix, ...args);
}
},
verbose: (...args: unknown[]) => {
if (LOG_LEVEL >= LogLevel.VERBOSE) {
this.recordLog('VERBOSE', context, args);
console.log(contextPrefix, ...args);
}
},
debug: (...args: unknown[]) => {
if (LOG_LEVEL >= LogLevel.DEBUG) {
this.recordLog('DEBUG', context, args);
console.debug(contextPrefix, ...args);
}
},
// Aliases
log: createConditionalLogger(LogLevel.NORMAL, console.log),
info: createConditionalLogger(LogLevel.NORMAL, console.log),
warn: createConditionalLogger(LogLevel.ERROR, console.error || console.log),
log: (...args: unknown[]) => {
if (LOG_LEVEL >= LogLevel.NORMAL) {
this.recordLog('NORMAL', context, args);
console.log(contextPrefix, ...args);
}
},
info: (...args: unknown[]) => {
if (LOG_LEVEL >= LogLevel.NORMAL) {
this.recordLog('INFO', context, args);
console.info(contextPrefix, ...args);
}
},
warn: (...args: unknown[]) => {
if (LOG_LEVEL >= LogLevel.ERROR) {
this.recordLog('WARN', context, args);
console.warn(contextPrefix, ...args);
}
},
};
}
}
// Pre-configured logger instances for specific contexts
// All context loggers now record to history
export const SyncLog = Log.withContext('sync');
export const PluginLog = Log.withContext('plugin');
/**
* Convenience function for normal logging that preserves line numbers
* Usage: logFn("my normal log message", data)
* Logs without any context prefix
*/
export const logFn = LOG_LEVEL >= LogLevel.NORMAL ? console.log.bind(console) : () => {};