APM
APM gives you per-transaction traces, a component breakdown (application versus database, cache, external), method-level spans, N+1 detection, and a CPU flamegraph. The provisioned runtimes are PHP (WordPress-aware), .NET, and Node.js. The client installs nothing: the agent provisions everything it needs, and it never restarts your services on its own.
APM comes in three depths. You can run any subset.
Layer 1: RED metrics, zero touch
Request rate, errors and duration at the protocol level, with no reload of your web server or app. On a full kernel this uses eBPF to observe HTTP request and reply timing on watched ports, plus the client-side round trip to the database and cache, giving you a component breakdown (which fraction of the request waited on the DB). Ports do not need to be well-known: for runtimes like Node the watched ports are resolved from the discovered listeners of the process itself. Where eBPF is not available (an unprivileged container), the same RED numbers are sourced from the access log instead. Nothing to install, no restart. The eBPF program is compiled once and adapts itself to the running kernel's structures when it loads (CO-RE), so it runs on the long-term kernels distributions ship today, from the 5.10 and 6.1 lines up, not only on the newest; the one requirement is a kernel built with BTF type information, which every mainstream distribution kernel has.
Layer 2: method-level spans
The full trace tree (the request, then the WP hooks, then the SQL query). This uses the OpenTelemetry PHP extension, the .NET CLR profiler, or the Node.js auto-instrumentation. The agent fingerprints the runtime (for PHP: version, thread safety, architecture, libc; for Node: version and how the app is launched), pulls the exact signed prebuilt artifact from the release channel, validates it aside, and then stages the change. A human applies the reload when it suits them. The agent writes the change and reports it, it does not touch php-fpm, the app pool, or your Node service itself. See Never restart your services.
Spans flow to the agent's local OTLP receiver (see Local ingest ports) and up the WebSocket to storage, where they become a waterfall. The receiver always answers "accepted" and keeps a disk spool of its own for the spans it cannot forward yet, so an outage between the host and the platform never shows up as export errors in your application's log; the traces are replayed when the connection is back. The receiver speaks standard OTLP, so it also accepts spans from any other OpenTelemetry SDK (Java, Node, Python, Go), not just the PHP and .NET runtimes the agent provisions.
Details that matter on real fleets:
- Multi-version PHP hosts are handled per version. A shared-hosting box running 8.1, 8.2 and 8.3 php-fpm side by side gets a fingerprint, an artifact, a staged ini and a reload command per version, not one guess. PHP 8.1 through 8.5 are covered.
- A published artifact never changes under a running site. Every build in the release channel is frozen: once a file is published under a name, that name keeps those bytes for good, so a host that applied an extension is never asked to reload for a rebuild it did not ask for. A new upstream version of the extension is published under a new name as a second line of artifacts. Which line a host runs is decided per PHP version by what is already on the host: a version already instrumented stays on its line, a version instrumented for the first time (a new host, a pool added later, a PHP upgrade) takes the newest line. Moving an existing version to the newer line is a decision you make, not a side effect of an agent update.
- Both fpm and mod_php are covered, on Debian-family Apache and RHEL-family httpd. The ini goes into the SAPI's own conf.d, not a guessed one.
- The receiver starts itself. When an APM target is staged or active, the agent brings up its loopback OTLP receiver automatically, so an applied change never exports into a dead port.
- Pre-flight before staging. The extension is load-tested aside (
php-fpm -d extension=... -mmust pass) before anything is staged, and the agent checks path permissions andopen_basedirrestrictions that would break workers, refusing to stage and reporting exactly what is in the way instead. - Artifact refresh is safe. A newly published signed artifact is re-downloaded and re-staged as a fresh "action required"; nothing swaps under a running service.
- External deactivation is detected. If someone removes the change behind the agent's back, the state machine re-arms and a fresh "action required" appears, instead of the console showing a stale "active". A half-removed activation (the prepend left behind without the extension) is detected too, and the agent stages a ready cleanup command.
- No dependency collisions. The PHP SDK bundle's vendored libraries are namespace-isolated, so they cannot clash with whatever versions the application itself ships.
- Noise is filtered at the source. Span patterns with no diagnostic value (for example the per-row iterator spans of batched SQL) are dropped in the agent's receiver before they cost bandwidth or storage.
Node.js specifics
Node is activated through NODE_OPTIONS, and the agent handles the launch models it
finds:
- systemd services get a staged unit drop-in, applied with a restart command that carries a dated backup and rolls itself back if the restarted service does not come up.
- pm2 apps get a staged environment file plus the
pm2commands that make the change survive a resurrect and a reboot; per-app service names come from pm2 itself. - Windows services are handled natively: the staged change sets the service's environment in the registry (the SCM equivalent of a unit drop-in), with the same guarded apply-and-revert. Wrapper-launched node (WinSW, nssm) is matched through the wrapper.
- Bare launches (a node process no supervisor owns) cannot be activated safely from outside, so the agent reports an honest ready-to-paste recipe instead.
The bundle's loader is inert by construction: without the endpoint variable it returns immediately, so a stray artifact can never instrument anything by accident. A runtime below the supported floor (Node 18.19) is reported as blocked and nothing is staged, so nobody is asked to restart a service for a change that would be a no-op.
Alongside spans, an instrumented Node service also reports runtime metrics with no extra setup: event loop delay and utilization, GC time, heap usage. The event loop lag is the "Node cannot keep up" signal and has a built-in alert rule. And matched Node services get service telemetry regardless of instrumentation: per-unit up/down, crash-restart counts from the supervisor, and restart detection that works even under pm2.
A CPU flamegraph for Node exists too, sampled by perf from outside the process.
It needs one launch flag in the staged NODE_OPTIONS, so it is a separate opt-in grant
(off by default): enabling it means a restart, and the agent never restarts a client
service without that explicit consent. Honest boundaries: on Windows there is no perf
and no eBPF, so Node there gets spans, runtime metrics, service telemetry and logs;
iisnode and pm2-on-Windows are not covered yet.
If you already instrument a runtime the agent does not provision (Java, Python, Go), point its OpenTelemetry exporter at the agent's local OTLP receiver and the spans ride the same pipeline.
Layer 3: CPU flamegraph
A sampling profiler that reads the live stack from outside the process, with no extension
and no reload. For PHP it reads the php-fpm VM stack directly. For .NET it uses the runtime's
built-in EventPipe through a provisioned dotnet-trace. For Node it samples with perf
(kernel-side, read-only; this one needs a launch flag and therefore its own opt-in grant,
see the Node section). All three fold into the same flamegraph pipeline. This opens up
the "main" time that request-level tools cannot see.
WordPress-aware
Two mechanisms attribute work to the responsible plugin, theme, or core.
CPU attribution (layer 3). The profiler reads each frame's file path and classifies it, so "which plugin burns CPU" shows up as an owner share per plugin, theme, or core. Where php-fpm pools are per site, the folded stacks also carry the pool, which makes the flamegraph per site.
Deep trace (layer 2). On a sampled fraction of requests, the instrumentation wraps
the WordPress hook callbacks that are registered and opens a span per callback
(hook:<tag>, carrying the function and its file). You see which plugin's callback sat
inside a slow request, not just that WordPress was slow. It is engineered to be safe on
production: only 1 in N requests pays the cost, micro-callbacks below a millisecond are
skipped, and hard caps bound the span count per request. Deep trace is off by default
and enabled per host from the console; the change is staged and applied like every
other APM change, and without the grant the code path is a single comparison.
N+1 and external-wait
The server detects N+1 query patterns in the span data (many repeated normalized queries in one trace) and raises an insight. The layer-1 component tracking also watches outbound calls so "the page is waiting on a third-party API" shows up alongside the database time.
Where it works
Layers 2 and 3 work inside an LXC container (same PID namespace). Only layer 1's eBPF path needs kernel privilege, so in an unprivileged container layer 1 falls back to access-log RED while spans and the flamegraph still work. Container APM is covered by the agent reading sibling container stacks by host PID plus the app exporting OTLP to the host's address on the container bridge.
Turning capabilities off
Deep capabilities are grants, and grants are revocable from the console: switching eBPF or the profiler off for a host reaches the agent within about a minute, stops the probes, tears down the running eBPF engine without any reload, and withdraws pending staged changes. Deep trace is the inverse case, off by default until granted.
arm64 prebuilt APM artifacts and a ZTS (thread-safe) variant of the PHP CPU profiler are planned. The amd64 artifact matrix (including ZTS builds of the extension itself) and the standard non-thread-safe php-fpm profiler case are live.