feat(monitoring): add Prometheus and Grafana integration - #1
feat(monitoring): add Prometheus and Grafana integration#1devin-ai-integration[bot] wants to merge 1 commit into
Conversation
Adds a first-class Monitoring surface to concave-web. Business logic stays in concave; concave-web remains a same-origin proxy plus presentation layer, consistent with CONTRIBUTING.md. Backend: - internal/config: add optional PrometheusURL and GrafanaURL fields, snake_case JSON tags (also fixes prior PascalCase JSON output for existing fields), expanded round-trip tests. - internal/proxy: add NewPath(target, stripPrefix) that strips a path prefix before forwarding, preserving SSE/WebSocket and cookies. - main.go: mount /monitoring/prometheus/* and /monitoring/grafana/* reverse proxies when the corresponding URLs are configured. - http.go: documents that monitoring URLs are optional; empty values simply skip mounting the route. Frontend: - types.ts: WebSettings gains prometheus_url and grafana_url, plus PromQL and monitoring shape types. - lib/monitoring.ts: probeMonitoring(), promQuery(), firstVectorValue(), detectMonitoringSuites(). - views/MonitoringView.vue: status hero, PromQL snapshot cards (targets, CPU, memory, filesystem, GPU), quick-open buttons, and an optional embedded Grafana iframe behind a toggle. - router + AppShell: /monitoring route gated at viewer, nav entry. - AppIcon: new monitoring glyph. - SettingsView: Monitoring URL fields. - test/monitoring.integration.test.ts: unit coverage for helpers and a gated integration test of the view behind CONCAVE_INTEGRATION=1.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| onMounted(async () => { | ||
| await refresh() | ||
| refreshTimer = window.setInterval(refresh, 15_000) | ||
| }) |
There was a problem hiding this comment.
🟡 setInterval timer leaks when component unmounts during initial async refresh
The onMounted callback awaits refresh() before setting refreshTimer (MonitoringView.vue:209-212). Since refresh() makes multiple network requests with 4-second timeouts, it can take several seconds. If the user navigates away during this window, onBeforeUnmount runs while refreshTimer is still null, so clearInterval is never called. After refresh() eventually resolves, setInterval is set up on an already-unmounted component — the timer is never cleaned up, causing persistent background network requests (6+ fetches every 15 seconds) that accumulate each time the user visits and leaves the page.
Prompt for agents
In MonitoringView.vue, the onMounted callback awaits refresh() before setting up setInterval, creating a race with onBeforeUnmount. The fix should track whether the component is still mounted using a boolean flag (e.g., `let mounted = true`) set to false in onBeforeUnmount. After await refresh() completes, check the flag before calling setInterval. Also, the onBeforeUnmount cleanup should clear the timer if it exists. A cleaner approach: set up the interval immediately (not after await), or use a flag to guard the setInterval call. For example:
let mounted = true
onMounted(async () => {
await refresh()
if (mounted) {
refreshTimer = window.setInterval(refresh, 15_000)
}
})
onBeforeUnmount(() => {
mounted = false
if (refreshTimer !== null) {
window.clearInterval(refreshTimer)
}
})
This ensures the interval is never started if the component has already been unmounted.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Adds a first-class Monitoring surface to
concave-webthat presents the Prometheus (:9090) and Grafana (:3000) containers already shipped with the Flow suite (and selectable in Forge). Business logic stays inconcave; the web binary remains a same-origin proxy plus presentation layer, consistent withCONTRIBUTING.md.Backend (Go)
internal/config: new optionalPrometheusURL/GrafanaURLfields with sensible defaults (http://127.0.0.1:9090,http://127.0.0.1:3000). The existingConfigfields also gainjson:"snake_case"tags — before this change/settingsreturnedAPIBaseURL-style keys while the Vue form expectedapi_base_url, so the settings form silently did not round-trip.internal/proxy: newNewPath(target, stripPrefix)reverse proxy that strips a URL prefix before forwarding, preserving SSE / WebSocket / cookie behaviour inherited from the existing proxy. Covered by new unit tests.main.go: mounts/monitoring/prometheus/*→ Prometheus and/monitoring/grafana/*→ Grafana when the corresponding URLs are configured. Empty URL ⇒ route not mounted, so this is zero-impact for users who have not installed Flow/Forge.http.go: documents that monitoring URLs are optional on the settings POST path.Frontend (Vue 3 + TS)
types.ts:WebSettingsgainsprometheus_url/grafana_url; addsPromQLInstantResponse,MonitoringReachability,MonitoringSuiteHint.lib/monitoring.ts:probeMonitoring(),promQuery(),firstVectorValue(),detectMonitoringSuites(). No new npm deps — usesfetchandAbortController.views/MonitoringView.vue: status hero (Prometheus + Grafana reachability + version), PromQL snapshot cards (up, CPU busy, node memory, root FS, DCGM GPU util), quick-open buttons, and an optional embedded Grafana iframe behind a toggle. Refreshes every 15 s. Falls back gracefully when the Flow suite is not installed (surfaces a link to/suites).router+components/layout/AppShell.vue:/monitoringroute gated atviewer, nav entry between Fleet and Suites.components/AppIcon.vue: newmonitoringglyph, same stroke style as existing icons.views/SettingsView.vue: Prometheus / Grafana URL fields with validation; empty values disable the proxy route on next restart.test/monitoring.integration.test.ts: unit coverage for helpers (always on) + integration test of the view behindCONCAVE_INTEGRATION=1, matching the existing gating convention.Out of scope for this PR (follow-ups requested by MUHAMMAD, tracked as separate PRs):
concave-tui(separate PR).concave+concave-web+concave-tui).Review & Testing Checklist for Human
Risk: yellow — adds a new proxy surface and new first-class view, but all new routes are behind optional config fields and cannot break existing behaviour when Prometheus/Grafana are not configured.
concave install flow && concave start flow), reload the web UI, and confirm the Prometheus/Grafana reachability badges flip toreachableand the PromQL cards populate./settingsdoes not break any external automation that hits the settings endpoint.Notes
go.modorpackage.json, perCONTRIBUTING.md.sandbox="allow-same-origin allow-scripts allow-forms allow-popups". If your Grafana enforces strict CSP frame-ancestors, setallow_embedding = trueingrafana.inior just use the "Pop out" button.Link to Devin session: https://app.devin.ai/sessions/5d19efa113054ca4953d9ed9309ce705
Requested by: @ElFariss