Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review
# BROWSERLESS_QUEUED=4 # (--profile visual-review) max requests waiting for a session.
# BROWSERLESS_TIMEOUT_MS=30000 # (--profile visual-review) per-session timeout.
# BROWSERLESS_MEM_LIMIT=2g # (--profile visual-review) container memory cap.
# BROWSERLESS_EXPORTER_MEM_LIMIT=64m # (--profile visual-review) browserless-exporter sidecar cap (#5368)
# REVIEW_AUDIT_DIR= # persist visual-review screenshot PNGs to this filesystem path
# # instead of re-rendering on demand. Unset = re-render each time.
# # Only relevant when BROWSER_WS_ENDPOINT above is set. Ignored
Expand Down
34 changes: 34 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,40 @@ services:
start_period: 15s
retries: 5

# browserless's own /metrics?token=<token> endpoint returns JSON (a rolling history of 5-minute-window
# snapshots), not Prometheus text -- this sidecar polls it and re-exposes the newest snapshot's fields as
# real Prometheus gauges (#5368), same "translate non-Prometheus output" pattern as backup-exporter above.
# Gated on the same --profile visual-review as browserless itself; meaningless without it running.
browserless-exporter:
image: alpine:3.20
restart: unless-stopped
<<: *default-logging
profiles: ["visual-review"]
depends_on:
browserless:
condition: service_healthy
environment:
BROWSERLESS_METRICS_URL: "http://browserless:3000/metrics"
BROWSERLESS_TOKEN: "${BROWSERLESS_TOKEN:-}"
volumes:
# Directory mount, not a single-file one -- same #reporting-exporter-stale-bind-mount rationale as
# the backup-exporter service above.
- ./scripts:/scripts:ro
expose:
- "9102"
command:
- /bin/sh
- -c
- "apk add --no-cache jq busybox-extras >/dev/null 2>&1 && sh /scripts/browserless-metrics.sh"
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9102/metrics | grep -q '^browserless_exporter_last_scrape_success'"]
interval: 30s
retries: 5
deploy:
resources:
limits:
memory: "${BROWSERLESS_EXPORTER_MEM_LIMIT:-64m}"

# ── REES / review-enrichment (--profile rees) ──────────────────────────────
# Self-hosted alternative to pointing REES_URL at a managed/external instance (see the self-hosting docs).
# REES runs the heavier PR analysis (dependency CVEs, secret scan, license, EOL runtime, lockfile drift, IaC
Expand Down
129 changes: 129 additions & 0 deletions grafana/dashboards/browserless-metrics.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{
"__inputs": [],
"__requires": [
{ "type": "grafana", "id": "grafana", "name": "Grafana", "version": "10.0.0" },
{ "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }
],
"annotations": { "list": [] },
"editable": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"description": "browserless (headless-Chromium visual-review screenshot capture, --profile visual-review, #5368) queue depth, concurrency, and rejection/error/timeout rate, translated from its own JSON /metrics by the browserless-exporter sidecar. Absent/empty unless the visual-review profile is active.",
"panels": [
{ "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Exporter health", "type": "row" },
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [
{ "type": "value", "options": { "0": { "text": "FAILED", "color": "red" }, "1": { "text": "OK", "color": "green" } } }
],
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "green", "value": 1 }] },
"noValue": "no data"
}
},
"gridPos": { "h": 5, "w": 8, "x": 0, "y": 1 },
"id": 2,
"options": { "colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "Last scrape of browserless's own /metrics",
"description": "0 means the exporter's most recent poll of browserless itself failed (wrong token, browserless down, or network issue) — the panels below are then serving stale data.",
"type": "stat",
"targets": [{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_exporter_last_scrape_success", "legendFormat": "scrape ok", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, "unit": "dtdurations" } },
"gridPos": { "h": 5, "w": 8, "x": 8, "y": 1 },
"id": 3,
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "Time since browserless's own sample",
"description": "browserless_sample_timestamp_seconds is the timestamp browserless itself assigned to its newest 5-minute window. A large, growing value here means browserless has stopped reporting new windows even if the exporter's own scrape is succeeding.",
"type": "stat",
"targets": [{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "time() - browserless_sample_timestamp_seconds", "legendFormat": "age", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, "unit": "ms" } },
"gridPos": { "h": 5, "w": 8, "x": 16, "y": 1 },
"id": 4,
"options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "auto" },
"title": "Mean session time",
"type": "stat",
"targets": [{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_session_mean_time_ms", "legendFormat": "mean", "refId": "A" }]
},

{ "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 }, "id": 10, "title": "Queue, concurrency & outcomes", "type": "row" },
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 } } },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 7 },
"id": 11,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi", "sort": "desc" } },
"title": "Queue depth & concurrency",
"description": "queued: sessions waiting over the configured concurrency limit. running: sessions active at the sample point. max_concurrent: peak concurrent sessions in the window.",
"type": "timeseries",
"targets": [
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_queued", "legendFormat": "queued", "refId": "A" },
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_running", "legendFormat": "running", "refId": "B" },
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_max_concurrent", "legendFormat": "max_concurrent", "refId": "C" }
]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 } } },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 7 },
"id": 12,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi", "sort": "desc" } },
"title": "Session outcomes by window",
"description": "Per-5-minute-window counts from browserless's own rolling buffer — a non-zero, sustained rejected/error/timeout/unhealthy count means capture is degrading, not just occasionally flaky.",
"type": "timeseries",
"targets": [
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_successful", "legendFormat": "successful", "refId": "A" },
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_rejected", "legendFormat": "rejected", "refId": "B" },
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_errors", "legendFormat": "error", "refId": "C" },
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_timedout", "legendFormat": "timedout", "refId": "D" },
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_unauthorized", "legendFormat": "unauthorized", "refId": "E" },
{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_unhealthy", "legendFormat": "unhealthy", "refId": "F" }
]
},

{ "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 15 }, "id": 20, "title": "Resource utilization", "type": "row" },
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 }, "unit": "percentunit", "max": 1, "min": 0 } },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
"id": 21,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi", "sort": "desc" } },
"title": "CPU utilization",
"type": "timeseries",
"targets": [{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_cpu_ratio", "legendFormat": "cpu", "refId": "A" }]
},
{
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "lineWidth": 2, "fillOpacity": 10 }, "unit": "percentunit", "max": 1, "min": 0 } },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
"id": 22,
"options": { "legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom" }, "tooltip": { "mode": "multi", "sort": "desc" } },
"title": "Memory utilization",
"type": "timeseries",
"targets": [{ "datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" }, "expr": "browserless_memory_ratio", "legendFormat": "memory", "refId": "A" }]
}
],
"refresh": "30s",
"schemaVersion": 38,
"tags": ["gittensory", "browserless", "observability"],
"templating": {
"list": [
{ "current": {}, "hide": 0, "includeAll": false, "name": "DS_PROMETHEUS", "options": [], "query": "prometheus", "refresh": 1, "type": "datasource" }
]
},
"time": { "from": "now-6h", "to": "now" },
"timepicker": {},
"timezone": "browser",
"title": "Gittensory — Browserless (visual review)",
"uid": "gittensory-browserless",
"version": 1
}
2 changes: 1 addition & 1 deletion grafana/dashboards/resource-hub.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"gridPos": { "h": 13, "w": 12, "x": 12, "y": 0 },
"options": {
"mode": "markdown",
"content": "## 📊 Dashboards\n- **[Upstream PRs & issues (GitHub)](/d/gittensory-github)** — live, accurate census + open-PR triage (GitHub API).\n- **[Reviews & PRs (maintainer)](/d/gittensory-maintainer)** — gittensory's own review activity + reviewed-PR log.\n- **[AI usage](/d/gittensory-ai-usage)** — durable cross-provider ai_usage_events (filterable by provider/feature/model), live Prometheus counters, and Claude Code's own OTEL session telemetry, all in one place.\n- **[Gittensory (infra)](/d/gittensory)** — queue, jobs, HTTP, GitHub API cache/rate limits.\n- **[GPU metrics](/d/gittensory-gpu)** — utilization/VRAM for a self-hosted Ollama GPU box.\n- **[Infra health](/d/gittensory-infra-health)** — host CPU/mem/disk/network (node-exporter), per-container resource usage (cAdvisor), Redis, Qdrant, and whether the observability stack itself is up.\n- **[REES (review-enrichment)](/d/gittensory-rees)** — request outcomes/latency and per-analyzer run/timeout/degrade rates for the optional `--profile rees` service.\n\n## 📈 Metrics & logs\n- **Prometheus** — [targets](http://localhost:9090/targets) · [graph](http://localhost:9090)\n- **Alertmanager** — [alerts](http://localhost:9093)\n- **Loki** — query in [Explore](/explore) (pick the *Loki* datasource), e.g. `{compose_service=\"gittensory\"}`\n- **Sentry** — release/source-map enriched errors. Edit the dashboard link if your project URL differs.\n\n## 🩺 Quick health checks\n| What | Where |\n|---|---|\n| App serving | `GET /ready` → 200 |\n| AI wired | boot log `selfhost_ai_provider` |\n| Embeds wired | boot log `selfhost_embed_provider` |\n| Vectors wired | boot log `selfhost_vectorize` |\n| Token spend | **[AI usage](/d/gittensory-ai-usage)** dashboard |\n\n## 📚 Docs\n- [Maintainer self-hosting](https://gittensory.aethereal.dev/docs/maintainer-self-hosting) — setup, configuration, AI, REES, RAG, operations, and troubleshooting."
"content": "## 📊 Dashboards\n- **[Upstream PRs & issues (GitHub)](/d/gittensory-github)** — live, accurate census + open-PR triage (GitHub API).\n- **[Reviews & PRs (maintainer)](/d/gittensory-maintainer)** — gittensory's own review activity + reviewed-PR log.\n- **[AI usage](/d/gittensory-ai-usage)** — durable cross-provider ai_usage_events (filterable by provider/feature/model), live Prometheus counters, and Claude Code's own OTEL session telemetry, all in one place.\n- **[Gittensory (infra)](/d/gittensory)** — queue, jobs, HTTP, GitHub API cache/rate limits.\n- **[GPU metrics](/d/gittensory-gpu)** — utilization/VRAM for a self-hosted Ollama GPU box.\n- **[Infra health](/d/gittensory-infra-health)** — host CPU/mem/disk/network (node-exporter), per-container resource usage (cAdvisor), Redis, Qdrant, and whether the observability stack itself is up.\n- **[REES (review-enrichment)](/d/gittensory-rees)** — request outcomes/latency and per-analyzer run/timeout/degrade rates for the optional `--profile rees` service.\n- **[Browserless (visual review)](/d/gittensory-browserless)** — queue depth, concurrency, and rejection/error/timeout rate for the optional `--profile visual-review` screenshot service.\n\n## 📈 Metrics & logs\n- **Prometheus** — [targets](http://localhost:9090/targets) · [graph](http://localhost:9090)\n- **Alertmanager** — [alerts](http://localhost:9093)\n- **Loki** — query in [Explore](/explore) (pick the *Loki* datasource), e.g. `{compose_service=\"gittensory\"}`\n- **Sentry** — release/source-map enriched errors. Edit the dashboard link if your project URL differs.\n\n## 🩺 Quick health checks\n| What | Where |\n|---|---|\n| App serving | `GET /ready` → 200 |\n| AI wired | boot log `selfhost_ai_provider` |\n| Embeds wired | boot log `selfhost_embed_provider` |\n| Vectors wired | boot log `selfhost_vectorize` |\n| Token spend | **[AI usage](/d/gittensory-ai-usage)** dashboard |\n\n## 📚 Docs\n- [Maintainer self-hosting](https://gittensory.aethereal.dev/docs/maintainer-self-hosting) — setup, configuration, AI, REES, RAG, operations, and troubleshooting."
}
}
]
Expand Down
9 changes: 9 additions & 0 deletions prometheus/prometheus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ scrape_configs:
- targets: ["rees:8080"]
scrape_interval: 15s

# browserless queue/concurrency/rejection/error metrics, translated from its own JSON /metrics by the
# browserless-exporter sidecar (--profile visual-review, #5368). Same BusyBox-httpd-wrapper shape as
# gittensory-backup above, so it needs the same explicit scrape protocol fallback.
- job_name: browserless
fallback_scrape_protocol: PrometheusText0.0.4
static_configs:
- targets: ["browserless-exporter:9102"]
scrape_interval: 30s

# Self-monitoring: the observability stack watching itself (--profile observability, #5366). Every
# component here ships its own native Prometheus /metrics — previously scraped by nothing, so a stack
# component silently degrading (e.g. Loki dropping ingested lines, Alertmanager failing to notify) had
Expand Down
Loading
Loading