brainy/src/utils/structuredLogger.ts
David Snelling c303ead318 feat: add browser environment compatibility support
- Remove top-level Node.js imports that break bundlers
- Use universal adapters for crypto operations
- Add dynamic imports for Node.js-specific modules
- Add browser field to package.json for bundler hints
- Maintain full Node.js functionality while enabling browser usage

This allows Brainy to be used with modern bundlers (Vite, Webpack, etc.)
without requiring Node.js polyfills. Browser environments get core features
while Node.js retains all capabilities including filesystem and networking.
2025-09-17 15:48:02 -07:00

550 lines
No EOL
15 KiB
TypeScript

/**
* Enhanced Structured Logging System for Brainy
* Provides production-ready logging with structured output, context preservation,
* performance tracking, and multiple transport support
*/
import { performance } from 'perf_hooks'
import { randomUUID } from '../universal/crypto.js'
export enum LogLevel {
SILENT = -1,
FATAL = 0,
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4,
TRACE = 5
}
export interface LogContext {
requestId?: string
userId?: string
operation?: string
entityId?: string
entityType?: string
[key: string]: any
}
export interface LogEntry {
timestamp: string
level: string
levelNumeric: number
module: string
message: string
context?: LogContext
data?: any
error?: {
name: string
message: string
stack?: string
code?: string
}
performance?: {
duration?: number
memory?: {
used: number
total: number
}
}
host?: string
pid: number
version?: string
}
export interface LogTransport {
name: string
log(entry: LogEntry): void | Promise<void>
flush?(): Promise<void>
}
export interface StructuredLoggerConfig {
level: LogLevel
modules?: Record<string, LogLevel>
format: 'json' | 'pretty' | 'simple'
transports: LogTransport[]
context?: LogContext
includeHost?: boolean
includeMemory?: boolean
bufferSize?: number
flushInterval?: number
version?: string
}
class ConsoleTransport implements LogTransport {
name = 'console'
private format: 'json' | 'pretty' | 'simple'
constructor(format: 'json' | 'pretty' | 'simple' = 'json') {
this.format = format
}
log(entry: LogEntry): void {
const method = this.getConsoleMethod(entry.levelNumeric)
if (this.format === 'json') {
method(JSON.stringify(entry))
} else if (this.format === 'pretty') {
const color = this.getColor(entry.levelNumeric)
const prefix = `${entry.timestamp} ${color}[${entry.level}]\\x1b[0m [${entry.module}]`
const message = entry.message
if (entry.error) {
method(`${prefix} ${message}`, entry.error)
} else if (entry.data) {
method(`${prefix} ${message}`, entry.data)
} else {
method(`${prefix} ${message}`)
}
} else {
// Simple format
method(`[${entry.level}] ${entry.message}`)
}
}
private getConsoleMethod(level: number): (...args: any[]) => void {
switch (level) {
case LogLevel.FATAL:
case LogLevel.ERROR:
return console.error
case LogLevel.WARN:
return console.warn
case LogLevel.INFO:
return console.info
default:
return console.log
}
}
private getColor(level: number): string {
switch (level) {
case LogLevel.FATAL:
return '\\x1b[35m' // Magenta
case LogLevel.ERROR:
return '\\x1b[31m' // Red
case LogLevel.WARN:
return '\\x1b[33m' // Yellow
case LogLevel.INFO:
return '\\x1b[36m' // Cyan
case LogLevel.DEBUG:
return '\\x1b[32m' // Green
case LogLevel.TRACE:
return '\\x1b[90m' // Gray
default:
return '\\x1b[0m' // Reset
}
}
}
class BufferedTransport implements LogTransport {
name = 'buffered'
private buffer: LogEntry[] = []
private innerTransport: LogTransport
private bufferSize: number
private flushTimer?: NodeJS.Timeout
constructor(innerTransport: LogTransport, bufferSize: number = 100, flushInterval: number = 5000) {
this.innerTransport = innerTransport
this.bufferSize = bufferSize
if (flushInterval > 0) {
this.flushTimer = setInterval(() => this.flush(), flushInterval)
}
}
log(entry: LogEntry): void {
this.buffer.push(entry)
if (this.buffer.length >= this.bufferSize) {
this.flush()
}
}
async flush(): Promise<void> {
const entries = this.buffer.splice(0)
for (const entry of entries) {
await this.innerTransport.log(entry)
}
if (this.innerTransport.flush) {
await this.innerTransport.flush()
}
}
destroy(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer)
}
this.flush()
}
}
export class StructuredLogger {
private static instance: StructuredLogger
private config: StructuredLoggerConfig
private defaultContext: LogContext = {}
private performanceMarks = new Map<string, number>()
private constructor() {
const isDevelopment = process.env.NODE_ENV !== 'production'
const format = isDevelopment ? 'pretty' : 'json'
this.config = {
level: isDevelopment ? LogLevel.DEBUG : LogLevel.INFO,
format,
transports: [new ConsoleTransport(format)],
includeHost: !isDevelopment,
includeMemory: false,
bufferSize: 100,
flushInterval: 5000,
version: process.env.npm_package_version
}
// Load from environment
this.loadEnvironmentConfig()
}
private loadEnvironmentConfig(): void {
const envLevel = process.env.BRAINY_LOG_LEVEL
if (envLevel) {
const level = LogLevel[envLevel.toUpperCase() as keyof typeof LogLevel]
if (level !== undefined) {
this.config.level = level
}
}
const envFormat = process.env.BRAINY_LOG_FORMAT
if (envFormat && ['json', 'pretty', 'simple'].includes(envFormat)) {
this.config.format = envFormat as 'json' | 'pretty' | 'simple'
}
const moduleConfig = process.env.BRAINY_MODULE_LOG_LEVELS
if (moduleConfig) {
try {
this.config.modules = JSON.parse(moduleConfig)
} catch {
// Ignore parse errors
}
}
}
static getInstance(): StructuredLogger {
if (!StructuredLogger.instance) {
StructuredLogger.instance = new StructuredLogger()
}
return StructuredLogger.instance
}
configure(config: Partial<StructuredLoggerConfig>): void {
this.config = { ...this.config, ...config }
}
setContext(context: LogContext): void {
this.defaultContext = { ...this.defaultContext, ...context }
}
clearContext(): void {
this.defaultContext = {}
}
withContext(context: LogContext): StructuredLogger {
const contextualLogger = Object.create(this)
contextualLogger.defaultContext = { ...this.defaultContext, ...context }
return contextualLogger
}
startTimer(label: string): void {
this.performanceMarks.set(label, performance.now())
}
endTimer(label: string): number | undefined {
const start = this.performanceMarks.get(label)
if (start === undefined) return undefined
const duration = performance.now() - start
this.performanceMarks.delete(label)
return duration
}
private shouldLog(level: LogLevel, module: string): boolean {
if (this.config.modules?.[module] !== undefined) {
return level <= this.config.modules[module]
}
return level <= this.config.level
}
private createLogEntry(
level: LogLevel,
module: string,
message: string,
context?: LogContext,
data?: any,
error?: Error
): LogEntry {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level: LogLevel[level],
levelNumeric: level,
module,
message,
pid: process.pid,
version: this.config.version
}
// Merge contexts
const mergedContext = { ...this.defaultContext, ...context }
if (Object.keys(mergedContext).length > 0) {
entry.context = mergedContext
}
if (data !== undefined) {
entry.data = data
}
if (error) {
entry.error = {
name: error.name,
message: error.message,
stack: error.stack,
code: (error as any).code
}
}
if (this.config.includeHost) {
// Use dynamic import for hostname (Node.js only)
if (typeof window === 'undefined') {
try {
const os = require('node:os')
entry.host = os.hostname()
} catch {
entry.host = 'unknown'
}
} else {
entry.host = window.location?.hostname || 'browser'
}
}
if (this.config.includeMemory && typeof process !== 'undefined' && process.memoryUsage) {
const mem = process.memoryUsage()
entry.performance = {
memory: {
used: Math.round(mem.heapUsed / 1024 / 1024),
total: Math.round(mem.heapTotal / 1024 / 1024)
}
}
}
return entry
}
private log(
level: LogLevel,
module: string,
message: string,
contextOrData?: LogContext | any,
data?: any
): void {
if (!this.shouldLog(level, module)) {
return
}
// Handle overloaded parameters
let context: LogContext | undefined
let logData: any
if (contextOrData && typeof contextOrData === 'object' && !Array.isArray(contextOrData)) {
// Check if it looks like a context object
const hasContextKeys = ['requestId', 'userId', 'operation', 'entityId', 'entityType']
.some(key => key in contextOrData)
if (hasContextKeys) {
context = contextOrData
logData = data
} else {
logData = contextOrData
}
} else {
logData = contextOrData
}
// Extract error if present
let error: Error | undefined
if (logData instanceof Error) {
error = logData
logData = undefined
} else if (logData?.error instanceof Error) {
error = logData.error
delete logData.error
}
const entry = this.createLogEntry(level, module, message, context, logData, error)
// Send to all transports
for (const transport of this.config.transports) {
try {
transport.log(entry)
} catch (err) {
// Fallback to console.error if transport fails
console.error('Logger transport error:', err)
}
}
}
fatal(module: string, message: string, contextOrData?: LogContext | any, data?: any): void {
this.log(LogLevel.FATAL, module, message, contextOrData, data)
}
error(module: string, message: string, contextOrData?: LogContext | any, data?: any): void {
this.log(LogLevel.ERROR, module, message, contextOrData, data)
}
warn(module: string, message: string, contextOrData?: LogContext | any, data?: any): void {
this.log(LogLevel.WARN, module, message, contextOrData, data)
}
info(module: string, message: string, contextOrData?: LogContext | any, data?: any): void {
this.log(LogLevel.INFO, module, message, contextOrData, data)
}
debug(module: string, message: string, contextOrData?: LogContext | any, data?: any): void {
this.log(LogLevel.DEBUG, module, message, contextOrData, data)
}
trace(module: string, message: string, contextOrData?: LogContext | any, data?: any): void {
this.log(LogLevel.TRACE, module, message, contextOrData, data)
}
createModuleLogger(module: string) {
const self = this
return {
fatal: (message: string, contextOrData?: LogContext | any, data?: any) =>
self.fatal(module, message, contextOrData, data),
error: (message: string, contextOrData?: LogContext | any, data?: any) =>
self.error(module, message, contextOrData, data),
warn: (message: string, contextOrData?: LogContext | any, data?: any) =>
self.warn(module, message, contextOrData, data),
info: (message: string, contextOrData?: LogContext | any, data?: any) =>
self.info(module, message, contextOrData, data),
debug: (message: string, contextOrData?: LogContext | any, data?: any) =>
self.debug(module, message, contextOrData, data),
trace: (message: string, contextOrData?: LogContext | any, data?: any) =>
self.trace(module, message, contextOrData, data),
withContext: (context: LogContext) => {
const contextual = self.withContext(context)
return contextual.createModuleLogger(module)
},
startTimer: (label: string) => self.startTimer(`${module}:${label}`),
endTimer: (label: string) => self.endTimer(`${module}:${label}`)
}
}
async flush(): Promise<void> {
const flushPromises = this.config.transports
.filter(t => t.flush)
.map(t => t.flush!())
await Promise.all(flushPromises)
}
addTransport(transport: LogTransport): void {
this.config.transports.push(transport)
}
removeTransport(name: string): void {
this.config.transports = this.config.transports.filter(t => t.name !== name)
}
child(context: LogContext): StructuredLogger {
return this.withContext(context)
}
}
// Singleton instance
export const structuredLogger = StructuredLogger.getInstance()
// Convenience functions
export function createModuleLogger(module: string) {
return structuredLogger.createModuleLogger(module)
}
export function setLogContext(context: LogContext) {
structuredLogger.setContext(context)
}
export function withLogContext(context: LogContext) {
return structuredLogger.withContext(context)
}
// Correlation ID middleware helper
export function createCorrelationId(): string {
return randomUUID()
}
// Performance logging helper
export function logPerformance(
logger: ReturnType<typeof createModuleLogger>,
operation: string,
fn: () => any
): any {
logger.startTimer(operation)
try {
const result = fn()
if (result && typeof result.then === 'function') {
return result.finally(() => {
const duration = logger.endTimer(operation)
logger.debug(`${operation} completed`, { duration })
})
}
const duration = logger.endTimer(operation)
logger.debug(`${operation} completed`, { duration })
return result
} catch (error) {
const duration = logger.endTimer(operation)
logger.error(`${operation} failed`, { duration, error })
throw error
}
}
// Backward compatibility wrapper for existing logger
export class LoggerCompatibilityWrapper {
private moduleLogger: ReturnType<typeof createModuleLogger>
constructor(module: string = 'legacy') {
this.moduleLogger = createModuleLogger(module)
}
error(module: string, message: string, ...args: any[]): void {
this.moduleLogger.error(message, { module, data: args })
}
warn(module: string, message: string, ...args: any[]): void {
this.moduleLogger.warn(message, { module, data: args })
}
info(module: string, message: string, ...args: any[]): void {
this.moduleLogger.info(message, { module, data: args })
}
debug(module: string, message: string, ...args: any[]): void {
this.moduleLogger.debug(message, { module, data: args })
}
trace(module: string, message: string, ...args: any[]): void {
this.moduleLogger.trace(message, { module, data: args })
}
createModuleLogger(module: string) {
const logger = createModuleLogger(module)
return {
error: (message: string, ...args: any[]) => logger.error(message, { data: args }),
warn: (message: string, ...args: any[]) => logger.warn(message, { data: args }),
info: (message: string, ...args: any[]) => logger.info(message, { data: args }),
debug: (message: string, ...args: any[]) => logger.debug(message, { data: args }),
trace: (message: string, ...args: any[]) => logger.trace(message, { data: args })
}
}
}
// Export types for external use
export type ModuleLogger = ReturnType<typeof createModuleLogger>
// Types are already exported above, no need to re-export