All files / src/masker MaskingInstruction.ts

100% Statements 10/10
100% Branches 8/8
100% Functions 3/3
100% Lines 10/10

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                                                                          73x 2x   71x                                                                   70x 70x 1x   68x 68x       2081x 2081x      
/**
 * Masking instructions — patterns for detecting and replacing variable
 * content in log messages before clustering.
 *
 * Maps 1:1 to Python drain3/masking.py:
 * - AbstractMaskingInstruction → AbstractMaskingInstruction (ABC)
 * - MaskingInstruction          → RegexMaskingInstruction / MaskingInstruction
 *
 * Drain3 supports pluggable masking backends via the abstract base class.
 * drain-ts matches this: `AbstractMaskingInstruction` allows custom
 * non-regex masking strategies (e.g., LLM-based semantic masking).
 */
 
/**
 * Abstract base for masking instructions.
 *
 * Python: `AbstractMaskingInstruction` (drain3/masking.py L9-L22)
 *
 * Subclasses implement `mask()` to replace variable content with
 * placeholder tokens. Built-in: `MaskingInstruction` (regex-based).
 * Custom implementations can use any strategy.
 *
 * @example
 * ```typescript
 * class SemanticMasker extends AbstractMaskingInstruction {
 *   mask(content: string, prefix: string, suffix: string): string {
 *     // Call LLM to identify PII, replace with <PII>
 *     return content.replace(/\b[A-Z][a-z]+ [A-Z][a-z]+\b/g, "<PERSON_NAME>");
 *   }
 * }
 * ```
 */
export abstract class AbstractMaskingInstruction {
  /** Symbolic name used in masked output (e.g. "IP", "NUM", "PERSON_NAME"). */
  readonly maskName: string;
 
  constructor(maskName: string) {
    if (!maskName || maskName.length === 0) {
      throw new Error("AbstractMaskingInstruction: maskName must be non-empty");
    }
    this.maskName = maskName;
  }
 
  /**
   * Apply the masking strategy to the given content.
   *
   * @param content - Raw log message content.
   * @param maskPrefix - Left delimiter for masked parameters (e.g. "<").
   * @param maskSuffix - Right delimiter for masked parameters (e.g. ">").
   * @returns Content with variable parts replaced by `<maskPrefix><maskName><maskSuffix>`.
   */
  abstract mask(content: string, maskPrefix: string, maskSuffix: string): string;
}
 
/**
 * Regex-based masking instruction.
 *
 * Python: `MaskingInstruction` / `RegexMaskingInstruction` (drain3/masking.py L25-L38)
 *
 * Uses a compiled regex to find and replace variable patterns.
 * The regex must use capturing groups to identify the portion to replace.
 */
export class MaskingInstruction extends AbstractMaskingInstruction {
  /** The raw regex pattern string. */
  readonly regexPattern: string;
 
  /** Pre-compiled regex for efficient replacement. */
  readonly compiledRegex: RegExp;
 
  /**
   * @param regexPattern - Regex pattern (without enclosing slashes or flags).
   * @param maskName - Symbolic name, used as `<maskName>` in masked output.
   */
  constructor(regexPattern: string, maskName: string) {
    super(maskName);
    if (!regexPattern || regexPattern.length === 0) {
      throw new Error("MaskingInstruction: regexPattern must be non-empty");
    }
    this.regexPattern = regexPattern;
    this.compiledRegex = new RegExp(regexPattern, "g");
  }
 
  mask(content: string, maskPrefix: string, maskSuffix: string): string {
    const replacement = maskPrefix + this.maskName + maskSuffix;
    return content.replace(this.compiledRegex, replacement);
  }
}