All files / src/masker LogMasker.ts

100% Statements 19/19
100% Branches 6/6
100% Functions 3/3
100% Lines 19/19

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                                                                                                              164x 164x 164x     164x 164x 83x 83x 2x   81x         164x 164x 81x   164x 164x                             19x                                               6419x   6419x 2082x     6419x      
import type { AbstractMaskingInstruction } from "./MaskingInstruction.js";
 
/**
 * LogMasker — applies masking instructions to log messages before clustering.
 *
 * Maps 1:1 to Python `LogMasker` class (drain3/masking.py).
 *
 * The masker scans raw log messages for recognized patterns (IP addresses,
 * numbers, UUIDs, etc.) and replaces matched substrings with labeled
 * placeholders like `<IP>`, `<NUM>`, or `<*>`.
 *
 * Masking runs BEFORE the Drain algorithm so that variable parts of log
 * messages are normalized before tokenization and clustering. This
 * dramatically improves template quality.
 *
 * Key design decisions:
 * - Instructions are applied sequentially (not combined regex), matching
 *   Python behavior exactly. This ensures deterministic replacement order.
 * - Multiple instructions can share the same mask name. This allows, for
 *   example, two different IP regex patterns that both produce `<IP>`.
 * - The special mask name `"*"` indicates a catch-all parameter — any
 *   instruction with this name produces `<*>` in the output.
 */
 
export class LogMasker {
  /** All masking instructions in application order. */
  readonly instructions: readonly AbstractMaskingInstruction[];
 
  /** Prefix wrapper for masked parameters (e.g. `<` in `<IP>`). */
  readonly maskPrefix: string;
 
  /** Suffix wrapper for masked parameters (e.g. `>` in `<IP>`). */
  readonly maskSuffix: string;
 
  /** Distinct mask names across all instructions. */
  readonly maskNames: readonly string[];
 
  /** Pre-built map: maskName → AbstractMaskingInstruction[] for fast lookup. */
  private readonly _instructionsByName: ReadonlyMap<
    string,
    readonly AbstractMaskingInstruction[]
  >;
 
  /**
   * Creates a LogMasker.
   *
   * @param instructions - Ordered list of masking instructions (applied sequentially).
   * @param maskPrefix - Left delimiter for masked values. Default: `"<"`.
   * @param maskSuffix - Right delimiter for masked values. Default: `">"`.
   */
  constructor(
    instructions: readonly AbstractMaskingInstruction[],
    maskPrefix: string = "<",
    maskSuffix: string = ">",
  ) {
    this.instructions = instructions;
    this.maskPrefix = maskPrefix;
    this.maskSuffix = maskSuffix;
 
    // Build mask name → instructions map
    const byName = new Map<string, AbstractMaskingInstruction[]>();
    for (const inst of instructions) {
      const list = byName.get(inst.maskName);
      if (list) {
        list.push(inst);
      } else {
        byName.set(inst.maskName, [inst]);
      }
    }
 
    // Freeze each list for immutability
    const frozen = new Map<string, readonly AbstractMaskingInstruction[]>();
    for (const [name, list] of byName) {
      frozen.set(name, Object.freeze([...list]));
    }
    this._instructionsByName = frozen;
    this.maskNames = Object.freeze([...frozen.keys()]);
  }
 
  /**
   * Returns all instructions registered under the given mask name.
   *
   * Python: masker.instructions_by_mask_name(name)
   *
   * Useful for parameter extraction, where the engine needs to know
   * which regex patterns correspond to which mask name.
   *
   * @param name - The mask name to look up (e.g. "IP", "NUM").
   * @returns Frozen array of instructions, or empty array if none found.
   */
  instructionsByMaskName(name: string): readonly AbstractMaskingInstruction[] {
    return this._instructionsByName.get(name) ?? [];
  }
 
  /**
   * Applies all masking instructions to a log message.
   *
   * Python: LogMasker.mask(s) → str
   *
   * Instructions are applied sequentially in the order they were
   * registered. Each instruction's regex is matched globally, and all
   * matches are replaced with `<MASK_NAME>` (using the configured
   * prefix/suffix).
   *
   * Example:
   * ```
   * const masker = new LogMasker([NUM_MASK], "<", ">");
   * masker.mask("error code 42 occurred");
   * // → "error code <NUM> occurred"
   * ```
   *
   * @param content - The raw log message to mask.
   * @returns The masked log message with parameters replaced.
   */
  mask(content: string): string {
    let result = content;
 
    for (const instruction of this.instructions) {
      result = instruction.mask(result, this.maskPrefix, this.maskSuffix);
    }
 
    return result;
  }
}