feat(surf): serve startup summary, shutdown report and http request log
- serve prints a startup summary on a terminal (version, environment, URL, admin, database host and name, pending migrations, plugins, drivers) and logs it as a 'startup summary' record plus 'listening on <addr>' - on SIGINT/SIGTERM serve drains, prints and logs a shutdown report and returns nil (exit 0), or the drain errors (exit 1) - the request log is on by default under diary component http with request_id; the route pattern, never the path, is logged - lagoon.PendingMigrations, conga.Worker.Stats, diary.HideOnTTY, bonfire.IsTerminal and bonfire.Details back the summary and report
This commit is contained in:
@@ -62,7 +62,7 @@ summer migrate
|
||||
summer serve
|
||||
```
|
||||
|
||||
The next steps also explain the admin. It opens at `/backend` once a plugin registers an admin controller; until then `summer serve` prints `admin: off` with these steps:
|
||||
The next steps also explain the admin. It opens at `/backend` once a plugin registers an admin controller; until then the Admin line of `summer serve`'s startup summary reads `off`, followed by these steps:
|
||||
|
||||
```sh
|
||||
summer make:admin-controller golem15.acme Posts
|
||||
|
||||
@@ -55,7 +55,7 @@ Copy the printed value into `SUMMER_APP__KEY`. When you rotate the key, keep the
|
||||
|
||||
| Command | Flags | Purpose |
|
||||
|---------|-------|---------|
|
||||
| `serve` | `--addr` (default `:8080`) | Opens the database and uploads bucket, builds the router, binds the address, starts the in-process job worker and serves HTTP until SIGINT or SIGTERM, then shuts down within 10 seconds. After binding it prints the listen address with a browser URL (an unspecified host shows as 127.0.0.1) and an admin status line: the admin URL, or `admin: off` with the steps to add an admin controller. |
|
||||
| `serve` | `--addr` (default `:8080`) | Opens the database and uploads bucket, builds the router, binds the address, starts the in-process job worker and serves HTTP until SIGINT or SIGTERM. Once it listens it prints a startup summary; on a signal it drains requests and the worker within 10 seconds and prints a shutdown report. |
|
||||
| `route:list` | none | Builds the router the way `serve` does, without opening the database or listening, and prints every route with its method, pattern, plugin, middleware and raw flag, followed by the admin status line. |
|
||||
|
||||
```sh
|
||||
@@ -67,6 +67,44 @@ The default `--addr` listens on every interface. Pass a loopback address during
|
||||
|
||||
From the application directory, `summer serve --addr 127.0.0.1:8123` passes the flag through to the built binary.
|
||||
|
||||
### Startup summary
|
||||
|
||||
On a terminal `serve` prints a block once the port is bound:
|
||||
|
||||
```text
|
||||
SummerCMS acme v1.4.0 (framework v0.4.0)
|
||||
Environment production
|
||||
URL http://127.0.0.1:8080/
|
||||
Admin http://127.0.0.1:8080/backend
|
||||
Database db.internal:5432/acme
|
||||
Migrations up to date
|
||||
Plugins 3 (acme.user, acme.blog, acme.shop)
|
||||
Queue default, mail, scheduled (worker in serve)
|
||||
Scheduler in serve
|
||||
Mail smtp
|
||||
Cache redis
|
||||
Realtime null
|
||||
Search null
|
||||
```
|
||||
|
||||
The database line shows the host, port and database name, never the user or password. Pending migrations read `3 pending — run bin/acme migrate`. The admin line shows the admin URL, or `off` with the steps to add an admin controller; a warning naming `summer admin:create` follows when the `backend_users` table is empty. `-v` adds one line per plugin with the plugins it requires and its pending migrations, and lets every component's info logs through; `-q` prints no summary, only warnings and errors.
|
||||
|
||||
Off a terminal (systemd, Docker, a redirected log) nothing is printed on stdout: the same facts are logged as one `startup summary` record and one record whose message is exactly `listening on <addr>`, both from the `serve` component, so journald and the daily files keep them and scripts can wait for the second one. On a terminal the two records still reach the daily files, but the console does not repeat them.
|
||||
|
||||
### Shutting down
|
||||
|
||||
Ctrl+C or SIGTERM drains the requests in flight for up to 10 seconds, stops the in-process worker and prints a report (on a terminal) and a `shutdown report` record:
|
||||
|
||||
```text
|
||||
Shutdown report
|
||||
Requests 1520 served, 2 drained, 0 aborted
|
||||
Worker 84 done, 1 failed, 0 handed back to the queue
|
||||
Uptime 3h12m40s
|
||||
Errors none
|
||||
```
|
||||
|
||||
Jobs still running at the deadline are cancelled; River records the cancelled attempt and retries the job later, which the report counts as handed back to the queue. `serve` exits 0 after a clean stop and 1 when draining fails (requests still running at the deadline, or the worker failing to stop).
|
||||
|
||||
## Admin accounts
|
||||
|
||||
| Command | Arguments and flags | Purpose |
|
||||
@@ -88,7 +126,7 @@ TOKEN=$(./bin/acme admin:token:create admin@example.com --name agent --scope blo
|
||||
|
||||
From the application directory, `summer admin:create --email admin@example.com --superuser` runs the same command on the built binary, prompt included. The four `admin:token:*` commands have matching `summer` delegates.
|
||||
|
||||
When the admin is mounted and the `backend_users` table is empty, `serve` prints a warning at start-up naming `summer admin:create`.
|
||||
When the admin is mounted and the `backend_users` table is empty, `serve` prints a warning after its startup summary naming `summer admin:create`.
|
||||
|
||||
## Queues and the scheduler
|
||||
|
||||
|
||||
@@ -146,14 +146,7 @@ Redaction is a safety net for mistakes, not a way to log secrets on purpose. Log
|
||||
|
||||
## Request log
|
||||
|
||||
The router can log every request. It is off by default; turn it on with the `http.access_log` key (environment `SUMMER_HTTP__ACCESS_LOG=true`):
|
||||
|
||||
```yaml
|
||||
# config/http.yaml
|
||||
access_log: true
|
||||
```
|
||||
|
||||
Each request then writes one Info record with the message `http request` once it has been served:
|
||||
The router logs every request. Each request writes one Info record with the message `http request` from the component `http` once it has been served:
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
@@ -161,9 +154,25 @@ Each request then writes one Info record with the message `http request` once it
|
||||
| `pattern` | The matched route, method and path pattern, for example `GET /posts/{id}`. Empty when no route matched. |
|
||||
| `status` | The response status; a handler that writes without setting one counts as 200. A 5xx is logged as 499 (client closed request) only when the client had already gone away when the status was written and the handler did not panic: the error came from the cancellation, and no client received it. A 5xx written before the client went away keeps its status, and a recovered panic is always 500, so a search for 5xx records finds every real server error. |
|
||||
| `duration_ms` | Time spent serving the request, in whole milliseconds. |
|
||||
| `request_id` | The `X-Request-Id` request header, trimmed and cut to 128 bytes; absent when the header is empty. Set it in your reverse proxy to follow one request across the proxy's and the application's logs. |
|
||||
| `client_gone` | `true` when the client went away before the request was served (its context ended with `context.Canceled`); absent otherwise. |
|
||||
|
||||
The record never carries the URL path, the query string, request or response headers or the client address, because tokens often travel in paths and query strings. That holds for a request that reached no route too: a token route requested with another method gets a 405 from the router, and its trailing-slash variant a 404, before any route handler runs, so the path would carry the token. It goes to the published `*slog.Logger` or `slog.Default`, so the redacting handler still applies. Grep for the empty `pattern` to find requests that reached no route; the client's own log (a browser, a proxy) shows which path it asked for.
|
||||
The record carries the route pattern, never the URL path, the query string, request or response headers or the client address, because tokens travel in paths and query strings: share and invitation links carry them in the path. That holds for a request that reached no route too: a token route requested with another method gets a 405 from the router, and its trailing-slash variant a 404, before any route handler runs, so the path would carry the token. Grep for the empty `pattern` to find requests that reached no route; the client's own log (a browser, a proxy) shows which path it asked for.
|
||||
|
||||
The log is on by default, at Info, so the console shows it and the daily files keep what their level lets through. Silence it with the component level, or turn it off with `http.access_log` (environment `SUMMER_HTTP__ACCESS_LOG=false`):
|
||||
|
||||
```yaml
|
||||
# config/log.yaml
|
||||
components:
|
||||
http: warn
|
||||
```
|
||||
|
||||
```yaml
|
||||
# config/http.yaml
|
||||
access_log: false
|
||||
```
|
||||
|
||||
It goes to the published `*slog.Logger` when a plugin publishes one, else to the `http` component logger, so the redacting handler applies on every channel.
|
||||
|
||||
## Panics
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ curl http://127.0.0.1:8080/
|
||||
|
||||
It returns `{"message":"Hello from SummerCMS","plugin":"golem15.demo"}`. Stop the database with `docker compose down`, or `docker compose down -v` to delete its data as well.
|
||||
|
||||
`summer serve` prints the URL to open and the admin status. The admin lives at `http://127.0.0.1:8080/backend` once a plugin registers an admin controller; until then the status line reads `admin: off` and names the steps. When the admin is there, create the first administrator:
|
||||
`summer serve` prints a startup summary with the URL to open and the admin status. The admin lives at `http://127.0.0.1:8080/backend` once a plugin registers an admin controller; until then the summary's Admin line reads `off` and the steps follow it. When the admin is there, create the first administrator:
|
||||
|
||||
```sh
|
||||
summer admin:create --email you@example.com --superuser
|
||||
|
||||
Reference in New Issue
Block a user