summaryrefslogtreecommitdiff
path: root/accel-pppd/extra/metrics.c
AgeCommit message (Collapse)Author
2026-05-14metrics: resolve protocol stat symbols via dlsymDenys Fedoryshchenko
The previous weak-symbol workaround let the module load on musl (which treats RTLD_LAZY as RTLD_NOW) but introduced a silent failure: weak undefined references are bound to NULL at our own dlopen time and are not updated when a later RTLD_GLOBAL dlopen brings the protocol module in. In any [modules] ordering where metrics comes before pppoe / l2tp / pptp / sstp / ipoe, the function pointers stay NULL and per-protocol session metrics silently disappear from both Prometheus and JSON output, with no log to indicate why. Replace the weak declarations with a small table and resolve each protocol's stat_starting/stat_active pair via dlsym(RTLD_DEFAULT, ...) the first time we render after the module is seen as loaded. dlsym walks the live global scope at call time, so it picks up symbols regardless of dlopen order; the resolved pointers are cached so subsequent scrapes do not re-walk the loader. The five near-identical render blocks in render_prometheus() and render_json() collapse into table-driven loops. libdl is already a transitive dependency of accel-pppd via triton, so no build-system changes are needed. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14metrics: queue partial response writesDenys Fedoryshchenko
write_all() previously did a blocking-style loop on a O_NONBLOCK socket and bailed on the first EAGAIN. With a slow scrape client or a small kernel send buffer that meant the response was truncated and the connection dropped mid-flight. Allocate one contiguous xmit_buf per response holding header + body, then drain it in xmit_flush(): * full write → mark the client for disconnect on the next event loop tick; * EAGAIN/EWOULDBLOCK → enable MD_MODE_WRITE so cln_write() resumes the drain when the socket becomes writable; * hard error → mark for disconnect, caller tears down. cln_read() now stops reading once a response is queued (read events during the response phase are uninteresting since we'll close on flush), and cln_write() finishes the drain and disconnects when the last byte is out. The existing per-client read timer doubles as a write deadline, so a peer that opens the connection and never reads still gets cleaned up after read_timeout seconds. Smoke-tested with a python client that uses SO_RCVBUF=256 and sleep(0.05) between recv()s — it now reads the entire ~2.8 KiB body across many short reads. Five concurrent slow readers plus a fast scrape all complete successfully. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
2026-05-14metrics: back off accept loop on persistent errorsDenys Fedoryshchenko
Previously the only path out of serv_read()'s accept loop was an EAGAIN/EWOULDBLOCK return; every other failure logged once and fell back into `continue`. With a level-readable listening fd, that means EMFILE/ENFILE/ENOBUFS/ENOMEM pin the worker thread spinning on accept() and saturate the log. Detect that class of error and pause the listener: disable MD_MODE_READ on serv_hnd, arm a one-shot triton timer for one second, and on expiry re-enable the handler. EINTR and ECONNABORTED are kept as transient retries — those are normal and short-lived. Verified by running the daemon under `prlimit --nofile=24` and opening enough slow connections to exhaust the limit. The first accept failure logs metrics: accept failed: Too many open files; backing off 1s then the daemon idles at 0% CPU instead of spinning; once fds free up it resumes accepting. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
2026-05-14metrics: per-client read timeout and max-clients capDenys Fedoryshchenko
A scrape client that opens a TCP connection and never sends a full request line+headers used to keep its accel-pppd-side fd registered indefinitely. Combined with the default `allowed_ips` (= allow all), a single peer could exhaust the daemon's file descriptors slowloris-style. Give every accepted connection a triton timer armed for `read_timeout` seconds (default 5). On expiry, disconnect_client() tears down the fd, the timer, and the buffer. The timer is canceled implicitly when the client is disconnected for any other reason because disconnect_client() now deletes the timer before freeing the client. Also cap the number of in-flight clients at `max_clients` (default 64). Excess connections are accepted and immediately closed so the kernel listen backlog still drains. Both knobs accept 0 to disable. The default values are documented in accel-ppp.conf(5) alongside the existing [metrics] options. Smoke-tested: * a connection that sends nothing is dropped from the daemon's fd table when read_timeout elapses; subsequent scrapes still succeed; * with max_clients=3 and five concurrent silent connections, the daemon holds exactly three ESTAB sockets, the others are closed. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
2026-05-14metrics: render JSON when format=jsonDenys Fedoryshchenko
Replace the previous `{}` stub with a structured JSON document carrying the same fields as the Prometheus exposition: build info, uptime, CPU%, RSS/virtual memory, the triton core counters, session counts by state, and a `protocols` object whose keys are only present for modules that are actually loaded. Strings are emitted through a small helper that escapes the JSON control characters (\b, \f, \n, \r, \t, \", \\) and falls back to \u00XX for other bytes below 0x20, so the version string and any future textual labels survive without producing invalid JSON. The output passes `python3 -m json.tool` against a running daemon. Content-Type is already set to application/json by content_type(), so no transport changes are needed. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
2026-05-14metrics: render Prometheus exposition for /metricsDenys Fedoryshchenko
Fill in the body that GET /metrics returns when format=prometheus. The exposed series mirror what `show stat` prints over the CLI today: * accel_ppp_build_info{version="..."} 1 * accel_ppp_uptime_seconds * accel_ppp_cpu_percent * accel_ppp_memory_{rss,virt}_bytes (read from /proc/<pid>/statm) * accel_ppp_core_mempool_{allocated,available}_bytes * accel_ppp_core_threads{,_active} * accel_ppp_core_contexts{,_sleeping,_pending} * accel_ppp_core_md_handlers{,_pending} * accel_ppp_core_timers{,_pending} * accel_ppp_sessions{state="starting|active|finishing"} * accel_ppp_protocol_sessions{protocol=...,state=...} for every protocol module that is currently loaded — pppoe, l2tp, pptp, sstp, ipoe — gated by triton_module_loaded() so we never call a stat helper from a module that wasn't loaded. A small growing strbuf helper coalesces the rendering into a single buffer that is passed to send_response() in one shot. Per-protocol forward declarations rely on the existing RTLD_LAZY|RTLD_GLOBAL load behaviour used by net-snmp, so the metrics .so does not need to link against pppoe.so, l2tp.so, etc. json format remains a stubbed `{}` body — the next commit replaces that with a real renderer. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
2026-05-14metrics: gate listener with allowed_ips ACLDenys Fedoryshchenko
Parse the `allowed_ips` option in [metrics] as a comma-separated list of IPv4 CIDR entries. Both the bracketed form allowed_ips = ["1.2.3.4/32", "5.6.7.0/24"] and a bare comma-separated form are accepted; surrounding whitespace, matched quotes, and the optional [ ] are stripped. A bare address without a prefix is treated as /32. When the list is empty (option missing or empty value), all peers are allowed and behavior is unchanged. Otherwise serv_read() rejects any peer that doesn't match a configured CIDR by closing the freshly accepted socket before allocating client state, so scanners get nothing more than a TCP reset. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
2026-05-14metrics: add HTTP listener with /metrics endpointDenys Fedoryshchenko
Stand up a minimal HTTP/1.1 server on top of triton's md handler, using the same pattern as cli/tcp.c. The listener: * binds the address configured in [metrics]/address (IPv4 only for now); * accepts non-blocking connections, reads up to the first "\r\n\r\n" into a fixed-size buffer (2 KiB), then dispatches one request and closes the connection; * routes GET /metrics to a placeholder 200 response (body is empty until the metrics rendering lands in a later commit); * returns 404 for other paths, 405 for non-GET, 413 if the request headers do not fit, and 400 for an unparseable request line. Content-Type is selected from the configured format (prometheus or json) so the next commits can plug in real bodies without touching the transport. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>
2026-05-13metrics: scaffold module with config parsingDenys Fedoryshchenko
Add a new extra module `metrics` that will eventually expose statistics over HTTP. This first commit only: * creates accel-pppd/extra/metrics.c with an init() that parses the [metrics] section options `format` (prometheus|json) and `address` (host:port); * wires the new shared library into the extras CMakeLists. No listener, no metrics rendering yet — those land in follow-up commits. With this commit alone, loading the `metrics` module just logs the configured listen address and format and is otherwise inert. Signed-off-by: Denys Fedoryshchenko <denys.f@collabora.com>