Monitoring and Observability
When the web interface responds but a library stops changing, check the background path as well as the web server. A successful web probe does not establish that a scan, outbox relay or consumer is making progress.
Run the commands below on the installed Docker host, from the installation checkout,
with the dc shell function and private environment files from
Getting Started. dc exec -T app runs a command
inside that installation's web container; it does not enter the worker deployment.
For HTTP probes, set BAANDER_URL to the HTTPS origin clients use, with no trailing
slash. The client must trust its certificate.
| Component or stage | First signal | What it establishes |
|---|---|---|
| Reverse proxy and web server | /live, dc logs nginx app |
The request reaches a responding process; read draining too |
| PostgreSQL and Redis | /ready, app:health:check |
Dependency connectivity at the time of the check |
| Web worker memory and pools | app:server:stats, health memory row |
Current web process pressure; the CLI health process measures its own memory |
| Admitted worker deployment | Host controller readiness action |
Current readiness observation of the exact admitted deployment |
| Redis consumer | Messenger heartbeat and job progress | Recent consumer activity; neither proves outbox progress |
| Outbox relay | Worker readiness observation and relay logs | Relay state; follow an actual queued operation to establish delivery |
| Queued and failed work | Job monitor and transport status | Job records and transport counts, with the limits below |
| Registry and relay service | Registry operations | Separate service health and recovery evidence |
See Background Work for the path from a request through the outbox and Redis to a consumer, and Troubleshooting for symptom-led checks.
Health Checks
Baander exposes three HTTP endpoints for health probing. None of these require authentication.
| Endpoint | Purpose | Healthy status | Unhealthy status |
|---|---|---|---|
GET /health |
Component results (PostgreSQL, Redis, consumer, Swoole, memory); worker and memory do not decide HTTP status | 200 with "status": "healthy" |
503 with "status": "unhealthy" |
GET /ready |
Dependency readiness (PostgreSQL, Redis); not ready while the web server drains | 200 with "status": "ready" |
503 with "status": "not_ready" |
GET /live |
Process liveness (Swoole worker alive) | 200 with "status": "alive" and "draining": false |
N/A (always 200 if responding; "draining": true while the web server drains) |
/health lists the background worker and memory, but neither decides the response code: a worker outage or a web server worker near its memory limit does not turn /health into 503, and /ready checks neither. The web server alerts administrators about both instead.
While the web server drains before it stops, /ready answers 503 without checking any dependency. Its checks list holds a single server entry whose details are {"draining": true}. The server accepts no new connections once the drain starts, so only a client that holds a kept-alive connection from before the stop sees this answer, and the answer carries Connection: close. /live still answers 200 during the drain, so liveness probes do not restart the container, and its body says "draining": true. The web player reads that answer as "server not back yet".
These are designed for orchestration use. The Docker healthcheck does not call an endpoint; it runs the app:health:check command. The web image defines it in Dockerfile, and the development stack in docker-compose.yml:
healthcheck:
test: ["CMD", "php", "bin/console", "app:health:check"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
CLI health check
You can also run the health check from the command line inside the container. It prints a table with per-component status and latency:
dc exec -T app php bin/console app:health:check
This runs the same checks as /health and exits with code 1 if a component other
than the background worker or memory is unhealthy; otherwise it exits with code 0.
A not_available row does not fail the command. Outside a web worker, Swoole can
therefore read N/A even when the command succeeds. The memory row measures the
CLI process, not the web server's workers. FFmpeg, provider access and successful
media processing are not checked. See the
full command reference.
What is checked
The health check verifies five components:
| Component | What it checks |
|---|---|
| PostgreSQL | Connection to the database via SELECT 1 |
| Redis | PING command and database size |
| Messenger (background worker) | The age of the heartbeat the worker writes to Redis. Unhealthy after 45 seconds without a renewal while idle or 90 seconds while handling a message, and when the heartbeat disappears. A heartbeat that says the worker stopped reads not available for 45 seconds and unhealthy after that; the 45 seconds give a restarted worker time to report in. Not available until a heartbeat has been seen, and while Redis is unreachable. |
| Swoole | Swoole VM statistics when running inside a Swoole worker; not available outside one or without the extension |
| Memory | The real memory use of each HTTP worker of the web server against PHP's memory_limit, which the production image sets to 512M. Each worker reports every 5 seconds, and the check reads the worker using the most. Unhealthy at 90 percent of the limit, about 461 MiB at 512M; not available when the limit is -1. |
The admitted worker deployment writes consumer heartbeats to Redis, where the
web container reads them. An idle consumer renews about every 10 seconds; while
handling a job, it renews every 30 seconds through the keepalive alarm. Messenger
health details include lastDeliveryQueueAgeSeconds and lastDeliveryAt: these
describe the last dequeued message, not the oldest message still waiting.
These health checks cover the Redis consumer, but do not check the outbox
relay. A fresh heartbeat can coexist with undelivered outbox work. Use the host
controller's separate readiness action and inspect progress and logs as described
in Observe background work.
Controller status returns readiness: "not_checked"; it is an ownership and state
observation, not a readiness test. See app:outbox:consume for relay behavior
and Worker deployment for ownership,
drain and explicit recovery procedures. Restarting web leaves background work
with the worker deployment.
The web server runs these checks every 60 seconds and alerts administrators once per outage; see Health alerts. A planned stop or restart of the web server sends no alert: once the server drains, its checks stop. An outage of a checked component while the server is not draining still alerts.
While Redis is unreachable, requests that require rate-limit or DPoP state answer
503 instead of treating the failure as an empty bucket or an invalid credential.
Rate-limit failures carry Retry-After: 1; DPoP store failures carry
Retry-After: 5. Both carry Cache-Control: no-store. The first check that rejects
the request determines its response. Each Redis connection has one-second connect
and read timeouts. A request can make several Redis calls, so those timeouts do
not promise a one-second response for the whole request.
Clients keep their sessions and retry after the delay. Logout and token revocation retain a narrow availability exception: they can continue when the replay store or generic API budget is unavailable, but still require the other authentication and proof checks. A confirmed exhausted budget still applies. See Sessions, logout and a Redis outage.
The Redis health check alerts once for the outage. The alert is delivered at once
because it is a row in PostgreSQL; the background worker check reads not available
meanwhile. The server logs the store failure, not every 503 it answers.
Job Monitoring
The job monitor records background work such as library scans, transcoding and
notifications. All job endpoints require authentication with ROLE_ADMIN.
API clients must send both the access token with the DPoP scheme and a fresh
DPoP proof signed by the key bound to that token. A token alone is insufficient;
the Bearer scheme is refused for a bound token. Use the authenticated admin
interface or a client implementing that protocol for the API routes listed here.
The shell examples use console counterparts and require host access to the
installation.
Status overview
Get a quick summary of all jobs grouped by status, plus a list of currently running jobs:
dc exec -T app php bin/console app:monitor:status
The console prints status counts and running jobs with progress. The API response
uses counts and running; add --json to the console command for structured
output.
Job list with filtering
Browse jobs with filtering, sorting, and cursor-based pagination:
# Failed jobs, newest first
dc exec -T app php bin/console app:monitor:jobs --status=failed --sort=createdAt --direction=desc --limit=20
# Jobs whose type name contains scan
dc exec -T app php bin/console app:monitor:jobs --type=scan
The API filters are status (queued, running, finished, failed, cancelled),
name (partial match) and queue (exact match). The console uses --type for
the API's name filter; --cursor continues a previous page.
Job detail
Get the full record, including exception details for a failed job. Replace
JOB_ID with the job ID from the list:
dc exec -T app php bin/console app:monitor:job:show JOB_ID
A job keeps one record and one job ID across its deliveries. An automatic retry, a retry from the failure transport, and a redelivery after a worker stopped mid-job all run the same job again: the record returns to running with a new start time, loses the previous attempt's finish time, progress and error, and its attempt number increases. The record keeps its creation time and the queue it was first received from. duration covers the current attempt only.
Retry and cancel
You can retry failed jobs or cancel running jobs through the API. Retrying a job here dispatches a copy of its stored payload as a new job, returned as newJobId, and sets the original record's retried flag while leaving its status failed.
After fixing the cause, choose the failed job deliberately before retrying:
# Dispatch the failed job again under a new job ID
dc exec -T app php bin/console app:monitor:job:retry JOB_ID
# Request cancellation of the selected job
dc exec -T app php bin/console app:monitor:job:cancel JOB_ID
Cancellation is cooperative. The job stops at its next checkpoint, between two items of its work, and its status becomes cancelled; it is not retried and does not go to the failure transport. The metadata sync jobs, the bulk lyrics fetch and the cover extraction have checkpoints; other jobs run to the end. app:monitor:job:cancel lists the jobs and their checkpoints.
A finished bulk lyrics fetch can still be cancelled while the fetches it queued are waiting: they are skipped when they come due, and the job's status changes from finished to cancelled. The job detail's cancellable field says whether cancelling a job can still change anything. Cancelling a failed job, or a finished job with no queued work waiting, answers 409.
Transport status
The admin Transport Health card shows the number of entries in the async Redis stream, the number of messages in the failure transport, and whether the consumer this container is configured with is registered on the stream. The same figures come from the API and the console:
dc exec -T app php bin/console app:monitor:transport
The async count is the Redis stream length. consumerRunning means that the
consumer name configured in the probing container is registered on the stream; a
stale registration can remain after a process stops, and the web container's name
can differ from the admitted consumer's. Compare heartbeat, controller readiness
and changing job progress before concluding that work is processing. This check
also excludes the outbox. See
app:monitor:transport.
Failed messages
A message that exhausts its retries moves to the failure transport, the failed_messages PostgreSQL table. The endpoints below list and manage it. Each one has a console counterpart that reads the same table, including messages waiting out a retry delay.
dc exec -T app php bin/console app:failed-message:list --page=1 --limit=50
Use the ID from that list to inspect the stored message before deciding on a retry:
dc exec -T app php bin/console messenger:failed:show MESSAGE_ID
A job-monitor retry dispatches a new job; a failure-transport retry handles the stored message again. Choose one recovery path after diagnosing the cause so you do not deliberately submit both. A failure-transport retry executes the work and can repeat external effects:
dc exec -T app php bin/console messenger:failed:retry MESSAGE_ID --force
Removal and flush discard the stored work. Use them only when that loss is the intended action; neither repairs its cause. The table keeps their references for that deliberate decision.
| Endpoint | Console counterpart |
|---|---|
GET /api/monitor/transport/failed |
app:failed-message:list |
GET /api/monitor/transport/failed/{id} |
messenger:failed:show <id> |
POST /api/monitor/transport/failed/{id}/retry |
messenger:failed:retry <id> --force |
DELETE /api/monitor/transport/failed/{id} |
messenger:failed:remove <id> --force |
POST /api/monitor/transport/failed/flush?confirm=true |
app:failed-message:flush |
Retrying runs messenger:failed:retry in a child process, which handles the message before the request returns. A message that fails again goes back to the failure transport under a new ID with its retry count increased, including after an unrecoverable exception. It stays listed until a retry succeeds or you remove it. GET /api/monitor/transport/status and app:monitor:transport report the number of failed messages as failedQueueDepth.
Job Analytics
The analytics endpoints provide aggregate insights into job performance over a time range. All require ROLE_ADMIN.
Summary
Status counts, job type breakdown, success rate, and throughput:
dc exec -T app php bin/console app:monitor:analytics --section=summary --from=2026-04-24T00:00:00Z --to=2026-04-25T00:00:00Z
Each endpoint counts the jobs created from from up to but not including to, so the example covers exactly 24 April. Both bounds take an RFC 3339 timestamp with a timezone; to must be after from. They default to the last 24 hours, and a range longer than 90 days ends 90 days after from.
Timing
Average, median, and P95 execution times per job type, plus average queue latency:
dc exec -T app php bin/console app:monitor:analytics --section=timing
Failures
Top failing job types, top exception classes, retry frequency, and recent failure details:
dc exec -T app php bin/console app:monitor:analytics --section=failures --limit=20
| Endpoint | Description |
|---|---|
GET /api/monitor/analytics/summary |
Status counts, success rate, throughput per hour |
GET /api/monitor/analytics/timing |
Execution time stats and queue latency per job type |
GET /api/monitor/analytics/failures |
Top failures, exception classes, retry stats |
Server Stats
The server stats endpoint returns a snapshot of every HTTP worker of the web server. Requires ROLE_ADMIN. The app:server:stats command prints the same data from the web container.
dc exec -T app php bin/console app:server:stats
The data object holds:
| Field | Contents |
|---|---|
workers |
One entry per worker that answered, each with worker_id and the groups below |
workers[].memory |
Current usage, peak, real usage, real peak (all in MB) |
workers[].process |
PID, UID, GID, user, uptime |
workers[].swoole |
Swoole VM status, or null |
workers[].coroutines |
Running coroutines and the peak count |
workers[].pools |
Swoole connection pool stats (active, free, limit per connection) |
missing_workers |
IDs of workers that did not answer in time |
worker_errors |
Workers whose figures could not be read, with the error |
redis |
Connected status, ping result, DB size, client count, memory usage; read once for the server |
Use this for lightweight debugging of memory leaks, connection exhaustion, or hangs without setting up external monitoring.
Prometheus Metrics
Baander exposes a Prometheus-compatible metrics endpoint at GET /metrics. No authentication is required.
curl --fail --show-error --silent "$BAANDER_URL/metrics"
The endpoint returns metrics in Prometheus text exposition format (text/plain; version=0.0.4). Available metrics:
| Metric | Type | Description |
|---|---|---|
swoole_up |
gauge | Whether the Swoole worker is running (1 = up) |
swoole_coroutine_num |
gauge | Current number of active coroutines |
swoole_request_count |
gauge | Total request count |
swoole_connection_num |
gauge | Current connection count |
swoole_worker_num |
gauge | Number of worker processes |
swoole_pool_active{connection="N"} |
gauge | Active connections in pool N |
swoole_pool_free{connection="N"} |
gauge | Free connections in pool N |
swoole_pool_limit{connection="N"} |
gauge | Connection limit for pool N |
Prometheus scrape configuration
Use a scrape target reachable from the Prometheus server. The example below assumes
that Prometheus shares the application network and resolves its app service.
The web port is not a public metrics listener to expose separately:
scrape_configs:
- job_name: 'baander'
scrape_interval: 15s
static_configs:
- targets: ['app:9501']
metrics_path: '/metrics'
Only metrics present in the serving worker's snapshot are emitted. These web metrics do not measure outbox delivery or establish consumer readiness. Restrict network access to diagnostics as described in Security. Grafana dashboard setup is out of scope for this guide.
Rate Limiter Monitor
Inspect and manage configured rate limiters. Requires ROLE_ADMIN.
List all rate limiters
dc exec -T app php bin/console app:rate-limiter:list
This lists every limiter configured under framework.rate_limiter in
config/packages/framework.yaml, with its effective policy, limit, interval,
description and Redis storage namespace. The API field for the namespace is
storageNamespace.
Clear rate limiter state
After diagnosing a rate-limit incident, clear one limiter only if resetting every subject's budget for that limiter is the intended action:
dc exec -T app php bin/console app:rate-limiter:clear auth_login_ip
Each limiter has its own Redis namespace. A clear advances its generation and
resets every subject's budget without scanning keys. Old subject states expire or
are replaced on the next read; an old writer must retry against the new generation.
Other limiters keep their state. An unknown name returns 404 through the API
and fails the console command.
To clear every limiter, call DELETE /api/monitor/rate-limiters/clear?confirm=true,
or use the Clear All button on the admin Rate Limits tab. Clear-all tries each
namespace sequentially and reports failures; successful clears remain in effect.
An unavailable or unacknowledged clear reports failure instead of claiming that
clients were unblocked. A successful generation change does not repair every kind
of corrupt subject record; see the
clear command's recovery limits.
The CLI calls the same application service. Clearing all limiters resets every subject's budgets across them; use it only when that is the intended scope:
dc exec -T app php bin/console app:rate-limiter:clear --all
Rate-limit storage outages
All 19 configured limiters use atomic Redis state while retaining their configured
budgets and fixed/sliding-window policies. A protected request whose limiter state
cannot be established receives a generic 503, Retry-After: 1 and
Cache-Control: no-store; it does not receive a fresh budget from a cache miss.
Ordinary exhaustion keeps the limiter's existing response and retry delay.
The generic API limiter alone permits logout and token revocation to continue when its store is unavailable. It still enforces confirmed exhaustion, and the remaining authentication and DPoP checks still run. Media exemptions and the development bypass are unchanged. The list command reads configuration only; a successful listing does not prove that Redis accepts consumption or clear operations.
Configuration Check
Validate your application configuration from the CLI or the API.
CLI
dc exec -T app php bin/console app:config:validate
This runs all configuration checks and prints a table of results. See the full command reference.
API
GET /api/debug/config-check is the admin API counterpart. Use the authenticated
admin interface or a DPoP-capable client.
The endpoint validates environment variables, key file existence, secret strength, and framework config. It returns a summary with errors, warnings, and passed counts. Rate-limited to prevent abuse.
What is checked
| Check | Severity when failing |
|---|---|
Required env vars (DATABASE_URL, REDIS_URL, APP_SECRET, APP_URL, APP_DOMAIN) |
Error |
REDIS_PASSWORD is set |
Error (prod only) |
DATABASE_URL and REDIS_URL format |
Error |
APP_URL uses HTTPS |
Error (prod only) |
APP_SECRET is not the default placeholder |
Error (prod only) |
| OAuth key file existence | Warning (dev) / Error (prod) |
| VAPID key pair consistency | Warning |
| External API key lengths | Warning |
Logs
Use Compose service names in the installed dc context; fixed Docker container
names can point at a different checkout. These commands read logs without restarting
anything:
dc ps
dc logs --tail=100 app nginx database redis
dc logs --since=1h --tail=200 app
dc logs -f app nginx
Stop following with Ctrl+C. PostgreSQL in the supplied Compose stack also writes
its collected server logs to /var/log/postgresql inside database; inspect that
location if dc logs database shows only the collector startup:
dc exec -T database ls -lt /var/log/postgresql
Read the file matching the incident time after identifying it. Nginx's access log
belongs to nginx, not app:
dc exec -T nginx tail -n 100 /var/log/nginx/access.log
The separate worker deployment has its own control and consumer containers. Use
its exact manifest and the inspection steps in
Background Work to locate their logs;
web logs alone cannot explain an outbox relay failure. For Swoole failures, also
check the configured application log directory: config/packages/swoole.yaml
selects swoole_<environment>.log under the application's var/log directory.
Before sharing logs or diagnostic output, redact tokens, credentials, private paths and personal data. Preserve timestamps, job IDs and the relevant error chain.
Quick Reference
| Command / Endpoint | Description |
|---|---|
GET /health |
System health check (no auth) |
GET /ready |
Readiness probe (no auth) |
GET /live |
Liveness probe (no auth) |
GET /metrics |
Prometheus metrics (no auth) |
GET /api/debug/stats |
Server diagnostics (admin) |
GET /api/debug/config-check |
Configuration validation (admin) |
GET /api/monitor/status |
Job status summary (admin) |
GET /api/monitor/jobs |
Job list with filtering (admin) |
GET /api/monitor/jobs/{jobId} |
Job detail (admin) |
POST /api/monitor/jobs/{jobId}/retry |
Retry a failed job (admin) |
POST /api/monitor/jobs/{jobId}/cancel |
Cancel a job (admin) |
GET /api/monitor/transport/status |
Transport queue depths and consumer state (admin) |
GET /api/monitor/transport/failed |
Failed message list (admin) |
GET /api/monitor/transport/failed/{id} |
Failed message detail (admin) |
POST /api/monitor/transport/failed/{id}/retry |
Retry a failed message (admin) |
DELETE /api/monitor/transport/failed/{id} |
Remove a failed message (admin) |
POST /api/monitor/transport/failed/flush?confirm=true |
Remove all failed messages (admin) |
dc exec -T app php bin/console app:monitor:transport |
CLI transport queue depths and consumer state |
dc exec -T app php bin/console app:failed-message:list |
CLI failed message list |
dc exec -T app php bin/console app:failed-message:flush |
CLI removal of all failed messages |
GET /api/monitor/analytics/summary |
Job analytics summary (admin) |
GET /api/monitor/analytics/timing |
Job timing analytics (admin) |
GET /api/monitor/analytics/failures |
Job failure analytics (admin) |
GET /api/monitor/rate-limiters |
Rate limiter catalog (admin) |
DELETE /api/monitor/rate-limiters/clear?confirm=true |
Clear every rate limiter (admin) |
DELETE /api/monitor/rate-limiters/{name}/clear?confirm=true |
Clear rate limiter state (admin) |
dc exec -T app php bin/console app:health:check |
CLI health check |
dc exec -T app php bin/console app:config:validate |
CLI configuration validation |
dc logs -f app |
Follow this installation's app logs |
dc logs --since=1h app |
Recent app logs |
dc ps |
This installation's Compose service status |