Skip to content

Commit 4cf8947

Browse files
authored
Improve Kestrel connection metrics with error reasons (#55565)
1 parent 0e1746b commit 4cf8947

File tree

77 files changed

+2178
-426
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2178
-426
lines changed

src/Servers/Kestrel/Core/src/Internal/BaseHttpConnectionContext.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public BaseHttpConnectionContext(
2020
IFeatureCollection connectionFeatures,
2121
MemoryPool<byte> memoryPool,
2222
IPEndPoint? localEndPoint,
23-
IPEndPoint? remoteEndPoint)
23+
IPEndPoint? remoteEndPoint,
24+
ConnectionMetricsContext metricsContext)
2425
{
2526
ConnectionId = connectionId;
2627
Protocols = protocols;
@@ -31,6 +32,7 @@ public BaseHttpConnectionContext(
3132
MemoryPool = memoryPool;
3233
LocalEndPoint = localEndPoint;
3334
RemoteEndPoint = remoteEndPoint;
35+
MetricsContext = metricsContext;
3436
}
3537

3638
public string ConnectionId { get; set; }
@@ -42,6 +44,7 @@ public BaseHttpConnectionContext(
4244
public MemoryPool<byte> MemoryPool { get; }
4345
public IPEndPoint? LocalEndPoint { get; }
4446
public IPEndPoint? RemoteEndPoint { get; }
47+
public ConnectionMetricsContext MetricsContext { get; }
4548

4649
public ITimeoutControl TimeoutControl { get; set; } = default!; // Always set by HttpConnection
4750
public ExecutionContext? InitialExecutionContext { get; set; }

src/Servers/Kestrel/Core/src/Internal/Http/Http1ChunkedEncodingMessageBody.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Diagnostics;
66
using System.IO.Pipelines;
77
using Microsoft.AspNetCore.Connections;
8+
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
89

910
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1011

@@ -136,6 +137,7 @@ private async Task PumpAsync()
136137
// Read() will have already have greedily consumed the entire request body if able.
137138
if (result.IsCompleted)
138139
{
140+
KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.UnexpectedEndOfRequestContent);
139141
ThrowUnexpectedEndOfRequestContent();
140142
}
141143
}

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,16 @@ public Http1Connection(HttpConnectionContext context)
6262
_context.ServiceContext.Log,
6363
_context.TimeoutControl,
6464
minResponseDataRateFeature: this,
65+
MetricsContext,
6566
outputAborter: this);
6667

6768
Input = _context.Transport.Input;
6869
Output = _http1Output;
6970
MemoryPool = _context.MemoryPool;
7071
}
7172

73+
public ConnectionMetricsContext MetricsContext => _context.MetricsContext;
74+
7275
public PipeReader Input { get; }
7376

7477
public bool RequestTimedOut => _requestTimedOut;
@@ -82,7 +85,7 @@ protected override void OnRequestProcessingEnded()
8285
if (IsUpgraded)
8386
{
8487
KestrelEventSource.Log.RequestUpgradedStop(this);
85-
ServiceContext.Metrics.RequestUpgradedStop(_context.MetricsContext);
88+
ServiceContext.Metrics.RequestUpgradedStop(MetricsContext);
8689

8790
ServiceContext.ConnectionManager.UpgradedConnectionCount.ReleaseOne();
8891
}
@@ -98,56 +101,66 @@ protected override void OnRequestProcessingEnded()
98101
void IRequestProcessor.OnInputOrOutputCompleted()
99102
{
100103
// Closed gracefully.
101-
_http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!);
104+
_http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!, ConnectionEndReason.TransportCompleted);
102105
CancelRequestAbortedToken();
103106
}
104107

105108
void IHttpOutputAborter.OnInputOrOutputCompleted()
106109
{
107-
_http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient));
110+
_http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient), ConnectionEndReason.TransportCompleted);
108111
CancelRequestAbortedToken();
109112
}
110113

111114
/// <summary>
112115
/// Immediately kill the connection and poison the request body stream with an error.
113116
/// </summary>
114-
public void Abort(ConnectionAbortedException abortReason)
117+
public void Abort(ConnectionAbortedException abortReason, ConnectionEndReason reason)
115118
{
116-
_http1Output.Abort(abortReason);
119+
_http1Output.Abort(abortReason, reason);
117120
CancelRequestAbortedToken();
118121
PoisonBody(abortReason);
119122
}
120123

121124
protected override void ApplicationAbort()
122125
{
123126
Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier);
124-
Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication));
127+
Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication), ConnectionEndReason.AbortedByApp);
125128
}
126129

