Skip to main content

Logvera

Five anomaly models vote on every log window and explain themselves

Published
27 Sept 2026
Reading Time
6 min read
PythonFastAPIscikit-learnNumPyNext.jsTypeScript

5

Anomaly models voting

60 min

Upload retention

41

Backend tests passing

How these numbers were measured: A bundled sample of one day of e-commerce Nginx traffic with planted incidents, not a production log.

The Problem With Reading Raw Logs

When something breaks in production, the evidence is usually sitting in a log file, but getting an answer out of it means grep, a lot of scrolling and a good memory for what normal looks like. Hosted observability platforms handle this well, but only if agents were installed and logs were shipping before the incident. For a one-off file, such as an auth.log from a server you suspect was brute-forced or an Nginx log from yesterday's outage, there was no quick path from raw text to an explanation.

I wanted a tool where you drop in a log and get back a health score, the unusual periods, the attacks and a plain-English root-cause hypothesis. It had to serve two readers at once: an engineer who wants patterns, line numbers and latency percentiles, and a manager who wants to know whether users were affected.

The Constraints I Set

Privacy came first, because nobody should upload production logs to a website that keeps them. I decided there would be no accounts, every upload would be encrypted under its own key, nothing would survive beyond 60 minutes, and no language model would ever see a log line that had not been redacted.

The second constraint was that it had to work on any text log with zero configuration, which ruled out per-format schemas as the main path and pushed me towards learning structure from the text itself. The third was cost: the backend runs as a single instance with one analysis worker, and model usage is capped by an in-process budget of 8,000 tokens per minute and 200,000 per day.

The last was resilience. Anything that could fail, including the whole AI layer, had to degrade to a useful report rather than an error.

How the Pipeline Fits Together

The frontend is a Next.js 16 app and the backend is FastAPI with a mounted volume. An upload is streamed through an archive unpacker straight into an encrypted vault, a job manager queues it (at most 8 waiting, with a 420-second limit per analysis), and a worker runs eight stages from parsing to indexing. Each analyser runs inside a guard, so a failure in, say, the geo lookup becomes a warning on the report instead of a failed job.

  • Intake: streams .log, .gz, .zip and .tar uploads of up to 100 MB, caps expanded text at 800 MB and 1.5 million lines, and skips binary archive members.
  • Vault: AES-GCM in 1 MiB chunks, with a per-analysis key derived by HKDF-SHA256 from a master key and a secret that only exists in the user's link; a sweeper shreds anything older than an hour.
  • Ingest: format and timestamp detection plus a Drain-style template miner that masks UUIDs, IPs, emails and numbers before clustering, with a similarity threshold of 0.45 and a cap of 4,000 templates.
  • ML: windowed features, a five-model anomaly vote, change points, Holt-Winters forecasts, K-Means and DBSCAN clustering, and association rules between error types.
  • Security: rules for brute force, credential stuffing, scanners, SQL injection, path traversal, port scans and secrets written to logs.
  • AI: an LLM for summaries and questions, and JEV by TypeSafe for structured decisions such as severity and whether an incident likely affected users.
  • Frontend: a 17-tab dashboard, live tail over WebSockets, a log comparison view and a ReportLab PDF export.

Why a Voting Ensemble Over a Single Anomaly Model

The simplest option was one Isolation Forest over time windows, since it is cheap and needs no labels. The problem is that every detector has a blind spot. Isolation Forest finds globally odd windows, Local Outlier Factor finds windows that are odd next to similar periods, PCA residuals on message-type counts (the Xu et al. method) catch a changed mix of messages at normal volume, MAD-based z-scores catch plain spikes, and a Markov model over template transitions catches events happening in an unusual order.

I run all five and flag a window only when at least two agree. A sensitivity setting sets each model's contamination at 2%, 4% or 8%, and the combined score averages each model's rank rather than its raw score, because the raw scales are not comparable. The trade-off is that a real anomaly visible to only one model gets missed, and I accepted that because one obvious false alarm costs trust in every other finding.

Agreement is not enough if the user cannot see why, so every incident lists the features whose z-score against the median exceeds 2.5 and the templates over-represented by a lift above 1.5. Users can mark an incident as a false alarm or confirmed, and windows within 0.35 of the median pairwise distance in feature space are then suppressed or promoted to match.

Why the LLM Sees Aggregates, Not Logs

The obvious design was to paste log lines into a large model and ask what went wrong. I rejected it for three reasons: logs contain secrets and personal data, a 100 MB file does not fit in a context window, and a model asked to summarise raw text will happily invent numbers. Instead every figure is computed deterministically first, and the model receives a compact JSON summary of that analysis.

Summaries come from gpt-oss-120b, with a system prompt that treats all log content as untrusted data and forbids claims the summary does not support. Questions go to gpt-oss-20b with retrieval: BM25 over the learned templates plus a search of the encrypted line index, passing at most 10 lines after redacting keys, tokens, card numbers, emails and the last two octets of every IP. Any line number the model cites that was not in the retrieved set is dropped. A rule-based summary built from the same numbers is always generated first, so if the model times out or the budget runs dry, the report still reads sensibly.

The Hardest Problem: Time Lies at the Edges of a Log

The trickiest failure mode was not in a model but in the windows fed to them. A log rarely starts or ends on a bucket boundary, so the last window usually covers only part of its interval and looks like volume collapsing. That artefact can resemble an outage to both the z-score and the Isolation Forest, which is enough votes to report a confident incident at the end of almost every file.

I fixed it by marking a window as partial when data starts more than 40% of the way into the first bucket, or covers less than 60% of the last one. Partial windows are still charted but can never be flagged, and the forecaster trims them before fitting so its projection does not start from an artificial drop. Bucket size comes from a fixed ladder between 1 second and 1 week, chosen so that any log produces no more than 180 windows.

Syslog added a second edge, because its timestamps have no year. I assume the current year, step back one if the first timestamps land more than a day in the future, and add 365 days whenever time jumps backwards by more than 150 days, so an auth.log spanning New Year still sorts correctly.

Results

Logvera is live at logvera.taaranjain.com. On the bundled e-commerce sample, one day of Nginx traffic in 34,492 lines (5.6 MB), it learned 84 message patterns, flagged 3 unusual periods and raised 8 security findings, including 588 rejected login requests from credential stuffing, 76 requests from an automated scanner and 72 sensitive-file probes that all failed. The strongest incident, a 30-minute stretch where average latency rose to 4.1 times normal alongside 500s and gateway timeouts on the cart API, was flagged by 4 of the 5 models, and JEV rated it 94% likely to have affected users.

The backend has 41 passing tests covering ingest, archive unpacking, the vault, security rules and the API.

What I Would Do Differently

I would build an evaluation harness before tuning the ensemble. The samples contain planted incidents, but my tests only assert that incidents exist, not that the right windows were caught, so choices such as the two-vote threshold and the 4.5 z-score limit were tuned by inspection rather than measured precision and recall.

I would also design for more than one machine from the start. The job queue lives in process, analyses sit on a single volume and the service runs as one replica with one worker. That kept it simple and cheap, but a second instance would need a shared queue and object storage before it could take any load.