All checks were successful
CI / changes (push) Successful in 18s
CI / test (push) Successful in 25s
CI / build-ai-gateway (push) Has been skipped
CI / build-ha-gateway (push) Has been skipped
CI / build-discord-bot (push) Has been skipped
CI / build-alexa-bridge (push) Has been skipped
CI / build-alert-bridge (push) Successful in 30s
CI / build-tts-gateway (push) Successful in 29s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
187 lines
6.6 KiB
Markdown
187 lines
6.6 KiB
Markdown
# alert-bridge
|
|
|
|
`alert-bridge` turns an authenticated HTTP POST from an external caller (e.g.
|
|
a cronjob running elsewhere on the home network) into a message posted to a
|
|
Discord channel. It is fire-and-forget: each request becomes one new Discord
|
|
message, there is no editing or reacting to prior messages.
|
|
|
|
Unlike `ha-gateway`/`ai-gateway`/`discord-bot`/`tts-gateway`, it has no
|
|
dependency on any other service in this repo — it posts directly to a Discord
|
|
[Incoming Webhook](https://discord.com/developers/docs/resources/webhook), not
|
|
through `discord-bot`'s bot session. That keeps the whole flow to one hop and
|
|
means `discord-bot` needed zero changes to support this feature.
|
|
|
|
## Runtime Flow
|
|
|
|
1. The process loads `.env`, configures logging/telemetry, and starts serving
|
|
HTTP on `HTTP_PORT`.
|
|
2. A caller `POST`s to `/alerts` with a bearer token and a JSON body.
|
|
3. The handler checks the bearer token (constant-time compare against
|
|
`API_KEY`), validates the body, and defaults `level` to `info` if omitted.
|
|
4. The alert is formatted as a Discord embed (color-coded by level) and POSTed
|
|
to `DISCORD_WEBHOOK_URL`. `level=error` alerts additionally prefix the
|
|
message with a mention of `MENTION_USER_ID`, since embeds alone never
|
|
trigger a Discord ping.
|
|
|
|
## HTTP API
|
|
|
|
- `POST /alerts` — requires `Authorization: Bearer <API_KEY>`.
|
|
|
|
```json
|
|
{"source": "ba-cronjob", "message": "Finished with 0 errors", "level": "info"}
|
|
```
|
|
|
|
- `source`, `message`: required.
|
|
- `level`: optional, one of `info` | `warn` | `error`, defaults to `info`.
|
|
- Responses: `202` delivered, `400` bad request, `401` bad/missing token,
|
|
`502` the Discord webhook call itself failed.
|
|
|
|
- `GET /healthz`: unauthenticated liveness/readiness check, always `200 OK`.
|
|
|
|
## Configuration
|
|
|
|
Environment variables:
|
|
|
|
| Variable | Default | Description |
|
|
| --- | --- | --- |
|
|
| `HTTP_PORT` | `8080` | HTTP listen port |
|
|
| `API_KEY` | required | Shared secret callers must present as `Authorization: Bearer <API_KEY>` |
|
|
| `DISCORD_WEBHOOK_URL` | required | Target channel's Discord Incoming Webhook URL |
|
|
| `MENTION_USER_ID` | empty | Discord user ID mentioned on `level=error` alerts; leave empty to never mention |
|
|
| `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry |
|
|
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` |
|
|
| `LOG_FORMAT` | `json` | `json` or `text` |
|
|
|
|
Example env file: [.env.example](.env.example)
|
|
|
|
## Local Run
|
|
|
|
```bash
|
|
cd alert-bridge
|
|
cp .env.example .env
|
|
# edit .env: set API_KEY and DISCORD_WEBHOOK_URL
|
|
go run ./cmd/bridge
|
|
```
|
|
|
|
```bash
|
|
curl -X POST http://localhost:8080/alerts \
|
|
-H "Authorization: Bearer $API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"source":"ba-cronjob","message":"Started","level":"info"}'
|
|
```
|
|
|
|
## Usage From Another LAN Host
|
|
|
|
Deployed at `http://alert-bridge.home.arpa` (plain HTTP, not HTTPS — see
|
|
"Deployment" below for why). Any host on the home network can reach it
|
|
directly, no VPN/tunnel/cluster access needed:
|
|
|
|
```bash
|
|
curl -X POST http://alert-bridge.home.arpa/alerts \
|
|
-H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"source":"ba-cronjob","message":"Started","level":"info"}'
|
|
|
|
# ... job runs ...
|
|
|
|
curl -X POST http://alert-bridge.home.arpa/alerts \
|
|
-H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"source":"ba-cronjob","message":"Finished with 0 errors","level":"info"}'
|
|
```
|
|
|
|
A `level=error` call additionally pings the configured `MENTION_USER_ID` in
|
|
Discord:
|
|
|
|
```bash
|
|
curl -X POST http://alert-bridge.home.arpa/alerts \
|
|
-H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"source":"ba-cronjob","message":"Finished with 3 errors","level":"error"}'
|
|
```
|
|
|
|
Reusable shape for a cronjob script — export `ALERT_BRIDGE_API_KEY` once at
|
|
the top (never hardcode the token in the script itself) and call `alert` at
|
|
each step:
|
|
|
|
```bash
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ALERT_URL="http://alert-bridge.home.arpa/alerts"
|
|
SOURCE="ba-cronjob"
|
|
|
|
alert() {
|
|
local level="$1" message="$2"
|
|
curl -sf -X POST "$ALERT_URL" \
|
|
-H "Authorization: Bearer $ALERT_BRIDGE_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"source\":\"$SOURCE\",\"message\":\"$message\",\"level\":\"$level\"}" \
|
|
> /dev/null
|
|
}
|
|
|
|
alert info "Started"
|
|
|
|
if ! run_the_actual_job; then
|
|
alert error "Finished with errors"
|
|
exit 1
|
|
fi
|
|
|
|
alert info "Finished with 0 errors"
|
|
```
|
|
|
|
Since this is plain HTTP, the bearer token travels in cleartext on the LAN —
|
|
acceptable for a home network, but don't reuse `ALERT_BRIDGE_API_KEY` for
|
|
anything more sensitive.
|
|
|
|
## Test And Build
|
|
|
|
```bash
|
|
go test ./...
|
|
go build ./...
|
|
```
|
|
|
|
Build the container image from the workspace root:
|
|
|
|
```bash
|
|
docker build -f alert-bridge/Dockerfile -t alert-bridge:dev .
|
|
```
|
|
|
|
## Package Map
|
|
|
|
```text
|
|
cmd/bridge/ # process entrypoint and wiring
|
|
internal/adapters/primary/http/ # POST /alerts handler: auth, decode, validate
|
|
internal/adapters/secondary/discordwebhook/ # builds the embed payload, posts to the Discord webhook
|
|
internal/app/ # thin orchestration between the HTTP handler and the Notifier
|
|
internal/core/domain/ # Alert{Source, Message, Level}
|
|
internal/core/ports/driven/ # Notifier interface
|
|
internal/config/ # environment loading
|
|
internal/logger/ # slog setup
|
|
internal/telemetry/ # OpenTelemetry setup
|
|
```
|
|
|
|
## Deployment
|
|
|
|
Kubernetes manifests (Deployment/Service, plus a plain-HTTP Traefik
|
|
`IngressRoute` at `alert-bridge.home.arpa` so LAN callers outside the cluster
|
|
can reach it) live in the separate `homelab` repo, not here. Deployed and
|
|
confirmed working as of 2026-08-01.
|
|
|
|
The ingress is plain HTTP rather than the internal-CA HTTPS every other
|
|
`*.home.arpa` service uses — LAN callers (e.g. `nik-gpu`) don't have this
|
|
cluster's internal CA trusted, and installing it on every caller was judged
|
|
not worth it for an endpoint that's already bearer-token-authenticated and
|
|
LAN-only. See `homelab/manifests/home-services/alert-bridge-ingress.yaml`'s
|
|
header comment for the full reasoning.
|
|
|
|
## Limitations
|
|
|
|
- Single channel/webhook only — no per-source routing to multiple channels.
|
|
- No rate limiting of its own; relies on Discord's own webhook rate limits
|
|
(5 requests/2s per webhook), which is more than enough for cronjob-scale
|
|
notification volume.
|
|
- Fire-and-forget only: no message editing, threading, or reaction handling.
|
|
That would require routing through `discord-bot`'s live bot session instead
|
|
of a plain webhook — not needed for the current use case.
|