Log Parser
by sloprun seed
2026-08-14
0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Paste raw nginx access logs and get a readable incident report: a regex-parsed table, a stacked status-code timeline, top clients and paths, and error-rate chips — all computed in your browser, nothing uploaded anywhere. It opens on 82 lines of real-shaped traffic hiding two stories, and the parser names both: a deploy that started returning 500s on /api/v2/checkout, and 45.155.205.233 walking twelve routes looking for .env and wp-login.php. It also accounts for its own failures: the three lines that don't match the regex are counted, quoted back with their line numbers, and folded into the stats rather than silently dropped. Click a finding to filter the table to that moment, then paste your own access log over the sample and watch every panel recompute.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Log Parser — sloprun.dev</title>
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
/* sloprun design tokens — inline this block into every demo (self-contained rule).
Identity: instrument-panel. Machine facts in mono; human words in sans.
Green is EARNED: only for "it ran / it worked" states, never decoration. */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end token block ──────────────────────────────────────────────────────── */
/* Status palette for charts. Derived from the tokens; 4xx is stepped away from
--danger so the amber/red pair clears the CVD + normal-vision separation
floors (validated: ΔE 24.1 normal, 17.0 protan in light mode). */
:root {
--s2: var(--run); /* 2xx — earned green, a genuine success state */
--s3: #7A828C; /* 3xx — deliberately recessive neutral */
--s4: #C98A00; /* 4xx — client error (chart fill) */
--s4-ink: #946400; /* 4xx as text — darkened to 4.7:1 on --accent-soft
(the table row-hover tint), not just on white */
--s5: var(--danger); /* 5xx — server error */
--danger-ink: #BE3530; /* danger as small text — raw --danger lands at
4.4:1 on --danger-soft, so 9-12px tags/labels
sitting on that tint use this instead */
--accent-ink: #A54A00; /* accent as small text — darkened so 11-13px UI
copy clears AA on --accent-soft / --danger-soft
tints, where raw --accent lands at 4.2:1 */
--danger-soft: #FCECEB;
--grid: rgba(26,29,33,.09);
--shadow: 0 6px 20px rgba(26,29,33,.14);
color-scheme: light; /* native select / search / scrollbars follow us */
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--s3: #8B939D; --s4: #F0B454; --s4-ink: #F0B454; --accent-ink: var(--accent);
--danger-ink: var(--danger);
--danger-soft: #2E1A19; --grid: rgba(232,230,225,.10);
--shadow: 0 6px 20px rgba(0,0,0,.45);
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--s3: #8B939D; --s4: #F0B454; --s4-ink: #F0B454; --accent-ink: var(--accent);
--danger-ink: var(--danger);
--danger-soft: #2E1A19; --grid: rgba(232,230,225,.10);
--shadow: 0 6px 20px rgba(0,0,0,.45);
color-scheme: dark;
}
:root[data-theme="light"] { color-scheme: light; }
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0; padding: 0 16px 56px;
line-height: 1.5; font-size: 15px;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1080px; margin: 0 auto; }
h1, h2, h3 { margin: 0; font-weight: 620; letter-spacing: -.01em; }
button, input, textarea, select { font: inherit; color: inherit; }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}
::selection { background: var(--accent-soft); color: var(--ink); }
/* ── header ───────────────────────────────────────────────────────────────── */
.rail {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 14px 0 12px; border-bottom: 1px solid var(--line);
font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
letter-spacing: .04em; text-transform: uppercase;
}
.rail .sep { opacity: .5; }
.rail .spacer { flex: 1 1 auto; }
.pulse {
width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft); flex: none;
}
header.hdr { padding: 26px 0 22px; }
.hdr h1 { font-size: clamp(26px, 5.5vw, 36px); }
.hdr .lede {
margin: 10px 0 0; max-width: 62ch; color: var(--muted); font-size: 15.5px;
}
.hdr .lede b { color: var(--ink); font-weight: 600; }
/* ── buttons ──────────────────────────────────────────────────────────────── */
.btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 8px 13px; border: 1px solid var(--line); border-radius: 8px;
background: var(--surface); color: var(--ink);
font-size: 13.5px; font-weight: 550; cursor: pointer;
transition: border-color .12s, background .12s, color .12s;
}
.btn:hover { border-color: var(--muted); }
.btn:active { transform: translateY(1px); }
.btn[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-ink); }
.btn.primary { background: var(--ink); color: var(--bg); border-color: var(--ink); }
.btn.primary:hover { opacity: .88; }
.btn.small { padding: 5px 10px; font-size: 12.5px; }
.btn:disabled { opacity: .42; cursor: not-allowed; }
.btn .k { font-family: var(--font-mono); font-size: 12px; }
select.btn { display: inline-block; }
/* ── receipt strip ────────────────────────────────────────────────────────── */
.receipt {
display: flex; align-items: baseline; gap: 10px 14px; flex-wrap: wrap;
margin: 18px 0 0; padding: 11px 14px;
border: 1px solid var(--line); border-left: 3px solid var(--run);
border-radius: var(--radius); background: var(--surface);
font-family: var(--font-mono); font-size: 12.5px; color: var(--muted);
}
.receipt.bad { border-left-color: var(--danger); }
.receipt .ok { color: var(--run); font-weight: 700; }
.receipt.bad .ok { color: var(--danger); }
.receipt .v { color: var(--ink); }
.receipt #rcpText { flex: 1 1 0; min-width: 0; }
/* ── findings ─────────────────────────────────────────────────────────────── */
.finds { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.find {
display: flex; align-items: flex-start; gap: 9px; width: 100%;
padding: 10px 12px; text-align: left; cursor: pointer;
border: 1px solid var(--line); border-left: 3px solid var(--danger);
border-radius: var(--radius); background: var(--surface);
font-family: var(--font-mono); font-size: 12.5px; line-height: 1.55; color: var(--muted);
transition: border-color .12s, background .12s;
}
.find:hover { background: var(--danger-soft); }
.find .ic { color: var(--danger-ink); flex: none; font-weight: 700; }
.find .tx { flex: 1 1 0; min-width: 0; } /* basis 0 keeps the icon on the same line */
.find b { color: var(--ink); font-weight: 700; }
.find .go {
margin-left: auto; padding-left: 10px; color: var(--accent-ink);
white-space: nowrap; font-size: 11.5px;
}
.find.ok { border-left-color: var(--run); cursor: default; }
.find.ok:hover { background: var(--surface); }
.find.ok .ic { color: var(--run); }
@media (max-width: 620px) {
.find { flex-wrap: wrap; }
.find .go { margin-left: 0; padding: 4px 0 0 24px; width: 100%; }
}
/* ── layout ───────────────────────────────────────────────────────────────── */
.card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 16px;
}
section { margin-top: 20px; }
.sec-head {
display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
margin-bottom: 12px;
}
.sec-head h2 { font-size: 14.5px; }
.sec-head .note {
font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
letter-spacing: .03em;
}
.sec-head .grow { flex: 1 1 auto; }
/* ── stat chips ───────────────────────────────────────────────────────────── */
.chips { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
@media (min-width: 560px) { .chips { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 780px) { .chips { grid-template-columns: repeat(4, 1fr); } }
.chip {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 12px 13px 11px;
position: relative; overflow: hidden;
}
.chip::before {
content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--line);
}
.chip .val {
font-family: var(--font-mono); font-size: 25px; font-weight: 600;
letter-spacing: -.02em; line-height: 1.15; font-variant-numeric: tabular-nums;
display: block; word-break: break-word;
}
.chip .val .u { font-size: 14px; color: var(--muted); margin-left: 4px; font-weight: 500; }
.chip .lbl {
display: block; margin-top: 3px;
font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .07em;
text-transform: uppercase; color: var(--muted);
}
.chip.good::before { background: var(--run); }
.chip.good .val { color: var(--run); }
.chip.warn::before { background: var(--s4); }
.chip.warn .val { color: var(--s4-ink); }
.chip.bad::before { background: var(--danger); }
.chip.bad .val { color: var(--danger); }
/* ── chart ────────────────────────────────────────────────────────────────── */
.chart-wrap { position: relative; }
canvas#timeline {
display: block; width: 100%; height: 208px; border-radius: 6px;
cursor: crosshair; touch-action: pan-y;
}
canvas#timeline:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.tip {
position: absolute; z-index: 5; pointer-events: none;
background: var(--surface); border: 1px solid var(--line);
border-radius: 8px; padding: 8px 10px; min-width: 132px;
box-shadow: var(--shadow);
font-family: var(--font-mono); font-size: 11.5px; line-height: 1.65;
opacity: 0; transform: translateY(2px); transition: opacity .1s;
}
.tip.on { opacity: 1; }
.tip .t-h { color: var(--ink); font-weight: 700; border-bottom: 1px solid var(--line); padding-bottom: 4px; margin-bottom: 4px; }
.tip .t-r { display: flex; align-items: center; gap: 7px; color: var(--muted); }
.tip .t-r .n { margin-left: auto; color: var(--ink); font-variant-numeric: tabular-nums; }
.sw { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.legend { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.lg {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 9px; border: 1px solid var(--line); border-radius: 999px;
background: transparent; cursor: pointer;
font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
transition: border-color .12s, background .12s;
}
.lg:hover { border-color: var(--muted); }
.lg[aria-pressed="true"] { border-color: currentColor; background: var(--accent-soft); }
.lg .n { color: var(--ink); font-variant-numeric: tabular-nums; }
/* ── two-up top lists ─────────────────────────────────────────────────────── */
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.rank { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.rank li { position: relative; }
.rank button {
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px;
width: 100%; padding: 7px 9px; border: 0; border-radius: 6px;
background: transparent; cursor: pointer; text-align: left; position: relative;
overflow: hidden;
}
.rank button:hover { background: var(--accent-soft); }
.rank button[aria-pressed="true"] { background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent); }
.rank .bar {
position: absolute; left: 0; top: 0; bottom: 0; z-index: 0;
background: var(--grid); border-radius: 0 5px 5px 0;
}
.rank .k, .rank .m { position: relative; z-index: 1; }
.rank .k {
font-family: var(--font-mono); font-size: 12.5px; color: var(--ink);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rank .m {
font-family: var(--font-mono); font-size: 12px; color: var(--muted);
font-variant-numeric: tabular-nums; display: flex; align-items: center; gap: 7px;
}
.tag {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .06em;
text-transform: uppercase; padding: 2px 5px; border-radius: 4px;
border: 1px solid currentColor; white-space: nowrap;
}
.tag.scan { color: var(--danger-ink); background: var(--danger-soft); }
.tag.crawl { color: var(--muted); }
.tag.auto { color: var(--muted); }
/* ── filters + table ──────────────────────────────────────────────────────── */
.tools { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
.search {
flex: 1 1 190px; min-width: 0;
padding: 8px 11px; border: 1px solid var(--line); border-radius: 8px;
background: var(--bg); font-family: var(--font-mono); font-size: 12.5px;
}
.search::placeholder { color: var(--muted); opacity: .8; }
.fchips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.fchip {
display: inline-flex; align-items: center; gap: 6px;
padding: 3px 6px 3px 9px; border-radius: 999px;
border: 1px solid var(--accent); background: var(--accent-soft); color: var(--accent-ink);
font-family: var(--font-mono); font-size: 11.5px; cursor: pointer;
}
.fchip .x { font-size: 13px; line-height: 1; opacity: .75; }
.fchip:hover .x { opacity: 1; }
.tscroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 8px; }
table { border-collapse: collapse; width: 100%; min-width: 640px; font-size: 13px; }
thead th {
position: sticky; top: 0; z-index: 2;
background: var(--surface); border-bottom: 1px solid var(--line);
text-align: left; padding: 0; white-space: nowrap;
}
thead th button, thead th .th {
width: 100%; display: flex; align-items: center; gap: 5px;
padding: 9px 10px; background: none; border: 0; cursor: pointer;
font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .07em;
text-transform: uppercase; color: var(--muted); text-align: left;
}
thead th .th { cursor: default; }
thead th button:hover { color: var(--ink); }
thead th button:hover .ar, thead th button:focus-visible .ar { opacity: .5; }
thead th .ar { opacity: 0; font-size: 9px; }
thead th[aria-sort] button .ar { opacity: 1; color: var(--accent-ink); }
tbody td {
padding: 7px 10px; border-bottom: 1px solid var(--line);
font-family: var(--font-mono); font-size: 12.5px; vertical-align: top;
white-space: nowrap;
}
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--accent-soft); }
td.num { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); }
td.path { max-width: 360px; overflow: hidden; text-overflow: ellipsis; color: var(--ink); }
td.dim { color: var(--muted); }
.st { display: inline-flex; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
.st .sw { border-radius: 2px; }
.st.c5 { color: var(--danger); font-weight: 700; }
.st.c4 { color: var(--s4-ink); font-weight: 600; }
.st.c2 { color: var(--run); }
.st.c3 { color: var(--muted); }
.empty { padding: 26px 14px; text-align: center; color: var(--muted); font-size: 13.5px; }
.pager {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
margin-top: 11px;
}
.pager .pos {
font-family: var(--font-mono); font-size: 12px; color: var(--muted);
font-variant-numeric: tabular-nums;
}
.pager .grow { flex: 1 1 auto; }
/* ── input panel ──────────────────────────────────────────────────────────── */
textarea#src {
width: 100%; height: 190px; resize: vertical;
padding: 12px; border: 1px solid var(--line); border-radius: 8px;
background: var(--bg); color: var(--ink);
font-family: var(--font-mono); font-size: 12px; line-height: 1.6;
white-space: pre; overflow-wrap: normal; overflow-x: auto; tab-size: 2;
}
details.skipped { margin-top: 12px; }
details.skipped summary {
cursor: pointer; font-family: var(--font-mono); font-size: 12px; color: var(--muted);
padding: 6px 0; list-style-position: inside;
}
details.skipped summary:hover { color: var(--ink); }
details.skipped[open] summary { color: var(--ink); }
.skiplist { margin: 6px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
.skiplist li {
border-left: 3px solid var(--danger); background: var(--danger-soft);
border-radius: 0 6px 6px 0; padding: 7px 10px;
font-family: var(--font-mono); font-size: 11.5px; color: var(--ink);
white-space: pre-wrap; overflow-wrap: anywhere;
}
.skiplist .ln { color: var(--danger-ink); margin-right: 10px; }
.hint { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); margin: 9px 0 0; }
.hint code { color: var(--accent-ink); }
.status-msg { font-family: var(--font-mono); font-size: 12px; color: var(--muted); }
.status-msg.ok { color: var(--run); }
footer.foot {
margin-top: 34px; padding-top: 14px; border-top: 1px solid var(--line);
font-family: var(--font-mono); font-size: 11.5px; color: var(--muted);
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
}
footer.foot .spacer { flex: 1 1 auto; }
.sr {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (max-width: 820px) { .two { grid-template-columns: 1fr; gap: 16px; } }
@media (max-width: 520px) {
body { padding: 0 12px 44px; }
.chip .val { font-size: 21px; }
.hdr { padding: 20px 0 16px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .001ms !important; transition-duration: .001ms !important;
}
.btn:active { transform: none; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="rail">
<span class="pulse" aria-hidden="true"></span>
<span>nginx access.log</span>
<span class="sep">·</span>
<span>combined format</span>
<span class="sep">·</span>
<span>parsed in your browser</span>
<span class="spacer"></span>
<button class="btn small" id="themeBtn" type="button" title="Toggle light / dark">
<span aria-hidden="true">◐</span> <span id="themeLbl">Dark</span>
</button>
</div>
<header class="hdr">
<h1>Log Parser</h1>
<p class="lede">
Raw web-server logs in, a readable incident report out. This one is loaded with
<b>82 lines of nginx traffic</b> that hide a bad deploy and a vulnerability scanner —
see how fast you can spot both. Then paste your own; nothing leaves this tab.
</p>
</header>
<div class="receipt" id="receipt" role="status">
<span class="ok" id="rcpIcon">✓</span>
<span id="rcpText">parsing…</span>
</div>
<div class="finds" id="finds" role="group" aria-label="What the parser found"></div>
<section aria-labelledby="h-stats">
<div class="sec-head">
<h2 id="h-stats">At a glance</h2>
<span class="note" id="statNote"></span>
</div>
<div class="chips" id="chips"></div>
</section>
<section aria-labelledby="h-time">
<div class="sec-head">
<h2 id="h-time">Status codes over time</h2>
<span class="grow"></span>
<span class="note" id="bucketNote"></span>
</div>
<div class="card">
<div class="chart-wrap">
<canvas id="timeline" tabindex="0" role="img"
aria-describedby="tlHelp" aria-label="Stacked bar chart of requests per time bucket, split by status class"></canvas>
<div class="tip" id="tip" aria-hidden="true"></div>
</div>
<div class="legend" id="legend"></div>
<p class="hint" id="tlHelp">
Hover or focus the chart and use <code>← →</code> to walk the buckets;
<code>Enter</code> filters the table to that moment, <code>Esc</code> clears.
Click a status pill to filter by class.
</p>
<p class="sr" id="tlLive" role="status" aria-live="polite"></p>
</div>
</section>
<section class="two" aria-label="Top talkers">
<div>
<div class="sec-head"><h2>Top clients</h2><span class="note">by requests</span></div>
<div class="card"><ul class="rank" id="topIps"></ul></div>
</div>
<div>
<div class="sec-head"><h2>Top paths</h2><span class="note">by requests</span></div>
<div class="card"><ul class="rank" id="topPaths"></ul></div>
</div>
</section>
<section aria-labelledby="h-table">
<div class="sec-head">
<h2 id="h-table">Parsed requests</h2>
<span class="grow"></span>
<span class="note" id="tblCount"></span>
</div>
<div class="card">
<div class="tools">
<label class="sr" for="q">Filter requests</label>
<input class="search" id="q" type="search" placeholder="filter: ip, path, status, agent…" autocomplete="off" spellcheck="false">
<button class="btn small" id="csvBtn" type="button">Copy CSV</button>
<span class="status-msg" id="csvMsg" role="status"></span>
</div>
<div class="fchips" id="fchips"></div>
<div class="tscroll" id="tscroll" tabindex="0" role="region" aria-label="Parsed requests table (scrolls sideways)">
<table id="tbl">
<caption class="sr">Parsed log lines, paginated</caption>
<thead>
<tr>
<th scope="col"><span class="th">#</span></th>
<th scope="col" data-sort="ts"><button type="button">time <span class="ar">▼</span></button></th>
<th scope="col" data-sort="ip"><button type="button">client <span class="ar">▼</span></button></th>
<th scope="col"><span class="th">method</span></th>
<th scope="col" data-sort="path"><button type="button">path <span class="ar">▼</span></button></th>
<th scope="col" data-sort="status"><button type="button">status <span class="ar">▼</span></button></th>
<th scope="col" data-sort="bytes"><button type="button">bytes <span class="ar">▼</span></button></th>
<th scope="col"><span class="th">agent</span></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div id="tblEmpty" class="empty" hidden></div>
<div class="pager">
<button class="btn small" id="prev" type="button">← Prev</button>
<button class="btn small" id="next" type="button">Next →</button>
<span class="pos" id="pos"></span>
<span class="grow"></span>
<label class="pos" for="per">rows
<select id="per" class="btn small" style="padding:4px 6px">
<option>12</option><option>25</option><option>50</option>
</select>
</label>
</div>
</div>
</section>
<section aria-labelledby="h-src">
<div class="sec-head">
<h2 id="h-src">Your logs</h2>
<span class="grow"></span>
<span class="note">re-parses as you type</span>
</div>
<div class="card">
<label class="sr" for="src">Raw log lines</label>
<textarea id="src" spellcheck="false" autocomplete="off" wrap="off"></textarea>
<div class="tools" style="margin:11px 0 0">
<button class="btn primary" id="parseBtn" type="button">Parse</button>
<button class="btn" id="sampleBtn" type="button">Restore sample</button>
<button class="btn" id="clearBtn" type="button">Clear</button>
</div>
<p class="hint">
Reads combined <em>and</em> common log format:
<code>ip - user [10/Aug/2026:13:52:04 +0000] "GET /path HTTP/1.1" 200 5821 "ref" "agent"</code>
</p>
<details class="skipped" id="skipBox" hidden>
<summary id="skipSum"></summary>
<ul class="skiplist" id="skipList"></ul>
</details>
</div>
</section>
<footer class="foot">
<span>demo · sloprun.dev</span>
<span class="spacer"></span>
<span id="perf"></span>
</footer>
</div>
<script>
(function () {
"use strict";
/* ── sample data ───────────────────────────────────────────────────────── */
var SAMPLE = `203.0.113.41 - - [10/Aug/2026:13:52:04 +0000] "GET / HTTP/1.1" 200 5821 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
203.0.113.41 - - [10/Aug/2026:13:52:05 +0000] "GET /static/app.4f2a1c.js HTTP/1.1" 200 184203 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
203.0.113.41 - - [10/Aug/2026:13:52:05 +0000] "GET /static/app.9b3d7e.css HTTP/1.1" 200 28114 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
203.0.113.41 - - [10/Aug/2026:13:52:06 +0000] "GET /assets/hero-dark.webp HTTP/1.1" 200 96410 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:52:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
198.51.100.77 - - [10/Aug/2026:13:52:18 +0000] "GET /pricing HTTP/1.1" 200 7402 "https://news.ycombinator.com/" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
198.51.100.77 - - [10/Aug/2026:13:52:19 +0000] "GET /static/app.4f2a1c.js HTTP/1.1" 304 0 "https://sloprun.dev/pricing" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
203.0.113.41 - - [10/Aug/2026:13:52:26 +0000] "GET /api/v2/orders?limit=25 HTTP/1.1" 200 14882 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:52:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
66.249.66.1 - - [10/Aug/2026:13:52:45 +0000] "GET /blog/scaling-postgres-to-1tb HTTP/1.1" 200 31204 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
192.0.2.204 - - [10/Aug/2026:13:52:52 +0000] "GET /docs/quickstart HTTP/1.1" 200 19338 "https://www.google.com/" "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
192.0.2.204 - - [10/Aug/2026:13:52:55 +0000] "GET /static/app.9b3d7e.css HTTP/1.1" 304 0 "https://sloprun.dev/docs/quickstart" "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
10.0.0.14 - - [10/Aug/2026:13:53:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
198.51.100.77 - - [10/Aug/2026:13:53:14 +0000] "GET /login HTTP/1.1" 200 4118 "https://sloprun.dev/pricing" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
198.51.100.77 - - [10/Aug/2026:13:53:18 +0000] "POST /login HTTP/1.1" 302 0 "https://sloprun.dev/login" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
198.51.100.77 - - [10/Aug/2026:13:53:20 +0000] "GET /dashboard HTTP/1.1" 200 22940 "https://sloprun.dev/login" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
203.0.113.12 - - [10/Aug/2026:13:53:32 +0000] "GET / HTTP/1.1" 200 5821 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:53:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:13:53:41 +0000] "GET /favicon.ico HTTP/1.1" 404 548 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
192.0.2.204 - - [10/Aug/2026:13:53:56 +0000] "GET /api/v2/projects HTTP/1.1" 200 8823 "https://sloprun.dev/docs/quickstart" "curl/8.4.0"
10.0.0.14 - - [10/Aug/2026:13:54:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
66.249.66.1 - - [10/Aug/2026:13:54:15 +0000] "GET /sitemap.xml HTTP/1.1" 200 4402 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
203.0.113.41 - - [10/Aug/2026:13:54:24 +0000] "POST /api/v2/checkout HTTP/1.1" 200 612 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:54:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
198.51.100.77 - - [10/Aug/2026:13:54:50 +0000] "GET /api/v2/orders?limit=25 HTTP/1.1" 200 14882 "https://sloprun.dev/dashboard" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
192.0.2.204 - - [10/Aug/2026:13:54:59 +0000] "GET /docs/api-reference HTTP/1.1" 200 44201 "https://sloprun.dev/docs/quickstart" "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
10.0.0.14 - - [10/Aug/2026:13:55:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:13:55:23 +0000] "GET /pricing HTTP/1.1" 200 7402 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
45.155.205.233 - - [10/Aug/2026:13:55:30 +0000] "GET /wp-login.php HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:55:31 +0000] "GET /wp-admin/setup-config.php HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
2026/08/10 13:55:42 [error] 3821#3821: *1204 connect() failed (111: Connection refused) while connecting to upstream
45.155.205.233 - - [10/Aug/2026:13:55:31 +0000] "GET /.env HTTP/1.1" 403 199 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:55:32 +0000] "GET /.git/config HTTP/1.1" 403 199 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:55:32 +0000] "GET /phpmyadmin/index.php HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:55:33 +0000] "POST /xmlrpc.php HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
10.0.0.14 - - [10/Aug/2026:13:55:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
45.155.205.233 - - [10/Aug/2026:13:55:45 +0000] "GET /admin/config.php HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:55:46 +0000] "GET /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:55:47 +0000] "GET /.aws/credentials HTTP/1.1" 403 199 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:55:48 +0000] "GET /backup.sql HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
203.0.113.41 - - [10/Aug/2026:13:55:55 +0000] "GET /dashboard HTTP/1.1" 200 22940 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:56:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:13:56:16 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/pricing" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
203.0.113.12 - - [10/Aug/2026:13:56:19 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/pricing" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
198.51.100.77 - - [10/Aug/2026:13:56:25 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/dashboard" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
192.0.2.204 - - [10/Aug/2026:13:56:30 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "-" "curl/8.4.0"
10.0.0.14 - - [10/Aug/2026:13:56:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:13:56:41 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/pricing" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
203.0.113.41 - - [10/Aug/2026:13:56:45 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
198.51.100.77 - - [10/Aug/2026:13:56:52 +0000] "GET /api/v2/orders?limit=25 HTTP/1.1" 502 167 "https://sloprun.dev/dashboard" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
192.0.2.204 - - [10/Aug/2026:13:56:58 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "-" "curl/8.4.0"
203.0.113.41 - - [10/Aug/2026:13:57:05 +0000] "GET /api/v2/orders?limit=25 HTTP/1.1" 503 290 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:57:07 +0000] "GET /health HTTP/1.1" 500 1204 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:13:57:15 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/pricing" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
66.249.66.1 - - [10/Aug/2026:13:57:22 +0000] "GET /blog/index HTTP/1.1" 200 12880 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
198.51.100.77 - - [10/Aug/2026:13:57:29 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/dashboard" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
10.0.0.14 - - [10/Aug/2026:13:57:37 +0000] "GET /health HTTP/1.1" 500 1204 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:13:58:19 +0000] "POST /api/v2/checkout HTTP/1.1" 5
192.0.2.204 - - [10/Aug/2026:13:57:44 +0000] "GET /status HTTP/1.1" 200 1044 "-" "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
203.0.113.41 - - [10/Aug/2026:13:57:56 +0000] "POST /api/v2/checkout HTTP/1.1" 500 1204 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:58:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:13:58:15 +0000] "POST /api/v2/checkout HTTP/1.1" 200 612 "https://sloprun.dev/pricing" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
198.51.100.77 - - [10/Aug/2026:13:58:23 +0000] "GET /api/v2/orders?limit=25 HTTP/1.1" 200 14882 "https://sloprun.dev/dashboard" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
203.0.113.41 - - [10/Aug/2026:13:58:28 +0000] "POST /api/v2/checkout HTTP/1.1" 200 612 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:58:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
192.0.2.204 - - [10/Aug/2026:13:58:45 +0000] "POST /api/v2/checkout HTTP/1.1" 201 344 "-" "curl/8.4.0"
203.0.113.12 - - [10/Aug/2026:13:58:56 +0000] "GET /dashboard HTTP/1.1" 200 22940 "https://sloprun.dev/pricing" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
10.0.0.14 - - [10/Aug/2026:13:59:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
45.155.205.233 - - [10/Aug/2026:13:59:14 +0000] "GET /actuator/env HTTP/1.1" 404 548 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
45.155.205.233 - - [10/Aug/2026:13:59:15 +0000] "GET /api/v1/../../etc/passwd HTTP/1.1" 400 166 "-" "Mozilla/5.0 (X11; Linux x86_64) zgrab/0.x"
66.249.66.1 - - [10/Aug/2026:13:59:22 +0000] "GET /blog/scaling-postgres-to-1tb HTTP/1.1" 304 0 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
198.51.100.77 - - [10/Aug/2026:13:59:31 +0000] "GET /settings/billing HTTP/1.1" 200 9611 "https://sloprun.dev/dashboard" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
10.0.0.14 - - [10/Aug/2026:13:59:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.41 - - [10/Aug/2026:13:59:45 +0000] "GET /api/v2/projects HTTP/1.1" 200 8823 "https://sloprun.dev/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
192.0.2.204 - - [10/Aug/2026:13:59:54 +0000] "GET /docs/quickstart HTTP/1.1" 304 0 "https://www.google.com/" "Mozilla/5.0 (X11; Linux x86_64; rv:127.0) Gecko/20100101 Firefox/127.0"
10.0.0.14 - - [10/Aug/2026:14:00:07 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.12 - - [10/Aug/2026:14:00:13 +0000] "GET /assets/hero-dark.webp HTTP/1.1" 200 96410 "https://sloprun.dev/dashboard" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
198.51.100.77 - - [10/Aug/2026:14:00:21 +0000] "POST /api/v2/checkout HTTP/1.1" 200 612 "https://sloprun.dev/dashboard" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Mobile/15E148 Safari/604.1"
87.236.176.9 - - [10/Aug/2026:14:00:32 +0000] "GET /robots.txt HTTP/1.1" 200 241 "-" "python-requests/2.31.0"
### rotated by logrotate at 14:00:00 ###
10.0.0.14 - - [10/Aug/2026:14:00:37 +0000] "GET /health HTTP/1.1" 200 17 "-" "kube-probe/1.29"
203.0.113.41 - - [10/Aug/2026:14:00:45 +0000] "GET /logout HTTP/1.1" 302 0 "https://sloprun.dev/dashboard" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"`;
/* ── theme: prefers-color-scheme by default, platform postMessage wins ─── */
var root = document.documentElement;
var themeBtn = document.getElementById("themeBtn");
var themeLbl = document.getElementById("themeLbl");
function effectiveTheme() {
var set = root.getAttribute("data-theme");
if (set === "dark" || set === "light") return set;
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function applyTheme(t) {
if (t === "dark" || t === "light") root.setAttribute("data-theme", t);
else root.removeAttribute("data-theme");
themeLbl.textContent = effectiveTheme() === "dark" ? "Light" : "Dark";
drawChart();
}
themeBtn.addEventListener("click", function () {
applyTheme(effectiveTheme() === "dark" ? "light" : "dark");
});
window.addEventListener("message", function (e) {
var d = e && e.data;
if (d && d.type === "sloprun:theme") applyTheme(d.theme);
});
if (window.matchMedia) {
var mq = window.matchMedia("(prefers-color-scheme: dark)");
var onScheme = function () { themeLbl.textContent = effectiveTheme() === "dark" ? "Light" : "Dark"; drawChart(); };
if (mq.addEventListener) mq.addEventListener("change", onScheme);
else if (mq.addListener) mq.addListener(onScheme);
}
/* ── parsing ───────────────────────────────────────────────────────────── */
// ip ident user [time] "METHOD path proto" status bytes ["referer" "agent"]
var LINE_RE = /^(\S+)\s+(\S+)\s+(\S+)\s+\[([^\]]+)\]\s+"([A-Z]{3,8})\s+(\S+)(?:\s+([^"]*))?"\s+(\d{3})\s+(\d+|-)(?:\s+"([^"]*)"\s+"([^"]*)")?\s*$/;
var TS_RE = /^(\d{1,2})\/([A-Za-z]{3})\/(\d{4}):(\d{2}):(\d{2}):(\d{2})(?:\s+([+-])(\d{2})(\d{2}))?$/;
var MONTHS = { jan:0, feb:1, mar:2, apr:3, may:4, jun:5, jul:6, aug:7, sep:8, oct:9, nov:10, dec:11 };
function parseTs(raw) {
var m = TS_RE.exec(raw.trim());
if (!m) return null;
var mo = MONTHS[m[2].toLowerCase()];
if (mo === undefined) return null;
var t = Date.UTC(+m[3], mo, +m[1], +m[4], +m[5], +m[6]);
if (m[7]) {
var off = (+m[8] * 60 + +m[9]) * 60000;
t += (m[7] === "+" ? -off : off);
}
return t;
}
function agentName(ua) {
if (!ua || ua === "-") return "—";
var u = ua.toLowerCase();
if (u.indexOf("googlebot") > -1) return "Googlebot";
if (u.indexOf("bingbot") > -1) return "Bingbot";
if (u.indexOf("zgrab") > -1) return "zgrab";
if (u.indexOf("kube-probe") > -1) return "kube-probe";
if (u.indexOf("curl/") > -1) return "curl";
if (u.indexOf("wget") > -1) return "wget";
if (u.indexOf("python-requests") > -1) return "python";
if (u.indexOf("bot") > -1 || u.indexOf("spider") > -1 || u.indexOf("crawl") > -1) return "bot";
if (u.indexOf("edg/") > -1) return "Edge";
if (u.indexOf("chrome/") > -1) return "Chrome";
if (u.indexOf("firefox/") > -1) return "Firefox";
if (u.indexOf("iphone") > -1 || u.indexOf("ipad") > -1) return "Safari iOS";
if (u.indexOf("safari/") > -1) return "Safari";
return ua.split("/")[0].slice(0, 14);
}
function isRobot(ua) {
var u = (ua || "").toLowerCase();
return /bot|spider|crawl|zgrab|curl\/|wget|python-requests|scan|kube-probe|http-client/.test(u);
}
function parse(text) {
var raw = String(text).split(/\r?\n/);
var rows = [], bad = [], blank = 0;
for (var i = 0; i < raw.length; i++) {
var line = raw[i];
if (!line.trim()) { blank++; continue; }
var m = LINE_RE.exec(line.trim());
if (!m) { bad.push({ n: i + 1, text: line }); continue; }
var st = +m[8];
rows.push({
n: i + 1,
ip: m[1],
user: m[3] === "-" ? "" : m[3],
tsRaw: m[4],
ts: parseTs(m[4]),
method: m[5],
path: m[6],
proto: m[7] || "",
status: st,
klass: (st / 100 | 0),
bytes: m[9] === "-" ? 0 : +m[9],
ref: m[10] || "-",
ua: m[11] || "-",
agent: agentName(m[11] || "-")
});
}
return { rows: rows, bad: bad, blank: blank, total: raw.length - blank };
}
/* ── aggregation ───────────────────────────────────────────────────────── */
var CLASSES = [
{ k: 2, label: "2xx", name: "success", varname: "--s2" },
{ k: 3, label: "3xx", name: "redirect", varname: "--s3" },
{ k: 4, label: "4xx", name: "client error", varname: "--s4" },
{ k: 5, label: "5xx", name: "server error", varname: "--s5" }
];
var STEPS = [1e3, 5e3, 15e3, 30e3, 60e3, 3e5, 9e5, 18e5, 36e5, 216e5, 864e5];
function aggregate(rows) {
var byClass = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
var ips = new Map(), paths = new Map();
var bytes = 0, minT = Infinity, maxT = -Infinity, timed = 0;
rows.forEach(function (r) {
byClass[r.klass] = (byClass[r.klass] || 0) + 1;
bytes += r.bytes;
var ipRec = ips.get(r.ip);
if (!ipRec) { ipRec = { key: r.ip, n: 0, err: 0, srv: 0, paths: new Set(), robot: false, agent: r.agent }; ips.set(r.ip, ipRec); }
ipRec.n++;
if (r.klass === 4) { ipRec.err++; ipRec.paths.add(r.path); }
if (r.klass === 5) ipRec.srv++;
if (isRobot(r.ua)) ipRec.robot = true;
var pKey = r.path.split("?")[0];
var pRec = paths.get(pKey);
if (!pRec) { pRec = { key: pKey, n: 0, srv: 0, cli: 0, bytes: 0 }; paths.set(pKey, pRec); }
pRec.n++; pRec.bytes += r.bytes;
if (r.klass === 5) pRec.srv++;
if (r.klass === 4) pRec.cli++;
if (r.ts != null) {
timed++;
if (r.ts < minT) minT = r.ts;
if (r.ts > maxT) maxT = r.ts;
}
});
ips.forEach(function (v) {
v.tag = null;
if (v.err >= 4 && v.paths.size >= 4) v.tag = { c: "scan", t: "scanner" };
else if (/bot|spider|crawl/.test((v.agent || "").toLowerCase())) v.tag = { c: "crawl", t: "crawler" };
else if (v.robot) v.tag = { c: "auto", t: "automated" };
});
var buckets = [], step = 60000;
if (timed > 0 && isFinite(minT)) {
var span = Math.max(maxT - minT, 1);
for (var s = 0; s < STEPS.length; s++) { step = STEPS[s]; if (span / step <= 30) break; }
var start = Math.floor(minT / step) * step;
var count = Math.floor((maxT - start) / step) + 1;
// Never truncate the axis: widen the bucket until the window fits, so a
// multi-year log loses resolution rather than losing rows.
while (count > 220) {
step *= 2;
start = Math.floor(minT / step) * step;
count = Math.floor((maxT - start) / step) + 1;
}
for (var b = 0; b < count; b++) buckets.push({ t: start + b * step, tot: 0, c: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 } });
rows.forEach(function (r) {
if (r.ts == null) return;
var idx = Math.floor((r.ts - start) / step);
if (idx < 0 || idx >= buckets.length) return;
buckets[idx].tot++;
buckets[idx].c[r.klass] = (buckets[idx].c[r.klass] || 0) + 1;
});
}
var rank = function (map) {
return Array.from(map.values()).sort(function (a, b) { return b.n - a.n || (a.key < b.key ? -1 : 1); });
};
return {
byClass: byClass, bytes: bytes, ips: rank(ips), paths: rank(paths),
uniqueIps: ips.size, uniquePaths: paths.size,
minT: isFinite(minT) ? minT : null, maxT: isFinite(maxT) ? maxT : null,
multiDay: isFinite(minT) && isFinite(maxT) && Math.floor(minT / 864e5) !== Math.floor(maxT / 864e5),
multiYear: isFinite(minT) && isFinite(maxT) && new Date(minT).getUTCFullYear() !== new Date(maxT).getUTCFullYear(),
buckets: buckets, step: step, timed: timed
};
}
/* ── formatting ────────────────────────────────────────────────────────── */
var MON_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
function pad(n) { return n < 10 ? "0" + n : "" + n; }
function plural(n, word) { return n === 1 ? word : word + "s"; }
function dmon(t) { var d = new Date(t); return d.getUTCDate() + " " + MON_NAMES[d.getUTCMonth()]; }
// Day + month is enough inside one year; add the year once the log crosses one.
function dstamp(t) { return (AGG && AGG.multiYear) ? dmon(t) + " " + new Date(t).getUTCFullYear() : dmon(t); }
function hhmmss(t) { var d = new Date(t); return pad(d.getUTCHours()) + ":" + pad(d.getUTCMinutes()) + ":" + pad(d.getUTCSeconds()); }
function hhmm(t) { var d = new Date(t); return pad(d.getUTCHours()) + ":" + pad(d.getUTCMinutes()); }
// Bare clock time is ambiguous the moment a log crosses midnight.
function stamp(t) { return (AGG && AGG.multiDay) ? dstamp(t) + " " + hhmmss(t) : hhmmss(t); }
function bucketLabel(t, step) {
if (step >= 864e5) return dstamp(t); // day+ buckets: date only
if (step >= 216e5) return dstamp(t) + " " + hhmm(t); // 6h+ buckets span days
if (step < 60000) return hhmmss(t);
return hhmm(t);
}
function fmtBytes(b) {
if (b < 1024) return [String(b), "B"];
if (b < 1048576) return [(b / 1024).toFixed(b < 10240 ? 1 : 0), "KB"];
if (b < 1073741824) return [(b / 1048576).toFixed(b < 10485760 ? 1 : 0), "MB"];
return [(b / 1073741824).toFixed(2), "GB"];
}
function fmtDur(ms) {
var s = Math.round(ms / 1000);
if (s >= 1728e2) { var dd = s / 86400; return (dd % 1 === 0 ? dd : dd.toFixed(1)) + "d"; }
if (s >= 3600 && s % 3600 === 0) return (s / 3600) + "h"; // 3600s → "1h", not "60m"
if (s >= 60 && s % 60 === 0 && s < 5400) return (s / 60) + "m"; // 60s → "1m", not "60s"
if (s < 90) return s + "s";
var m = Math.round(s / 60);
if (m < 90) return m + "m";
return (m / 60).toFixed(1) + "h";
}
function css(name) { return getComputedStyle(root).getPropertyValue(name).trim() || "#888"; }
function focusAttr(container, attr, val) {
var list = container.querySelectorAll("[" + attr + "]");
for (var i = 0; i < list.length; i++) {
if (list[i].getAttribute(attr) === val) { list[i].focus(); return true; }
}
return false;
}
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]; }); }
/* ── state ─────────────────────────────────────────────────────────────── */
var DATA = { rows: [], bad: [], blank: 0, total: 0 };
var AGG = null;
var filters = { klass: null, ip: null, path: null, bucket: null, q: "" };
var sort = { key: "ts", dir: 1 };
var page = 0, perPage = 12;
var hoverIdx = -1, focusIdx = -1;
var el = {
receipt: document.getElementById("receipt"),
rcpIcon: document.getElementById("rcpIcon"),
rcpText: document.getElementById("rcpText"),
chips: document.getElementById("chips"),
finds: document.getElementById("finds"),
statNote: document.getElementById("statNote"),
bucketNote: document.getElementById("bucketNote"),
legend: document.getElementById("legend"),
topIps: document.getElementById("topIps"),
topPaths: document.getElementById("topPaths"),
tbody: document.getElementById("tbody"),
tblEmpty: document.getElementById("tblEmpty"),
tblCount: document.getElementById("tblCount"),
fchips: document.getElementById("fchips"),
pos: document.getElementById("pos"),
prev: document.getElementById("prev"),
next: document.getElementById("next"),
per: document.getElementById("per"),
q: document.getElementById("q"),
src: document.getElementById("src"),
skipBox: document.getElementById("skipBox"),
skipSum: document.getElementById("skipSum"),
skipList: document.getElementById("skipList"),
canvas: document.getElementById("timeline"),
tip: document.getElementById("tip"),
live: document.getElementById("tlLive"),
perf: document.getElementById("perf"),
csvMsg: document.getElementById("csvMsg")
};
/* ── render: receipt + chips ───────────────────────────────────────────── */
function renderReceipt(ms) {
var ok = DATA.rows.length > 0;
el.receipt.classList.toggle("bad", !ok);
el.rcpIcon.textContent = ok ? "✓" : "!";
var parts = [];
parts.push('<span class="v">' + DATA.total + "</span>" + plural(DATA.total, " line") + " in");
parts.push('<span class="v">' + DATA.rows.length + "</span> parsed");
parts.push('<span class="v">' + DATA.bad.length + "</span> unreadable");
if (AGG && AGG.minT != null) {
parts.push('<span class="v">' + stamp(AGG.minT) + " → " + stamp(AGG.maxT) + "</span> UTC");
parts.push('<span class="v">' + fmtDur(AGG.maxT - AGG.minT) + "</span> window");
}
el.rcpText.innerHTML = ok
? parts.join(' <span class="sep">·</span> ')
: "nothing parsed — check the format, or press <b>Restore sample</b>";
el.perf.textContent = ok ? DATA.rows.length + plural(DATA.rows.length, " row") + " in " + ms.toFixed(1) + " ms" : "";
}
function renderChips() {
var parsedAny = DATA.rows.length > 0;
var n = DATA.rows.length || 1;
var c = AGG.byClass;
var errRate = (c[5] / n) * 100;
var okRate = (c[2] / n) * 100;
var by = fmtBytes(AGG.bytes);
var badPct = DATA.total ? (DATA.bad.length / DATA.total) * 100 : 0;
var items = [
{ v: String(DATA.rows.length), u: "", l: "requests", cls: "" },
{ v: errRate.toFixed(1), u: "%", l: "5xx error rate", cls: c[5] > 0 ? "bad" : (parsedAny ? "good" : "") },
{ v: String(c[5]), u: "", l: "server errors", cls: c[5] > 0 ? "bad" : "" },
{ v: String(c[4]), u: "", l: "client errors", cls: c[4] > 0 ? "warn" : "" },
{ v: okRate.toFixed(0), u: "%", l: "2xx success", cls: parsedAny && okRate >= 95 ? "good" : "" },
{ v: String(AGG.uniqueIps), u: "", l: "unique clients", cls: "" },
{ v: by[0], u: by[1], l: "bytes served", cls: "" },
{ v: String(DATA.bad.length), u: "", l: "unparsed lines", cls: DATA.bad.length > 0 ? "warn" : (parsedAny ? "good" : "") }
];
el.chips.innerHTML = items.map(function (it) {
return '<div class="chip ' + it.cls + '"><span class="val">' + esc(it.v) +
(it.u ? '<span class="u">' + it.u + "</span>" : "") +
'</span><span class="lbl">' + it.l + "</span></div>";
}).join("");
el.statNote.textContent = !parsedAny ? "nothing to summarise yet"
: DATA.bad.length ? badPct.toFixed(1) + "% of lines did not match"
: "every line matched";
}
/* ── render: findings (computed, and each one is a filter) ─────────────── */
function renderFindings() {
var out = [];
if (AGG.buckets.length) {
var w = AGG.buckets.reduce(function (a, b) { return (b.c[5] || 0) > (a.c[5] || 0) ? b : a; }, AGG.buckets[0]);
if ((w.c[5] || 0) >= 2) {
var pc = {}, tot = 0;
DATA.rows.forEach(function (r) {
if (r.klass === 5 && r.ts != null && r.ts >= w.t && r.ts < w.t + AGG.step) {
var k = r.path.split("?")[0]; pc[k] = (pc[k] || 0) + 1; tot++;
}
});
var top = Object.keys(pc).sort(function (a, b) { return pc[b] - pc[a]; })[0];
out.push({
ok: false,
html: "<b>" + w.c[5] + " of " + w.tot + "</b> requests failed at <b>" + bucketLabel(w.t, AGG.step) +
"</b>" + (top ? ", mostly on <b>" + esc(top) + "</b>" : "") + " — that is a server-side incident, not client noise.",
go: "show the window →",
act: function () { filters.bucket = w.t; filters.klass = 5; }
});
}
}
var sc = AGG.ips.filter(function (i) { return i.tag && i.tag.c === "scan"; })[0];
if (sc) {
out.push({
ok: false,
html: "<b>" + esc(sc.key) + "</b> probed <b>" + sc.paths.size + " distinct paths</b> and collected <b>" +
sc.err + " 4xx</b> — that is a vulnerability scanner walking your routes.",
go: "show its requests →",
act: function () { filters.ip = sc.key; }
});
}
if (!out.length && DATA.rows.length) {
out.push({ ok: true, html: "No error spikes and no scanning behaviour in this window.", go: "", act: null });
}
el.finds.innerHTML = "";
out.forEach(function (f) {
var b = document.createElement(f.act ? "button" : "div");
if (f.act) b.type = "button"; else b.setAttribute("role", "status");
b.className = "find" + (f.ok ? " ok" : "");
b.innerHTML = '<span class="ic" aria-hidden="true">' + (f.ok ? "\u2713" : "\u26A0") + "</span><span class=\"tx\">" + f.html + "</span>" +
(f.go ? '<span class="go">' + f.go + "</span>" : "");
if (f.act) b.addEventListener("click", function () {
filters.klass = filters.ip = filters.path = null; filters.bucket = null;
filters.q = ""; el.q.value = "";
f.act();
page = 0;
renderFilters(); renderTable(); renderRanks(); renderLegend(); drawChart();
var tgt = document.getElementById("h-table");
var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (tgt.scrollIntoView) tgt.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "start" });
});
el.finds.appendChild(b);
});
}
/* ── render: legend ────────────────────────────────────────────────────── */
function renderLegend() {
el.legend.innerHTML = CLASSES.map(function (c) {
var n = AGG.byClass[c.k] || 0;
var on = filters.klass === c.k;
return '<button class="lg" type="button" data-klass="' + c.k + '" aria-pressed="' + on + '"' +
' title="filter the table to ' + c.label + '"><span class="sw" style="background:var(' + c.varname + ')"></span>' +
c.label + ' <span class="n">' + n + "</span></button>";
}).join("");
Array.prototype.forEach.call(el.legend.querySelectorAll("[data-klass]"), function (b) {
b.addEventListener("click", function () {
var k = b.getAttribute("data-klass");
var keep = document.activeElement === b;
filters.klass = filters.klass === +k ? null : +k;
page = 0; renderFilters(); renderTable(); renderLegend();
if (keep) focusAttr(el.legend, "data-klass", k);
});
});
}
/* ── render: rank lists ────────────────────────────────────────────────── */
function renderRanks() {
var maxIp = AGG.ips.length ? AGG.ips[0].n : 1;
el.topIps.innerHTML = AGG.ips.slice(0, 6).map(function (r) {
var on = filters.ip === r.key;
var tag = r.tag ? '<span class="tag ' + r.tag.c + '">' + r.tag.t + "</span>" : "";
var srv = r.srv ? '<span class="tag scan">' + r.srv + " 5xx</span>" : "";
return '<li><button type="button" data-ip="' + esc(r.key) + '" aria-pressed="' + on + '">' +
'<span class="bar" style="width:' + (r.n / maxIp * 100).toFixed(1) + '%"></span>' +
'<span class="k">' + esc(r.key) + "</span>" +
'<span class="m">' + tag + srv + "<span>" + r.n + "</span></span></button></li>";
}).join("");
var maxP = AGG.paths.length ? AGG.paths[0].n : 1;
el.topPaths.innerHTML = AGG.paths.slice(0, 6).map(function (r) {
var on = filters.path === r.key;
var bad = r.srv ? '<span class="tag scan">' + r.srv + " 5xx</span>" : (r.cli ? '<span class="tag auto">' + r.cli + " 4xx</span>" : "");
return '<li><button type="button" data-path="' + esc(r.key) + '" aria-pressed="' + on + '">' +
'<span class="bar" style="width:' + (r.n / maxP * 100).toFixed(1) + '%"></span>' +
'<span class="k">' + esc(r.key) + "</span>" +
'<span class="m">' + bad + "<span>" + r.n + "</span></span></button></li>";
}).join("");
Array.prototype.forEach.call(el.topIps.querySelectorAll("[data-ip]"), function (b) {
b.addEventListener("click", function () {
var v = b.getAttribute("data-ip");
var keep = document.activeElement === b;
filters.ip = filters.ip === v ? null : v;
page = 0; renderFilters(); renderTable(); renderRanks();
if (keep) focusAttr(el.topIps, "data-ip", v);
});
});
Array.prototype.forEach.call(el.topPaths.querySelectorAll("[data-path]"), function (b) {
b.addEventListener("click", function () {
var v = b.getAttribute("data-path");
var keep = document.activeElement === b;
filters.path = filters.path === v ? null : v;
page = 0; renderFilters(); renderTable(); renderRanks();
if (keep) focusAttr(el.topPaths, "data-path", v);
});
});
}
/* ── filtering + table ─────────────────────────────────────────────────── */
function filtered() {
var q = filters.q.trim().toLowerCase();
return DATA.rows.filter(function (r) {
if (filters.klass && r.klass !== filters.klass) return false;
if (filters.ip && r.ip !== filters.ip) return false;
if (filters.path && r.path.split("?")[0] !== filters.path) return false;
if (filters.bucket != null) {
if (r.ts == null) return false;
if (r.ts < filters.bucket || r.ts >= filters.bucket + AGG.step) return false;
}
if (q) {
var hay = r.ip + " " + r.method + " " + r.path + " " + r.status + " " + r.agent + " " + r.tsRaw;
if (hay.toLowerCase().indexOf(q) === -1) return false;
}
return true;
});
}
function renderFilters() {
var chips = [];
if (filters.klass) chips.push({ k: "klass", t: "status " + filters.klass + "xx" });
if (filters.ip) chips.push({ k: "ip", t: "client " + filters.ip });
if (filters.path) chips.push({ k: "path", t: "path " + filters.path });
if (filters.bucket != null) chips.push({ k: "bucket", t: "at " + bucketLabel(filters.bucket, AGG.step) });
if (filters.q.trim()) chips.push({ k: "q", t: '"' + filters.q.trim() + '"' });
el.fchips.innerHTML = chips.map(function (c) {
return '<button class="fchip" type="button" data-clear="' + c.k + '">' + esc(c.t) +
' <span class="x" aria-hidden="true">×</span><span class="sr">remove filter</span></button>';
}).join("") + (chips.length > 1 ? '<button class="fchip" type="button" data-clear="all">clear all <span class="x" aria-hidden="true">×</span></button>' : "");
Array.prototype.forEach.call(el.fchips.querySelectorAll("[data-clear]"), function (b) {
b.addEventListener("click", function () {
var k = b.getAttribute("data-clear");
var keep = document.activeElement === b;
if (k === "all") { filters.klass = filters.ip = filters.path = null; filters.bucket = null; filters.q = ""; el.q.value = ""; }
else if (k === "q") { filters.q = ""; el.q.value = ""; }
else if (k === "bucket") filters.bucket = null;
else filters[k] = null;
page = 0; renderFilters(); renderTable(); renderRanks(); renderLegend(); drawChart();
if (keep) {
var nextChip = el.fchips.querySelector(".fchip");
(nextChip || el.q).focus();
}
});
});
}
function sortRows(rows) {
var k = sort.key, d = sort.dir;
return rows.slice().sort(function (a, b) {
var x = a[k], y = b[k];
if (k === "ts") { x = x == null ? -Infinity : x; y = y == null ? -Infinity : y; }
if (typeof x === "string") return d * (x < y ? -1 : x > y ? 1 : 0) || (a.n - b.n);
return d * (x - y) || (a.n - b.n);
});
}
function renderTable() {
var rows = sortRows(filtered());
var total = rows.length;
var pages = Math.max(1, Math.ceil(total / perPage));
if (page >= pages) page = pages - 1;
if (page < 0) page = 0;
var slice = rows.slice(page * perPage, page * perPage + perPage);
el.tbody.innerHTML = slice.map(function (r) {
var cls = "c" + r.klass;
var v = CLASSES.filter(function (c) { return c.k === r.klass; })[0];
var sw = v ? '<span class="sw" style="background:var(' + v.varname + ')"></span>' : "";
return "<tr>" +
'<td class="num dim">' + r.n + "</td>" +
"<td>" + (r.ts != null ? stamp(r.ts) : '<span class="dim">?</span>') + "</td>" +
"<td>" + esc(r.ip) + "</td>" +
'<td class="dim">' + esc(r.method) + "</td>" +
'<td class="path" title="' + esc(r.path) + '">' + esc(r.path) + "</td>" +
'<td><span class="st ' + cls + '">' + sw + r.status + "</span></td>" +
'<td class="num">' + (r.bytes ? r.bytes.toLocaleString("en-US") : "—") + "</td>" +
'<td class="dim">' + esc(r.agent) + "</td>" +
"</tr>";
}).join("");
el.tblEmpty.hidden = total !== 0;
el.tblEmpty.textContent = DATA.rows.length === 0
? "Nothing parsed yet — paste log lines into Your logs below, or press Restore sample."
: "No rows match these filters.";
el.pos.textContent = total
? (page * perPage + 1) + "–" + Math.min(total, (page + 1) * perPage) + " of " + total
: "0 of 0";
el.tblCount.textContent = total === DATA.rows.length
? DATA.rows.length + plural(DATA.rows.length, " row")
: total + " of " + DATA.rows.length + plural(DATA.rows.length, " row");
el.prev.disabled = page === 0;
el.next.disabled = page >= pages - 1;
Array.prototype.forEach.call(document.querySelectorAll("thead th[data-sort]"), function (th) {
var k = th.getAttribute("data-sort");
if (k === sort.key) {
th.setAttribute("aria-sort", sort.dir === 1 ? "ascending" : "descending");
th.querySelector(".ar").textContent = sort.dir === 1 ? "▲" : "▼";
} else th.removeAttribute("aria-sort");
});
}
/* ── skipped lines ─────────────────────────────────────────────────────── */
function renderSkipped() {
if (!DATA.bad.length) { el.skipBox.hidden = true; return; }
el.skipBox.hidden = false;
el.skipSum.textContent = DATA.bad.length + " line" + (DATA.bad.length === 1 ? "" : "s") + " could not be parsed — show them";
el.skipList.innerHTML = DATA.bad.slice(0, 40).map(function (b) {
return '<li><span class="ln">L' + b.n + "</span>" + esc(b.text.slice(0, 300)) + "</li>";
}).join("") + (DATA.bad.length > 40 ? '<li class="ln">…and ' + (DATA.bad.length - 40) + " more</li>" : "");
}
/* ── chart ─────────────────────────────────────────────────────────────── */
var cv = el.canvas, ctx = cv.getContext("2d");
var geom = { x0: 0, slot: 0, w: 0, h: 0, top: 0, bottom: 0 };
function drawChart() {
if (!AGG) return;
var wrapW = cv.clientWidth || 600;
var cssH = 208;
var dpr = Math.min(window.devicePixelRatio || 1, 2);
cv.width = Math.max(1, Math.round(wrapW * dpr));
cv.height = Math.round(cssH * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, wrapW, cssH);
var muted = css("--muted"), grid = css("--grid"), surface = css("--surface"), accent = css("--accent");
var colors = { 1: muted, 2: css("--s2"), 3: css("--s3"), 4: css("--s4"), 5: css("--s5") };
var padL = 34, padR = 6, padT = 12, padB = 26;
var W = wrapW - padL - padR, H = cssH - padT - padB;
var bs = AGG.buckets;
ctx.font = "500 10px " + (getComputedStyle(root).getPropertyValue("--font-mono").trim() || "monospace");
ctx.textBaseline = "middle";
if (!bs.length) {
ctx.fillStyle = muted; ctx.textAlign = "center";
ctx.fillText("no timestamps to plot", wrapW / 2, cssH / 2);
geom.slot = 0;
return;
}
var max = 1;
bs.forEach(function (b) { if (b.tot > max) max = b.tot; });
var ticks = max <= 4 ? max : 4;
var stepV = Math.max(1, Math.ceil(max / ticks));
max = stepV * Math.ceil(max / stepV);
// grid + y labels
ctx.textAlign = "right";
for (var g = 0; g <= max; g += stepV) {
var y = padT + H - (g / max) * H;
ctx.strokeStyle = grid; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(padL, Math.round(y) + .5); ctx.lineTo(padL + W, Math.round(y) + .5); ctx.stroke();
ctx.fillStyle = muted;
ctx.fillText(String(g), padL - 7, y);
}
var slot = W / bs.length;
var bw = Math.max(2, Math.min(slot * 0.74, 46));
geom = { x0: padL, slot: slot, w: W, h: H, top: padT, bottom: padT + H, bw: bw };
// highlight column
var hi = hoverIdx >= 0 ? hoverIdx : focusIdx;
if (hi >= 0 && hi < bs.length) {
ctx.fillStyle = css("--accent-soft");
ctx.fillRect(padL + hi * slot, padT - 4, slot, H + 6);
}
if (filters.bucket != null) {
var fi = Math.round((filters.bucket - bs[0].t) / AGG.step);
if (fi >= 0 && fi < bs.length) {
ctx.strokeStyle = accent; ctx.lineWidth = 1;
ctx.strokeRect(Math.round(padL + fi * slot) + .5, padT - 4 + .5, Math.round(slot) - 1, H + 6);
}
}
// stacked bars, 2px surface gap between segments, 4px rounded cap on top
bs.forEach(function (b, i) {
var x = padL + i * slot + (slot - bw) / 2;
var yBase = padT + H;
var order = [2, 3, 4, 5, 1];
Object.keys(b.c).forEach(function (k) { if (order.indexOf(+k) === -1) order.push(+k); });
var segs = [];
order.forEach(function (k) { if (b.c[k]) segs.push({ k: k, n: b.c[k] }); });
var topSeg = segs.length - 1;
segs.forEach(function (s, si) {
var hSeg = (s.n / max) * H;
var yTop = yBase - hSeg;
ctx.fillStyle = colors[s.k] || muted;
var r = si === topSeg ? Math.min(4, bw / 2, hSeg) : 0;
roundRect(ctx, x, yTop, bw, Math.max(hSeg, 1.2), r, r, 0, 0);
ctx.fill();
if (si < topSeg && hSeg > 4) {
ctx.fillStyle = surface;
ctx.fillRect(x, yTop - 1, bw, 2);
}
yBase = yTop;
});
});
// x axis
ctx.strokeStyle = grid; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(padL, padT + H + .5); ctx.lineTo(padL + W, padT + H + .5); ctx.stroke();
ctx.fillStyle = muted; ctx.textAlign = "center";
var minGap = Math.max(40, ctx.measureText(bucketLabel(bs[bs.length - 1].t, AGG.step)).width + 16);
var every = Math.max(1, Math.ceil(minGap / slot));
for (var i2 = 0; i2 < bs.length; i2 += every) {
var cx = padL + i2 * slot + slot / 2;
if (cx > padL + W - 12) break;
ctx.fillText(bucketLabel(bs[i2].t, AGG.step), cx, padT + H + 13);
}
}
function roundRect(c, x, y, w, h, r1, r2, r3, r4) {
c.beginPath();
c.moveTo(x + r1, y);
c.lineTo(x + w - r2, y); c.quadraticCurveTo(x + w, y, x + w, y + r2);
c.lineTo(x + w, y + h - r3); c.quadraticCurveTo(x + w, y + h, x + w - r3, y + h);
c.lineTo(x + r4, y + h); c.quadraticCurveTo(x, y + h, x, y + h - r4);
c.lineTo(x, y + r1); c.quadraticCurveTo(x, y, x + r1, y);
c.closePath();
}
function idxFromX(px) {
if (!geom.slot) return -1;
var i = Math.floor((px - geom.x0) / geom.slot);
return (i >= 0 && i < AGG.buckets.length) ? i : -1;
}
function otherCount(b) {
var known = 0;
CLASSES.forEach(function (c) { known += b.c[c.k] || 0; });
return Math.max(0, b.tot - known);
}
function showTip(i, px) {
var b = AGG.buckets[i];
if (!b) { el.tip.classList.remove("on"); return; }
var rows = CLASSES.filter(function (c) { return b.c[c.k]; }).map(function (c) {
return '<div class="t-r"><span class="sw" style="background:var(' + c.varname + ')"></span>' +
c.label + '<span class="n">' + b.c[c.k] + "</span></div>";
}).join("");
var other = otherCount(b);
if (other) rows += '<div class="t-r"><span class="sw" style="background:var(--muted)"></span>other<span class="n">' + other + "</span></div>";
if (!rows) rows = '<div class="t-r">no requests</div>';
el.tip.innerHTML = '<div class="t-h">' + bucketLabel(b.t, AGG.step) + " · " + b.tot + " req</div>" + rows;
el.tip.classList.add("on");
var tw = el.tip.offsetWidth || 140;
var maxX = cv.clientWidth - tw - 4;
var x = Math.max(0, Math.min(px - tw / 2, maxX));
el.tip.style.left = x + "px";
el.tip.style.top = "4px";
}
function hideTip() { el.tip.classList.remove("on"); }
cv.addEventListener("mousemove", function (e) {
var r = cv.getBoundingClientRect();
var px = e.clientX - r.left;
var i = idxFromX(px);
if (i !== hoverIdx) { hoverIdx = i; drawChart(); }
if (i >= 0) showTip(i, px); else hideTip();
});
cv.addEventListener("mouseleave", function () { hoverIdx = -1; hideTip(); drawChart(); });
cv.addEventListener("click", function (e) {
var r = cv.getBoundingClientRect();
var i = idxFromX(e.clientX - r.left);
if (i < 0) return;
focusIdx = i;
setBucketFilter(AGG.buckets[i].t);
});
cv.addEventListener("keydown", function (e) {
if (!AGG || !AGG.buckets.length) return;
var k = e.key;
if (k === "ArrowRight" || k === "ArrowLeft") {
e.preventDefault();
if (focusIdx < 0) focusIdx = k === "ArrowRight" ? 0 : AGG.buckets.length - 1;
else focusIdx = Math.max(0, Math.min(AGG.buckets.length - 1, focusIdx + (k === "ArrowRight" ? 1 : -1)));
announce(focusIdx); drawChart();
showTip(focusIdx, geom.x0 + focusIdx * geom.slot + geom.slot / 2);
} else if (k === "Home" || k === "End") {
e.preventDefault();
focusIdx = k === "Home" ? 0 : AGG.buckets.length - 1;
announce(focusIdx); drawChart();
showTip(focusIdx, geom.x0 + focusIdx * geom.slot + geom.slot / 2);
} else if (k === "Enter" || k === " ") {
e.preventDefault();
if (focusIdx >= 0) setBucketFilter(AGG.buckets[focusIdx].t);
} else if (k === "Escape") {
if (filters.bucket != null) {
e.preventDefault();
filters.bucket = null; page = 0;
renderFilters(); renderTable(); drawChart();
el.live.textContent = "time filter cleared";
}
}
});
cv.addEventListener("blur", function () { hideTip(); });
function setBucketFilter(t) {
filters.bucket = filters.bucket === t ? null : t;
page = 0;
renderFilters(); renderTable(); drawChart();
el.live.textContent = filters.bucket == null
? "time filter cleared"
: "table filtered to " + bucketLabel(t, AGG.step);
}
function announce(i) {
var b = AGG.buckets[i];
if (!b) return;
var bits = CLASSES.filter(function (c) { return b.c[c.k]; })
.map(function (c) { return b.c[c.k] + " " + c.label; });
if (otherCount(b)) bits.push(otherCount(b) + " other");
el.live.textContent = bucketLabel(b.t, AGG.step) + ": " + b.tot + plural(b.tot, " request") +
(bits.length ? " — " + bits.join(", ") : "");
}
function chartAria() {
if (!AGG || !AGG.buckets.length) { cv.setAttribute("aria-label", "No timestamped requests to plot"); return; }
var peak = AGG.buckets.reduce(function (a, b) { return b.tot > a.tot ? b : a; }, AGG.buckets[0]);
var worst = AGG.buckets.reduce(function (a, b) { return (b.c[5] || 0) > (a.c[5] || 0) ? b : a; }, AGG.buckets[0]);
var span = AGG.multiDay
? dstamp(AGG.minT) + " " + hhmm(AGG.minT) + " to " + dstamp(AGG.maxT) + " " + hhmm(AGG.maxT)
: hhmm(AGG.minT) + " to " + hhmm(AGG.maxT);
var s = "Requests per " + fmtDur(AGG.step) + " from " + span +
" UTC, stacked by status class. Busiest bucket " + bucketLabel(peak.t, AGG.step) +
" with " + peak.tot + plural(peak.tot, " request") + ".";
if (worst.c[5]) s += " Most server errors at " + bucketLabel(worst.t, AGG.step) + " with " + worst.c[5] + " 5xx.";
cv.setAttribute("aria-label", s);
}
/* ── pipeline ──────────────────────────────────────────────────────────── */
function run(text) {
var t0 = (performance && performance.now) ? performance.now() : 0;
DATA = parse(text);
AGG = aggregate(DATA.rows);
var t1 = (performance && performance.now) ? performance.now() : 0;
if (filters.bucket != null && (!AGG.buckets.length ||
filters.bucket < AGG.buckets[0].t || filters.bucket > AGG.buckets[AGG.buckets.length - 1].t)) {
filters.bucket = null;
}
if (filters.ip && !DATA.rows.some(function (r) { return r.ip === filters.ip; })) filters.ip = null;
if (filters.path && !DATA.rows.some(function (r) { return r.path.split("?")[0] === filters.path; })) filters.path = null;
if (filters.klass && !AGG.byClass[filters.klass]) filters.klass = null;
focusIdx = -1; hoverIdx = -1; hideTip();
renderReceipt(t1 - t0);
renderChips();
renderFindings();
renderLegend();
renderRanks();
renderFilters();
renderTable();
renderSkipped();
el.bucketNote.textContent = AGG.buckets.length
? AGG.buckets.length + plural(AGG.buckets.length, " bucket") + " · " + fmtDur(AGG.step) + " each"
: "no timestamps";
chartAria();
drawChart();
}
/* ── wiring ────────────────────────────────────────────────────────────── */
var debounce;
el.src.addEventListener("input", function () {
clearTimeout(debounce);
debounce = setTimeout(function () { run(el.src.value); }, 220);
});
document.getElementById("parseBtn").addEventListener("click", function () {
clearTimeout(debounce); run(el.src.value);
});
document.getElementById("sampleBtn").addEventListener("click", function () {
clearTimeout(debounce);
el.src.value = SAMPLE;
el.src.scrollLeft = 0; el.src.scrollTop = 0;
filters.klass = filters.ip = filters.path = null; filters.bucket = null;
filters.q = ""; el.q.value = ""; page = 0;
run(SAMPLE);
resetSrcScroll();
});
document.getElementById("clearBtn").addEventListener("click", function () {
clearTimeout(debounce);
el.src.value = "";
el.src.scrollLeft = 0; el.src.scrollTop = 0;
filters.klass = filters.ip = filters.path = null; filters.bucket = null;
filters.q = ""; el.q.value = ""; page = 0;
run("");
resetSrcScroll();
});
function resetSrcScroll() {
try { el.src.setSelectionRange(0, 0); } catch (err) { /* not selectable */ }
el.src.focus();
el.src.scrollLeft = 0; el.src.scrollTop = 0;
requestAnimationFrame(function () { el.src.scrollLeft = 0; el.src.scrollTop = 0; });
}
el.q.addEventListener("input", function () {
filters.q = el.q.value; page = 0;
renderFilters(); renderTable();
});
// A button that disables itself drops focus to <body>; hand it to its partner.
el.prev.addEventListener("click", function () {
var had = document.activeElement === el.prev;
page--; renderTable();
if (had && el.prev.disabled) el.next.focus();
});
el.next.addEventListener("click", function () {
var had = document.activeElement === el.next;
page++; renderTable();
if (had && el.next.disabled) el.prev.focus();
});
el.per.addEventListener("change", function () { perPage = +el.per.value || 12; page = 0; renderTable(); });
Array.prototype.forEach.call(document.querySelectorAll("thead th[data-sort] button"), function (b) {
b.addEventListener("click", function () {
var k = b.parentNode.getAttribute("data-sort");
if (sort.key === k) sort.dir *= -1; else { sort.key = k; sort.dir = 1; }
page = 0; renderTable();
});
});
var csvMsgTimer;
document.getElementById("csvBtn").addEventListener("click", function () {
var rows = sortRows(filtered());
var head = "line,time,ip,method,path,status,bytes,agent\n";
var body = rows.map(function (r) {
return [r.n, r.ts != null ? new Date(r.ts).toISOString() : r.tsRaw, r.ip, r.method,
'"' + r.path.replace(/"/g, '""') + '"', r.status, r.bytes,
'"' + r.agent.replace(/"/g, '""') + '"'].join(",");
}).join("\n");
var csv = head + body;
var done = function (ok) {
el.csvMsg.className = "status-msg" + (ok ? " ok" : "");
el.csvMsg.textContent = ok ? "✓ " + rows.length + " rows copied" : "copy blocked — select the rows instead";
clearTimeout(csvMsgTimer);
csvMsgTimer = setTimeout(function () { el.csvMsg.textContent = ""; el.csvMsg.className = "status-msg"; }, 3000);
};
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(csv).then(function () { done(true); }, function () { done(legacyCopy(csv)); });
return;
}
} catch (err) { /* fall through */ }
done(legacyCopy(csv));
});
function legacyCopy(text) {
try {
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.cssText = "position:fixed;top:0;left:-9999px;opacity:0";
document.body.appendChild(ta);
ta.select();
var ok = document.execCommand ? document.execCommand("copy") : false;
document.body.removeChild(ta);
return !!ok;
} catch (err) { return false; }
}
var rAF;
function onResize() {
cancelAnimationFrame(rAF);
rAF = requestAnimationFrame(function () { hideTip(); drawChart(); });
}
if (window.ResizeObserver) new ResizeObserver(onResize).observe(cv.parentNode);
window.addEventListener("resize", onResize);
/* ── boot ──────────────────────────────────────────────────────────────── */
themeLbl.textContent = effectiveTheme() === "dark" ? "Light" : "Dark";
el.src.value = SAMPLE;
run(SAMPLE);
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.