feat: dumphttp command initial implementation (#4488)
authorAlexey Sosnin <sosnin.alexey@outlook.com>
Tue, 12 Mar 2024 17:57:44 +0000 (20:57 +0300)
committerGitHub <noreply@github.com>
Tue, 12 Mar 2024 17:57:44 +0000 (10:57 -0700)
Add a new command mentioned in
https://github.com/dotnet/diagnostics/issues/536#issuecomment-1521192550

Options:
```
> help dumphttp
dumphttp:
  Displays information about HTTP requests

Usage:
  > dumphttp [options]

Options:
  --stats                      Summarize all HTTP requests found rather than showing detailed info.
  --pending                    Show only requests without response
  --completed                  Show only requests with response
  --uri <uri>                  Show only requests with with specified request uri
  --statuscode <statuscode>    Show only requests with with specified response status code

Examples:
    Summarize all http requests:                                dumphttp --stats
    Show only completed http requests:                          dumphttp --completed
    Show failed request with request uri contains weather:      dumphttp --statuscode 500 --uri weather
```

Output example:
```
> dumphttp
         Address      MethodTable Method StatusCode Uri
    0263714267f0     7ffb87afb750 GET           200 http://localhost:5180/weatherforecast
    0263714133c8     7ffb87afb750 GET               http://localhost:5180/weatherforecast/slow
Total 2 requests

> dumphttp --pending
         Address      MethodTable Method StatusCode Uri
    0263714133c8     7ffb87afb750 GET               http://localhost:5180/weatherforecast/slow
Total 1 requests

> dumphttp --stats
Statistics:
         Count Method StatusCode Host
             1 GET           200 localhost
             1 GET               localhost
Total 2 requests
```

---------

Co-authored-by: Mike McLaughlin <mikem@microsoft.com>
src/Microsoft.Diagnostics.ExtensionCommands/DumpHttpCommand.cs [new file with mode: 0644]
src/Microsoft.Diagnostics.ExtensionCommands/DumpHttpRequestsCommand.cs [deleted file]
src/Microsoft.Diagnostics.ExtensionCommands/DumpRequestsCommand.cs [new file with mode: 0644]
src/SOS/Strike/managedcommands.cpp
src/SOS/Strike/sos.def
src/SOS/lldbplugin/soscommand.cpp

diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHttpCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHttpCommand.cs
new file mode 100644 (file)
index 0000000..5fb4d4e
--- /dev/null
@@ -0,0 +1,313 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Microsoft.Diagnostics.DebugServices;
+using Microsoft.Diagnostics.ExtensionCommands.Output;
+using Microsoft.Diagnostics.Runtime;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+    [Command(Name = "dumphttp", Aliases = new string[] { "DumpHttp" }, Help = "Displays information about HTTP requests.")]
+    public sealed class DumpHttpCommand : ClrRuntimeCommandBase
+    {
+        // Field names to get System.Net.Http.HttpRequestMessage.Method
+        private static readonly string[] s_httpMethodFieldNames =["_method", "method"];
+
+        // Field names to get System.Net.Http.HttpMethod.Method
+        private static readonly string[] s_methodFieldNames =["_method", "method"];
+
+        private static readonly string[] s_requestMessageFieldNames =["_requestMessage", "requestMessage"];
+        private static readonly string[] s_statusCodeFieldNames =["_statusCode", "statusCode"];
+        private static readonly string[] s_requestUriFieldNames =["_requestUri", "requestUri"];
+        private static readonly string[] s_uriStringFieldNames =["_string", "m_String"];
+
+        /// <summary>Gets whether to summarize all httpRequests found rather than showing detailed info.</summary>
+        [Option(Name = "--stats", Help = "Summarize all HTTP requests found rather than showing detailed info.")]
+        public bool Summarize { get; set; }
+
+        /// <summary>Gets whether to show only requests without response.</summary>
+        [Option(Name = "--pending", Help = "Show only requests without response")]
+        public bool Pending { get; set; }
+
+        /// <summary>Gets whether to show only requests with response.</summary>
+        [Option(Name = "--completed", Help = "Show only requests with response")]
+        public bool Completed { get; set; }
+
+        /// <summary>Gets whether to show only requests with with specified request uri.</summary>
+        [Option(Name = "--uri", Help = "Show only requests with with specified request uri")]
+        public string? Uri { get; set; }
+
+        /// <summary>Gets whether to show only requests with with specified response status codei.</summary>
+        [Option(Name = "--statuscode", Help = "Show only requests with with specified response status code")]
+        public int? StatusCode { get; set; }
+
+        private HeapWithFilters? FilteredHeap { get; set; }
+
+        private static readonly Column s_httpMethodColumn = new(Align.Left, 6, Formats.Text);
+        private static readonly Column s_statusCodeColumn = new(Align.Right, 10, Formats.Integer);
+
+        /// <summary>Invokes the command.</summary>
+        public override void Invoke()
+        {
+            ParseArguments();
+
+            // Enumerate the heap, gathering up all relevant HTTP request related httpRequests.
+            IEnumerable<HttpRequestInfo> httpRequests = CollectHttpRequests();
+            httpRequests = FilterDuplicates(httpRequests);
+            httpRequests = FilterByOptions(httpRequests);
+
+            // Render the data according to the options specified.
+            if (Summarize)
+            {
+                RenderStats();
+            }
+            else
+            {
+                RenderRequests();
+            }
+
+            return;
+
+            // <summary>Group httpRequests and summarize how many of each occurred.</summary>
+            void RenderStats()
+            {
+                Dictionary<HttpRequestStatGroupKey, HttpRequestStat> statCounts = new();
+
+                foreach (HttpRequestInfo httpRequest in httpRequests)
+                {
+                    HttpRequestStatGroupKey statKey = httpRequest.GetGroupKey();
+                    if (statCounts.TryGetValue(statKey, out HttpRequestStat stat))
+                    {
+                        stat.AddRequest();
+                    }
+                    else
+                    {
+                        stat = new HttpRequestStat(httpRequest);
+                        statCounts.Add(statKey, stat);
+                    }
+                }
+
+                WriteLine("Statistics:");
+                Table output = new(Console, ColumnKind.Integer, s_httpMethodColumn, s_statusCodeColumn, ColumnKind.Text);
+                output.WriteHeader("Count", "Method", "StatusCode", "Host");
+
+                foreach (KeyValuePair<HttpRequestStatGroupKey, HttpRequestStat> entry in statCounts.OrderByDescending(s => s.Value.Count))
+                {
+                    output.WriteRow(entry.Value.Count, entry.Value.HttpMethod, entry.Value.StatusCode, entry.Value.Host);
+                }
+
+                int total = statCounts.Select(stat => stat.Value.Count).Sum();
+                WriteLine($"Total {total} requests");
+            }
+
+            // <summary>Render each http request.</summary>
+            void RenderRequests()
+            {
+                Table output = new(Console, ColumnKind.Pointer, ColumnKind.Pointer, s_httpMethodColumn, s_statusCodeColumn, ColumnKind.Text);
+                output.WriteHeader("Address", "MethodTable", "Method", "StatusCode", "Uri");
+
+                int total = 0;
+                foreach (HttpRequestInfo httpRequest in httpRequests)
+                {
+                    output.WriteRow(httpRequest.Address, httpRequest.MethodTable, httpRequest.HttpMethod, httpRequest.StatusCode, httpRequest.Url);
+                    total++;
+                }
+                WriteLine($"Total {total} requests");
+            }
+        }
+
+        private IEnumerable<HttpRequestInfo> CollectHttpRequests()
+        {
+            IEnumerable<ClrObject> objectsToPrint = FilteredHeap!.EnumerateFilteredObjects(Console.CancellationToken);
+
+            foreach (ClrObject clrObject in objectsToPrint)
+            {
+                if (clrObject.Type?.Name == "System.Net.Http.HttpRequestMessage")
+                {
+                    // HttpRequestMessage doesn't have reference to HttpResponseMessage
+                    // the same http request can be found by HttpResponseMessage
+                    // these duplicates handled by FilterDuplicates method
+                    yield return BuildRequest(clrObject, null);
+                }
+
+                if (clrObject.Type?.Name == "System.Net.Http.HttpResponseMessage")
+                {
+                    ClrObject request = ReadAnyObjectField(clrObject, s_requestMessageFieldNames, "Unable to read HttpResponseMessage");
+                    yield return BuildRequest(request, clrObject);
+                }
+
+                // TODO handle System.Net.HttpWebRequest for .NET Framework dumps
+            }
+        }
+
+        private static HttpRequestInfo BuildRequest(ClrObject request, ClrObject? response)
+        {
+            string httpMethod = GetHttpMethod(request);
+            string uri = GetRequestUri(request);
+            int? statusCode = response != null ? ReadAnyField<int>(response.Value, s_statusCodeFieldNames, "Unable to read status code") : null;
+
+            return new HttpRequestInfo(
+                request.Address,
+                request.Type!.MethodTable,
+                httpMethod,
+                uri, statusCode);
+        }
+
+        private static string GetHttpMethod(ClrObject request)
+        {
+            ClrObject httpMethodObject = ReadAnyObjectField(request, s_httpMethodFieldNames, "Unable to read HTTP Method");
+            return ReadAnyObjectField(httpMethodObject, s_methodFieldNames, "Unable to read Method").AsString()!;
+        }
+
+        private static string GetRequestUri(ClrObject request)
+        {
+            ClrObject requestUriObject = ReadAnyObjectField(request, s_requestUriFieldNames, "Unable to read request uri");
+            return ReadAnyObjectField(requestUriObject, s_uriStringFieldNames, "Unable to read uri string").AsString()!;
+        }
+
+        private static T ReadAnyField<T>(ClrObject clrObject, string[] fieldNames, string errorMessage)
+            where T : unmanaged
+        {
+            foreach (string fieldName in fieldNames)
+            {
+                if (clrObject.TryReadField(fieldName, out T result))
+                {
+                    return result;
+                }
+            }
+
+            throw new ArgumentException(BuildMissingFieldMessage(clrObject, fieldNames, errorMessage));
+        }
+
+        private static ClrObject ReadAnyObjectField(ClrObject clrObject, string[] fieldNames, string errorMessage)
+        {
+            foreach (string fieldName in fieldNames)
+            {
+                if (clrObject.TryReadObjectField(fieldName, out ClrObject result))
+                {
+                    return result;
+                }
+            }
+
+            throw new ArgumentException(BuildMissingFieldMessage(clrObject, fieldNames, errorMessage));
+        }
+
+        private static string BuildMissingFieldMessage(ClrObject clrObject, string[] fieldNames, string errorMessage)
+        {
+            return $"{errorMessage}. Type '{clrObject.Type?.Name}' does not contain any field named {string.Join(" or ", fieldNames)}";
+        }
+
+        /// <summary>
+        /// Filter duplicates from requests collection
+        /// </summary>
+        /// <remarks>
+        /// Filter out requests found by HttpRequestMessage only.
+        /// Requests found by HttpResponseMessage+HttpRequestMessage have more filled props.
+        /// </remarks>
+        private static IEnumerable<HttpRequestInfo> FilterDuplicates(IEnumerable<HttpRequestInfo> requests)
+        {
+            HashSet<ulong> processedRequests = new();
+
+            foreach (HttpRequestInfo request in requests.OrderBy(r => r.StatusCode == null))
+            {
+                if (!processedRequests.Add(request.Address))
+                {
+                    continue;
+                }
+
+                yield return request;
+            }
+        }
+
+        private IEnumerable<HttpRequestInfo> FilterByOptions(IEnumerable<HttpRequestInfo> requests)
+        {
+            foreach (HttpRequestInfo request in requests)
+            {
+                bool matchesPendingCompletedFilter = !Pending && !Completed
+                                                     || Pending && request.StatusCode == null
+                                                     || Completed && request.StatusCode != null;
+
+                bool matchesUriFilter = Uri == null || request.Url.IndexOf(Uri, StringComparison.OrdinalIgnoreCase) >= 0;
+                bool matchesStatusCodeFilter = StatusCode == null || request.StatusCode == StatusCode;
+
+                if (matchesPendingCompletedFilter && matchesUriFilter && matchesStatusCodeFilter)
+                {
+                    yield return request;
+                }
+            }
+        }
+
+        private void ParseArguments()
+        {
+            if (Pending && Completed)
+            {
+                Pending = false;
+                Completed = false;
+            }
+
+            FilteredHeap = new HeapWithFilters(Runtime.Heap);
+        }
+
+        /// <summary>Gets detailed help for the command.</summary>
+        [HelpInvoke]
+        public static string GetDetailedHelp() =>
+            @"Examples:
+    Summarize all http requests:                                dumphttp --stats
+    Show only completed http requests:                          dumphttp --completed
+    Show failed request with request uri contains weather:      dumphttp --statuscode 500 --uri weather
+";
+
+        private sealed class HttpRequestInfo
+        {
+            public ulong Address { get; }
+            public ulong MethodTable { get; }
+            public string HttpMethod { get; }
+            public int? StatusCode { get; }
+            public string Url { get; }
+            public string Host { get; }
+
+            // TODO add response content-type header?
+            // TODO add response length? (can be difficult to calculate)
+
+            public HttpRequestInfo(ulong address, ulong methodTable, string httpMethod, string url, int? statusCode)
+            {
+                Address = address;
+                MethodTable = methodTable;
+                HttpMethod = httpMethod;
+                Url = url;
+                Host = new Uri(url).Host;
+                StatusCode = statusCode;
+            }
+
+            public HttpRequestStatGroupKey GetGroupKey() => new(StatusCode, HttpMethod, Host);
+        }
+
+        private sealed class HttpRequestStat
+        {
+            public int Count { get; private set; }
+            public string Host { get; }
+            public string HttpMethod { get; }
+            public int? StatusCode { get; }
+
+            public HttpRequestStat(HttpRequestInfo request)
+            {
+                Count = 1;
+                Host = new Uri(request.Url).Host;
+                HttpMethod = request.HttpMethod;
+                StatusCode = request.StatusCode;
+            }
+
+            public void AddRequest()
+            {
+                Count++;
+            }
+        }
+
+        private record struct HttpRequestStatGroupKey(int? StatusCode, string HttpMethod, string Host);
+    }
+}
diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpHttpRequestsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpHttpRequestsCommand.cs
deleted file mode 100644 (file)
index 9eb932c..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Linq;
-using Microsoft.Diagnostics.DebugServices;
-using Microsoft.Diagnostics.ExtensionCommands.Output;
-using Microsoft.Diagnostics.Runtime;
-using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind;
-
-namespace Microsoft.Diagnostics.ExtensionCommands
-{
-    [Command(Name = "dumphttprequests", Aliases = new string[] { "DumpHttpRequests" }, Help = "Shows all currently active incoming HTTP requests.")]
-    public class DumpHttpRequestsCommand : ClrRuntimeCommandBase
-    {
-        public override void Invoke()
-        {
-            List<(ulong Address, string Method, string Protocol, string Url)> requests = new();
-            if (Runtime.Heap.CanWalkHeap)
-            {
-                foreach (ClrObject obj in Runtime.Heap.EnumerateObjects())
-                {
-                    Console.CancellationToken.ThrowIfCancellationRequested();
-
-                    if (!obj.IsValid || obj.IsNull)
-                    {
-                        continue;
-                    }
-
-                    if (obj.Type?.Name?.Equals("Microsoft.AspNetCore.Http.DefaultHttpContext") ?? false)
-                    {
-                        ClrObject collection = obj.ReadValueTypeField("_features").ReadObjectField("<Collection>k__BackingField");
-                        if (!collection.IsNull)
-                        {
-                            string method = collection.ReadStringField("<Method>k__BackingField") ?? "";
-                            string scheme = collection.ReadStringField("<Scheme>k__BackingField") ?? "";
-                            string path = collection.ReadStringField("<Path>k__BackingField") ?? "";
-                            string query = collection.ReadStringField("<QueryString>k__BackingField") ?? "";
-                            requests.Add((obj.Address, method, $"{scheme}", $"{path}{query}"));
-                        }
-                    }
-                }
-            }
-            else
-            {
-                Console.WriteLine("The GC heap is not in a valid state for traversal.");
-            }
-
-            if (requests.Count > 0)
-            {
-                PrintRequests(requests);
-                Console.WriteLine($"Found {requests.Count} active requests");
-            }
-            else
-            {
-                Console.WriteLine("No requests found");
-            }
-        }
-
-        public void PrintRequests(List<(ulong Address, string Method, string scheme, string Url)> requests)
-        {
-            Column methodColumn = Text.GetAppropriateWidth(requests.Select(r => r.Method)).WithAlignment(Align.Left);
-            Column schemeColumn = Text.GetAppropriateWidth(requests.Select(r => r.scheme)).WithAlignment(Align.Left);
-            Column urlColumn = Text.GetAppropriateWidth(requests.Select(r => r.Url)).WithAlignment(Align.Left);
-            Table output = new(Console, DumpObj.WithAlignment(Align.Left), methodColumn, schemeColumn, urlColumn); ;
-            output.WriteHeader("Address", "Method", "Scheme", "Url");
-
-            foreach ((ulong address, string method, string scheme, string url) in requests)
-            {
-                Console.CancellationToken.ThrowIfCancellationRequested();
-                output.WriteRow(address, method, scheme, url);
-            }
-        }
-    }
-}
diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpRequestsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpRequestsCommand.cs
new file mode 100644 (file)
index 0000000..660a619
--- /dev/null
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Diagnostics.DebugServices;
+using Microsoft.Diagnostics.ExtensionCommands.Output;
+using Microsoft.Diagnostics.Runtime;
+using static Microsoft.Diagnostics.ExtensionCommands.Output.ColumnKind;
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+    [Command(Name = "dumprequests", Aliases = new string[] { "DumpRequests" }, Help = "Displays all currently active incoming HTTP requests.")]
+    public class DumpRequestsCommand : ClrRuntimeCommandBase
+    {
+        public override void Invoke()
+        {
+            List<(ulong Address, string Method, string Protocol, string Url)> requests = new();
+            if (Runtime.Heap.CanWalkHeap)
+            {
+                foreach (ClrObject obj in Runtime.Heap.EnumerateObjects())
+                {
+                    Console.CancellationToken.ThrowIfCancellationRequested();
+
+                    if (!obj.IsValid || obj.IsNull)
+                    {
+                        continue;
+                    }
+
+                    if (obj.Type?.Name?.Equals("Microsoft.AspNetCore.Http.DefaultHttpContext") ?? false)
+                    {
+                        ClrObject collection = obj.ReadValueTypeField("_features").ReadObjectField("<Collection>k__BackingField");
+                        if (!collection.IsNull)
+                        {
+                            string method = collection.ReadStringField("<Method>k__BackingField") ?? "";
+                            string scheme = collection.ReadStringField("<Scheme>k__BackingField") ?? "";
+                            string path = collection.ReadStringField("<Path>k__BackingField") ?? "";
+                            string query = collection.ReadStringField("<QueryString>k__BackingField") ?? "";
+                            requests.Add((obj.Address, method, $"{scheme}", $"{path}{query}"));
+                        }
+                    }
+                }
+            }
+            else
+            {
+                Console.WriteLine("The GC heap is not in a valid state for traversal.");
+            }
+
+            if (requests.Count > 0)
+            {
+                PrintRequests(requests);
+                Console.WriteLine($"Found {requests.Count} active requests");
+            }
+            else
+            {
+                Console.WriteLine("No requests found");
+            }
+        }
+
+        public void PrintRequests(List<(ulong Address, string Method, string scheme, string Url)> requests)
+        {
+            Column methodColumn = Text.GetAppropriateWidth(requests.Select(r => r.Method)).WithAlignment(Align.Left);
+            Column schemeColumn = Text.GetAppropriateWidth(requests.Select(r => r.scheme)).WithAlignment(Align.Left);
+            Column urlColumn = Text.GetAppropriateWidth(requests.Select(r => r.Url)).WithAlignment(Align.Left);
+            Table output = new(Console, DumpObj.WithAlignment(Align.Left), methodColumn, schemeColumn, urlColumn); ;
+            output.WriteHeader("Address", "Method", "Scheme", "Url");
+
+            foreach ((ulong address, string method, string scheme, string url) in requests)
+            {
+                Console.CancellationToken.ThrowIfCancellationRequested();
+                output.WriteRow(address, method, scheme, url);
+            }
+        }
+    }
+}
index e0a5cbf055500f94f740f66cda66d928c0cc5bd5..79de0556f84f71cb67a833bd2ebc3ababc446bda 100644 (file)
@@ -181,10 +181,16 @@ DECLARE_API(sizestats)
     return ExecuteManagedOnlyCommand("sizestats", args);
 }
 
