Files
terdut-server/README.md
T
Niklas Ye 17f09558cb Stage 7: Dockerfile, integration tests, updated README
Dockerfile:
- Multi-stage build (golang:1.25-alpine → scratch)
- CGO_ENABLED=0, static binary, stripped with -ldflags="-w -s" (~11 MB)

Tests (13 cases, internal/api/api_test.go):
- Auth middleware: missing token, invalid token, valid token
- Bootstrap idempotency (second call → 403)
- Alert upsert: same fingerprint updates row; different fingerprints add rows
- Acknowledge: set and clear, verified via GET
- Comment ownership: only author can delete own comment (404 for others)
- Schedule conflict: duplicate date → 409; multi-date rollback on partial conflict
- Stats: totals, by-hour returns 24 slots, by-day returns 7 slots

README: quick start, Docker, env vars, Alertmanager config, full API reference
2026-05-20 22:35:26 +02:00

150 lines
4.2 KiB
Markdown

# Terminal Duty (terdut-server)
On-call alert management server for teams using Prometheus Alertmanager.
- Receives Alertmanager webhooks directly — no adapter needed
- Stores and queries alerts (acknowledge, comment)
- On-call schedule management (user-to-day assignments)
- Alert statistics (by status, by hour, by day)
- REST API with per-user API key authentication
- Single binary, SQLite storage — trivial to self-host
---
## Quick start
**Prerequisites:** Go 1.21+
```bash
git clone https://github.com/yeniklas/terdut-server
cd terdut-server
go run ./cmd/terdut
```
The server starts on `:8080` with a `terdut.db` file in the working directory.
### Create the first user
```bash
curl -X POST http://localhost:8080/api/bootstrap \
-H "Content-Type: application/json" \
-d '{"username": "admin", "email": "admin@example.com"}'
```
Save the `api_key.key` value from the response — it is shown **once only**.
Use it as a bearer token for all subsequent requests:
```bash
export KEY=<your-key>
curl -H "Authorization: Bearer $KEY" http://localhost:8080/api/users
```
### Docker
```bash
docker build -t terdut-server .
docker run -p 8080:8080 -v $(pwd)/data:/data \
-e TERDUT_DB_PATH=/data/terdut.db \
terdut-server
```
---
## Configuration
| Variable | Default | Description |
|---|---|---|
| `TERDUT_ADDR` | `:8080` | TCP address to listen on |
| `TERDUT_DB_PATH` | `terdut.db` | Path to the SQLite database file |
---
## Alertmanager configuration
Add terdut-server as a webhook receiver in your `alertmanager.yml`:
```yaml
receivers:
- name: terdut
webhook_configs:
- url: http://terdut-server:8080/api/alertmanager/webhook
send_resolved: true
route:
receiver: terdut
```
The webhook endpoint requires no authentication.
---
## API reference
### Authentication
All endpoints except `/api/bootstrap` and `/api/alertmanager/webhook` require:
```
Authorization: Bearer <api-key>
```
### Users
| Method | Path | Description |
|---|---|---|
| `POST` | `/api/bootstrap` | Create first user + API key (only works on empty DB) |
| `GET` | `/api/users` | List users |
| `POST` | `/api/users` | Create user `{"username","email"}` |
| `DELETE` | `/api/users/{id}` | Delete user (cascades to keys) |
| `POST` | `/api/users/{id}/api-keys` | Issue API key `{"name"}` — key shown once |
| `DELETE` | `/api/users/{id}/api-keys/{keyID}` | Revoke API key |
### Alert ingestion
| Method | Path | Description |
|---|---|---|
| `POST` | `/api/alertmanager/webhook` | Alertmanager v4 webhook receiver (no auth) |
### Alerts
| Method | Path | Description |
|---|---|---|
| `GET` | `/api/alerts` | List alerts. Filters: `?status=firing\|resolved`, `?name=`, `?from=YYYY-MM-DD`, `?to=YYYY-MM-DD`, `?limit=` (default 50, max 500) |
| `GET` | `/api/alerts/{id}` | Get single alert |
| `POST` | `/api/alerts/{id}/acknowledge` | Acknowledge alert (stamps authed user + time) |
| `DELETE` | `/api/alerts/{id}/acknowledge` | Clear acknowledgement |
| `GET` | `/api/alerts/{id}/comments` | List comments (chronological) |
| `POST` | `/api/alerts/{id}/comments` | Add comment `{"content"}` |
| `DELETE` | `/api/alerts/{id}/comments/{commentID}` | Delete own comment |
### On-call schedule
| Method | Path | Description |
|---|---|---|
| `POST` | `/api/schedule` | Assign user to dates `{"user_id", "dates":["YYYY-MM-DD",...]}` — all-or-nothing |
| `GET` | `/api/schedule` | List entries. Filters: `?from=YYYY-MM-DD`, `?to=YYYY-MM-DD` |
| `GET` | `/api/schedule/current` | Today's on-call user (UTC), 404 if none |
| `DELETE` | `/api/schedule/{id}` | Remove schedule entry |
### Statistics
All stat endpoints accept optional `?from=YYYY-MM-DD` and `?to=YYYY-MM-DD` to filter by `received_at`.
| Method | Path | Description |
|---|---|---|
| `GET` | `/api/stats/alerts` | `{total, firing, resolved}` counts |
| `GET` | `/api/stats/alerts/top` | Most frequent alert names. `?limit=` (default 10, max 100) |
| `GET` | `/api/stats/alerts/by-hour` | Count per hour-of-day (UTC), all 24 slots returned |
| `GET` | `/api/stats/alerts/by-day` | Count per day-of-week, all 7 slots with names returned |
---
## Development
```bash
go test ./... # run all tests
go build ./... # compile all packages
go run ./cmd/terdut # run locally
```