feat(logging): add professional logging with pino and headless E2E tests - Add ILogger port interface in application layer - Implement PinoLogAdapter with Electron-compatible structured logging - Add NoOpLogAdapter for testing - Wire logging into DI container and all adapters - Create 32 E2E tests for automation workflow (headless-only) - Add vitest.e2e.config.ts for E2E test configuration - All tests enforce HEADLESS mode (no headed browser allowed)
This commit is contained in:
19
packages/infrastructure/adapters/logging/NoOpLogAdapter.ts
Normal file
19
packages/infrastructure/adapters/logging/NoOpLogAdapter.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ILogger, LogContext } from '../../../application/ports/ILogger';
|
||||
|
||||
export class NoOpLogAdapter implements ILogger {
|
||||
debug(_message: string, _context?: LogContext): void {}
|
||||
|
||||
info(_message: string, _context?: LogContext): void {}
|
||||
|
||||
warn(_message: string, _context?: LogContext): void {}
|
||||
|
||||
error(_message: string, _error?: Error, _context?: LogContext): void {}
|
||||
|
||||
fatal(_message: string, _error?: Error, _context?: LogContext): void {}
|
||||
|
||||
child(_context: LogContext): ILogger {
|
||||
return this;
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {}
|
||||
}
|
||||
116
packages/infrastructure/adapters/logging/PinoLogAdapter.ts
Normal file
116
packages/infrastructure/adapters/logging/PinoLogAdapter.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { ILogger, LogContext, LogLevel } from '../../../application/ports/ILogger';
|
||||
import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig';
|
||||
|
||||
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
debug: 10,
|
||||
info: 20,
|
||||
warn: 30,
|
||||
error: 40,
|
||||
fatal: 50,
|
||||
};
|
||||
|
||||
/**
|
||||
* PinoLogAdapter - Electron-compatible logger implementation.
|
||||
*
|
||||
* Note: We use a custom console-based implementation instead of pino
|
||||
* because pino's internal use of diagnostics_channel.tracingChannel
|
||||
* is not compatible with Electron's Node.js version.
|
||||
*
|
||||
* This provides structured JSON logging to stdout with the same interface.
|
||||
*/
|
||||
export class PinoLogAdapter implements ILogger {
|
||||
private readonly config: LoggingEnvironmentConfig;
|
||||
private readonly baseContext: LogContext;
|
||||
private readonly levelPriority: number;
|
||||
|
||||
constructor(config?: LoggingEnvironmentConfig, baseContext?: LogContext) {
|
||||
this.config = config || loadLoggingConfig();
|
||||
this.baseContext = {
|
||||
app: 'gridpilot-companion',
|
||||
version: process.env.npm_package_version || '0.0.0',
|
||||
processType: process.type || 'main',
|
||||
...baseContext,
|
||||
};
|
||||
this.levelPriority = LOG_LEVEL_PRIORITY[this.config.level];
|
||||
}
|
||||
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
return LOG_LEVEL_PRIORITY[level] >= this.levelPriority;
|
||||
}
|
||||
|
||||
private formatLog(level: LogLevel, message: string, context?: LogContext, error?: Error): string {
|
||||
const entry: Record<string, unknown> = {
|
||||
level,
|
||||
time: new Date().toISOString(),
|
||||
...this.baseContext,
|
||||
...context,
|
||||
msg: message,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
entry.err = {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.stringify(entry);
|
||||
}
|
||||
|
||||
private log(level: LogLevel, message: string, context?: LogContext, error?: Error): void {
|
||||
if (!this.shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = this.formatLog(level, message, context, error);
|
||||
|
||||
if (this.config.prettyPrint) {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const levelColors: Record<LogLevel, string> = {
|
||||
debug: '\x1b[36m', // cyan
|
||||
info: '\x1b[32m', // green
|
||||
warn: '\x1b[33m', // yellow
|
||||
error: '\x1b[31m', // red
|
||||
fatal: '\x1b[35m', // magenta
|
||||
};
|
||||
const reset = '\x1b[0m';
|
||||
const color = levelColors[level];
|
||||
|
||||
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
|
||||
const errorStr = error ? `\n ${error.stack || error.message}` : '';
|
||||
|
||||
console.log(`${color}[${timestamp}] ${level.toUpperCase()}${reset}: ${message}${contextStr}${errorStr}`);
|
||||
} else {
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: LogContext): void {
|
||||
this.log('debug', message, context);
|
||||
}
|
||||
|
||||
info(message: string, context?: LogContext): void {
|
||||
this.log('info', message, context);
|
||||
}
|
||||
|
||||
warn(message: string, context?: LogContext): void {
|
||||
this.log('warn', message, context);
|
||||
}
|
||||
|
||||
error(message: string, error?: Error, context?: LogContext): void {
|
||||
this.log('error', message, context, error);
|
||||
}
|
||||
|
||||
fatal(message: string, error?: Error, context?: LogContext): void {
|
||||
this.log('fatal', message, context, error);
|
||||
}
|
||||
|
||||
child(context: LogContext): ILogger {
|
||||
return new PinoLogAdapter(this.config, { ...this.baseContext, ...context });
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
// Console output is synchronous, nothing to flush
|
||||
}
|
||||
}
|
||||
2
packages/infrastructure/adapters/logging/index.ts
Normal file
2
packages/infrastructure/adapters/logging/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { PinoLogAdapter } from './PinoLogAdapter';
|
||||
export { NoOpLogAdapter } from './NoOpLogAdapter';
|
||||
Reference in New Issue
Block a user