Commit ae64afa
Add Prometheus metrics endpoint for browser VM observability (#309)
## Summary
Adds a `GET /metrics` endpoint (Prometheus text format, dedicated
listener on `METRICS_PORT`, default `10002`) so an external collector
can scrape per-VM browser and system metrics and aggregate them across
the fleet. Metrics carry no per-session or per-user labels — instance
identity is attached (and aggregated away) by the scraper.
### Chrome metrics — `lib/metrics/chrome.go`
Read browser-level over CDP (`Browser.getHistograms`,
`Browser.getVersion`, `Target.getTargets`): no page attach, no script
injection, no navigation — invisible to automation running in the
browser.
| Metric | Meaning |
| --- | --- |
| `kernel_chromium_uma{histogram=...}` | Prometheus histogram
(`_bucket`/`_sum`/`_count`) per curated UMA histogram: FCP, LCP,
DOMContentLoaded, load event, parse start (~TTFB), INP, FID,
interactions per page, CLS, per-page CPU ms and
browser-start-to-first-paint. Cumulative since browser start, so
`rate()`/`increase()` work naturally. |
| `kernel_chromium_up` | CDP endpoint reachable and responsive |
| `kernel_chromium_info{product}` | Browser version (fleet version
distribution) |
| `kernel_chromium_pages` | Open page targets (tabs) |
Chrome's sparse `[low, high)` UMA buckets map to cumulative `le` bounds;
the overflow bucket folds into `+Inf`. The curated list lives in
`DefaultUMAHistograms`; histograms with no samples yet are simply
absent.
### VM / process metrics — `lib/metrics/system.go`
`kernel_vm_cpu_seconds_total{mode}`,
`kernel_vm_memory_{total,available}_bytes`, `kernel_vm_load1`,
`kernel_vm_uptime_seconds` (monotonic — excludes suspended time),
`kernel_vm_network_{receive,transmit}_bytes_total`,
`kernel_vm_disk_{total,free}_bytes{mount="/"}`,
`kernel_chromium_processes`, `kernel_chromium_memory_rss_bytes`.
## Design notes
- **Separate listener, no scale-to-zero middleware**: periodic scrapes
must not count as session activity or hold VMs awake. Also skips
per-request logging so scrapes don't drown the logs.
- **Not in the OpenAPI spec**: this is an internal scrape surface,
following the `/internal/fork-identity` precedent.
- **Browser-process histograms only**: `Browser.getHistograms` cannot
see renderer/child-process histograms (`EventLatency.*`,
`Graphics.Smoothness.*`, `Blink.*`) — those only merge into the
browser's recorder when a UMA log is staged or `chrome://histograms` is
opened, neither of which happens on these images. Verified empirically
against a live browser; the curated set is restricted to histograms that
populate without a sync.
- **No new dependencies**: the exposition format is rendered directly;
scrape cost is a few small CDP round-trips + `/proc` reads (~50ms).
- New `cdpclient` methods `GetHistograms` and `CountPageTargets` follow
the existing minimal-client patterns.
## Test plan
- [x] Unit tests for exposition rendering/escaping, UMA→Prometheus
bucket conversion (incl. exact-name filtering and overflow fold), /proc
parsers, and the chrome collector against a fake CDP server
- [x] Full unit suite (`go vet` + `go test -race`, non-e2e) passes
- [x] Validated the real `cmd/api` binary inside two live browser VMs
(both image variants) on alternate ports: all metric families present
with sane values; page-load histogram sums matched values read
independently via raw CDP
- [ ] e2e suite not run (requires pre-built Docker images); the endpoint
has no interaction with existing routes
## Sample output (from a live VM)
```
kernel_chromium_up 1
kernel_chromium_info{product="Chrome/145.0.7632.75"} 1
kernel_chromium_pages 1
kernel_chromium_uma_bucket{histogram="PageLoad.PaintTiming.NavigationToFirstContentfulPaint",le="885"} 1
kernel_chromium_uma_bucket{histogram="PageLoad.PaintTiming.NavigationToFirstContentfulPaint",le="1240"} 2
kernel_chromium_uma_bucket{histogram="PageLoad.PaintTiming.NavigationToFirstContentfulPaint",le="+Inf"} 2
kernel_chromium_uma_sum{histogram="PageLoad.PaintTiming.NavigationToFirstContentfulPaint"} 1977
kernel_chromium_uma_count{histogram="PageLoad.PaintTiming.NavigationToFirstContentfulPaint"} 2
kernel_vm_cpu_seconds_total{mode="user"} 75.1
kernel_chromium_processes 8
kernel_chromium_memory_rss_bytes 1.277575168e+09
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> New exposed listener and CDP/nvidia-smi work on each scrape add
operational surface and load; metrics path is intentionally isolated
from scale-to-zero and the public API.
>
> **Overview**
> Adds a **dedicated Prometheus scrape surface** on `METRICS_PORT`
(default `10002`) with `GET /metrics`, wired in `cmd/api` on its own
HTTP listener **without** scale-to-zero middleware or request logging so
periodic scrapes do not keep VMs awake or flood logs.
>
> Introduces **`lib/metrics`** with a custom text exposition handler
(serialized scrapes, per-collector timeouts, failed collectors dropped
without partial families) and three collectors: **Chrome**
(browser-level CDP: `kernel_chromium_up`, version, page count, curated
UMA histograms re-bucketed for fleet aggregation), **system** (`/proc`
CPU/memory/load/PSI/network/disk plus Chromium process count/RSS), and
**GPU** (`nvidia-smi` when present, else `kernel_gpu_present 0`).
>
> Extends **`cdpclient`** with `GetHistograms` and `CountPageTargets`.
Config and README document `METRICS_PORT`; config tests cover the new
default and override.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f993190. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: sjmiller609 <7516283+sjmiller609@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>1 parent de7e9a4 commit ae64afa
13 files changed
Lines changed: 1285 additions & 0 deletions
File tree
- server
- cmd
- api
- config
- lib
- cdpclient
- metrics
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
| |||
257 | 258 | | |
258 | 259 | | |
259 | 260 | | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
260 | 277 | | |
261 | 278 | | |
262 | 279 | | |
| |||
281 | 298 | | |
282 | 299 | | |
283 | 300 | | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
284 | 309 | | |
285 | 310 | | |
286 | 311 | | |
| |||
308 | 333 | | |
309 | 334 | | |
310 | 335 | | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
311 | 339 | | |
312 | 340 | | |
313 | 341 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
16 | 21 | | |
17 | 22 | | |
18 | 23 | | |
| |||
57 | 62 | | |
58 | 63 | | |
59 | 64 | | |
| 65 | + | |
60 | 66 | | |
61 | 67 | | |
62 | 68 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| 41 | + | |
40 | 42 | | |
41 | 43 | | |
42 | 44 | | |
| |||
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
| 56 | + | |
54 | 57 | | |
55 | 58 | | |
56 | 59 | | |
| |||
73 | 76 | | |
74 | 77 | | |
75 | 78 | | |
| 79 | + | |
76 | 80 | | |
77 | 81 | | |
78 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
133 | 133 | | |
134 | 134 | | |
135 | 135 | | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
136 | 194 | | |
137 | 195 | | |
138 | 196 | | |
| |||
0 commit comments