mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
feat: Add unified logger utility with file and console output
- Create logger factory with explicit module names (no stack traces) - Support log/warn/error/debug levels with clean method names - Write to ~/.vibetunnel/log.txt with automatic cleanup on startup - Add colored console output with timestamps and module names - Support debug mode via flag or environment variable 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1c007a2181
commit
3a3b5e44e9
1 changed files with 180 additions and 0 deletions
180
web/src/server/utils/logger.ts
Normal file
180
web/src/server/utils/logger.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
// Log file path
|
||||||
|
const LOG_DIR = path.join(os.homedir(), '.vibetunnel');
|
||||||
|
const LOG_FILE = path.join(LOG_DIR, 'log.txt');
|
||||||
|
|
||||||
|
// Debug mode flag
|
||||||
|
let debugMode = false;
|
||||||
|
|
||||||
|
// File handle for log file
|
||||||
|
let logFileHandle: fs.WriteStream | null = null;
|
||||||
|
|
||||||
|
// ANSI color codes for stripping from file output
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the logger - creates log directory and file
|
||||||
|
*/
|
||||||
|
export function initLogger(debug: boolean = false): void {
|
||||||
|
debugMode = debug;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure log directory exists
|
||||||
|
if (!fs.existsSync(LOG_DIR)) {
|
||||||
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old log file if it exists
|
||||||
|
if (fs.existsSync(LOG_FILE)) {
|
||||||
|
fs.unlinkSync(LOG_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new log file write stream
|
||||||
|
logFileHandle = fs.createWriteStream(LOG_FILE, { flags: 'a' });
|
||||||
|
} catch (error) {
|
||||||
|
// Don't throw, just log to console
|
||||||
|
console.error('Failed to initialize log file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the logger
|
||||||
|
*/
|
||||||
|
export function closeLogger(): void {
|
||||||
|
if (logFileHandle) {
|
||||||
|
logFileHandle.end();
|
||||||
|
logFileHandle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format log message with timestamp
|
||||||
|
*/
|
||||||
|
function formatMessage(
|
||||||
|
level: string,
|
||||||
|
module: string,
|
||||||
|
args: any[]
|
||||||
|
): { console: string; file: string } {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
// Format arguments
|
||||||
|
const message = args
|
||||||
|
.map((arg) => {
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
try {
|
||||||
|
// Use JSON.stringify with 2-space indent for objects
|
||||||
|
return JSON.stringify(arg, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(arg);
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// Console format with colors
|
||||||
|
let consoleFormat: string;
|
||||||
|
const moduleColor = chalk.cyan(`[${module}]`);
|
||||||
|
const timestampColor = chalk.gray(timestamp);
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'ERROR':
|
||||||
|
consoleFormat = `${timestampColor} ${chalk.red(level)} ${moduleColor} ${chalk.red(message)}`;
|
||||||
|
break;
|
||||||
|
case 'WARN':
|
||||||
|
consoleFormat = `${timestampColor} ${chalk.yellow(level)} ${moduleColor} ${chalk.yellow(message)}`;
|
||||||
|
break;
|
||||||
|
case 'DEBUG':
|
||||||
|
consoleFormat = `${timestampColor} ${chalk.magenta(level)} ${moduleColor} ${chalk.gray(message)}`;
|
||||||
|
break;
|
||||||
|
default: // LOG
|
||||||
|
consoleFormat = `${timestampColor} ${chalk.green(level)} ${moduleColor} ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File format (no colors)
|
||||||
|
const fileFormat = `${timestamp} ${level.padEnd(5)} [${module}] ${message}`;
|
||||||
|
|
||||||
|
return { console: consoleFormat, file: fileFormat };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write to log file
|
||||||
|
*/
|
||||||
|
function writeToFile(message: string): void {
|
||||||
|
if (logFileHandle) {
|
||||||
|
try {
|
||||||
|
// Strip ANSI color codes from message
|
||||||
|
const cleanMessage = message.replace(ANSI_PATTERN, '');
|
||||||
|
logFileHandle.write(cleanMessage + '\n');
|
||||||
|
} catch {
|
||||||
|
// Silently ignore file write errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable debug mode
|
||||||
|
*/
|
||||||
|
export function setDebugMode(enabled: boolean): void {
|
||||||
|
debugMode = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log from a specific module (used by client-side API)
|
||||||
|
*/
|
||||||
|
export function logFromModule(level: string, module: string, args: any[]): void {
|
||||||
|
if (level === 'DEBUG' && !debugMode) return;
|
||||||
|
|
||||||
|
const { console: consoleMsg, file: fileMsg } = formatMessage(level, module, args);
|
||||||
|
|
||||||
|
// Log to console
|
||||||
|
switch (level) {
|
||||||
|
case 'ERROR':
|
||||||
|
console.error(consoleMsg);
|
||||||
|
break;
|
||||||
|
case 'WARN':
|
||||||
|
console.warn(consoleMsg);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log(consoleMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log to file
|
||||||
|
writeToFile(fileMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a logger for a specific module
|
||||||
|
* This is the main factory function that should be used
|
||||||
|
*/
|
||||||
|
export function createLogger(moduleName: string) {
|
||||||
|
return {
|
||||||
|
log: (...args: any[]) => {
|
||||||
|
const { console: consoleMsg, file: fileMsg } = formatMessage('LOG', moduleName, args);
|
||||||
|
console.log(consoleMsg);
|
||||||
|
writeToFile(fileMsg);
|
||||||
|
},
|
||||||
|
warn: (...args: any[]) => {
|
||||||
|
const { console: consoleMsg, file: fileMsg } = formatMessage('WARN', moduleName, args);
|
||||||
|
console.warn(consoleMsg);
|
||||||
|
writeToFile(fileMsg);
|
||||||
|
},
|
||||||
|
error: (...args: any[]) => {
|
||||||
|
const { console: consoleMsg, file: fileMsg } = formatMessage('ERROR', moduleName, args);
|
||||||
|
console.error(consoleMsg);
|
||||||
|
writeToFile(fileMsg);
|
||||||
|
},
|
||||||
|
debug: (...args: any[]) => {
|
||||||
|
if (debugMode) {
|
||||||
|
const { console: consoleMsg, file: fileMsg } = formatMessage('DEBUG', moduleName, args);
|
||||||
|
console.log(consoleMsg);
|
||||||
|
writeToFile(fileMsg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue