All files / lib/core/runtime AvenxLogger.js

96.11% Statements 371/386
74.54% Branches 82/110
100% Functions 17/17
96.11% Lines 371/386

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 31x 1x 1x 30x 30x 31x       31x     31x     30x 31x 31x 22x 22x 22x 30x 31x 2x 2x 31x 27x 27x 1x 1x 1x   31x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 189x 189x     189x 189x 189x 189x 189x 189x 189x 189x 189x 32x 189x 32x 32x 32x 32x 32x 32x 5x 32x 32x 27x 27x 27x 27x 27x 27x 32x 189x 189x 189x 185x 185x 185x 18x 3x 3x 189x 1x 189x 189x 505x 505x 505x 505x 505x 505x 505x 1482x 1482x 1482x 1482x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 519x 519x 519x 519x 519x 519x 519x 519x 519x 505x 505x 505x 505x 505x 505x 757x 757x 757x 757x 757x 757x 757x 757x 757x 757x 757x       757x 505x 505x 505x 505x 505x 505x 1x 1x 505x 505x 505x 505x 505x 505x 505x 1765x 152x 152x 1765x 1765x 1765x 1765x 505x 505x 505x 505x 505x 505x 505x 1765x 264x 264x 1765x 1765x 1765x 1765x 1502x 1x 1502x 1501x 1501x 1502x 1765x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 505x 8x 8x 8x 8x 8x 8x 10x 10x 10x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 3x 3x 2x 2x 3x 10x 10x 10x 10x 10x 8x 8x 8x     8x 10x 10x 8x 8x 8x 8x 505x 505x 505x 505x 505x 505x 3x 3x 505x 505x 505x 505x 505x 505x 2x 2x 505x 505x 505x 505x 505x 505x 1447x 1447x 505x 505x 505x 505x 505x 505x 1x 1x 505x 505x 505x 505x 505x 505x 238x 238x 505x 505x 505x 505x 505x 505x 70x 70x 505x 505x 505x 505x 505x 505x 4x 4x 505x 505x 505x  
/**
 * @file AvenxLogger.js
 * @description Centralized logging module for the Avenx-JS framework.
 * Supports trace, debug, info, warn, error, fatal log levels, alias log -> info,
 * global silent/off option, custom formatters, and custom transports.
 */
 
export const LogLevels = {
  trace: 0,
  debug: 1,
  info: 2,
  warn: 3,
  error: 4,
  fatal: 5,
  off: 6,
  silent: 6,
};
 
/**
 * Formats component context metadata (componentName, fileName) into a diagnostic tag.
 * @param {object} context - Context object or component instance.
 * @returns {string} Formatted context tag string or empty string.
 */
export function formatContextTag(context) {
  if (!context || typeof context !== 'object' || context instanceof Error) {
    return '';
  }
 
  let compName = context.componentName;
  if (!compName && context.component) {
    const comp = context.component;
    compName = comp.componentName || comp.name || (comp.constructor && comp.constructor.name !== 'Object' && comp.constructor.name !== 'Function' ? comp.constructor.name : null);
  }
  if (!compName && context.name && context.name !== 'Error' && !context.name.endsWith('Error')) {
    compName = context.name;
  }
  if (!compName && context.constructor && context.constructor.name !== 'Object' && context.constructor.name !== 'Function' && context.constructor.name !== 'Error' && !context.constructor.name.endsWith('Error')) {
    compName = context.constructor.name;
  }
 
  let file = context.fileName || context.__filename || context.file;
  if (!file && context.component) {
    const comp = context.component;
    file = comp.fileName || comp.__filename || comp.file;
  }
 
  if (compName && file) {
    return `[${compName} <${file}>]`;
  }
  if (compName) {
    return `[${compName}]`;
  }
  if (file) {
    return `[<${file}>]`;
  }
  return '';
}
 
/**
 * Default formatter for browser runtime.
 * Prefixes messages with [Avenx level] and formats component context metadata if present.
 * Preserves interactive object logs by prepending to string or prepending as separate arg.
 * @param {string} level - Log level name.
 * @param {any[]} args - Array of raw arguments.
 * @returns {any[]} Array of formatted arguments.
 */
