Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 67 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
# relaybox

Generic relay hub: receives any inbound protocol/format and delivers to outbound channels (CEL/Expr expression filter/transform/route rules planned).
Generic relay hub: receives any inbound protocol/format and delivers to outbound channels via CEL/Expr expression filter, transform, and route rules.

```
any inbound (HTTP REST / WebSocket / TCP / ...)
↓
parser pipeline (JSON / Form / XML / Logfmt / Regex)
↓
CEL / Expr expression filter + transform + route
↓
any outbound (Webhook / Slack / Discord / ...)
```

## Features

- **Multi-protocol inbound** — HTTP REST + WebSocket (TCP planned)
- **Expression-based routing** — CEL/Expr filter and transform rules per route (planned)
- **Template transformation** — Go `text/template` payload rendering
- **Multi-protocol inbound** — HTTP REST + WebSocket + TCP
- **Parser pipeline** — JSON, Form, XML, Logfmt, Regex per input
- **Expression-based routing** — CEL/Expr filter, mapping, and routing conditions per rule
- **at-least-once delivery** — file-queue backed, survives restarts
- **Exponential backoff retry** — per-channel `retryCount` / `retryDelayMs`
- **Hot config reload** — change outputs / rules without restart
Expand Down Expand Up @@ -46,27 +48,48 @@ cp internal/config/config.example.yaml config.yaml
```yaml
server:
port: 8080
readTimeout: 30s
writeTimeout: 30s

log:
level: info # debug | info | warn | error
format: json # json | text

expression:
defaultEngine: cel # cel | expr

inputs:
- id: beszel
type: BESZEL
parser: json # json | form | xml | logfmt | regex
secret: "your-secret"
- id: tcp-input
type: GENERIC
address: ":9001"
delimiter: "\n"
parser: json
secret: "" # secret unused for TCP inputs

outputs:
- id: ops-webhook
type: WEBHOOK
url: "https://hooks.example.com/xyz"
template: '{"text": "{{ .Source }}: {{ .Payload }}"}'
template:
text: 'data.input + ": " + data.payload'
retryCount: 3
retryDelayMs: 1000

rules:
- inputId: beszel
outputIds: [ops-webhook]
engine: cel # override default engine per rule
filter: 'data.input == "BESZEL"'
mapping:
severity: '"HIGH"'
routing:
- condition: 'data.severity == "HIGH"'
outputIds: [ops-webhook]
- inputId: tcp-input
outputIds: [ops-webhook] # simple: no filter/routing, send to all

storage:
type: SQLITE
Expand All @@ -78,14 +101,42 @@ queue:
workerCount: 2
```

### Template Variables
### Expression Variables

All expressions (filter, mapping, routing, template) share the same `data` context:

| Variable | Description |
|----------|-------------|
| `{{ .ID }}` | Alert ULID |
| `{{ .Source }}` | Source type (`BESZEL`, `DOZZLE`, etc.) |
| `{{ .Payload }}` | Raw JSON payload (string) |
| `{{ .CreatedAt }}` | Receive time (`time.Time`) |
| `data.id` | Message ULID |
| `data.input` | Input type (`BESZEL`, `DOZZLE`, `GENERIC`, etc.) |
| `data.payload` | Raw payload string |
| `data.createdAt` | Receive timestamp (RFC3339) |
| `data.<field>` | Any field added via `mapping` expressions |

**Filter** — boolean expression; message is dropped if `false`:
```yaml
filter: 'data.input == "BESZEL"'
```

**Mapping** — enrich `data` with computed fields:
```yaml
mapping:
severity: '"HIGH"'
label: 'data.input + "-alert"'
```

**Routing** — conditional output selection (evaluated after mapping):
```yaml
routing:
- condition: 'data.severity == "HIGH"'
outputIds: [ops-webhook]
```

**Template** — map of output fields rendered as expressions:
```yaml
template:
text: 'data.input + ": " + data.payload'
```

## API

Expand Down Expand Up @@ -113,14 +164,18 @@ Authorization: Bearer <secret>

JSON messages sent after connect are processed identically to HTTP POST.

### TCP Inbound

Connect to the configured `address` and send newline-delimited (or custom `delimiter`) messages. No token auth — secure via network policy.

### Health Check

```
GET /healthz
→ 200 OK
```

All responses include an `X-API-Version` header.
All HTTP responses include an `X-API-Version` header.

## Architecture

Expand Down