{"id":51423886,"url":"https://github.com/yeet-src/container-traffic","last_synced_at":"2026-07-05T01:30:30.758Z","repository":{"id":368693251,"uuid":"1283284344","full_name":"yeet-src/container-traffic","owner":"yeet-src","description":"Live per-container HTTP dashboard read from the kernel with eBPF. Rate, errors, latency. No sidecar, no app changes.","archived":false,"fork":false,"pushed_at":"2026-07-01T19:17:30.000Z","size":3223,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-01T20:23:18.054Z","etag":null,"topics":["cgroups","containers","docker","ebpf","http","http-monitoring","kernel","kprobes","linux","observability","red-method","tui","uprobes","yeet"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-28T18:49:54.000Z","updated_at":"2026-07-01T19:59:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yeet-src/container-traffic","commit_stats":null,"previous_names":["yeet-src/containertraffic"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/yeet-src/container-traffic","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fcontainer-traffic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fcontainer-traffic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fcontainer-traffic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fcontainer-traffic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yeet-src","download_url":"https://codeload.github.com/yeet-src/container-traffic/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yeet-src%2Fcontainer-traffic/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35141083,"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-07-04T02:00:05.987Z","response_time":113,"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":["cgroups","containers","docker","ebpf","http","http-monitoring","kernel","kprobes","linux","observability","red-method","tui","uprobes","yeet"],"created_at":"2026-07-05T01:30:27.054Z","updated_at":"2026-07-05T01:30:30.721Z","avatar_url":"https://github.com/yeet-src.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `container-traffic`\n\n\u003e **htop for your containers' HTTP.** Every request each container serves, ranked by what's actually wrong with it.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/platform-Linux-1793D1\" alt=\"Linux\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/built%20with-yeet%20%2B%20eBPF-8A2BE2\" alt=\"yeet + eBPF\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-GPL--2.0-3DA639\" alt=\"GPL-2.0\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/category-HTTP-005A9C\" alt=\"HTTP\"\u003e\n  \u003ca href=\"https://discord.gg/dYZu9PjKB\"\u003e\u003cimg src=\"https://img.shields.io/badge/chat-Discord-5865F2\" alt=\"Discord\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n![container-traffic dashboard: four containers ranked by HTTP request rate, with per-container RED metrics and a live detail pane](assets/container-traffic.gif)\n\n**`container-traffic` is a live, zero-config dashboard that shows every container's HTTP traffic, attributed to the container by cgroup and read straight from the kernel with eBPF.**\n\n\u003e [!TIP]\n\u003e No sidecar, no proxy, no app changes. It reads HTTP at the socket layer and tags each request with the kernel's cgroup id, so the per-container split comes from the kernel rather than from a service mesh you had to install first.\n\n## Quick start\n\n```sh\ncurl -fsSL https://yeet.cx | sh\nyeet run github:yeet-src/container-traffic\n```\n\u003csub\u003e[Manual install guide](https://yeet.cx/docs/install/manual-installation) | Linux only\u003c/sub\u003e\n\nNavigation is two regions, driven by the arrow keys (or `hjkl`). On the left nav rail, `↑` / `↓` switch views (Containers, Routes, Notable, Report) and `→` steps into the list; in the list, `↑` / `↓` move the cursor and `←` returns to the rail. The detail pane on the right follows the cursor; on the Containers view it also tails the selected container's logs automatically. `+` / `-` tunes the slow-request threshold. `q` quits.\n\n## A 60-second primer on watching container HTTP\n\nA container is just a process in its own cgroup. Its HTTP requests leave through the same kernel socket calls as any other process; the only thing that ties a request to \"the checkout container\" is which cgroup the calling task belongs to. `container-traffic` reads both: the HTTP off the socket, and the cgroup off the task.\n\n| Term | What it means here |\n|---|---|\n| **cgroup** | The kernel's grouping for a container's processes. On cgroup v2 a Docker container's leaf is named for its 64-char container id, which is how a request gets attributed to a named container. |\n| **kprobe** | A kernel hook on a function. `container-traffic` hooks `tcp_sendmsg` (the request) and `tcp_recvmsg` (the response) to read HTTP as the kernel moves it. |\n| **uprobe** | A hook on a *userspace* function. `container-traffic` hooks `SSL_write` / `SSL_read` in the host TLS library to read HTTPS as plaintext, before encryption and after decryption. |\n| **RED** | Rate, Errors, Duration. The three signals a request-driven service is judged by, framed here as the three ways a service gets sick: **broken** (errors), **overloaded** (rate), **slow** (tail latency). |\n| **route pattern** | A path with its variable parts collapsed: `/users/1839` and `/users/204` both become `/users/{id}`. How requests get grouped without the cardinality exploding. |\n\nThe trick that makes `container-traffic` cheap: it never sits in the request path. It watches the kernel's socket and TLS calls, so the container serves traffic exactly as it would if the tool were not running.\n\n## Common use cases\n\nBackend developers debugging which container is misbehaving under load, and SREs watching a host full of services without standing up a mesh to do it.\n\nWhere you'd normally reach for a service mesh's dashboard or a per-container `docker logs` tail plus a metrics scrape, `container-traffic` reads the HTTP and the attribution straight from the kernel, so you get per-container rate, errors, and latency with nothing injected into the containers.\n\n- A deploy went out and error rates moved. Which container is returning the 5xxs?\n- One host runs a dozen services. Which one is slow right now, and on which route?\n- p99 climbed but the average looks fine. Which endpoint is dragging the tail?\n- A container is throwing 500s. What is it actually printing to its logs as it fails?\n\n## What you're looking at\n\nThe screen is a three-column layout: a vertical tab rail on the left, the active view in the middle, and a detail pane on the right that tracks whatever row you have selected.\n\n- **Status strip (top).** The live headline across all containers: total request rate (overloaded), error count and percentage (broken), and the encrypted-versus-plaintext split.\n- **Containers tab.** The headline census, one row per container, ranked by request rate. Each row carries req/s, an error rate, and p99 latency. Selecting a container fills the detail pane with its full RED breakdown (p50/p95/p99, method mix, status mix, bytes), its top routes, and a live tail of its logs. This is the \"click in to see what this service is doing\" view.\n- **Routes tab.** Every route pattern across all containers, ranked by volume, with error rate and p99. The detail pane shows which containers serve the selected route. This is the cross-cutting lens: which endpoint is hot or failing, regardless of who owns it.\n- **Notable tab.** A triage queue, not a firehose. Only requests worth a look land here: errors and anything slower than the threshold. New rows arrive at the top but the view holds still on your selection so you can read it, and the detail pane shows the selected request in full. `+` / `-` raises or lowers the slow threshold; the header says how many ordinary requests were elided, so the tab is honest about what it is hiding.\n- **Report tab.** The opinionated summary: a ranked list of findings tagged **broken**, **overloaded**, or **slow**, worst first, with the selected finding's evidence in the detail pane. This is the part that reads the data for you instead of making you read it.\n\n## How it works\n\nThree layers: a single BPF object, a BPF-aware data layer in JS, and pure UI that reads signals.\n\n**The BPF side.** One object, `bin/probe.bpf.o`, linked from `src/bpf/container-traffic.bpf.c`, with six programs feeding one ring buffer. Each event carries the cgroup id and name, the method, path, status, latency, byte counts, and a source tag (wire or TLS).\n\n| Program | Hook | What it captures |\n|---|---|---|\n| `on_sendmsg` | `kprobe/tcp_sendmsg` | The outgoing request. Parses the HTTP request line (method, path) and stashes it by socket. |\n| `on_recvmsg` / `on_recvmsg_ret` | `kprobe` + `kretprobe` on `tcp_recvmsg` | The response. Reads the reply buffer on return, lifts the status code, pairs it with the stashed request for latency. |\n| `on_ssl_write` | `uprobe/SSL_write` | The request inside TLS, read as plaintext before encryption. |\n| `on_ssl_read` | `uprobe/SSL_read` | The response inside TLS, read as plaintext after decryption; lifts the status code. |\n\nAttribution is read in-kernel: each request site records the task's leaf cgroup name, which on cgroup v2 contains the container id. A slow-request floor lives in the program's `.bss` as a live knob; userspace patches it so the kernel does the filtering.\n\n**The JS side.** One ring-buffer subscription, rolled into the views.\n\n- `src/probes/probe.js` loads the object and attaches the programs once.\n- `src/probes/container-traffic.js` subscribes to the ring buffer and aggregates every request into RED metrics per container and per route, with latency tracked as percentiles from a recent-sample reservoir.\n- `src/probes/logs.js` streams a selected container's stdout/stderr (this is the only piece that is not eBPF; it reads the daemon's container-logs API).\n- `src/lib/containers.js` resolves a cgroup name to a container name by matching the embedded id against the running container list.\n- `src/lib/report.js` is `analyze()`, the broken/overloaded/slow heuristics behind the Report tab.\n- `src/components/*.jsx` are pure UI reading signals; `src/lib/{format,theme,layout}.js` are pure helpers.\n\n## Requirements\n\n\u003e [!IMPORTANT]\n\u003e A Linux kernel built with BTF (`CONFIG_DEBUG_INFO_BTF=y`), which is the default on recent Ubuntu, Debian, Fedora, and Amazon Linux. eBPF needs root; the yeet daemon handles the privileged load.\n\nThe yeet daemon, which handles the privileged BPF load. `curl -fsSL https://yeet.cx | sh` installs it.\n\nContainer attribution to a *named* container reads the running container list from the daemon's container API, so names resolve for Docker containers; traffic from anything else is bucketed honestly as `host`. (extrapolated, grounding 2 — review: tested against Docker; other runtimes that use the same cgroup-id-in-leaf convention should resolve too but were not verified.)\n\n## Honest caveats\n\n\u003e [!NOTE]\n\u003e What `container-traffic` does not do, and what it gets wrong.\n\n- **HTTP/1.x only.** HTTP/2's binary HPACK framing is not decoded; that traffic shows nothing rather than garbage. (Many clients negotiate h2 by default over TLS; forcing HTTP/1.1 makes the traffic visible.)\n- **Containerized TLS is not captured.** The `SSL_write` / `SSL_read` uprobes attach to the *host's* `libssl`. A container that ships its own `libssl` inside its image is a different library the host-side uprobe cannot hook, so encrypted traffic resolves for host processes on the system `libssl`, not for in-container HTTPS. The status strip marks this: `enc(host)` when the probe is attached, `enc(off)` if it could not attach. Plaintext is attributed for containers regardless. (extrapolated, grounding 2 — review: this is a current limit of the uprobe binding, which targets a library by name and has no per-container or per-pid attach.)\n- **Statically-linked TLS has no `libssl` to hook.** Go binaries and other static-TLS builds will not show their HTTPS on the TLS path.\n- **Logs need a real container.** The log tail reads the container-logs API, so it works for Docker containers; the `host` bucket and non-container cgroups have nothing to show.\n- **Latency on a local round trip reads `\u003c1ms`.** Percentiles come alive on real network paths; loopback traffic will mostly show sub-millisecond durations.\n\n## Community questions\n\n**Do I need a service mesh or a sidecar to get per-container HTTP?**\nNo. The per-container split is read from the kernel's cgroup id on each request. There is nothing to inject into the containers and nothing in the request path.\n\n**Will running this slow down my containers?**\nIt is not in the request path. It reads the kernel's existing socket and TLS calls, so a container serves traffic the same whether the tool is running or not. (extrapolated, grounding 3 — review: overhead is the standard kprobe/uprobe-plus-ringbuf cost; no benchmark numbers are claimed here.)\n\n**Why is the encrypted column zero for my containers?**\nThe TLS uprobe attaches to the host `libssl`, and most containers ship their own. In-container HTTPS is not captured in this version; the status strip shows `enc(host)` to make the scope explicit. Plaintext HTTP is still attributed per container.\n\n**Is it safe to run on a shared host?**\nIt reads only HTTP metadata and container logs from the kernel and the daemon; it does not modify traffic or inject into containers. (extrapolated, grounding 3 — review: a security/appropriateness claim worth an engineer's confirmation before publishing.)\n\n**How is this different from `docker stats` or a metrics dashboard?**\n`docker stats` shows CPU, memory, and network bytes, not HTTP. A metrics dashboard shows what the service was instrumented to emit. `container-traffic` shows the actual requests, methods, paths, status codes, and latency, with no instrumentation, read from the kernel.\n\n## Building from source\n\n```sh\nmake          # clang + bpftool build the BPF object, esbuild bundles the JS\nmake clean\n```\n\nThe toolchain (clang, bpftool, esbuild) is vendored by the yeet build setup; on macOS the BPF build runs in a Linux VM. The compiled object (`bin/probe.bpf.o`), the bundled `src/index.jsx`, and the generated `vmlinux.h` are gitignored and rebuilt by `make`.\n\n## License\n\nGPL-2.0\n\n---\n\nBuilt with [yeet](https://yeet.cx/docs/?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=container-traffic), a JS runtime for writing eBPF programs on Linux machines. Join us on [discord](https://discord.gg/dYZu9PjKB?utm_source=github\u0026utm_medium=readme\u0026utm_campaign=container-traffic).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fcontainer-traffic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyeet-src%2Fcontainer-traffic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyeet-src%2Fcontainer-traffic/lists"}