Skip to content

Latest commit

 

History

172 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Compass

Compass

A toolkit for pointing developers in the right direction for performance issues.

Compass shows you where a request spent its time — function by function — for PHP and Node.js applications, without a profiler in the request path. Instrumentation is emitted as USDT probes, collected out of process with eBPF, and streamed to a terminal UI.

Compass CLI

How it works

graph LR
    subgraph Application container
        PHP[php-fpm + compass.so]
        Node[node + compass.node]
    end

    subgraph Sidecar container
        Sidecar[compass-sidecar]
    end

    PHP -- USDT probes --> Sidecar
    Node -- USDT probes --> Sidecar
    Sidecar -- HTTP stream --> CLI[compass]
Loading
Component Lives in Role
PHP extension (compass.so) skpr/compass-extension Emits USDT probes from PHP requests and CLI commands.
Node addon (compass.node) nickschuch/compass-node Emits USDT probes from Node HTTP requests.
compass-sidecar this repository Attaches eBPF programs to those probes, assembles traces and streams them over HTTP.
compass this repository Terminal UI which connects to a sidecar and displays traces.

The extension and addon are only probe emitters — they do nothing measurable until the sidecar attaches to them, which it only does while a CLI is connected.

Quickstart

The compose stack runs Drupal, a Node app, the sidecar and the CLI together:

# Build and start the stack.
docker compose up -d --build

# Generate some traffic.
curl http://localhost:8080
curl http://localhost:8080/frontend

# Watch traces.
docker compose exec compass compass

The interface has two levels. Search and Logs are the top one, and / moves between them. Pressing enter on a trace in Search opens it, which swaps the tabs for that trace's own pages — Functions and Drupal Cacheable Metadata — that the same / then moves between. esc closes the trace and returns to the main menu. The filled tab is the one you are on.

An open trace is described by a block of named fields above its pages: what was requested, its id, how long it took, how much memory it used, and what Drupal made of its cacheability. The block lays itself out in as many columns as the terminal is wide enough for, with the values nobody wants abbreviated — the URI and the request id — on rows of their own.

/ narrows the list on screen, ? shows the keys and what the glyphs mean, and q quits.

Functions is where the request spent its time, in the order it ran, so the page reads as a call sequence: what called what, and where the time went in between. The timeline beside each row shows when in the request the call happened and for how long, which puts a call visibly inside the one which made it. The bar sits on a rail rather than on empty space, so its position is readable even where the colour is not.

Colour and weight come from self time — how long a function took minus how long the functions it called took. That distinction is what makes the page useful: a framework's kernel and dispatch frames wrap the whole request, so colouring by elapsed time paints them all at maximum severity while the function actually burning the time stays cool. Ordering is chronological and severity is self time, so the hotspot is still the heaviest mark on the page wherever in the sequence it falls.

Self time is an upper bound rather than a measurement. The extension only fires a probe for calls above compass.function_threshold, so a child cheaper than the threshold is missing from the tree and its time is counted against its parent. Lower the threshold if a frame looks suspiciously hot.

Drupal Cacheable Metadata is what the Drupal specific probes reported. The tab only appears when the trace has any: a Node trace, a PHP CLI run and any PHP application which is not Drupal all have none, and a page which can only say "there is nothing here" is not worth offering. Drupal derives the cacheability of a response as it builds it, and the lowest max age that any part of the page contributes wins, so a single line of code can make an otherwise cacheable page uncacheable. The page lists every cacheability event Drupal produced, most restrictive first, with a max age of zero — the thing which made the response uncacheable — shown in red.

Both trace pages abbreviate, and both carry a panel beneath the table showing whichever row the cursor is on in full — so moving down a table is how you read through the detail, rather than something you do and then look elsewhere for.

On Functions that is the whole name, which the table shortens to namespace initials and then truncates, along with the numbers behind the two columns which are a percentage and a picture: what the self share is in milliseconds, and where in the request the call sat. On Drupal Cacheable Metadata it is the cache tags and contexts, which the table only counts, and the object's full class name — the namespace being exactly what says which module it came from.

Every list has a two cell gutter: the left marks the row the cursor is on, and the right carries severity as a weight, from a hairline to a solid block. It says the same thing as the colour beside it, in a channel which survives the colour being turned off — so the interface still reads under NO_COLOR or over a sixteen colour link.

The masthead draws the wordmark as block letterforms — six rows of pixels rendered two to a character row with the half block glyphs — with a gradient running across them from Compass's primary blue to the palette's dim blue, standing in a field of diagonal hatching. At terminal resolution there is no type size to reach for, so the only way to make something bigger is to draw it out of more cells. The treatment is borrowed from crush.

Every colour on screen comes from the Skpr palette in pkg/app/theme, with two exceptions: a derived grey — a blend of the palette's own White and Grey, for the rung of the text hierarchy the palette does not have — and PHP's own purple in the runtime column, because that column is naming somebody else's project. There is a test asserting that list stays at two.

A red beside the runtime on the Search list marks a request worth going and looking at: something in it set a max age of zero, so the response cannot be cached. It fires for that and nothing else — a mark which means two things stops meaning either of them. ? has the legend.

The trace metadata carries the resulting max age, and a trace whose max age ended up at zero is flagged C on the Search page. A D there means events were dropped and the trace is truncated.

Each trace carries its request ID, taken from the X-Request-ID header, or its process ID for a CLI run. Search shows the first eight characters, which is enough to recognise a trace and enough to grep a log with; the open trace shows the value in full. A request which arrived without the header has no ID, and shows · rather than the extension's UNKNOWN placeholder.

Install

Both binaries are published as container images on each release. Releases are tagged by version, there is no latest:

docker pull ghcr.io/skpr/compass:v1.10.0          # CLI
docker pull ghcr.io/skpr/compass-sidecar:v1.10.0  # Sidecar

The sidecar needs to see the application's processes and load eBPF programs, so it runs privileged and shares the application's PID namespace:

  sidecar:
    image: ghcr.io/skpr/compass-sidecar:v1.10.0
    privileged: true
    pid: "service:php-fpm"

Then point the CLI at it:

compass --uri http://localhost:28624/v1/traces

The application also needs the extension or addon installed. The compose stack takes the PHP extension from its skpr/php-fpm base image, so the version it gets is whichever that image ships — see docker/compose/php-fpm/Dockerfile.

The Node addon is installed from a compass-node release, see docker/compose/node/Dockerfile.

The probe names and arities have to match the tracers in this repository (fpm_request_init, fpm_function, fpm_request_shutdown, cli_* and canary), so an older extension build will fail to attach.

The Drupal probes (drupal_cacheablemetadata_createfromrenderarray and drupal_cacheablemetadata_createfromobject) are the exception: they are attached when the extension has them and skipped when it does not, so an extension predating them keeps its PHP tracing and loses only the Drupal page.

Configuration

CLI

Flag Environment variable Default Description
--uri COMPASS_URI http://localhost:28624/v1/traces Trace stream to connect to. extension:///path/to/compass.so traces a probe file directly.
--token COMPASS_TOKEN Sent to the sidecar as the X-Skpr-Token header.
--ca-file COMPASS_CA_FILE Certificate authority which signed the sidecar certificate.
--insecure-skip-verify COMPASS_INSECURE_SKIP_VERIFY false Skip verification of the sidecar certificate.
--max-traces COMPASS_MAX_TRACES 500 Traces to retain, oldest are discarded first.

The CLI reconnects with a backoff if the sidecar restarts, and the footer shows the current connection state.

Sidecar

Configured by environment variable, or with --config pointing at a YAML file — see docs/sidecar-config.yaml.

Environment variable Default Description
COMPASS_SIDECAR_ADDR :28624 Address to serve traces and metrics on.
COMPASS_SIDECAR_LOG_LEVEL info debug, info, warn or error.
COMPASS_SIDECAR_PHP_PROCESS_NAME php-fpm Process which loads the PHP extension.
COMPASS_SIDECAR_PHP_EXTENSION_PATH /usr/lib/php/modules/compass.so Extension path, inside the PHP container.
COMPASS_SIDECAR_NODE_PROCESS_NAME node Process which loads the Node addon.
COMPASS_SIDECAR_NODE_ADDON_PATH /usr/lib/compass/node/compass.node Addon path, inside the Node container.
COMPASS_SIDECAR_DISCOVERY_TIMEOUT 1m How long to wait for a runtime before deciding it is not present.
COMPASS_SIDECAR_TOKEN Require this token from clients.
COMPASS_SIDECAR_CERT_FILE Serve traces over TLS with this certificate.
COMPASS_SIDECAR_KEY_FILE Key for the TLS certificate.

Runtimes are optional: a PHP-only deployment does not need Node, and vice versa. The sidecar only fails to start when neither is found.

/metrics exposes Prometheus metrics, including compass_sidecar_runtime_discovered, compass_sidecar_subscriptions, compass_sidecar_collector_running and compass_sidecar_traces_dropped_total.

Development

Tooling is managed with mise:

mise run generate   # Compile the eBPF programs with bpf2go.
mise run test       # Run the tests.
mise run test:race  # Run the tests with the race detector.
mise run lint       # Run golangci-lint.
mise run build      # Build both binaries into _output.

generate needs clang, libbpf, bpftool and kernel BTF (/sys/kernel/btf/vmlinux), so it only runs on Linux. Everything above is also available in the build image, which is how CI runs it:

docker build --target=test .      # Lint and test.
docker build --target=cli .       # Build the CLI image.
docker build --target=sidecar .   # Build the sidecar image.

Toolchain

graph TD
    subgraph Development
        Mise[Mise] --> Go[Go]
        Mise --> BPF2Go[bpf2go]
        Mise --> Lint[golangci-lint]
    end

    subgraph Build_Runtime
        Docker[Docker] --> Alpine[Alpine Linux]
        Alpine --> Clang[Clang/LLVM]
        Alpine --> LibBpf[libbpf]
    end

    subgraph Observability
        Compass[Compass] --> eBPF[eBPF]
        Compass --> Prometheus[Prometheus]
    end
Loading

About

A tool for pointing developers in the right direction for performance issues.

Resources

Stars

12 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages