XTQ/Web/DocsDownload

Operations

Commands, signals and reloads, logs, the dashboard, stats and Prometheus metrics, and the headers xTQ Web adds.

Commands

Command Does
xtq-web Runs the server, configured by XTQW_CONFIG
xtq-web check [file] Validates a config file and exits
xtq-web --version Prints the version, like xtq-web 1.0.1. Also -V or version
xtq-web --help Prints the usage. Also -h or help

Any other argument is an error: unknown argument "x", see xtq-web --help.

xtq-web check

xtq-web check /etc/xtq-web/xtq-web.conf

Loads the file exactly as a start or a reload would, without opening any ports, and reports what it found:

/etc/xtq-web/xtq-web.conf: OK
  listeners: 0.0.0.0:80,0.0.0.0:443(tls)
  sites:     example.com, api.example.com, *
  proxies:   3 routes, 5 backends
  php:       0

proxies counts proxy handlers and the backends they list. php counts php handlers.

Without a file argument it checks the file in XTQW_CONFIG. With neither, it fails with usage: xtq-web check <config file>, or set XTQW_CONFIG.

On a mistake it prints the error, with the file and line, and exits with a non-zero status:

Error: /etc/xtq-web/xtq-web.conf:9: proxy takes lb, health, health_interval, health_timeout, fail_duration, timeout, host, header_up, limit and max_body, not "retries"

The check also verifies things outside the file that a load needs, such as php docroots existing. Such errors name the line and then the cause:

Error: /etc/xtq-web/xtq-web.conf:5

Caused by:
    0: docroot /var/www/blog
    1: No such file or directory (os error 2)

IP list files that can't be read produce a warning, not a failure, just as at runtime. It does not need root, but it reads the files with your permissions, which may differ from the service user's.

Starting

At startup xTQ Web prints one line that sums up what it is doing:

xtq-web: config=/etc/xtq-web/xtq-web.conf listeners=0.0.0.0:80,0.0.0.0:443(tls) sites=example.com,* backends=2 cache=true max_conns=16384 max_handshakes=8 nofile=1048576

config=environment means XTQW_CONFIG was not set and xTQ Web is running in environment-only mode.

If a listen address is taken, xTQ Web does not fail. It retries every 100 ms and logs once:

xtq-web: can't listen on 0.0.0.0:80 yet (Address already in use (os error 98)), retrying every 100 ms
xtq-web: now listening on 0.0.0.0:80

That lets you start xTQ Web while the server it replaces is still running. Stop the old one and xTQ Web picks the port up within 100 ms. Other listeners work in the meantime.

Signals and reloads

Signal Effect
SIGHUP Reloads the config
SIGTERM, SIGINT (Ctrl-C) Stops, after letting open requests finish for up to XTQW_DRAIN_SECS seconds

Reload

sudo systemctl reload xtq-web

Under the packaged service, reload runs xtq-web check first and sends SIGHUP only if the config passes. If the check fails, systemctl reload reports the failure and the running server is not touched.

Outside systemd, send the signal yourself:

sudo kill -HUP "$(pidof xtq-web)"
docker kill -s HUP xtq-web

On SIGHUP xTQ Web reads the config file again, and the files it names. If the new config loads, it logs:

xtq-web: reloaded /etc/xtq-web/xtq-web.conf: listeners=0.0.0.0:80,0.0.0.0:443(tls) sites=example.com,*

and from then on:

  • New requests use the new sites and routes. Requests already in flight finish on the old ones.
  • New listen addresses are opened. Listeners that are gone stop accepting (stopped listening on ...). Listeners that stay are not touched, so nobody is disconnected.
  • Proxy pools start fresh: health checks run again at once, and passive failure marks, idle backend connections and in-flight counts start from zero.
  • Certificate management restarts, so missing certificates are tried again at once.
  • The page and file cache, issued certificates and open connections are kept.
  • Without XTQW_SECRET, browser check passes stop being valid.

