DEV Community

Joe Lin for BeGoodTool.com

Posted on

Log Multi-Regex Filter Dashboard

I built this after spending too much time switching between Ctrl+F, shell commands, and a spreadsheet just to answer a few basic incident questions: how many lines mention ERROR, which requests came from one IP range, and whether a fix reduced the number of warnings. The annoying part was not writing one regular expression. It was applying several of them to the same pasted log without losing the line numbers or accidentally counting the same line differently in each search.

The useful mental model here is not “search the log several times.” It is “turn each rule into a small, independent classifier, then compare the resulting sets of lines.” That distinction explains several implementation choices in this Vue component.

A Rule Is a Label, a Pattern, and a Color

The UI starts with three rules and gives every rule a label, a pattern, and a color:

const COLORS = ["#e53e3e", "#dd6b20", "#d69e2e", "#38a169", "#3182ce"];

const defaultRules = [
  { label: "ERROR", pattern: "ERROR|FATAL|Exception|error", color: COLORS[0] },
  { label: "WARN", pattern: "WARN|WARNING", color: COLORS[1] },
  { label: "INFO", pattern: "INFO", color: COLORS[3] },
];

const rules = ref(defaultRules.map((r) => ({ ...r })));
Enter fullscreen mode Exit fullscreen mode

The spread is small but important. It gives the reactive state fresh objects instead of letting edits mutate the template constants. New rules are capped at ten and receive a color based on their position. A blank pattern is ignored when analysis runs, so adding a row while experimenting does not automatically create an error.

Labels are presentation, not matching logic. If a label is empty, the result falls back to the pattern itself. That keeps an unlabeled rule useful in the summary and in the exported filename, while letting a human-readable label such as Specific IP make an incident report easier to scan.

Matching Lines Without Counting Occurrences

The analysis splits the input into lines once, then constructs a JavaScript RegExp for each non-empty rule:

const lines = logText.value.split("\n");

for (const rule of rules.value) {
  if (!rule.pattern.trim()) continue;

  let re;
  try {
    const flags = caseSensitive.value ? "g" : "gi";
    re = new RegExp(rule.pattern, flags);
  } catch {
    errorMessages.value.push(
      t("logRegexDashboard.errorInvalidRegex")
        .replace("{label}", rule.label || rule.pattern)
    );
    continue;
  }

  const matches = [];
  for (let i = 0; i < lines.length; i++) {
    re.lastIndex = 0;
    if (re.test(lines[i])) {
      matches.push({ lineNo: i + 1, line: lines[i] });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The g flag makes test() stateful: after a successful test, lastIndex can point past the beginning of the next test. Resetting re.lastIndex before every line makes the result deterministic. It is an easy detail to miss because the same expression often appears to work when tested only once.

Notice what this counts: matching lines, not every occurrence. A line containing ERROR ERROR ERROR contributes one hit to the ERROR rule. That is intentional for triage dashboards, where “how many log entries are affected?” is usually more useful than raw substring frequency. Separate rules can overlap; the ERROR and Specific IP counts are independent rather than mutually exclusive categories.

Invalid patterns do not abort the whole run. The try/catch records a localized error and continues with the other rules. That is a much friendlier failure mode when one experimental pattern has an unmatched [ but the remaining incident checks are valid.

Results Are Data You Can Inspect, Not Just Bars

Each result keeps the original line text and its one-based line number. The bar width is relative to the largest rule count:

const maxCount = computed(() => {
  if (!results.value.length) return 1;
  return Math.max(...results.value.map((r) => r.matches.length), 1);
});

width: maxCount > 0
  ? (res.matches.length / maxCount * 100) + "%"
  : "0%"
Enter fullscreen mode Exit fullscreen mode

That means the chart answers “which rule has the most matching lines?” while the expandable list answers “which exact entries caused that number?” The export path preserves both pieces:

const lines = [`=== ${res.label} (${res.pattern}) ===`, ""];
res.matches.forEach((m) => {
  lines.push(`[Line ${m.lineNo}] ${m.line}`);
});
const blob = new Blob([lines.join("\n")], {
  type: "text/plain;charset=utf-8",
});
Enter fullscreen mode Exit fullscreen mode

The browser creates a temporary object URL and downloads log-filter-${res.label}.txt. Nothing in the component sends the pasted text to a server; the work happens in the page.

The Practical Limits

This is a line-oriented filter, not a log parser. It does not understand JSON fields, multiline stack traces, timestamps, or log levels beyond whatever your regex expresses. A stack trace split over several lines can therefore be counted as several unrelated entries. Regex syntax is JavaScript syntax, including its escaping rules, and a pathological pattern can still be expensive.

The implementation also scans every line for every rule. Ten rules over tens of thousands of lines can make the browser feel slow, and the result list retains the matching text in memory. I would start with a broad rule, inspect the reduced set, and then run more specific patterns when working with a very large paste.

I turned this approach into a small free tool: Log Multi-Regex Filter Dashboard. It is useful when you need comparable per-rule line counts and the actual lines behind them, without first building a log pipeline.

Top comments (0)