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
KeyDefaultReloadMeaning
formatjsonread when the process startsjson: one JSON object per line. text: human-readable lines.
levelinforead when the process startsA tracing filter directive: a level (error, warn, info, debug, trace), optionally with per-target levels such as info,scalws::access=off.
access_logtrueapplies on reloadOne 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 (level info). A level of warn or higher therefore also hides the access log; info,scalws::access=off keeps 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.

FieldContent
request_idThe request id, also sent as the x-request-id response header: 24 hexadecimal characters.
listenerName of the listener (server.listeners[].name).
remoteClient address: the peer, or the address reported by a trusted proxy.
schemehttp or https.
versionHTTP/1.0, HTTP/1.1, HTTP/2.0 or HTTP/3.0.
methodRequest method.
hostNormalised host: the configured domain for an exact match, otherwise the request’s host; - when the request was refused before the host was read.
pathThe raw path as received (still percent-encoded, without the query).
tenant, appThe application that answered, or _unmatched for requests refused before routing or for unknown hosts.
handlerstatic, proxy (also PHP, Node.js, Python and containers), respond (also redirect), cache (answered from the response cache) or none (no handler ran).
upstream_memberThe upstream pool member that answered (host:port or unix:/path); - when the request did not go through an upstream pool.
upstream_attemptsMembers of the pool the request was sent to (more than 1 after retries); 0 without a pool.
user_agentThe request’s User-Agent header, at most 256 bytes (bytes that are not UTF-8 are replaced); - without the header.
statusResponse status code.
bytesResponse body bytes sent.
completefalse when the body was not sent completely.
duration_msMilliseconds 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            # default

enabled 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.

