UNPKG

@sixbell-telco/sdk

Version:

A collection of reusable components designed for use in Sixbell Telco Angular projects

579 lines (573 loc) 22.7 kB
import * as i0 from '@angular/core'; import { Injectable, makeEnvironmentProviders } from '@angular/core'; /** * Logger Constants * Color schemes, log levels, and configuration defaults */ const LOG_LEVELS = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, fatal: 5, }; // Light mode colors const LOG_COLORS_LIGHT = { trace: 'color: #666666; font-weight: bold;', // Dark Gray debug: 'color: #0052a3; font-weight: bold;', // Dark Blue info: 'color: #006600; font-weight: bold;', // Dark Green warn: 'color: #cc7700; font-weight: bold;', // Dark Orange error: 'color: #cc0000; font-weight: bold;', // Dark Red fatal: 'color: #ffffff; background-color: #cc0000; font-weight: bold; padding: 2px 4px;', // White on Red }; // Dark mode colors const LOG_COLORS_DARK = { trace: 'color: #aaaaaa; font-weight: bold;', // Light Gray debug: 'color: #66b3ff; font-weight: bold;', // Light Blue info: 'color: #66ff66; font-weight: bold;', // Light Green warn: 'color: #ffcc66; font-weight: bold;', // Light Orange error: 'color: #ff6666; font-weight: bold;', // Light Red fatal: 'color: #ffffff; background-color: #ff4444; font-weight: bold; padding: 2px 4px;', // White on Light Red }; // Component/context color - Light mode (dark purple) / Dark mode (light purple) const CONTEXT_COLOR_LIGHT = 'color: #5500aa; font-weight: bold;'; // Dark Purple const CONTEXT_COLOR_DARK = 'color: #dd99ff; font-weight: bold;'; // Light Purple // Timestamp color - Light mode (dark teal) / Dark mode (light teal) const TIMESTAMP_COLOR_LIGHT = 'color: #006666; font-weight: bold;'; // Dark Teal const TIMESTAMP_COLOR_DARK = 'color: #66ffff; font-weight: bold;'; // Light Teal // IndexedDB defaults const IDB_STORE_NAME = 'sixbell-logs'; const IDB_DB_NAME = 'sixbell-ui-kit'; const MAX_STORED_LOGS = 1000; // Prevent memory bloat class LoggerService { config = { enableLogging: false, logLevel: 'error', showTimestamp: false, showDate: false, silentMode: false, persistToIndexedDB: false, idbRetentionDays: 7, // Default: keep logs for 7 days idbMaxLogs: 5000, // Default: store max 5000 logs in IndexedDB isDarkMode: false, }; logStore = []; darkModeMediaQuery = null; darkModeListenerInitialized = false; idbReady = Promise.resolve(null); /** * Initialize automatic dark mode detection based on system preferences * Only called when logging is enabled to minimize performance impact */ initializeDarkModeDetection() { // Skip if already initialized or not needed if (this.darkModeListenerInitialized || this.config.silentMode) { return; } if (typeof window !== 'undefined' && window.matchMedia) { this.darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); // Set initial value based on system preference this.config.isDarkMode = this.darkModeMediaQuery.matches; // Listen for changes in system preference with both addEventListener and addListener for better browser support const handleChange = (e) => { const isDark = 'matches' in e ? e.matches : false; this.config.isDarkMode = isDark; // Log the theme change for debugging if (this.config.enableLogging) { console.log(`%c[THEME CHANGED] Theme switched to ${isDark ? 'dark' : 'light'} mode`, isDark ? LOG_COLORS_DARK.info : LOG_COLORS_LIGHT.info); } }; this.darkModeMediaQuery.addEventListener('change', handleChange); // Fallback for older browsers if (this.darkModeMediaQuery.addListener) { this.darkModeMediaQuery.addListener(handleChange); } this.darkModeListenerInitialized = true; } } configure(config) { this.config = { ...this.config, ...config }; // Only initialize dark mode detection when logging is enabled and not in silent mode if (this.config.enableLogging && !this.config.silentMode) { this.initializeDarkModeDetection(); } // Initialize IndexedDB if persistence is enabled if (this.config.persistToIndexedDB) { this.idbReady = this.initializeIndexedDB(); } } /** * Get all stored logs (useful for sending to external services) */ getStoredLogs() { return [...this.logStore]; } /** * Clear stored logs */ clearStoredLogs() { this.logStore = []; } /** * Initialize IndexedDB for persistent log storage * Only called when persistToIndexedDB is enabled */ initializeIndexedDB() { if (!this.config.persistToIndexedDB || typeof indexedDB === 'undefined') { return Promise.resolve(null); } return new Promise((resolve) => { const request = indexedDB.open(IDB_DB_NAME, 1); request.onerror = () => { console.error('IndexedDB open error:', request.error); resolve(null); }; request.onsuccess = () => { resolve(request.result); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(IDB_STORE_NAME)) { const store = db.createObjectStore(IDB_STORE_NAME, { keyPath: 'id', autoIncrement: true }); store.createIndex('timestamp', 'timestamp', { unique: false }); } }; }); } /** * Persist a log entry to IndexedDB */ persistLogToIndexedDB(logEntry) { if (!this.config.persistToIndexedDB) return; this.idbReady.then((db) => { if (!db) return; try { const transaction = db.transaction([IDB_STORE_NAME], 'readwrite'); const store = transaction.objectStore(IDB_STORE_NAME); // Don't persist Error objects directly (they don't serialize well) const sanitizedEntry = { ...logEntry, error: logEntry.error ? { name: logEntry.error.name, message: logEntry.error.message, stack: logEntry.error.stack, } : undefined, }; store.add(sanitizedEntry); // Clean up old logs based on retention policy this.cleanupOldLogs(db); } catch (error) { console.error('Error persisting log to IndexedDB:', error); } }); } /** * Clean up old logs based on retention policy (days and max count) */ cleanupOldLogs(db) { try { const transaction = db.transaction([IDB_STORE_NAME], 'readwrite'); const store = transaction.objectStore(IDB_STORE_NAME); const index = store.index('timestamp'); // Calculate cutoff time (now - retention days) const retentionMs = (this.config.idbRetentionDays || 7) * 24 * 60 * 60 * 1000; const cutoffTime = Date.now() - retentionMs; // Delete logs older than retention period const range = IDBKeyRange.upperBound(cutoffTime, true); const cursorRequest = index.openCursor(range); cursorRequest.onsuccess = (event) => { const target = event.target; const cursor = target.result; if (cursor) { cursor.delete(); cursor.continue(); } }; // Also check max log count and delete oldest if exceeded const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { const allLogs = getAllRequest.result; const maxLogs = this.config.idbMaxLogs || 5000; if (allLogs.length > maxLogs) { // Sort by timestamp and delete oldest ones allLogs.sort((a, b) => a.timestamp - b.timestamp); const logsToDelete = allLogs.slice(0, allLogs.length - maxLogs); logsToDelete.forEach((log) => { store.delete(log.id); }); } }; } catch (error) { console.error('Error cleaning up old logs:', error); } } /** * Retrieve logs from IndexedDB */ async getPersistedLogs() { const db = await this.idbReady; if (!db) return []; return new Promise((resolve) => { try { const transaction = db.transaction([IDB_STORE_NAME], 'readonly'); const store = transaction.objectStore(IDB_STORE_NAME); const request = store.getAll(); request.onsuccess = () => { resolve(request.result); }; request.onerror = () => { console.error('Error retrieving logs from IndexedDB:', request.error); resolve([]); }; } catch (error) { console.error('Error reading IndexedDB:', error); resolve([]); } }); } /** * Clear persisted logs from IndexedDB */ async clearPersistedLogs() { const db = await this.idbReady; if (!db) return; return new Promise((resolve, reject) => { try { const transaction = db.transaction([IDB_STORE_NAME], 'readwrite'); const store = transaction.objectStore(IDB_STORE_NAME); const request = store.clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); } catch (error) { reject(error); } }); } /** * Download logs as a JSON file * Combines in-memory logs with persisted IndexedDB logs */ async downloadLogs(filename = `logs-${Date.now()}.json`) { try { // Get all logs (memory + IndexedDB) const memoryLogs = this.getStoredLogs(); const persistedLogs = await this.getPersistedLogs(); const allLogs = [...memoryLogs, ...persistedLogs]; // Create JSON blob const jsonStr = JSON.stringify(allLogs, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); // Create download link and trigger const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } catch (error) { console.error('Error downloading logs:', error); } } /** * Update theme mode for color adjustment * If you want to restore automatic detection, pass undefined * * @param isDark true for dark mode, false for light mode, undefined to use system preference */ setDarkMode(isDark) { if (isDark === undefined) { // Restore automatic detection - initialize listener if needed if (this.config.enableLogging && !this.config.silentMode) { this.initializeDarkModeDetection(); } if (this.darkModeMediaQuery) { this.config.isDarkMode = this.darkModeMediaQuery.matches; } } else { this.config.isDarkMode = isDark; } } /** * Get current dark mode status */ isDarkMode() { return this.config.isDarkMode || false; } /** * Create a temporary logger with override configuration. * Useful for local use-case-specific logging without global provider changes. * * @param overrideConfig Configuration that overrides current logger settings * @returns A new LoggerService instance with merged config * * @example * ```typescript * // In theme service: * private readonly logger = inject(LoggerService); * * // For this specific operation, use debug level: * const debugLogger = this.logger.withConfig({ logLevel: 'debug' }); * debugLogger.debug('Detailed operation trace'); * * // Back to global config: * this.logger.debug('This respects global config'); * ``` */ withConfig(overrideConfig) { const logger = new LoggerService(); logger.configure({ ...this.config, ...overrideConfig }); return logger; } shouldLog(level) { if (!this.config.enableLogging) return false; const currentLevel = LOG_LEVELS[this.config.logLevel || 'error']; const messageLevel = LOG_LEVELS[level]; return messageLevel >= currentLevel; } getColors() { return this.config.isDarkMode ? LOG_COLORS_DARK : LOG_COLORS_LIGHT; } getContextColor() { return this.config.isDarkMode ? CONTEXT_COLOR_DARK : CONTEXT_COLOR_LIGHT; } getTimestampColor() { return this.config.isDarkMode ? TIMESTAMP_COLOR_DARK : TIMESTAMP_COLOR_LIGHT; } storeLog(level, message, context, error) { this.logStore.push({ timestamp: Date.now(), level, message, context, error, }); // Prevent memory bloat if (this.logStore.length > MAX_STORED_LOGS) { this.logStore.shift(); } } /** * Extract component name from message pattern like "[ComponentName] message" * and return formatted parts for colored logging */ formatMessageWithContext(message) { const componentMatch = message.match(/^\[([^\]]+)\]\s(.*)$/); const contextColor = this.getContextColor(); if (componentMatch) { const componentName = componentMatch[1]; const restOfMessage = componentMatch[2]; return { formatted: `%c[${componentName}]%c ${restOfMessage}`, styles: [contextColor, ''], }; } return { formatted: `%c${message}`, styles: [''], }; } /** * Format complete log output with level, timestamp, context, and message * Returns array of format string and all styles for console output */ formatCompleteLogOutput(level, message, context) { const colors = this.getColors(); const levelStyle = colors[level]; const contextColor = this.getContextColor(); const timestampColor = this.getTimestampColor(); let format = `%c[${level.toUpperCase()}]`; const styles = [levelStyle]; // Add timestamp if enabled if (this.config.showTimestamp || this.config.showDate) { const now = new Date(); const isoString = now.toISOString(); let timeStr; if (this.config.showDate) { const date = isoString.split('T')[0]; const time = isoString.split('T')[1].slice(0, 12); timeStr = `${date} ${time}`; } else { timeStr = isoString.split('T')[1].slice(0, 12); } format += `%c [${timeStr}]`; styles.push(timestampColor); } // Add context if provided if (context?.component) { format += `%c [${context.component}]`; styles.push(contextColor); } // Add message format += `%c ${message}`; styles.push(''); return { format, styles }; } trace(message, context) { if (this.shouldLog('trace')) { const logEntry = { level: 'trace', message, context, timestamp: Date.now(), error: undefined }; this.storeLog('trace', message, context); if (!this.config.silentMode) { const { format, styles } = this.formatCompleteLogOutput('trace', message, context); if (context && Object.keys(context).length > 1) { console.debug(format, ...styles, context); } else { console.debug(format, ...styles); } } this.persistLogToIndexedDB(logEntry); } } debug(message, context) { if (this.shouldLog('debug')) { const logEntry = { level: 'debug', message, context, timestamp: Date.now(), error: undefined }; this.storeLog('debug', message, context); if (!this.config.silentMode) { const { format, styles } = this.formatCompleteLogOutput('debug', message, context); if (context && Object.keys(context).length > 1) { console.debug(format, ...styles, context); } else { console.debug(format, ...styles); } } this.persistLogToIndexedDB(logEntry); } } info(message, context) { if (this.shouldLog('info')) { const logEntry = { level: 'info', message, context, timestamp: Date.now(), error: undefined }; this.storeLog('info', message, context); if (!this.config.silentMode) { const { format, styles } = this.formatCompleteLogOutput('info', message, context); if (context && Object.keys(context).length > 1) { console.info(format, ...styles, context); } else { console.info(format, ...styles); } } this.persistLogToIndexedDB(logEntry); } } warn(message, context) { if (this.shouldLog('warn')) { const logEntry = { level: 'warn', message, context, timestamp: Date.now(), error: undefined }; this.storeLog('warn', message, context); if (!this.config.silentMode) { const { format, styles } = this.formatCompleteLogOutput('warn', message, context); if (context && Object.keys(context).length > 1) { console.warn(format, ...styles, context); } else { console.warn(format, ...styles); } } this.persistLogToIndexedDB(logEntry); } this.reportToExternal('warn', message, context); } error(message, error, context) { if (this.shouldLog('error')) { const logEntry = { level: 'error', message, context, timestamp: Date.now(), error }; this.storeLog('error', message, context, error); if (!this.config.silentMode) { const { format, styles } = this.formatCompleteLogOutput('error', message, context); if (error && context && Object.keys(context).length > 1) { console.error(format, ...styles, error, context); } else if (error) { console.error(format, ...styles, error); } else if (context && Object.keys(context).length > 1) { console.error(format, ...styles, context); } else { console.error(format, ...styles); } } this.persistLogToIndexedDB(logEntry); } this.reportToExternal('error', message, context, error); } fatal(message, error, context) { if (this.shouldLog('fatal')) { const logEntry = { level: 'fatal', message, context, timestamp: Date.now(), error }; this.storeLog('fatal', message, context, error); if (!this.config.silentMode) { const { format, styles } = this.formatCompleteLogOutput('fatal', message, context); if (error && context && Object.keys(context).length > 1) { console.error(format, ...styles, error, context); } else if (error) { console.error(format, ...styles, error); } else if (context && Object.keys(context).length > 1) { console.error(format, ...styles, context); } else { console.error(format, ...styles); } } this.persistLogToIndexedDB(logEntry); } this.reportToExternal('fatal', message, context, error); } reportToExternal(level, message, context, error) { // Only report warn, error, and fatal to external services if (this.config.onError && error) { this.config.onError(error, context || this.createContext('unknown')); } if (this.config.errorReporter && error) { this.config.errorReporter.captureException(error, context); } } createContext(component) { return { component, action: 'unknown', timestamp: Date.now(), }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: LoggerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: LoggerService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: LoggerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); function provideLogger(config) { return makeEnvironmentProviders([ { provide: LoggerService, useFactory: () => { const logger = new LoggerService(); logger.configure(config || {}); return logger; }, }, ]); } /** * Generated bundle index. Do not edit. */ export { LoggerService, provideLogger }; //# sourceMappingURL=sixbell-telco-sdk-utils-logger.mjs.map