{"id":51002102,"url":"https://github.com/yeet-src/upstreamtop","last_synced_at":"2026-06-20T15:33:13.646Z","repository":{"id":361257618,"uuid":"1251960143","full_name":"yeet-src/upstreamtop","owner":"yeet-src","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-29T19:56:14.000Z","size":23,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-29T21:18:14.919Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yeet-src.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-28T04:08:28.000Z","updated_at":"2026-05-29T19:56:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yeet-src/upstreamtop","commit_stats":null,"previous_names":["yeet-src/upstreamtop"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/yeet-src/upstreamtop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fupstreamtop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fupstreamtop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fupstreamtop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fupstreamtop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yeet-src","download_url":"https://codeload.github.com/yeet-src/upstreamtop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fupstreamtop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34576043,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-20T15:33:10.268Z","updated_at":"2026-06-20T15:33:13.637Z","avatar_url":"https://github.com/yeet-src.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# upstreamtop — which backends nginx talks to, and how fast\n\nA live dashboard of the upstream endpoints an **nginx reverse proxy**\nis `proxy_pass`-ing to, ranked by **requests per second**, broken down\nper route, with per-backend status mix and average latency. Watch\nnginx fan requests across its backends in real time — spot a hot\nserver, a slow one, or one quietly returning 5xx.\n\n```\nupstreamtop — nginx upstreams  ·  248 req/s  ·  1s window\n\n BACKEND               RPS        SHARE       avg     STATUS\n 10.0.1.10:8080  ▁▂▄▆█  104  ████████   42%   2.1ms   2xx 99% 5xx 1%\n   /api/orders                                61/s\n   /healthz                                   30/s\n 10.0.1.11:8080  ▃▄▃▅▄   97  ███████    39%   2.4ms   2xx 100%\n   /api/orders                                97/s\n 10.0.1.12:8080  ▆▃▁▁▁   31  ██         13%   140ms   2xx 88% 5xx 12%\n   /checkout                                  31/s\n\n recent requests\n  14:31:02  GET     /api/orders                      → 10.0.1.10:8080\n  14:31:02  POST    /checkout                        → 10.0.1.12:8080\n```\n\n\u003e [!TIP]\n\u003e **No nginx config, no log parsing, no restart.** upstreamtop attaches\n\u003e two TC programs to the kernel's egress/ingress path and reads the\n\u003e plain-HTTP upstream traffic as it flows by. nginx never knows it's\n\u003e there, and it keeps working across `nginx -s reload`.\n\n## How it works\n\nnginx's upstream leg (`proxy_pass http://backend;`) is almost always\nplain HTTP on a private network, so we read it straight off the wire\nwith a pair of `tcx` programs — no nginx symbols, no userspace probes:\n\n- **`tcx/egress`** — a TCP segment that begins with an HTTP method\n  (`GET `, `POST `, …) is a request nginx is sending to a backend. The\n  destination IP:port *is* the upstream endpoint. It's counted in the\n  `stats` hash, keyed by `(backend, request path)`, and the 4-tuple is\n  stashed in an LRU map so the response can be matched later.\n- **`tcx/ingress`** — a segment that begins with `HTTP/` is a backend's\n  response. We parse the status code, look up the request by 4-tuple,\n  and attribute status + latency back to its `(backend, route)` bucket.\n\nDirection does the hard part for free: client→nginx requests arrive on\ningress (skipped by the `HTTP/` check) and nginx→client responses leave\non egress (skipped by the method check), so what's left on each hook is\nexactly the upstream conversation.\n\n`main.js` reads the `stats` hash once a second, diffs it against the\nprevious read to get per-interval RPS, and redraws the leaderboard.\n\nCounting **request lines** rather than connections is deliberate: nginx\nkeepalives reuse upstream connections, so a connection count would\nbadly undercount. Parsing requests gives true RPS.\n\n## Build\n\n```sh\nmake\n```\n\nDumps `vmlinux.h` from the running kernel's BTF (`bpftool btf dump`)\nand compiles `upstreamtop.bpf.c` against it plus the bpf headers shipped\nwith libbpf-sys (falling back to `/usr/include`). Requires `clang` and\n`bpftool`. CO-RE: the packet-header structs (`ethhdr`, `iphdr`,\n`tcphdr`), `struct __sk_buff`, and the `IPPROTO_*` enum come from the\nkernel's own types — no system `linux/*.h` packet headers needed.\n\n## Run\n\n```sh\nyeet run .\n```\n\nThen drive some traffic through nginx so it proxies to its backends.\n\nFlags (via `yeet run . -- --flag value`):\n\n- `--secs \u003cn\u003e` — how long to run (default `600`).\n- `--backends \u003cn\u003e` — max backends shown (default `8`).\n- `--routes \u003cn\u003e` — top routes listed per backend (default `4`).\n\n## Try it with the simulator\n\nNo production nginx handy? `simulate.py` stands up a real nginx reverse\nproxy in front of three local Python backends and drives weighted load\nat it — purely with the standard library (plus the `nginx` binary).\n\n```sh\npython3 simulate.py            # runs until Ctrl-C\npython3 simulate.py --workers 40 --duration 120\n```\n\nIt prints the exact command to run in a second terminal:\n\n```sh\ncd examples/upstreamtop \u0026\u0026 make\nsudo yeet run . -- --ignore 8080\n```\n\nThe backends have distinct profiles so the dashboard is worth looking\nat: `:9001`/`:9002` are a fast round-robin pool, `:9003` (`/checkout`)\nis slow (60–160ms) and returns 5xx ~12% of the time.\n\n\u003e [!NOTE]\n\u003e `--ignore 8080` hides nginx's own listen port. The simulator's load\n\u003e generator runs on the same host, so its client→nginx requests *also*\n\u003e leave on egress and nginx's front door would otherwise appear as a\n\u003e backend. On a real instance with remote clients you don't need it.\n\u003e This is also why all the simulated traffic rides `lo` — a good test\n\u003e of the loopback (bare-IP, no Ethernet header) parsing path.\n\n## Requirements \u0026 scope\n\n- **Kernel ≥ 6.6** for `tcx` attach (the modern TC hook), built with\n  `CONFIG_DEBUG_INFO_BTF=y` so `make` can dump `vmlinux.h` from\n  `/sys/kernel/btf/vmlinux`. Any kernel new enough for `tcx` ships it.\n- **Plain-HTTP upstream.** If nginx talks to backends over TLS\n  (`proxy_pass https://…`), the bytes on the wire are encrypted and\n  this won't see them — switch to a `uprobe` on `SSL_write` and recover\n  the backend via `SSL_get_fd` → `getpeername`.\n- **IPv4 only**, and only the **request/status line in a full first\n  segment** (payloads under 64 bytes are skipped). Real HTTP requests\n  carry headers, so the first segment is comfortably past that.\n- **One request ↔ one response per connection at a time.** Serial\n  keepalive is matched correctly; HTTP/1.1 pipelining (rare) is not.\n- **HTTP/2 / gRPC upstream** is framed, not line-based — decode the h2\n  frames to pull `:path` and `:status`.\n- For `127.0.0.1` backends the loopback path is handled (bare-IP frames\n  with no Ethernet header are detected), but a dedicated reverse-proxy\n  box talking to remote backends is the intended setting.\n\nTo watch only real backends on a busy box, an `LPM_TRIE` of upstream\nprefixes makes an easy filter to bolt onto the egress program.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fupstreamtop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyeet-src%2Fupstreamtop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fupstreamtop/lists"}