127130
/// <summary>
128131
/// Stops the request processing loop between requests.
129132
/// Called on all active connections when the server wants to initiate a shutdown
130133
/// and after a keep-alive timeout.
131134
/// </summary>
132-
public void StopProcessingNextRequest()
135+
public void StopProcessingNextRequest(ConnectionEndReason reason)
133136
{
134-
_keepAlive = false;
137+
DisableKeepAlive(reason);
135138
Input.CancelPendingRead();
136139
}
137140

141+
internal override void DisableKeepAlive(ConnectionEndReason reason)
142+
{
143+
KestrelMetrics.AddConnectionEndReason(MetricsContext, reason);
144+
_keepAlive = false;
145+
}
146+
138147
public void SendTimeoutResponse()
139148
{
140149
_requestTimedOut = true;
141150
Input.CancelPendingRead();
142151
}
143152

144153
public void HandleRequestHeadersTimeout()
145-
=> SendTimeoutResponse();
154+
{
155+
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.RequestHeadersTimeout);
156+
SendTimeoutResponse();
157+
}
146158

147159
public void HandleReadDataRateTimeout()
148160
{
149161
Debug.Assert(MinRequestBodyDataRate != null);
150162

163+
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.MinRequestBodyDataRate);
151164
Log.RequestBodyMinimumDataRateNotSatisfied(ConnectionId, TraceIdentifier, MinRequestBodyDataRate.BytesPerSecond);
152165
SendTimeoutResponse();
153166
}
@@ -606,6 +619,7 @@ internal void EnsureHostHeaderExists()
606619
}
607620
else if (!HttpUtilities.IsHostHeaderValid(hostText))
608621
{
622+
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders);
609623
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
610624
}
611625
}
@@ -616,6 +630,7 @@ private void ValidateNonOriginHostHeader(string hostText)
616630
{
617631
if (hostText != RawTarget)
618632
{
633+
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders);
619634
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
620635
}
621636
}
@@ -640,6 +655,7 @@ private void ValidateNonOriginHostHeader(string hostText)
640655
}
641656
else
642657
{
658+
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders);
643659
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
644660
}
645661
}
@@ -648,6 +664,7 @@ private void ValidateNonOriginHostHeader(string hostText)
648664

649665
if (!HttpUtilities.IsHostHeaderValid(hostText))
650666
{
667+
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.InvalidRequestHeaders);
651668
KestrelBadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
652669
}
653670
}
@@ -707,11 +724,15 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
707724
#pragma warning disable CS0618 // Type or member is obsolete
708725
catch (BadHttpRequestException ex)
709726
{
710-
DetectHttp2Preface(result.Buffer, ex);
711-
727+
OnBadRequest(result.Buffer, ex);
712728
throw;
713729
}
714730
#pragma warning restore CS0618 // Type or member is obsolete
731+
catch (Exception)
732+
{
733+
KestrelMetrics.AddConnectionEndReason(MetricsContext, ConnectionEndReason.OtherError);
734+
throw;
735+
}
715736
finally
716737
{
717738
Input.AdvanceTo(reader.Position, isConsumed ? reader.Position : result.Buffer.End);
@@ -758,9 +779,65 @@ protected override bool TryParseRequest(ReadResult result, out bool endConnectio
758779
}
759780
}
760781

782+
internal static ConnectionEndReason GetConnectionEndReason(Microsoft.AspNetCore.Http.BadHttpRequestException ex)
783+
{
784+
#pragma warning disable CS0618 // Type or member is obsolete
785+
var kestrelEx = ex as BadHttpRequestException;
786+
#pragma warning restore CS0618 // Type or member is obsolete
787+
788+
switch (kestrelEx?.Reason)
789+
{
790+
case RequestRejectionReason.UnrecognizedHTTPVersion:
791+
return ConnectionEndReason.InvalidHttpVersion;
792+
case RequestRejectionReason.InvalidRequestLine:
793+
case RequestRejectionReason.RequestLineTooLong:
794+
case RequestRejectionReason.InvalidRequestTarget:
795+
return ConnectionEndReason.InvalidRequestLine;
796+
case RequestRejectionReason.InvalidRequestHeadersNoCRLF:
797+
case RequestRejectionReason.InvalidRequestHeader:
798+
case RequestRejectionReason.InvalidContentLength:
799+
case RequestRejectionReason.MultipleContentLengths:
800+
case RequestRejectionReason.MalformedRequestInvalidHeaders:
801+
case RequestRejectionReason.InvalidCharactersInHeaderName:
802+
case RequestRejectionReason.LengthRequiredHttp10:
803+
case RequestRejectionReason.OptionsMethodRequired:
804+
case RequestRejectionReason.ConnectMethodRequired:
805+
case RequestRejectionReason.MissingHostHeader:
806+
case RequestRejectionReason.MultipleHostHeaders:
807+
case RequestRejectionReason.InvalidHostHeader:
808+
return ConnectionEndReason.InvalidRequestHeaders;
809+
case RequestRejectionReason.HeadersExceedMaxTotalSize:
810+
return ConnectionEndReason.MaxRequestHeadersTotalSizeExceeded;
811+
case RequestRejectionReason.TooManyHeaders:
812+
return ConnectionEndReason.MaxRequestHeaderCountExceeded;
813+
case RequestRejectionReason.TlsOverHttpError:
814+
return ConnectionEndReason.TlsNotSupported;
815+
case RequestRejectionReason.UnexpectedEndOfRequestContent:
816+
return ConnectionEndReason.UnexpectedEndOfRequestContent;
817+
default:
818+
// In some scenarios the end reason might already be set to a more specific error
819+
// and attempting to set the reason again has no impact.
820+
return ConnectionEndReason.OtherError;
821+
}
822+
}
823+
761824
#pragma warning disable CS0618 // Type or member is obsolete
762-
private void DetectHttp2Preface(ReadOnlySequence<byte> requestData, BadHttpRequestException ex)
825+
private void OnBadRequest(ReadOnlySequence<byte> requestData, BadHttpRequestException ex)
763826
#pragma warning restore CS0618 // Type or member is obsolete
827+
{
828+
// Some code shared between HTTP versions throws errors. For example, HttpRequestHeaders collection
829+
// throws when an invalid content length is set.
830+
// Only want to set a reasons for HTTP/1.1 connection, so set end reason by catching the exception here.
831+
var reason = GetConnectionEndReason(ex);
832+
KestrelMetrics.AddConnectionEndReason(MetricsContext, reason);
833+
834+
if (ex.Reason == RequestRejectionReason.UnrecognizedHTTPVersion)
835+
{
836+
DetectHttp2Preface(requestData);
837+
}
838+
}
839+
840+
private void DetectHttp2Preface(ReadOnlySequence<byte> requestData)
764841
{
765842
const int PrefaceLineLength = 16;
766843

@@ -770,8 +847,7 @@ private void DetectHttp2Preface(ReadOnlySequence<byte> requestData, BadHttpReque
770847
{
771848
// If there is an unrecognized HTTP version, it is the first request on the connection, and the request line
772849
// bytes matches the HTTP/2 preface request line bytes then log and return a HTTP/2 GOAWAY frame.
773-
if (ex.Reason == RequestRejectionReason.UnrecognizedHTTPVersion
774-
&& _requestCount == 1
850+
if (_requestCount == 1
775851
&& requestData.Length >= PrefaceLineLength)
776852
{
777853
var clientPrefaceRequestLine = Http2.Http2Connection.ClientPreface.Slice(0, PrefaceLineLength);

src/Servers/Kestrel/Core/src/Internal/Http/Http1ContentLengthMessageBody.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ protected override void OnReadStarting()
246246
var maxRequestBodySize = _context.MaxRequestBodySize;
247247
if (_contentLength > maxRequestBodySize)
248248
{
249-
_context.DisableHttp1KeepAlive();
249+
_context.DisableKeepAlive(ConnectionEndReason.MaxRequestBodySizeExceeded);
250250
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.GetValueOrDefault().ToString(CultureInfo.InvariantCulture));
251251
}
252252
}

src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Globalization;
56
using System.IO.Pipelines;
67
using Microsoft.AspNetCore.Connections;
78
using Microsoft.AspNetCore.Http;
@@ -68,11 +69,10 @@ protected override Task OnConsumeAsync()
6869
}
6970
catch (InvalidOperationException ex)
7071
{
71-
var connectionAbortedException = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication, ex);
72-
_context.ReportApplicationError(connectionAbortedException);
72+
Log.RequestBodyDrainBodyReaderInvalidState(_context.ConnectionIdFeature, _context.TraceIdentifier, ex);
7373

7474
// Have to abort the connection because we can't finish draining the request
75-
_context.StopProcessingNextRequest();
75+
_context.StopProcessingNextRequest(ConnectionEndReason.InvalidBodyReaderState);
7676
return Task.CompletedTask;
7777
}
7878

@@ -104,18 +104,23 @@ protected async Task OnConsumeAsyncAwaited()
104104
}
105105
catch (InvalidOperationException ex)
106106
{
107-
var connectionAbortedException = new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication, ex);
108-
_context.ReportApplicationError(connectionAbortedException);
107+
Log.RequestBodyDrainBodyReaderInvalidState(_context.ConnectionIdFeature, _context.TraceIdentifier, ex);
109108

