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)
This commit is contained in:
321
services/logging/Logger.ts
Normal file
321
services/logging/Logger.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
// 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;
|
||||
}
|
||||
10
services/logging/index.ts
Normal file
10
services/logging/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Logging Service - Centralized logging system
|
||||
// This module provides structured logging to replace console.log statements
|
||||
// throughout the application with proper log levels and contexts
|
||||
|
||||
export { LogLevel, logger, log } from './Logger';
|
||||
export type { LogEntry, LoggerConfig } from './Logger';
|
||||
|
||||
// Re-export for convenience
|
||||
import { logger } from './Logger';
|
||||
export default logger;
|
||||
Reference in New Issue
Block a user