Files
gridpilot.gg/packages/automation/infrastructure/adapters/logging/PinoLogAdapter.ts
2025-12-14 18:11:59 +01:00

119 lines
3.7 KiB
TypeScript

import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext';
import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel';
import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig';
import type { ILogger } from '@gridpilot/shared/logging/ILogger';
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 LoggerPort, 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): LoggerPort {
return new PinoLogAdapter(this.config, { ...this.baseContext, ...context });
}
async flush(): Promise<void> {
// Console output is synchronous, nothing to flush
}
}