feat(pyroscope): Update debuginfo client for HTTP/1.1 upload API#6037
feat(pyroscope): Update debuginfo client for HTTP/1.1 upload API#6037korniltsev-grafanista wants to merge 19 commits intomainfrom
Conversation
🔍 Dependency Reviewgithub.com/grafana/pyroscope/api v1.3.2 → v1.3.2-0.20260414051146-5eb4b919ec16 — ❌ Changes NeededBreaking API changes in the Debuginfo service require code updates. The bi-directional streaming RPC
This impacts both client-side uploaders and server-side (proxy) implementations. Evidence (based on API surface used in this PR):
What you need to change
- stream := client.Upload(ctx)
- // Send ShouldInitiateUploadRequest on stream
- _ = stream.Send(&debuginfov1alpha1.UploadRequest{ Data: &debuginfov1alpha1.UploadRequest_Init{ ... }})
- resp, _ := stream.Receive()
- if !resp.GetInit().ShouldInitiateUpload { return }
- // Stream file chunks
- for { stream.Send(&debuginfov1alpha1.UploadRequest{ Data: &debuginfov1alpha1.UploadRequest_Chunk{ ... }}) }
- stream.CloseRequest()
- // Drain server responses
- for { _, err := stream.Receive(); if err == io.EOF { break } }
+ // Step 1: ShouldInitiateUpload (unary)
+ initResp, err := client.ShouldInitiateUpload(ctx, connect.NewRequest(&debuginfov1alpha1.ShouldInitiateUploadRequest{
+ File: &debuginfov1alpha1.FileMetadata{
+ GnuBuildId: buildID,
+ OtelFileId: fileID.StringNoQuotes(),
+ Name: fileName,
+ Type: fileType,
+ },
+ }))
+ if err != nil || !initResp.Msg.ShouldInitiateUpload { return err }
+ // Step 2: HTTP POST upload of bytes to:
+ // {BaseURL}/debuginfo.v1alpha1.DebuginfoService/Upload/{gnu_build_id}
+ // Use the same HTTP client configured for the endpoint (auth, TLS, etc.)
+ if err := httpUpload(ctx, httpClient, baseURL, buildID, reader); err != nil { return err }
+ // Step 3: UploadFinished (unary)
+ _, err = client.UploadFinished(ctx, connect.NewRequest(&debuginfov1alpha1.UploadFinishedRequest{
+ GnuBuildId: buildID,
+ }))
+ return err
- func (c *Component) Upload(ctx context.Context, stream *connect.BidiStream[debuginfov1alpha1.UploadRequest, debuginfov1alpha1.UploadResponse]) error {
- // recv init, forward to downstreams, recv downstream init, forward chunks, drain, etc.
- }
- // Registered as:
- debuginfov1alpha1connect.RegisterDebuginfoServiceHandler(router, c) // contained Upload implementation
+ // Unary init. Forward to downstream.
+ func (c *Component) ShouldInitiateUpload(
+ ctx context.Context,
+ req *connect.Request[debuginfov1alpha1.ShouldInitiateUploadRequest],
+ ) (*connect.Response[debuginfov1alpha1.ShouldInitiateUploadResponse], error) {
+ client, err := c.firstClient()
+ if err != nil { return nil, connect.NewError(connect.CodeUnavailable, err) }
+ return client.ShouldInitiateUpload(ctx, connect.NewRequest(req.Msg.CloneVT()))
+ }
+ // Unary finished. Forward to downstream.
+ func (c *Component) UploadFinished(
+ ctx context.Context,
+ req *connect.Request[debuginfov1alpha1.UploadFinishedRequest],
+ ) (*connect.Response[debuginfov1alpha1.UploadFinishedResponse], error) {
+ client, err := c.firstClient()
+ if err != nil { return nil, connect.NewError(connect.CodeUnavailable, err) }
+ return client.UploadFinished(ctx, connect.NewRequest(req.Msg.CloneVT()))
+ }
+ // Plain HTTP handler that posts raw bytes to the downstream Upload endpoint.
+ func (c *Component) UploadHTTPHandler() http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ gnuBuildID := mux.Vars(r)["gnu_build_id"]
+ client, err := c.firstClient()
+ if err != nil {
+ http.Error(w, "no downstream", http.StatusServiceUnavailable)
+ return
+ }
+ ctx, cancel := context.WithTimeout(r.Context(), c.debugInfoUploadTimeout)
+ defer cancel()
+ if err := client.Upload(ctx, gnuBuildID, r.Body); err != nil {
+ http.Error(w, fmt.Sprintf("downstream upload: %v", err), http.StatusBadGateway)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ })
+ }
+ // Register both unary RPCs via Connect and the HTTP POST endpoint:
+ debuginfov1alpha1connect.RegisterDebuginfoServiceHandler(router, c)
+ router.Handle("/debuginfo.v1alpha1.DebuginfoService/Upload/{gnu_build_id}", c.UploadHTTPHandler()).Methods("POST")
+ type DebugInfoClient struct {
+ DebuginfoServiceClient debuginfov1alpha1connect.DebuginfoServiceClient
+ HTTPClient *http.Client
+ BaseURL string
+ UploadTimeout time.Duration
+ }
+ func (c *DebugInfoClient) Upload(ctx context.Context, buildID string, body io.Reader) error {
+ req, _ := http.NewRequestWithContext(ctx, "POST",
+ strings.TrimRight(c.BaseURL, "/")+"/debuginfo.v1alpha1.DebuginfoService/Upload/"+url.PathEscape(buildID), body)
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil { return err }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("upload: HTTP %d", resp.StatusCode)
+ }
+ return nil
+ }
- func (a Appender) DebugInfoClients() []debuginfov1alpha1connect.DebuginfoServiceClient
+ func (a Appender) DebugInfoClients() []*debuginfoclient.Client
Why this is required
Reference snippets (from the PR changes for this upgrade)
- stream := client.Upload(ctx)
- // send init on stream, receive response, stream chunks...
+ resp, err := client.ShouldInitiateUpload(ctx, connect.NewRequest(&debuginfov1alpha1.ShouldInitiateUploadRequest{ File: fileMeta }))
+ if err != nil { return fmt.Errorf("ShouldInitiateUpload: %w", err) }
+ if !resp.Msg.ShouldInitiateUpload { return nil }
+ if err := client.Upload(ctx, buildID, r); err != nil { return fmt.Errorf("upload: %w", err) }
+ if _, err := client.UploadFinished(ctx, connect.NewRequest(&debuginfov1alpha1.UploadFinishedRequest{ GnuBuildId: buildID })); err != nil {
+ return fmt.Errorf("UploadFinished: %w", err)
+ }
+ debuginfov1alpha1connect.RegisterDebuginfoServiceHandler(router, c) // unary only
+ router.Handle("/debuginfo.v1alpha1.DebuginfoService/Upload/{gnu_build_id}", c.UploadHTTPHandler()).Methods("POST")Notes
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 346ed2f910
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func (c *Component) firstClient() (debuginfo.Client, error) { | ||
| clients := c.getDebugInfoClients() | ||
| if len(clients) == 0 { | ||
| return connect.NewError(connect.CodeUnavailable, fmt.Errorf("no downstream endpoints available")) | ||
| return nil, fmt.Errorf("no downstream endpoints available") | ||
| } | ||
| return clients[0], nil |
There was a problem hiding this comment.
Preserve debuginfo fan-out across all downstream endpoints
Selecting clients[0] here changes debuginfo proxying from fan-out to single-target routing: ShouldInitiateUpload, the raw Upload POST handler, and UploadFinished now only hit the first downstream client. In any pyroscope.receive_http config with multiple forward_to write targets, profiles are still replicated but debug symbols are uploaded to only one backend, so the others can no longer symbolize frames correctly. This is a functional regression from the previous multi-downstream behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
I assume this was done since its trickier because of the the POST handler
| func (c *Component) update(args component.Arguments) (bool, error) { | ||
| shutdown := false | ||
| newArgs := args.(Arguments) | ||
| // required for debug info upload over connect over http2 over http server port | ||
| if newArgs.Server.HTTP.HTTP2 == nil { | ||
| newArgs.Server.HTTP.HTTP2 = &fnet.HTTP2Config{} | ||
| } | ||
| newArgs.Server.HTTP.HTTP2.Enabled = true | ||
|
|
There was a problem hiding this comment.
Keep HTTP/2 enabled when serving gRPC on receive_http
Removing the HTTP2.Enabled = true override in update makes this component inherit the default server setting where HTTP/2 is disabled, which breaks callers using the gRPC protocol on the HTTP port (for example connect.WithGRPC() clients) unless every deployment explicitly opts in to HTTP/2. This change broadens impact beyond debuginfo upload and introduces a backward-incompatible protocol regression for existing gRPC ingestion clients.
Useful? React with 👍 / 👎.
0dac671 to
729e260
Compare
| } | ||
|
|
||
| func (c *DebugInfoClient) Upload(ctx context.Context, buildID string, body io.Reader) error { | ||
| uploadURL := strings.TrimRight(c.BaseURL, "/") + "/debuginfo.v1alpha1.DebuginfoService/Upload/" + buildID |
There was a problem hiding this comment.
Nit: using url.PathEscape(buildID) as a safeguard
| func (c *Component) firstClient() (debuginfo.Client, error) { | ||
| clients := c.getDebugInfoClients() | ||
| if len(clients) == 0 { | ||
| return connect.NewError(connect.CodeUnavailable, fmt.Errorf("no downstream endpoints available")) | ||
| return nil, fmt.Errorf("no downstream endpoints available") | ||
| } | ||
| return clients[0], nil |
There was a problem hiding this comment.
I assume this was done since its trickier because of the the POST handler
Addresses review comment on PR #6037: escape the build_id path segment as a safeguard when constructing the Upload URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
729e260 to
1fea1ed
Compare
The Pyroscope server debuginfo API was rewritten from a single bidirectional streaming RPC to three HTTP/1.1-compatible endpoints (grafana/pyroscope#5046). This updates the Alloy client to match. The upload flow is now: 1. ShouldInitiateUpload (connect-go unary RPC) 2. POST /debuginfo.v1alpha1.DebuginfoService/Upload/{build_id} (plain HTTP) 3. UploadFinished (connect-go unary RPC) Key changes: - Define reporter.Endpoint (ConnectClient + HTTPClient + BaseURL) - Rename DebugInfoClients() to DebugInfoEndpoints() across all interfaces - Rewrite attemptUpload() from bidi streaming to 3-step HTTP/1.1 flow - Rewrite receive_http proxy to forward to first downstream (no fan-out) - Remove h2c client, HTTP/2 forcing, and chunk-streaming logic - All test servers use plain HTTP/1.1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…m build The previous approach used `type Endpoint = reporter.Endpoint` in cross-platform code, which imported the linux-only reporter package and broke macOS/Windows/FreeBSD builds. Replace with a DebugInfoClient interface that embeds the connect client and adds an Upload method for plain HTTP POST. The concrete implementation lives in write.go where the HTTP client is constructed. No Endpoint struct needed, no cross-platform import of reporter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Export the concrete DebugInfoClient implementation as write.Client so tests can use the real type instead of duplicating it as a mock. The reporter test keeps its own local implementation because importing write from reporter would create an import cycle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- debuginfo.DebugInfoClient interface → debuginfo.Client - debuginfo.Client struct → debuginfo.Uploader - debuginfo.NewClient → debuginfo.NewUploader - write.Client → write.DebugInfoClient Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The repo has three go.mod files that depend on pyroscope/api. Only the root was updated, causing build failures in CI where the other modules still resolved the old version without ShouldInitiateUpload/UploadFinished. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses review comment on PR #6037: escape the build_id path segment as a safeguard when constructing the Upload URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the struct + Upload method out of package write into a new write/debuginfoclient package, breaking the cycle that prevented reporter tests from reusing the production type. Reporter tests now use debuginfoclient.Client instead of a duplicated test stub. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9ca4c44 to
9c0dcb3
Compare
Both debuginfo.Client and reporter.DebugInfoClient had exactly one implementation (*debuginfoclient.Client), so the interfaces were pure indirection. Removes them and updates all DebugInfoClients() method signatures to return []*debuginfoclient.Client. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o proxy Adds pyroscope_receive_http_debuginfo_downstream_calls_total counter with method (ShouldInitiateUpload|UploadFinished|Upload) and result (success|failure) labels. Each handler increments once per call via deferred increment, so "no downstream available" errors are also counted as failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d block Collapses the success/error log into the same defer that increments the metric, so the path is uniform and the "no downstream" failure is now logged with the method-specific context. firstClient no longer logs its own error since the caller's defer handles it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts recordDownstream(logger, method, err) helper so all three proxy methods share one line of defer plumbing for the metric + result log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the Alloy debuginfo upload client to match the new Pyroscope server API (grafana/pyroscope#5046) which replaced the single bidirectional streaming RPC with three HTTP/1.1-compatible endpoints.
New upload flow:
ShouldInitiateUpload— connect-go unary RPCPOST /debuginfo.v1alpha1.DebuginfoService/Upload/{build_id}— plain HTTP, raw bodyUploadFinished— connect-go unary RPCThis removes the hard dependency on HTTP/2 (h2/h2c) for debuginfo uploads.
Depends on grafana/pyroscope#5046 (pyroscope/api module needs to be tagged before the
replacedirective can be swapped for a proper version).🤖 Generated with Claude Code