Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add Response Modifier Option #57

Closed
wants to merge 12 commits into from

Conversation

riandyrn
Copy link
Owner

@riandyrn riandyrn commented Aug 3, 2024

This PR offers a new option called WithResponseModifier, which allows end-users to modify the middleware's response.

So in the scenario mentioned in #56, the end user could create their own modifier just like demonstrated here.

In addition to adding the WithResponseModifier option, I also created a predefined modifier called ResponseModifierTraceIDResponseHeader. It allows the end user to show the trace ID in the response header along with its sampled status, catering to the use case I mentioned here.

@riandyrn riandyrn marked this pull request as ready for review August 3, 2024 13:02
@riandyrn
Copy link
Owner Author

riandyrn commented Aug 3, 2024

Hello, @costela

Let me know your feedback about this PR.

config.go Outdated
if opt.HeaderKeyFunc != nil {
headerKey = opt.HeaderKeyFunc()
if len(headerKey) == 0 {
panic("header key must not be empty")
Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to also add test for testing this behaviour

Copy link
Owner Author

Choose a reason for hiding this comment

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

Ok, I've added the test for this, @ilhamsyahids.

However, during the process, I realized it is better to verify the HeaderKeyFunc() during initialization since the end user will immediately know something is wrong (not waiting until there is an incoming request, just like now).

You can check the commit 0fb3a5e for details.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we move to initialization, I think we can move headerKey as well like this:

func ResponseModifierTraceIDResponseHeader(opt ResponseModifierTraceIDResponseHeaderOption) ResponseModifier {
	if err := opt.Validate(); err != nil {
		panic(err)
	}

	// moved headerKey here
	headerKey := DefaultTraceResponseHeaderKey
	if opt.HeaderKeyFunc != nil {
		headerKey = opt.HeaderKeyFunc()
	}

	return func(ctx context.Context, w http.ResponseWriter) {
		// remove headerKey

		...
	}
}

Copy link
Owner Author

Choose a reason for hiding this comment

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

Haha.., you are right, @ilhamsyahids! 😁

Ok, updated in 10e1b73. Please recheck.

Copy link
Collaborator

@ilhamsyahids ilhamsyahids left a comment

Choose a reason for hiding this comment

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

Hello @riandyrn, everything is looking good except for above minor request

Copy link
Collaborator

@ilhamsyahids ilhamsyahids left a comment

Choose a reason for hiding this comment

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

LGTM 🔥👍

Nice idea to handle different use cases!

Copy link
Contributor

@costela costela left a comment

Choose a reason for hiding this comment

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

I'm a bit concerned this may be too powerful a construct. Suddenly my middleware has multiple middlewares itself?

And, slightly confusingly, these "response modifiers" are actually called before my code responds?

Don't get me wrong: I think I understand the reasoning, and it would solve my problem, so I don't want to come across too harsh 😅, but it just feels a bit like shooting a fly with a missile.

@riandyrn
Copy link
Owner Author

riandyrn commented Aug 6, 2024

Hello, @costela

Suddenly my middleware has multiple middlewares itself?

Haha.., valid point. Sometimes I forget the end user can just write their own middleware. So essentially ResponseModifier is not necessary. 😂

Okay, if we return to the root problem, essentially, we want to inform the end user whether the trace is sampled or not. Otherwise, the end user will be misled into thinking the trace is always sampled.

However, at the same time, we want to also minimize unexpected changes on the user side.

So what do you think if we just do the following:

  1. Deprecate WithTraceIDResponseHeader(headerKeyFunc func() string) Option.
  2. Supersede it with WithTraceIDResponseHeaderConfigurable(cfg WithTraceIDResponseHeaderConfig) Option.
  3. We define HeaderKeyFunc(): string & IncludeSampledStatus: bool inside WithTraceIDResponseHeaderConfig.

By doing this we can achieve our main goal while minimizing the impact on the user side.

What do you think, @costela? Or maybe you have another perspective?

cc: @ilhamsyahids

@costela
Copy link
Contributor

costela commented Aug 6, 2024

Yes, that sounds cool!

However, I do wonder if we can't siplify it even further? E.g.: what's the concrete use-case for the HeaderKeyFunc? Since it takes no parameter, it can't really use any context to derive the header name? In that case there seems to be little gain compared to a string? OTOH, if we do want more configurability, than maybe we also need the context in that function?

How about something like this?

func WithTraceResponseHeaders(opts TraceResponseHeadersConfig) Option { /* ... */ }

type TraceHeaderConfig struct {
  TraceIDHeader string // if non-empty overrides the default of X-Trace-ID
  TraceSampledHeader string // if non-empty overrides the default of X-Trace-Sampled
}

If the consumer doesn't want to expose some of the headers, I don't think we need an extra bool: the consumer can drop them from the response directly themselves.

So we keep the API simple, provide the one thing the consumer cannot easily do by themselves and have a relatively clean path forward in case we need more headers.

WDYT?

@riandyrn
Copy link
Owner Author

riandyrn commented Aug 7, 2024

Nice, I like your suggestion, @costela! It is simple and hits all the marks.

Let us implement it your way. 👍

Would you like to create a new PR for it?

Also related to this:

func WithTraceResponseHeaders(opts TraceResponseHeadersConfig) Option { /* ... */ }

I think it's better to name the config cfg rather than opts.

@costela
Copy link
Contributor

costela commented Aug 9, 2024

Sorry I didn't get around to it yet. I'll reopen my previous MR and implement a first pass there! 👍

@costela
Copy link
Contributor

costela commented Aug 12, 2024

Unfortunately I could not reopen the original PR, so I opened #59

PTAL 🙏

@riandyrn
Copy link
Owner Author

Superseded by #59

@riandyrn riandyrn closed this Aug 19, 2024
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.

3 participants