PathAnswer
/metricsAll metric families below, Prometheus text format (text/plain; version=0.0.4).
/healthz200 ok; 503 draining once shutdown has started. Liveness.
/readyzJSON {"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, /explainThe 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_total

Metric 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; _unmatched for requests refused before routing.
  • listener: the listener name.
  • status (requests): the status class 1xx to 5xx.
  • method: GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, CONNECT, TRACE or OTHER.
  • handler: static, proxy, respond, cache or none (as in the access log).
  • protocol (upstream): http1 (reverse proxy and managed HTTP runtimes) or fastcgi (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

NameTypeLabelsMeaning
scalws_http_requests_totalcountertenant, app, listener, method, status, handlerHTTP requests handled.
scalws_http_request_duration_secondshistogramtenant, app, handlerTime from request head received to response head sent.
scalws_http_response_transfer_secondshistogramtenant, appTime from the response head to the end of the response body.
scalws_http_response_body_bytes_totalcountertenant, appResponse body bytes sent.
scalws_http_requests_in_flightgaugetenant, appRequests being processed.
scalws_connections_opengaugelistenerOpen client connections.
scalws_connections_accepted_totalcounterlistenerAccepted client connections.
scalws_rejections_totalcounterlistener, reasonConnections 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_secondshistogramlistenerDuration of successful TLS handshakes.
scalws_h3_goaway_sent_totalcounterlistenerHTTP/3 GOAWAY frames sent while draining (ADR-0041).
scalws_h3_connection_closes_totalcounterlistener, howHTTP/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_secondsgaugeevent_loopHighest delay of a 100 ms timer on each event loop over the last second. Only with server.loop_lag_probe.

Upstreams

NameTypeLabelsMeaning
scalws_upstream_failures_totalcountertenant, app, reasonResponses synthesised because an upstream failed (502, 503, 504). reason, for example connect, timeout, protocol, unavailable.
scalws_upstream_connections_opened_totalcountertenant, app, protocolNew connections opened to upstreams.
scalws_upstream_connections_reused_totalcountertenant, app, protocolUpstream requests sent on a kept-alive connection.
scalws_upstream_retries_totalcountertenant, app, protocolUpstream requests sent again after a pooled connection turned out stale.
scalws_upstream_cancelled_totalcountertenant, app, protocolUpstream exchanges abandoned before the response was complete (client gone, response timeout).
scalws_upstream_wait_secondshistogramtenant, app, protocolTime until an upstream request is written (pool checkout, connect, FastCGI queue).
scalws_upstream_ttfb_secondshistogramtenant, app, protocolTime from writing an upstream request to its response head.
scalws_upstream_requests_totalcounterpool, member, classRequests 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_secondshistogrampoolTime to the response head of pool requests, retries included.
scalws_upstream_member_ejectedgaugepool, member1 while passive health keeps the member out.
scalws_upstream_pool_retries_totalcounterpoolRequests 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_membersgaugepoolMembers of the pool. Series of removed members and pools disappear; see load-balancing.md.
scalws_runtime_ready_workersgaugetenant, appManaged application workers that are ready.
scalws_runtime_restarts_totalcountertenant, appManaged application process restarts.

Security and quotas

NameTypeLabelsMeaning
scalws_security_blocked_totalcountertenant, app, reasonRequests 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_totalcountertenant, reasonRequests refused by tenant quotas. reason: rate (429) or concurrency (503).

Response cache

NameTypeLabelsMeaning
scalws_cache_lookups_totalcountertenant, app, resultResponse cache lookups. result: hit, stale, miss, bypass.
scalws_cache_entriesgaugenoneCached URLs (updated when /metrics is read).
scalws_cache_bytesgaugenoneMemory used by cached responses.
scalws_cache_disk_entriesgaugenoneResponses held by the disk tier of the cache.
scalws_cache_disk_bytesgaugenoneBytes 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).

NameTypeLabelsMeaning
scalws_cgroups_enabledgaugenone1 when cgroup v2 limits are enforced.
scalws_tenant_cpu_secondsgaugetenantCPU time used by the tenant’s application processes.
scalws_tenant_memory_bytesgaugetenantMemory used by the tenant’s application processes.
scalws_tenant_pidsgaugetenantProcesses and threads in the tenant’s cgroup.
scalws_tenant_oom_killsgaugetenantOOM kills in the tenant’s cgroup since it was created.
scalws_app_cpu_secondsgaugetenant, appCPU time used by the application’s processes.
scalws_app_memory_bytesgaugetenant, appMemory used by the application’s processes.
scalws_app_pidsgaugetenant, appProcesses and threads in the application’s cgroup.
scalws_app_network_bytesgaugetenant, app, directionNetwork bytes of the application’s processes since start. direction: ingress or egress, from the application’s point of view.
scalws_app_network_packetsgaugetenant, app, directionNetwork packets of the application’s processes since start.

TLS and ACME

NameTypeLabelsMeaning
scalws_tls_certificate_expiry_secondsgaugelistener, certificateExpiry (Unix time) of configured listener certificates; certificate is the first configured server name.
scalws_acme_certificate_expiry_secondsgaugetenant, appExpiry (Unix time) of ACME-managed certificates.
scalws_acme_orders_totalcounterresultACME certificate orders. result: success or failure.

Configuration, diagnostics and advisors

NameTypeLabelsMeaning
scalws_config_reloads_totalcounterresultConfiguration activation attempts. result: success or failure.
scalws_config_generationgaugenoneGeneration number of the active configuration.
scalws_config_last_success_timestamp_secondsgaugenoneUnix time of the last successful configuration activation.
scalws_diagnostic_findings_totalcountertenant, app, codeDiagnostic findings, counted when they appear (ADR-0015).
scalws_optimizer_decisions_totalcountertenant, app, rule, dispositionOptimizer decisions (ADR-0014). disposition: recommended, applied, rolled_back, kept, advisory.
scalws_ai_requests_totalcounterresultLocal AI advisor requests by result (ADR-0016).
scalws_ai_queue_depthgaugenonePending local AI advisor requests.
scalws_build_infogaugeversionAlways 1; version is the ScalWS version.

Per-domain counters

NameTypeLabelsMeaning
scalws_http_host_requests_totalcountertenant, app, host, statusHTTP requests by configured domain and status class.
scalws_http_host_response_bytes_totalcountertenant, app, hostResponse body bytes by configured domain.
scalws_http_host_request_bytes_totalcountertenant, app, hostRequest body bytes read by configured domain.
scalws_http_host_cache_totalcountertenant, app, host, resultResponse cache lookups by configured domain. result: hit (stale answers included) or miss.
scalws_http_host_requests_in_flightgaugetenant, app, hostRequests 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).

  • host is the configured domain pattern, not the request’s host: all hosts matched by *.example.com count under host="*.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 beyond server.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