debounced-logger.js
1 export const LOG_LEVELS = { 2 ERROR: 0, 3 WARN: 1, 4 INFO: 2, 5 DEBUG: 3, 6 TRACE: 4, 7 }; 8 9 const LOG_LEVEL_NAMES = { 10 [LOG_LEVELS.ERROR]: 'ERROR', 11 [LOG_LEVELS.WARN]: 'WARN', 12 [LOG_LEVELS.INFO]: 'INFO', 13 [LOG_LEVELS.DEBUG]: 'DEBUG', 14 [LOG_LEVELS.TRACE]: 'TRACE', 15 }; 16 17 const DEFAULT_CONFIG = { 18 level: LOG_LEVELS.INFO, 19 debounceTime: 5000, 20 maxRepeats: 10, 21 truncateObjects: true, 22 maxObjectDepth: 2, 23 maxArrayLength: 5, 24 maxStringLength: 200, 25 useGroups: true, 26 showTimestamps: true, 27 }; 28 29 let globalConfig = { ...DEFAULT_CONFIG }; 30 31 export function configureLogger(config) { 32 globalConfig = { ...DEFAULT_CONFIG, ...config }; 33 } 34 35 export function resetLoggerConfig() { 36 globalConfig = { ...DEFAULT_CONFIG }; 37 } 38 39 export class DebouncedLogger { 40 constructor(prefix, config = {}) { 41 this.prefix = prefix; 42 this.config = { ...globalConfig, ...config }; 43 this.logCache = new Map(); 44 this.groupStack = []; 45 } 46 47 error(message, ...args) { 48 this._log(LOG_LEVELS.ERROR, message, args); 49 } 50 51 warn(message, ...args) { 52 this._log(LOG_LEVELS.WARN, message, args); 53 } 54 55 info(message, ...args) { 56 this._log(LOG_LEVELS.INFO, message, args); 57 } 58 59 debug(message, ...args) { 60 this._log(LOG_LEVELS.DEBUG, message, args); 61 } 62 63 trace(message, ...args) { 64 this._log(LOG_LEVELS.TRACE, message, args); 65 } 66 67 group(label, collapsed = false) { 68 if (!this.config.useGroups) { 69 return; 70 } 71 72 const groupMethod = collapsed ? console.groupCollapsed : console.group; 73 groupMethod(`[${this.prefix}] ${label}`); 74 this.groupStack.push(label); 75 } 76 77 groupEnd() { 78 if (!this.config.useGroups || this.groupStack.length === 0) { 79 return; 80 } 81 82 console.groupEnd(); 83 this.groupStack.pop(); 84 } 85 86 clearCache() { 87 this.logCache.forEach(entry => { 88 if (entry.timeoutId) { 89 clearTimeout(entry.timeoutId); 90 } 91 }); 92 93 this.logCache.clear(); 94 } 95 96 _log(level, message, args) { 97 if (level > this.config.level) { 98 return; 99 } 100 101 const processedArgs = this._processArgs(args); 102 103 if (level <= LOG_LEVELS.WARN) { 104 this._writeLog(level, message, processedArgs); 105 return; 106 } 107 108 const key = this._getCacheKey(level, message, processedArgs); 109 110 if (this.logCache.has(key)) { 111 const entry = this.logCache.get(key); 112 113 entry.count++; 114 115 if (entry.timeoutId) { 116 clearTimeout(entry.timeoutId); 117 } 118 119 if (entry.count >= this.config.maxRepeats) { 120 this._writeLog(level, message, processedArgs, entry.count); 121 this.logCache.delete(key); 122 return; 123 } 124 125 entry.timeoutId = setTimeout(() => { 126 this._writeLog(level, message, processedArgs, entry.count); 127 this.logCache.delete(key); 128 }, this.config.debounceTime); 129 130 this.logCache.set(key, entry); 131 } else { 132 this._writeLog(level, message, processedArgs); 133 134 const timeoutId = setTimeout(() => { 135 this.logCache.delete(key); 136 }, this.config.debounceTime); 137 138 this.logCache.set(key, { count: 1, timeoutId }); 139 } 140 } 141 142 _writeLog(level, message, args, repeatCount = 1) { 143 const logMethod = this._getLogMethod(level); 144 const prefix = this._getPrefix(level); 145 146 let formattedMessage = `${prefix} ${message}`; 147 148 if (repeatCount > 1) { 149 formattedMessage += ` (repeated ${repeatCount} times)`; 150 } 151 152 logMethod(formattedMessage, ...args); 153 } 154 155 _getLogMethod(level) { 156 switch (level) { 157 case LOG_LEVELS.ERROR: 158 return console.error; 159 case LOG_LEVELS.WARN: 160 return console.warn; 161 case LOG_LEVELS.INFO: 162 return console.info; 163 case LOG_LEVELS.DEBUG: 164 return console.debug; 165 case LOG_LEVELS.TRACE: 166 return console.debug; 167 default: 168 return console.log; 169 } 170 } 171 172 _getPrefix(level) { 173 let prefix = `[${this.prefix}]`; 174 175 if (this.config.showTimestamps) { 176 const now = new Date(); 177 const timestamp = now.toISOString().split('T')[1].slice(0, 12); 178 prefix = `[${timestamp}] ${prefix}`; 179 } 180 181 if (level <= LOG_LEVELS.WARN) { 182 prefix = `${prefix} [${LOG_LEVEL_NAMES[level]}]`; 183 } 184 185 return prefix; 186 } 187 188 _processArgs(args) { 189 if (!this.config.truncateObjects) { 190 return args; 191 } 192 193 return args.map(arg => this._truncateValue(arg, this.config.maxObjectDepth)); 194 } 195 196 _truncateValue(value, depth) { 197 if (depth <= 0) { 198 return '[Object]'; 199 } 200 201 if (value === null || value === undefined) { 202 return value; 203 } 204 205 if (typeof value === 'string') { 206 if (value.length > this.config.maxStringLength) { 207 return `${value.substring(0, this.config.maxStringLength)}...`; 208 } 209 return value; 210 } 211 212 if (typeof value !== 'object') { 213 return value; 214 } 215 216 if (Array.isArray(value)) { 217 if (value.length > this.config.maxArrayLength) { 218 return [ 219 ...value 220 .slice(0, this.config.maxArrayLength) 221 .map(item => this._truncateValue(item, depth - 1)), 222 `... (${value.length - this.config.maxArrayLength} more items)`, 223 ]; 224 } 225 return value.map(item => this._truncateValue(item, depth - 1)); 226 } 227 228 const result = {}; 229 const keys = Object.keys(value); 230 231 for (const key of keys.slice(0, this.config.maxArrayLength)) { 232 result[key] = this._truncateValue(value[key], depth - 1); 233 } 234 235 if (keys.length > this.config.maxArrayLength) { 236 result['...'] = `(${keys.length - this.config.maxArrayLength} more properties)`; 237 } 238 239 return result; 240 } 241 242 _getCacheKey(level, message, args) { 243 try { 244 return `${level}:${message}:${JSON.stringify(args)}`; 245 } catch (e) { 246 return `${level}:${message}:${args.length}_args`; 247 } 248 } 249 } 250 251 export function createLogger(prefix, config = {}) { 252 return new DebouncedLogger(prefix, config); 253 }