Operate
Network logs
See every request your units answer, every name they look up and every connection they make, and search all of it. Nothing to install or configure; it works for every unit.
On this page
What Kuiper records#
Open a unit and choose Network. Three views show what is going in and out of it.
| View | A row is | It comes from |
|---|---|---|
| HTTP | A request to your unit's address: method, path, status, how long it took, who sent it | The edge proxy, which every request passes through |
| DNS | A name your unit's containers looked up, and what they were told | A small resolver Kuiper runs on the node |
| Connections | A connection to or from your unit's containers, with bytes each way and how long it lasted | The node's connection table |
There is nothing to switch on and nothing to add to your app. It works for every unit, on its Kuiper address and on your own domains. Only traffic to and from your units is recorded. The dashboard, the API and other people's units never appear.
At the top of the page a short summary says whether the unit is healthy: requests, the share that failed, how long they took, and a graph of the last hour.
Requests#
Each row shows the time, method, path, status, duration and the client's address. Click a row to see the rest: the query string, host, user agent, referer, response size, the deployment that answered, and the address the edge sent the request to.
When the edge cannot get an answer from your app, the row says why, for example dial tcp 10.0.0.2:30000: connect: connection refused. A 502 is easier to understand with that sentence next to it.
Request IDs#
Kuiper adds an X-Request-Id header to each request that does not already carry one, passes it to your app, and sends it back to the client in the response. If your app logs that header, you can go from a request to the app's own output:
- Open the request in the dashboard.
- Press App logs around this time. The link carries the request ID.
A request ID that the client sent is kept as it was. Send your own and you can find that exact request later with @rid:your-id.
Copy as curl#
The details panel has Copy as curl. It repeats the method and URL. Headers and bodies are not recorded, so add your own -H and -d. Query parameters that were redacted (see What is kept) are copied as REDACTED.
Search#
Every filter box on this page uses the same language as the rest of the dashboard.
| Write | It matches |
|---|---|
@status:503 | Rows whose status is 503. Text is compared ignoring case. |
@status:5xx | A class: 500 to 599. |
@status:400..499 | A range, ends included. |
@duration:>1s | A comparison: >, >=, <, <=. Times take ms and s. |
@status:502,503,504 | Any of several values. |
@path:/api/* | * matches anything. *.openai.com works too. |
-@method:OPTIONS | A leading - leaves rows out. It works on any term. |
timeout or "connection reset" | Words in the row: the path, host, client and request ID for HTTP; the name and answers for DNS; the peer and ports for connections. |
Terms are combined: @status:5xx @path:/api/* -@method:POST means all three. A key can be written in any case. If you write a key Kuiper does not know, or a number it cannot read, the box tells you what to write instead of showing nothing.
Start typing @ in the dashboard for the keys, and @status: for values to pick from. The chips under the box are common searches. Press / to jump to the box.
Keys#
| View | Keys |
|---|---|
| HTTP | status (also code), method, path, host, ip (the client), duration (also ms, latency), bytes (response size), ua, referer, rid (request ID), upstream, deployment, query, error, proto |
| DNS | name, type, rcode, answer, duration, client, upstream, transport, deployment |
| Connections | ip (the other end), dst, src, port (destination), proto, direction (egress or ingress), peer, bytes (both ways), duration, state (open or closed), kind, deployment |
Some searches to start from:
@status:5xx -@path:/healthzshows failures that are not the health check.@ip:203.0.113.9shows everything one client did.@rcode:NXDOMAINshows names that did not exist.@direction:egress @bytes:>1000000shows where your unit sends the most data.@peer:*.openai.comshows every connection to a provider.
Live and history#
New rows appear as they arrive while Live is on. If you have scrolled down, they wait behind a new rows button instead of moving the page. Scroll to the end of the table and older rows load. Paused stops the table moving so you can read it.
The summary#
The strip above the table answers whether the unit is healthy.
- Requests and Errors (5xx) for the period, and the latency at the 50th, 95th and 99th percentile.
- A graph of requests per minute, with the failed part in red, and a bar of responses by class.
- Busiest paths, clients and destinations. Numeric and UUID parts of a path are grouped, so
/users/17and/users/18are one row,/users/:id. Click any entry to search for it.
The health label follows simple rules. Failing is 10% or more of requests ending in a 5xx, from at least five requests. Degraded is 1% or more of at least twenty, or a 95th percentile of five seconds or more. No traffic means there were no requests in the period. Change the period at the top right, from 15 minutes to 7 days.
DNS lookups#
Containers of your unit are given Kuiper's resolver on their network as their first DNS server and the host's own resolvers as the second. A lookup that the container's built-in resolver cannot answer goes to Kuiper's resolver, which passes it on unchanged and writes down the name, the type, the response code (NOERROR, NXDOMAIN, SERVFAIL), the answers and how long the real resolver took.
The resolver can only make DNS better. If it is stopped, or the node's firewall will not let containers reach it, the container uses the host's resolvers as before and logging stays off for that network.
Containers pick this up when they start. Units already running keep the DNS they started with until their next deploy or restart.
Sandboxes and browsers that a unit starts use their own resolver settings and are not logged.
Connections#
A row is written when a connection ends, and again every minute for a connection that is still open, so a connection that has stopped moving bytes shows up with a growing age and a flat byte count. A later row for the same connection replaces the earlier one: the list shows each connection once, in its latest state.
- Direction is
egresswhen your unit opened the connection andingresswhen it accepted one. - Peer is the other end. It is the unit's name when that is another unit of the project, edge for Kuiper's proxy passing a request on, and otherwise the name your unit last looked up for that address (
api.openai.com), or the address. - Bytes are from your unit's side:
↑is what it sent and↓what it received.
Connections come from the node's connection table. A connection that was already open when the node's daemon started is timed from then. Volumes need the kernel to count bytes; the node setup does that, and the page says so when it is off.
From the CLI#
kuiper network http -u web
kuiper network http -u web -q '@status:5xx -@path:/healthz' --since 6h
kuiper network http -u web -q '@rid:client-req-0001'
kuiper network dns -u web -q '@rcode:NXDOMAIN'
kuiper network flows -u web -q '@direction:egress @bytes:>1000000'
kuiper network summary -u web --window 24hRows print oldest first, like kuiper logs. -u limits to a unit; without it the whole project is searched. --since takes 15m, 6h, 7d or a time, and --until ends the range. --limit N sets how many rows to start with (default 100). -f follows new rows, and with --json prints one JSON object per line. See the CLI reference.
What is kept, and what is not#
Recorded: the fields above. Not recorded: request and response bodies, cookies, and headers other than the user agent, the referer and the request ID.
Secrets are removed before a row leaves the machine it was written on:
Authorization,Cookieand similar headers are replaced by the edge and never recorded.- The value of a query parameter named
token,key,secret,password,code,sig,signature,auth,access_tokenorapi_key(in any capitalisation, along with a few close relatives such asrefresh_tokenand names ending in_token,_secretor_password) is replaced byREDACTED, in the query string and in the referer. The name stays, so you can see that it was sent. The control plane does this again when a row arrives. - A path is recorded as it was sent. Do not put secrets in paths.
Rows are visible to the people in your organisation and kept for a limited time: seven days for requests, three days for lookups and connections. A project that is very busy keeps its newest rows only: 500,000 requests, 300,000 lookups and 300,000 connections.
When nothing shows up#
An empty view says why. These are the usual reasons.
| The page says | What it means |
|---|---|
| No node has reported network logs yet | The node daemon is older than this feature. Update Kuiper and re-run the installer. |
| The edge has not logged a request yet | The edge is running and has not served a request since it started logging. If requests have been made and this stays, re-run the installer so the edge writes an access log. |
| The edge's log directory does not exist | The edge was started before the log directory was mounted. Re-run the installer. |
| Byte counts are off | The kernel is not counting bytes per connection. Run the node setup again, or sysctl -w net.netfilter.nf_conntrack_acct=1. |
| DNS is unavailable | The node could not set up the resolver for this project's network. The message says why. |
Requests that reach no running instance of a unit, for example while it is stopped, get Kuiper's 404 page and are not logged.
Turning it on in a self-hosted Kuiper#
On a hosted Kuiper there is nothing to do. On your own Droplet or server, update the checkout and run the installer again, as for any update:
git pull
sudo deploy/droplet/install.shThe installer rebuilds the control plane and kuiperd, and then:
- creates
/var/lib/kuiper/edge-logsand recreates thekuiper-caddycontainer once with that directory mounted at/var/log/kuiper-edge. The edge is unavailable for a few seconds. The routes it was given are kept in thekuiper-caddy-configvolume (the container runscaddy run --resume) and the certificates inkuiper-caddy-data, and the installer pushes the routes again straight after. - pulls the current
caddy:2when it recreates the container. The access log needs Caddy 2.8 or newer. If an older Caddy refuses it, the control plane pushes the routes without logging so they keep updating, and says so in its log. - on every node it sets up, sets
net.netfilter.nf_conntrack_acct=1(in/etc/sysctl.d/99-kuiper-network.conf) and loadsnf_conntrackat boot.kuiperdalso sets it when it starts.
Then redeploy or restart units so their containers use the resolver. Other nodes: update kuiperd and run deploy/node/setup-node.sh again.
An edge you run yourself, for example a regional one, needs the same mount on its Caddy container, -v /var/lib/kuiper/edge-logs:/var/log/kuiper-edge, on the node that runs kuiperd next to it. The control plane pushes the logging config to every edge it manages.
The resolver and the firewall#
The resolver listens on each project network's gateway address, port 53, so it never collides with systemd-resolved on 127.0.0.53. Containers reach it through the host's INPUT chain. Before it points a container at the resolver, kuiperd adds two rules, UDP and TCP port 53 from that network's subnet to its gateway, ahead of ufw's rules, and re-checks them every minute. If it cannot (no iptables, for example) it leaves the container's DNS alone. If you manage that firewall yourself, allow the same traffic and set KUIPER_DNS_FIREWALL=off.
Settings#
On the control plane:
| Variable | Default | Meaning |
|---|---|---|
KUIPER_EDGE_ACCESS_LOG | on | off leaves the edge without an access log. |
KUIPER_EDGE_ACCESS_LOG_FILE | /var/log/kuiper-edge/access.log | Where the edge's Caddy writes it (inside its container). |
KUIPER_NETWORK_HTTP_RETENTION_DAYS | 7 | Days to keep requests. |
KUIPER_NETWORK_DNS_RETENTION_DAYS | 3 | Days to keep lookups. |
KUIPER_NETWORK_FLOW_RETENTION_DAYS | 3 | Days to keep connections. |
KUIPER_NETWORK_HTTP_MAX_ROWS | 500000 | Most request rows one project keeps. |
KUIPER_NETWORK_DNS_MAX_ROWS | 300000 | Most lookup rows one project keeps. |
KUIPER_NETWORK_FLOW_MAX_ROWS | 300000 | Most connection rows one project keeps. |
On kuiperd:
| Variable | Default | Meaning |
|---|---|---|
KUIPER_NETWORK_LOGS | on | off switches all three off. |
KUIPER_EDGE_LOG_FILE | /var/lib/kuiper/edge-logs/access.log | The edge's log on this host. Empty switches HTTP logs off. |
KUIPER_DNS_LOGGING | on | off leaves Docker's default DNS untouched. |
KUIPER_DNS_FIREWALL | on | Let kuiperd open port 53 from a project network to its gateway. |
KUIPER_FLOW_LOGGING | on | Log connections. |
KUIPER_FLOW_INTERVAL | 10 | Seconds between readings of the connection table. |
A node that cannot reach the control plane keeps up to 20,000 rows of each kind and drops the oldest beyond that, then reports how many it dropped. Nothing else about the node is affected.