From 2c28fb6642a99b97a287f0d5e2082dbaa816a9f5 Mon Sep 17 00:00:00 2001 From: sjmiller609 <7516283+sjmiller609@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:22:26 +0000 Subject: [PATCH] Keep chrome essentials on histogram failure; gate PID walk on deadline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A UMA histogram fetch failure now logs and skips only the histogram family instead of discarding the whole chrome scrape — the up, info, and pages samples survive, so browser-up alerting keeps its signal. The system collector checks the scrape deadline before starting the /proc PID walk. Co-Authored-By: Claude Opus 4.7 --- server/cmd/api/main.go | 2 +- server/lib/metrics/chrome.go | 12 +++++++++--- server/lib/metrics/chrome_test.go | 15 +++++++++++---- server/lib/metrics/system.go | 5 +++++ 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index babb8f40..ff6ab49e 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -265,7 +265,7 @@ func main() { rMetrics := chi.NewRouter() rMetrics.Use(chiMiddleware.Recoverer) rMetrics.Method(http.MethodGet, "/metrics", metrics.Handler(slogger, - metrics.NewChromeCollector(upstreamMgr), + metrics.NewChromeCollector(upstreamMgr, slogger), metrics.NewGPUCollector(), metrics.NewSystemCollector(), )) diff --git a/server/lib/metrics/chrome.go b/server/lib/metrics/chrome.go index 6b5c6f79..48fa19e2 100644 --- a/server/lib/metrics/chrome.go +++ b/server/lib/metrics/chrome.go @@ -3,6 +3,7 @@ package metrics import ( "context" "errors" + "log/slog" "math" "strconv" "strings" @@ -78,10 +79,11 @@ type DevToolsUpstream interface { type ChromeCollector struct { upstream DevToolsUpstream histograms []UMAHistogram + log *slog.Logger } -func NewChromeCollector(upstream DevToolsUpstream) *ChromeCollector { - return &ChromeCollector{upstream: upstream, histograms: DefaultUMAHistograms} +func NewChromeCollector(upstream DevToolsUpstream, log *slog.Logger) *ChromeCollector { + return &ChromeCollector{upstream: upstream, histograms: DefaultUMAHistograms, log: log} } func (c *ChromeCollector) Name() string { return "chrome" } @@ -109,9 +111,13 @@ func (c *ChromeCollector) Collect(ctx context.Context, w *Writer) error { w.Sample("kernel_chromium_pages", nil, float64(pages)) } + // A histogram fetch failure must not discard the up/info/pages + // samples already gathered: log it, skip the UMA family for this + // scrape, and report success for the rest. fetched, err := c.fetchHistograms(ctx, client) if err != nil { - return err + c.log.Error("chrome histogram fetch failed", "err", err) + return nil } w.Metric("kernel_chromium_uma", "Chrome UMA histogram, cumulative since browser start, re-bucketed onto fixed bounds. Units follow the UMA definition: PageLoad timings are milliseconds, NumInteractions is a count, CLS is a unitless score, Memory.GPU.* is MB.", diff --git a/server/lib/metrics/chrome_test.go b/server/lib/metrics/chrome_test.go index 324908cd..af6541ee 100644 --- a/server/lib/metrics/chrome_test.go +++ b/server/lib/metrics/chrome_test.go @@ -3,6 +3,7 @@ package metrics import ( "context" "encoding/json" + "log/slog" "net/http" "net/http/httptest" "strings" @@ -91,7 +92,7 @@ func TestChromeCollector(t *testing.T) { srv := fakeCDP(t) defer srv.Close() - c := NewChromeCollector(staticUpstream("ws" + strings.TrimPrefix(srv.URL, "http"))) + c := NewChromeCollector(staticUpstream("ws"+strings.TrimPrefix(srv.URL, "http")), slog.New(slog.DiscardHandler)) w := &Writer{} require.NoError(t, c.Collect(context.Background(), w)) out := string(w.Bytes()) @@ -119,18 +120,24 @@ func TestChromeCollector(t *testing.T) { func TestChromeCollectorDown(t *testing.T) { // An unreachable browser is not a collector error: the up sample must // survive the handler's discard-on-error policy. - c := NewChromeCollector(staticUpstream("")) + c := NewChromeCollector(staticUpstream(""), slog.New(slog.DiscardHandler)) w := &Writer{} require.NoError(t, c.Collect(context.Background(), w)) assert.Contains(t, string(w.Bytes()), "kernel_chromium_up 0\n") } func TestChromeCollectorHistogramFailure(t *testing.T) { + // A histogram fetch failure must not take down the whole chrome + // scrape: up/info/pages survive and only the UMA family is skipped. srv := fakeCDP(t) defer srv.Close() - c := NewChromeCollector(staticUpstream("ws" + strings.TrimPrefix(srv.URL, "http"))) + c := NewChromeCollector(staticUpstream("ws"+strings.TrimPrefix(srv.URL, "http")), slog.New(slog.DiscardHandler)) c.histograms = []UMAHistogram{{Name: "Fail.Me", Bounds: boundsPageLoadMs}} w := &Writer{} - require.Error(t, c.Collect(context.Background(), w)) + require.NoError(t, c.Collect(context.Background(), w)) + out := string(w.Bytes()) + assert.Contains(t, out, "kernel_chromium_up 1\n") + assert.Contains(t, out, "kernel_chromium_pages 2\n") + assert.NotContains(t, out, "kernel_chromium_uma") } diff --git a/server/lib/metrics/system.go b/server/lib/metrics/system.go index 0db76f3e..d3150387 100644 --- a/server/lib/metrics/system.go +++ b/server/lib/metrics/system.go @@ -104,6 +104,11 @@ func (c *SystemCollector) Collect(ctx context.Context, w *Writer) error { w.Sample("kernel_vm_disk_free_bytes", []Label{{"mount", c.rootPath}}, float64(fs.Bavail)*bsize) } + // The /proc reads above are microseconds; the PID walk is the only + // part worth gating on an expired scrape deadline. + if ctx.Err() != nil { + return ctx.Err() + } procs, rssBytes := c.chromiumProcesses() w.Metric("kernel_chromium_processes", "Number of running Chromium processes (browser, renderers, utilities).", "gauge") w.Sample("kernel_chromium_processes", nil, float64(procs))