-DECLARE_API(DumpHttpRequests)
+DECLARE_API(DumpHttp)
 {
     INIT_API_EXT();
-    return ExecuteManagedOnlyCommand("dumphttprequests", args);
+    return ExecuteManagedOnlyCommand("dumphttp", args);
+}
+
+DECLARE_API(DumpRequests)
+{
+    INIT_API_EXT();
+    return ExecuteManagedOnlyCommand("dumprequests", args);
 }
 
 typedef HRESULT (*PFN_COMMAND)(PDEBUG_CLIENT client, PCSTR args);
index 05c873d3ea2a4ce928366b8a4b980b8d39d94c13..b22c8b14c882b3c6f41e8eef0aa301f9dc00ce0a 100644 (file)
@@ -29,8 +29,10 @@ EXPORTS
     DumpDomain
     dumpdomain=DumpDomain
     dumpexceptions
-    DumpHttpRequests
-    dumphttprequests=DumpHttpRequests
+    DumpHttp
+    dumphttp=DumpHttp
+    DumpRequests
+    dumprequests=DumpRequests
 #ifdef TRACE_GC
     DumpGCLog
     dumpgclog=DumpGCLog
index 7edf9fca72fdb6300b161239b27f4bf3c30865c8..fa605ccb97c3dad17fd7124daa170bc6cde600b8 100644 (file)
@@ -173,7 +173,8 @@ sosCommandInitialize(lldb::SBDebugger debugger)
     g_services->AddCommand("dumpdomain", new sosCommand("DumpDomain"), "Displays information about the all assemblies within all the AppDomains or the specified one.");
     g_services->AddCommand("dumpgcdata", new sosCommand("DumpGCData"), "Displays information about the GC data.");
     g_services->AddManagedCommand("dumpheap", "Displays info about the garbage-collected heap and collection statistics about objects.");
-    g_services->AddManagedCommand("dumphttprequests", "Shows all currently active incoming HTTP requests.");
+    g_services->AddManagedCommand("dumphttp", "Displays information about HTTP requests.");
+    g_services->AddManagedCommand("dumprequests", "Displays all currently active incoming HTTP requests.");
     g_services->AddCommand("dumpil", new sosCommand("DumpIL"), "Displays the Microsoft intermediate language (MSIL) that's associated with a managed method.");
     g_services->AddCommand("dumplog", new sosCommand("DumpLog"), "Writes the contents of an in-memory stress log to the specified file.");
     g_services->AddCommand("dumpmd", new sosCommand("DumpMD"), "Displays information about a MethodDesc structure at the specified address.");