File: Internal\Infrastructure\KestrelMetrics.cs
Web Access
Project: src\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core)
// 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;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Authentication;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
 
internal sealed class KestrelMetrics
{
    // Note: Dot separated instead of dash.
    public const string MeterName = "Microsoft.AspNetCore.Server.Kestrel";
 
    public const string ErrorTypeAttributeName = "error.type";
 
    public const string Http11 = "1.1";
    public const string Http2 = "2";
    public const string Http3 = "3";
 
    private readonly Meter _meter;
    private readonly UpDownCounter<long> _activeConnectionsCounter;
    private readonly Histogram<double> _connectionDuration;
    private readonly Counter<long> _rejectedConnectionsCounter;
    private readonly UpDownCounter<long> _queuedConnectionsCounter;
    private readonly UpDownCounter<long> _queuedRequestsCounter;
    private readonly UpDownCounter<long> _currentUpgradedRequestsCounter;
    private readonly Histogram<double> _tlsHandshakeDuration;
    private readonly UpDownCounter<long> _activeTlsHandshakesCounter;
 
    public KestrelMetrics(IMeterFactory meterFactory)
    {
        _meter = meterFactory.Create(MeterName);
 
        _activeConnectionsCounter = _meter.CreateUpDownCounter<long>(
            "kestrel.active_connections",
            unit: "{connection}",
            description: "Number of connections that are currently active on the server.");
 
        _connectionDuration = _meter.CreateHistogram<double>(
            "kestrel.connection.duration",
            unit: "s",
            description: "The duration of connections on the server.",
            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.LongSecondsBucketBoundaries });
 
        _rejectedConnectionsCounter = _meter.CreateCounter<long>(
           "kestrel.rejected_connections",
            unit: "{connection}",
            description: "Number of connections rejected by the server. Connections are rejected when the currently active count exceeds the value configured with MaxConcurrentConnections.");
 
        _queuedConnectionsCounter = _meter.CreateUpDownCounter<long>(
           "kestrel.queued_connections",
            unit: "{connection}",
            description: "Number of connections that are currently queued and are waiting to start.");
 
        _queuedRequestsCounter = _meter.CreateUpDownCounter<long>(
           "kestrel.queued_requests",
            unit: "{request}",
            description: "Number of HTTP requests on multiplexed connections (HTTP/2 and HTTP/3) that are currently queued and are waiting to start.");
 
        _currentUpgradedRequestsCounter = _meter.CreateUpDownCounter<long>(
           "kestrel.upgraded_connections",
            unit: "{connection}",
            description: "Number of HTTP connections that are currently upgraded (WebSockets). The number only tracks HTTP/1.1 connections.");
 
        _tlsHandshakeDuration = _meter.CreateHistogram<double>(
            "kestrel.tls_handshake.duration",
            unit: "s",
            description: "The duration of TLS handshakes on the server.",
            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
 
        _activeTlsHandshakesCounter = _meter.CreateUpDownCounter<long>(
           "kestrel.active_tls_handshakes",
            unit: "{handshake}",
            description: "Number of TLS handshakes that are currently in progress on the server.");
    }
 
    public void ConnectionStart(ConnectionMetricsContext metricsContext)
    {
        if (metricsContext.CurrentConnectionsCounterEnabled)
        {
            ConnectionStartCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ConnectionStartCore(ConnectionMetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        _activeConnectionsCounter.Add(1, tags);
    }
 
    public void ConnectionStop(ConnectionMetricsContext metricsContext, Exception? exception, List<KeyValuePair<string, object?>>? customTags, long startTimestamp, long currentTimestamp)
    {
        if (metricsContext.CurrentConnectionsCounterEnabled || metricsContext.ConnectionDurationEnabled)
        {
            ConnectionStopCore(metricsContext, exception, customTags, startTimestamp, currentTimestamp);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ConnectionStopCore(ConnectionMetricsContext metricsContext, Exception? exception, List<KeyValuePair<string, object?>>? customTags, long startTimestamp, long currentTimestamp)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
 
        if (metricsContext.CurrentConnectionsCounterEnabled)
        {
            // Decrease in connections counter must match tags from increase. No custom tags.
            _activeConnectionsCounter.Add(-1, tags);
        }
 
        if (metricsContext.ConnectionDurationEnabled)
        {
            // Add custom tags for duration.
            if (customTags != null)
            {
                for (var i = 0; i < customTags.Count; i++)
                {
                    tags.Add(customTags[i]);
                }
            }
 
            // Check if there is an end reason on the context. For example, the connection could have been aborted by shutdown.
            if (metricsContext.ConnectionEndReason is { } reason && TryGetErrorType(reason, out var errorValue))
            {
                tags.TryAddTag(ErrorTypeAttributeName, errorValue);
            }
            else if (exception != null)
            {
                tags.TryAddTag(ErrorTypeAttributeName, exception.GetType().FullName);
            }
 
            var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
            _connectionDuration.Record(duration.TotalSeconds, tags);
        }
    }
 
    public void ConnectionRejected(ConnectionMetricsContext metricsContext)
    {
        AddConnectionEndReason(metricsContext, ConnectionEndReason.MaxConcurrentConnectionsExceeded);
 
        // Check live rather than cached state because this is just a counter, it's not a start/stop event like the other metrics.
        if (_rejectedConnectionsCounter.Enabled)
        {
            ConnectionRejectedCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ConnectionRejectedCore(ConnectionMetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        _rejectedConnectionsCounter.Add(1, tags);
    }
 
    public void ConnectionQueuedStart(ConnectionMetricsContext metricsContext)
    {
        if (metricsContext.QueuedConnectionsCounterEnabled)
        {
            ConnectionQueuedStartCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ConnectionQueuedStartCore(ConnectionMetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        _queuedConnectionsCounter.Add(1, tags);
    }
 
    public void ConnectionQueuedStop(ConnectionMetricsContext metricsContext)
    {
        if (metricsContext.QueuedConnectionsCounterEnabled)
        {
            ConnectionQueuedStopCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void ConnectionQueuedStopCore(ConnectionMetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        _queuedConnectionsCounter.Add(-1, tags);
    }
 
    public void RequestQueuedStart(ConnectionMetricsContext metricsContext, string httpVersion)
    {
        if (metricsContext.QueuedRequestsCounterEnabled)
        {
            RequestQueuedStartCore(metricsContext, httpVersion);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void RequestQueuedStartCore(ConnectionMetricsContext metricsContext, string httpVersion)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        tags.Add("network.protocol.name", "http");
        tags.Add("network.protocol.version", httpVersion);
        _queuedRequestsCounter.Add(1, tags);
    }
 
    public void RequestQueuedStop(ConnectionMetricsContext metricsContext, string httpVersion)
    {
        if (metricsContext.QueuedRequestsCounterEnabled)
        {
            RequestQueuedStopCore(metricsContext, httpVersion);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void RequestQueuedStopCore(ConnectionMetricsContext metricsContext, string httpVersion)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        tags.Add("network.protocol.name", "http");
        tags.Add("network.protocol.version", httpVersion);
        _queuedRequestsCounter.Add(-1, tags);
    }
 
    public void RequestUpgradedStart(ConnectionMetricsContext metricsContext)
    {
        if (metricsContext.CurrentUpgradedRequestsCounterEnabled)
        {
            RequestUpgradedStartCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void RequestUpgradedStartCore(ConnectionMetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        _currentUpgradedRequestsCounter.Add(1, tags);
    }
 
    public void RequestUpgradedStop(ConnectionMetricsContext metricsContext)
    {
        if (metricsContext.CurrentUpgradedRequestsCounterEnabled)
        {
            RequestUpgradedStopCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void RequestUpgradedStopCore(ConnectionMetricsContext metricsContext)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        _currentUpgradedRequestsCounter.Add(-1, tags);
    }
 
    public void TlsHandshakeStart(ConnectionMetricsContext metricsContext)
    {
        if (metricsContext.CurrentTlsHandshakesCounterEnabled)
        {
            TlsHandshakeStartCore(metricsContext);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void TlsHandshakeStartCore(ConnectionMetricsContext metricsContext)
    {
        // Tags must match TLS handshake end.
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
        _activeTlsHandshakesCounter.Add(1, tags);
    }
 
    public void TlsHandshakeStop(ConnectionMetricsContext metricsContext, long startTimestamp, long currentTimestamp, SslProtocols? protocol = null, Exception? exception = null)
    {
        if (metricsContext.CurrentTlsHandshakesCounterEnabled || _tlsHandshakeDuration.Enabled)
        {
            TlsHandshakeStopCore(metricsContext, startTimestamp, currentTimestamp, protocol, exception);
        }
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void TlsHandshakeStopCore(ConnectionMetricsContext metricsContext, long startTimestamp, long currentTimestamp, SslProtocols? protocol = null, Exception? exception = null)
    {
        var tags = new TagList();
        InitializeConnectionTags(ref tags, metricsContext);
 
        if (metricsContext.CurrentTlsHandshakesCounterEnabled)
        {
            // Tags must match TLS handshake start.
            _activeTlsHandshakesCounter.Add(-1, tags);
        }
 
        if (protocol != null && TryGetHandshakeProtocol(protocol.Value, out var protocolName, out var protocolVersion))
        {
            // Protocol name should always be TLS. Have logic to a tls.protocol.name tag if not TLS just in case.
            if (protocolName != "tls")
            {
                tags.Add("tls.protocol.name", protocolName);
            }
            tags.Add("tls.protocol.version", protocolVersion);
        }
        if (exception != null)
        {
            // Set exception name as error.type if there isn't already a value.
            tags.TryAddTag(ErrorTypeAttributeName, exception.GetType().FullName);
        }
 
        var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
        _tlsHandshakeDuration.Record(duration.TotalSeconds, tags);
    }
 
    private static void InitializeConnectionTags(ref TagList tags, in ConnectionMetricsContext metricsContext)
    {
        var localEndpoint = metricsContext.ConnectionContext.LocalEndPoint;
        if (localEndpoint is IPEndPoint localIPEndPoint)
        {
            tags.Add("server.address", localIPEndPoint.Address.ToString());
            tags.Add("server.port", localIPEndPoint.Port);
 
            switch (localIPEndPoint.Address.AddressFamily)
            {
                case AddressFamily.InterNetwork:
                    tags.Add("network.type", "ipv4");
                    break;
                case AddressFamily.InterNetworkV6:
                    tags.Add("network.type", "ipv6");
                    break;
            }
 
            // There isn't an easy way to detect whether QUIC is the underlying transport.
            // This code assumes that a multiplexed connection is QUIC.
            // Improve in the future if there are additional multiplexed connection types.
            var transport = metricsContext.ConnectionContext is not MultiplexedConnectionContext ? "tcp" : "udp";
            tags.Add("network.transport", transport);
        }
        else if (localEndpoint is UnixDomainSocketEndPoint udsEndPoint)
        {
            tags.Add("server.address", udsEndPoint.ToString());
            tags.Add("network.transport", "unix");
        }
        else if (localEndpoint is NamedPipeEndPoint namedPipeEndPoint)
        {
            tags.Add("server.address", namedPipeEndPoint.ToString());
            tags.Add("network.transport", "pipe");
        }
        else if (localEndpoint != null)
        {
            tags.Add("server.address", localEndpoint.ToString());
            tags.Add("network.transport", localEndpoint.AddressFamily.ToString());
        }
    }
 
    public ConnectionMetricsContext CreateContext(BaseConnectionContext connection)
    {
        // Cache the state at the start of the connection so we produce consistent start/stop events.
        return new ConnectionMetricsContext
        {
            ConnectionContext = connection,
            CurrentConnectionsCounterEnabled = _activeConnectionsCounter.Enabled,
            ConnectionDurationEnabled = _connectionDuration.Enabled,
            QueuedConnectionsCounterEnabled = _queuedConnectionsCounter.Enabled,
            QueuedRequestsCounterEnabled = _queuedRequestsCounter.Enabled,
            CurrentUpgradedRequestsCounterEnabled = _currentUpgradedRequestsCounter.Enabled,
            CurrentTlsHandshakesCounterEnabled = _activeTlsHandshakesCounter.Enabled
        };
    }
 
    public static bool TryGetHandshakeProtocol(SslProtocols protocols, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out string? version)
    {
        // Protocol should be either TLS 1.2 or 1.3. Many older SslProtocols are no longer supported.
        // Logic for resolving older known values is still here out of an abundence of caution.
 
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable SYSLIB0039 // Type or member is obsolete
        switch (protocols)
        {
            case SslProtocols.Ssl2:
                name = "ssl";
                version = "2.0";
                return true;
            case SslProtocols.Ssl3:
                name = "ssl";
                version = "3.0";
                return true;
            case SslProtocols.Tls:
                name = "tls";
                version = "1.0";
                return true;
            case SslProtocols.Tls11:
                name = "tls";
                version = "1.1";
                return true;
            case SslProtocols.Tls12:
                name = "tls";
                version = "1.2";
                return true;
            case SslProtocols.Tls13:
                name = "tls";
                version = "1.3";
                return true;
        }
#pragma warning restore SYSLIB0039 // Type or member is obsolete
#pragma warning restore CS0618 // Type or member is obsolete
 
        name = null;
        version = null;
        return false;
    }
 
    public static void AddConnectionEndReason(IConnectionMetricsTagsFeature? feature, ConnectionEndReason reason)
    {
        Debug.Assert(reason != ConnectionEndReason.Unset);
 
        if (feature != null)
        {
            if (TryGetErrorType(reason, out var errorTypeValue))
            {
                feature.TryAddTag(ErrorTypeAttributeName, errorTypeValue);
            }
        }
    }
 
    public static void AddConnectionEndReason(ConnectionMetricsContext? context, ConnectionEndReason reason, bool overwrite = false)
    {
        Debug.Assert(reason != ConnectionEndReason.Unset);
 
        if (context != null)
        {
            // Set end reason when either:
            // - Overwrite is true. For example, AppShutdownTimeout reason is forced when shutting down
            //   the app reguardless of whether there is already a value.
            // - New reason is an error type and there isn't already an error type set.
            //   In other words, first error wins.
            if (overwrite)
            {
                Debug.Assert(TryGetErrorType(reason, out _), "Overwrite should only be set for an error reason.");
                context.ConnectionEndReason = reason;
            }
            else if (TryGetErrorType(reason, out _))
            {
                if (context.ConnectionEndReason == null)
                {
                    context.ConnectionEndReason = reason;
                }
            }
        }
    }
 
    internal static string? GetErrorType(ConnectionEndReason reason)
    {
        TryGetErrorType(reason, out var errorTypeValue);
        return errorTypeValue;
    }
 
    internal static bool TryGetErrorType(ConnectionEndReason reason, [NotNullWhen(true)]out string? errorTypeValue)
    {
        errorTypeValue = reason switch
        {
            ConnectionEndReason.Unset => null, // Not an error
            ConnectionEndReason.ClientGoAway => null, // Not an error
            ConnectionEndReason.TransportCompleted => null, // Not an error
            ConnectionEndReason.GracefulAppShutdown => null, // Not an error
            ConnectionEndReason.RequestNoKeepAlive => null, // Not an error
            ConnectionEndReason.ResponseNoKeepAlive => null, // Not an error
            ConnectionEndReason.ErrorAfterStartingResponse => "error_after_starting_response",
            ConnectionEndReason.ConnectionReset => "connection_reset",
            ConnectionEndReason.FlowControlWindowExceeded => "flow_control_window_exceeded",
            ConnectionEndReason.KeepAliveTimeout => "keep_alive_timeout",
            ConnectionEndReason.InsufficientTlsVersion => "insufficient_tls_version",
            ConnectionEndReason.InvalidHandshake => "invalid_handshake",
            ConnectionEndReason.InvalidStreamId => "invalid_stream_id",
            ConnectionEndReason.FrameAfterStreamClose => "frame_after_stream_close",
            ConnectionEndReason.UnknownStream => "unknown_stream",
            ConnectionEndReason.UnexpectedFrame => "unexpected_frame",
            ConnectionEndReason.InvalidFrameLength => "invalid_frame_length",
            ConnectionEndReason.InvalidDataPadding => "invalid_data_padding",
            ConnectionEndReason.InvalidRequestHeaders => "invalid_request_headers",
            ConnectionEndReason.StreamResetLimitExceeded => "stream_reset_limit_exceeded",
            ConnectionEndReason.InvalidWindowUpdateSize => "invalid_window_update_size",
            ConnectionEndReason.StreamSelfDependency => "stream_self_dependency",
            ConnectionEndReason.InvalidSettings => "invalid_settings",
            ConnectionEndReason.MissingStreamEnd => "missing_stream_end",
            ConnectionEndReason.MaxFrameLengthExceeded => "max_frame_length_exceeded",
            ConnectionEndReason.ErrorReadingHeaders => "error_reading_headers",
            ConnectionEndReason.ErrorWritingHeaders => "error_writing_headers",
            ConnectionEndReason.OtherError => "other_error",
            ConnectionEndReason.InvalidHttpVersion => "invalid_http_version",
            ConnectionEndReason.RequestHeadersTimeout => "request_headers_timeout",
            ConnectionEndReason.MinRequestBodyDataRate => "min_request_body_data_rate",
            ConnectionEndReason.MinResponseDataRate => "min_response_data_rate",
            ConnectionEndReason.FlowControlQueueSizeExceeded => "flow_control_queue_size_exceeded",
            ConnectionEndReason.OutputQueueSizeExceeded => "output_queue_size_exceeded",
            ConnectionEndReason.ClosedCriticalStream => "closed_critical_stream",
            ConnectionEndReason.AbortedByApp => "aborted_by_app",
            ConnectionEndReason.WriteCanceled => "write_canceled",
            ConnectionEndReason.InvalidBodyReaderState => "invalid_body_reader_state",
            ConnectionEndReason.ServerTimeout => "server_timeout",
            ConnectionEndReason.StreamCreationError => "stream_creation_error",
            ConnectionEndReason.IOError => "io_error",
            ConnectionEndReason.AppShutdownTimeout => "app_shutdown_timeout",
            ConnectionEndReason.TlsHandshakeFailed => "tls_handshake_failed",
            ConnectionEndReason.InvalidRequestLine => "invalid_request_line",
            ConnectionEndReason.TlsNotSupported => "tls_not_supported",
            ConnectionEndReason.MaxRequestBodySizeExceeded => "max_request_body_size_exceeded",
            ConnectionEndReason.UnexpectedEndOfRequestContent => "unexpected_end_of_request_content",
            ConnectionEndReason.MaxConcurrentConnectionsExceeded => "max_concurrent_connections_exceeded",
            ConnectionEndReason.MaxRequestHeadersTotalSizeExceeded => "max_request_headers_total_size_exceeded",
            ConnectionEndReason.MaxRequestHeaderCountExceeded => "max_request_header_count_exceeded",
            ConnectionEndReason.ResponseContentLengthMismatch => "response_content_length_mismatch",
            _ => throw new InvalidOperationException($"Unable to calculate whether {reason} resolves to error.type value.")
        };
 
        return errorTypeValue != null;
    }
}