Source

filters/PatternFilter.ts

import { LogFilter } from "./LogFilter";
import { LoggingConfig } from "../types";
import { final } from "../decorators";

/**
 * @description Replacement callback used to transform RegExp matches.
 * @summary Receives the matched substring and additional capture arguments, returning the replacement text that will be injected into the log message.
 * @typedef {function(string, any[]): string} ReplacementFunction
 * @memberOf module:Logging
 */
export type ReplacementFunction = (substring: string, ...args: any[]) => string;

/**
 * @description Filter that patches log messages using regular expressions.
 * @summary Applies a configured {@link RegExp} and replacement strategy to redact, mask, or restructure log payloads before they are emitted.
 * @param {RegExp} regexp - Expression used to detect sensitive or formatted text.
 * @param {string|ReplacementFunction} replacement - Replacement string or callback invoked for each match.
 * @class PatternFilter
 * @example
 * const filter = new PatternFilter(/token=[^&]+/g, "token=***");
 * const sanitized = filter.filter(config, "token=123&user=tom", []);
 * // sanitized === "token=***&user=tom"
 * @mermaid
 * sequenceDiagram
 *   participant Logger
 *   participant Filter as PatternFilter
 *   participant RegExp
 *   Logger->>Filter: filter(config, message, context)
 *   Filter->>RegExp: execute match()
 *   alt match found
 *     RegExp-->>Filter: captures
 *     Filter->>RegExp: replace(message, replacement)
 *     RegExp-->>Filter: transformed message
 *   else no match
 *     RegExp-->>Filter: null
 *   end
 *   Filter-->>Logger: sanitized message
 */
export class PatternFilter extends LogFilter {
  constructor(
    protected readonly regexp: RegExp,
    protected readonly replacement: string | ReplacementFunction
  ) {
    super();
  }

  /**
   * @description Ensures deterministic RegExp matching.
   * @summary Runs the configured expression, then resets its state so repeated invocations behave consistently.
   * @param {string} message - Message to test for matches.
   * @return {RegExpExecArray|null} Match result or null when no match is found.
   */
  @final()
  protected match(message: string) {
    const match = this.regexp.exec(message);
    this.regexp.lastIndex = 0;
    return match;
  }

  /**
   * @description Applies the replacement strategy to the incoming message.
   * @summary Executes {@link PatternFilter.match} and, when a match is found, replaces every occurrence using the configured replacement handler.
   * @param {LoggingConfig} config - Active logging configuration (unused but part of the filter contract).
   * @param {string} message - Message to be sanitized.
   * @param {string[]} context - Context entries associated with the log event.
   * @return {string} Sanitized log message.
   */
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  filter(config: LoggingConfig, message: string, context: string[]): string {
    const log = this.log.for(this.filter);
    const match = this.match(message);
    if (!match) return message;
    try {
      return message.replace(this.regexp, this.replacement as any);
    } catch (e: unknown) {
      log.error(`PatternFilter replacement error: ${e}`);
    }
    return "";
  }
}