All files / src/logger tee-logger.ts

98.87% Statements 88/89
95% Branches 19/20
100% Functions 51/51
98.55% Lines 68/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                                                                    29x 41x   21x 1x     20x 1x     19x 6x 16x 1x     15x         15x     6x     19x   19x 54x 34x     20x 20x 20x         19x 68x 9x 9x 14x 7x   14x       59x     19x 31x 31x 31x 62x 22x   62x       19x         23x 23x 23x 23x 46x 18x   46x       19x   19x   6x       20x 20x     4x 8x 10x 4x 14x 6x 6x 4x 4x 2x   4x 8x 4x 4x 6x 8x 4x 4x 4x         19x        
import type { Logger, LogLevel, LogMessage, LogTemplateStringsArray } from './definition.ts';
import type { MergeFinalizer } from './implementation/finalizer.ts';
import { asSingleFinalizer } from './implementation/finalizer.ts';
import { HIGHEST_SEVERITY_LOG_LEVEL, toLevelSeverity } from './implementation/level-utils.ts';
import { OFF_LOGGER } from './off-logger.ts';
 
/**
 * Creates a new logger that forwards all log entries to the provided loggers.
 *
 * The returned logger (a "tee") behaves like a standard logger but duplicates every log operation to each of the
 * underlying loggers.
 *
 * ### Log level behavior
 *
 * The tee's `level` is computed from the underlying loggers to be the less severe among them. It does not expose a
 * setter and does not attempt to keep child levels in sync. Changing the level of individual child loggers after
 * composing may lead to different filtering per destination, which is expected.
 *
 * @example
 *
 * ```ts
 * import { createConsoleErrorLogger, tee } from 'emitnlog/logger';
 * import { createFileLogger } from 'emitnlog/logger/node';
 *
 * const consoleLogger = createConsoleErrorLogger('info');
 * const fileLogger = createFileLogger('~/tmp/entries.log');
 * const logger = tee(consoleLogger, fileLogger);
 * logger.i`This will be logged to both console error and file`;
 * ```
 *
 * @param loggers One or more loggers to combine.
 * @returns A new logger that fans out logs to the provided loggers. Returns the 'off logger' if loggers is empty or the
 *   specified logger is loggers length is one.
 */
export const tee = <T extends readonly Logger[]>(...loggers: T): TeeLogger<T> => {
  loggers = loggers.filter((logger) => logger !== OFF_LOGGER) as unknown as T;
 
  if (!loggers.length) {
    return OFF_LOGGER as TeeLogger<T>;
  }
 
  if (loggers.length === 1) {
    return loggers[0] as TeeLogger<T>;
  }
 
  const computeLevel = (): LogLevel | 'off' => {
    const level = loggers.reduce<LogLevel | 'off'>((acc, logger) => {
      if (logger.level === 'off') {
        return acc;
      }
 
      Iif (acc === 'off') {
        return logger.level;
      }
 
      // Return the lowest level among all loggers
      return toLevelSeverity(acc) <= toLevelSeverity(logger.level) ? acc : logger.level;
    }, HIGHEST_SEVERITY_LOG_LEVEL);
 
    return level;
  };
 
  let pendingArgs: unknown[] = [];
 
  const consumePendingArgs = (): readonly unknown[] | undefined => {
    if (!pendingArgs.length) {
      return undefined;
    }
 
    const args = pendingArgs;
    pendingArgs = [];
    return args;
  };
 
  type TeeInput = LogMessage | Error | { error: unknown };
 
  const toTeeInputProvider = <I>(message: I): I => {
    if (typeof message === 'function') {
      let cache: unknown = toTeeInputProvider;
      return (() => {
        if (cache === toTeeInputProvider) {
          cache = (message as () => I)();
        }
        return cache;
      }) as I;
    }
 
    return message;
  };
 
  const runLogOperation = <I extends TeeInput>(input: I, operation: (logger: Logger, input: I) => void) => {
    const currentArgs = consumePendingArgs();
    input = toTeeInputProvider(input);
    loggers.forEach((logger) => {
      if (currentArgs) {
        logger.args(...currentArgs);
      }
      operation(logger, input);
    });
  };
 
  const runTemplateOperation = <I extends LogTemplateStringsArray>(
    input: I,
    values: unknown[],
    operation: (logger: Logger, input: I, values: unknown[]) => void,
  ) => {
    const currentArgs = consumePendingArgs();
    input = toTeeInputProvider(input);
    values = values.map(toTeeInputProvider);
    loggers.forEach((logger) => {
      if (currentArgs) {
        logger.args(...currentArgs);
      }
      operation(logger, input, values);
    });
  };
 
  const finalizer = asSingleFinalizer(...loggers);
 
  const teeLogger: Logger = {
    get level() {
      return computeLevel();
    },
 
    args: (...args) => {
      pendingArgs.push(...args);
      return teeLogger;
    },
 
    trace: (message, ...args) => runLogOperation(message, (logger, i) => logger.trace(i, ...args)),
    debug: (message, ...args) => runLogOperation(message, (logger, i) => logger.debug(i, ...args)),
    info: (message, ...args) => runLogOperation(message, (logger, i) => logger.info(i, ...args)),
    notice: (message, ...args) => runLogOperation(message, (logger, i) => logger.notice(i, ...args)),
    error: (input, ...args) => runLogOperation(input, (logger, i) => logger.error(i, ...args)),
    warning: (input, ...args) => runLogOperation(input, (logger, i) => logger.warning(i, ...args)),
    critical: (input, ...args) => runLogOperation(input, (logger, i) => logger.critical(i, ...args)),
    alert: (input, ...args) => runLogOperation(input, (logger, i) => logger.alert(i, ...args)),
    emergency: (input, ...args) => runLogOperation(input, (logger, i) => logger.emergency(i, ...args)),
    log: (level, message, ...args) => runLogOperation(message, (logger, i) => logger.log(level, i, ...args)),
 
    t: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.t(i, ...v)),
    d: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.d(i, ...v)),
    i: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.i(i, ...v)),
    n: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.n(i, ...v)),
    w: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.w(i, ...v)),
    e: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.e(i, ...v)),
    c: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.c(i, ...v)),
    a: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.a(i, ...v)),
    em: (strings, ...values) => runTemplateOperation(strings, values, (logger, i, v) => logger.em(i, ...v)),
 
    ...finalizer,
  };
 
  return teeLogger as TeeLogger<T>;
};
 
type TeeLogger<TLoggers extends readonly Logger[]> = MergeFinalizer<Logger, TLoggers>;