Skip to content

[API Proposal]: pass custom tags for metrics enrichment in HttpClientHandler #86281

Closed

Description

@antonfirsov

Latest update: switch to callback-based API to observe HttpResponseMessage for enrichment. Starting a new round of API discussion.

Background and motivation

As part of #84978, we need to enable enriching metrics logs with additional custom tags injected by the application/middleware. These tags need to be injected on per-request basis meaning that they should be provided in HttpRequestMessage. Enrichment is an advanced scenario, and metrics will be only implemented in HttpClientHandler/SocketsHttpHandler, therefore the value of exposing a property on HttpRequestMessage would be questionable. Instead, we can pass the custom tags using HttpRequestMessage.Options. The problem is that there are no good established practices to document and it's hard to discover discover it. Defining a key alone would also leave the door open to mistakes. As a compromise, I'm proposing an API that works via an extension method over HttpRequestOptions using an internal key.

The API should provide an ability inject tags based on HttpResponseMessage. Since the metrics implementation will live within HttpClientHandler/SocketsHttpHandler, it would be too late to do this in a DelegatingHandler. To handle this, the proposed API is passing user-provided callbacks to the metrics implementation which can the custom tags to be added for enrichment.

API Proposal

namespace System.Net.Http.Metrics;

public sealed class HttpMetricsEnrichmentContext
{
    public HttpRequestMessage Request { get; }
    public HttpResponseMessage? Response { get; } // null when an exception is thrown by the innermost handler
    public Exception? Exception { get; } // null when no exception is thrown
    public void AddCustomTag(string name, object? value);
    // Alternatively we can expose a collection to allow enumerating, removing in the future, but such functionality is not needed today.
    // public ICollection<KeyValuePair<string, object?>> CustomTags { get; }
}

// Or should it be MetricsHttpRequestOptionsExtensions? 
// runtime/libraries is not consistent on this, see HttpClientJsonExtensions vs  OptionsServiceCollectionExtensions
public static class HttpRequestOptionsMetricsExtensions 
{
    public static void AddMetricsEnrichmentCallback(this HttpRequestOptions options, Action<HttpMetricsEnrichmentContext> callback));
}

API Usage

public class EnricherHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Options.AddMetricsEnrichmentCallback(
            static ctx => {
                ctx.AddCustomTag("org-name", ctx.Request.Headers.GetValue("x-org-name"));
                ctx.AddCustomTag("result-category", GetResultCategory(ctx.Response.StatusCode));
            }
        );
        return await base.SendAsync(request, cancellationToken);
    }

    private static string GetResultCategory(HttpStatusCode statusCode) => statusCode >= 200 && statusCode < 300 ? "Good" : "Bad";
}

Alternative Designs

Attempt to reduce heap allocations by working with value types

Note that the prerequisite for such an attempt to be successful is to resolve #83701, otherwise TagList would allocate in almost all R9 scenarios.

namespace System.Net.Http.Metrics;

public readonly struct HttpMetricsEnrichmentContext
{
    public readonly HttpRequestMessage Request { get; }
    public readonly HttpResponseMessage? Response { get; }
    public readonly Exception? Exception { get; }
}

public delegate void HttpMetricsEnrichmentCallback(in HttpMetricsEnrichmentContext context, ref TagList customTags);

public static class HttpRequestOptionsMetricsExtensions
{
    public static void AddMetricsEnrichmentCallback(this HttpRequestOptions options, HttpMetricsEnrichmentCallback callback));
}

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on May 15, 2023
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on May 15, 2023
  3. antonfirsov commented on May 15, 2023

    @antonfirsov
    ContributorAuthor

    @noahfalk @JamesNK any thoughts? Where would usages of CustomMetricsTags live? Only R9 /other middleware or do we expect it to be directly used by customer applications?

  4. JamesNK commented on May 17, 2023

    @JamesNK
    Member

    Typo in the proposal. I'm guessing public ICollection<string, object?> CustomMetricsTags { get; } is wrong and you mean ICollection<KeyValuePair<string, object?>>

    @noahfalk @JamesNK any thoughts? Where would usages of CustomMetricsTags live? Only R9 /other middleware or do we expect it to be directly used by customer applications?

    Adding custom metrics would be something for power users. Potential users:

    • Devs who are writing an SDK and wish to add extra metadata such as the request's route template, e.g. /products/{id}. This is likely added while creating the HttpRequestMessage.
    • Microsoft.Extensions.Http integration. This will probably be in the form of an interface + DelegatingHandler. The interface would be configured for a client on the client factory. The interface would then be called by the delegating handler during the handler pipeline. The result would be stored on the request's tags collection.
    • Libraries like R9 that want to add metadata to request metrics that are important for the company. This could come from DelegatingHandler. Because the handler pipeline for all HttpClient uses can be configured by a library, we'd want to test that someone can hook into the HTTP DiagnosticsSource events and add tags from there.
  5. MihaZupan commented on May 22, 2023

    @MihaZupan
    Member

    Moving to 8.0 to track this as part of #84978, feel free to bump back to Future

  6. removed
    untriagedNew issue has not been triaged by the area owner
    on May 22, 2023
  7. added this to the 8.0.0 milestone on May 22, 2023
  8. antonfirsov commented on May 24, 2023

    @antonfirsov
    ContributorAuthor
  9. noahfalk commented on May 26, 2023

    @noahfalk
    Member

    I agree with James that the scenario for arbitrary key/value pairs is aimed at power users and SDK/library authors, not the general audience that uses HttpClient. I don't think it needs to be overtly discoverable as a top-level property would be. Defining a convention for Options key name could work for R9, though it feels quite hard to discover for anyone else and leaves the door open for mistakes or weird incompatibilities like different library authors assuming they can store their own uniquely typed ICollection in there. I think a nice middle ground would be defining either a property or an extension method on the HttpRequestOptions.

    It looks like code elsewhere assumes that HttpRequestOptions can be reliably copied by enumerating and copying all the dictionary keys (https://source.dot.net/#Microsoft.AspNetCore.Mvc.Testing/Handlers/RedirectHandler.cs,160) so unless you planned to change that (including where user code might do the same thing) then I think we'd want the property backed by a dictionary entry rather than a field.

    I think the suggestion is close to your option (2) above but I am suggesting that the property/extension method be defined on the Options type, not on HttpRequestMessage to reduce its visibility. If the implementation of MetricsHandler is going to be in System.Net.Http then I'd put the API there too, not in M.E.Http, because I don't see any benefit in limiting it that way.

    All of this is just a suggestion though. If networking team or BCL design review believe one of the other options is better then go for it. I'll sleep fine with any of options you proposed. Hope that helps!

  10. antonfirsov commented on May 28, 2023

    @antonfirsov
    ContributorAuthor

    a nice middle ground would be defining either a property or an extension method on the HttpRequestOptions

    I had a same thought recently. Updated the proposal, thanks for the feedback!

  11. changed the title [-][API Proposal]: HttpRequestMessage.CustomMetricsTags[/-] [+][API Proposal]: pass custom tags for metrics enrichment in HttpClientHandler[/+] on May 28, 2023
  12. antonfirsov commented on Jun 1, 2023

    @antonfirsov
    ContributorAuthor

    One concern that came up in todays discussion that the cost of using HttpRequestOptions (enforcing the dictionary allocation) may end up being significant.

    @noahfalk @JamesNK @dpk83 do you have any insights whether R9 or other middleware use-cases would come with enrichment handlers wired-in by default or in the majority of use-cases?

  13. 20 remaining items

  14. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Jun 29, 2023
  15. JamesNK commented on Jul 3, 2023

    @JamesNK
    Member
    public static void AddCallback(HttpRequestOptions options, Action<HttpMetricsEnrichmentContext> callback);

    The add callback method is no longer an extension method. Part of the reason to choose HttpRequestOptions as the this argument for it to hang off was to hide it away. That's no longer a concern.

    I propose a small change to make the AddCallback method take HttpRequestMessage as an argument instead of HttpRequestOptions:

    public static void AddCallback(HttpRequestMessage message, Action<HttpMetricsEnrichmentContext> callback);

    Reasons:

    1. It's more user-friendly. HttpRequestMessage makes it slightly more obvious what to pass to the method (more people are familiar with HttpRequestMessage than HttpRequestOptions)
    2. In the future, if you decide to change how callbacks are stored, such as to a field on the request message, it is easy to do because you already have the HttpRequestMessage reference.
    3. Related to the point above, accessing HttpRequestMessage.Options automatically allocates the collection if it wasn't already present. There would be no way to ever optimize that away with the current shape.
  16. stephentoub commented on Jul 3, 2023

    @stephentoub
    Member

    +1 to James' comment

  17. added
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    and removed
    api-approvedAPI was approved in API review, it can be implemented
    on Jul 4, 2023
  18. JamesNK commented on Jul 4, 2023

    @JamesNK
    Member

    Removing API approved tag and putting back api-ready-for-review. Change to consider: #86281 (comment)

    I don't know the runtime API review system. Hopefully this is the right way to do it.

    This isn't urgent. We can proceed with the current approved API and easily change it based on feedback. As long as it is considered before .NET 8 locks down.

  19. karelz commented on Jul 4, 2023

    @karelz
    Member

    @antonfirsov perhaps we can do the incremental change review over email?
    Implementation should be unblocked per @JamesNK as the difference is apparently very small.

  20. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Jul 4, 2023
  21. antonfirsov commented on Jul 4, 2023

    @antonfirsov
    ContributorAuthor

    Email sent. If it doesn't work out, I will open a new issue, since I don't want #87319 to be blocked on this.

  22. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Jul 9, 2023
  23. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Jul 13, 2023
  24. ghost locked as resolved and limited conversation to collaborators on Aug 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions