Files
unkin-agent abc81e60c8
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
Add logviewer: web UI for the ClickHouse log store
Single Go binary with embedded Bootstrap 3 + jQuery UI, querying logs.raw
over the ClickHouse HTTP interface as the readonly logreader user. Runs
behind oauth2-proxy; the app does no auth itself. Server-side enforced
time bounds (15m default, 72h max), parameterized queries, raw-SQL WHERE
fragment wrapped with enforced bounds and LIMIT, tail polling with a
clamped cursor, facets, healthz. Woodpecker build/test plus tag-driven
image push to artifactapi docker-internal.
2026-08-23 16:39:22 +10:00

123 lines
5.1 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>logviewer</title>
<link rel="stylesheet" href="/static/bootstrap.min.css">
<link rel="stylesheet" href="/static/logviewer.css">
</head>
<body>
<nav class="navbar navbar-inverse navbar-static-top">
<div class="container-fluid">
<div class="navbar-header">
<a class="navbar-brand" href="/">logviewer</a>
</div>
<p class="navbar-text navbar-right" id="status-text"></p>
</div>
</nav>
<div class="container-fluid">
<form id="search-form" onsubmit="return false;">
<div class="row">
<div class="col-md-4">
<div class="btn-group" role="group" id="range-buttons">
<button type="button" class="btn btn-default range-btn active" data-range="15m">15m</button>
<button type="button" class="btn btn-default range-btn" data-range="1h">1h</button>
<button type="button" class="btn btn-default range-btn" data-range="6h">6h</button>
<button type="button" class="btn btn-default range-btn" data-range="1d">1d</button>
<button type="button" class="btn btn-default range-btn" data-range="custom">custom</button>
</div>
</div>
<div class="col-md-5">
<div class="input-group">
<input type="text" class="form-control" id="q" placeholder="fuzzy search (space-separated terms, all must match)">
<span class="input-group-btn">
<button class="btn btn-primary" type="button" id="search-btn">Search</button>
</span>
</div>
</div>
<div class="col-md-3 text-right">
<button type="button" class="btn btn-default" id="sql-toggle">SQL</button>
<button type="button" class="btn btn-default" id="tail-toggle">Tail</button>
</div>
</div>
<div class="row" id="custom-range" style="display:none; margin-top:8px;">
<div class="col-md-3">
<div class="input-group">
<span class="input-group-addon">since</span>
<input type="datetime-local" class="form-control" id="since-input" step="1">
</div>
</div>
<div class="col-md-3">
<div class="input-group">
<span class="input-group-addon">until</span>
<input type="datetime-local" class="form-control" id="until-input" step="1">
</div>
</div>
</div>
<div class="row" id="sql-row" style="display:none; margin-top:8px;">
<div class="col-md-12">
<textarea class="form-control mono" id="sql" rows="2"
placeholder="raw WHERE fragment, e.g. severity = 'error' AND message LIKE '%timeout%' (runs as readonly user, time bounds and LIMIT always enforced)"></textarea>
</div>
</div>
<div class="row" style="margin-top:8px;">
<div class="col-md-2"><select class="form-control" id="f-namespace"><option value="">namespace</option></select></div>
<div class="col-md-2"><select class="form-control" id="f-app"><option value="">app</option></select></div>
<div class="col-md-2"><select class="form-control" id="f-host"><option value="">host</option></select></div>
<div class="col-md-2"><input type="text" class="form-control" id="f-pod" placeholder="pod"></div>
<div class="col-md-1"><input type="text" class="form-control" id="f-container" placeholder="container"></div>
<div class="col-md-2">
<select class="form-control" id="f-severity">
<option value="">severity</option>
<option>fatal</option><option>error</option><option>warn</option>
<option>info</option><option>debug</option><option>trace</option>
</select>
</div>
<div class="col-md-1">
<select class="form-control" id="f-stream">
<option value="">stream</option>
<option>stdout</option><option>stderr</option>
</select>
</div>
</div>
</form>
<div class="row" style="margin-top:10px;">
<div class="col-md-6">
<p id="result-info" class="text-muted"></p>
</div>
<div class="col-md-6 text-right" id="pager">
<div class="btn-group">
<button type="button" class="btn btn-default btn-sm" id="prev-btn" disabled>&laquo; newer</button>
<button type="button" class="btn btn-default btn-sm" id="next-btn" disabled>older &raquo;</button>
</div>
<select id="page-size" class="btn btn-default btn-sm">
<option>100</option><option>200</option><option>500</option><option>1000</option>
</select>
</div>
</div>
<table class="table table-condensed table-hover" id="log-table">
<thead>
<tr>
<th class="col-ts">timestamp (UTC)</th>
<th class="col-src">namespace / pod</th>
<th class="col-sev">severity</th>
<th>message</th>
</tr>
</thead>
<tbody id="log-body"></tbody>
</table>
</div>
<script src="/static/jquery.min.js"></script>
<script src="/static/bootstrap.min.js"></script>
<script src="/static/app.js"></script>
</body>
</html>