< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 29
Coverable lines: 29
Total lines: 201
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 10
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%
Increment(...)100%110%
Decrement(...)0%880%
GetMeasurements()0%220%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Metrics/SocketsHttpHandlerMetrics.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.Metrics;
 8
 9namespace System.Net.Http.Metrics
 10{
 11    /// <summary>
 12    /// Represents a unique combination of tags for tracking open connections.
 13    /// </summary>
 14    internal readonly struct OpenConnectionsTagKey : IEquatable<OpenConnectionsTagKey>
 15    {
 16        public readonly string ProtocolVersion;
 17        public readonly string Scheme;
 18        public readonly string Host;
 19        public readonly int Port;
 20        public readonly bool IsIdle;
 21        public readonly string? PeerAddress;
 22        private readonly int _hashCode;
 23
 24        public OpenConnectionsTagKey(string protocolVersion, string scheme, string host, int port, bool isIdle, string? 
 25        {
 26            ProtocolVersion = protocolVersion;
 27            Scheme = scheme;
 28            Host = host;
 29            Port = port;
 30            IsIdle = isIdle;
 31            PeerAddress = peerAddress;
 32            _hashCode = HashCode.Combine(protocolVersion, scheme, host, port, isIdle, peerAddress);
 33        }
 34
 35        public bool Equals(OpenConnectionsTagKey other) =>
 36            ProtocolVersion == other.ProtocolVersion &&
 37            Scheme == other.Scheme &&
 38            Host == other.Host &&
 39            Port == other.Port &&
 40            IsIdle == other.IsIdle &&
 41            PeerAddress == other.PeerAddress;
 42
 43        public override bool Equals(object? obj) => obj is OpenConnectionsTagKey other && Equals(other);
 44
 45        public override int GetHashCode() => _hashCode;
 46
 47        public TagList ToTagList()
 48        {
 49            TagList tags = default;
 50            tags.Add("network.protocol.version", ProtocolVersion);
 51            tags.Add("url.scheme", Scheme);
 52            tags.Add("server.address", Host);
 53            tags.Add("server.port", DiagnosticsHelper.GetBoxedInt32(Port));
 54            tags.Add("http.connection.state", IsIdle ? "idle" : "active");
 55            if (PeerAddress is not null)
 56            {
 57                tags.Add("network.peer.address", PeerAddress);
 58            }
 59            return tags;
 60        }
 61
 62        public override string ToString() =>
 63            $"HTTP/{ProtocolVersion} {Scheme}://{Host}:{Port} {(IsIdle ? "idle" : "active")}{(PeerAddress is not null ? 
 64    }
 65
 66    /// <summary>
 67    /// Thread-safe tracker for open connection counts by tag combination.
 68    /// </summary>
 69    internal sealed class OpenConnectionsTracker
 70    {
 071        private readonly ConcurrentDictionary<OpenConnectionsTagKey, long> _counts = new();
 72
 73        /// <summary>
 74        /// Increments the count for the specified tag combination.
 75        /// </summary>
 76        public void Increment(in OpenConnectionsTagKey key)
 077        {
 078            _counts.AddOrUpdate(key, 1, static (_, currentValue) => currentValue + 1);
 079        }
 80
 81        /// <summary>
 82        /// Decrements the count for the specified tag combination.
 83        /// Removes the entry if the count reaches zero.
 84        /// </summary>
 85        public void Decrement(in OpenConnectionsTagKey key)
 086        {
 87            // We need to atomically decrement and remove if zero.
 88            // Use a spin loop with TryGetValue/TryUpdate/TryRemove to handle this safely.
 089            while (true)
 090            {
 091                if (!_counts.TryGetValue(key, out long currentValue))
 092                {
 93                    // Key doesn't exist, nothing to decrement.
 94                    // This shouldn't happen in normal operation but we handle it gracefully.
 095                    Debug.Fail($"Decrement for non-existing connection {key}");
 96                    return;
 97                }
 98
 099                if (currentValue <= 1)
 0100                {
 101                    // Try to remove the entry since it will become zero.
 102                    // Use the overload that checks the current value to ensure atomicity.
 0103                    if (_counts.TryRemove(new KeyValuePair<OpenConnectionsTagKey, long>(key, currentValue)))
 0104                    {
 0105                        return;
 106                    }
 107                    // Another thread modified the value, retry.
 0108                }
 109                else
 0110                {
 111                    // Try to decrement the value.
 0112                    if (_counts.TryUpdate(key, currentValue - 1, currentValue))
 0113                    {
 0114                        return;
 115                    }
 116                    // Another thread modified the value, retry.
 0117                }
 0118            }
 0119        }
 120
 121        /// <summary>
 122        /// Returns measurements for all tag combinations with non-zero counts.
 123        /// </summary>
 124        public IEnumerable<Measurement<long>> GetMeasurements()
 0125        {
 0126            foreach (KeyValuePair<OpenConnectionsTagKey, long> entry in _counts)
 0127            {
 0128                yield return new Measurement<long>(entry.Value, entry.Key.ToTagList());
 0129            }
 0130        }
 131    }
 132
 133    internal sealed class SocketsHttpHandlerMetrics
 134    {
 135        public readonly OpenConnectionsTracker OpenConnectionsTracker = new();
 136
 137        public readonly ObservableUpDownCounter<long> OpenConnections;
 138
 139        public readonly Histogram<double> ConnectionDuration;
 140
 141        public readonly Histogram<double> RequestsQueueDuration;
 142
 143        public SocketsHttpHandlerMetrics(Meter meter)
 144        {
 145            OpenConnections = meter.CreateObservableUpDownCounter<long>(
 146                name: "http.client.open_connections",
 147                observeValues: OpenConnectionsTracker.GetMeasurements,
 148                unit: "{connection}",
 149                description: "Number of outbound HTTP connections that are currently active or idle on the client.");
 150
 151            ConnectionDuration = meter.CreateHistogram<double>(
 152                name: "http.client.connection.duration",
 153                unit: "s",
 154                description: "The duration of successfully established outbound HTTP connections.",
 155                advice: new InstrumentAdvice<double>()
 156                {
 157                    // These values are not based on a standard and may change in the future.
 158                    HistogramBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300]
 159                });
 160
 161            RequestsQueueDuration = meter.CreateHistogram<double>(
 162                name: "http.client.request.time_in_queue",
 163                unit: "s",
 164                description: "The amount of time requests spent on a queue waiting for an available connection.",
 165                advice: DiagnosticsHelper.ShortHistogramAdvice);
 166        }
 167
 168        public void RequestLeftQueue(HttpRequestMessage request, HttpConnectionPool pool, TimeSpan duration, int version
 169        {
 170            Debug.Assert(versionMajor is 1 or 2 or 3);
 171
 172            if (RequestsQueueDuration.Enabled)
 173            {
 174                TagList tags = default;
 175
 176                // While requests may report HTTP/1.0 as the protocol, we treat all HTTP/1.X connections as HTTP/1.1.
 177                tags.Add("network.protocol.version", versionMajor switch
 178                {
 179                    1 => "1.1",
 180                    2 => "2",
 181                    _ => "3"
 182                });
 183
 184                tags.Add("url.scheme", pool.IsSecure ? "https" : "http");
 185
 186                Debug.Assert(pool.TelemetryServerAddress is not null, "TelemetryServerAddress should not be null when Sy
 187                tags.Add("server.address", pool.TelemetryServerAddress);
 188
 189                if (!pool.IsDefaultPort)
 190                {
 191                    tags.Add("server.port", pool.OriginAuthority.Port);
 192                }
 193
 194                tags.Add(DiagnosticsHelper.GetMethodTag(request.Method, out _));
 195
 196                RequestsQueueDuration.Record(duration.TotalSeconds, tags);
 197            }
 198        }
 199    }
 200}
 201