netstats

package
v0.7.21 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package netstats reads per-sandbox network byte counters from /proc/<pid>/net/dev. The procfs `net/` subtree is per-netns: opening it through a process PID joins that PID's network namespace, so a host-side reader sees the container's interfaces (eth0 etc.) and their cumulative rx/tx_bytes from the container's perspective.

rx/tx semantics here are the *container's*: rx_bytes = bytes the container received (= ingress to the sandbox = NetworkBytesIn); tx_bytes = bytes the container sent (= egress from the sandbox = NetworkBytesOut). This matches what an operator would intuitively bill against.

We deliberately ignore the host-side veth (the peer interface visible in the host netns). That path is correct too, but discovering it requires matching iflink/ifindex pairs — measurably more code than just reading /proc/<pid>/net/dev for a number that is already per-netns by construction.

The "lo" interface is skipped: pure intra-container loopback shouldn't count against an external network quota.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotRunning = errors.New("netstats: container process not present")

ErrNotRunning is returned when the procfs entry for the requested PID is gone — typically because the container exited between the caller obtaining the PID and the read. Callers should treat it as "no sample this tick" rather than a hard error, since reconcile/event paths will pick up the state change shortly.

Functions

This section is empty.

Types

type Counters

type Counters struct {
	BytesIn   int64 // container rx (ingress to sandbox)
	BytesOut  int64 // container tx (egress from sandbox)
	ActiveTCP bool  // at least one established TCP socket in the netns
}

Counters is one snapshot of per-container byte counters. Cumulative since container start — the poller computes deltas itself so it can survive container restarts without double-counting.

type PIDLookup

type PIDLookup interface {
	ContainerPID(ctx context.Context, containerRef string) (int, error)
}

PIDLookup resolves a sandbox container reference to the host PID of its init process. Implemented by *docker.Client.ContainerPID.

type Poller

type Poller struct {
	// contains filtered or unexported fields
}

Poller drives the netstats loop. Construct via NewPoller and call Start once; call Stop on shutdown to release the goroutine cleanly.

func NewPoller

func NewPoller(logger *slog.Logger, reader *Reader, lookup PIDLookup, lister SandboxLister, sink SampleSink, interval time.Duration) *Poller

NewPoller wires the dependencies together. interval <= 0 is rejected at Start; the caller controls the cadence so deployments with thousands of sandboxes can dial it down.

func (*Poller) Start

func (p *Poller) Start(ctx context.Context) error

Start launches the poll loop in a goroutine and returns immediately. The goroutine exits when ctx is cancelled. Calling Start more than once on the same Poller will spawn additional loops — don't do that.

type Reader

type Reader struct {
	// contains filtered or unexported fields
}

Reader reads /proc/<pid>/net/dev. The fs.FS indirection exists so tests can supply an in-memory fixture — the production caller passes nil to use the real /proc.

func NewReader

func NewReader() *Reader

NewReader builds a Reader against the host /proc.

func NewReaderFS

func NewReaderFS(fsys fs.FS) *Reader

NewReaderFS builds a Reader against an injected filesystem rooted such that "<pid>/net/dev" resolves correctly. Used by tests.

func (*Reader) Read

func (r *Reader) Read(pid int) (Counters, error)

Read returns the cumulative per-container byte counters for the given PID. PID 0 (Docker reports Pid:0 for a non-running container) yields ErrNotRunning so the poller can short-circuit without a procfs lookup.

type Sample

type Sample struct {
	SandboxID string
	BytesIn   int64
	BytesOut  int64
	ActiveTCP bool
	SampledAt time.Time
}

Sample is the per-sandbox delta the poller produces each tick. BytesIn/BytesOut are non-negative deltas since the last successful sample for the same (sandbox, PID) pair. The first observation of any new PID produces a Sample with zero deltas — the cumulative reading at that point is stored as the baseline so subsequent ticks compute meaningful diffs. This makes the poller restart-safe: container restart → new PID → fresh baseline → no spurious giant delta from the cumulative counter resetting to a small number. ActiveTCP is the current socket-state observation, not a delta, so the first sample can still carry "this sandbox has an established TCP connection" even while byte deltas are zero.

type SampleSink

type SampleSink interface {
	HandleSamples(ctx context.Context, samples []Sample)
}

SampleSink receives per-tick deltas. Implementations typically call into the store to add deltas to the cumulative counters and check quotas. Implementations MUST be safe to call from the poller goroutine.

type SandboxLister

type SandboxLister interface {
	NetstatsTargets(ctx context.Context) []Target
}

SandboxLister returns the sandboxes that should be polled this tick. Returning a fresh slice on each call is fine — the poller doesn't retain it. Items must include both the sandbox ID (for callbacks) and the container reference (for the PID lookup); when a container is not yet running the lister can omit it or include it with empty containerRef.

type Target

type Target struct {
	SandboxID    string
	ContainerRef string
}

Target is one row the poller will sample on the next tick.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL