Timeline: scales, graduations, tooltip, colors, live updates and zoom
The histogram above the list becomes a configurable timeline: - Scale linear / sqrt (default) / log, height S/M/L, bars or area. - Colors: stacked by severity (error+, warning, the rest), intensity against the median of the window (calm, burst >3x, anomaly >10x), or none; colors are CSS variables with light and dark values. - Vertical graduations on round local times (hh:mm:ss, hh:mm, dd/mm). - Tooltip: interval bounds, total and detail per severity. - Division automatic (~100 intervals) or fixed (1 s to 1 day), capped at 300 intervals by the server; intervals aligned on the local time zone. - Refresh off / 5 s / 15 s / 30 s / 1 min / at each new interval. In live mode the last interval is incremented from the SSE stream and the timeline reloads at each new interval; paused while the tab is hidden. - Click a bar to zoom on its interval, drag to zoom on a selection: the list, export and counters follow through new from/to parameters. The timeline now runs on the server clock (bounds and "now" come from /api/histogram): the axis was drawn from the browser clock and refreshed every 30 s only, so a clock difference with the server or a hidden tab left it behind the logs. /api/histogram returns interval indexes with the count per severity; the division logic lives in histogram.go with table-driven tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
32593b9515
commit
ebf6340ff0
8 files changed
+1119
-86
No files matched your search
@@ -142,40 +142,6 @@ func (a *API) logs(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
var histSteps = map[string]struct {
|
||||
expr string
|
||||
sec int
|
||||
}{
|
||||
"5m": {"10s", 10}, "15m": {"30s", 30}, "1h": {"1m", 60}, "6h": {"5m", 300},
|
||||
"24h": {"15m", 900}, "7d": {"2h", 7200}, "30d": {"12h", 43200},
|
||||
}
|
||||
|
||||
// GET /api/histogram: message count per time bucket.
|
||||
func (a *API) histogram(w http.ResponseWriter, r *http.Request) {
|
||||
f := FilterFromRequest(r)
|
||||
step, ok := histSteps[f.Range]
|
||||
if !ok {
|
||||
step.expr, step.sec = "1d", 86400
|
||||
}
|
||||
filter, _ := f.LogsQL()
|
||||
q := filter + " | stats by (_time:" + step.expr + ") count() hits | sort by (_time)"
|
||||
rows, err := a.store.Query(r.Context(), q)
|
||||
if err != nil {
|
||||
writeErr(w, queryStatus(err), err)
|
||||
return
|
||||
}
|
||||
type bucket struct {
|
||||
T string `json:"t"`
|
||||
N int64 `json:"n"`
|
||||
}
|
||||
out := make([]bucket, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
t, _ := row["_time"].(string)
|
||||
out = append(out, bucket{T: t, N: toInt(row["hits"])})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"step": step.sec, "buckets": out})
|
||||
}
|
||||
|
||||
func toInt(v any) int64 {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
|
||||
Reference in new issue
Block a user