Skip to content

Continuous Pyroscope export + distroless GHCR sidecar image#1

Merged
loks0n merged 3 commits into
mainfrom
feat/pyroscope-sidecar
Jun 5, 2026
Merged

Continuous Pyroscope export + distroless GHCR sidecar image#1
loks0n merged 3 commits into
mainfrom
feat/pyroscope-sidecar

Conversation

@loks0n
Copy link
Copy Markdown
Owner

@loks0n loks0n commented Jun 5, 2026

Summary

Adds a continuous-profiling sidecar mode that pushes profiles to a Grafana Pyroscope server, plus a distroless multi-arch image published to GHCR on release. Verified end-to-end.

The discovery side already existed (--this-container + -P/--cmdline), so "sidecar" here is a new push sink + container packaging — not new attach logic. PprofSink already emits the exact gzipped pprof v3 that Pyroscope ingests, so most of the work is reuse.

What's included

Export sink

  • Extract a reusable PprofBuilder from PprofSink.
  • New PyroscopeSink: accumulates samples and POSTs gzipped pprof to /ingest every --push-interval-secs. Push failures are logged and never stop sampling.
  • CLI: --pyroscope-url (enables sidecar mode), --pyroscope-app, repeatable --pyroscope-label, --push-interval-secs, --pyroscope-auth-token, --pyroscope-tenant-id (env fallbacks). Behind the default-on pyroscope feature (sync ureq + rustls — no async runtime).
  • target.rs: when the binary path from /proc/PID/maps isn't in pfp's own mount namespace, fall back to /proc/PID/root/<path> so a separate-container sidecar can resolve symbols.

Packaging & CI

  • Dockerfile: multi-stage static musl build into distroless/static.
  • release.yml: builds the image natively per-arch (amd64 + arm64, no QEMU compile), pushes by digest, and stitches a multi-arch manifest tagged :<version> and :latest at ghcr.io/loks0n/php-fast-profile.
  • ci.yml: new smoke-pyroscope job against a grafana/pyroscope service container. Inlined smoke scripts also extracted into scripts/ci/ (shellcheck-clean).
  • deny.toml: allow CDLA-Permissive-2.0 (webpki-roots CA bundle).

Two non-obvious findings while testing

  1. Grafana Pyroscope's /ingest wants the raw pprof body, not multipart — the multipart --boundary byte (0x2D) parses as protobuf field 5/wiretype 5 (422 wrong wireType = 5 for field Function).
  2. A sidecar must read the target's filesystem via /proc/<pid>/root (distroless has no PHP binary at the mapped path).

Verification

  • 42 tests pass; clippy clean across all 4 feature combos; fmt clean; cargo-deny licenses/bans/sources pass.
  • True e2e via docker-compose.e2e.yml: the distroless sidecar attached to an adjacent php:8.3 container over a shared PID namespace, pushed to Pyroscope, and the queried flamegraph showed the real call stack {main} → outer → inner → sqrt/sin.

Scope notes

  • Pyroscope only; native OTLP profiles deferred (the signal is experimental; collectors can forward OTLP→Pyroscope today).

🤖 Generated with Claude Code

loks0n and others added 3 commits June 5, 2026 11:28
Add a sidecar mode that continuously pushes profiles to a Grafana
Pyroscope server, and publish a distroless multi-arch image to GHCR on
release.

- output: extract a reusable PprofBuilder from PprofSink; add PyroscopeSink
  that accumulates samples and POSTs gzipped pprof to /ingest every
  --push-interval-secs. Grafana Pyroscope reads the raw body (not multipart),
  so we send it directly; push failures are logged and never stop sampling.
- cli: --pyroscope-url (enables sidecar mode), --pyroscope-app,
  --pyroscope-label, --push-interval-secs, --pyroscope-auth-token,
  --pyroscope-tenant-id, with env fallbacks. Behind the default-on
  `pyroscope` feature (ureq + rustls).
- target: when the binary path from /proc/PID/maps is absent in our mount
  namespace, fall back to /proc/PID/root/<path> so a separate-container
  sidecar can resolve symbols.
- Dockerfile: multi-stage static musl build into distroless/static.
- release.yml: build the image natively per-arch, push by digest, and
  stitch a multi-arch manifest tagged :<version> and :latest on GHCR.
- ci.yml: smoke-pyroscope job pushes to a grafana/pyroscope service and
  asserts a clean push (plus a best-effort query-back).
- deny.toml: allow CDLA-Permissive-2.0 (webpki-roots CA bundle).

Verified end-to-end via docker-compose.e2e.yml: the distroless sidecar
attaches to an adjacent php container over a shared PID namespace and the
resulting Pyroscope flamegraph shows the real PHP call stack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the multi-line bash blocks (Sury PHP install, NTS/ZTS/Pyroscope smoke
tests) out of ci.yml into dedicated, shellcheck-clean scripts under
scripts/ci/, with a shared spin.php fixture. ci.yml steps now just invoke
them, and the scripts are runnable locally.

Fixes a latent bug carried over from the inlined version: `! grep -q
'push failed'` is exempt from `set -e`, so a push failure never failed the
job; replaced with an explicit conditional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a repeatable --pyroscope-header "Name: value" flag (env PYROSCOPE_HEADER,
newline-separated for several) for gateway auth schemes the built-in
--pyroscope-auth-token / --pyroscope-tenant-id don't cover (Basic, X-API-Key,
proxy headers). Applied after the built-in auth headers so it can override
them. Verified e2e: pfp pushes successfully to Pyroscope with custom headers set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@loks0n loks0n merged commit bd15203 into main Jun 5, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant