All files / logger base-logger.ts

97.46% Statements 77/79
96.55% Branches 28/29
100% Functions 36/36
97.1% Lines 67/69

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 23518x 18x   18x               18x         337x         337x               337x       137x 137x       16x       13x       294x       324x       331x       156x       21x       14x       42x       20x       53x 49x 28x     49x 8x 8x 3x   5x       49x         25x       19x       11x       17x       10x       20x       11x       582x       812x 812x 757x                     1000x 560x   1000x                           757x   8x 8x     282x 282x     317x 317x     14x 14x     37x 37x     49x 49x     16x 16x     14x 14x     20x 20x                             865x                               812x 676x     136x 136x 136x      
import { exhaustiveCheck } from '../utils/common/exhaustive-check.ts';
import { stringify } from '../utils/converter/stringify.ts';
import type { Logger, LogLevel, LogMessage } from './definition.ts';
import { shouldEmitEntry } from './level-utils.ts';
 
/**
 * Base class for logger implementations, providing a complete implementation of the {@link Logger} interface.
 *
 * Logger implementors are strongly encouraged to extend this class to reduce the chances of future modifications
 * breaking current code.
 */
export abstract class BaseLogger implements Logger {
  /**
   * The minimum severity level for log entries to be emitted. Log entries with levels below this threshold will be
   * filtered out. Default is 'info'.
   */
  public level: LogLevel | 'off' = 'info';
 
  /**
   * Additional arguments to include with the next template literal log entry. This is reset after each log operation.
   */
  private _pendingArgs: unknown[] = [];
 
  /**
   * Creates a new BaseLogger with the specified minimum severity level.
   *
   * @param level The minimum severity level for log entries (default: 'info')
   */
  public constructor(level: LogLevel | 'off' = 'info') {
    this.level = level;
  }
 
  public args(...args: unknown[]): Logger {
    this._pendingArgs = args;
    return this;
  }
 
  public trace(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('trace', message, ...args);
  }
 
  public t(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.trace(() => this.taggedLog(strings, ...values));
  }
 
  public debug(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('debug', message, ...args);
  }
 
  public d(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.debug(() => this.taggedLog(strings, ...values));
  }
 
  public info(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('info', message, ...args);
  }
 
  public i(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.info(() => this.taggedLog(strings, ...values));
  }
 
  public notice(message: LogMessage, ...args: unknown[]): void {
    this.log('notice', message, ...args);
  }
 
  public n(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.notice(() => this.taggedLog(strings, ...values));
  }
 
  public warning(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('warning', message, ...args);
  }
 
  public w(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.warning(() => this.taggedLog(strings, ...values));
  }
 
  public error(value: unknown, ...args: readonly unknown[]): void {
    if (this.shouldEmitEntry('error')) {
      if (typeof value === 'function') {
        value = (value as () => unknown)();
      }
 
      if (value && typeof value === 'object') {
        args = [value, ...args];
        if ('error' in value) {
          value = this.stringify(value.error);
        } else {
          value = this.stringify(value);
        }
      }
 
      this.log('error', value as string, ...args);
    }
  }
 
  public e(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.error(() => this.taggedLog(strings, ...values));
  }
 
  public critical(message: LogMessage, ...args: unknown[]): void {
    this.log('critical', message, ...args);
  }
 
  public c(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.critical(() => this.taggedLog(strings, ...values));
  }
 
  public alert(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('alert', message, ...args);
  }
 
  public a(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.alert(() => this.taggedLog(strings, ...values));
  }
 
  public emergency(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('emergency', message, ...args);
  }
 
  public em(strings: TemplateStringsArray, ...values: readonly unknown[]): void {
    this.emergency(() => this.taggedLog(strings, ...values));
  }
 
  protected taggedLog(strings: TemplateStringsArray, ...values: readonly unknown[]): string {
    return String.raw({ raw: strings }, ...values.map((arg) => this.stringify(arg)));
  }
 
  public log(level: LogLevel, message: LogMessage, ...args: readonly unknown[]): void {
    const pendingArgs = this.consumePendingArgs();
    if (this.shouldEmitEntry(level)) {
      this.emit(level, this.stringify(message), pendingArgs ? [...pendingArgs, ...args] : args);
    }
  }
 
  /**
   * Stringifies the message.
   *
   * @param message The message to stringify
   * @returns The stringified message
   */
  protected stringify(message: unknown): string {
    if (typeof message === 'function') {
      message = (message as () => unknown)();
    }
    return stringify(message);
  }
 
  /**
   * Emits the resolved log message at the specified level and extra arguments.
   *
   * This method is the ideal extension point for clients providing a different logging mechanism because it is only
   * invoked if the level is applicable to the logger.
   *
   * @param level
   * @param message
   * @param args
   */
  protected emit(level: LogLevel, message: string, args: readonly unknown[]): void {
    switch (level) {
      case 'trace':
        this.emitLine(level, message, args);
        break;
 
      case 'debug':
        this.emitLine(level, message, args);
        break;
 
      case 'info':
        this.emitLine(level, message, args);
        break;
 
      case 'notice':
        this.emitLine(level, message, args);
        break;
 
      case 'warning':
        this.emitLine(level, message, args);
        break;
 
      case 'error':
        this.emitLine(level, message, args);
        break;
 
      case 'critical':
        this.emitLine(level, message, args);
        break;
 
      case 'alert':
        this.emitLine(level, message, args);
        break;
 
      case 'emergency':
        this.emitLine(level, message, args);
        break;
 
      default:
        exhaustiveCheck(level);
        throw new Error(`IllegalArgument: unsupported log level: '${level}'`);
    }
  }
 
  /**
   * Returns true if a log with the specified level is written.
   *
   * @param level The log level to check
   * @returns True if the log level is applicable, false otherwise
   */
  protected shouldEmitEntry(level: LogLevel): boolean {
    return shouldEmitEntry(this.level, level);
  }
 
  /**
   * Emits a log line. This is the main extensibility point of this class.
   *
   * @param line The log line
   * @param message The log message
   * @param args The log arguments
   */
  protected abstract emitLine(level: LogLevel, message: string, args: readonly unknown[]): void;
 
  /**
   * Consumes and returns the pending arguments, then clears them.
   */
  protected consumePendingArgs(): readonly unknown[] | undefined {
    if (!this._pendingArgs.length) {
      return undefined;
    }
 
    const args = this._pendingArgs;
    this._pendingArgs = [];
    return args;
  }
}