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

100% Statements 130/130
98.46% Branches 64/65
100% Functions 30/30
100% Lines 130/130

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 2221x   1x   1x                                   1x               555x 203x 203x 203x 203x 203x 203x 197x 6x   203x 186x 47x 139x 7x 132x   203x   203x 203x           555x         555x   555x               555x 555x 555x 555x   555x 2001x 2001x   555x 207x 207x 207x   555x 22x 22x   555x 165x 165x   555x 57x 57x   555x 325x 325x   555x 385x 385x   555x 167x 167x   555x 15x 15x   555x 16x 16x   555x 48x 48x 48x   555x 21x 21x   555x 75x 75x 75x   555x 48x 48x   555x 19x 19x 19x   555x 16x 16x   555x 16x 16x 16x   555x 13x 13x   555x 18x 18x 18x   555x 16x 16x   555x 1663x 1663x 1395x 877x 877x   1395x 190x 1395x 1160x 1160x   1395x 1395x 1663x         555x 1663x 1457x 1457x   206x 206x 206x 1663x   555x 596x 449x 449x 596x 596x               555x 380x 36x 36x 380x 380x                   555x  
import { emptyArray } from '../../utils/common/singleton.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;
}