@sixbell-telco/sdk
Version:
A collection of reusable components designed for use in Sixbell Telco Angular projects
579 lines (573 loc) • 22.7 kB
JavaScript
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