brainy/src/utils/logger.ts
David Snelling 27fb3ef905 feat: add complete silent mode for TUI applications
- Add silent option to BrainyConfig to suppress ALL console output
- Override console methods (log, info, warn, error) when silent=true
- Add LogLevel.SILENT to Logger for proper silent mode support
- Propagate silent mode to all augmentations automatically
- Update augmentation configs to support silent property
- Restore console methods properly on brain.close()
- Perfect for terminal UI applications that need clean output

Usage:
```javascript
const brain = new Brainy({
  storage: { type: 'filesystem', path: './data' },
  silent: true  // Complete silence - zero console output
})
```

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-16 13:18:49 -07:00

274 lines
No EOL
7.9 KiB
TypeScript

/**
* Centralized logging utility for Brainy
* Provides configurable log levels and consistent logging across the codebase
* Automatically reduces logging in production environments to minimize costs
*/
import { isProductionEnvironment, getLogLevel } from './environment.js'
export enum LogLevel {
SILENT = -1, // New: Completely silent mode
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
TRACE = 4
}
export interface LoggerConfig {
level: LogLevel
// Specific module log levels
modules?: {
[moduleName: string]: LogLevel
}
// Whether to include timestamps
timestamps?: boolean
// Whether to include module names
includeModule?: boolean
// Custom log handler
handler?: (level: LogLevel, module: string, message: string, ...args: any[]) => void
}
class Logger {
private static instance: Logger
private config: LoggerConfig = {
level: LogLevel.ERROR, // Default to ERROR in production for cost optimization
timestamps: false, // Disable timestamps in production to reduce log size
includeModule: true
}
private constructor() {
// Auto-detect production environment and set appropriate defaults
this.applyEnvironmentDefaults()
// Set log level from environment variable if available (overrides auto-detection)
const envLogLevel = process.env.BRAINY_LOG_LEVEL
if (envLogLevel) {
const level = LogLevel[envLogLevel.toUpperCase() as keyof typeof LogLevel]
if (level !== undefined) {
this.config.level = level
}
}
// Parse module-specific log levels
const moduleLogLevels = process.env.BRAINY_MODULE_LOG_LEVELS
if (moduleLogLevels) {
try {
this.config.modules = JSON.parse(moduleLogLevels)
} catch (e) {
// Ignore parsing errors
}
}
}
private applyEnvironmentDefaults(): void {
const envLogLevel = getLogLevel()
// Convert environment log level to Logger LogLevel
switch (envLogLevel) {
case 'silent':
this.config.level = LogLevel.SILENT
break
case 'error':
this.config.level = LogLevel.ERROR
this.config.timestamps = false // Minimize log size in production
break
case 'warn':
this.config.level = LogLevel.WARN
this.config.timestamps = false
break
case 'info':
this.config.level = LogLevel.INFO
this.config.timestamps = true
break
case 'verbose':
this.config.level = LogLevel.DEBUG
this.config.timestamps = true
break
}
// In production environments, be extra conservative to minimize costs
if (isProductionEnvironment()) {
this.config.level = Math.min(this.config.level, LogLevel.ERROR)
this.config.timestamps = false
this.config.includeModule = false // Reduce log size
}
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
configure(config: Partial<LoggerConfig>): void {
this.config = { ...this.config, ...config }
}
private shouldLog(level: LogLevel, module: string): boolean {
// Silent mode - never log anything
if (this.config.level === LogLevel.SILENT) {
return false
}
// Check module-specific level first
if (this.config.modules && this.config.modules[module] !== undefined) {
return level <= this.config.modules[module]
}
// Otherwise use global level
return level <= this.config.level
}
private formatMessage(level: LogLevel, module: string, message: string): string {
const parts: string[] = []
if (this.config.timestamps) {
parts.push(`[${new Date().toISOString()}]`)
}
parts.push(`[${LogLevel[level]}]`)
if (this.config.includeModule) {
parts.push(`[${module}]`)
}
parts.push(message)
return parts.join(' ')
}
private log(level: LogLevel, module: string, message: string, ...args: any[]): void {
if (!this.shouldLog(level, module)) {
return
}
if (this.config.handler) {
this.config.handler(level, module, message, ...args)
return
}
const formattedMessage = this.formatMessage(level, module, message)
switch (level) {
case LogLevel.ERROR:
console.error(formattedMessage, ...args)
break
case LogLevel.WARN:
console.warn(formattedMessage, ...args)
break
case LogLevel.INFO:
console.info(formattedMessage, ...args)
break
case LogLevel.DEBUG:
case LogLevel.TRACE:
console.log(formattedMessage, ...args)
break
}
}
error(module: string, message: string, ...args: any[]): void {
this.log(LogLevel.ERROR, module, message, ...args)
}
warn(module: string, message: string, ...args: any[]): void {
this.log(LogLevel.WARN, module, message, ...args)
}
info(module: string, message: string, ...args: any[]): void {
this.log(LogLevel.INFO, module, message, ...args)
}
debug(module: string, message: string, ...args: any[]): void {
this.log(LogLevel.DEBUG, module, message, ...args)
}
trace(module: string, message: string, ...args: any[]): void {
this.log(LogLevel.TRACE, module, message, ...args)
}
// Create a module-specific logger
createModuleLogger(module: string) {
return {
error: (message: string, ...args: any[]) => this.error(module, message, ...args),
warn: (message: string, ...args: any[]) => this.warn(module, message, ...args),
info: (message: string, ...args: any[]) => this.info(module, message, ...args),
debug: (message: string, ...args: any[]) => this.debug(module, message, ...args),
trace: (message: string, ...args: any[]) => this.trace(module, message, ...args)
}
}
}
// Export singleton instance
export const logger = Logger.getInstance()
// Export convenience function for creating module loggers
export function createModuleLogger(module: string) {
return logger.createModuleLogger(module)
}
// Export function to configure logger
export function configureLogger(config: Partial<LoggerConfig>) {
logger.configure(config)
}
/**
* Smart console replacement that automatically reduces logging in production
* Dramatically reduces Google Cloud Run logging costs
*
* Usage: Replace console.log with smartConsole.log, etc.
*/
export const smartConsole = {
log: (message?: any, ...args: any[]) => {
if (logger['shouldLog'](LogLevel.INFO, 'console')) {
console.log(message, ...args)
}
},
info: (message?: any, ...args: any[]) => {
if (logger['shouldLog'](LogLevel.INFO, 'console')) {
console.info(message, ...args)
}
},
warn: (message?: any, ...args: any[]) => {
if (logger['shouldLog'](LogLevel.WARN, 'console')) {
console.warn(message, ...args)
}
},
error: (message?: any, ...args: any[]) => {
if (logger['shouldLog'](LogLevel.ERROR, 'console')) {
console.error(message, ...args)
}
},
debug: (message?: any, ...args: any[]) => {
if (logger['shouldLog'](LogLevel.DEBUG, 'console')) {
console.debug(message, ...args)
}
},
trace: (message?: any, ...args: any[]) => {
if (logger['shouldLog'](LogLevel.TRACE, 'console')) {
console.trace(message, ...args)
}
}
}
/**
* Production-optimized logging functions
* These only log in non-production environments or when explicitly enabled
*/
export const prodLog = {
// Only log errors in production (always visible)
error: (message?: any, ...args: any[]) => {
console.error(message, ...args)
},
// These are suppressed in production unless BRAINY_LOG_LEVEL is set
warn: (message?: any, ...args: any[]) => smartConsole.warn(message, ...args),
info: (message?: any, ...args: any[]) => smartConsole.info(message, ...args),
debug: (message?: any, ...args: any[]) => smartConsole.debug(message, ...args),
log: (message?: any, ...args: any[]) => smartConsole.log(message, ...args)
}