08ea5fbe98
添加用户管理视图、API和状态管理文件
165 lines
4.6 KiB
TypeScript
165 lines
4.6 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
export enum LogLevel {
|
|
DEBUG = 'debug',
|
|
INFO = 'info',
|
|
WARN = 'warn',
|
|
ERROR = 'error'
|
|
}
|
|
|
|
interface LogEntry {
|
|
timestamp: string;
|
|
level: LogLevel;
|
|
message: string;
|
|
context?: Record<string, unknown>;
|
|
}
|
|
|
|
export class Logger {
|
|
private static instance: Logger;
|
|
private logDir: string;
|
|
private logFile: string;
|
|
private logLevel: LogLevel;
|
|
private context: Record<string, unknown>;
|
|
|
|
private constructor(logDir: string = 'test-results/logs') {
|
|
this.logDir = logDir;
|
|
this.logFile = path.join(logDir, `test-run-${new Date().toISOString().replace(/[:.]/g, '-')}.log`);
|
|
this.logLevel = this.parseLogLevel(process.env.LOG_LEVEL || 'info');
|
|
this.context = {};
|
|
this.ensureLogDir();
|
|
}
|
|
|
|
static getInstance(logDir?: string): Logger {
|
|
if (!Logger.instance) {
|
|
Logger.instance = new Logger(logDir);
|
|
}
|
|
return Logger.instance;
|
|
}
|
|
|
|
private parseLogLevel(level: string): LogLevel {
|
|
switch (level.toLowerCase()) {
|
|
case 'debug': return LogLevel.DEBUG;
|
|
case 'info': return LogLevel.INFO;
|
|
case 'warn': return LogLevel.WARN;
|
|
case 'error': return LogLevel.ERROR;
|
|
default: return LogLevel.INFO;
|
|
}
|
|
}
|
|
|
|
private ensureLogDir(): void {
|
|
if (!fs.existsSync(this.logDir)) {
|
|
fs.mkdirSync(this.logDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
setContext(context: Record<string, unknown>): this {
|
|
this.context = { ...this.context, ...context };
|
|
return this;
|
|
}
|
|
|
|
clearContext(): this {
|
|
this.context = {};
|
|
return this;
|
|
}
|
|
|
|
private formatTimestamp(): string {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
private formatMessage(level: LogLevel, message: string): string {
|
|
const timestamp = this.formatTimestamp();
|
|
const levelStr = level.toUpperCase().padEnd(5);
|
|
const contextStr = Object.keys(this.context).length > 0
|
|
? ` [${JSON.stringify(this.context)}]`
|
|
: '';
|
|
return `${timestamp} | ${levelStr} |${contextStr} ${message}`;
|
|
}
|
|
|
|
private shouldLog(level: LogLevel): boolean {
|
|
const levels = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR];
|
|
return levels.indexOf(level) >= levels.indexOf(this.logLevel);
|
|
}
|
|
|
|
private writeToFile(entry: LogEntry): void {
|
|
try {
|
|
this.ensureLogDir();
|
|
const logLine = JSON.stringify(entry) + '\n';
|
|
fs.appendFileSync(this.logFile, logLine, 'utf-8');
|
|
} catch {
|
|
// Ignore file write errors
|
|
}
|
|
}
|
|
|
|
private log(level: LogLevel, message: string, context?: Record<string, unknown>): void {
|
|
if (!this.shouldLog(level)) return;
|
|
|
|
const entry: LogEntry = {
|
|
timestamp: this.formatTimestamp(),
|
|
level,
|
|
message,
|
|
context: { ...this.context, ...context }
|
|
};
|
|
|
|
this.writeToFile(entry);
|
|
|
|
const formattedMessage = this.formatMessage(level, message);
|
|
const colors: Record<LogLevel, string> = {
|
|
[LogLevel.DEBUG]: '\x1b[36m',
|
|
[LogLevel.INFO]: '\x1b[32m',
|
|
[LogLevel.WARN]: '\x1b[33m',
|
|
[LogLevel.ERROR]: '\x1b[31m'
|
|
};
|
|
const reset = '\x1b[0m';
|
|
console.log(`${colors[level]}${formattedMessage}${reset}`);
|
|
}
|
|
|
|
debug(message: string, context?: Record<string, unknown>): void {
|
|
this.log(LogLevel.DEBUG, message, context);
|
|
}
|
|
|
|
info(message: string, context?: Record<string, unknown>): void {
|
|
this.log(LogLevel.INFO, message, context);
|
|
}
|
|
|
|
warn(message: string, context?: Record<string, unknown>): void {
|
|
this.log(LogLevel.WARN, message, context);
|
|
}
|
|
|
|
error(message: string, error?: Error | unknown, context?: Record<string, unknown>): void {
|
|
const errorContext = error instanceof Error
|
|
? { errorMessage: error.message, stack: error.stack, ...context }
|
|
: { error, ...context };
|
|
this.log(LogLevel.ERROR, message, errorContext);
|
|
}
|
|
|
|
section(title: string): void {
|
|
const separator = '='.repeat(60);
|
|
console.log(`\n${separator}`);
|
|
console.log(` ${title}`);
|
|
console.log(`${separator}\n`);
|
|
this.info(`=== ${title} ===`);
|
|
}
|
|
|
|
progress(current: number, total: number, message: string): void {
|
|
const percentage = Math.round((current / total) * 100);
|
|
const bar = this.createProgressBar(percentage);
|
|
process.stdout.write(`\r${bar} ${percentage}% - ${message}`);
|
|
if (current === total) {
|
|
process.stdout.write('\n');
|
|
}
|
|
}
|
|
|
|
private createProgressBar(percentage: number, width: number = 30): string {
|
|
const filled = Math.round((percentage / 100) * width);
|
|
const empty = width - filled;
|
|
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
|
}
|
|
|
|
getLogFile(): string {
|
|
return this.logFile;
|
|
}
|
|
}
|
|
|
|
export const logger = Logger.getInstance();
|