Back to Articles

witr: The Process Genealogist That Answers "Why Is This Running?"

[ View on GitHub ]

witr: The Process Genealogist That Answers "Why Is This Running?"

Hook

You've SSH'd into a production server to find 47 Python processes consuming CPU, three nginx instances when you expected one, and a mystery port binding on 8080. Traditional tools tell you what's running—witr tells you why.

Context

The modern server is an archaeological site of execution contexts. A single application might be launched through systemd, wrapped in a Docker container, supervised by a process manager like PM2, spawned from a tmux session, or fork-bombed from a rogue cron job. Each layer adds metadata—environment variables, working directories, resource limits—but this context fragments across disparate APIs: /proc for process tables, systemctl for systemd units, docker ps for containers, lsof for ports.

Debuggers reach for a mental stack trace: "This nginx process → started by systemd unit nginx.service → enabled during last deploy by Ansible → configured to bind ports 80 and 443." But reconstructing this narrative requires querying five different tools and correlating PIDs, service names, and timestamps manually. witr emerged from this friction: a single binary that walks backward through execution ancestry, querying every relevant system API to answer the fundamental question plaguing every on-call engineer at 3 AM—why is this running?

Technical Insight

witr's architecture is deceptively simple: it's a recursive interrogator of process metadata with platform-specific adapters. Written in Go for cross-compilation and static linking, the tool starts with a process identifier (PID, port number, or process name) and builds a causal graph by traversing parent-child relationships while enriching each node with context from init systems, container runtimes, and shell environments.

The core insight is unifying fragmented data sources into a single query model. On Linux, witr reads /proc/[pid]/status for basic process info, /proc/[pid]/environ for environment variables, and /proc/[pid]/cmdline for execution arguments. But the magic happens in the correlation layer: it queries systemd's D-Bus API to map PIDs to service units, calls the Docker socket API to identify containerized processes, and parses shell history to detect interactive sessions. Here's the conceptual flow:

// Simplified architecture of witr's causal chain builder
type ProcessInfo struct {
    PID         int
    Name        string
    Parent      *ProcessInfo
    Context     ExecutionContext  // systemd, docker, shell, etc.
    StartReason string            // "Started by systemd unit X" or "Forked from shell session"
}

func TraceCausalChain(pid int) (*ProcessInfo, error) {
    proc := readProcFS(pid)
    
    // Check if process belongs to a systemd unit
    if unit := querySystemdUnit(pid); unit != nil {
        proc.Context = SystemdContext{Unit: unit}
        proc.StartReason = fmt.Sprintf("Managed by systemd unit %s", unit.Name)
    }
    
    // Check if process is containerized
    if container := queryDockerRuntime(pid); container != nil {
        proc.Context = DockerContext{Container: container}
        proc.StartReason = fmt.Sprintf("Running in Docker container %s", container.ID[:12])
    }
    
    // Recurse to parent process
    if proc.PPID != 0 {
        proc.Parent, _ = TraceCausalChain(proc.PPID)
    }
    
    return proc, nil
}

The platform adapter pattern handles OS-specific differences elegantly. On macOS, witr queries launchd instead of systemd using the Service Management framework. On Windows, it interrogates the Service Control Manager via Windows API calls. FreeBSD support leverages the process descriptor API (pdfork/pdkill) for more reliable parent tracking than traditional PPID lookups.

The TUI mode showcases Go's concurrent design strengths. Built on tview or bubbletea (common terminal UI frameworks), witr runs background goroutines to continuously refresh process state while rendering an interactive tree view. Users can expand/collapse process hierarchies, filter by context type ("show only Docker processes"), and drill into environment variables—all without blocking the UI thread. The single-binary distribution is possible because Go statically links all dependencies, including C bindings for platform APIs, producing a ~8MB executable with no runtime requirements.

Port binding analysis demonstrates witr's cross-API correlation. When you run witr port 8080, it:

  1. Calls netstat/ss to find the socket's owning PID
  2. Traces that PID's ancestry as described above
  3. Identifies whether the port was opened by the process directly or inherited from a parent (e.g., systemd socket activation)
  4. Displays the full chain: "Port 8080 → nginx (PID 1234) → systemd unit nginx.service → socket-activated by nginx.socket"

This level of detail is impossible with traditional tools because they each expose a single dimension of the problem space. witr's innovation is the integration layer that turns fragmented APIs into a unified narrative.

Gotcha

witr's causal tracing is only as complete as the system APIs it queries. On heavily customized systems—think embedded devices with BusyBox init, custom supervisor daemons like runit/s6, or Kubernetes nodes where processes are orchestrated through CRI-O or containerd without Docker—the tool may hit blind spots. The README mentions 12+ package managers, but GitHub issues reveal edge cases: processes started through nohup or disown that detach from terminal sessions can lose ancestry metadata, and nested containerization (Docker-in-Docker, Podman pods) sometimes produces incomplete chains because runtime APIs don't expose parent container relationships.

Performance on systems with thousands of processes is another constraint. Because witr queries multiple APIs per process and recursively walks parent chains, scanning a busy Kubernetes node with 2,000+ pods can take several seconds. The tool lacks a daemon mode or caching layer—every invocation is a cold start that re-queries /proc, systemd, and Docker from scratch. For continuous monitoring, you'd still reach for Prometheus exporters or eBPF-based tracers like bpftrace. witr shines in interactive debugging sessions, not production telemetry pipelines.

The cross-platform promise also comes with caveats. Windows support is functional but less polished—the tool can trace services and processes, but the TUI mode has rendering quirks in PowerShell versus Windows Terminal, and the causal chains are shallower because Windows doesn't expose as much parent-process metadata through standard APIs. If you're debugging IIS worker processes or COM+ services, expect to supplement witr with platform-specific tools like Process Explorer.

Verdict

Use if: You regularly debug mystery processes on Linux servers running mixed workloads (systemd services, Docker containers, manual shells), need to audit what's listening on ports and why, or want a fast interactive tool for understanding process relationships without piping ps/grep/awk chains. The single-binary distribution makes it perfect for sysadmin toolkits and incident response. Skip if: You need kernel-level tracing (syscalls, file I/O, network packets—use bpftrace or strace), work exclusively in Kubernetes where pod-level observability tools like kubectl and Lens provide better context, or require historical process tracking over time rather than point-in-time snapshots. For Windows-heavy environments, stick with Process Explorer or Process Monitor until witr's Windows adapter matures.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/pranshuparmar-witr.svg)](https://starlog.is/api/badge-click/developer-tools/pranshuparmar-witr)