< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 25
Coverable lines: 25
Total lines: 291
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 12
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(...)100%110%
Equals(...)0%660%
Equals(...)0%220%
GetHashCode()100%110%
ToTagList()0%220%
ToString()0%220%

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)
 026        {
 027            Scheme = scheme;
 028            Host = host;
 029            Port = port;
 030            Method = method;
 031            _hashCode = HashCode.Combine(scheme, host, port, method);
 032        }
 33
 34        public bool Equals(ActiveRequestsTagKey other) =>
 035            Scheme == other.Scheme &&
 036            Host == other.Host &&
 037            Port == other.Port &&
 038            Method == other.Method;
 39
 040        public override bool Equals(object? obj) => obj is ActiveRequestsTagKey other && Equals(other);
 41
 042        public override int GetHashCode() => _hashCode;
 43
 44        public TagList ToTagList()
 045        {
 046            TagList tags = default;
 047            if (Scheme is not null)
 048            {
 049                tags.Add("url.scheme", Scheme);
 050                tags.Add("server.address", Host);
 051                tags.Add("server.port", DiagnosticsHelper.GetBoxedInt32(Port));
 052            }
 053            tags.Add("http.request.method", Method);
 054            return tags;
 055        }
 56
 57        public override string ToString() =>
 058            $"{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;
 131        private readonly ActiveRequestsTracker _activeRequestsTracker = new();
 132        private readonly ObservableUpDownCounter<long> _activeRequests;
 133        private readonly Histogram<double> _requestsDuration;
 134        private readonly IWebProxy? _proxy;
 135
 136        public MetricsHandler(HttpMessageHandler innerHandler, IMeterFactory? meterFactory, IWebProxy? proxy, out Meter 
 137        {
 138            Debug.Assert(GlobalHttpSettings.MetricsHandler.IsGloballyEnabled);
 139
 140            _innerHandler = innerHandler;
 141            _proxy = proxy;
 142
 143            meter = meterFactory?.Create("System.Net.Http") ?? SharedMeter.Instance;
 144
 145            // Meter has a cache for the instruments it owns
 146            _activeRequests = meter.CreateObservableUpDownCounter<long>(
 147                "http.client.active_requests",
 148                observeValues: _activeRequestsTracker.GetMeasurements,
 149                unit: "{request}",
 150                description: "Number of outbound HTTP requests that are currently active on the client.");
 151            _requestsDuration = meter.CreateHistogram<double>(
 152                "http.client.request.duration",
 153                unit: "s",
 154                description: "Duration of HTTP client requests.",
 155                advice: DiagnosticsHelper.ShortHistogramAdvice);
 156        }
 157
 158        internal override ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationT
 159        {
 160            if (_activeRequests.Enabled || _requestsDuration.Enabled)
 161            {
 162                return SendAsyncWithMetrics(request, async, cancellationToken);
 163            }
 164            else
 165            {
 166                return async ?
 167                    new ValueTask<HttpResponseMessage>(_innerHandler.SendAsync(request, cancellationToken)) :
 168                    new ValueTask<HttpResponseMessage>(_innerHandler.Send(request, cancellationToken));
 169            }
 170        }
 171
 172        private async ValueTask<HttpResponseMessage> SendAsyncWithMetrics(HttpRequestMessage request, bool async, Cancel
 173        {
 174            Debug.Assert(GlobalHttpSettings.MetricsHandler.IsGloballyEnabled);
 175
 176            (long startTimestamp, bool recordCurrentRequests, ActiveRequestsTagKey requestTagKey) = RequestStart(request
 177            HttpResponseMessage? response = null;
 178            Exception? exception = null;
 179            try
 180            {
 181                response = async ?
 182                    await _innerHandler.SendAsync(request, cancellationToken).ConfigureAwait(false) :
 183                    _innerHandler.Send(request, cancellationToken);
 184                return response;
 185            }
 186            catch (Exception ex)
 187            {
 188                exception = ex;
 189                throw;
 190            }
 191            finally
 192            {
 193                RequestStop(request, response, exception, startTimestamp, recordCurrentRequests, requestTagKey);
 194            }
 195        }
 196
 197        protected override void Dispose(bool disposing)
 198        {
 199            if (disposing)
 200            {
 201                _innerHandler.Dispose();
 202            }
 203
 204            base.Dispose(disposing);
 205        }
 206
 207        private (long StartTimestamp, bool RecordCurrentRequests, ActiveRequestsTagKey RequestTagKey) RequestStart(HttpR
 208        {
 209            bool recordCurrentRequests = _activeRequests.Enabled;
 210            long startTimestamp = Stopwatch.GetTimestamp();
 211
 212            ActiveRequestsTagKey requestTagKey = CreateActiveRequestsTagKey(request);
 213            if (recordCurrentRequests)
 214            {
 215                _activeRequestsTracker.Increment(requestTagKey);
 216            }
 217
 218            return (startTimestamp, recordCurrentRequests, requestTagKey);
 219        }
 220
 221        private void RequestStop(HttpRequestMessage request, HttpResponseMessage? response, Exception? exception, long s
 222        {
 223            if (recordCurrentRequests)
 224            {
 225                _activeRequestsTracker.Decrement(requestTagKey);
 226            }
 227
 228            if (!_requestsDuration.Enabled)
 229            {
 230                return;
 231            }
 232
 233            TagList tags = requestTagKey.ToTagList();
 234            if (response is not null)
 235            {
 236                tags.Add("http.response.status_code", DiagnosticsHelper.GetBoxedInt32((int)response.StatusCode));
 237                tags.Add("network.protocol.version", DiagnosticsHelper.GetProtocolVersionString(response.Version));
 238            }
 239
 240            if (DiagnosticsHelper.TryGetErrorType(response, exception, out string? errorType))
 241            {
 242                tags.Add("error.type", errorType);
 243            }
 244
 245            TimeSpan durationTime = Stopwatch.GetElapsedTime(startTimestamp, Stopwatch.GetTimestamp());
 246
 247            List<Action<HttpMetricsEnrichmentContext>>? callbacks = HttpMetricsEnrichmentContext.GetEnrichmentCallbacksF
 248            if (callbacks is null)
 249            {
 250                _requestsDuration.Record(durationTime.TotalSeconds, tags);
 251            }
 252            else
 253            {
 254                HttpMetricsEnrichmentContext.RecordDurationWithEnrichment(callbacks, request, response, exception, durat
 255            }
 256        }
 257
 258        private ActiveRequestsTagKey CreateActiveRequestsTagKey(HttpRequestMessage request)
 259        {
 260            string? scheme = null;
 261            string? host = null;
 262            int port = 0;
 263
 264            if (request.RequestUri is Uri requestUri && requestUri.IsAbsoluteUri)
 265            {
 266                scheme = requestUri.Scheme;
 267                host = DiagnosticsHelper.GetServerAddress(request, _proxy);
 268                port = requestUri.Port;
 269            }
 270
 271            string method = (string)DiagnosticsHelper.GetMethodTag(request.Method, out _).Value!;
 272
 273            return new ActiveRequestsTagKey(scheme, host, port, method);
 274        }
 275
 276        private sealed class SharedMeter : Meter
 277        {
 278            public static Meter Instance { get; } = new SharedMeter();
 279            private SharedMeter()
 280                : base("System.Net.Http")
 281            {
 282            }
 283
 284            protected override void Dispose(bool disposing)
 285            {
 286                // NOP to prevent disposing the global instance from MeterListener callbacks.
 287            }
 288        }
 289    }
 290}
 291