All files / src/logger/implementation base-logger.ts

100% Statements 67/67
96.77% Branches 30/31
100% Functions 40/40
100% Lines 56/56

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                                                                        222x 142x 8x   222x 205x           222x   222x                       576x                     1763x 576x       2179x       209x 209x       22x       165x       58x       325x       409x       208x       15x       16x       48x 48x       30x       86x 86x       50x       19x 19x       16x       16x 16x       13x       18x 18x       16x       1787x 1787x 1518x 944x     1518x 192x 1326x 1272x     1518x               1787x 1579x     208x 208x 208x       648x 479x   648x                   407x 36x   407x                        
import { emptyArray } from '../../utils/common/empty.ts';
import type { StringifyOptions } from '../../utils/converter/stringify.ts';
import { stringify } from '../../utils/converter/stringify.ts';
import type { Logger, LogLevel, LogMessage, LogTemplateStringsArray } from '../definition.ts';
import { shouldEmitEntry } from './level-utils.ts';
 
/**
 * Options for the BaseLogger class.
 */
export type BaseLoggerOptions = {
  /**
   * Options for how values are stringified in log messages.
   */
  readonly stringifyOptions?: StringifyOptions;
};
 
/**
 * 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 {
  /**
   * Converts an error input to a log message and arguments.
   *
   * @param logger The logger to use for stringification
   * @param input The input to convert
   * @param args The arguments to convert
   */
  public static convertErrorInput(
    logger: Logger,
    input: LogMessage | Error | { error: unknown },
    args: readonly unknown[],
  ): { readonly message: LogMessage; readonly args: readonly unknown[] } {
    const stringifyValue =
      logger instanceof BaseLogger
        ? (value: unknown) => logger.stringifyValue(value)
        : (value: unknown) => stringify(value);
 
    const message: LogMessage = () =>
      typeof input === 'function'
        ? input()
        : input && typeof input === 'object' && 'error' in input
          ? stringifyValue(input.error)
          : stringifyValue(input);
 
    const convertedArgs = typeof input === 'object' ? (args.length ? [input, ...args] : [input]) : args;
 
    return { message, args: convertedArgs };
  }
 
  /**
   * The minimum severity level for log entries to be emitted. Log entries with levels below this threshold will be
   * filtered out. Default is 'info'.
   */
  private readonly _levelProvider: () => LogLevel | 'off';
 
  /**
   * Additional arguments to include with the next template literal log entry. This is reset after each log operation.
   */
  private _pendingArgs: unknown[] = [];
 
  private readonly _options?: BaseLoggerOptions;
 
  /**
   * Creates a new BaseLogger with the specified minimum severity level.
   *
   * @param level The minimum severity level for log entries or a function that returns this value
   * @param options Options for how values are stringified in log messages
   */
  public constructor(level: LogLevel | 'off' | (() => LogLevel | 'off'), options?: BaseLoggerOptions) {
    this._levelProvider = typeof level === 'function' ? level : () => level;
    this._options = options;
  }
 
  public get level(): LogLevel | 'off' {
    return this._levelProvider();
  }
 
  public args(...args: unknown[]): Logger {
    this._pendingArgs.push(...args);
    return this;
  }
 
  public trace(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('trace', message, ...args);
  }
 
  public t(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('trace', () => this.taggedLog(strings, values));
  }
 
  public debug(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('debug', message, ...args);
  }
 
  public d(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('debug', () => this.taggedLog(strings, values));
  }
 
  public info(message: LogMessage, ...args: readonly unknown[]): void {
    this.log('info', message, ...args);
  }
 
  public i(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('info', () => this.taggedLog(strings, values));
  }
 
  public notice(message: LogMessage, ...args: unknown[]): void {
    this.log('notice', message, ...args);
  }
 
  public n(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('notice', () => this.taggedLog(strings, values));
  }
 
  public warning(input: LogMessage | Error | { error: unknown }, ...args: readonly unknown[]): void {
    const converted = BaseLogger.convertErrorInput(this, input, args);
    this.log('warning', converted.message, ...converted.args);
  }
 
  public w(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('warning', () => this.taggedLog(strings, values));
  }
 
  public error(input: LogMessage | Error | { error: unknown }, ...args: readonly unknown[]): void {
    const converted = BaseLogger.convertErrorInput(this, input, args);
    this.log('error', converted.message, ...converted.args);
  }
 
  public e(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('error', () => this.taggedLog(strings, values));
  }
 
  public critical(input: LogMessage | Error | { error: unknown }, ...args: unknown[]): void {
    const converted = BaseLogger.convertErrorInput(this, input, args);
    this.log('critical', converted.message, ...converted.args);
  }
 
  public c(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('critical', () => this.taggedLog(strings, values));
  }
 
  public alert(input: LogMessage | Error | { error: unknown }, ...args: readonly unknown[]): void {
    const converted = BaseLogger.convertErrorInput(this, input, args);
    this.log('alert', converted.message, ...converted.args);
  }
 
  public a(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('alert', () => this.taggedLog(strings, values));
  }
 
  public emergency(input: LogMessage | Error | { error: unknown }, ...args: readonly unknown[]): void {
    const converted = BaseLogger.convertErrorInput(this, input, args);
    this.log('emergency', converted.message, ...converted.args);
  }
 
  public em(strings: LogTemplateStringsArray, ...values: readonly unknown[]): void {
    this.log('emergency', () => this.taggedLog(strings, values));
  }
 
  public log(level: LogLevel, message: LogMessage, ...args: readonly unknown[]): void {
    const pendingArgs = this.consumePendingArgs();
    if (shouldEmitEntry(this, level)) {
      if (typeof message === 'function') {
        message = message();
      }
 
      if (pendingArgs) {
        args = args.length ? [...pendingArgs, ...args] : pendingArgs;
      } else if (!args.length) {
        args = emptyArray();
      }
 
      this.emit(level, String(message), args);
    }
  }
 
  /**
   * 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;
  }
 
  protected taggedLog(strings: LogTemplateStringsArray, values: readonly unknown[]): string {
    if (typeof strings === 'function') {
      strings = strings();
    }
    return String.raw({ raw: strings }, ...values.map((arg) => this.stringifyValue(arg)));
  }
 
  /**
   * Stringifies a value.
   *
   * @param value The message to stringify
   * @returns The stringified message
   */
  protected stringifyValue(value: unknown): string {
    if (typeof value === 'function') {
      value = (value as () => unknown)();
    }
    return stringify(value, this._options?.stringifyOptions);
  }
 
  /**
   * Emits the resolved log message at the specified level and extra arguments.
   *
   * @param level
   * @param message
   * @param args
   */
  protected abstract emit(level: LogLevel, message: string, args: readonly unknown[]): void;
}