Add ngx_http_ipng_stats_module: per-VIP, per-device traffic counters

Full implementation of the nginx dynamic module with:
- SO_BINDTODEVICE-based per-interface traffic attribution
- Per-worker lock-free counters flushed to shared memory
- Prometheus text and JSON scrape endpoint at configurable location
- UDP-only global logtail (ipng_stats_logtail) for fire-and-forget
  access log streaming
- $ipng_source_tag nginx variable for use in log_format/map
- Histogram buckets, EWMA rate gauges, zone meta-metrics
- Debian packaging (libnginx-mod-http-ipng-stats)
- Robot Framework end-to-end tests via containerlab
- SPDX Apache-2.0 headers on all source files
This commit is contained in:
2026-04-16 17:36:42 +02:00
parent c05bcf6aa6
commit 5a7e2f77f1
25 changed files with 4016 additions and 102 deletions

View File

@@ -0,0 +1,23 @@
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Client container entrypoint: installs curl, waits for containerlab
# to attach the data-plane veth, configures the IP, removes the mgmt
# default route so traffic to the server goes through eth1 (data-plane),
# and stays alive for docker-exec commands from the Robot test.
apt-get update -qq
apt-get install -y -qq curl iproute2 > /dev/null 2>&1
# Wait for containerlab to attach eth1.
echo "Waiting for eth1 ..."
while ! ip link show eth1 > /dev/null 2>&1; do
sleep 0.2
done
ip link set eth1 up
ip addr add ${MY_IP} dev eth1
# Remove the default route so packets to 10.0.x.0/24 go out eth1
# (the connected route) instead of through the mgmt bridge.
ip route del default 2>/dev/null || true
exec sleep infinity

View File

@@ -0,0 +1,54 @@
# SPDX-License-Identifier: Apache-2.0
# Containerlab topology for nginx-ipng-stats-plugin end-to-end tests.
#
# Three nodes:
# server — nginx with the module, a slow Python backend, two data-plane interfaces
# client1 — sends traffic via eth1 (attributed to source_tag=cl1)
# client2 — sends traffic via eth2 (attributed to source_tag=cl2)
#
# Links:
# server:eth1 ←→ client1:eth1 (10.0.1.0/24)
# server:eth2 ←→ client2:eth1 (10.0.2.0/24)
name: ipng-stats-test
mgmt:
network: ipng-stats-test-net
ipv4-subnet: 172.20.40.0/24
topology:
nodes:
server:
kind: linux
image: debian:trixie-slim
mgmt-ipv4: 172.20.40.2
binds:
- ../../../build:/opt/build:ro
- ./server/nginx.conf:/opt/config/nginx.conf:ro
- ./server/slow-backend.py:/opt/config/slow-backend.py:ro
- ./server/start.sh:/start.sh:ro
cmd: bash /start.sh
client1:
kind: linux
image: debian:trixie-slim
mgmt-ipv4: 172.20.40.11
binds:
- ./client/start.sh:/start.sh:ro
cmd: bash /start.sh
env:
MY_IP: 10.0.1.2/24
client2:
kind: linux
image: debian:trixie-slim
mgmt-ipv4: 172.20.40.12
binds:
- ./client/start.sh:/start.sh:ro
cmd: bash /start.sh
env:
MY_IP: 10.0.2.2/24
links:
- endpoints: ["server:eth1", "client1:eth1"]
- endpoints: ["server:eth2", "client2:eth1"]

View File

@@ -0,0 +1,58 @@
# SPDX-License-Identifier: Apache-2.0
# Test nginx configuration for the ipng_stats module.
load_module /usr/lib/nginx/modules/ngx_http_ipng_stats_module.so;
error_log stderr notice;
events {
worker_connections 128;
}
http {
ipng_stats_zone ipng:1m;
ipng_stats_flush_interval 500ms;
ipng_stats_default_source direct;
log_format tagged '$remote_addr src=$ipng_source_tag vip=$server_addr '
'"$request" $status $body_bytes_sent';
access_log /var/log/nginx/access.log tagged;
# Global logtail — fires for ALL requests regardless of server block.
log_format logtail '$host\t$remote_addr\t$ipng_source_tag\t$server_addr\t'
'$request_method\t$request_uri\t$status\t$body_bytes_sent\t'
'$request_time';
ipng_stats_logtail logtail udp://127.0.0.1:9514 buffer=4k flush=500ms;
server {
# Mgmt-only listener for direct traffic (tagged "direct").
listen 172.20.40.2:8080;
# Per-interface listeners for attributed traffic.
listen 10.0.1.1:8080 device=eth1 ipng_source_tag=cl1;
listen 10.0.2.1:8080 device=eth2 ipng_source_tag=cl2;
server_name _;
location / {
return 200 "ok $server_addr\n";
}
location /notfound {
return 404 "nope\n";
}
location /slow {
proxy_pass http://127.0.0.1:29080/;
}
}
server {
listen 172.20.40.2:9113;
location = /.well-known/ipng/statsz {
ipng_stats;
allow all;
}
}
}

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Minimal HTTP server that sleeps 50 ms before responding.
# Used by the test harness to produce measurable request durations.
import http.server
import socketserver
import time
class SlowHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
time.sleep(0.05)
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"slow\n")
def log_message(self, format, *args):
pass
with socketserver.TCPServer(("127.0.0.1", 29080), SlowHandler) as srv:
srv.serve_forever()

View File

@@ -0,0 +1,42 @@
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# Server container entrypoint: installs nginx + module, waits for
# containerlab to create data-plane interfaces, starts the slow
# Python backend, and runs nginx in the foreground.
# Suppress automatic service start/restart during apt/dpkg.
printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d
chmod +x /usr/sbin/policy-rc.d
apt-get update -qq
apt-get install -y -qq nginx python3 procps iproute2 ncat > /dev/null 2>&1
# Install the module .deb built by `make pkg-deb`.
dpkg -i /opt/build/libnginx-mod-http-ipng-stats_*.deb 2>/dev/null || true
# Re-enable module symlink in case postinst disabled it.
ln -sf /etc/nginx/modules-available/50-mod-http-ipng-stats.conf \
/etc/nginx/modules-enabled/50-mod-http-ipng-stats.conf
# Remove the policy block now that packages are installed.
rm -f /usr/sbin/policy-rc.d
# Wait for containerlab to attach the data-plane veth pairs.
for iface in eth1 eth2; do
echo "Waiting for $iface ..."
while ! ip link show "$iface" > /dev/null 2>&1; do
sleep 0.2
done
ip link set "$iface" up
done
ip addr add 10.0.1.1/24 dev eth1
ip addr add 10.0.2.1/24 dev eth2
# Slow backend: 50 ms sleep per request.
python3 /opt/config/slow-backend.py &
# UDP logtail listener — captures datagrams to a file for test validation.
ncat -u -l -k 127.0.0.1 9514 --recv-only >> /var/log/nginx/logtail-udp.log &
exec nginx -g 'daemon off;' -c /opt/config/nginx.conf