Skip to content

feat(monitoring): add Prometheus and Grafana integration - #1

Open
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1777384731-monitoring-integration
Open

feat(monitoring): add Prometheus and Grafana integration#1
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1777384731-monitoring-integration

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

Adds a first-class Monitoring surface to concave-web that presents the Prometheus (:9090) and Grafana (:3000) containers already shipped with the Flow suite (and selectable in Forge). Business logic stays in concave; the web binary remains a same-origin proxy plus presentation layer, consistent with CONTRIBUTING.md.

Backend (Go)

  • internal/config: new optional PrometheusURL / GrafanaURL fields with sensible defaults (http://127.0.0.1:9090, http://127.0.0.1:3000). The existing Config fields also gain json:"snake_case" tags — before this change /settings returned APIBaseURL-style keys while the Vue form expected api_base_url, so the settings form silently did not round-trip.
  • internal/proxy: new NewPath(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: WebSettings gains prometheus_url / grafana_url; adds PromQLInstantResponse, MonitoringReachability, MonitoringSuiteHint.
  • lib/monitoring.ts: probeMonitoring(), promQuery(), firstVectorValue(), detectMonitoringSuites(). No new npm deps — uses fetch and AbortController.
  • 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: /monitoring route gated at viewer, nav entry between Fleet and Suites.
  • components/AppIcon.vue: new monitoring glyph, 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 behind CONCAVE_INTEGRATION=1, matching the existing gating convention.

Out of scope for this PR (follow-ups requested by MUHAMMAD, tracked as separate PRs):

  • TUI parity in concave-tui (separate PR).
  • Ephemeral JupyterLab/Hub environments with TTL + persistent-disk tiering (separate multi-repo PR touching 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.

  • Verify that with no Flow/Forge suite installed the Monitoring page renders the "No monitoring suite installed" card and does not error-toast elsewhere in the app.
  • Install the Flow suite (concave install flow && concave start flow), reload the web UI, and confirm the Prometheus/Grafana reachability badges flip to reachable and the PromQL cards populate.
  • Click "Open Prometheus" / "Open Grafana" and confirm they open correctly via the same-origin proxy.
  • Toggle the embedded Grafana iframe — verify it loads (if anonymous viewing is enabled) or the "Open in new tab" fallback works.
  • Confirm the JSON field casing change on /settings does not break any external automation that hits the settings endpoint.

Notes

  • No new dependencies in go.mod or package.json, per CONTRIBUTING.md.
  • The Grafana iframe uses sandbox="allow-same-origin allow-scripts allow-forms allow-popups". If your Grafana enforces strict CSP frame-ancestors, set allow_embedding = true in grafana.ini or just use the "Pop out" button.
  • The companion roadmap document (phases 0.2 → 0.6, industry-standard gap analysis) is attached to the originating chat session.

Link to Devin session: https://app.devin.ai/sessions/5d19efa113054ca4953d9ed9309ce705
Requested by: @ElFariss

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-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +209 to +212
onMounted(async () => {
await refresh()
refreshTimer = window.setInterval(refresh, 15_000)
})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant