Files
rxminder/services/logging/Logger.ts
William Valentin 8c591563c9 feat: consolidate architecture and eliminate code duplication
🏗️ Major architectural improvements:

Database Layer:
- Consolidated duplicate CouchDB services (~800 lines of duplicated code eliminated)
- Implemented strategy pattern with MockDatabaseStrategy and ProductionDatabaseStrategy
- Created unified DatabaseService with automatic environment detection
- Maintained backward compatibility via updated factory pattern

Configuration System:
- Centralized all environment variables in single config/app.config.ts
- Added comprehensive configuration validation with clear error messages
- Eliminated hardcoded base URLs and scattered env var access across 8+ files
- Supports both legacy and new environment variable names

Logging Infrastructure:
- Replaced 25+ scattered console.log statements with structured Logger service
- Added log levels (ERROR, WARN, INFO, DEBUG, TRACE) and contexts (AUTH, DATABASE, API, UI)
- Production-safe logging with automatic level adjustment
- Development helpers for debugging and performance monitoring

Docker & Deployment:
- Removed duplicate docker/Dockerfile configuration
- Enhanced root Dockerfile with comprehensive environment variable support
- Added proper health checks and security improvements

Code Quality:
- Fixed package name consistency (rxminder → RxMinder)
- Updated services to use centralized configuration and logging
- Resolved all ESLint errors and warnings
- Added comprehensive documentation and migration guides

📊 Impact:
- Eliminated ~500 lines of duplicate code
- Single source of truth for database, configuration, and logging
- Better type safety and error handling
- Improved development experience and maintainability

📚 Documentation:
- Added ARCHITECTURE_MIGRATION.md with detailed migration guide
- Created IMPLEMENTATION_SUMMARY.md with metrics and benefits
- Inline documentation for all new services and interfaces

🔄 Backward Compatibility:
- All existing code continues to work unchanged
- Legacy services show deprecation warnings but remain functional
- Gradual migration path available for development teams

Breaking Changes: None (full backward compatibility maintained)
2025-09-08 01:09:48 -07:00

322 lines
8.0 KiB
TypeScript

// Centralized Logging Service
// Provides structured logging with different levels and contexts
// Replaces scattered console.log statements throughout the application
import { getEnvVar, isProduction, isTest } from '../../utils/env';
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
TRACE = 4,
}
export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: string;
data?: unknown;
error?: Error;
}
export interface LoggerConfig {
level: LogLevel;
enableConsole: boolean;
enableStorage: boolean;
maxStoredLogs: number;
contexts: string[];
}
class Logger {
private config: LoggerConfig;
private logs: LogEntry[] = [];
constructor() {
this.config = {
level: this.getDefaultLogLevel(),
enableConsole: true,
enableStorage: !isProduction(),
maxStoredLogs: 1000,
contexts: [],
};
}
private getDefaultLogLevel(): LogLevel {
if (isProduction()) {
return LogLevel.WARN;
} else if (isTest()) {
return LogLevel.ERROR;
} else {
return LogLevel.DEBUG;
}
}
private shouldLog(level: LogLevel, context?: string): boolean {
// Check if level is enabled
if (level > this.config.level) {
return false;
}
// Check if context is filtered (if contexts filter is set)
if (this.config.contexts.length > 0 && context) {
return this.config.contexts.includes(context);
}
return true;
}
private formatMessage(
level: LogLevel,
message: string,
context?: string
): string {
const levelName = LogLevel[level];
const timestamp = new Date().toISOString();
const contextPart = context ? `[${context}] ` : '';
return `${timestamp} ${levelName} ${contextPart}${message}`;
}
private log(
level: LogLevel,
message: string,
context?: string,
data?: unknown,
error?: Error
): void {
if (!this.shouldLog(level, context)) {
return;
}
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
context,
data,
error,
};
// Store log entry
if (this.config.enableStorage) {
this.logs.push(entry);
// Trim logs if exceeding max
if (this.logs.length > this.config.maxStoredLogs) {
this.logs = this.logs.slice(-this.config.maxStoredLogs);
}
}
// Console output
if (this.config.enableConsole) {
const formattedMessage = this.formatMessage(level, message, context);
switch (level) {
case LogLevel.ERROR:
if (error) {
console.error(formattedMessage, data, error);
} else {
console.error(formattedMessage, data);
}
break;
case LogLevel.WARN:
console.warn(formattedMessage, data);
break;
case LogLevel.INFO:
// eslint-disable-next-line no-console
console.info(formattedMessage, data);
break;
case LogLevel.DEBUG:
case LogLevel.TRACE:
// eslint-disable-next-line no-console
console.log(formattedMessage, data);
break;
}
}
}
// Public logging methods
error(
message: string,
context?: string,
data?: unknown,
error?: Error
): void {
this.log(LogLevel.ERROR, message, context, data, error);
}
warn(message: string, context?: string, data?: unknown): void {
this.log(LogLevel.WARN, message, context, data);
}
info(message: string, context?: string, data?: unknown): void {
this.log(LogLevel.INFO, message, context, data);
}
debug(message: string, context?: string, data?: unknown): void {
this.log(LogLevel.DEBUG, message, context, data);
}
trace(message: string, context?: string, data?: unknown): void {
this.log(LogLevel.TRACE, message, context, data);
}
// Authentication specific logging
auth = {
login: (message: string, data?: unknown) =>
this.info(message, 'AUTH', data),
logout: (message: string, data?: unknown) =>
this.info(message, 'AUTH', data),
register: (message: string, data?: unknown) =>
this.info(message, 'AUTH', data),
error: (message: string, error?: Error, data?: unknown) =>
this.error(message, 'AUTH', data, error),
};
// Database specific logging
db = {
query: (message: string, data?: unknown) =>
this.debug(message, 'DATABASE', data),
error: (message: string, error?: Error, data?: unknown) =>
this.error(message, 'DATABASE', data, error),
warn: (message: string, data?: unknown) =>
this.warn(message, 'DATABASE', data),
};
// API specific logging
api = {
request: (message: string, data?: unknown) =>
this.debug(message, 'API', data),
response: (message: string, data?: unknown) =>
this.debug(message, 'API', data),
error: (message: string, error?: Error, data?: unknown) =>
this.error(message, 'API', data, error),
};
// UI specific logging
ui = {
action: (message: string, data?: unknown) =>
this.debug(message, 'UI', data),
error: (message: string, error?: Error, data?: unknown) =>
this.error(message, 'UI', data, error),
};
// Configuration methods
setLevel(level: LogLevel): void {
this.config.level = level;
}
setContext(contexts: string[]): void {
this.config.contexts = contexts;
}
enableConsoleLogging(enable: boolean = true): void {
this.config.enableConsole = enable;
}
enableStorageLogging(enable: boolean = true): void {
this.config.enableStorage = enable;
}
// Utility methods
getLogs(context?: string, level?: LogLevel): LogEntry[] {
let filteredLogs = [...this.logs];
if (context) {
filteredLogs = filteredLogs.filter(log => log.context === context);
}
if (level !== undefined) {
filteredLogs = filteredLogs.filter(log => log.level === level);
}
return filteredLogs;
}
clearLogs(): void {
this.logs = [];
}
exportLogs(): string {
return JSON.stringify(this.logs, null, 2);
}
getLogSummary(): { [key: string]: number } {
const summary: { [key: string]: number } = {};
this.logs.forEach(log => {
const key = `${LogLevel[log.level]}${log.context ? `:${log.context}` : ''}`;
summary[key] = (summary[key] || 0) + 1;
});
return summary;
}
// Performance logging utilities
time(label: string): void {
if (this.shouldLog(LogLevel.DEBUG)) {
// eslint-disable-next-line no-console
console.time(label);
}
}
timeEnd(label: string): void {
if (this.shouldLog(LogLevel.DEBUG)) {
// eslint-disable-next-line no-console
console.timeEnd(label);
}
}
// Group logging for related operations
group(title: string, level: LogLevel = LogLevel.DEBUG): void {
if (this.shouldLog(level) && this.config.enableConsole) {
// eslint-disable-next-line no-console
console.group(title);
}
}
groupEnd(): void {
if (this.config.enableConsole) {
// eslint-disable-next-line no-console
console.groupEnd();
}
}
// Development helpers
table(data: unknown, context?: string): void {
if (this.shouldLog(LogLevel.DEBUG, context) && this.config.enableConsole) {
// eslint-disable-next-line no-console
console.table(data);
}
}
dir(object: unknown, context?: string): void {
if (this.shouldLog(LogLevel.DEBUG, context) && this.config.enableConsole) {
// eslint-disable-next-line no-console
console.dir(object);
}
}
}
// Singleton logger instance
export const logger = new Logger();
// Convenience exports for common logging patterns
export const log = {
error: logger.error.bind(logger),
warn: logger.warn.bind(logger),
info: logger.info.bind(logger),
debug: logger.debug.bind(logger),
trace: logger.trace.bind(logger),
auth: logger.auth,
db: logger.db,
api: logger.api,
ui: logger.ui,
};
// Development helper to expose logger globally
if (getEnvVar('DEBUG_MODE') === 'true' && typeof window !== 'undefined') {
(window as unknown as { __logger: Logger }).__logger = logger;
}