Log anomaly detection: a statistical baseline
2026-09-23
Sentinel’s job is to take ten thousand log lines and hand back the ten worth reading. The obvious approach — a trained model — needs training data nobody has for a brand-new product, and an inference cost on every request. Here’s the baseline that ships instead: two techniques, both explainable, both free to run.
1. Keyword rules
Some lines are noteworthy regardless of how often they repeat. fatal, panic and out of memory are always worth a look, even if the same fatal error happens on every line. Three severity tiers — fatal, error, reliability — catch these before frequency ever gets consulted.
2. Frequency-based outliers
Most of what’s actually interesting isn’t a known keyword — it’s a line that just doesn’t look like the others. The approach:
- Normalize each line’s “shape” — replace timestamps, IPs, UUIDs and numbers with placeholders, so
request completed in 42msandrequest completed in 51mscollapse to the same bucket instead of counting as two different lines. - Count each shape’s frequency across the whole sample.
- Flag shapes below a rarity threshold — 2% of the sample, so it scales with volume instead of using a fixed count that’s meaningless on both a 50-line and a 50,000-line sample.
function normalizeShape(line: string): string {
return line
.replace(UUID_PATTERN, "<uuid>")
.replace(IP_PATTERN, "<ip>")
.replace(TIMESTAMP_PATTERN, "<timestamp>")
.replace(/\b\d+\b/g, "<n>")
.trim();
}What this catches, and what it doesn’t
It catches: known-bad keywords regardless of volume, and any line shape that’s genuinely rare in the sample — a one-off stack trace, an unusual diagnostic dump, a request pattern that only happened once. It doesn’t catch: an anomaly that happens to share a shape with common traffic (a slow request logged in exactly the same format as a fast one), or anything that needs semantic understanding of what the log actually means. That gap is exactly where a real model earns its cost later — this baseline is the floor, not the ceiling.
The real, unit-tested implementation is apps/sentinel/src/lib/analyze.ts in the NEXORA monorepo — this write-up describes what actually ships, not a simplified version of it.