A target's settings move from the per-type JSON string in targets.config to columns: url, headers, timeout_seconds, forward_query, archive_expiry and archive_rotation. A column a type does not use stays empty. headers holds the header map as JSON text through GORM's JSON serializer, and archive_expiry holds a time.Duration, 0 meaning never, so nothing that reads a target parses it. The forms check each setting once, with the same refusals and messages, and the edit form is filled from the columns, so the per-type config structs, their parsers and the "could not be read" branches are gone. A delivery task carries the six settings in place of the JSON string. Model: opus-5-5
This commit is contained in:
@@ -1579,15 +1579,20 @@ did not arrive on the URL and counts in neither.
|
||||
A delivery destination for events. Each target defines where and how events
|
||||
should be forwarded.
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | UUID | Primary key |
|
||||
| `webhook_id` | UUID | Foreign key → Webhook |
|
||||
| `name` | string | Human-readable name |
|
||||
| `type` | TargetType | One of: `http`, `slack`, `database`, `log` |
|
||||
| `active` | boolean | Whether deliveries are enabled (default: true) |
|
||||
| `config` | JSON text | Type-specific configuration |
|
||||
| `max_attempts` | integer | How many times an `http` or `slack` target attempts each delivery in all: at least 1, and 1 by default. Ignored by `database` and `log` targets. The web UI labels it Delivery attempts |
|
||||
| Field | Type | Description |
|
||||
| ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | UUID | Primary key |
|
||||
| `webhook_id` | UUID | Foreign key → Webhook |
|
||||
| `name` | string | Human-readable name |
|
||||
| `type` | TargetType | One of: `http`, `slack`, `database`, `log` |
|
||||
| `active` | boolean | Whether deliveries are enabled (default: true) |
|
||||
| `url` | string | Where an `http` or `slack` target sends each event. It is a credential: the target list shows only its scheme and host, and only the target edit form shows it whole |
|
||||
| `headers` | JSON text | An `http` target's extra request headers, a JSON object of name to value |
|
||||
| `timeout_seconds` | integer | An `http` target's per-request timeout; 0 uses the shared 30-second client timeout |
|
||||
| `forward_query` | boolean | Whether an `http` target is sent the query string each event arrived with |
|
||||
| `archive_expiry` | integer | How long a `database` target keeps each archived event, as a Go `time.Duration` (nanoseconds); 0 keeps them forever |
|
||||
| `archive_rotation` | string | How often a `database` target starts a new archive file: `none`, `monthly`, `daily` or `hourly` |
|
||||
| `max_attempts` | integer | How many times an `http` or `slack` target attempts each delivery in all: at least 1, and 1 by default. Ignored by `database` and `log` targets. The web UI labels it Delivery attempts |
|
||||
|
||||
**Relations:** Belongs to Webhook. Has many Deliveries.
|
||||
|
||||
@@ -1598,10 +1603,10 @@ should be forwarded.
|
||||
failed delivery with exponential backoff, up to that many attempts in all,
|
||||
behind a per-target circuit breaker.
|
||||
- **`slack`** — Post the event as a formatted message to a Slack-compatible
|
||||
incoming webhook URL (`webhookUrl` in `config`). It is built on the same HTTP
|
||||
core as `http` and honours `max_attempts` identically, circuit breaker
|
||||
included. See the Slack target section under "Per-Webhook Event Databases" for
|
||||
the message format.
|
||||
incoming webhook URL, its `url`. It is built on the same HTTP core as `http`
|
||||
and honours `max_attempts` identically, circuit breaker included. See the
|
||||
Slack target section under "Per-Webhook Event Databases" for the message
|
||||
format.
|
||||
- **`database`** — Archive the full event as a row into the target's own archive
|
||||
database (`archive-{webhook_name}-{target_name}-{target_uuid}.db`) for
|
||||
long-term retention, with an optional creation-validated expiry (default: keep
|
||||
@@ -1617,29 +1622,27 @@ should be forwarded.
|
||||
- **`log`** — Write the event to the application log (stdout). Useful for
|
||||
debugging.
|
||||
|
||||
The `config` field stores type-specific configuration as JSON (e.g., destination
|
||||
URL, custom headers, timeout settings).
|
||||
Each type uses only its own settings, and the ones it does not use stay empty:
|
||||
`http` uses `url`, `headers`, `timeout_seconds` and `forward_query`; `slack`
|
||||
uses `url`; `database` uses `archive_expiry` and `archive_rotation`; `log` uses
|
||||
none. The add target form, the new webhook form and the target edit form check
|
||||
every setting before it is saved, so nothing that reads a target checks it
|
||||
again.
|
||||
|
||||
**`http` target configuration:**
|
||||
An `http` target POSTs the event to its `url`. Its `headers` are applied last,
|
||||
so they win over the event's own forwarded headers.
|
||||
|
||||
| Key | Type | Description |
|
||||
| -------------- | ------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `url` | string | Destination the event is POSTed to |
|
||||
| `headers` | object | Extra request headers, applied last so they win over the event's own forwarded headers |
|
||||
| `timeout` | integer (sec) | Per-target request timeout; unset (or 0) uses the shared 30-second client timeout |
|
||||
| `forwardQuery` | boolean | Pass the query string each event arrived with on to the target; unset (or false) does not |
|
||||
|
||||
`forwardQuery` is off by default, and the target URL is then sent exactly as
|
||||
`forward_query` is off by default, and the target URL is then sent exactly as
|
||||
configured. On, each delivery appends the event's query string to the target
|
||||
URL, joined with `&` when the URL already has a query string of its own; a
|
||||
replayed delivery and a resubmitted event's deliveries do the same. Both target
|
||||
forms offer it as "Pass the query string on to this target", and the target list
|
||||
shows it when it is on.
|
||||
|
||||
`timeout` is capped at **300 seconds**, and the form rejects anything above it
|
||||
rather than substituting the cap. A delivery attempt holds one of the bounded
|
||||
pool's workers for its whole duration, so an unbounded timeout would let a
|
||||
single unresponsive destination stall the queue.
|
||||
`timeout_seconds` is capped at **300 seconds**, and the form rejects anything
|
||||
above it rather than substituting the cap. A delivery attempt holds one of the
|
||||
bounded pool's workers for its whole duration, so an unbounded timeout would let
|
||||
a single unresponsive destination stall the queue.
|
||||
|
||||
`headers` rejects the names the delivery path or `net/http` writes regardless of
|
||||
what is configured: `Host`, `Content-Length`, `Transfer-Encoding`, `Connection`,
|
||||
@@ -1713,7 +1716,7 @@ The event log and the event's own page show the query string with the rest of
|
||||
the request. The event log leaves out one larger than 32 KiB, as it does request
|
||||
headers, and links to the event's page, which shows it whole. The `database` and
|
||||
`log` targets carry it with the rest of the event. An `http` target receives it
|
||||
only when its `forwardQuery` setting is on.
|
||||
only when its `forward_query` setting is on.
|
||||
|
||||
#### Delivery
|
||||
|
||||
@@ -1992,11 +1995,10 @@ target UUID keeps the file name unique. A webhook named `Orders (EU)` with a
|
||||
target named `Long-term archive` archives into
|
||||
`archive-orders-eu-long-term-archive-{target_uuid}.db`.
|
||||
|
||||
An optional `rotation` in the target's config JSON (e.g. `{"rotation":"daily"}`)
|
||||
is `none`, the default, which keeps the one file, or `monthly`, `daily` or
|
||||
`hourly`. A target that rotates writes each event to a file named for the period
|
||||
of the event's receive time, in UTC, put before the `.db`:
|
||||
`archive-orders-eu-long-term-archive-{target_uuid}-2026-10.db` monthly,
|
||||
A target's `archive_rotation` is `none`, the default, which keeps the one file,
|
||||
or `monthly`, `daily` or `hourly`. A target that rotates writes each event to a
|
||||
file named for the period of the event's receive time, in UTC, put before the
|
||||
`.db`: `archive-orders-eu-long-term-archive-{target_uuid}-2026-10.db` monthly,
|
||||
`…-2026-10-01.db` daily and `…-2026-10-01-19.db` hourly. Each file holds exactly
|
||||
its period's events, and the first event of a new period starts the next file,
|
||||
so a finished period's file can be moved away like any archive. A changed
|
||||
@@ -2031,12 +2033,11 @@ archiving without stopping the service; a moved or removed archive file is
|
||||
recreated automatically on the next write, from the SQL files in
|
||||
`internal/db/migrations/archive/`. A zero-length archive file is written to as a
|
||||
new archive: SQLite opens it as an empty database, so it holds nothing to lose.
|
||||
An optional `expiry` in the target's config JSON (e.g. `{"expiry":"720h"}`) is
|
||||
validated when the target is created — the default (unset or the literal
|
||||
`never`) keeps rows forever — and rows older than the expiry are pruned each
|
||||
time the archive is (re)opened. An archive write failure is never silent
|
||||
success: the delivery records a failed attempt with the error and is marked
|
||||
failed.
|
||||
A target's `archive_expiry` is checked by the form that saves it — the default,
|
||||
`never` on the forms and 0 in the column, keeps rows forever — and rows older
|
||||
than the expiry are pruned each time the archive is (re)opened. An archive write
|
||||
failure is never silent success: the delivery records a failed attempt with the
|
||||
error and is marked failed.
|
||||
|
||||
Because reopens only happen on writes, an archive whose target has stopped
|
||||
receiving events would never be pruned. A background **archive sweeper** closes
|
||||
@@ -2047,8 +2048,7 @@ traffic. The sweep never creates an archive — a target whose archive file does
|
||||
not yet exist is skipped, not initialised — it takes the same per-target lock
|
||||
the write path uses, so it can never interleave with a write, and it leaves the
|
||||
archive closed afterwards so the move-the-file-away workflow keeps working.
|
||||
Archives with no expiry, or the expiry `never`, are not touched by the sweep at
|
||||
all.
|
||||
Archives with the expiry `never` are not touched by the sweep at all.
|
||||
|
||||
For a target with several files, the write path prunes only the file it writes
|
||||
to, and the sweep prunes every one of them, taking the target's lock for one
|
||||
@@ -2116,9 +2116,8 @@ compatible services). Each message includes event metadata (HTTP method, content
|
||||
type, timestamp, body size) and the payload pretty-printed in a code block. JSON
|
||||
payloads are automatically formatted with indentation for readability; non-JSON
|
||||
payloads are shown as raw text. Large payloads are truncated to keep messages
|
||||
reasonable. Config stores `webhookUrl` — the Slack/Mattermost incoming webhook
|
||||
endpoint. That is the JSON key; the error text for a missing one reads
|
||||
`webhook_url is required`, which is the message, not the key.
|
||||
reasonable. The target's `url` is the Slack/Mattermost incoming webhook
|
||||
endpoint.
|
||||
|
||||
The database uses the
|
||||
[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) driver.
|
||||
@@ -2156,7 +2155,7 @@ External Service
|
||||
3. Capture full request as Event
|
||||
4. Create Delivery records for each active Target
|
||||
5. Build self-contained delivery.Task structs
|
||||
(target config + event data inline for
|
||||
(target settings + event data inline for
|
||||
bodies < 16 KiB)
|
||||
6. Notify Engine via channel (no DB read needed)
|
||||
│
|
||||
@@ -3060,14 +3059,14 @@ webhooker/
|
||||
│ │ ├── target_slack.go # Slack/Mattermost incoming-webhook target
|
||||
│ │ ├── target_database.go # Database archive target, writing through internal/archive
|
||||
│ │ ├── target_log.go # Log target (stdout)
|
||||
│ │ ├── target_config_view.go # Masked target config for templates
|
||||
│ │ ├── target_config_view.go # Masked target settings for templates
|
||||
│ │ ├── queue_depth.go # Periodic sampler behind the queue-depth gauges
|
||||
│ │ ├── url_mask.go # Strips credentials from *url.Error
|
||||
│ │ └── ssrf.go # SSRF prevention (IP validation, safe HTTP transport)
|
||||
│ ├── handlers/
|
||||
│ │ ├── handlers.go # Base handler struct, JSON helpers, template rendering
|
||||
│ │ ├── auth.go # Login, logout handlers
|
||||
│ │ ├── delivery_replay.go # Per-delivery replay: new delivery, current target config
|
||||
│ │ ├── delivery_replay.go # Per-delivery replay: new delivery, current target settings
|
||||
│ │ ├── event_resubmit.go # Event resubmit: new event, all currently active targets
|
||||
│ │ ├── entrypoint_view.go # Masked entrypoint view for templates
|
||||
│ │ ├── event_log_view.go # Event log projection, byte-capped in SQL
|
||||
@@ -3082,7 +3081,7 @@ webhooker/
|
||||
│ │ ├── webhook_delete.go # Webhook delete, event database and archive writer removal
|
||||
│ │ ├── event_log.go # Event log page: loaders, filters, delivery views
|
||||
│ │ ├── entrypoint.go # Entrypoint create, edit, delete and toggle
|
||||
│ │ ├── target_create.go # Target create, per-type config builders
|
||||
│ │ ├── target_create.go # Target create, each type's settings from the form
|
||||
│ │ ├── target_delete.go # Target delete
|
||||
│ │ ├── target_toggle.go # Target toggle
|
||||
│ │ ├── shared.go # Helpers shared by several handlers
|
||||
@@ -3164,7 +3163,7 @@ The server starts via
|
||||
which triggers the fx lifecycle hooks in dependency order. The
|
||||
`delivery.Notifier` interface allows the webhook handler to send self-contained
|
||||
`delivery.Task` slices to the engine without a direct package dependency. Each
|
||||
task carries all target config and event data inline (for bodies under 16 KiB,
|
||||
task carries all target settings and event data inline (for bodies under 16 KiB,
|
||||
`delivery.MaxInlineBodySize`), so the engine can deliver without reading from
|
||||
any database — it only writes to record results.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user