Skip to content

Add file-based dashboard provisioner#1962

Open
ZeynelKoca wants to merge 1 commit intohyperdxio:mainfrom
ZeynelKoca:feature/dashboard-provisioner
Open

Add file-based dashboard provisioner#1962
ZeynelKoca wants to merge 1 commit intohyperdxio:mainfrom
ZeynelKoca:feature/dashboard-provisioner

Conversation

@ZeynelKoca
Copy link

@ZeynelKoca ZeynelKoca commented Mar 22, 2026

Summary

Add a file-based dashboard provisioner that watches a directory for .json files and upserts dashboards into MongoDB (matched by name for idempotency).

Enabled by setting DASHBOARD_PROVISIONER_DIR to a directory path. Syncs every 30s by default (configurable via DASHBOARD_PROVISIONER_INTERVAL in ms). When the env var is not set, the provisioner is a no-op. This enables GitOps workflows where dashboards are managed as code, similar to Grafana's file-based provisioning (whose implementation heavily inspired this PR).

Provisioned dashboards are flagged with provisioned: true so they never overwrite user-created dashboards with the same name. Removing a file from the directory does not delete the dashboard (safe by default, same as Grafana).

Variable Required Default Description
DASHBOARD_PROVISIONER_DIR Yes Directory to watch for .json files
DASHBOARD_PROVISIONER_INTERVAL No 30000 Sync interval in ms
DASHBOARD_PROVISIONER_TEAM_ID No all teams Scope to a specific team ID. When unset, provisions to all teams.
DASHBOARD_PROVISIONER_ALL_TEAMS No* false Set to true to provision to all teams

*One of DASHBOARD_PROVISIONER_TEAM_ID or DASHBOARD_PROVISIONER_ALL_TEAMS=true is required.

How to test locally or on Vercel

  1. Create a directory with a dashboard JSON file:
    mkdir /tmp/dashboards
    echo '{"name":"Test Dashboard","tiles":[]}' > /tmp/dashboards/test.json
  2. Start the API with DASHBOARD_PROVISIONER_DIR=/tmp/dashboards
  3. Optionally set DASHBOARD_PROVISIONER_TEAM_ID= to scope to one team
  4. Log in to create a team, wait ~30s
  5. Verify the dashboard appears in the UI
  6. Modify the JSON file, wait ~30s, verify the dashboard updates

References

@changeset-bot
Copy link

changeset-bot bot commented Mar 22, 2026

🦋 Changeset detected

Latest commit: abdafb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Minor
@hyperdx/app Minor
@hyperdx/otel-collector Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Mar 22, 2026

@ZeynelKoca is attempting to deploy a commit to the HyperDX Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 22, 2026

PR Review

  • ⚠️ Provisioned dashboards unprotected at API leveldeleteDashboard and updateDashboard controllers don't check provisioned: true, so users can delete or overwrite provisioned dashboards via API/UI. Add a guard (if (dashboard.provisioned) throw ...) in both controller functions, or document this as intentional (next sync restores them).

  • ⚠️ provisioned field missing from DashboardSchema Zod typeServerDashboard (z.infer<typeof DashboardSchema>) used by the frontend doesn't include provisioned, so the UI can never show a "managed by provisioner" indicator. Add provisioned: z.boolean().optional() to DashboardSchema in packages/common-utils/src/types.ts.

  • ⚠️ isSyncing race in stopDashboardProvisioner → The flag is reset to false unconditionally, so if a sync is in-flight when stop is called (e.g., during server restart calling startstop), the new cycle's guard check passes immediately and two syncs run concurrently. Low practical impact since the MongoDB upserts are atomic, but the guard logic is broken. Fix: don't reset isSyncing in stop.

@ZeynelKoca ZeynelKoca force-pushed the feature/dashboard-provisioner branch 16 times, most recently from c7f5fd7 to 4c24c45 Compare March 22, 2026 23:34
@ZeynelKoca ZeynelKoca force-pushed the feature/dashboard-provisioner branch from 4c24c45 to abdafb3 Compare March 22, 2026 23:40
ZeynelKoca added a commit to ZeynelKoca/ClickStack-helm-charts that referenced this pull request Mar 22, 2026
k8s-sidecar watches ConfigMaps labeled "hyperdx.io/dashboard: true"
across all namespaces and writes dashboard JSON to a shared volume.
HyperDX reads and upserts them natively via file-based provisioner.

Requires hyperdxio/hyperdx#1962
@ZeynelKoca
Copy link
Author

PR Review

* ⚠️ **Provisioned dashboards unprotected at API level** → `deleteDashboard` and `updateDashboard` controllers don't check `provisioned: true`, so users can delete or overwrite provisioned dashboards via API/UI. Add a guard (`if (dashboard.provisioned) throw ...`) in both controller functions, or document this as intentional (next sync restores them).

* ⚠️ **`provisioned` field missing from `DashboardSchema` Zod type** → `ServerDashboard` (`z.infer<typeof DashboardSchema>`) used by the frontend doesn't include `provisioned`, so the UI can never show a "managed by provisioner" indicator. Add `provisioned: z.boolean().optional()` to `DashboardSchema` in `packages/common-utils/src/types.ts`.

* ⚠️ **`isSyncing` race in `stopDashboardProvisioner`** → The flag is reset to `false` unconditionally, so if a sync is in-flight when `stop` is called (e.g., during server restart calling `start` → `stop`), the new cycle's guard check passes immediately and two syncs run concurrently. Low practical impact since the MongoDB upserts are atomic, but the guard logic is broken. Fix: don't reset `isSyncing` in `stop`.
  1. API protection for provisioned dashboards: Intentional for now. If a user deletes a provisioned dashboard, the next sync cycle recreates it. Adding guards to the existing controllers is a separate change that touches the API contract.
  2. provisioned in DashboardSchema/UI: Agreed this would be useful for UI indication, but it's a frontend concern that doesn't affect the provisioner's correctness. Suggest a follow-up issue/PR.
  3. isSyncing in stop: The upserts are atomic (partial unique index ensures no duplicates), so even if two syncs overlap the result is correct. Removing the reset makes isSyncing stick after restart, which is worse. Keeping as-is.

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