Skip to content

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

@antonfirsov

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));
}

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions