Skip to content

refactor: extract request interceptor registry#19828

Draft
parametalol wants to merge 6 commits intomichael/fix-grpc-ua-orderfrom
michael/interceptor-registry
Draft

refactor: extract request interceptor registry#19828
parametalol wants to merge 6 commits intomichael/fix-grpc-ua-orderfrom
michael/interceptor-registry

Conversation

@parametalol
Copy link
Copy Markdown
Contributor

Description

Extract the generic request interceptor infrastructure from pkg/telemetry/phonehome into dedicated packages:

  • pkg/clientprofile/ — campaign matching logic (APICallCampaign, Headers, GlobMap, glob-based header matching)
  • pkg/grpc/common/requestinterceptor/ — generic RequestInterceptor registry that computes RequestParams once per API request and fans out to registered handlers; owns RequestParams, NewHeaders, and GetGRPCRequestBody

phonehome retains only the telemetry-specific Interceptor type and Client methods. Type aliases (phonehome.RequestParams, clientprofile.RequestParams) keep the diff minimal for consumers.

The RequestParams.Headers field is now plain http.Header, breaking the dependency from requestinterceptor on clientprofile. Campaign code converts to Headers(rp.Headers) for glob matching.

Also includes fixes from the michael/fix-grpc-ua-order base branch: correct User-Agent value ordering (HTTP client first, gRPC gateway second) and deduplication.

User-facing documentation

Testing and quality

  • the change is production ready: the change is GA, or otherwise the functionality is gated by a feature flag
  • CI results are inspected

Automated testing

  • added unit tests
  • added e2e tests
  • added regression tests
  • added compatibility tests
  • modified existing tests

How I validated my change

  • All existing unit tests pass after the refactor
  • Build verified across central/..., roxctl/..., sensor/...
  • No functional changes: pure refactor moving types between packages with type aliases for backward compatibility

@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Apr 4, 2026

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In pkg/grpc/common/requestinterceptor/interceptor.go the helper setHeader is currently unused and can be removed to keep the new interceptor package minimal and focused.
  • In RequestInterceptor.dispatch, handlers are called while holding the read lock; if handlers ever add/remove other handlers or perform long-running work, it may be safer to snapshot the handler funcs under lock and then invoke them after releasing the lock to avoid potential contention or deadlock patterns.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `pkg/grpc/common/requestinterceptor/interceptor.go` the helper `setHeader` is currently unused and can be removed to keep the new interceptor package minimal and focused.
- In `RequestInterceptor.dispatch`, handlers are called while holding the read lock; if handlers ever add/remove other handlers or perform long-running work, it may be safer to snapshot the handler funcs under lock and then invoke them after releasing the lock to avoid potential contention or deadlock patterns.

## Individual Comments

### Comment 1
<location path="pkg/grpc/common/requestinterceptor/interceptor.go" line_range="56-59" />
<code_context>
+	delete(ri.handlers, name)
+}
+
+func (ri *RequestInterceptor) dispatch(rp *RequestParams) {
+	ri.mu.RLock()
+	defer ri.mu.RUnlock()
+	for _, h := range ri.handlers {
+		h(rp)
+	}
</code_context>
<issue_to_address>
**issue (bug_risk):** Calling handlers while holding the read lock can deadlock if a handler modifies handlers (Add/Remove).

In `dispatch`, the RWMutex is held with `RLock` while invoking handlers. If any handler calls `ri.Add`, `ri.Remove`, or any method that takes the write lock, this can deadlock (handler runs under `RLock` and then tries to take `Lock`). Instead, snapshot the handlers under the read lock, release the lock, and then invoke the snapshot:

```go
func (ri *RequestInterceptor) dispatch(rp *RequestParams) {
    ri.mu.RLock()
    handlers := make([]RequestHandler, 0, len(ri.handlers))
    for _, h := range ri.handlers {
        handlers = append(handlers, h)
    }
    ri.mu.RUnlock()

    for _, h := range handlers {
        h(rp)
    }
}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Refactor
    • Restructured request interception infrastructure to use a unified request interceptor instead of separate HTTP and gRPC interceptors.
    • Migrated telemetry campaign matching logic to the clientprofile package for improved organization.
    • Simplified request handler registration system with thread-safe callback registry.

Walkthrough

This pull request refactors telemetry and request interception infrastructure by introducing a new requestinterceptor package that abstracts HTTP and gRPC request handling, migrating campaign matching logic to a new clientprofile package, and updating the phonehome client to use a request handler pattern instead of direct HTTP/gRPC interceptors. Multiple consumers are updated to use the new types and interceptor APIs.

Changes

Cohort / File(s) Summary
New Request Interceptor Framework
pkg/grpc/common/requestinterceptor/interceptor.go, pkg/grpc/common/requestinterceptor/interceptor_test.go, pkg/grpc/common/requestinterceptor/request_details_test.go, pkg/grpc/common/requestinterceptor/request_params.go
Introduces RequestInterceptor with thread-safe handler registry and request dispatch for gRPC unary/streaming and HTTP. Defines RequestParams struct capturing user identity, method/path, status code, gRPC request payload, optional HTTP request, and headers. Provides helpers for extracting request details from gRPC contexts and HTTP requests.
ClientProfile Package
pkg/clientprofile/campaign.go, pkg/clientprofile/campaign_test.go, pkg/clientprofile/headers.go, pkg/clientprofile/headers_test.go
Migrates campaign matching types from phonehome to new clientprofile package. Refactors APICallCampaignCriterion.isFulfilled and APICallCampaign.CountFulfilled to use requestinterceptor.RequestParams. Removes gRPC metadata dependency from Headers, adds GlobMap and NoHeaderOrAnyValue for pattern matching, and implements new Headers.Match method with comprehensive glob-pattern tests.
Phonehome Client Refactoring
pkg/telemetry/phonehome/client.go, pkg/telemetry/phonehome/client_test.go
Removes GetGRPCInterceptor() and GetHTTPInterceptor() methods; adds GetRequestHandler() returning a request tracking callback. Updates tests to use clientprofile types for campaign construction.
Phonehome Config & Examples
pkg/telemetry/phonehome/download_config.go, pkg/telemetry/phonehome/download_config_test.go, pkg/telemetry/phonehome/examples_test.go
Updates RuntimeConfig.APICallCampaign field to use clientprofile.APICallCampaign. Refactors example to construct RequestInterceptor, register client handler, and wire HTTP middleware through interceptor instead of direct client method.
Phonehome Request Details Migration
pkg/telemetry/phonehome/interceptor.go, pkg/telemetry/phonehome/interceptor_test.go, pkg/telemetry/phonehome/request_params.go, pkg/telemetry/phonehome/request_params_test.go
Removes getGRPCRequestDetails, getHTTPRequestDetails helpers and associated tests. Converts RequestParams to type alias pointing to requestinterceptor.RequestParams. Deletes local GlobMap, NoHeaderOrAnyValue, and MatchHeaders implementations.
Central Telemetry
central/telemetry/centralclient/client.go, central/telemetry/centralclient/interceptors.go, central/telemetry/centralclient/interceptors_test.go
Updates telemetry campaign field and method parameters from phonehome.APICallCampaign to clientprofile.APICallCampaign. Migrates campaign construction and header-pattern matching to use clientprofile types.
Server Setup & Request Info
central/main.go, pkg/grpc/requestinfo/requestinfo_test.go
Refactors gRPC server interceptor registration to use single RequestInterceptor instance instead of direct phonehome client interceptor methods. Enhances request info test to validate User-Agent metadata handling across gRPC transport and HTTP layer.
Client Utilities
roxctl/common/client.go, roxctl/common/client_test.go, sensor/common/centralproxy/authorizer.go
Replaces phonehome.Headers references with clientprofile.Headers for HTTP header manipulation across roxctl and sensor client utilities.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main refactoring activity: extracting the request interceptor registry infrastructure.
Description check ✅ Passed The PR description includes all major required sections: overview of changes, justification, validation approach, and testing status, though testing checkboxes show no new unit/e2e/regression tests added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch michael/interceptor-registry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/grpc/common/requestinterceptor/interceptor_test.go (1)

73-84: Strengthen remove test with behavioral assertion (not only internal state).

TestRequestInterceptor_remove currently validates hasHandlers() but does not prove that dispatch is actually skipped on interceptor invocation after Remove. Consider asserting non-invocation via a real unary call.

Proposed test strengthening
 func TestRequestInterceptor_remove(t *testing.T) {
 	ri := NewRequestInterceptor()

 	var called atomic.Bool
 	ri.Add("temp", func(rp *RequestParams) {
 		called.Store(true)
 	})
 	ri.Remove("temp")

 	// With no handlers, dispatch should be skipped entirely.
 	assert.False(t, ri.hasHandlers())
+
+	interceptor := ri.UnaryServerInterceptor()
+	_, err := interceptor(context.Background(), "req",
+		&grpc.UnaryServerInfo{FullMethod: "/test.Service/Method"},
+		func(ctx context.Context, req any) (any, error) {
+			return "ok", nil
+		})
+	require.NoError(t, err)
+	assert.False(t, called.Load())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/grpc/common/requestinterceptor/interceptor_test.go` around lines 73 - 84,
The test TestRequestInterceptor_remove only checks internal state; update it to
assert behavior by invoking the interceptor after Remove and ensuring the
handler is not called: create the interceptor via NewRequestInterceptor, Add a
handler that sets an atomic flag, Remove that handler, then call the public
invocation method used elsewhere in tests (the interceptor's dispatch/invocation
method - same one other tests use to trigger handlers) with a dummy
RequestParams and assert the atomic flag remains false; keep the existing
hasHandlers() assertion as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/grpc/common/requestinterceptor/interceptor.go`:
- Around line 100-104: The code currently defaults status to 0 when
wrappedWriter.GetStatusCode() is nil; change this to use HTTP default 200 by
initializing status to http.StatusOK (or 200) and only override it if sptr !=
nil (keep the wrappedWriter.GetStatusCode() check), and ensure the file imports
net/http if not already; update the block around ri.hasHandlers(),
wrappedWriter.GetStatusCode(), and the status variable accordingly so
telemetry/matching sees 200 for handlers that never explicitly write a status.
- Around line 56-61: The dispatch method runs each handler while holding
ri.mu.RLock(), which can deadlock if a handler calls Add/Remove (which acquire
the write lock); fix by copying ri.handlers into a local slice while holding the
read lock inside RequestInterceptor.dispatch, then release the lock and iterate
over the copied slice calling each handler (so the lock is not held during
handler execution and Add/Remove can proceed).

---

Nitpick comments:
In `@pkg/grpc/common/requestinterceptor/interceptor_test.go`:
- Around line 73-84: The test TestRequestInterceptor_remove only checks internal
state; update it to assert behavior by invoking the interceptor after Remove and
ensuring the handler is not called: create the interceptor via
NewRequestInterceptor, Add a handler that sets an atomic flag, Remove that
handler, then call the public invocation method used elsewhere in tests (the
interceptor's dispatch/invocation method - same one other tests use to trigger
handlers) with a dummy RequestParams and assert the atomic flag remains false;
keep the existing hasHandlers() assertion as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: a6fe9b9f-4eef-456e-82db-74d7c64d74e3

📥 Commits

Reviewing files that changed from the base of the PR and between 2d5d7a2 and e69a4a7.

📒 Files selected for processing (25)
  • central/main.go
  • central/telemetry/centralclient/client.go
  • central/telemetry/centralclient/interceptors.go
  • central/telemetry/centralclient/interceptors_test.go
  • pkg/clientprofile/campaign.go
  • pkg/clientprofile/campaign_test.go
  • pkg/clientprofile/headers.go
  • pkg/clientprofile/headers_test.go
  • pkg/grpc/common/requestinterceptor/interceptor.go
  • pkg/grpc/common/requestinterceptor/interceptor_test.go
  • pkg/grpc/common/requestinterceptor/request_details_test.go
  • pkg/grpc/common/requestinterceptor/request_params.go
  • pkg/grpc/requestinfo/requestinfo_test.go
  • pkg/telemetry/phonehome/client.go
  • pkg/telemetry/phonehome/client_test.go
  • pkg/telemetry/phonehome/download_config.go
  • pkg/telemetry/phonehome/download_config_test.go
  • pkg/telemetry/phonehome/examples_test.go
  • pkg/telemetry/phonehome/interceptor.go
  • pkg/telemetry/phonehome/interceptor_test.go
  • pkg/telemetry/phonehome/request_params.go
  • pkg/telemetry/phonehome/request_params_test.go
  • roxctl/common/client.go
  • roxctl/common/client_test.go
  • sensor/common/centralproxy/authorizer.go
💤 Files with no reviewable changes (3)
  • pkg/telemetry/phonehome/interceptor.go
  • pkg/telemetry/phonehome/interceptor_test.go
  • pkg/telemetry/phonehome/request_params_test.go

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

🚀 Build Images Ready

Images are ready for commit 248b8a0. To use with deploy scripts:

export MAIN_IMAGE_TAG=4.11.x-566-g248b8a0427

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 4, 2026

Codecov Report

❌ Patch coverage is 96.09375% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.61%. Comparing base (5f687ce) to head (1b189f8).

Files with missing lines Patch % Lines
central/telemetry/centralclient/interceptors.go 42.85% 4 Missing ⚠️
roxctl/common/client.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                      @@
##           michael/fix-grpc-ua-order   #19828      +/-   ##
=============================================================
+ Coverage                      49.60%   49.61%   +0.01%     
=============================================================
  Files                           2763     2764       +1     
  Lines                         208335   208298      -37     
=============================================================
+ Hits                          103337   103349      +12     
+ Misses                         97331    97283      -48     
+ Partials                        7667     7666       -1     
Flag Coverage Δ
go-unit-tests 49.61% <96.09%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@parametalol parametalol changed the base branch from master to michael/fix-grpc-ua-order April 4, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant