-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
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));
}