Extend SocketsHttpHandler Connection and Request telemetry (#88853)
authorAnton Firszov <antonfir@gmail.com>
Tue, 18 Jul 2023 11:03:39 +0000 (13:03 +0200)
committerGitHub <noreply@github.com>
Tue, 18 Jul 2023 11:03:39 +0000 (13:03 +0200)
Fixes #63159, fixes #85729.

src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionBase.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentStream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
src/libraries/System.Net.Http/tests/FunctionalTests/TelemetryTest.cs

index 8126bda..9989bc4 100644 (file)
@@ -90,16 +90,23 @@ namespace System.Net.Http
             WriteEvent(eventId: 3, exceptionMessage);
         }
 
-        [Event(4, Level = EventLevel.Informational)]
-        private void ConnectionEstablished(byte versionMajor, byte versionMinor)
+        [NonEvent]
+        private void ConnectionEstablished(byte versionMajor, byte versionMinor, long connectionId, string scheme, string host, int port, IPEndPoint? remoteEndPoint)
+        {
+            string? remoteAddress = remoteEndPoint?.Address?.ToString();
+            ConnectionEstablished(versionMajor, versionMinor, connectionId, scheme, host, port, remoteAddress);
+        }
+
+        [Event(4, Level = EventLevel.Informational, Version = 1)]
+        private void ConnectionEstablished(byte versionMajor, byte versionMinor, long connectionId, string scheme, string host, int port, string? remoteAddress)
         {
-            WriteEvent(eventId: 4, versionMajor, versionMinor);
+            WriteEvent(eventId: 4, versionMajor, versionMinor, connectionId, scheme, host, port, remoteAddress);
         }
 
-        [Event(5, Level = EventLevel.Informational)]
-        private void ConnectionClosed(byte versionMajor, byte versionMinor)
+        [Event(5, Level = EventLevel.Informational, Version = 1)]
+        private void ConnectionClosed(byte versionMajor, byte versionMinor, long connectionId)
         {
-            WriteEvent(eventId: 5, versionMajor, versionMinor);
+            WriteEvent(eventId: 5, versionMajor, versionMinor, connectionId);
         }
 
         [Event(6, Level = EventLevel.Informational)]
@@ -108,10 +115,10 @@ namespace System.Net.Http
             WriteEvent(eventId: 6, timeOnQueueMilliseconds, versionMajor, versionMinor);
         }
 
-        [Event(7, Level = EventLevel.Informational)]
-        public void RequestHeadersStart()
+        [Event(7, Level = EventLevel.Informational, Version = 1)]
+        public void RequestHeadersStart(long connectionId)
         {
-            WriteEvent(eventId: 7);
+            WriteEvent(eventId: 7, connectionId);
         }
 
         [Event(8, Level = EventLevel.Informational)]
@@ -162,49 +169,55 @@ namespace System.Net.Http
             WriteEvent(eventId: 15, exception);
         }
 
+        [Event(16, Level = EventLevel.Informational)]
+        public void Redirect(string redirectUri)
+        {
+            WriteEvent(eventId: 16, redirectUri);
+        }
+
         [NonEvent]
-        public void Http11ConnectionEstablished()
+        public void Http11ConnectionEstablished(long connectionId, string scheme, string host, int port, IPEndPoint? remoteEndPoint)
         {
             Interlocked.Increment(ref _openedHttp11Connections);
-            ConnectionEstablished(versionMajor: 1, versionMinor: 1);
+            ConnectionEstablished(versionMajor: 1, versionMinor: 1, connectionId, scheme, host, port, remoteEndPoint);
         }
 
         [NonEvent]
-        public void Http11ConnectionClosed()
+        public void Http11ConnectionClosed(long connectionId)
         {
             long count = Interlocked.Decrement(ref _openedHttp11Connections);
             Debug.Assert(count >= 0);
-            ConnectionClosed(versionMajor: 1, versionMinor: 1);
+            ConnectionClosed(versionMajor: 1, versionMinor: 1, connectionId);
         }
 
         [NonEvent]
-        public void Http20ConnectionEstablished()
+        public void Http20ConnectionEstablished(long connectionId, string scheme, string host, int port, IPEndPoint? remoteEndPoint)
         {
             Interlocked.Increment(ref _openedHttp20Connections);
-            ConnectionEstablished(versionMajor: 2, versionMinor: 0);
+            ConnectionEstablished(versionMajor: 2, versionMinor: 0, connectionId, scheme, host, port, remoteEndPoint);
         }
 
         [NonEvent]
-        public void Http20ConnectionClosed()
+        public void Http20ConnectionClosed(long connectionId)
         {
             long count = Interlocked.Decrement(ref _openedHttp20Connections);
             Debug.Assert(count >= 0);
-            ConnectionClosed(versionMajor: 2, versionMinor: 0);
+            ConnectionClosed(versionMajor: 2, versionMinor: 0, connectionId);
         }
 
         [NonEvent]
-        public void Http30ConnectionEstablished()
+        public void Http30ConnectionEstablished(long connectionId, string scheme, string host, int port, IPEndPoint? remoteEndPoint)
         {
             Interlocked.Increment(ref _openedHttp30Connections);
-            ConnectionEstablished(versionMajor: 3, versionMinor: 0);
+            ConnectionEstablished(versionMajor: 3, versionMinor: 0, connectionId, scheme, host, port, remoteEndPoint);
         }
 
         [NonEvent]
