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
@@ -5,20 +5,34 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Filter holds the search criteria sent by the UI.
|
||||
type Filter struct {
|
||||
Mode string // "simple" or "logsql"
|
||||
Text string
|
||||
Range string // 5m, 15m, 1h, 6h, 24h, 7d, 30d; anything else = no time limit
|
||||
Range string // 5m, 15m, 1h, 6h, 24h, 7d, 30d; anything else = no time limit
|
||||
From, To time.Time // absolute range (timeline zoom), replaces Range when From is set
|
||||
Host string
|
||||
App string
|
||||
Severity int // highest severity number included (0 = emerg … 7 = debug), -1 = all
|
||||
Source string // "syslog", "docker" or "" for all
|
||||
}
|
||||
|
||||
var validRanges = map[string]bool{"5m": true, "15m": true, "1h": true, "6h": true, "24h": true, "7d": true, "30d": true}
|
||||
var rangeDurations = map[string]time.Duration{
|
||||
"5m": 5 * time.Minute, "15m": 15 * time.Minute, "1h": time.Hour, "6h": 6 * time.Hour,
|
||||
"24h": 24 * time.Hour, "7d": 7 * 24 * time.Hour, "30d": 30 * 24 * time.Hour,
|
||||
}
|
||||
|
||||
// msParam reads a Unix time in milliseconds (zero time when absent or invalid).
|
||||
func msParam(v string) time.Time {
|
||||
ms, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil || ms <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.UnixMilli(ms)
|
||||
}
|
||||
|
||||
func FilterFromRequest(r *http.Request) Filter {
|
||||
q := r.URL.Query()
|
||||
@@ -34,6 +48,13 @@ func FilterFromRequest(r *http.Request) Filter {
|
||||
if v, err := strconv.Atoi(q.Get("severity")); err == nil && v >= 0 && v <= 7 {
|
||||
f.Severity = v
|
||||
}
|
||||
// from/to: Unix milliseconds, sent when the timeline is zoomed.
|
||||
if from := msParam(q.Get("from")); !from.IsZero() {
|
||||
f.From, f.To = from, msParam(q.Get("to"))
|
||||
if !f.To.IsZero() && !f.To.After(f.From) {
|
||||
f.To = time.Time{}
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -83,7 +104,12 @@ func parseTerms(s string) []term {
|
||||
// filterExpr turns the criteria (except raw LogsQL text) into a LogsQL filter.
|
||||
func (f Filter) filterExpr() string {
|
||||
var parts []string
|
||||
if validRanges[f.Range] {
|
||||
switch {
|
||||
case !f.From.IsZero() && !f.To.IsZero():
|
||||
parts = append(parts, "_time:["+logsqlTime(f.From)+", "+logsqlTime(f.To)+")")
|
||||
case !f.From.IsZero():
|
||||
parts = append(parts, "_time:>="+logsqlTime(f.From))
|
||||
case rangeDurations[f.Range] > 0:
|
||||
parts = append(parts, "_time:"+f.Range)
|
||||
}
|
||||
if f.Host != "" {
|
||||
@@ -119,6 +145,8 @@ func (f Filter) filterExpr() string {
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func logsqlTime(t time.Time) string { return t.UTC().Format(time.RFC3339Nano) }
|
||||
|
||||
// LogsQL returns the filter part and any pipes ("| …") typed in LogsQL mode.
|
||||
func (f Filter) LogsQL() (filter, pipes string) {
|
||||
filter = f.filterExpr()
|
||||
|
||||
Reference in new issue
Block a user