Skip to content

Commit 65f5509

Browse files
committed
Add file-based dashboard provisioner
1 parent af86da6 commit 65f5509

9 files changed

Lines changed: 610 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/api": minor
3+
---
4+
5+
feat: add file-based dashboard provisioner that watches a directory for JSON files and upserts dashboards into MongoDB

docker/hyperdx/entry.prod.sh

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ echo ""
1212
echo "Visit the HyperDX UI at $FRONTEND_URL"
1313
echo ""
1414

15-
# Use concurrently to run both the API and App servers
15+
# Optionally include the dashboard provisioner task
16+
EXTRA_NAMES=""
17+
EXTRA_CMDS=""
18+
if [ -n "$DASHBOARD_PROVISIONER_DIR" ]; then
19+
EXTRA_NAMES=",DASH-PROVISION"
20+
EXTRA_CMDS="./packages/api/bin/hyperdx task provision-dashboards"
21+
fi
22+
23+
# Use concurrently to run all services
1624
./node_modules/.bin/concurrently \
1725
"--kill-others-on-fail" \
18-
"--names=API,APP,ALERT-TASK" \
26+
"--names=API,APP,ALERT-TASK${EXTRA_NAMES}" \
1927
"PORT=${HYPERDX_API_PORT:-8000} HYPERDX_APP_PORT=${HYPERDX_APP_PORT:-8080} ./packages/api/bin/hyperdx api" \
2028
"cd ./packages/app/packages/app && HOSTNAME='${HYPERDX_APP_LISTEN_HOSTNAME:-0.0.0.0}' HYPERDX_API_PORT=${HYPERDX_API_PORT:-8000} PORT=${HYPERDX_APP_PORT:-8080} node server.js" \
21-
"./packages/api/bin/hyperdx task check-alerts"
29+
"./packages/api/bin/hyperdx task check-alerts" \
30+
${EXTRA_CMDS:+"$EXTRA_CMDS"}

packages/api/docs/auto_provision/AUTO_PROVISION.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,64 @@ services:
163163
For more complex configurations, you can use environment files or Docker secrets
164164
to manage these values.
165165
166+
## Dashboard Provisioning
167+
168+
HyperDX supports file-based dashboard provisioning, similar to Grafana's
169+
provisioning system. A scheduled task reads `.json` files from a directory
170+
and upserts dashboards into MongoDB, matched by name for idempotency.
171+
The task runs on the same schedule as other HyperDX tasks (every minute
172+
when using the built-in scheduler, or on your own schedule when running
173+
tasks externally).
174+
175+
### Environment Variables
176+
177+
| Variable | Required | Default | Description |
178+
| ----------------------------------- | -------- | ------- | --------------------------------------------------------------- |
179+
| `DASHBOARD_PROVISIONER_DIR` | Yes | | Directory to watch for `.json` dashboard files |
180+
| `DASHBOARD_PROVISIONER_TEAM_ID` | No\* | | Scope provisioning to a specific team ID |
181+
| `DASHBOARD_PROVISIONER_ALL_TEAMS` | No\* | `false` | Set to `true` to provision dashboards to all teams |
182+
183+
\*One of `DASHBOARD_PROVISIONER_TEAM_ID` or `DASHBOARD_PROVISIONER_ALL_TEAMS=true`
184+
is required when `DASHBOARD_PROVISIONER_DIR` is set.
185+
186+
### Dashboard JSON Format
187+
188+
Each `.json` file in the provisioner directory should contain a dashboard object
189+
with at minimum a `name` and `tiles` array:
190+
191+
```json
192+
{
193+
"name": "My Dashboard",
194+
"tiles": [
195+
{
196+
"id": "tile-1",
197+
"x": 0,
198+
"y": 0,
199+
"w": 6,
200+
"h": 4,
201+
"config": {
202+
"name": "Request Count",
203+
"source": "Metrics",
204+
"displayType": "line",
205+
"select": [{ "aggFn": "count" }]
206+
}
207+
}
208+
],
209+
"tags": ["provisioned"]
210+
}
211+
```
212+
213+
### Behavior
214+
215+
- Dashboards are matched by name and team for idempotency
216+
- Provisioned dashboards are flagged with `provisioned: true` so they never
217+
overwrite user-created dashboards with the same name
218+
- Removing a file from the directory does **not** delete the dashboard from
219+
MongoDB (safe by default)
220+
- Files are validated against the `DashboardWithoutIdSchema` Zod schema; invalid
221+
files are skipped with a warning
222+
223+
166224
## Note on Security
167225

168226
While this feature is convenient for development and initial setup, be careful

packages/api/src/models/dashboard.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ObjectId } from '.';
77
export interface IDashboard extends z.infer<typeof DashboardSchema> {
88
_id: ObjectId;
99
team: ObjectId;
10+
provisioned?: boolean;
1011
createdAt: Date;
1112
updatedAt: Date;
1213
}
@@ -32,10 +33,14 @@ export default mongoose.model<IDashboard>(
3233
savedQueryLanguage: { type: String, required: false },
3334
savedFilterValues: { type: mongoose.Schema.Types.Array, required: false },
3435
containers: { type: mongoose.Schema.Types.Array, required: false },
36+
provisioned: { type: Boolean, default: false },
3537
},
3638
{
3739
timestamps: true,
3840
toJSON: { getters: true },
3941
},
42+
).index(
43+
{ name: 1, team: 1 },
44+
{ unique: true, partialFilterExpression: { provisioned: true } },
4045
),
4146
);

packages/api/src/tasks/__tests__/types.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,21 @@ describe('asTaskArgs', () => {
275275
});
276276
});
277277

278+
describe('provision-dashboards task', () => {
279+
it('should accept provision-dashboards task', () => {
280+
const validArgs = {
281+
_: ['provision-dashboards'],
282+
};
283+
284+
const result = asTaskArgs(validArgs);
285+
286+
expect(result).toEqual({
287+
taskName: 'provision-dashboards',
288+
});
289+
expect(result.taskName).toBe('provision-dashboards');
290+
});
291+
});
292+
278293
describe('ping-pong task', () => {
279294
it('should accept ping-pong task without provider', () => {
280295
const validArgs = {

packages/api/src/tasks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
timeExec,
1212
} from '@/tasks/metrics';
1313
import PingPongTask from '@/tasks/pingPongTask';
14+
import ProvisionDashboardsTask from '@/tasks/provisionDashboards';
1415
import { asTaskArgs, HdxTask, TaskArgs, TaskName } from '@/tasks/types';
1516
import logger from '@/utils/logger';
1617

@@ -23,6 +24,8 @@ function createTask(argv: TaskArgs): HdxTask<TaskArgs> {
2324
return new CheckAlertTask(argv);
2425
case TaskName.PING_PONG:
2526
return new PingPongTask(argv);
27+
case TaskName.PROVISION_DASHBOARDS:
28+
return new ProvisionDashboardsTask(argv);
2629
default:
2730
throw new Error(`Unknown task name ${taskName}`);
2831
}

0 commit comments

Comments
 (0)