-        public void Http30ConnectionClosed()
+        public void Http30ConnectionClosed(long connectionId)
         {
             long count = Interlocked.Decrement(ref _openedHttp30Connections);
             Debug.Assert(count >= 0);
-            ConnectionClosed(versionMajor: 3, versionMinor: 0);
+            ConnectionClosed(versionMajor: 3, versionMinor: 0, connectionId);
         }
 
         [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
@@ -293,9 +306,9 @@ namespace System.Net.Http
         [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
             Justification = "Parameters to this method are primitive and are trimmer safe")]
         [NonEvent]
-        private unsafe void WriteEvent(int eventId, byte arg1, byte arg2)
+        private unsafe void WriteEvent(int eventId, byte arg1, byte arg2, long arg3)
         {
-            const int NumEventDatas = 2;
+            const int NumEventDatas = 3;
             EventData* descrs = stackalloc EventData[NumEventDatas];
 
             descrs[0] = new EventData
@@ -308,6 +321,67 @@ namespace System.Net.Http
                 DataPointer = (IntPtr)(&arg2),
                 Size = sizeof(byte)
             };
+            descrs[2] = new EventData
+            {
+                DataPointer = (IntPtr)(&arg3),
+                Size = sizeof(long)
+            };
+
+            WriteEventCore(eventId, NumEventDatas, descrs);
+        }
+
+        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
+           Justification = "Parameters to this method are primitive and are trimmer safe")]
+        [NonEvent]
+        private unsafe void WriteEvent(int eventId, byte arg1, byte arg2, long arg3, string? arg4, string arg5, int arg6, string? arg7)
+        {
+            arg4 ??= "";
+            arg5 ??= "";
+            arg7 ??= "";
+
+            const int NumEventDatas = 7;
+            EventData* descrs = stackalloc EventData[NumEventDatas];
+
+            fixed (char* arg4Ptr = arg4)
+            fixed (char* arg5Ptr = arg5)
+            fixed (char* arg7Ptr = arg7)
+            {
+                descrs[0] = new EventData
+                {
+                    DataPointer = (IntPtr)(&arg1),
+                    Size = sizeof(byte)
+                };
+                descrs[1] = new EventData
+                {
+                    DataPointer = (IntPtr)(&arg2),
+                    Size = sizeof(byte)
+                };
+                descrs[2] = new EventData
+                {
+                    DataPointer = (IntPtr)(&arg3),
+                    Size = sizeof(long)
+                };
+                descrs[3] = new EventData
+                {
+                    DataPointer = (IntPtr)arg4Ptr,
+                    Size = (arg4.Length + 1) * sizeof(char)
+                };
+                descrs[4] = new EventData
+                {
+                    DataPointer = (IntPtr)arg5Ptr,
+                    Size = (arg5.Length + 1) * sizeof(char)
+                };
+                descrs[5] = new EventData
+                {
+                    DataPointer = (IntPtr)(&arg6),
+                    Size = sizeof(int)
+                };
+                descrs[6] = new EventData
+                {
+                    DataPointer = (IntPtr)arg7Ptr,
+                    Size = (arg7.Length + 1) * sizeof(char)
+                };
+            }
 
             WriteEventCore(eventId, NumEventDatas, descrs);
         }
index 88678ae..064825a 100644 (file)
@@ -132,8 +132,8 @@ namespace System.Net.Http
         private long _keepAlivePingTimeoutTimestamp;
         private volatile KeepAliveState _keepAliveState;
 
