Continuous Pyroscope export + distroless GHCR sidecar image#1
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.PprofSinkalready emits the exact gzipped pprof v3 that Pyroscope ingests, so most of the work is reuse.What's included
Export sink
PprofBuilderfromPprofSink.PyroscopeSink: accumulates samples and POSTs gzipped pprof to/ingestevery--push-interval-secs. Push failures are logged and never stop sampling.--pyroscope-url(enables sidecar mode),--pyroscope-app, repeatable--pyroscope-label,--push-interval-secs,--pyroscope-auth-token,--pyroscope-tenant-id(env fallbacks). Behind the default-onpyroscopefeature (syncureq+ rustls — no async runtime).target.rs: when the binary path from/proc/PID/mapsisn'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 intodistroless/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:latestatghcr.io/loks0n/php-fast-profile.ci.yml: newsmoke-pyroscopejob against agrafana/pyroscopeservice container. Inlined smoke scripts also extracted intoscripts/ci/(shellcheck-clean).deny.toml: allowCDLA-Permissive-2.0(webpki-roots CA bundle).Two non-obvious findings while testing
/ingestwants the raw pprof body, not multipart — the multipart--boundarybyte (0x2D) parses as protobuf field 5/wiretype 5 (422 wrong wireType = 5 for field Function)./proc/<pid>/root(distroless has no PHP binary at the mapped path).Verification
fmtclean;cargo-denylicenses/bans/sources pass.docker-compose.e2e.yml: the distroless sidecar attached to an adjacentphp:8.3container over a shared PID namespace, pushed to Pyroscope, and the queried flamegraph showed the real call stack{main} → outer → inner → sqrt/sin.Scope notes
🤖 Generated with Claude Code