Users' guide
Logging and metrics
This page describes the server log and the access log of ScalWS, the endpoints of the metrics listener, and every metric family it exports.
Design record: ADR-0005; per-domain counters: ADR-0042; upstream telemetry: ADR-0043.
Logging
server:
logging:
format: json # json (default) | text
level: info # default
access_log: true # default| Key | Default | Reload | Meaning |
|---|---|---|---|
format | json | read when the process starts | json: one JSON object per line. text: human-readable lines. |
level | info | read when the process starts | A tracing filter directive: a level (error, warn, info, debug, trace), optionally with per-target levels such as info,scalws::access=off. |
access_log | true | applies on reload | One line per request. |
The environment variable SCALWS_LOG overrides level when it is set (same syntax). An
invalid filter stops the start with invalid log filter: ....
All logs go to standard output. The scalwsd unit writes them to the journal
(journalctl -u scalwsd -f); under scalws-controller each instance’s output goes to its
own file (see files-and-services.md).
Log targets worth knowing:
scalws::access: the access log (levelinfo). Alevelofwarnor higher therefore also hides the access log;info,scalws::access=offkeeps the server log and drops the access log.scalws::audit: one line per change made through the admin API ("admin request"), with the method, path, query, status, and the caller’s uid, gid and pid (admin socket) or client certificate (remote admin).
The access log line
One line is written per request that reached the request pipeline, when the response body
has been sent completely or was dropped (for example because the client went away). Requests
refused while the connection was set up or the request head was parsed (TLS failures, 400
or 431 from the HTTP parser, timeouts) have no access log line; they are counted in
scalws_rejections_total.
| Field | Content |
|---|---|
request_id | The request id, also sent as the x-request-id response header: 24 hexadecimal characters. |
listener | Name of the listener (server.listeners[].name). |
remote | Client address: the peer, or the address reported by a trusted proxy. |
scheme | http or https. |
version | HTTP/1.0, HTTP/1.1, HTTP/2.0 or HTTP/3.0. |
method | Request method. |
host | Normalised host: the configured domain for an exact match, otherwise the request’s host; - when the request was refused before the host was read. |
path | The raw path as received (still percent-encoded, without the query). |
tenant, app | The application that answered, or _unmatched for requests refused before routing or for unknown hosts. |
handler | static, proxy (also PHP, Node.js, Python and containers), respond (also redirect), cache (answered from the response cache) or none (no handler ran). |
upstream_member | The upstream pool member that answered (host:port or unix:/path); - when the request did not go through an upstream pool. |
upstream_attempts | Members of the pool the request was sent to (more than 1 after retries); 0 without a pool. |
user_agent | The request’s User-Agent header, at most 256 bytes (bytes that are not UTF-8 are replaced); - without the header. |
status | Response status code. |
bytes | Response body bytes sent. |
complete | false when the body was not sent completely. |
duration_ms | Milliseconds from the start of the request to the end of the response body. |
Example with format: json (one line, wrapped here):
{"timestamp":"2026-10-12T09:15:02.481233Z","level":"INFO","request_id":"5f0c2a9be41d7730000004d2",
"listener":"https","remote":"192.0.2.10","scheme":"https","version":"HTTP/2.0","method":"GET",
"host":"shop.example.com","path":"/products/42","tenant":"shop","app":"storefront",
"handler":"proxy","upstream_member":"192.0.2.5:8080","upstream_attempts":1,
"user_agent":"Mozilla/5.0 (X11; Linux x86_64)","status":200,
"bytes":5120,"complete":true,"duration_ms":12.7,
"target":"scalws::access"}With format: text the same fields follow the time, level and target as key=value pairs.
The query string is not logged. The path contains only characters valid in a URI, so it cannot break a log line.
The metrics listener
server:
metrics:
enabled: true # default
address: "127.0.0.1:9901" # default; the packaged scalws.yaml uses 127.0.0.1:9900
hosts: true # per-domain counters, default
max_hosts: 1000 # defaultenabled and
address need a restart;
hosts and
max_hosts apply on reload.
The listener has no authentication. Keep it on loopback: on another address the check warns
metrics endpoint is not bound to loopback; it has no authentication. It answers GET only
and serves a read-only set of paths; each path may also be written with a /v1 prefix.
| Path | Answer |
|---|---|
/metrics | All metric families below, Prometheus text format (text/plain; version=0.0.4). |
/healthz | 200 ok; 503 draining once shutdown has started. Liveness. |
/readyz | JSON {"state", "generation", "instance", "pid"}. state is active (accepting connections), ready (prepared, waiting for activation by scalws-controller) or draining (with status 503). |
/dashboard and / | A read-only HTML status page that refreshes every 5 seconds from /v1/status and /v1/apps. |
/status, /apps, /diagnose, /optimizer, /explain | The JSON views behind scalwsctl status, apps, diagnose, recommendations and explain. |
server.health.address serves the same two endpoints (and /metrics with
server.health.metrics: true) on an address of its own, for orchestrator probes; it serves
nothing else. During limits.shutdown_delay, /readyz answers 503 with
"state":"draining" while requests are still served.
Other admin views answer 403 with
this view is only available on the admin socket (server.admin.socket), a POST answers
403 with changes are only accepted on the admin socket (server.admin.socket), other
methods 405 (use GET or POST), unknown paths 404.
curl -s 127.0.0.1:9901/healthz
curl -s 127.0.0.1:9901/metrics | grep scalws_http_requests_totalMetric families
All names carry the prefix scalws. Counters end in _total; histograms are exposed with the
usual _bucket, _sum and _count series. A labelled family appears in /metrics once it
has at least one series. Series of tenants and applications that a reload removed are
dropped.
Common labels:
tenant,app: the tenant and application names;_unmatchedfor requests refused before routing.listener: the listener name.status(requests): the status class1xxto5xx.method:GET,HEAD,POST,PUT,DELETE,PATCH,OPTIONS,CONNECT,TRACEorOTHER.handler:static,proxy,respond,cacheornone(as in the access log).protocol(upstream):http1(reverse proxy and managed HTTP runtimes) orfastcgi(PHP).
Latency histograms (http_request_duration, http_response_transfer, tls_handshake)
use buckets from 0.5 ms to 60 s; the upstream histograms from 50 µs to 30 s.
Requests and connections
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_http_requests_total | counter | tenant, app, listener, method, status, handler | HTTP requests handled. |
scalws_http_request_duration_seconds | histogram | tenant, app, handler | Time from request head received to response head sent. |
scalws_http_response_transfer_seconds | histogram | tenant, app | Time from the response head to the end of the response body. |
scalws_http_response_body_bytes_total | counter | tenant, app | Response body bytes sent. |
scalws_http_requests_in_flight | gauge | tenant, app | Requests being processed. |
scalws_connections_open | gauge | listener | Open client connections. |
scalws_connections_accepted_total | counter | listener | Accepted client connections. |
scalws_rejections_total | counter | listener, reason | Connections or requests refused before routing. reason: tls_handshake, tls_handshake_timeout, bad_request (malformed or too large request head), header_timeout, connection_limit, source_connection_limit, untrusted_proxy, proxy_protocol. |
scalws_tls_handshake_seconds | histogram | listener | Duration of successful TLS handshakes. |
scalws_h3_goaway_sent_total | counter | listener | HTTP/3 GOAWAY frames sent while draining (ADR-0041). |
scalws_h3_connection_closes_total | counter | listener, how | HTTP/3 connections ended. how: client (closed by the client), drained (closed by the client after a GOAWAY), forced (still open when the drain grace period ended). |
scalws_event_loop_lag_seconds | gauge | event_loop | Highest delay of a 100 ms timer on each event loop over the last second. Only with server.loop_lag_probe. |
Upstreams
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_upstream_failures_total | counter | tenant, app, reason | Responses synthesised because an upstream failed (502, 503, 504). reason, for example connect, timeout, protocol, unavailable. |
scalws_upstream_connections_opened_total | counter | tenant, app, protocol | New connections opened to upstreams. |
scalws_upstream_connections_reused_total | counter | tenant, app, protocol | Upstream requests sent on a kept-alive connection. |
scalws_upstream_retries_total | counter | tenant, app, protocol | Upstream requests sent again after a pooled connection turned out stale. |
scalws_upstream_cancelled_total | counter | tenant, app, protocol | Upstream exchanges abandoned before the response was complete (client gone, response timeout). |
scalws_upstream_wait_seconds | histogram | tenant, app, protocol | Time until an upstream request is written (pool checkout, connect, FastCGI queue). |
scalws_upstream_ttfb_seconds | histogram | tenant, app, protocol | Time from writing an upstream request to its response head. |
scalws_upstream_requests_total | counter | pool, member, class | Requests answered through upstream pools by member and status class (2xx to 5xx, error when the member gave no response). Every attempt counts, retries included. |
scalws_upstream_request_duration_seconds | histogram | pool | Time to the response head of pool requests, retries included. |
scalws_upstream_member_ejected | gauge | pool, member | 1 while passive health keeps the member out. |
scalws_upstream_pool_retries_total | counter | pool | Requests sent to another member of the pool (retries). Not to be confused with scalws_upstream_retries_total above, which counts requests sent again on a fresh connection to the same upstream. |
scalws_upstream_members | gauge | pool | Members of the pool. Series of removed members and pools disappear; see load-balancing.md. |
scalws_runtime_ready_workers | gauge | tenant, app | Managed application workers that are ready. |
scalws_runtime_restarts_total | counter | tenant, app | Managed application process restarts. |
Security and quotas
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_security_blocked_total | counter | tenant, app, reason | Requests refused by security rules. reason: ip_rate (429), misdirected (421), denied_path (404), route_rate (429), route_access (403, a route’s allow/deny), basic_auth (401, missing or wrong credentials on a route with basic_auth), basic_auth_busy (503, 64 password checks already running). See request-processing.md. |
scalws_tenant_limited_total | counter | tenant, reason | Requests refused by tenant quotas. reason: rate (429) or concurrency (503). |
Response cache
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_cache_lookups_total | counter | tenant, app, result | Response cache lookups. result: hit, stale, miss, bypass. |
scalws_cache_entries | gauge | none | Cached URLs (updated when /metrics is read). |
scalws_cache_bytes | gauge | none | Memory used by cached responses. |
scalws_cache_disk_entries | gauge | none | Responses held by the disk tier of the cache. |
scalws_cache_disk_bytes | gauge | none | Bytes used by the disk tier of the cache. |
See caching.md.
Tenant and application resources
These are sampled from cgroup v2 (ADR-0010) and
eBPF (ADR-0025) when those are enabled
(server.cgroups,
server.ebpf).
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_cgroups_enabled | gauge | none | 1 when cgroup v2 limits are enforced. |
scalws_tenant_cpu_seconds | gauge | tenant | CPU time used by the tenant’s application processes. |
scalws_tenant_memory_bytes | gauge | tenant | Memory used by the tenant’s application processes. |
scalws_tenant_pids | gauge | tenant | Processes and threads in the tenant’s cgroup. |
scalws_tenant_oom_kills | gauge | tenant | OOM kills in the tenant’s cgroup since it was created. |
scalws_app_cpu_seconds | gauge | tenant, app | CPU time used by the application’s processes. |
scalws_app_memory_bytes | gauge | tenant, app | Memory used by the application’s processes. |
scalws_app_pids | gauge | tenant, app | Processes and threads in the application’s cgroup. |
scalws_app_network_bytes | gauge | tenant, app, direction | Network bytes of the application’s processes since start. direction: ingress or egress, from the application’s point of view. |
scalws_app_network_packets | gauge | tenant, app, direction | Network packets of the application’s processes since start. |
TLS and ACME
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_tls_certificate_expiry_seconds | gauge | listener, certificate | Expiry (Unix time) of configured listener certificates; certificate is the first configured server name. |
scalws_acme_certificate_expiry_seconds | gauge | tenant, app | Expiry (Unix time) of ACME-managed certificates. |
scalws_acme_orders_total | counter | result | ACME certificate orders. result: success or failure. |
Configuration, diagnostics and advisors
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_config_reloads_total | counter | result | Configuration activation attempts. result: success or failure. |
scalws_config_generation | gauge | none | Generation number of the active configuration. |
scalws_config_last_success_timestamp_seconds | gauge | none | Unix time of the last successful configuration activation. |
scalws_diagnostic_findings_total | counter | tenant, app, code | Diagnostic findings, counted when they appear (ADR-0015). |
scalws_optimizer_decisions_total | counter | tenant, app, rule, disposition | Optimizer decisions (ADR-0014). disposition: recommended, applied, rolled_back, kept, advisory. |
scalws_ai_requests_total | counter | result | Local AI advisor requests by result (ADR-0016). |
scalws_ai_queue_depth | gauge | none | Pending local AI advisor requests. |
scalws_build_info | gauge | version | Always 1; version is the ScalWS version. |
Per-domain counters
| Name | Type | Labels | Meaning |
|---|---|---|---|
scalws_http_host_requests_total | counter | tenant, app, host, status | HTTP requests by configured domain and status class. |
scalws_http_host_response_bytes_total | counter | tenant, app, host | Response body bytes by configured domain. |
scalws_http_host_request_bytes_total | counter | tenant, app, host | Request body bytes read by configured domain. |
scalws_http_host_cache_total | counter | tenant, app, host, result | Response cache lookups by configured domain. result: hit (stale answers included) or miss. |
scalws_http_host_requests_in_flight | gauge | tenant, app, host | Requests being handled by configured domain. |
These counters are controlled by
server.metrics.hosts. They are on
by default; hosts: false turns them off completely (nothing is recorded or exported).
hostis the configured domain pattern, not the request’s host: all hosts matched by*.example.comcount underhost="*.example.com". The number of series is therefore bounded by the configuration, never by clients.- Requests whose host matches no domain count under
host="_other"(with tenant and app_unmatched), and so do domains beyondserver.metrics.max_hosts(default 1000, at most 16383). - Domains without requests are left out. A reload keeps the counters of every domain that stays.
See also
- request-processing.md: when each counter is incremented.
- troubleshooting.md: health checks.
- security.md: limits and rate limits behind the
reasonlabels.