110109
// Have to abort the connection because we can't finish draining the request
111-
_context.StopProcessingNextRequest();
110+
_context.StopProcessingNextRequest(ConnectionEndReason.InvalidBodyReaderState);
112111
}
113112
finally
114113
{
115114
_context.TimeoutControl.CancelTimeout();
116115
}
117116
}
118117

118+
protected override void OnObservedBytesExceedMaxRequestBodySize(long maxRequestBodySize)
119+
{
120+
_context.DisableKeepAlive(ConnectionEndReason.MaxRequestBodySizeExceeded);
121+
KestrelBadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge, maxRequestBodySize.ToString(CultureInfo.InvariantCulture));
122+
}
123+
119124
public static MessageBody For(
120125
HttpVersion httpVersion,
121126
HttpRequestHeaders headers,
@@ -202,6 +207,7 @@ public static MessageBody For(
202207
// Reject with Length Required for HTTP 1.0.
203208
if (httpVersion == HttpVersion.Http10 && (context.Method == HttpMethod.Post || context.Method == HttpMethod.Put))
204209
{
210+
KestrelMetrics.AddConnectionEndReason(context.MetricsContext, ConnectionEndReason.InvalidRequestHeaders);
205211
KestrelBadHttpRequestException.Throw(RequestRejectionReason.LengthRequiredHttp10, context.Method);
206212
}
207213

@@ -221,6 +227,9 @@ protected void ThrowIfReaderCompleted()
221227
[StackTraceHidden]
222228
protected void ThrowUnexpectedEndOfRequestContent()
223229
{
230+
// Set before calling OnInputOrOutputCompleted.
231+
KestrelMetrics.AddConnectionEndReason(_context.MetricsContext, ConnectionEndReason.UnexpectedEndOfRequestContent);
232+
224233
// OnInputOrOutputCompleted() is an idempotent method that closes the connection. Sometimes
225234
// input completion is observed here before the Input.OnWriterCompleted() callback is fired,
226235
// so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400

src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal class Http1OutputProducer : IHttpOutputProducer, IDisposable
2929
private readonly MemoryPool<byte> _memoryPool;
3030
private readonly KestrelTrace _log;
3131
private readonly IHttpMinResponseDataRateFeature _minResponseDataRateFeature;
32+
private readonly ConnectionMetricsContext _connectionMetricsContext;
3233
private readonly IHttpOutputAborter _outputAborter;
3334
private readonly TimingPipeFlusher _flusher;
3435

@@ -75,6 +76,7 @@ public Http1OutputProducer(
7576
KestrelTrace log,
7677
ITimeoutControl timeoutControl,
7778
IHttpMinResponseDataRateFeature minResponseDataRateFeature,
79+
ConnectionMetricsContext connectionMetricsContext,
7880
IHttpOutputAborter outputAborter)
7981
{
8082
// Allow appending more data to the PipeWriter when a flush is pending.
@@ -84,6 +86,7 @@ public Http1OutputProducer(
8486
_memoryPool = memoryPool;
8587
_log = log;
8688
_minResponseDataRateFeature = minResponseDataRateFeature;
89+
_connectionMetricsContext = connectionMetricsContext;
8790
_outputAborter = outputAborter;
8891

8992
_flusher = new TimingPipeFlusher(timeoutControl, log);
@@ -455,7 +458,7 @@ private void CompletePipe()
455458
}
456459
}
457460

458-
public void Abort(ConnectionAbortedException error)
461+
public void Abort(ConnectionAbortedException error, ConnectionEndReason reason)
459462
{
460463
// Abort can be called after Dispose if there's a flush timeout.
461464
// It's important to still call _lifetimeFeature.Abort() in this case.
@@ -466,6 +469,8 @@ public void Abort(ConnectionAbortedException error)
466469
return;
467470
}
468471

472+
KestrelMetrics.AddConnectionEndReason(_connectionMetricsContext, reason);
473+
469474
_aborted = true;
470475
_connectionContext.Abort(error);
471476

0 commit comments

Comments
 (0)