// the idea
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.
The alert fires. You grab two minutes of the nginx access log and paste it in. The status field immediately reads:
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:
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.
// what it does
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.
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.
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%".
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.
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
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.
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
}
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.
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.
// what this demonstrates
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.
Parse the timestamp field and draw matches per minute, so a spike is visible as a shape rather than a count.
Pick a field and get a count table — status by path, program by host — the pivot that answers "is it one or everything".
Splunk SPL, Elastic KQL and a SQL WHERE clause for logs that landed in a warehouse.
Paste your own regex with named groups and it becomes a format — the LogQL export inherits it as a regexp stage.
The tool is the source — view the page and it's all there, MIT. loglab.html