1 Commits
Author SHA1 Message Date
sneak a69ec23979 Rewrite README.md sentence by sentence
check / check (push) Canceled after 0s
Rewritten from scratch against the code on next. The old text was
the list of what a reader needs covered.

Model: fable-5-1
2026-10-25 10:00:32 +00:00
+134 -118
View File
@@ -1,8 +1,8 @@
# webhooker # webhooker
webhooker is an MIT-licensed web service, written in [Go](https://golang.org) by webhooker is an MIT-licensed [Go](https://golang.org) web service by
[@sneak](https://sneak.berlin), that receives the HTTP requests other services [@sneak](https://sneak.berlin) that receives the HTTP requests other services
send when something happens and forwards them to your own systems. send and forwards them to your own systems.
Many services send an HTTP request to a URL you give them when something Many services send an HTTP request to a URL you give them when something
happens: a code push, a payment, a new ticket. webhooker receives those happens: a code push, a payment, a new ticket. webhooker receives those
@@ -30,8 +30,8 @@ Five words name the moving parts.
it. The UUID is a random 36-character identifier, and it is the only thing it. The UUID is a random 36-character identifier, and it is the only thing
that authorises a sender. that authorises a sender.
- A **target** is a place events are delivered to. There are four kinds: an HTTP - A **target** is a place events are delivered to. There are four kinds: an HTTP
URL, a Slack or Mattermost message-posting URL, an archive file, and the URL, the message-posting URL of a chat service (Slack or Mattermost), an
application log. archive file, and the application log.
- An **event** is one stored request: its method, query string, headers and - An **event** is one stored request: its method, query string, headers and
body. body.
- A **delivery** is one event paired with one target. It records every attempt. - A **delivery** is one event paired with one target. It records every attempt.
@@ -102,7 +102,7 @@ sooner after 24 hours without use, unless `SESSION_IDLE_TIMEOUT` says otherwise.
### Webhooks ### Webhooks
**New Webhook** on the webhook list at `/hooks` asks for **Name**, an optional On the webhook list at `/hooks`, **New Webhook** asks for **Name**, an optional
**Description** and **Retention (days)**. Events older than the retention period **Description** and **Retention (days)**. Events older than the retention period
are deleted, along with their deliveries. The default is 30 days. `0` keeps are deleted, along with their deliveries. The default is 30 days. `0` keeps
events forever. The form also has an **HTTP target URL** field and an events forever. The form also has an **HTTP target URL** field and an
@@ -123,7 +123,7 @@ deactivated entrypoint answers `410 Gone`. A deleted or unknown one answers
### Targets ### Targets
**Add** under **Targets** asks for the type first, then that type's settings. Under **Targets**, **Add** asks for the type first, then that type's settings.
- An `http` target posts each event to a URL, with optional **Headers** and a - An `http` target posts each event to a URL, with optional **Headers** and a
**Timeout**. **Timeout**.
@@ -174,19 +174,19 @@ Do not set `BIND_ADDRESS` to an empty value in a container. An empty value means
unset, and unset falls back to `127.0.0.1`, so the container answers nothing on unset, and unset falls back to `127.0.0.1`, so the container answers nothing on
its published port. its published port.
The app runs as user `webhooker`. The image's start-up script, The app runs as user `webhooker`. The image's start-up script is
`deploy/docker-entrypoint.sh`, gives `DATA_DIR` and its contents to that user `deploy/docker-entrypoint.sh`. It gives `DATA_DIR` and its contents to that user
before the app starts. So a host directory can be mounted as it is. webhooker before the app starts. So a host directory can be mounted as it is. webhooker
creates every SQLite file with mode `0600`. creates every SQLite file with mode `0600`.
### Running under upaas ### Running under upaas
[upaas](https://git.eeqj.de/sneak/upaas), a deployment tool, builds the image [upaas](https://git.eeqj.de/sneak/upaas) is a deployment tool. It builds the
from this repository's `Dockerfile` and runs it. Add no port mapping, because image from this repository's `Dockerfile` and runs it. Add no port mapping,
upaas publishes every mapped port on all interfaces of the host. Put the app on because upaas publishes every mapped port on all interfaces of the host. Put the
your reverse proxy's Docker network instead, and mount one host directory at app on your reverse proxy's Docker network instead, and mount one host directory
`/var/lib/webhooker`. The first-boot password goes to the container's log. Read at `/var/lib/webhooker`. The first-boot password goes to the container's log.
it with `docker logs` and the name upaas gave the container. Read it with `docker logs` and the name upaas gave the container.
### Behind a reverse proxy ### Behind a reverse proxy
@@ -201,19 +201,22 @@ Four things must be right.
1. **Keep the app port private.** Only the proxy may reach it: `127.0.0.1` on a 1. **Keep the app port private.** Only the proxy may reach it: `127.0.0.1` on a
bare host, or a port published to `127.0.0.1` for the image. bare host, or a port published to `127.0.0.1` for the image.
2. **Send `X-Forwarded-Proto: https`.** This header tells webhooker the request 2. **Send `X-Forwarded-Proto: https`.** This header tells webhooker the request
arrived over TLS. It decides the `Secure` flag on cookies, and whether a form arrived over TLS. It decides the `Secure` flag on cookies, the flag that
submission must carry a matching `Origin` or `Referer`. webhooker reads it makes a browser send them only over HTTPS. It also decides whether a form
from any connecting machine, so the proxy must overwrite whatever the client submission must carry a matching `Origin` or `Referer` header. A browser
sent. sends those two headers to say which site or page a request came from.
webhooker reads `X-Forwarded-Proto` from any connecting machine, so the proxy
must overwrite whatever the client sent.
3. **Cover the proxy in `TRUSTED_PROXIES`.** Otherwise every client shares one 3. **Cover the proxy in `TRUSTED_PROXIES`.** Otherwise every client shares one
rate-limit bucket, the count a limit keeps per client. A proxy on the host rate-limit bucket, the count a limit keeps per client. A proxy on the host
reaches the container from the Docker bridge address, such as `172.17.0.1`. reaches the container from the Docker bridge address, such as `172.17.0.1`.
The default list covers it. The list does not cover loopback, so on a bare The default list covers it. The list does not cover loopback, so on a bare
host set `TRUSTED_PROXIES=127.0.0.1`. host set `TRUSTED_PROXIES=127.0.0.1`.
4. **Pass on the client's `Host` header with its port.** webhooker checks a 4. **Pass on the client's `Host` header with its port.** `Host` is the header
form's `Origin` against the `Host` header it receives. A `Host` without the that names the site the client asked for. webhooker checks a form's `Origin`
port makes every form submission fail with `403` on any port other than 443. against the `Host` header it receives. A `Host` without the port makes every
In nginx that means `$http_host`, not `$host`. form submission fail with `403` on any port other than `443`. For the nginx
reverse proxy that means `$http_host`, not `$host`.
Protect the proxy's access log: its request paths hold the entrypoint UUIDs. Protect the proxy's access log: its request paths hold the entrypoint UUIDs.
@@ -246,7 +249,10 @@ client first.
HSTS is a response header that tells browsers to use HTTPS only for this HSTS is a response header that tells browsers to use HTTPS only for this
hostname and every name under it, for `max-age` seconds. Every response carries hostname and every name under it, for `max-age` seconds. Every response carries
it unless `HSTS_MAX_AGE` is `0`. A browser keeps the rule until `max-age` runs it unless `HSTS_MAX_AGE` is `0`. A browser keeps the rule until `max-age` runs
out, so serve webhooker on a hostname you will keep on HTTPS. out, so serve webhooker on a hostname you will keep on HTTPS. The header always
carries `preload`. On a whole domain such as `example.com`, that lets the domain
be added to the HTTPS-only list that browsers ship with. That listing is hard to
undo.
## Recovering a lost admin password ## Recovering a lost admin password
@@ -282,11 +288,11 @@ All state lives in `DATA_DIR`. Back up the whole directory. It holds:
- the files of each archive target; - the files of each archive target;
- the lock file, `webhooker.lock`, which is always empty. - the lock file, `webhooker.lock`, which is always empty.
Every database runs in WAL mode. WAL is SQLite's write-ahead log: finished Every database runs in WAL mode. WAL is SQLite's write-ahead log. Finished
writes land in a `-wal` file beside the database before they reach the `.db` writes land in a `-wal` file beside the database before they reach the `.db`
itself, and a `-shm` file beside it indexes that log. A copy of the `.db` itself. A `-shm` file beside it indexes that log. A copy of the `.db` without
without its `-wal` is missing data. A clean shutdown removes the `-wal` and its `-wal` is missing data. A clean shutdown removes the `-wal` and `-shm`
`-shm` files; a crash leaves them, and they must be carried with the `.db`. Keep files; a crash leaves them, and they must be carried with the `.db`. Keep
`DATA_DIR` on a local disk: WAL mode is not reliable on network filesystems, and `DATA_DIR` on a local disk: WAL mode is not reliable on network filesystems, and
webhooker does not check for one. webhooker does not check for one.
@@ -302,7 +308,7 @@ done
``` ```
Each file is captured at its own instant. A webhook created during the backup Each file is captured at its own instant. A webhook created during the backup
can be in the copy of `webhooker.db` with no event database in the backup; a can be in the copy of `webhooker.db` with no event database in the backup. A
restore then creates that event database empty. restore then creates that event database empty.
To restore, stop the service and put back every file together, with any `-wal` To restore, stop the service and put back every file together, with any `-wal`
@@ -320,11 +326,11 @@ plain text. Protect it accordingly.
### The entrypoint URL is the credential ### The entrypoint URL is the credential
Each entrypoint is a random version 4 UUID served at `/h/{uuid}`. Holding that Each entrypoint is a random UUID served at `/h/{uuid}`. Holding that URL is what
URL is what authorises a sender. The receiver checks nothing else: no shared authorises a sender. The receiver checks nothing else: no shared secret, no HMAC
secret, no HMAC signature (a hash of the body made with a shared key), no token signature (a hash of the body made with a shared key), no token header. If a
header. Signature headers a sender adds anyway are stored and forwarded as sender adds signature headers anyway, they are stored and forwarded as ordinary
ordinary headers. headers.
Keep the URL out of tickets, chat and screenshots. webhooker's own log never Keep the URL out of tickets, chat and screenshots. webhooker's own log never
writes it. The reverse proxy's access log does hold it, in the request path. writes it. The reverse proxy's access log does hold it, in the request path.
@@ -333,21 +339,26 @@ writes it. The reverse proxy's access log does hold it, in the request path.
- Passwords are hashed with Argon2id, a slow password hashing function. - Passwords are hashed with Argon2id, a slow password hashing function.
- Session cookies are signed, `HttpOnly` (hidden from page scripts) and - Session cookies are signed, `HttpOnly` (hidden from page scripts) and
`SameSite=Lax` (not sent with cross-site form posts). They carry `Secure` when `SameSite=Lax` (not sent with cross-site form posts). They carry `Secure`
the request carries `X-Forwarded-Proto: https`. (sent only over HTTPS) when the request carries `X-Forwarded-Proto: https`.
- Every form is protected against cross-site request forgery (CSRF). CSRF is - Every form is protected against cross-site request forgery (CSRF). CSRF is
when another site makes your browser submit a form here. Each form carries a when another site makes your browser submit a form here. Each form carries a
token tied to a cookie, and a submission's `Origin` must name this host. token tied to a cookie, and a submission's `Origin` must name this host.
- Form bodies are capped at 1 MB. - Form bodies are capped at 1 MB.
- Every response carries these headers: - Every response carries these headers:
- `Strict-Transport-Security`, unless `HSTS_MAX_AGE` is `0`; - `Strict-Transport-Security`, the HSTS header, unless `HSTS_MAX_AGE` is
- `X-Content-Type-Options: nosniff`; `0`;
- `X-Frame-Options: DENY`; - `X-Content-Type-Options: nosniff`, which stops the browser guessing a
file's type;
- `X-Frame-Options: DENY`, which stops other sites showing these pages in a
frame;
- a Content-Security-Policy (CSP), the header that tells the browser where - a Content-Security-Policy (CSP), the header that tells the browser where
scripts and styles may come from, set to scripts and styles may come from, set to
`default-src 'self'; script-src 'self'; style-src 'self'`; `default-src 'self'; script-src 'self'; style-src 'self'`;
- `Referrer-Policy: strict-origin-when-cross-origin`; - `Referrer-Policy: strict-origin-when-cross-origin`, which tells another
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`. site only which site a visitor came from, not which page;
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`, which
denies the pages the camera, microphone and location.
- `/metrics` sits behind HTTP basic authentication, a username and password sent - `/metrics` sits behind HTTP basic authentication, a username and password sent
with each request. with each request.
@@ -357,9 +368,9 @@ The password is checked first, and only a failed sign-in is counted. A correct
password is never answered `429`. Failures are counted per client and submitted password is never answered `429`. Failures are counted per client and submitted
username. From the fifth failure within a minute, each failure is answered username. From the fifth failure within a minute, each failure is answered
`429`. At most two password checks run at once; a request that gets no turn `429`. At most two password checks run at once; a request that gets no turn
within 5 seconds is answered `503`. So those two checks, not the failure count, within 5 seconds is answered `503`. So guessing speed is bounded by the limit of
bound how fast passwords can be guessed. Choose a long random admin password, or two checks at a time, not by the failure count. Choose a long random admin
rate-limit `POST /pages/login` at the reverse proxy. password, or rate-limit `POST /pages/login` at the reverse proxy.
### Delivery destinations ### Delivery destinations
@@ -367,8 +378,9 @@ A delivery target could be used to make webhooker send requests into the network
it sits in and read the answers back. This is called server-side request forgery it sits in and read the answers back. This is called server-side request forgery
(SSRF). webhooker refuses it with the SSRF guard. (SSRF). webhooker refuses it with the SSRF guard.
Every target URL must use `http` or `https` and resolve to a public address. The Every target URL must use `http` or `https`. By default its hostname must also
SSRF guard refuses these ranges: point at a public address, one reachable from the internet. The SSRF guard
refuses these ranges:
- loopback, `127.0.0.0/8` and `::1`; - loopback, `127.0.0.0/8` and `::1`;
- the RFC 1918 private ranges, `10.0.0.0/8`, `172.16.0.0/12` and - the RFC 1918 private ranges, `10.0.0.0/8`, `172.16.0.0/12` and
@@ -376,14 +388,16 @@ SSRF guard refuses these ranges:
- link-local space, which is valid only on one network segment; - link-local space, which is valid only on one network segment;
- carrier-grade NAT space, which internet providers use inside their own - carrier-grade NAT space, which internet providers use inside their own
networks; networks;
- multicast, the IPv6 unique local range, and the other reserved ranges. - multicast, the addresses that reach a group of hosts at once;
- the IPv6 unique local range, `fc00::/7`, the private space of IPv6;
- the other reserved ranges.
The SSRF guard runs when a target is saved and again when each delivery The SSRF guard runs when a target is saved and again when each delivery
connects, so a hostname that changes its address later is still checked. connects. So a hostname that changes its address later is still checked.
Redirects are followed up to ten hops, and each hop is checked. A hop to another Redirects are followed up to ten hops, and each hop is checked. A hop to another
host or port, or from `https` to `http`, drops every configured or forwarded host or port, or from `https` to `http`, drops every configured or forwarded
header except `Content-Type`. So a redirect cannot pass a credential to another header except `Content-Type`, which names the body's format. So a redirect
host. cannot pass a credential to another host.
Egress means the connections webhooker opens outward to deliver. Egress means the connections webhooker opens outward to deliver.
[`ALLOWED_EGRESS_CIDRS`](#allowing-egress-to-your-own-network) lets deliveries [`ALLOWED_EGRESS_CIDRS`](#allowing-egress-to-your-own-network) lets deliveries
@@ -401,8 +415,9 @@ The `admin` password is printed in the clear on the first boot and by
query string, headers and body. query string, headers and body.
When `SENTRY_DSN` is set, error reports go to Sentry, an error reporting When `SENTRY_DSN` is set, error reports go to Sentry, an error reporting
service. A report carries no query string, body or cookies. In place of the path service. A report carries no query string, body or cookies. It carries the route
it carries the route pattern, the route as registered, such as `/h/{uuid}`. pattern in place of the path. A route pattern is the path with its variable part
left as a name, such as `/h/{uuid}`.
## Design ## Design
@@ -411,9 +426,10 @@ sends them on in the background, and SQLite holds everything in between.
The receiver stores the event and one pending delivery per active target in one The receiver stores the event and one pending delivery per active target in one
database write, then queues the deliveries. Background workers run them and database write, then queues the deliveries. Background workers run them and
record each attempt. At start and then every minute, a recovery routine queues record each attempt. At start, a recovery routine queues again every delivery
again any delivery left `pending` or `retrying`, so a stop or a crash loses no left `pending` or `retrying`. It then runs every minute. Each run sends again a
delivery. `retrying` delivery the engine is not working on, and a `pending` one that has
sat untouched for 15 minutes. So a stop or a crash loses no delivery.
The main database holds configuration. Each webhook has its own event database, The main database holds configuration. Each webhook has its own event database,
so writes for one webhook never wait on another webhook's database. The program so writes for one webhook never wait on another webhook's database. The program
@@ -423,10 +439,9 @@ the binary.
## Configuration reference ## Configuration reference
All configuration comes from environment variables. For development, variables All configuration comes from environment variables. Variables can also go in a
can also go in a `.env` file in the working directory. A variable set in the `.env` file in the working directory. A variable set in the real environment
real environment wins over the file. A `.env` file that cannot be parsed aborts wins over the file. A `.env` file that cannot be parsed aborts startup.
startup.
A variable that is unset or empty takes its default. A variable that is set but A variable that is unset or empty takes its default. A variable that is set but
cannot be parsed aborts startup with a message naming it. A Go duration is a cannot be parsed aborts startup with a message naming it. A Go duration is a
@@ -440,10 +455,9 @@ is allowed. In `prod`, no CORS headers are sent.
### `PORT` ### `PORT`
The TCP port the listener binds. Default `8080`. Docker marks the image's The port the listener binds. Default `8080`. Docker marks the image's container
container healthy by a check every 30 seconds. The check requests port `8080` healthy by a check every 30 seconds. The check requests port `8080` whatever
whatever `PORT` says, so a container started with another `PORT` reports `PORT` says, so a container started with another `PORT` reports unhealthy.
unhealthy.
### `BIND_ADDRESS` ### `BIND_ADDRESS`
@@ -456,11 +470,11 @@ are refused. `0.0.0.0` listens on IPv4 only; `::` listens on IPv6 and IPv4.
The directory for every SQLite database and the lock file. Default The directory for every SQLite database and the lock file. Default
`/var/lib/webhooker`. It is created if it is missing. `/var/lib/webhooker`. It is created if it is missing.
Exactly one webhooker process may use a `DATA_DIR` at a time; two would both run Exactly one webhooker process may use a `DATA_DIR` at a time. Two would both run
the recovery routine and send every pending delivery twice. At startup the the recovery routine and could send one delivery twice. At startup the process
process locks `{DATA_DIR}/webhooker.lock` with `flock`, the kernel's file lock, locks `{DATA_DIR}/webhooker.lock` with `flock`, the kernel's file lock. A second
and a second process pointed at the same directory exits with a message naming process pointed at the same directory exits with a message that names the
it. directory and the lock file.
### `DEBUG` ### `DEBUG`
@@ -515,11 +529,11 @@ private ranges. A set value replaces the default.
The list decides whose `X-Forwarded-For` header is believed. `X-Forwarded-For` The list decides whose `X-Forwarded-For` header is believed. `X-Forwarded-For`
is the header a proxy uses to pass on the address of the client it is serving. is the header a proxy uses to pass on the address of the client it is serving.
It is read only when the connecting machine is inside the list. `X-Real-IP` and It is read only when the connecting machine is inside the list. webhooker never
`True-Client-IP` are never read. If any client can reach webhooker, or the proxy reads `X-Real-IP` or `True-Client-IP`, two other headers with the same job. If
in front of it, from a private address, set the list to the proxy's address any client can reach webhooker, or the proxy in front of it, from a private
alone. Otherwise those clients can choose their own `X-Forwarded-For` and bypass address, set the list to the proxy's address alone. Otherwise those clients can
every rate limit. choose their own `X-Forwarded-For` and bypass every rate limit.
### Allowing egress to your own network ### Allowing egress to your own network
@@ -529,8 +543,8 @@ Default empty. Example: `10.0.0.0/8, 172.17.0.0/16`. When the list is set,
startup logs it at `WARN`. startup logs it at `WARN`.
Anyone with the admin account can make webhooker send requests to every network Anyone with the admin account can make webhooker send requests to every network
you list, and read the responses in the event log. List the smallest blocks that you list, and read the responses in **Full Event Log**. List the smallest blocks
cover your destinations, and never `0.0.0.0/0` or `::/0`. that cover your destinations, and never `0.0.0.0/0` or `::/0`.
These addresses stay blocked whatever you list. They are link-local space, the These addresses stay blocked whatever you list. They are link-local space, the
unspecified addresses and cloud metadata endpoints. The unspecified addresses unspecified addresses and cloud metadata endpoints. The unspecified addresses
@@ -555,19 +569,19 @@ default but is reopened by a list that covers it.
## Delivery reference ## Delivery reference
An `http` target posts the event body to its URL with the event's `Content-Type` An `http` target posts the event body to its URL. The request carries the
and headers, then the target's configured **Headers**, which replace event event's `Content-Type` and headers. The target's configured **Headers** are
headers of the same name. Every `http` and `slack` request carries added next and replace event headers of the same name. Every `http` and `slack`
`User-Agent: webhooker/{version}`. The **Headers** field refuses `User-Agent`. A request carries `User-Agent: webhooker/{version}`. `User-Agent` is the header
`2xx` response is a success. The first 4 KiB of the response body is stored with that names the sending program. The **Headers** field refuses `User-Agent`. A
the attempt. **Timeout** is at most 300 seconds, and blank means the 30 second response with a status from 200 to 299 is a success. The first 4 KiB of the
default. **Pass the query string on to this target** appends the event's query response body is stored with the attempt. **Timeout** is at most 300 seconds,
string to the URL. and blank means the 30 second default. **Pass the query string on to this
target** appends the event's query string to the URL.
A `slack` target posts a message that describes the event and shows its body in A `slack` target posts a message that describes the event and shows its body in
a code block, cut at 3500 bytes. An archive target, whose stored type is a code block, cut at 3500 bytes. An archive target appends the event as a row to
`database`, appends the event as a row to its own SQLite file. A `log` target its own SQLite file. A `log` target writes the event as one `INFO` line.
writes the event as one `INFO` line.
**Delivery attempts** is from 1 to 20. A failed attempt marks the delivery **Delivery attempts** is from 1 to 20. A failed attempt marks the delivery
`retrying` and schedules the next attempt; the wait doubles each time, starting `retrying` and schedules the next attempt; the wait doubles each time, starting
@@ -585,11 +599,12 @@ paused.
An archive target writes to An archive target writes to
`archive-{webhook_name}-{target_name}-{target_uuid}.db` in `DATA_DIR`, the two `archive-{webhook_name}-{target_name}-{target_uuid}.db` in `DATA_DIR`, the two
names made safe for a file name. A rotation of `monthly`, `daily` or `hourly` names made safe for a file name. A rotation of `monthly`, `daily` or `hourly`
starts a new file for each period, in UTC, with the period before `.db`, such as starts a new file for each period. Periods follow UTC, the universal time that
`-2026-10-01.db`. Rows older than the expiry are deleted on each write and by ignores local time zones. The period goes before `.db`, such as
the archive sweep. Renaming the webhook or the target renames the files. `-2026-10-01.db`. Rows older than the expiry are deleted each time a file is
**Download** on the webhook page returns the target's whole archive as one opened and by the archive sweep. Renaming the webhook or the target renames the
gzipped JSON file. files. **Download** on the webhook page returns the target's whole archive as
one JSON file compressed with gzip.
## HTTP routes reference ## HTTP routes reference
@@ -613,9 +628,9 @@ gzipped JSON file.
## Metrics reference ## Metrics reference
`/metrics` serves these metrics for Prometheus, the monitoring system, plus the `/metrics` serves these metrics for Prometheus, the monitoring system, plus the
Go runtime and process metrics the Prometheus library provides. Each delivery Go runtime and process metrics the Prometheus library provides. A label is a
metric carries one label, `target_type`, which is `http`, `slack`, `database`, name and value attached to a metric. Each delivery metric carries one label,
`log` or `unknown`. `target_type`, which is `http`, `slack`, `database`, `log` or `unknown`.
- `webhooker_events_received_total` and `webhooker_events_resubmitted_total`, - `webhooker_events_received_total` and `webhooker_events_resubmitted_total`,
two counters (totals that only grow) with no label; two counters (totals that only grow) with no label;
@@ -629,17 +644,17 @@ metric carries one label, `target_type`, which is `http`, `slack`, `database`,
(current values) sampled every 30 seconds; (current values) sampled every 30 seconds;
- `webhooker_circuit_breakers_open`. - `webhooker_circuit_breakers_open`.
Two inbound HTTP metrics, `http_request_duration_seconds` and `http_request_duration_seconds` and `http_response_size_bytes` measure inbound
`http_response_size_bytes`, carry the route pattern as their `handler` label, requests. Their `handler` label is the route pattern, never the real path.
never the real path. A third, `http_requests_inflight`, counts all routes `http_requests_inflight` counts all routes together.
together.
## Development ## Development
You need the Go version named in `go.mod`, and Docker. The tests, the linters, You need the Go version named in `go.mod`, and Docker. The tests, the linters,
prettier and the stylesheet build run in Docker, at versions pinned (fixed to `prettier` and the stylesheet build run in Docker. Their versions are pinned
one exact release) in the `Dockerfile`, `package.json` and `yarn.lock`. `gofmt` (fixed to one exact release) in the `Dockerfile`, `package.json` and
and `goimports` run on the host. `yarn.lock`. `gofmt` runs on the host. `goimports` runs on the host too, when it
is installed. `script/bootstrap` does not install it.
```bash ```bash
make bootstrap # Install git, make, Go and the Go modules make bootstrap # Install git, make, Go and the Go modules
@@ -652,25 +667,26 @@ make build # Build bin/webhooker, version-stamped
make docker # Build the Docker image make docker # Build the Docker image
``` ```
Each `make` target except `build`, `run`, `dev`, `deps` and `clean` runs a Most `make` targets run a script in `script/`, listed under
script in `script/`, listed under [Entrypoints](#entrypoints). Every [Entrypoints](#entrypoints). `build`, `run`, `dev`, `deps` and `clean` run their
`docker build` in `script/` passes `--no-cache`, so a check that passes has own commands. Every `docker build` in `script/` passes `--no-cache`, so a check
really run. that passes has really run.
The binary's version comes from `script/version`: `git describe` on the The binary's version comes from `script/version`: `git describe` on the
checkout, or `VERSION` when that is set. It is shown at checkout, or `VERSION` when that is set. It is shown at
`/.well-known/healthcheck` and in the page footer. `/.well-known/healthcheck` and in the page footer.
The pages use one third-party script, Alpine.js, in its CSP build The pages use one third-party script, Alpine.js. They load `@alpinejs/csp`, the
`@alpinejs/csp`, which runs under a Content-Security-Policy that forbids `eval`. version of it that works when the Content-Security-Policy forbids running text
The npm registry is where JavaScript packages are published. The package file in as code. The npm registry is where JavaScript packages are published. The
`3p/` is byte for byte the one it publishes. package file in `3p/` is byte for byte the one the npm registry publishes.
A browser test in `internal/server` loads the pages in a headless browser, one A browser test in `internal/server` loads the pages in a headless browser, one
without a window, under the real Content-Security-Policy. `make test` leaves it without a window, under the real Content-Security-Policy. `make test` leaves it
out; run `make test-browser` after changing `templates/` or `static/js/`. out; run `make test-browser` after changing `templates/` or `static/js/`.
`static/css/tailwind.css` is generated by Tailwind and committed; `make check` `static/css/tailwind.css` is generated by Tailwind, a CSS generator, and
fails when it is out of date, and `make css` regenerates it. committed; `make check` fails when it is out of date, and `make css` regenerates
it.
## Entrypoints ## Entrypoints
@@ -687,22 +703,22 @@ application are under [Entrypoint URLs](#entrypoint-urls).
- `script/test` runs the tests in Docker. - `script/test` runs the tests in Docker.
- `script/test-browser` runs the browser test in Docker. - `script/test-browser` runs the browser test in Docker.
- `script/lint` runs `gofmt`, `golangci-lint` and ESLint in Docker. - `script/lint` runs `gofmt`, `golangci-lint` and ESLint in Docker.
- `script/fmt` formats the Go code with `gofmt` and `goimports` on the host, and - `script/fmt` formats the Go code with `gofmt` on the host and the Markdown
the Markdown with prettier in Docker. with `prettier` in Docker. It also runs `goimports` when that is installed.
- `script/fmt-check` fails when `gofmt` or prettier would change a file. It does - `script/fmt-check` fails when `gofmt` or `prettier` would change a file. It
not run `goimports`, and it writes nothing. does not run `goimports`, and it writes nothing.
- `script/css` regenerates `static/css/tailwind.css` in Docker. - `script/css` regenerates `static/css/tailwind.css` in Docker.
- `script/css-check` fails when `static/css/tailwind.css` is out of date. - `script/css-check` fails when `static/css/tailwind.css` is out of date.
- `script/check` runs `script/test`, `script/lint`, `script/fmt-check` and - `script/check` runs `script/test`, `script/lint`, `script/fmt-check` and
`script/css-check`. `script/css-check`.
- `script/version` prints the version to stamp into the binary. - `script/version` prints the version to stamp into the binary.
- `script/docker` builds the image tagged `webhooker`. - `script/docker` builds the image tagged `webhooker`.
- `script/cibuild` is what CI, the check run on every push, runs: - CI is the check that runs on every push. It runs `script/cibuild`:
`script/bootstrap`, `script/check`, then the image build. `script/bootstrap`, `script/check`, then the image build.
- `script/precommit` checks that `go mod tidy` changes nothing, then runs - `script/precommit` checks that `go mod tidy` changes nothing, then runs
`script/check`. `script/check`.
- `script/install-precommit` installs the git pre-commit hook that runs - `script/install-precommit` installs the git hook that runs `script/precommit`
`script/precommit`. before each commit.
## TODO ## TODO