If the new config has a mistake, it is refused and the running config keeps serving:

xtq-web: reload refused, still serving the previous config: /etc/xtq-web/xtq-web.conf:14: unknown directive "proxi"

Environment variables are not read again on reload. After changing one, restart.

Windows has no SIGHUP. There, restart to apply a change.

Stop

SIGTERM (what systemctl stop and docker stop send) and SIGINT stop the process gracefully:

  • xTQ Web stops accepting new connections at once.
  • Each open connection finishes the request it is working on. Keep-alive connections are closed after that request instead of waiting for another.
  • It waits until every connection is done, or XTQW_DRAIN_SECS seconds have passed (default 10), and exits.

While connections are open, it logs:

xtq-web: stopping, letting 12 open connection(s) finish (up to 10s)

With no open connections it exits at once. XTQW_DRAIN_SECS=0 exits at once in any case. WebSockets that were already handed off to their backend are not waited for, and are closed when the process exits. A long download that takes more than the drain time is cut.

The packaged unit gives the process 20 seconds to stop (TimeoutStopSec=20), and the Docker installer passes --stop-timeout 20, so the default drain fits. If you raise XTQW_DRAIN_SECS past that, raise those too, or systemd and Docker kill the process before it is done.

A restart therefore lets requests in flight finish, but new connections are refused from the moment the old process stops listening until the new one listens. For config changes, reload is still better: nothing is refused at all.

Logs

xTQ Web writes to standard error. Under systemd that ends up in the journal, in Docker in the container log:

journalctl -u xtq-web -f
docker logs -f xtq-web

xTQ Web does not write an access log. It logs events:

Message Meaning
xtq-web: config=... listeners=... sites=... Startup summary
xtq-web: no certificate configured, using a self-signed one for [...] No XTQW_CERT and XTQW_KEY, see HTTPS
xtq-web: reloaded <file>: ... A reload succeeded
xtq-web: reload refused, still serving the previous config: <error> A reload failed, nothing changed
xtq-web: can't listen on <addr> yet (<error>), retrying every 100 ms A listen address is not available yet
xtq-web: now listening on <addr> It became available
xtq-web: stopped listening on <addr> A reload removed that listener
xtq-web: stopping, letting <n> open connection(s) finish (up to <s>s) SIGTERM or SIGINT arrived while connections were open, see Stop
xtq-web: <pool>: <backend> failed (<error>), out of rotation for <n>s Passive health check, see Reverse proxy
xtq-web: <pool>: <backend> failed its health check Active health check failed
xtq-web: <pool>: <backend> passes its health check again And recovered
xtq-web: <pool>: under heavy load, browser check on (<n>% of its in-flight cap) challenge switched on
xtq-web: <pool>: load back to normal, browser check off (<n>% of its in-flight cap) And off again
xtq-web: WARNING: ip list ...; it matches no one until it can be read An @file could not be read at load
xtq-web: <file>: can't resolve <hostname>: <error> A hostname in an IP list did not resolve
xtq-web: acme: managing certificates for [...] Automatic HTTPS is on, for these names
xtq-web: acme: certificate issued for <host> A certificate was issued or renewed
xtq-web: acme: <host>: <error> Issuing failed, retried at the next check
accept: <error> The operating system refused to hand over a new connection, often for lack of file descriptors

Dashboard, stats and metrics

Three paths show what xTQ Web is doing:

Path Content
/.xtq/dashboard A live page, updated every 2 seconds
/.xtq/stats.json The same numbers as JSON
/.xtq/metrics Prometheus text format

They are off until you set XTQW_ADMIN_TOKEN to a secret of at least 16 characters. A shorter token leaves them off. Use letters and digits only, for example:

openssl rand -hex 32

Put it in /etc/xtq-web/env and restart:

XTQW_ADMIN_TOKEN=3f9c...

Pass the token as a token query parameter or in an X-Xtq-Token header:

https://example.com/.xtq/dashboard?token=3f9c...
curl -H "X-Xtq-Token: $TOKEN" https://example.com/.xtq/stats.json
curl "https://example.com/.xtq/metrics?token=$TOKEN"

Without the token, or with a wrong one, these paths answer 404 like any unknown path. They work on every listener and every hostname, before any site is chosen, so you can also reach them on 127.0.0.1. Use HTTPS when you open them from outside, since the token travels in the URL or a header.

The dashboard shows requests per second, cache hit rate, browser checks, busy answers, connections, TLS handshakes, backend errors, proxied requests and open WebSockets. Below that come tables of PHP sites, proxy backends with their state and requests in flight, and issued certificates with the days they have left.

stats.json

Field Meaning
time Unix time of the snapshot
requests Requests received since start
cache.hit, cache.stale, cache.miss PHP page cache results
static_files Files up to 8 MB served by files and PHP docroots
browser_checks.shown, browser_checks.passed Browser check pages shown and solved
busy_answers.edge_full 503 Server busy answers from pool limits, the WebSocket cap and the PHP waiting room
busy_answers.fair_share 503 answers from the PHP per-address limit
upstream_errors 502 and 504 answers, and PHP errors
proxied Requests handed to proxy handlers, WebSockets included
probes Scanner probes answered
out_of_ports Backend connections refused by this machine for lack of ports or descriptors
websockets WebSockets open now
connections.open, connections.max Client connections open now, and the cap
handshakes.active, handshakes.max TLS handshakes running now, and the cap
sites[] Per php handler: name, php_up, busy, workers, queue, queue_cap, waiting_at_edge, overloaded, browser_check
upstreams[] Per proxy backend: route (the pool name), server, up, in_flight
certificates[] Per issued certificate: host, days_left

Counters count since the process started. A reload does not reset them.

Prometheus

Metric Labels
xtq_requests_total
xtq_cache_total result = hit, stale, miss
xtq_static_files_total
xtq_browser_checks_shown_total
xtq_browser_checks_passed_total
xtq_busy_answers_total reason = edge_full, fair_share
xtq_upstream_errors_total
xtq_proxied_total
xtq_probes_total
xtq_out_of_ports_total
xtq_connections_open
xtq_handshakes_active
xtq_websockets_open
xtq_upstream_up route, server. 1 when usable
xtq_upstream_in_flight route, server
xtq_php_busy, xtq_php_workers, xtq_php_queue, xtq_edge_waiting, xtq_overloaded site
xtq_certificate_days_left host

A scrape job with the token as a parameter:

scrape_configs:
  - job_name: xtq-web
    metrics_path: /.xtq/metrics
    params:
      token: ["3f9c..."]
    static_configs:
      - targets: ["127.0.0.1:80"]

Server and X-Powered-By

Every response gets Server: xtq-web and X-Powered-By: xTQ, replacing whatever the app sent. That also hides version banners like Next.js or PHP/8.3. A header line in a route can't change these two, because they are set last.

Variable Default off
XTQW_SERVER_HEADER xtq-web Leaves the app's own Server header as it is, or none
XTQW_POWERED_BY xTQ Leaves the app's own X-Powered-By as it is, or none

Any other value is sent as the header. With off, header lines can set or remove these headers like any other.

The X-Xtq-Cache header

Some responses say where they came from:

Value Meaning
HIT From memory. A cached page or file
STALE A PHP page past its fresh time, while a new copy renders
MISS Rendered or read now, and stored
BYPASS Rendered or read now, not stored
CHALLENGE The browser check page
FAIR-SHARE Refused by the PHP per-address limit
EDGE-FULL Refused by the PHP waiting room

Reserved paths

Path Belongs to
/.well-known/acme-challenge/<token> Answered by xTQ Web while that token is pending with Let's Encrypt. Other tokens go to your site as usual
/.xtq/ and everything under it xTQ Web on every site: the dashboard paths, /.xtq/verify for the browser check, 404 for the rest