< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 109
Coverable lines: 109
Total lines: 291
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 30
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)0%440%
SendAsync(...)0%660%
SendAsyncWithMetrics(...)0%220%
Dispose(...)0%220%
RequestStart(...)0%220%
RequestStop(...)0%10100%
CreateActiveRequestsTagKey(...)0%440%
.ctor()100%110%
Dispose(...)100%110%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Diagnostics;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Diagnostics.Metrics;
 9using System.Threading;
 10using System.Threading.Tasks;
 11
 12namespace System.Net.Http.Metrics
 13{
 14    /// <summary>
 15    /// Represents a unique combination of tags for tracking active requests.
 16    /// </summary>
 17    internal readonly struct ActiveRequestsTagKey : IEquatable<ActiveRequestsTagKey>
 18    {
 19        public readonly string? Scheme;
 20        public readonly string? Host;
 21        public readonly int Port;
 22        public readonly string Method;
 23        private readonly int _hashCode;
 24
 25        public ActiveRequestsTagKey(string? scheme, string? host, int port, string method)
 26        {
 27            Scheme = scheme;
 28            Host = host;
 29            Port = port;
 30            Method = method;
 31            _hashCode = HashCode.Combine(scheme, host, port, method);
 32        }
 33
 34        public bool Equals(ActiveRequestsTagKey other) =>
 35            Scheme == other.Scheme &&
 36            Host == other.Host &&
 37            Port == other.Port &&
 38            Method == other.Method;
 39
 40        public override bool Equals(object? obj) => obj is ActiveRequestsTagKey other && Equals(other);
 41
 42        public override int GetHashCode() => _hashCode;
 43
 44        public TagList ToTagList()
 45        {
 46            TagList tags = default;
 47            if (Scheme is not null)
 48            {
 49                tags.Add("url.scheme", Scheme);
 50                tags.Add("server.address", Host);
 51                tags.Add("server.port", DiagnosticsHelper.GetBoxedInt32(Port));
 52            }
 53            tags.Add("http.request.method", Method);
 54            return tags;
 55        }
 56
 57        public override string ToString() =>
 58            $"{Method}{(Scheme is not null ? $" {Scheme}://{Host}:{Port}" : "")}";
 59    }
 60
 61    /// <summary>
 62    /// Thread-safe tracker for active request counts by tag combination.
 63    /// </summary>
 64    internal sealed class ActiveRequestsTracker
 65    {
 66        private readonly ConcurrentDictionary<ActiveRequestsTagKey, long> _counts = new();
 67
 68        /// <summary>
 69        /// Increments the count for the specified tag combination.
 70        /// </summary>
 71        public void Increment(in ActiveRequestsTagKey key)
 72        {
 73            _counts.AddOrUpdate(key, 1, static (_, currentValue) => currentValue + 1);
 74        }
 75
 76        /// <summary>
 77        /// Decrements the count for the specified tag combination.
 78        /// Removes the entry if the count reaches zero.
 79        /// </summary>
 80        public void Decrement(in ActiveRequestsTagKey key)
 81        {
 82            // We need to atomically decrement and remove if zero.
 83            // Use a spin loop with TryGetValue/TryUpdate/TryRemove to handle this safely.
 84            while (true)
 85            {
 86                if (!_counts.TryGetValue(key, out long currentValue))
 87                {
 88                    // Key doesn't exist, nothing to decrement.
 89                    // This shouldn't happen in normal operation but we handle it gracefully.
 90                    Debug.Fail($"Decrement for non-existing request {key}");
 91                    return;
 92                }
 93
 94                if (currentValue <= 1)
 95                {
 96                    // Try to remove the entry since it will become zero.
 97                    // Use the overload that checks the current value to ensure atomicity.
 98                    if (_counts.TryRemove(new KeyValuePair<ActiveRequestsTagKey, long>(key, currentValue)))
 99                    {
 100                        return;
 101                    }
 102                    // Another thread modified the value, retry.
 103                }
 104                else
 105                {
 106                    // Try to decrement the value.
 107                    if (_counts.TryUpdate(key, currentValue - 1, currentValue))
 108                    {
 109                        return;
 110                    }
 111                    // Another thread modified the value, retry.
 112                }
 113            }
 114        }
 115
 116        /// <summary>
 117        /// Returns measurements for all tag combinations with non-zero counts.
 118        /// </summary>
 119        public IEnumerable<Measurement<long>> GetMeasurements()
 120        {
 121            foreach (KeyValuePair<ActiveRequestsTagKey, long> entry in _counts)
 122            {
 123                yield return new Measurement<long>(entry.Value, entry.Key.ToTagList());
 124            }
 125        }
 126    }
 127
 128    internal sealed class MetricsHandler : HttpMessageHandlerStage
 129    {
 130        private readonly HttpMessageHandler _innerHandler;
 0131        private readonly ActiveRequestsTracker _activeRequestsTracker = new();
 132        private readonly ObservableUpDownCounter<long> _activeRequests;
 133        private readonly Histogram<double> _requestsDuration;
 134        private readonly IWebProxy? _proxy;
 135
 0136        public MetricsHandler(HttpMessageHandler innerHandler, IMeterFactory? meterFactory, IWebProxy? proxy, out Meter 
 0137        {
 0138            Debug.Assert(GlobalHttpSettings.MetricsHandler.IsGloballyEnabled);
 139
 0140            _innerHandler = innerHandler;
 0141            _proxy = proxy;
 142
 0143            meter = meterFactory?.Create("System.Net.Http") ?? SharedMeter.Instance;
 144
 145            // Meter has a cache for the instruments it owns
 0146            _activeRequests = meter.CreateObservableUpDownCounter<long>(
 0147                "http.client.active_requests",
 0148                observeValues: _activeRequestsTracker.GetMeasurements,
 0149                unit: "{request}",
 0150                description: "Number of outbound HTTP requests that are currently active on the client.");
 0151            _requestsDuration = meter.CreateHistogram<double>(
 0152                "http.client.request.duration",
 0153                unit: "s",
 0154                description: "Duration of HTTP client requests.",
 0155                advice: DiagnosticsHelper.ShortHistogramAdvice);
 0156        }
 157
 158        internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationT
 0159        {
 0160            if (_activeRequests.Enabled || _requestsDuration.Enabled)
 0161            {
 0162                return SendAsyncWithMetrics(request, async, cancellationToken);
 163            }
 164            else
 0165            {
 0166                return async ?
 0167                    new ValueTask<HttpResponseMessage>(_innerHandler.SendAsync(request, cancellationToken)) :
 0168                    new ValueTask<HttpResponseMessage>(_innerHandler.Send(request, cancellationToken));
 169            }
 0170        }
 171
 172        private async ValueTask<HttpResponseMessage> SendAsyncWithMetrics(HttpRequestMessage request, bool async, Cancel
 0173        {
 0174            Debug.Assert(GlobalHttpSettings.MetricsHandler.IsGloballyEnabled);
 175
 0176            (long startTimestamp, bool recordCurrentRequests, ActiveRequestsTagKey requestTagKey) = RequestStart(request
 0177            HttpResponseMessage? response = null;
 0178            Exception? exception = null;
 179            try
 0180            {
 0181                response = async ?
 0182                    await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false) :
 0183                    _innerHandler.Send(request, cancellationToken);
 0184                return response;
 185            }
 0186            catch (Exception ex)
 0187            {
 0188                exception = ex;
 0189                throw;
 190            }
 191            finally
 0192            {
 0193                RequestStop(request, response, exception, startTimestamp, recordCurrentRequests, requestTagKey);
 0194            }
 0195        }
 196
 197        protected override void Dispose(bool disposing)
 0198        {
 0199            if (disposing)
 0200            {
 0201                _innerHandler.Dispose();
 0202            }
 203
 0204            base.Dispose(disposing);
 0205        }
 206
 207        private (long StartTimestamp, bool RecordCurrentRequests, ActiveRequestsTagKey RequestTagKey) RequestStart(HttpR
 0208        {
 0209            bool recordCurrentRequests = _activeRequests.Enabled;
 0210            long startTimestamp = Stopwatch.GetTimestamp();
 211
 0212            ActiveRequestsTagKey requestTagKey = CreateActiveRequestsTagKey(request);
 0213            if (recordCurrentRequests)
 0214            {
 0215                _activeRequestsTracker.Increment(requestTagKey);
 0216            }
 217
 0218            return (startTimestamp, recordCurrentRequests, requestTagKey);
 0219        }
 220
 221        private void RequestStop(HttpRequestMessage request, HttpResponseMessage? response, Exception? exception, long s
 0222        {
 0223            if (recordCurrentRequests)
 0224            {
 0225                _activeRequestsTracker.Decrement(requestTagKey);
 0226            }
 227
 0228            if (!_requestsDuration.Enabled)
 0229            {
 0230                return;
 231            }
 232
 0233            TagList tags = requestTagKey.ToTagList();
 0234            if (response is not null)
 0235            {
 0236                tags.Add("http.response.status_code", DiagnosticsHelper.GetBoxedInt32((int)response.StatusCode));
 0237                tags.Add("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
 0238            }
 239
 0240            if (DiagnosticsHelper.TryGetErrorType(response, exception, out string? errorType))
 0241            {
 0242                tags.Add("error.type", errorType);
 0243            }
 244
 0245            TimeSpan durationTime = Stopwatch.GetElapsedTime(startTimestamp, Stopwatch.GetTimestamp());
 246
 0247            List<Action<HttpMetricsEnrichmentContext>>? callbacks = HttpMetricsEnrichmentContext.GetEnrichmentCallbacksF
 0248            if (callbacks is null)
 0249            {
 0250                _requestsDuration.Record(durationTime.TotalSeconds, tags);
 0251            }
 252            else
 0253            {
 0254                HttpMetricsEnrichmentContext.RecordDurationWithEnrichment(callbacks, request, response, exception, durat
 0255            }
 0256        }
 257
 258        private ActiveRequestsTagKey CreateActiveRequestsTagKey(HttpRequestMessage request)
 0259        {
 0260            string? scheme = null;
 0261            string? host = null;
 0262            int port = 0;
 263
 0264            if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri)
 0265            {
 0266                scheme = requestUri.Scheme;
 0267                host = DiagnosticsHelper.GetServerAddress(request, _proxy);
 0268                port = requestUri.Port;
 0269            }
 270
 0271            string method = (string)DiagnosticsHelper.GetMethodTag(request.Method, out _).Value!;
 272
 0273            return new ActiveRequestsTagKey(scheme, host, port, method);
 0274        }
 275
 276        private sealed class SharedMeter : Meter
 277        {
 0278            public static Meter Instance { get; } = new SharedMeter();
 279            private SharedMeter()
 0280                : base("System.Net.Http")
 0281            {
 0282            }
 283
 284            protected override void Dispose(bool disposing)
 0285            {
 286                // NOP to prevent disposing the global instance from MeterListener callbacks.
 0287            }
 288        }
 289    }
 290}
 291