export function defaultFormatter(level, args) {
  const prefix = `[Avenx ${level}]`;
  if (!args || args.length === 0) {
    return [prefix];
  }
 
  let contextTag = '';
  const cleanArgs = [...args];
 
  const lastArg = cleanArgs[cleanArgs.length - 1];
  if (
    lastArg &&
    typeof lastArg === 'object' &&
    !(lastArg instanceof Error) &&
    !Array.isArray(lastArg)
  ) {
    const isExplicitContext = Boolean(
      lastArg.componentName ||
        lastArg.fileName ||
        lastArg.__filename ||
        lastArg.component ||
        lastArg.$logContext ||
        lastArg.__isAvenxComponent
    );
    if (isExplicitContext) {
      const tag = formatContextTag(lastArg);
      if (tag) {
        contextTag = tag;
        cleanArgs.pop();
      }
    }
  }
 
  if (cleanArgs.length > 0) {
    if (typeof cleanArgs[0] === 'string') {
      const firstStr = contextTag ? `${prefix} ${contextTag} ${cleanArgs[0]}` : `${prefix} ${cleanArgs[0]}`;
      return [firstStr, ...cleanArgs.slice(1)];
    }
    if (cleanArgs[0] instanceof Error) {
      return contextTag ? [`${prefix} ${contextTag}`, ...cleanArgs] : cleanArgs;
    }
  }
 
  return contextTag ? [`${prefix} ${contextTag}`, ...cleanArgs] : [prefix, ...cleanArgs];
}
 
/**
 * Default console transport.
 * Dispatches messages to console methods dynamically.
 */
export const consoleTransport = {
  log(level, formattedArgs) {
    const method = level === 'fatal' ? 'error' : level === 'trace' ? 'debug' : console[level] ? level : 'log';
    if (typeof console !== 'undefined' && console[method]) {
      console[method](...formattedArgs);
    }
  },
};
 
/**
 * @typedef {Object} LoggingConfig
 * @property {('trace'|'debug'|'info'|'warn'|'error'|'fatal')} [level='info'] - The minimum severity level to output.
 * @property {boolean} [silent=false] - Global silence setting. If true, suppresses all logging.
 * @property {Function} [formatter=defaultFormatter] - Custom formatting callback function. Receives (level, args).
 * @property {Array<Object|Function>} [transports=[consoleTransport]] - Collection of transport targets.
 */
 
/**
 * Central logger class for Avenx-JS framework.
 * @example
 * // 1. Basic initialization during application setup:
 * const app = new AvenxApp({
 *   logging: {
 *     level: 'debug',
 *     silent: false
 *   }
 * });
 * @example
 * // 2. Usage inside component methods:
 * // Import the shared logger from the runtime entry point — component
 * // instances do not expose `this.logger`.
 * import { logger } from 'avenx-core/runtime';
 *
 * export default {
 *   name: 'TargetSyncComponent',
 *   methods: {
 *     async syncDatabase(targets) {
 *       logger.info('Starting celestial synchronization...', { count: targets.length });
 *       try {
 *         if (!targets || targets.length === 0) {
 *           logger.warn('Sync skipped: list is empty.');
 *           return;
 *         }
 *         logger.debug('Processing batch.', { sampleId: targets[0].id });
 *       } catch (error) {
 *         logger.error('Database sync failed.', { error: error.message });
 *       }
 *     }
 *   }
 * };
 * @example
 * // 3. Registering a Custom Formatter:
 * const customFormatter = (level, args) => {
 *   return [`[MY-APP] [${level.toUpperCase()}]:`, ...args];
 * };
 * const loggerWithFormatter = new AvenxLogger({ formatter: customFormatter });
 * @example
 * // 4. Registering a Custom Transport:
 * const fileTransport = {
 *   log(level, formattedArgs, rawArgs) {
 *     // Append custom streaming/file logic here
 *     fs.appendFileSync('./app.log', formattedArgs.join(' ') + '\n');
 *   }
 * };
 * const loggerWithTransport = new AvenxLogger({ transports: [fileTransport] });
 */
export class AvenxLogger {
  /**
   * Creates an instance of AvenxLogger.
   * @param {object} [config] - Application logger configuration options.
   */
  constructor(config = {}) {
    this.config = {
      level: 'info',
      silent: false,
      formatter: defaultFormatter,
      transports: [consoleTransport],
    };
    this.bindings = {};
    this.configure(config);
  }
 
  /**
   * Configures the logger instance options.
   * @param {object} config - Configuration options.
   */
  configure(config) {
    if (!config) return;
    this.config = {
      ...this.config,
      ...config,
    };
    // Ensure lowercase for level
    if (typeof this.config.level === 'string') {
      this.config.level = this.config.level.toLowerCase();
    }
    // Validate level against known LogLevels; fall back to 'info' on mismatch
    if (LogLevels[this.config.level] === undefined) {
      this.write('warn', `Invalid log level "${this.config.level}" — falling back to "info"`);
      this.config.level = 'info';
    }
  }
 
  /**
   * Sets the minimum log severity level programmatically.
   * @param {string} level - Log level name ('trace', 'debug', 'info', 'warn', 'error', 'fatal', 'off', 'silent').
   */
  setLevel(level) {
    this.configure({ level });
  }
 