-        public Http2Connection(HttpConnectionPool pool, Stream stream)
-            : base(pool)
+        public Http2Connection(HttpConnectionPool pool, Stream stream, IPEndPoint? remoteEndPoint)
+            : base(pool, remoteEndPoint)
         {
             _pool = pool;
             _stream = stream;
@@ -1656,7 +1656,7 @@ namespace System.Net.Http
             ArrayBuffer headerBuffer = default;
             try
             {
-                if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart();
+                if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart(Id);
 
                 // Serialize headers to a temporary buffer, and do as much work to prepare to send the headers as we can
                 // before taking the write lock.
index 77e9108..2c69841 100644 (file)
@@ -66,7 +66,7 @@ namespace System.Net.Http
         }
 
         public Http3Connection(HttpConnectionPool pool, HttpAuthority authority, QuicConnection connection, bool includeAltUsedHeader)
-            : base(pool)
+            : base(pool, connection.RemoteEndPoint)
         {
             _pool = pool;
             _authority = authority;
index 5bb3a29..0469cbc 100644 (file)
@@ -552,7 +552,7 @@ namespace System.Net.Http
 
         private void BufferHeaders(HttpRequestMessage request)
         {
-            if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart();
+            if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart(_connection.Id);
 
             // Reserve space for the header frame envelope.
             // The envelope needs to be written after headers are serialized, as we need to know the payload length first.
index 20b4063..a30aeb0 100644 (file)
@@ -43,7 +43,7 @@ namespace System.Net.Http
         private static readonly ulong s_http11Bytes = BitConverter.ToUInt64("HTTP/1.1"u8);
 
         private readonly HttpConnectionPool _pool;
-        private readonly Stream _stream;
+        internal readonly Stream _stream;
         private readonly TransportContext? _transportContext;
 
         private HttpRequestMessage? _currentRequest;
@@ -73,8 +73,9 @@ namespace System.Net.Http
         public HttpConnection(
             HttpConnectionPool pool,
             Stream stream,
-            TransportContext? transportContext)
-            : base(pool)
+            TransportContext? transportContext,
+            IPEndPoint? remoteEndPoint)
+            : base(pool, remoteEndPoint)
         {
             Debug.Assert(pool != null);
             Debug.Assert(stream != null);
@@ -515,7 +516,7 @@ namespace System.Net.Http
             CancellationTokenRegistration cancellationRegistration = RegisterCancellation(cancellationToken);
             try
             {
-                if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart();
+                if (HttpTelemetry.Log.IsEnabled()) HttpTelemetry.Log.RequestHeadersStart(Id);
 
                 WriteHeaders(request, normalizedMethod);
 
index 337e416..d9bcb42 100644 (file)
@@ -16,6 +16,8 @@ namespace System.Net.Http
 {
     internal abstract class HttpConnectionBase : IDisposable, IHttpTrace
     {
+        private static long s_connectionCounter = -1;
+
         // May be null if none of the counters were enabled when the connection was established.
         private readonly ConnectionMetrics? _connectionMetrics;
 
@@ -31,7 +33,9 @@ namespace System.Net.Http
         /// <summary>Cached string for the last Server header received on this connection.</summary>
         private string? _lastServerHeaderValue;
 
-        public HttpConnectionBase(HttpConnectionPool pool)
+        public long Id { get; } = Interlocked.Increment(ref s_connectionCounter);
+
+        public HttpConnectionBase(HttpConnectionPool pool, IPEndPoint? remoteEndPoint)
         {
             Debug.Assert(this is HttpConnection or Http2Connection or Http3Connection);
             Debug.Assert(pool.Settings._metrics is not null);
@@ -64,9 +68,13 @@ namespace System.Net.Http
             {
                 _httpTelemetryMarkedConnectionAsOpened = true;
 
-                if (this is HttpConnection) HttpTelemetry.Log.Http11ConnectionEstablished();
-                else if (this is Http2Connection) HttpTelemetry.Log.Http20ConnectionEstablished();
-                else HttpTelemetry.Log.Http30ConnectionEstablished();
+                string scheme = pool.IsSecure ? "https" : "http";
+                string host = pool.OriginAuthority.HostValue;
+                int port = pool.OriginAuthority.Port;
+
+                if (this is HttpConnection) HttpTelemetry.Log.Http11ConnectionEstablished(Id, scheme, host, port, remoteEndPoint);
+                else if (this is Http2Connection) HttpTelemetry.Log.Http20ConnectionEstablished(Id, scheme, host, port, remoteEndPoint);
+                else HttpTelemetry.Log.Http30ConnectionEstablished(Id, scheme, host, port, remoteEndPoint);
             }
         }
 
@@ -79,9 +87,9 @@ namespace System.Net.Http
                 // Only decrement the connection count if we counted this connection
                 if (_httpTelemetryMarkedConnectionAsOpened)
                 {
-                    if (this is HttpConnection) HttpTelemetry.Log.Http11ConnectionClosed();
-                    else if (this is Http2Connection) HttpTelemetry.Log.Http20ConnectionClosed();
-                    else HttpTelemetry.Log.Http30ConnectionClosed();
+                    if (this is HttpConnection) HttpTelemetry.Log.Http11ConnectionClosed(Id);
+                    else if (this is Http2Connection) HttpTelemetry.Log.Http20ConnectionClosed(Id);
+                    else HttpTelemetry.Log.Http30ConnectionClosed(Id);
                 }
             }
         }
@@ -125,7 +133,7 @@ namespace System.Net.Http
             if (stream is SslStream sslStream)
             {
                 Trace(
-                    $"{this}. " +
+                    $"{this}. Id:{Id}, " +
                     $"SslProtocol:{sslStream.SslProtocol}, NegotiatedApplicationProtocol:{sslStream.NegotiatedApplicationProtocol}, " +
                     $"NegotiatedCipherSuite:{sslStream.NegotiatedCipherSuite}, CipherAlgorithm:{sslStream.CipherAlgorithm}, CipherStrength:{sslStream.CipherStrength}, " +
                     $"HashAlgorithm:{sslStream.HashAlgorithm}, HashStrength:{sslStream.HashStrength}, " +
@@ -134,7 +142,7 @@ namespace System.Net.Http
             }
             else
             {
-                Trace($"{this}");
+                Trace($"{this}. Id:{Id}");
             }
         }
 
index 76f4c4a..d66d961 100644 (file)
@@ -8,6 +8,7 @@ using System.Globalization;
 using System.IO;
 using System.Net.Http.Headers;
 using System.Net.Http.HPack;
+using System.Net.Http.Metrics;
 using System.Net.Http.QPack;
 using System.Net.Quic;
 using System.Net.Security;
@@ -603,7 +604,7 @@ namespace System.Net.Http
             }
         }
 
-        private async Task HandleHttp11Downgrade(HttpRequestMessage request, Stream stream, TransportContext? transportContext, CancellationToken cancellationToken)
+        private async Task HandleHttp11Downgrade(HttpRequestMessage request, Stream stream, TransportContext? transportContext, IPEndPoint? remoteEndPoint, CancellationToken cancellationToken)
         {
             if (NetEventSource.Log.IsEnabled()) Trace("Server does not support HTTP2; disabling HTTP2 use and proceeding with HTTP/1.1 connection");
 
@@ -660,7 +661,7 @@ namespace System.Net.Http
             try
             {
                 // Note, the same CancellationToken from the original HTTP2 connection establishment still applies here.
-                http11Connection = await ConstructHttp11ConnectionAsync(true, stream, transportContext, request, cancellationToken).ConfigureAwait(false);
+                http11Connection = await ConstructHttp11ConnectionAsync(true, stream, transportContext, request, remoteEndPoint, cancellationToken).ConfigureAwait(false);
             }
             catch (OperationCanceledException oce) when (oce.CancellationToken == cancellationToken)
             {
@@ -692,7 +693,7 @@ namespace System.Net.Http
             waiter.ConnectionCancellationTokenSource = cts;
             try
             {
-                (Stream stream, TransportContext? transportContext) = await ConnectAsync(queueItem.Request, true, cts.Token).ConfigureAwait(false);
+                (Stream stream, TransportContext? transportContext, IPEndPoint? remoteEndPoint) = await ConnectAsync(queueItem.Request, true, cts.Token).ConfigureAwait(false);
 
                 if (IsSecure)
                 {
@@ -709,19 +710,19 @@ namespace System.Net.Http
                         }
                         else
                         {
-                            connection = await ConstructHttp2ConnectionAsync(stream, queueItem.Request, cts.Token).ConfigureAwait(false);
+                            connection = await ConstructHttp2ConnectionAsync(stream, queueItem.Request, remoteEndPoint, cts.Token).ConfigureAwait(false);
                         }
                     }
                     else
                     {
                         // We established an SSL connection, but the server denied our request for HTTP2.
-                        await HandleHttp11Downgrade(queueItem.Request, stream, transportContext, cts.Token).ConfigureAwait(false);
+                        await HandleHttp11Downgrade(queueItem.Request, stream, transportContext, remoteEndPoint, cts.Token).ConfigureAwait(false);
                         return;
                     }
                 }
                 else
                 {
-                    connection = await ConstructHttp2ConnectionAsync(stream, queueItem.Request, cts.Token).ConfigureAwait(false);
+                    connection = await ConstructHttp2ConnectionAsync(stream, queueItem.Request, remoteEndPoint, cts.Token).ConfigureAwait(false);
                 }
             }
             catch (Exception e)
@@ -1528,15 +1529,18 @@ namespace System.Net.Http
 
         private CancellationTokenSource GetConnectTimeoutCancellationTokenSource() => new CancellationTokenSource(Settings._connectTimeout);
 
-        private async ValueTask<(Stream, TransportContext?)> ConnectAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
+        private async ValueTask<(Stream, TransportContext?, IPEndPoint?)> ConnectAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
         {
             Stream? stream = null;
+            IPEndPoint? remoteEndPoint = null;
             switch (_kind)
             {
                 case HttpConnectionKind.Http:
                 case HttpConnectionKind.Https:
                 case HttpConnectionKind.ProxyConnect:
                     stream = await ConnectToTcpHostAsync(_originAuthority.IdnHost, _originAuthority.Port, request, async, cancellationToken).ConfigureAwait(false);
+                    // remoteEndPoint is returned for diagnostic purposes.
+                    remoteEndPoint = GetRemoteEndPoint(stream);
                     if (_kind == HttpConnectionKind.ProxyConnect && _sslOptionsProxy != null)
                     {
                         stream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptionsProxy, request, async, stream, cancellationToken).ConfigureAwait(false);
@@ -1545,6 +1549,8 @@ namespace System.Net.Http
 
                 case HttpConnectionKind.Proxy:
                     stream = await ConnectToTcpHostAsync(_proxyUri!.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false);
+                    // remoteEndPoint is returned for diagnostic purposes.
+                    remoteEndPoint = GetRemoteEndPoint(stream);
                     if (_sslOptionsProxy != null)
                     {
                         stream = await ConnectHelper.EstablishSslConnectionAsync(_sslOptionsProxy, request, async, stream, cancellationToken).ConfigureAwait(false);
@@ -1554,12 +1560,20 @@ namespace System.Net.Http
                 case HttpConnectionKind.ProxyTunnel:
                 case HttpConnectionKind.SslProxyTunnel:
                     stream = await EstablishProxyTunnelAsync(async, cancellationToken).ConfigureAwait(false);
+
+                    if (stream is HttpContentStream contentStream && contentStream._connection?._stream is Stream innerStream)
+                    {
+                        remoteEndPoint = GetRemoteEndPoint(innerStream);
+                    }
+
                     break;
 
                 case HttpConnectionKind.SocksTunnel:
                 case HttpConnectionKind.SslSocksTunnel:
                     stream = await EstablishSocksTunnel(request, async, cancellationToken).ConfigureAwait(false);
-                break;
+                    // remoteEndPoint is returned for diagnostic purposes.
+                    remoteEndPoint = GetRemoteEndPoint(stream);
+                    break;
             }
 
             Debug.Assert(stream != null);
@@ -1583,7 +1597,9 @@ namespace System.Net.Http
                 stream = sslStream;
             }
 
-            return (stream, transportContext);
+            static IPEndPoint? GetRemoteEndPoint(Stream stream) => (stream as NetworkStream)?.Socket?.RemoteEndPoint as IPEndPoint;
+
+            return (stream, transportContext, remoteEndPoint);
         }
 
         private async ValueTask<Stream> ConnectToTcpHostAsync(string host, int port, HttpRequestMessage initialRequest, bool async, CancellationToken cancellationToken)
@@ -1649,8 +1665,8 @@ namespace System.Net.Http
 
         internal async ValueTask<HttpConnection> CreateHttp11ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
         {
-            (Stream stream, TransportContext? transportContext) = await ConnectAsync(request, async, cancellationToken).ConfigureAwait(false);
-            return await ConstructHttp11ConnectionAsync(async, stream, transportContext, request, cancellationToken).ConfigureAwait(false);
+            (Stream stream, TransportContext? transportContext, IPEndPoint? remoteEndPoint) = await ConnectAsync(request, async, cancellationToken).ConfigureAwait(false);
+            return await ConstructHttp11ConnectionAsync(async, stream, transportContext, request, remoteEndPoint, cancellationToken).ConfigureAwait(false);
         }
 
         private SslClientAuthenticationOptions GetSslOptionsForRequest(HttpRequestMessage request)
@@ -1713,17 +1729,17 @@ namespace System.Net.Http
             return newStream;
         }
 
-        private async ValueTask<HttpConnection> ConstructHttp11ConnectionAsync(bool async, Stream stream, TransportContext? transportContext, HttpRequestMessage request, CancellationToken cancellationToken)
+        private async ValueTask<HttpConnection> ConstructHttp11ConnectionAsync(bool async, Stream stream, TransportContext? transportContext, HttpRequestMessage request, IPEndPoint? remoteEndPoint, CancellationToken cancellationToken)
         {
             Stream newStream = await ApplyPlaintextFilterAsync(async, stream, HttpVersion.Version11, request, cancellationToken).ConfigureAwait(false);
-            return new HttpConnection(this, newStream, transportContext);
+            return new HttpConnection(this, newStream, transportContext, remoteEndPoint);
         }
 
-        private async ValueTask<Http2Connection> ConstructHttp2ConnectionAsync(Stream stream, HttpRequestMessage request, CancellationToken cancellationToken)
+        private async ValueTask<Http2Connection> ConstructHttp2ConnectionAsync(Stream stream, HttpRequestMessage request, IPEndPoint? remoteEndPoint, CancellationToken cancellationToken)
         {
             stream = await ApplyPlaintextFilterAsync(async: true, stream, HttpVersion.Version20, request, cancellationToken).ConfigureAwait(false);
 
-            Http2Connection http2Connection = new Http2Connection(this, stream);
+            Http2Connection http2Connection = new Http2Connection(this, stream, remoteEndPoint);
             try
             {
                 await http2Connection.SetupAsync(cancellationToken).ConfigureAwait(false);
index 4dc8fa1..0c40bdb 100644 (file)
@@ -5,7 +5,7 @@ namespace System.Net.Http
 {
     internal abstract class HttpContentStream : HttpBaseStream
     {
-        protected HttpConnection? _connection;
+        protected internal HttpConnection? _connection;
 
         public HttpContentStream(HttpConnection connection)
         {
index 5d4b484..961f6ff 100644 (file)
@@ -53,6 +53,10 @@ namespace System.Net.Http
                 // Clear the authorization header.
                 request.Headers.Authorization = null;
 
+                if (HttpTelemetry.Log.IsEnabled())
+                {
+                    HttpTelemetry.Log.Redirect(redirectUri.AbsoluteUri);
+                }
                 if (NetEventSource.Log.IsEnabled())
                 {
                     Trace($"Redirecting from {request.RequestUri} to {redirectUri} in response to status code {(int)response.StatusCode} '{response.StatusCode}'.", request.GetHashCode());
index 4f4cf14..82fac90 100644 (file)
@@ -69,11 +69,13 @@ namespace System.Net.Http.Functional.Tests
 
                 bool buffersResponse = false;
                 var events = new ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)>();
+                Uri expectedUri = null;
                 await listener.RunWithCallbackAsync(e => events.Enqueue((e, e.ActivityId)), async () =>
                 {
                     await GetFactoryForVersion(version).CreateClientAndServerAsync(
                         async uri =>
                         {
+                            expectedUri = uri;
                             using HttpClientHandler handler = CreateHttpClientHandler(version);
                             using HttpClient client = CreateHttpClient(handler, useVersionString);
                             using var invoker = new HttpMessageInvoker(handler);
@@ -177,7 +179,7 @@ namespace System.Net.Http.Functional.Tests
 
                 ValidateStartFailedStopEvents(events, version);
 
-                ValidateConnectionEstablishedClosed(events, version);
+                ValidateConnectionEstablishedClosed(events, version, expectedUri);
 
                 ValidateRequestResponseStartStopEvents(
                     events,
@@ -207,6 +209,7 @@ namespace System.Net.Http.Functional.Tests
                 listener.AddActivityTracking();
 
                 var events = new ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)>();
+                Uri expectedUri = null;
                 await listener.RunWithCallbackAsync(e => events.Enqueue((e, e.ActivityId)), async () =>
                 {
                     var semaphore = new SemaphoreSlim(0, 1);
@@ -215,6 +218,7 @@ namespace System.Net.Http.Functional.Tests
                     await GetFactoryForVersion(version).CreateClientAndServerAsync(
                         async uri =>
                         {
+                            expectedUri = uri;
                             using HttpClientHandler handler = CreateHttpClientHandler(version);
                             using HttpClient client = CreateHttpClient(handler, useVersionString);
                             using var invoker = new HttpMessageInvoker(handler);
@@ -286,7 +290,7 @@ namespace System.Net.Http.Functional.Tests
 
                 ValidateStartFailedStopEvents(events, version, shouldHaveFailures: true);
 
-                ValidateConnectionEstablishedClosed(events, version);
+                ValidateConnectionEstablishedClosed(events, version, expectedUri);
 
                 ValidateEventCounters(events, requestCount: 1, shouldHaveFailures: true, versionMajor: version.Major);
             }, UseVersion.ToString(), testMethod).Dispose();
@@ -318,11 +322,13 @@ namespace System.Net.Http.Functional.Tests
                 listener.AddActivityTracking();
 
                 var events = new ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)>();
+                Uri expectedUri = null;
                 await listener.RunWithCallbackAsync(e => events.Enqueue((e, e.ActivityId)), async () =>
                 {
                     await GetFactoryForVersion(version).CreateClientAndServerAsync(
                         async uri =>
                         {
+                            expectedUri = uri;
                             using HttpClientHandler handler = CreateHttpClientHandler(version);
                             using HttpClient client = CreateHttpClient(handler, useVersionString);
                             using var invoker = new HttpMessageInvoker(handler);
@@ -381,7 +387,7 @@ namespace System.Net.Http.Functional.Tests
 
                 ValidateStartFailedStopEvents(events, version);
 
-                ValidateConnectionEstablishedClosed(events, version);
+                ValidateConnectionEstablishedClosed(events, version, expectedUri);
 
                 ValidateRequestResponseStartStopEvents(
                     events,
@@ -449,32 +455,57 @@ namespace System.Net.Http.Functional.Tests
             }
         }
 
-        private static void ValidateConnectionEstablishedClosed(ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)> events, Version version, int count = 1)
+        // The validation asssumes that the connection id's are in range 0..(connectionCount-1)
+        protected static void ValidateConnectionEstablishedClosed(ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)> events, Version version, Uri uri, int connectionCount = 1)
         {
             EventWrittenEventArgs[] connectionsEstablished = events.Select(e => e.Event).Where(e => e.EventName == "ConnectionEstablished").ToArray();
-            Assert.Equal(count, connectionsEstablished.Length);
+            Assert.Equal(connectionCount, connectionsEstablished.Length);
+            HashSet<long> connectionIds = new HashSet<long>();
             foreach (EventWrittenEventArgs connectionEstablished in connectionsEstablished)
             {
-                Assert.Equal(2, connectionEstablished.Payload.Count);
+                Assert.Equal(7, connectionEstablished.Payload.Count);
+                Assert.Equal(new[] { "versionMajor", "versionMinor", "connectionId", "scheme", "host", "port", "remoteAddress" }, connectionEstablished.PayloadNames);
                 Assert.Equal(version.Major, (byte)connectionEstablished.Payload[0]);
                 Assert.Equal(version.Minor, (byte)connectionEstablished.Payload[1]);
+                long connectionId = (long)connectionEstablished.Payload[2];
+                connectionIds.Add(connectionId);
+
+                Assert.Equal(uri.Scheme, (string)connectionEstablished.Payload[3]);
+                Assert.Equal(uri.Host, (string)connectionEstablished.Payload[4]);
+                Assert.Equal(uri.Port, (int)connectionEstablished.Payload[5]);
+
+                IPAddress ip = IPAddress.Parse((string)connectionEstablished.Payload[6]);
+                Assert.True(ip.Equals(IPAddress.Loopback.MapToIPv6()) ||
+                    ip.Equals(IPAddress.Loopback) ||
+                    ip.Equals(IPAddress.IPv6Loopback));
             }
+            Assert.True(connectionIds.SetEquals(Enumerable.Range(0, connectionCount).Select(i => (long)i)), "ConnectionEstablished has logged an unexpected connectionId.");
 
             EventWrittenEventArgs[] connectionsClosed = events.Select(e => e.Event).Where(e => e.EventName == "ConnectionClosed").ToArray();
-            Assert.Equal(count, connectionsClosed.Length);
+            Assert.Equal(connectionCount, connectionsClosed.Length);
             foreach (EventWrittenEventArgs connectionClosed in connectionsClosed)
             {
-                Assert.Equal(2, connectionClosed.Payload.Count);
+                Assert.Equal(3, connectionClosed.Payload.Count);
+                Assert.Equal(new[] { "versionMajor", "versionMinor", "connectionId" }, connectionClosed.PayloadNames);
                 Assert.Equal(version.Major, (byte)connectionClosed.Payload[0]);
                 Assert.Equal(version.Minor, (byte)connectionClosed.Payload[1]);
+                long connectionId = (long)connectionClosed.Payload[2];
+                Assert.True(connectionIds.Remove(connectionId), $"ConnectionClosed has logged an unexpected connectionId={connectionId}");
             }
+            Assert.Empty(connectionIds);
         }
 
-        private static void ValidateRequestResponseStartStopEvents(ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)> events, int? requestContentLength, int? responseContentLength, int count)
+        private static void ValidateRequestResponseStartStopEvents(ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)> events, int? requestContentLength, int? responseContentLength, int count, long connectionId = 0)
         {
             (EventWrittenEventArgs Event, Guid ActivityId)[] requestHeadersStarts = events.Where(e => e.Event.EventName == "RequestHeadersStart").ToArray();
             Assert.Equal(count, requestHeadersStarts.Length);
-            Assert.All(requestHeadersStarts, r => Assert.Empty(r.Event.Payload));
+            Assert.All(requestHeadersStarts, r =>
+            {
+                EventWrittenEventArgs e = r.Event;
+                Assert.Equal(1, e.Payload.Count);
+                Assert.Equal("connectionId", e.PayloadNames.Single());
+                Assert.Equal(connectionId, (long)e.Payload[0]);
+            });
 
             (EventWrittenEventArgs Event, Guid ActivityId)[] requestHeadersStops = events.Where(e => e.Event.EventName == "RequestHeadersStop").ToArray();
             Assert.Equal(count, requestHeadersStops.Length);
@@ -643,6 +674,7 @@ namespace System.Net.Http.Functional.Tests
                 listener.AddActivityTracking();
 
                 var events = new ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)>();
+                Uri expectedUri = null;
                 await listener.RunWithCallbackAsync(e => events.Enqueue((e, e.ActivityId)), async () =>
                 {
                     var firstRequestReceived = new SemaphoreSlim(0, 1);
@@ -652,6 +684,7 @@ namespace System.Net.Http.Functional.Tests
                     await GetFactoryForVersion(version).CreateClientAndServerAsync(
                         async uri =>
                         {
+                            expectedUri = uri;
                             using HttpClientHandler handler = CreateHttpClientHandler(version, allowAllCertificates: true);
                             using HttpClient client = CreateHttpClient(handler, useVersionString);
 
@@ -715,7 +748,7 @@ namespace System.Net.Http.Functional.Tests
 
                 ValidateStartFailedStopEvents(events, version, count: 3);
 
-                ValidateConnectionEstablishedClosed(events, version);
+                ValidateConnectionEstablishedClosed(events, version, expectedUri);
 
                 var requestLeftQueueEvents = events.Where(e => e.Event.EventName == "RequestLeftQueue");
                 var (minCount, maxCount) = version.Major switch
@@ -744,7 +777,102 @@ namespace System.Net.Http.Functional.Tests
             }, UseVersion.ToString()).Dispose();
         }
 
-        private static async Task WaitForEventCountersAsync(ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)> events)
+        [OuterLoop]
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void EventSource_Redirect_LogsRedirect()
+        {
+            RemoteExecutor.Invoke(static async (string useVersionString) =>
+            {
+                Version version = Version.Parse(useVersionString);
+
+                using var listener = new TestEventListener("System.Net.Http", EventLevel.Verbose, eventCounterInterval: 0.1d);
+                listener.AddActivityTracking();
+                var events = new ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)>();
+                Uri expectedUri = null;
+
+                await listener.RunWithCallbackAsync(e => events.Enqueue((e, e.ActivityId)), async () =>
+                {
+                    await GetFactoryForVersion(version).CreateServerAsync((originalServer, originalUri) =>
+                    {
+                        return GetFactoryForVersion(version).CreateServerAsync(async (redirectServer, redirectUri) =>
+                        {
+                            expectedUri = redirectUri;
+                            using HttpClient client = CreateHttpClient(useVersionString);
+
+                            using HttpRequestMessage request = new(HttpMethod.Get, originalUri) { Version = version };
+
+                            Task clientTask = client.SendAsync(request);
+                            Task serverTask = originalServer.HandleRequestAsync(HttpStatusCode.Redirect, new[] { new HttpHeaderData("Location", redirectUri.AbsoluteUri) });
+
+                            await Task.WhenAny(clientTask, serverTask);
+                            Assert.False(clientTask.IsCompleted, $"{clientTask.Status}: {clientTask.Exception}");
+                            await serverTask;
+
+                            serverTask = redirectServer.HandleRequestAsync();
+                            await TestHelper.WhenAllCompletedOrAnyFailed(clientTask, serverTask);
+                            await clientTask;
+                        });
+                    });
+
+                    await WaitForEventCountersAsync(events);
+                });
+
+                EventWrittenEventArgs redirectEvent = events.Where(e => e.Event.EventName == "Redirect").Single().Event;
+                Assert.Equal(1, redirectEvent.Payload.Count);
+                Assert.Equal(expectedUri.ToString(), (string)redirectEvent.Payload[0]);
+                Assert.Equal("redirectUri", redirectEvent.PayloadNames[0]);
+            }, UseVersion.ToString()).Dispose();
+        }
+
+        public static bool SupportsRemoteExecutorAndAlpn = RemoteExecutor.IsSupported && PlatformDetection.SupportsAlpn;
+
+        [OuterLoop]
+        [ConditionalTheory(nameof(SupportsRemoteExecutorAndAlpn))]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void EventSource_Proxy_LogsIPAddress(bool useSsl)
+        {
+            if (UseVersion.Major == 3)
+            {
+                return;
+            }
+
+            RemoteExecutor.Invoke(static async (string useVersionString, string useSslString) =>
+            {
+                using var listener = new TestEventListener("System.Net.Http", EventLevel.Verbose, eventCounterInterval: 0.1d);
+                listener.AddActivityTracking();
+                var events = new ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)>();
+
+                await listener.RunWithCallbackAsync(e => events.Enqueue((e, e.ActivityId)), async () =>
+                {
+                    using LoopbackProxyServer proxyServer = LoopbackProxyServer.Create();
+
+                    await LoopbackServer.CreateClientAndServerAsync(async uri =>
+                    {
+                        using (HttpClientHandler handler = CreateHttpClientHandler(useVersionString))
+                        using (HttpClient client = CreateHttpClient(handler, useVersionString))
+                        {
+                            handler.Proxy = new WebProxy(proxyServer.Uri);
+                            await client.GetAsync(uri);
+                        }
+                    }, server => server.HandleRequestAsync(), options: new LoopbackServer.Options() { UseSsl = bool.Parse(useSslString) });
+
+                    await WaitForEventCountersAsync(events);
+                });
+
+                EventWrittenEventArgs[] connectionsEstablishedEvents = events.Select(e => e.Event).Where(e => e.EventName == "ConnectionEstablished").ToArray();
+
+                foreach (EventWrittenEventArgs e in connectionsEstablishedEvents)
+                {
+                    IPAddress ip = IPAddress.Parse((string)e.Payload[6]);
+                    Assert.True(ip.Equals(IPAddress.Loopback.MapToIPv6()) ||
+                        ip.Equals(IPAddress.Loopback) ||
+                        ip.Equals(IPAddress.IPv6Loopback));
+                }
+            }, UseVersion.ToString(), useSsl.ToString()).Dispose();
+        }
+
+        protected static async Task WaitForEventCountersAsync(ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)> events)
         {
             DateTime startTime = DateTime.UtcNow;
             int startCount = events.Count;
@@ -772,6 +900,74 @@ namespace System.Net.Http.Functional.Tests
     public sealed class TelemetryTest_Http11 : TelemetryTest
     {
         public TelemetryTest_Http11(ITestOutputHelper output) : base(output) { }
+
+        [OuterLoop]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/89035", TestPlatforms.OSX)]
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void EventSource_ParallelRequests_LogsNewConnectionIdForEachRequest()
+        {
+            RemoteExecutor.Invoke(async () =>
+            {
+                TimeSpan timeout = TimeSpan.FromSeconds(60);
+                const int NumParallelRequests = 4;
+
+                using var listener = new TestEventListener("System.Net.Http", EventLevel.Verbose, eventCounterInterval: 0.1d);
+                listener.AddActivityTracking();
+
+                var events = new ConcurrentQueue<(EventWrittenEventArgs Event, Guid ActivityId)>();
+                Uri expectedUri = null;
+                await listener.RunWithCallbackAsync(e => events.Enqueue((e, e.ActivityId)), async () =>
+                {
+                    await Http11LoopbackServerFactory.Singleton.CreateClientAndServerAsync(async uri =>
+                    {
+                        expectedUri = uri;
+                        using HttpClient client = CreateHttpClient(HttpVersion.Version11.ToString());
+
+                        Task<HttpResponseMessage>[] responseTasks = Enumerable.Repeat(uri, NumParallelRequests)
+                            .Select(_ => client.GetAsync(uri))
+                            .ToArray();
+                        await Task.WhenAll(responseTasks).WaitAsync(timeout);
+                    }, async server =>
+                    {
+                        ManualResetEventSlim allConnectionsOpen = new(false);
+                        int connectionCounter = 0;
+
+                        Task[] parallelConnectionTasks = Enumerable.Repeat(server, NumParallelRequests)
+                            .Select(_ => server.AcceptConnectionAsync(HandleConnectionAsync))
+                            .ToArray();
+
+                        await Task.WhenAll(parallelConnectionTasks);
+
+                        async Task HandleConnectionAsync(GenericLoopbackConnection connection)
+                        {
+                            if (Interlocked.Increment(ref connectionCounter) == NumParallelRequests)
+                            {
+                                allConnectionsOpen.Set();
+                            }
+                            await connection.ReadRequestDataAsync().WaitAsync(timeout);
+                            allConnectionsOpen.Wait(timeout);
+                            await connection.SendResponseAsync(HttpStatusCode.OK);
+                        }
+                    });
+                    await WaitForEventCountersAsync(events);
+                });
+
+                Assert.DoesNotContain(events, e => e.Event.EventId == 0); // errors from the EventSource itself
+
+                ValidateConnectionEstablishedClosed(events, HttpVersion.Version11, expectedUri, NumParallelRequests);
+
+                EventWrittenEventArgs[] requestHeadersStart = events.Select(e => e.Event).Where(e => e.EventName == "RequestHeadersStart").ToArray();
+                Assert.Equal(NumParallelRequests, requestHeadersStart.Length);
+                HashSet<long> connectionIds = new(Enumerable.Range(0, NumParallelRequests).Select(i => (long)i));
+                foreach (EventWrittenEventArgs e in requestHeadersStart)
+                {
+                    long connectionId = (long)e.Payload.Single();
+                    Assert.Equal("connectionId", e.PayloadNames.Single());
+                    Assert.True(connectionIds.Remove(connectionId), $"RequestHeadersStart has logged an unexpected connectionId={connectionId}.");
+                }
+                Assert.Empty(connectionIds);
+            }).Dispose();
+        }
     }
 
     public sealed class TelemetryTest_Http20 : TelemetryTest