← Back to Work

// 22 / DETECTION  ·  28 Oct 2026

Log Lab — Paste Logs, Click
to Filter, Take the Query

TypeBrowser tool · live
StackSingle HTML file · vanilla JS
Formatsnginx · JSON · syslog · logfmt · columns
Exportsgrep · jq · LogQL · awk

// the idea

The hard part of an incident is the first five minutes

Once you know what you're looking for, grep is enough. The problem is the stretch before that: an alert says "error rate up", you pull a few thousand lines of log, and you don't yet know which field matters. Is it one endpoint or the whole site? One client or everyone? Is the 500 count really higher, or did traffic just double? The tooling that answers those questions well — Loki, Splunk, Elastic — is a query language away, and at 3 a.m. the query language is the obstacle.

Log Lab is the scratchpad for that stretch. Paste the lines. It works out whether they're nginx, JSON, syslog, logfmt or just columns, breaks each one into named fields, and shows the top values in every field with a count. Click a value and it becomes a filter; the counts in every other field update to the filtered set, so the next click is obvious. When you've found the shape of the problem, the filter you built is already written out as a grep pipeline, a jq expression, a LogQL query and an awk one-liner — copy whichever matches where the real logs live.

A concrete example

The alert fires. You grab two minutes of the nginx access log and paste it in. The status field immediately reads:

log lab · fields180 lines · nginx
status                  path                        ip
200  ████████████ 112    /                      31   10.0.0.14      30
500  ██████        54    /static/app.js         18   10.0.0.9       30
404  ▌              6    /api/products          18   203.0.113.42   41
401  ▌              4    /api/checkout          58   198.51.100.7   27

Click 500. The path field re-counts against only the 500s and /api/checkout is 54 of 54. Click that. The ip field now shows one address responsible for three-quarters of them. Two clicks: it's one endpoint, and mostly one client hammering it — not a site-wide outage. That's the triage done, and the export panel has already written the next step:

log lab · exportthe same filter, four ways
grep   cat access.log | grep -Ei '\b500\b' | grep -Ei '\b\/api\/checkout\b'
LogQL  {job="nginx"} | pattern `<ip> - <user> [<ts>] "<method> <path> <_>" <status> <bytes> …`
         | status="500" | path="/api/checkout"
awk    awk '$9 == "500" && $7 == "/api/checkout"' access.log

Paste the LogQL into Grafana and the same question runs against the last six hours instead of the last two minutes. The pasted sample was for finding the question; the real system is for answering it at scale.

Try it — four built-in samples

Hit Load nginx sample and click 500 under status. Then try the JSON, syslog and logfmt samples and watch the export panel change shape to match the format.

Open Log Lab →

// what it does

Five panels

01

Log lines

A plain textarea with four sample loaders. Under it, the detected format, the line count and how many lines parsed cleanly, plus an override dropdown for when the detector guesses wrong on a format it hasn't seen — in which case "whitespace columns" still gives you something to click on.

02

Fields

One card per field, the top six values with counts and a bar. The counts are computed against the currently filtered lines, which is what makes clicking feel like drilling down rather than starting over. Click a value to add an equals filter.

03

Filter

The conditions you've built, each editable: field, operator (equals, not equals, contains, not contains, regex, numeric ≥ and ≤) and value. All ANDed. The match count updates live with a percentage, which is often the number you actually wanted — "30% of requests are failing" is a different incident from "0.3%".

04

The same filter, four ways

grep, jq, LogQL and awk, each with a copy button. They are honest about their limits: grep matches text anywhere in the line and is the loosest; jq refuses non-JSON input and awk refuses JSON, with a comment saying why; awk flags conditions it can't express by column. LogQL gets the right parser stage for the format — | json, | logfmt, a | pattern for nginx, a named-group | regexp for syslog.

05

Matching lines

The filtered rows as a table, levels and status codes colour-coded, every cell clickable to add it as a filter. Capped at 500 rows so a big paste stays responsive.

// how it was built

Build notes

Detection is "which parser accepts most of the first forty lines"

Each format has a parser with a cheap test and a fuller parse. Detection runs the tests over a sample of forty lines in a fixed order — nginx, JSON, syslog, logfmt — and takes the first that accepts at least 60% of them. The order matters: a logfmt line can look like columns and a JSON line contains plenty of key=value-shaped text, so the stricter formats go first. "Whitespace columns" is the fallback that never fails, which means the tool always gives you something to click on.

loglab.htmljavascript
function detect(lines){
  const sample = lines.slice(0, 40);
  for (const k of ['nginx','json','syslog','logfmt']){
    const hits = sample.filter(PARSERS[k].test).length;
    if (hits >= sample.length * 0.6) return k;
  }
  return 'columns';   // always something to click on
}

Query generation is per-target, not a common AST

The tempting design is one abstract filter tree compiled to four backends. The honest design is four small generators that each know their target's quirks: grep needs regex escaping and a -v for negation; jq needs tostring before contains because fields aren't always strings; LogQL wants (?i) inside the pattern for case-insensitivity; awk needs the column index, which only exists for columnar formats, and a +0 to force numeric comparison. Each generator is about ten lines and can be read on its own. The single-tree version would have been cleverer and worse.

The samples are generated, not pasted

Each sample is a small function that produces 140–180 lines with realistic distributions and a planted incident — the nginx one has checkout 500s concentrated on one IP, the syslog one has an SSH brute-force from one address, the JSON one has errors clustering on one service. They're generated so the file stays small and so the story the fields tell is the same every time someone loads the page.

Lines
~330 incl. CSS
Parsers
5, each ~6 lines
Query targets
grep · jq · LogQL · awk
Network calls
None

// what this demonstrates

Why this matters

The detection stack on this site — Grafana, Loki, Alloy, CrowdSec — is only useful if you can ask it questions quickly, and LogQL under pressure is not quick for most people. Log Lab is the on-ramp: find the question by clicking, then take the query to the system that has all the data. It is the kind of tool that gets opened fifty times and never thought about, which is the highest compliment a tool can get.

It's also a small argument about where AI does and doesn't belong. It would be easy to put a "ask the model what's wrong" box on this page. But the whole workflow here is deterministic, instant and legible — every count is exact and every exported query can be read before it's run. That's the right tool for triage. The model's turn comes afterwards, when you want the incident written up.

Where it could go next

Time histogram

Parse the timestamp field and draw matches per minute, so a spike is visible as a shape rather than a count.

Group by

Pick a field and get a count table — status by path, program by host — the pivot that answers "is it one or everything".

More exports

Splunk SPL, Elastic KQL and a SQL WHERE clause for logs that landed in a warehouse.

Custom patterns

Paste your own regex with named groups and it becomes a format — the LogQL export inherits it as a regexp stage.

Source

The tool is the source — view the page and it's all there, MIT. loglab.html