Skip to content

Commit

Permalink
Update to latest (#4026)
Browse files Browse the repository at this point in the history
Co-authored-by: Martin Taillefer <[email protected]>
  • Loading branch information
geeknoid and Martin Taillefer authored Jun 2, 2023
1 parent 07e88b2 commit 8967a24
Show file tree
Hide file tree
Showing 15 changed files with 1,127 additions and 125 deletions.
1 change: 1 addition & 0 deletions eng/Packages/General.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.0.1" />
<PackageVersion Include="Microsoft.Extensions.DiagnosticAdapter" Version="3.1.32" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.0" />
<PackageVersion Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
<PackageVersion Include="Microsoft.ServiceFabric.Services" Version="4.2.434" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Http.Telemetry.Metering;

internal static class HelperExtensions
{
internal static HttpStatusCode GetStatusCode(this Exception ex)
{
if (ex is TaskCanceledException)
{
return HttpStatusCode.GatewayTimeout;
}
else if (ex is HttpRequestException exception)
{
#if NET5_0_OR_GREATER
return exception.StatusCode ?? HttpStatusCode.ServiceUnavailable;
#else
return HttpStatusCode.ServiceUnavailable;
#endif
}
else
{
return HttpStatusCode.InternalServerError;
}
}

/// <summary>
/// Classifies the result of the HTTP response.
/// </summary>
/// <param name="statusCode">An <see cref="HttpStatusCode"/> to categorize the status code of HTTP response.</param>
/// <returns><see cref="HttpRequestResultType"/> type of HTTP response and its <see cref="HttpStatusCode"/>.</returns>
internal static HttpRequestResultType GetResultCategory(this HttpStatusCode statusCode)
{
if (statusCode >= HttpStatusCode.Continue && statusCode < HttpStatusCode.BadRequest)
{
return HttpRequestResultType.Success;
}
else if (statusCode >= HttpStatusCode.BadRequest && statusCode < HttpStatusCode.InternalServerError)
{
return HttpRequestResultType.ExpectedFailure;
}

return HttpRequestResultType.Failure;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
#if !NETFRAMEWORK
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Http.Telemetry.Metering.Internal;
#endif
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Telemetry;
using Microsoft.Extensions.Telemetry.Internal;
using Microsoft.Extensions.Telemetry.Metering;
using Microsoft.Shared.Collections;
Expand All @@ -18,11 +22,50 @@ namespace Microsoft.Extensions.Http.Telemetry.Metering;
/// <seealso cref="DelegatingHandler" />
public static class HttpClientMeteringExtensions
{
#if !NETFRAMEWORK
/// <summary>
/// Adds a <see cref="DelegatingHandler" /> to collect and emit metrics for outgoing requests from all http clients.
/// Adds Http client diagnostics listener to capture metrics for requests from all http clients.
/// </summary>
/// <remarks>
/// This extension configures outgoing request metrics auto collection globally for all http clients.
/// This extension configures outgoing request metrics auto collection
/// globally for all http clients regardless of how the http client is created.
/// </remarks>
/// <param name="services">The <see cref="IServiceCollection" />.</param>
/// <returns>
/// <see cref="IServiceCollection" /> instance for chaining.
/// </returns>
[Experimental]
public static IServiceCollection AddHttpClientMeteringForAllHttpClients(this IServiceCollection services)
{
_ = Throw.IfNull(services);

_ = services
.RegisterMetering()
.AddOutgoingRequestContext();

services.TryAddSingleton(sp =>
{
var meter = sp.GetRequiredService<Meter<HttpMeteringHandler>>();
var outgoingRequestMetricEnrichers = sp.GetService<IEnumerable<IOutgoingRequestMetricEnricher>>().EmptyIfNull();
var requestMetadataContext = sp.GetService<IOutgoingRequestContext>();
var downstreamDependencyMetadataManager = sp.GetService<IDownstreamDependencyMetadataManager>();
return new HttpMeteringHandler(meter, outgoingRequestMetricEnrichers, requestMetadataContext, downstreamDependencyMetadataManager);
});

services.TryAddSingleton<HttpClientRequestAdapter>();
services.TryAddSingleton<HttpClientDiagnosticObserver>();
services.TryAddActivatedSingleton<HttpClientMeteringListener>();

return services;
}
#endif

/// <summary>
/// Adds a <see cref="DelegatingHandler" /> to collect and emit metrics for outgoing requests from all http clients created using IHttpClientFactory.
/// </summary>
/// <remarks>
/// This extension configures outgoing request metrics auto collection
/// for all http clients created using IHttpClientFactory.
/// </remarks>
/// <param name="services">The <see cref="IServiceCollection" />.</param>
/// <returns>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ namespace Microsoft.Extensions.Http.Telemetry.Metering;
/// <seealso cref="DelegatingHandler" />
public class HttpMeteringHandler : DelegatingHandler
{
#pragma warning disable CA2213 // Disposable fields should be disposed
internal readonly Meter<HttpMeteringHandler> Meter;
#pragma warning restore CA2213 // Disposable fields should be disposed
internal TimeProvider TimeProvider = TimeProvider.System;

private const int StandardDimensionsCount = 4;
private const int StandardDimensionsCount = 5;
private const int MaxCustomDimensionsCount = 14;

private static readonly RequestMetadata _fallbackMetadata = new();
Expand Down Expand Up @@ -60,7 +63,7 @@ internal HttpMeteringHandler(
IOutgoingRequestContext? requestMetadataContext,
IDownstreamDependencyMetadataManager? downstreamDependencyMetadataManager = null)
{
_ = Throw.IfNull(meter);
Meter = Throw.IfNull(meter);
_ = Throw.IfNull(enrichers);

_requestEnrichers = enrichers.ToArray();
Expand All @@ -84,7 +87,8 @@ internal HttpMeteringHandler(
Metric.ReqHost,
Metric.DependencyName,
Metric.ReqName,
Metric.RspResultCode
Metric.RspResultCode,
Metric.RspResultCategory
};

for (int i = 0; i < _requestEnrichers.Length; i++)
Expand All @@ -104,39 +108,12 @@ internal HttpMeteringHandler(
_downstreamDependencyMetadataManager = downstreamDependencyMetadataManager;
}

internal static string GetHostName(HttpRequestMessage request) => string.IsNullOrWhiteSpace(request.RequestUri?.Host) ? TelemetryConstants.Unknown : request.RequestUri!.Host;
internal static string GetHostName(HttpRequestMessage request)
=> string.IsNullOrWhiteSpace(request.RequestUri?.Host)
? TelemetryConstants.Unknown
: request.RequestUri!.Host;

/// <summary>
/// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.
/// </summary>
/// <param name="request">The HTTP request message to send to the server.</param>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
/// <returns>
/// The task object representing the asynchronous operation.
/// </returns>
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
_ = Throw.IfNull(request);

var timestamp = TimeProvider.GetTimestamp();

try
{
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
OnRequestEnd(request, timestamp, response.StatusCode);
return response;
}
catch
{
// This will not catch a response that returns 4xx, 5xx, etc. but will only catch when base.SendAsync() fails.
OnRequestEnd(request, timestamp, HttpStatusCode.InternalServerError);
throw;
}
}

private void OnRequestEnd(HttpRequestMessage request, long timestamp, HttpStatusCode statusCode)
internal void OnRequestEnd(HttpRequestMessage request, long elapsedMilliseconds, HttpStatusCode statusCode)
{
var requestMetadata = request.GetRequestMetadata() ??
_requestMetadataContext?.RequestMetadata ??
Expand All @@ -145,15 +122,16 @@ private void OnRequestEnd(HttpRequestMessage request, long timestamp, HttpStatus
var dependencyName = requestMetadata.DependencyName;
var requestName = $"{request.Method} {requestMetadata.GetRequestName()}";
var hostName = GetHostName(request);
var duration = (long)TimeProvider.GetElapsedTime(timestamp, TimeProvider.GetTimestamp()).TotalMilliseconds;

var duration = elapsedMilliseconds;
var result = statusCode.GetResultCategory();
var tagList = new TagList
{
new(Metric.ReqHost, hostName),
new(Metric.DependencyName, dependencyName),
new(Metric.ReqName, requestName),
new(Metric.RspResultCode, (int)statusCode)
};
new(Metric.RspResultCode, (int)statusCode),
new(Metric.RspResultCategory, result.ToInvariantString())
};

// keep default case fast by avoiding allocations
if (_enrichersCount == 0)
Expand Down Expand Up @@ -183,4 +161,44 @@ private void OnRequestEnd(HttpRequestMessage request, long timestamp, HttpStatus
}
}
}

/// <summary>
/// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation.
/// </summary>
/// <param name="request">The HTTP request message to send to the server.</param>
/// <param name="cancellationToken">A cancellation token to cancel operation.</param>
/// <returns>
/// The task object representing the asynchronous operation.
/// </returns>
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
_ = Throw.IfNull(request);

#if !NETFRAMEWORK
if (HttpClientMeteringListener.UsingDiagnosticsSource)
{
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
#endif

var start = TimeProvider.GetTimestamp();

try
{
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
var elapsed = TimeProvider.GetTimestamp() - start;
long ms = (long)Math.Round(((double)elapsed / TimeProvider.System.TimestampFrequency) * 1000);
OnRequestEnd(request, ms, response.StatusCode);
return response;
}
catch (Exception ex)
{
var elapsed = TimeProvider.GetTimestamp() - start;
long ms = (long)Math.Round(((double)elapsed / TimeProvider.System.TimestampFrequency) * 1000);
OnRequestEnd(request, ms, ex.GetStatusCode());
throw;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.EnumStrings;

namespace Microsoft.Extensions.Http.Telemetry.Metering;

/// <summary>
/// Statuses for classifying http request result.
/// </summary>
[Experimental]
[EnumStrings]
public enum HttpRequestResultType
{
/// <summary>
/// The status code of the http request indicates that the request is successful.
/// </summary>
Success,

/// <summary>
/// The status code of the http request indicates that this request did not succeed and to be treated as failure.
/// </summary>
Failure,

/// <summary>
/// The status code of the http request indicates that the request did not succeed but has failed with an error which is expected and acceptable for this request.
/// </summary>
/// <remarks>
/// Expected failures are generally excluded from availability calculations i.e. they are neither
/// treated as success nor as failures for availability calculation.
/// </remarks>
ExpectedFailure,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if !NETFRAMEWORK
using System;
using System.Diagnostics;

namespace Microsoft.Extensions.Http.Telemetry.Metering.Internal;

internal sealed class HttpClientDiagnosticObserver : IObserver<DiagnosticListener>, IDisposable
{
private const string ListenerName = "HttpHandlerDiagnosticListener";
private readonly HttpClientRequestAdapter _httpClientRequestAdapter;
private IDisposable? _observer;

public HttpClientDiagnosticObserver(HttpClientRequestAdapter httpClientRequestAdapter)
{
_httpClientRequestAdapter = httpClientRequestAdapter;
}

public void OnNext(DiagnosticListener value)
{
if (value.Name == ListenerName)
{
_observer?.Dispose();
_observer = value.SubscribeWithAdapter(_httpClientRequestAdapter);
}
}

public void OnCompleted()
{
// Method intentionally left empty.
}

public void OnError(Exception error)
{
// Method intentionally left empty.
}

public void Dispose()
{
_observer?.Dispose();
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if !NETFRAMEWORK
using System;
using System.Diagnostics;

namespace Microsoft.Extensions.Http.Telemetry.Metering.Internal;

internal sealed class HttpClientMeteringListener : IDisposable
{
internal static bool UsingDiagnosticsSource { get; set; }

private readonly IDisposable _listener;

public HttpClientMeteringListener(HttpClientDiagnosticObserver httpClientDiagnosticObserver)
{
UsingDiagnosticsSource = true;
_listener = DiagnosticListener.AllListeners.Subscribe(httpClientDiagnosticObserver);
}

public void Dispose()
{
_listener.Dispose();
}
}
#endif
Loading

0 comments on commit 8967a24

Please sign in to comment.