Skip to content

feat: add upstream request/response logging for passthrough routes#186

Merged
pawbana merged 5 commits intomainfrom
pb/dump-passthrough
Mar 4, 2026
Merged

feat: add upstream request/response logging for passthrough routes#186
pawbana merged 5 commits intomainfrom
pb/dump-passthrough

Conversation

@pawbana
Copy link
Contributor

@pawbana pawbana commented Feb 19, 2026

Added request/response dump functionality for passthrough routes.

  • Added RoundTripperMiddleware in the apidump package.
  • Updated the Provider interface to expose the APIDumpDir method

Drive by change

  • Improved error handling in the dumper code

Copy link
Contributor Author

pawbana commented Feb 19, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@pawbana pawbana marked this pull request as ready for review February 19, 2026 16:21
@pawbana pawbana force-pushed the pb/dump-passthrough branch from 5e86692 to 63d2dcb Compare February 19, 2026 16:25
@pawbana pawbana requested a review from dannykopping February 19, 2026 16:35
if !bytes.HasSuffix(result, []byte("\n")) {
result = append(result, []byte("\n")...)
}
// Trim trailing newline added by pretty.Pretty.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was there a reson for removing trailing new line?
Viewing dumps in terminal without it is annoying.

@dannykopping
Copy link
Collaborator

I'm thinking we should perhaps hold off on this change until #166 lands. If we have a session ID then we can correlate all intercepted and passthrough routes.

If you'd rather land this in the interim and then add session ID correlation later, that's fine too - in which case I'll give this a review.

@pawbana
Copy link
Contributor Author

pawbana commented Feb 23, 2026

I'm thinking we should perhaps hold off on this change until #166 lands. If we have a session ID then we can correlate all intercepted and passthrough routes.

Will session ID be available in each request? Or only in requests from clients that set it?

@dannykopping
Copy link
Collaborator

I'm thinking we should perhaps hold off on this change until #166 lands. If we have a session ID then we can correlate all intercepted and passthrough routes.

Will session ID be available in each request? Or only in requests from clients that set it?

Only if clients set it

@pawbana
Copy link
Contributor Author

pawbana commented Feb 23, 2026

I'm thinking we should perhaps hold off on this change until #166 lands. If we have a session ID then we can correlate all intercepted and passthrough routes.

Will session ID be available in each request? Or only in requests from clients that set it?

Only if clients set it

If session ID is not always set I'm not sure what do you mean by session ID correlation. Traffic dumping (both intercepted or passed though) will need to work without session ID.

I agree that dump file names are unfortunate for pass though routes (file names contain random UUIDs, not related to anything) but I wanted to keep the same format. Maybe different approach would be better.
Now that I think about it maybe it would be helpful to add this UUID to Passthrough trace and PassthroughCount metric? Although I'm not sure if it matches original intention behind interception ID.

@pawbana pawbana requested a review from ssncferreira March 2, 2026 14:39
Copy link
Contributor

@ssncferreira ssncferreira left a comment

Choose a reason for hiding this comment

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

Nice addition 💪 this will be super nice for debugging. Just a few comments, but nothing blockin

// NewRoundTripperMiddleware returns http.RoundTripper that dumps requests and responses to files.
// If baseDir is empty, returns the original transport unchanged.
// Used for logging passed through requests.
func NewRoundTripperMiddleware(transport http.RoundTripper, baseDir string, provider string, logger slog.Logger, clk quartz.Clock) http.RoundTripper {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Since this is just for Passthrough routes, I would suggest renaming it:

Suggested change
func NewRoundTripperMiddleware(transport http.RoundTripper, baseDir string, provider string, logger slog.Logger, clk quartz.Clock) http.RoundTripper {
func NewPassthroughMiddleware(transport http.RoundTripper, baseDir string, provider string, logger slog.Logger, clk quartz.Clock) http.RoundTripper {

Similarly, rename NewMiddleware to NewBridgeMiddleware

// A random UUID is generated for the filename. "passthrough" is used as the directory name
// in place of the model.
func passthroughDumpPath(baseDir string, provider string, clk quartz.Clock) string {
return filepath.Join(baseDir, provider, "passthrough", fmt.Sprintf("%d-%s", clk.Now().UTC().UnixMilli(), uuid.New()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a randomly generated UUID here doesn't seem very useful 🤔 What if we use the sanitized route instead (e.g., /v1/models → v1-models) to make it easier to identify requests in the dump files. In order to avoid collisions, we can also adda short UUID suffix.

Comment on lines +267 to +270
result := body
if json.Valid(body) {
result = pretty.Pretty(body)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did this change? Since result is body, if json.Valid returns false we'll be appending a newline to it, couldn't this impact the caller's slice?

fmt.Fprintf(&buf, "\r\n")
_, err = fmt.Fprintf(&buf, "\r\n")
if err != nil {
return fmt.Errorf("write request body: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: not really the body, right?

Suggested change
return fmt.Errorf("write request body: %w", err)
return fmt.Errorf("write header terminator: %w", err)

}
_, err = fmt.Fprintf(&headerBuf, "\r\n")
if err != nil {
return fmt.Errorf("write response body: %w", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: same as above

Suggested change
return fmt.Errorf("write response body: %w", err)
return fmt.Errorf("write header terminator: %w", err)

fmt.Fprintf(w, "%s: %s\r\n", key, override)
_, err := fmt.Fprintf(w, "%s: %s\r\n", key, override)
if err != nil {
return fmt.Errorf("write response header override: %w", err)
Copy link
Contributor

@ssncferreira ssncferreira Mar 3, 2026

Choose a reason for hiding this comment

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

This function is used for both request/responses, right? Consider removing response from here, to avoid confusion. Same below 👀

Copy link
Collaborator

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

As discussed, we'll leave session indexing for later.

@pawbana pawbana force-pushed the pb/dump-passthrough branch from 36705e3 to 153f439 Compare March 3, 2026 13:26
@pawbana pawbana merged commit b84b1c3 into main Mar 4, 2026
5 checks passed
@pawbana pawbana linked an issue Mar 4, 2026 that may be closed by this pull request
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.

Dump passthrough requests/responses

3 participants