UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

340 lines 12.6 kB
/** * Logger Provider - Implements dependency injection pattern for logging * This file provides centralized logger management without circular dependencies */ import { createLogMethods } from './log-method-factory.js'; export class LoggerProvider { static instance = null; _logger = null; _config = null; _fallbackInitialized = false; _realDependenciesLoaded = false; // Lazy-loaded dependencies _createPinoLogger = null; _createConfig = null; constructor() { // Private constructor for singleton pattern } /** * Detect if running under Bun runtime * Bun's worker threads are incompatible with Pino's transport API */ static isBunRuntime() { return typeof globalThis.Bun !== 'undefined'; } /** * Get the singleton logger provider instance */ static getInstance() { if (!LoggerProvider.instance) { LoggerProvider.instance = new LoggerProvider(); } return LoggerProvider.instance; } /** * Initialize lazy-loaded dependencies to avoid circular imports */ ensureDependenciesLoaded() { if (this._fallbackInitialized) { return; } // For now, use fallback logger synchronously to avoid circular dependencies // The real Pino logger will be loaded asynchronously when needed this._createPinoLogger = () => this.createFallbackLogger(); this._createConfig = (config) => this.createFallbackConfig(config); this._fallbackInitialized = true; // Skip loading real Pino dependencies if running under Bun // Bun's worker threads are incompatible with Pino's transport API (pino/file) // which uses thread-stream and real-require packages if (LoggerProvider.isBunRuntime()) { if (process.env.NODE_ENV === 'development') { // Use console directly since this._config is not yet set, // so createFallbackLogger() would default to 'silent' level console.info('[LOGGER_PROVIDER] Bun runtime detected - using fallback logger (Pino transport incompatible)'); } return; } // Asynchronously load the real dependencies and replace the fallback this.loadRealDependencies().catch(error => { try { const fallbackLogger = this.createFallbackLogger(); fallbackLogger.error('[LOGGER_PROVIDER] Failed to load real dependencies', { error: this.formatErrorForLogging(error), fallback: true, source: 'logger-provider', timestamp: new Date().toISOString(), }); } catch (fallbackError) { // Absolute fallback to console if everything else fails console.error('[LOGGER_PROVIDER] Critical failure - fallback logger failed:', fallbackError, 'Original error:', error); } }); } /** * Asynchronously load real Pino dependencies * Uses dynamic imports to avoid circular dependency issues */ async loadRealDependencies() { // Skip if already loaded to prevent duplicate loading if (this._realDependenciesLoaded) { // Only log in development mode to avoid noise for end users if (process.env.NODE_ENV === 'development') { this.createFallbackLogger().debug('Real dependencies already loaded', { source: 'logger-provider', status: 'already-loaded', }); } return; } const startTime = Date.now(); // Only log in development mode to avoid noise for end users if (process.env.NODE_ENV === 'development') { this.createFallbackLogger().info('Loading real Pino dependencies', { source: 'logger-provider', method: 'dynamic-import', status: 'starting', }); } try { // Load dependencies dynamically to avoid circular imports // Using Promise.all for parallel loading to improve performance const [pinoLogger, configModule] = await Promise.all([ import('./pino-logger.js'), import('./config.js'), ]); // Verify imports were successful if (!pinoLogger?.createPinoLogger || !configModule?.createConfig) { throw new Error('Dynamic imports returned invalid modules'); } this._createPinoLogger = pinoLogger.createPinoLogger; this._createConfig = configModule.createConfig; this._realDependenciesLoaded = true; // Don't reinitialize existing logger to avoid breaking ongoing operations // The real Pino logger will be used for new logger instances created after this point // Only log in development mode if (process.env.NODE_ENV === 'development') { this.createFallbackLogger().info('Real dependencies loaded successfully', { source: 'logger-provider', status: 'success', duration: Date.now() - startTime, modules: ['pino-logger', 'config'], }); } } catch (error) { try { const fallbackLogger = this.createFallbackLogger(); fallbackLogger.error('[LOGGER_PROVIDER] Failed to load real dependencies', { error: this.formatErrorForLogging(error), fallback: true, source: 'logger-provider', status: 'load-failed', duration: Date.now() - startTime, }); } catch (fallbackError) { // Absolute fallback to console if everything else fails console.error('[LOGGER_PROVIDER] Critical failure - fallback logger failed:', fallbackError, 'Original error:', error); } // Keep the fallback logger } } /** * Get the default log level based on environment * Single source of truth for log level defaults (used before config.ts loads) * * IMPORTANT: The fallback logger outputs to console, so in production we use * 'silent' to avoid polluting the UI. Once the real pino logger loads * (which writes to files), it will use 'info' level from config.ts. */ getDefaultLogLevel() { const envLevel = process.env.NANOCODER_LOG_LEVEL; if (envLevel) return envLevel; const isTest = process.env.NODE_ENV === 'test'; const isDev = process.env.NODE_ENV === 'development'; if (isTest) return 'silent'; if (isDev) return 'debug'; // Production fallback logger: silent (outputs to console, not file) // The real pino logger will use 'info' once loaded return 'silent'; } /** * Create fallback config when config.ts hasn't loaded yet * Uses getDefaultLogLevel() for consistent defaults */ createFallbackConfig(override = {}) { const isDev = process.env.NODE_ENV === 'development'; const isTest = process.env.NODE_ENV === 'test'; return { level: this.getDefaultLogLevel(), pretty: isDev, redact: ['apiKey', 'token', 'password', 'secret'], correlation: !isTest, serialize: !isDev, ...override, }; } /** * Create fallback logger when dependencies fail to load */ createFallbackLogger() { // Use config level if available, otherwise compute default const configLevel = this._config?.level || this.getDefaultLogLevel(); const isSilent = configLevel === 'silent'; // If silent, return a no-op logger if (isSilent) { const noOp = () => { }; return { fatal: noOp, error: noOp, warn: noOp, info: noOp, http: noOp, debug: noOp, trace: noOp, child: (_bindings) => this.createFallbackLogger(), isLevelEnabled: (_level) => false, flush: async () => Promise.resolve(), flushSync: () => { }, end: async () => Promise.resolve(), }; } const fallbackConsole = console; // Use console as the logger // Create all log methods using the factory const logMethods = createLogMethods(fallbackConsole, { consolePrefix: '', transformArgs: (args, _level, _msg) => { // Note: Level prefix is handled by consolePrefix option in createLogMethod return args; }, }); return { ...logMethods, child: (_bindings) => this.createFallbackLogger(), isLevelEnabled: (_level) => true, flush: async () => Promise.resolve(), flushSync: () => { }, end: async () => Promise.resolve(), }; } /** * Create default configuration based on environment * Delegates to config.ts when loaded, otherwise uses createFallbackConfig */ createDefaultConfig(override = {}) { // Use the loaded createConfig from config.ts if available if (this._createConfig) { return this._createConfig(override); } // Fallback to local config creation (same logic as createFallbackConfig) return this.createFallbackConfig(override); } /** * Initialize the logger with configuration */ initializeLogger(config) { if (this._logger) { return this._logger; } this.ensureDependenciesLoaded(); this._config = this.createDefaultConfig(config); this._logger = this._createPinoLogger?.(this._config) ?? this.createFallbackLogger(); return this._logger; } /** * Get the current logger instance */ getLogger() { if (!this._logger) { // Auto-initialize with defaults if not already done return this.initializeLogger(); } return this._logger; } /** * Get the current configuration */ getLoggerConfig() { return this._config; } /** * Create a child logger with additional context */ createChildLogger(bindings) { const parent = this.getLogger(); return parent.child(bindings); } /** * Format error for structured logging */ formatErrorForLogging(error) { if (error instanceof Error) { return { message: error.message, stack: error.stack, name: error.name, cause: error.cause, }; } return { value: error }; } /** * Check if a log level is enabled */ isLevelEnabled(level) { const logger = this.getLogger(); return logger.isLevelEnabled(level); } /** * Reset the logger instance (useful for testing) */ reset() { this._logger = null; this._config = null; this._fallbackInitialized = false; this._realDependenciesLoaded = false; this._createPinoLogger = null; this._createConfig = null; } /** * Flush any pending logs */ async flush() { if (this._logger) { try { await this._logger.flush(); } catch (_error) { // Ignore flush errors as they're usually due to logger being closed // This can happen in test environments or during shutdown } } } /** * Flush logs synchronously (for signal handlers) */ flushSync() { if (this._logger) { this._logger.flushSync(); } } /** * End the logger and close all streams */ async end() { if (this._logger) { await this._logger.end(); this._logger = null; this._config = null; // Don't reset dependency flags - they can be reused for new loggers } } } // Export the singleton instance for easy access export const loggerProvider = LoggerProvider.getInstance(); //# sourceMappingURL=logger-provider.js.map