mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
feat(api): add DELETE /api/v4/monitors/{monitor_tag} and fix alert-config orphans on delete, fixes #716
Monitor deletion is now available via the v4 API, reusing the same DeleteMonitorCompletelyUsingTag path as the manage UI. While wiring it in, monitor deletion was found to orphan alert configs on SQLite: the code relied on FK cascades that SQLite never enforces (the foreign_keys pragma is off). Delete paths now remove child rows explicitly — v2 alerts, trigger junctions, monitor junctions — in both the by-id and by-tag config deletes; see ADR 0008 for why explicit deletes were chosen over enabling the pragma. Also corrects the CONTEXT.md Stale Member entry (deletion strips group membership and rebalances weights; only pausing produces a stale member), documents the DELETE endpoint in the OpenAPI spec, points the pages doc at the ~home token, and removes an orphaned fictional api-reference markdown page superseded by the spec tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -46,7 +46,7 @@ A Monitoring Sample produced by a check that actually ran against the target, wh
|
||||
A Monitoring Sample written by the system or an admin rather than by a check: a raw heartbeat receipt (`SIGNAL`), a status pushed through the data API (`MANUAL`), a default-status fill (`DEFAULT_STATUS`), or an incident/maintenance overlay (`INCIDENT`, `MAINTENANCE`).
|
||||
|
||||
**Stale Member**:
|
||||
A Member whose monitor is no longer an Eligible Monitor (paused or deleted after being added). It remains a Member until explicitly removed, but is excluded from the group score.
|
||||
A Member whose monitor was paused after being added, making it no longer an Eligible Monitor. It remains a Member until explicitly removed, but is excluded from the group score. Deletion never produces a Stale Member: deleting a monitor strips it from every group and rebalances the remaining members' weights equally.
|
||||
|
||||
### Alerting
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Delete paths remove child rows explicitly and never rely on FK cascades
|
||||
|
||||
The schema declares `ON DELETE CASCADE` foreign keys (alert configs → monitors, trigger/monitor junctions → configs, v2 alerts → configs), but SQLite — the default deployment — never enforces them: knex does not enable the `foreign_keys` pragma, and `knexfile.ts` does not either. Code that trusted the declared cascades (`deleteMonitorAlertConfig`, and `DeleteMonitorCompletelyUsingTag`, which skipped alert configs entirely) silently orphaned junction rows, configs, and `monitor_alerts_v2` state on SQLite, while behaving correctly on Postgres and MySQL (#716).
|
||||
|
||||
We considered fixing the root cause instead: enabling `PRAGMA foreign_keys = ON` per connection via `pool.afterCreate`. That would make every declared cascade real and align the three databases. We rejected it because knex implements SQLite `ALTER TABLE` as a table rebuild, which violates FK constraints transiently — the multi-monitor-alerts migration already has to toggle the pragma off around its own rebuild — so enforcement would put every past and future migration on a tightrope, and it changes write-failure behavior for all existing SQLite installs at once (inserts that used to succeed against missing parents would start throwing).
|
||||
|
||||
The rule is the opposite and uniform: a delete path owns its children and removes them explicitly, in child-before-parent order. `deleteMonitorAlertConfig` deletes v2 alerts, trigger junctions, and monitor junctions before the config row; `deleteMonitorAlertConfigsByMonitorTag` detaches the monitor and routes zero-monitor configs through that same method; `DeleteMonitorCompletelyUsingTag` calls it alongside its other per-tag cleanups. The declared cascades stay in the schema — on Postgres/MySQL they make the explicit deletes idempotent no-ops, and they document intent — but no code may depend on them.
|
||||
|
||||
A consequence: shared alert configs (one config spanning several monitors) survive the deletion of one member; a config dies only when its last monitor is detached. Monitor deletion also strips the tag from group monitors and rebalances remaining member weights equally — deletion never produces a stale member (see CONTEXT.md, "Stale Member").
|
||||
@@ -328,7 +328,8 @@ export async function DeleteMonitorAlertConfig(id: number): Promise<boolean> {
|
||||
throw new Error(`Monitor alert config with id '${id}' not found`);
|
||||
}
|
||||
|
||||
// Triggers will be deleted automatically due to CASCADE
|
||||
// The repository deletes trigger/monitor junctions and v2 alerts explicitly;
|
||||
// FK cascades are not enforced on SQLite
|
||||
const deleted = await db.deleteMonitorAlertConfig(id);
|
||||
return deleted > 0;
|
||||
}
|
||||
|
||||
@@ -437,6 +437,7 @@ export const DeleteMonitorCompletelyUsingTag = async (tag: string): Promise<numb
|
||||
await db.deleteMonitorDataByTag(tag);
|
||||
await db.deleteIncidentMonitorsByTag(tag);
|
||||
await db.deleteMonitorAlertsByTag(tag);
|
||||
await db.deleteMonitorAlertConfigsByMonitorTag(tag);
|
||||
await db.deletePageMonitorsByTag(tag);
|
||||
await db.deleteMaintenanceMonitorsByTag(tag);
|
||||
await removeTagFromGroupMonitors(tag);
|
||||
|
||||
@@ -141,9 +141,17 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a monitor alert config by ID
|
||||
* Delete a monitor alert config by ID, including all child rows.
|
||||
*
|
||||
* Child rows are removed explicitly even though FK cascades are declared:
|
||||
* SQLite never enforces them (foreign_keys pragma is off), so relying on
|
||||
* CASCADE orphans children on the default deployment. See
|
||||
* docs/adr/0008-explicit-deletes-over-fk-cascades.md.
|
||||
*/
|
||||
async deleteMonitorAlertConfig(id: number): Promise<number> {
|
||||
await this.knex("monitor_alerts_v2").where({ config_id: id }).del();
|
||||
await this.knex("monitor_alerts_config_triggers").where({ monitor_alerts_id: id }).del();
|
||||
await this.knex("monitor_alerts_config_monitors").where({ monitor_alerts_id: id }).del();
|
||||
return await this.knex("monitor_alerts_config").where({ id }).del();
|
||||
}
|
||||
|
||||
@@ -170,7 +178,7 @@ export class MonitorAlertConfigRepository extends BaseRepository {
|
||||
.where({ monitor_alerts_id: id })
|
||||
.first<CountResult>();
|
||||
if (Number(remainingMonitors?.count) === 0) {
|
||||
await this.knex("monitor_alerts_config").where({ id }).del();
|
||||
await this.deleteMonitorAlertConfig(id);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,10 @@ export interface UpdateMonitorResponse {
|
||||
monitor: MonitorRecordTyped;
|
||||
}
|
||||
|
||||
export interface DeleteMonitorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Monitoring Data API types
|
||||
export interface MonitoringDataPoint {
|
||||
monitor_tag: string;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { json, type RequestHandler } from "@sveltejs/kit";
|
||||
import db from "$lib/server/db/db";
|
||||
import { GetMonitorsParsed } from "$lib/server/controllers/monitorsController";
|
||||
import { GetMonitorsParsed, DeleteMonitorCompletelyUsingTag } from "$lib/server/controllers/monitorsController";
|
||||
import type {
|
||||
GetMonitorResponse,
|
||||
MonitorResponse,
|
||||
UpdateMonitorRequest,
|
||||
UpdateMonitorResponse,
|
||||
DeleteMonitorResponse,
|
||||
BadRequestResponse,
|
||||
} from "$lib/types/api";
|
||||
|
||||
@@ -143,3 +144,20 @@ export const PATCH: RequestHandler = async ({ locals, request }) => {
|
||||
|
||||
return json(response);
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ locals }) => {
|
||||
// Monitor is validated by middleware and available in locals
|
||||
const monitor = locals.monitor!;
|
||||
|
||||
// Removes the monitor and everything keyed to its tag: monitoring data,
|
||||
// incident/maintenance/page links, alerts, alert configs, group
|
||||
// memberships (with weight rebalancing), and caches. The scheduler drops
|
||||
// the orphaned BullMQ job on its next reconcile.
|
||||
await DeleteMonitorCompletelyUsingTag(monitor.tag);
|
||||
|
||||
const response: DeleteMonitorResponse = {
|
||||
message: `Monitor '${monitor.tag}' deleted successfully`,
|
||||
};
|
||||
|
||||
return json(response);
|
||||
};
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
---
|
||||
title: Monitors API
|
||||
description: Create, read, update, and delete monitors via the REST API
|
||||
---
|
||||
|
||||
Create, read, update, and delete monitors via the API.
|
||||
|
||||
## List Monitors
|
||||
|
||||
Get all monitors with their current status.
|
||||
|
||||
```http
|
||||
GET /api/monitors
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------- | ------------------------------------ |
|
||||
| `page` | integer | Page number (default: 1) |
|
||||
| `limit` | integer | Items per page (default: 20) |
|
||||
| `status` | string | Filter by status: up, down, degraded |
|
||||
| `type` | string | Filter by type: API, PING, TCP, etc. |
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X GET "https://your-kener.com/api/monitors?status=up&limit=10" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"monitors": [
|
||||
{
|
||||
"id": "mon_abc123",
|
||||
"name": "API Server",
|
||||
"type": "API",
|
||||
"url": "https://api.example.com/health",
|
||||
"status": "up",
|
||||
"uptime": 99.95,
|
||||
"lastChecked": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"total": 25
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Get Monitor
|
||||
|
||||
Get details for a specific monitor.
|
||||
|
||||
```http
|
||||
GET /api/monitors/:id
|
||||
```
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X GET https://your-kener.com/api/monitors/mon_abc123 \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "mon_abc123",
|
||||
"name": "API Server",
|
||||
"type": "API",
|
||||
"url": "https://api.example.com/health",
|
||||
"method": "GET",
|
||||
"expectedStatusCode": 200,
|
||||
"interval": 60,
|
||||
"timeout": 10000,
|
||||
"status": "up",
|
||||
"uptime": {
|
||||
"day": 100,
|
||||
"week": 99.95,
|
||||
"month": 99.87
|
||||
},
|
||||
"responseTime": {
|
||||
"current": 145,
|
||||
"average": 152
|
||||
},
|
||||
"lastChecked": "2024-01-15T10:30:00Z",
|
||||
"createdAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Create Monitor
|
||||
|
||||
Create a new monitor.
|
||||
|
||||
```http
|
||||
POST /api/monitors
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Production API",
|
||||
"type": "API",
|
||||
"url": "https://api.example.com/health",
|
||||
"method": "GET",
|
||||
"expectedStatusCode": 200,
|
||||
"interval": 60,
|
||||
"timeout": 10000,
|
||||
"headers": {
|
||||
"Authorization": "Bearer token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Monitor Types
|
||||
|
||||
#### API Monitor
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "API Health",
|
||||
"type": "API",
|
||||
"url": "https://api.example.com",
|
||||
"method": "GET",
|
||||
"expectedStatusCode": 200,
|
||||
"headers": {},
|
||||
"body": null
|
||||
}
|
||||
```
|
||||
|
||||
#### Ping Monitor
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Server Ping",
|
||||
"type": "PING",
|
||||
"host": "server.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
#### TCP Monitor
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Database Port",
|
||||
"type": "TCP",
|
||||
"host": "db.example.com",
|
||||
"port": 5432
|
||||
}
|
||||
```
|
||||
|
||||
#### DNS Monitor
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "DNS Check",
|
||||
"type": "DNS",
|
||||
"host": "example.com",
|
||||
"recordType": "A"
|
||||
}
|
||||
```
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-kener.com/api/monitors \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Production API",
|
||||
"type": "API",
|
||||
"url": "https://api.example.com/health",
|
||||
"interval": 60
|
||||
}'
|
||||
```
|
||||
|
||||
### Example Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "mon_xyz789",
|
||||
"name": "Production API",
|
||||
"type": "API",
|
||||
"status": "pending",
|
||||
"createdAt": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
"message": "Monitor created successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## Update Monitor
|
||||
|
||||
Update an existing monitor.
|
||||
|
||||
```http
|
||||
PUT /api/monitors/:id
|
||||
```
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X PUT https://your-kener.com/api/monitors/mon_abc123 \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Updated API Name",
|
||||
"interval": 120
|
||||
}'
|
||||
```
|
||||
|
||||
## Delete Monitor
|
||||
|
||||
Delete a monitor.
|
||||
|
||||
```http
|
||||
DELETE /api/monitors/:id
|
||||
```
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X DELETE https://your-kener.com/api/monitors/mon_abc123 \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
|
||||
## Monitor History
|
||||
|
||||
Get historical data for a monitor.
|
||||
|
||||
```http
|
||||
GET /api/monitors/:id/history
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------ | ------ | ---------------------------------- |
|
||||
| `start` | string | Start date (ISO 8601) |
|
||||
| `end` | string | End date (ISO 8601) |
|
||||
| `resolution` | string | Data resolution: minute, hour, day |
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X GET "https://your-kener.com/api/monitors/mon_abc123/history?resolution=hour" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
|
||||
## Pause/Resume Monitor
|
||||
|
||||
```http
|
||||
POST /api/monitors/:id/pause
|
||||
POST /api/monitors/:id/resume
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
# Pause
|
||||
curl -X POST https://your-kener.com/api/monitors/mon_abc123/pause \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
|
||||
# Resume
|
||||
curl -X POST https://your-kener.com/api/monitors/mon_abc123/resume \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
@@ -196,7 +196,7 @@ status.yourdomain.com {
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Set up [Monitors](/docs/monitors)
|
||||
- Configure [Incidents](/docs/incidents)
|
||||
- Set up [Monitors](/docs/v4/monitors)
|
||||
- Configure [Incidents](/docs/v4/incidents)
|
||||
- Configure [Sharing Monitors](/docs/v4/sharing)
|
||||
- Explore the [API Reference](/docs/api-reference)
|
||||
- Explore the [API Reference](/docs/spec/v4/)
|
||||
|
||||
@@ -97,6 +97,10 @@ Example:
|
||||
delete services
|
||||
```
|
||||
|
||||
## Manage pages via API {#manage-pages-via-api}
|
||||
|
||||
Pages support full CRUD through the v4 REST API (`/api/v4/pages`), including assigning monitors. The home page has an empty stored path, so the API addresses it with the special segment `~home` (for example `PATCH /api/v4/pages/~home`); its path cannot be changed and it cannot be deleted. See the [API Reference](/docs/spec/v4/) for endpoints and schemas.
|
||||
|
||||
## Tips {#tips}
|
||||
|
||||
- Keep page paths short and stable (changing links later is disruptive).
|
||||
|
||||
@@ -230,6 +230,30 @@
|
||||
"$ref": "#/components/responses/InternalServerError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Monitors"],
|
||||
"operationId": "deleteMonitor",
|
||||
"summary": "Delete monitor",
|
||||
"description": "Deletes the monitor and everything keyed to its tag: monitoring data, incident/maintenance/page links, alerts, alert configurations, and group memberships (remaining member weights are rebalanced equally). This is irreversible. Scheduled checks stop within seconds.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Monitor deleted",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/MessageResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/Unauthorized"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/components/responses/NotFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v4/monitors/{monitor_tag}/data": {
|
||||
|
||||
Reference in New Issue
Block a user