  /**
   * Helper to check if a specific level should be logged.
   * @param {string} level - Log level to test.
   * @returns {boolean} True if logger should log the given level.
   */
  shouldLog(level) {
    if (this.config.silent || this.config.level === 'silent' || this.config.level === 'off') {
      return false;
    }
    const currentPriority = LogLevels[this.config.level] !== undefined ? LogLevels[this.config.level] : LogLevels.info;
    const targetPriority = LogLevels[level] !== undefined ? LogLevels[level] : LogLevels.info;
    return targetPriority >= currentPriority;
  }
 
  /**
   * Writes the log statement through configured formatter and transports.
   * @param {string} level - Log level name.
   * @param {...any} args - Arguments to log.
   */
  write(level, ...args) {
    if (!this.shouldLog(level)) {
      return;
    }
    const formatted = this.config.formatter ? this.config.formatter(level, args) : args;
 
    const transports = Array.isArray(this.config.transports) ? this.config.transports : [consoleTransport];
    for (const transport of transports) {
      if (typeof transport === 'function') {
        transport(level, formatted, args);
      } else if (transport && typeof transport.log === 'function') {
        transport.log(level, formatted, args);
      }
    }
  }
 
  /**
   * Creates a child logger instance that inherits the parent's log level, transports, and formatter.
   * Supports a string shorthand for prefix-only binding, or an object with prefix and/or componentName.
   * Parent bindings are merged with the new bindings (child overrides parent on conflict).
   * @param {string|object} [bindings] - Bindings configuration for the child logger.
   * @param {string} [bindings.prefix] - A prefix string prepended to every formatted log message.
   * @param {string} [bindings.componentName] - Component name injected as context metadata, formatted as [ComponentName] by defaultFormatter.
   * @returns {AvenxLogger} A new logger instance with inherited configuration and merged bindings.
   * @example
   * // String shorthand
   * const authLogger = logger.child('[AuthBridge]');
   * authLogger.info('User logged in');
   * // Output: [AuthBridge] [Avenx info] User logged in
   * @example
   * // Object bindings with context
   * const dbLogger = logger.child({ prefix: '[DB]', componentName: 'DatabaseBridge' });
   * dbLogger.warn('Connection slow');
   * // Output: [DB] [Avenx warn] [DatabaseBridge] Connection slow
   * @example
   * // Nested child loggers
   * const child = logger.child('[Parent]').child('[Child]');
   */
  child(bindings = {}) {
    const normBindings = typeof bindings === 'string' ? { prefix: bindings } : bindings;
    const childLogger = new AvenxLogger(this.config);
    childLogger.bindings = { ...this.bindings, ...normBindings };
 
    const parentFormatter = this.config.formatter || defaultFormatter;
    childLogger.config.formatter = (level, args) => {
      const injectArgs = [...args];
 
      if (childLogger.bindings.componentName) {
        const lastArg = injectArgs[injectArgs.length - 1];
        const hasContext =
          lastArg &&
          typeof lastArg === 'object' &&
          !Array.isArray(lastArg) &&
          !(lastArg instanceof Error) &&
          (lastArg.componentName ||
            lastArg.fileName ||
            lastArg.__filename ||
            lastArg.component ||
            lastArg.$logContext ||
            lastArg.__isAvenxComponent);
        if (!hasContext) {
          injectArgs.push({ componentName: childLogger.bindings.componentName });
        }
      }
 
      const formatted = parentFormatter(level, injectArgs);
 
      const prefix = childLogger.bindings.prefix || '';
      if (prefix && formatted.length > 0) {
        if (typeof formatted[0] === 'string') {
          formatted[0] = `${prefix} ${formatted[0]}`;
        } else {
          formatted.unshift(prefix);
        }
      }
 
      return formatted;
    };
 
    return childLogger;
  }
 
  /**
   * Logs a message with trace level.
   * @param {...any} args - Arguments to log.
   */
  trace(...args) {
    this.write('trace', ...args);
  }
 
  /**
   * Logs a message with debug level.
   * @param {...any} args - Arguments to log.
   */
  debug(...args) {
    this.write('debug', ...args);
  }
 
  /**
   * Logs a message with info level.
   * @param {...any} args - Arguments to log.
   */
  info(...args) {
    this.write('info', ...args);
  }
 
  /**
   * Alias for info level logging.
   * @param {...any} args - Arguments to log.
   */
  log(...args) {
    this.write('info', ...args);
  }
 
  /**
   * Logs a message with warn level.
   * @param {...any} args - Arguments to log.
   */
  warn(...args) {
    this.write('warn', ...args);
  }
 
  /**
   * Logs a message with error level.
   * @param {...any} args - Arguments to log.
   */
  error(...args) {
    this.write('error', ...args);
  }
 
  /**
   * Logs a message with fatal level.
   * @param {...any} args - Arguments to log.
   */
  fatal(...args) {
    this.write('fatal', ...args);
  }
}
 
export const logger = new AvenxLogger();