Browse Source

Adds HttpLogging middleware (#31816)

* Adding logging
* Progress
* Logging
* nit
* Polishing HttpLogging
* Namespace and nit
* System
* Fix public API
* Feedback
* Big perf wins for response body
* Adds request body side
* Another API pass
* Combine request and response stream base type
* Fixing variable
* nit
* Updating samples
* Some feedback
* Small fixups
* API review feedback
* Tests working and most of the feedback
* rename
* Feedback
* bit more logging
* More overloads
* Fixing truncation
* Update src/Middleware/HttpLogging/src/HttpRequestLog.cs
  Co-authored-by: Kahbazi <[email protected]>
* More tests and log headers later
* Test for invalid media type
* Logging request body if it isn't logged
* nit
* Update src/Middleware/HttpLogging/src/HttpResponseLog.cs
  Co-authored-by: Kahbazi <[email protected]>
* Feedback
* Remove uneeded dep
* Removing mroe
* Abstractions?
* Targeting pack and comments
* Extra check
* Fixing tests
* Fixing tests
* All of that feedback
* Another round of feedback
* Override writeasync
* Feedback
* Fixing some small parts
* Fixing response buffering check
  Co-authored-by: Kahbazi <[email protected]>
Justin Kotalik 4 years ago
parent
commit
4ee074f1bc
35 changed files with 2773 additions and 1 deletions
  1. 48 0
      AspNetCore.sln
  2. 1 0
      eng/ProjectReferences.props
  3. 1 0
      eng/SharedFramework.Local.props
  4. 2 0
      src/Framework/test/TestData.cs
  5. 16 0
      src/Middleware/HttpLogging/samples/HttpLogging.Sample/HttpLogging.Sample.csproj
  6. 34 0
      src/Middleware/HttpLogging/samples/HttpLogging.Sample/Program.cs
  7. 28 0
      src/Middleware/HttpLogging/samples/HttpLogging.Sample/Properties/launchSettings.json
  8. 36 0
      src/Middleware/HttpLogging/samples/HttpLogging.Sample/Startup.cs
  9. 9 0
      src/Middleware/HttpLogging/samples/HttpLogging.Sample/appsettings.Development.json
  10. 10 0
      src/Middleware/HttpLogging/samples/HttpLogging.Sample/appsettings.json
  11. 326 0
      src/Middleware/HttpLogging/src/BufferingStream.cs
  12. 30 0
      src/Middleware/HttpLogging/src/HttpLoggingBuilderExtensions.cs
  13. 40 0
      src/Middleware/HttpLogging/src/HttpLoggingExtensions.cs
  14. 176 0
      src/Middleware/HttpLogging/src/HttpLoggingFields.cs
  15. 247 0
      src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs
  16. 78 0
      src/Middleware/HttpLogging/src/HttpLoggingOptions.cs
  17. 35 0
      src/Middleware/HttpLogging/src/HttpLoggingServicesExtensions.cs
  18. 74 0
      src/Middleware/HttpLogging/src/HttpRequestLog.cs
  19. 73 0
      src/Middleware/HttpLogging/src/HttpResponseLog.cs
  20. 70 0
      src/Middleware/HttpLogging/src/MediaTypeHelpers.cs
  21. 127 0
      src/Middleware/HttpLogging/src/MediaTypeOptions.cs
  22. 22 0
      src/Middleware/HttpLogging/src/Microsoft.AspNetCore.HttpLogging.csproj
  23. 3 0
      src/Middleware/HttpLogging/src/Properties/AssemblyInfo.cs
  24. 1 0
      src/Middleware/HttpLogging/src/PublicAPI.Shipped.txt
  25. 42 0
      src/Middleware/HttpLogging/src/PublicAPI.Unshipped.txt
  26. 116 0
      src/Middleware/HttpLogging/src/RequestBufferingStream.cs
  27. 177 0
      src/Middleware/HttpLogging/src/ResponseBufferingStream.cs
  28. 867 0
      src/Middleware/HttpLogging/test/HttpLoggingMiddlewareTests.cs
  29. 67 0
      src/Middleware/HttpLogging/test/HttpLoggingOptionsTests.cs
  30. 12 0
      src/Middleware/HttpLogging/test/Microsoft.AspNetCore.HttpLogging.Tests.csproj
  31. 1 1
      src/Middleware/HttpsPolicy/src/HttpsRedirectionOptions.cs
  32. 3 0
      src/Middleware/Middleware.slnf
  33. 1 0
      src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj
  34. 0 0
      src/Shared/Buffers/BufferSegment.cs
  35. 0 0
      src/Shared/Buffers/BufferSegmentStack.cs

+ 48 - 0
AspNetCore.sln

@@ -1626,6 +1626,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.SpaSer
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalSample", "src\Http\samples\MinimalSample\MinimalSample.csproj", "{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpLogging", "HttpLogging", "{022B4B80-E813-4256-8034-11A68146F4EF}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpLogging", "src\Middleware\HttpLogging\src\Microsoft.AspNetCore.HttpLogging.csproj", "{FF413F1C-A998-4FA2-823F-52AC0916B35C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpLogging.Tests", "src\Middleware\HttpLogging\test\Microsoft.AspNetCore.HttpLogging.Tests.csproj", "{3A1EC883-EF9C-43E8-95E5-6B527428867B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpLogging.Sample", "src\Middleware\HttpLogging\samples\HttpLogging.Sample\HttpLogging.Sample.csproj", "{908B2263-B58B-4261-A125-B5F2DFF92799}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -7709,6 +7717,42 @@ Global
 		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x64.Build.0 = Release|Any CPU
 		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x86.ActiveCfg = Release|Any CPU
 		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38}.Release|x86.Build.0 = Release|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Debug|x64.Build.0 = Debug|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Debug|x86.Build.0 = Debug|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Release|x64.ActiveCfg = Release|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Release|x64.Build.0 = Release|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Release|x86.ActiveCfg = Release|Any CPU
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C}.Release|x86.Build.0 = Release|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Debug|x64.Build.0 = Debug|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Debug|x86.Build.0 = Debug|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Release|Any CPU.Build.0 = Release|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Release|x64.ActiveCfg = Release|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Release|x64.Build.0 = Release|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Release|x86.ActiveCfg = Release|Any CPU
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B}.Release|x86.Build.0 = Release|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Debug|x64.Build.0 = Debug|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Debug|x86.Build.0 = Debug|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Release|Any CPU.Build.0 = Release|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Release|x64.ActiveCfg = Release|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Release|x64.Build.0 = Release|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Release|x86.ActiveCfg = Release|Any CPU
+		{908B2263-B58B-4261-A125-B5F2DFF92799}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -8514,6 +8558,10 @@ Global
 		{DF4637DA-5F07-4903-8461-4E2DAB235F3C} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
 		{AAB50C64-39AA-4AED-8E9C-50D68E7751AD} = {7F99E967-3DC1-4198-9D55-47CD9471D0B6}
 		{9647D8B7-4616-4E05-B258-BAD5CAEEDD38} = {EB5E294B-9ED5-43BF-AFA9-1CD2327F3DC1}
+		{022B4B80-E813-4256-8034-11A68146F4EF} = {E5963C9F-20A6-4385-B364-814D2581FADF}
+		{FF413F1C-A998-4FA2-823F-52AC0916B35C} = {022B4B80-E813-4256-8034-11A68146F4EF}
+		{3A1EC883-EF9C-43E8-95E5-6B527428867B} = {022B4B80-E813-4256-8034-11A68146F4EF}
+		{908B2263-B58B-4261-A125-B5F2DFF92799} = {022B4B80-E813-4256-8034-11A68146F4EF}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

+ 1 - 0
eng/ProjectReferences.props

@@ -83,6 +83,7 @@
     <ProjectReferenceProvider Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" ProjectPath="$(RepoRoot)src\Middleware\HealthChecks.EntityFrameworkCore\src\Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" ProjectPath="$(RepoRoot)src\Middleware\HealthChecks\src\Microsoft.AspNetCore.Diagnostics.HealthChecks.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.HostFiltering" ProjectPath="$(RepoRoot)src\Middleware\HostFiltering\src\Microsoft.AspNetCore.HostFiltering.csproj" />
+    <ProjectReferenceProvider Include="Microsoft.AspNetCore.HttpLogging" ProjectPath="$(RepoRoot)src\Middleware\HttpLogging\src\Microsoft.AspNetCore.HttpLogging.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.HttpOverrides" ProjectPath="$(RepoRoot)src\Middleware\HttpOverrides\src\Microsoft.AspNetCore.HttpOverrides.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.HttpsPolicy" ProjectPath="$(RepoRoot)src\Middleware\HttpsPolicy\src\Microsoft.AspNetCore.HttpsPolicy.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization.Routing" ProjectPath="$(RepoRoot)src\Middleware\Localization.Routing\src\Microsoft.AspNetCore.Localization.Routing.csproj" />

+ 1 - 0
eng/SharedFramework.Local.props

@@ -69,6 +69,7 @@
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Diagnostics" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.HostFiltering" />
+    <AspNetCoreAppReference Include="Microsoft.AspNetCore.HttpLogging" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.HttpOverrides" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.HttpsPolicy" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Localization.Routing" />

+ 2 - 0
src/Framework/test/TestData.cs

@@ -55,6 +55,7 @@ namespace Microsoft.AspNetCore
                 "Microsoft.AspNetCore.Http.Connections.Common",
                 "Microsoft.AspNetCore.Http.Extensions",
                 "Microsoft.AspNetCore.Http.Features",
+                "Microsoft.AspNetCore.HttpLogging",
                 "Microsoft.AspNetCore.HttpOverrides",
                 "Microsoft.AspNetCore.HttpsPolicy",
                 "Microsoft.AspNetCore.Identity",
@@ -186,6 +187,7 @@ namespace Microsoft.AspNetCore
                 { "Microsoft.AspNetCore.Http.Connections.Common", "6.0.0.0" },
                 { "Microsoft.AspNetCore.Http.Extensions", "6.0.0.0" },
                 { "Microsoft.AspNetCore.Http.Features", "6.0.0.0" },
+                { "Microsoft.AspNetCore.HttpLogging", "6.0.0.0" },
                 { "Microsoft.AspNetCore.HttpOverrides", "6.0.0.0" },
                 { "Microsoft.AspNetCore.HttpsPolicy", "6.0.0.0" },
                 { "Microsoft.AspNetCore.Identity", "6.0.0.0" },

+ 16 - 0
src/Middleware/HttpLogging/samples/HttpLogging.Sample/HttpLogging.Sample.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
+    <Reference Include="Microsoft.AspNetCore.Routing" />
+    <Reference Include="Microsoft.AspNetCore" />
+    <Reference Include="Microsoft.Extensions.Logging.Console" />
+    <Reference Include="Microsoft.AspNetCore.HttpLogging" />
+    <Reference Include="Microsoft.Extensions.Hosting" />
+  </ItemGroup>
+
+</Project>

+ 34 - 0
src/Middleware/HttpLogging/samples/HttpLogging.Sample/Program.cs

@@ -0,0 +1,34 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace HttpLogging.Sample
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            CreateHostBuilder(args).Build().Run();
+        }
+
+        public static IHostBuilder CreateHostBuilder(string[] args) =>
+            Host.CreateDefaultBuilder(args)
+                .ConfigureLogging(logging =>
+                {
+                    // Json Logging
+                    logging.ClearProviders();
+                    logging.AddJsonConsole(options =>
+                    {
+                        options.JsonWriterOptions = new JsonWriterOptions()
+                        {
+                            Indented = true
+                        };
+                    });
+                })
+                .ConfigureWebHostDefaults(webBuilder =>
+                {
+                    webBuilder.UseStartup<Startup>();
+                });
+    }
+}

+ 28 - 0
src/Middleware/HttpLogging/samples/HttpLogging.Sample/Properties/launchSettings.json

@@ -0,0 +1,28 @@
+{
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:63240",
+      "sslPort": 0
+    }
+  },
+  "profiles": {
+    "HttpLogging.Sample": {
+      "commandName": "Project",
+      "dotnetRunMessages": "true",
+      "launchBrowser": true,
+      "applicationUrl": "http://localhost:5000",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchBrowser": true,
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}

+ 36 - 0
src/Middleware/HttpLogging/samples/HttpLogging.Sample/Startup.cs

@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpLogging;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HttpLogging.Sample
+{
+    public class Startup
+    {
+        // This method gets called by the runtime. Use this method to add services to the container.
+        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+        public void ConfigureServices(IServiceCollection services)
+        {
+            services.AddHttpLogging(logging =>
+            {
+                logging.LoggingFields = HttpLoggingFields.All;
+            });
+        }
+
+        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+        {
+            app.UseHttpLogging();
+            app.UseRouting();
+            app.UseEndpoints(endpoints =>
+            {
+                endpoints.Map("/", async context =>
+                {
+                    context.Response.ContentType = "text/plain";
+                    await context.Response.WriteAsync("Hello World!");
+                });
+            });
+        }
+    }
+}

+ 9 - 0
src/Middleware/HttpLogging/samples/HttpLogging.Sample/appsettings.Development.json

@@ -0,0 +1,9 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft": "Information",
+      "Microsoft.Hosting.Lifetime": "Information"
+    }
+  }
+}

+ 10 - 0
src/Middleware/HttpLogging/samples/HttpLogging.Sample/appsettings.json

@@ -0,0 +1,10 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Information",
+      "Microsoft": "Information",
+      "Microsoft.Hosting.Lifetime": "Information"
+    }
+  },
+  "AllowedHosts": "*"
+}

+ 326 - 0
src/Middleware/HttpLogging/src/BufferingStream.cs

@@ -0,0 +1,326 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Pipelines;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    internal abstract class BufferingStream : Stream, IBufferWriter<byte>
+    {
+        private const int MinimumBufferSize = 4096; // 4K
+        protected int _bytesBuffered;
+        private BufferSegment? _head;
+        private BufferSegment? _tail;
+        protected Memory<byte> _tailMemory; // remainder of tail memory
+        protected int _tailBytesBuffered;
+        protected ILogger _logger;
+        protected Stream _innerStream;
+
+        public BufferingStream(Stream innerStream, ILogger logger)
+        {
+            _logger = logger;
+            _innerStream = innerStream;
+        }
+
+        public override bool CanSeek => _innerStream.CanSeek;
+
+        public override bool CanRead => _innerStream.CanRead;
+
+        public override bool CanWrite => _innerStream.CanWrite;
+
+        public override long Length => _innerStream.Length;
+
+        public override long Position
+        {
+            get => _innerStream.Position;
+            set => _innerStream.Position = value;
+        }
+
+        public override int WriteTimeout
+        {
+            get => _innerStream.WriteTimeout;
+            set => _innerStream.WriteTimeout = value;
+        }
+
+        public string GetString(Encoding? encoding)
+        {
+            try
+            {
+                if (_head == null || _tail == null)
+                {
+                    // nothing written
+                    return "";
+                }
+
+                if (encoding == null)
+                {
+                    _logger.UnrecognizedMediaType();
+                    return "";
+                }
+
+                // Only place where we are actually using the buffered data.
+                // update tail here.
+                _tail.End = _tailBytesBuffered;
+
+                var ros = new ReadOnlySequence<byte>(_head, 0, _tail, _tailBytesBuffered);
+
+                var bufferWriter = new ArrayBufferWriter<char>();
+
+                var decoder = encoding.GetDecoder();
+                // First calls convert on the entire ReadOnlySequence, with flush: false.
+                // flush: false is required as we don't want to write invalid characters that
+                // are spliced due to truncation. If we set flush: true, if effectively means
+                // we expect EOF in this array, meaning it will try to write any bytes at the end of it.
+                EncodingExtensions.Convert(decoder, ros, bufferWriter, flush: false, out var charUsed, out var completed);
+
+                // Afterwards, we need to call convert in a loop until complete is true.
+                // The first call to convert many return true, but if it doesn't, we call
+                // Convert with a empty ReadOnlySequence and flush: true until we get completed: true.
+
+                // This should never infinite due to the contract for decoders.
+                // But for safety, call this only 10 times, throwing a decode failure if it fails.
+                for (var i = 0; i < 10; i++)
+                {
+                    if (completed)
+                    {
+                        return new string(bufferWriter.WrittenSpan);
+                    }
+                    else
+                    {
+                        EncodingExtensions.Convert(decoder, ReadOnlySequence<byte>.Empty, bufferWriter, flush: true, out charUsed, out completed);
+                    }
+                }
+
+                throw new DecoderFallbackException("Failed to decode after 10 calls to Decoder.Convert");
+            }
+            catch (DecoderFallbackException ex)
+            {
+                _logger.DecodeFailure(ex);
+                return "<Decoder failure>";
+            }
+            finally
+            {
+                Reset();
+            }
+        }
+
+        public void Advance(int bytes)
+        {
+            if ((uint)bytes > (uint)_tailMemory.Length)
+            {
+                ThrowArgumentOutOfRangeException(nameof(bytes));
+            }
+
+            _tailBytesBuffered += bytes;
+            _bytesBuffered += bytes;
+            _tailMemory = _tailMemory.Slice(bytes);
+        }
+
+        public Memory<byte> GetMemory(int sizeHint = 0)
+        {
+            AllocateMemory(sizeHint);
+            return _tailMemory;
+        }
+
+        public Span<byte> GetSpan(int sizeHint = 0)
+        {
+            AllocateMemory(sizeHint);
+            return _tailMemory.Span;
+        }
+
+        private void AllocateMemory(int sizeHint)
+        {
+            if (_head is null)
+            {
+                // We need to allocate memory to write since nobody has written before
+                var newSegment = AllocateSegment(sizeHint);
+
+                // Set all the pointers
+                _head = _tail = newSegment;
+                _tailBytesBuffered = 0;
+            }
+            else
+            {
+                var bytesLeftInBuffer = _tailMemory.Length;
+
+                if (bytesLeftInBuffer == 0 || bytesLeftInBuffer < sizeHint)
+                {
+                    Debug.Assert(_tail != null);
+
+                    if (_tailBytesBuffered > 0)
+                    {
+                        // Flush buffered data to the segment
+                        _tail.End += _tailBytesBuffered;
+                        _tailBytesBuffered = 0;
+                    }
+
+                    var newSegment = AllocateSegment(sizeHint);
+
+                    _tail.SetNext(newSegment);
+                    _tail = newSegment;
+                }
+            }
+        }
+
+        private BufferSegment AllocateSegment(int sizeHint)
+        {
+            var newSegment = CreateSegment();
+
+            // We can't use the recommended pool so use the ArrayPool
+            newSegment.SetOwnedMemory(ArrayPool<byte>.Shared.Rent(GetSegmentSize(sizeHint)));
+
+            _tailMemory = newSegment.AvailableMemory;
+
+            return newSegment;
+        }
+
+        private BufferSegment CreateSegment()
+        {
+            return new BufferSegment();
+        }
+
+        private static int GetSegmentSize(int sizeHint, int maxBufferSize = int.MaxValue)
+        {
+            // First we need to handle case where hint is smaller than minimum segment size
+            sizeHint = Math.Max(MinimumBufferSize, sizeHint);
+            // After that adjust it to fit into pools max buffer size
+            var adjustedToMaximumSize = Math.Min(maxBufferSize, sizeHint);
+            return adjustedToMaximumSize;
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                Reset();
+            }
+        }
+
+        public void Reset()
+        {
+            var segment = _head;
+            while (segment != null)
+            {
+                var returnSegment = segment;
+                segment = segment.NextSegment;
+
+                // We haven't reached the tail of the linked list yet, so we can always return the returnSegment.
+                returnSegment.ResetMemory();
+            }
+
+            _head = _tail = null;
+
+            _bytesBuffered = 0;
+            _tailBytesBuffered = 0;
+        }
+
+        // Copied from https://github.com/dotnet/corefx/blob/de3902bb56f1254ec1af4bf7d092fc2c048734cc/src/System.Memory/src/System/ThrowHelper.cs
+        private static void ThrowArgumentOutOfRangeException(string argumentName) { throw CreateArgumentOutOfRangeException(argumentName); }
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        private static Exception CreateArgumentOutOfRangeException(string argumentName) { return new ArgumentOutOfRangeException(argumentName); }
+
+        public override void Flush()
+        {
+            _innerStream.Flush();
+        }
+
+        public override Task FlushAsync(CancellationToken cancellationToken)
+        {
+            return _innerStream.FlushAsync(cancellationToken);
+        }
+
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            return _innerStream.Read(buffer, offset, count);
+        }
+
+        public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            return _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
+        }
+
+        public override long Seek(long offset, SeekOrigin origin)
+        {
+            return _innerStream.Seek(offset, origin);
+        }
+
+        public override void SetLength(long value)
+        {
+            _innerStream.SetLength(value);
+        }
+
+        public override void Write(byte[] buffer, int offset, int count)
+        {
+            _innerStream.Write(buffer, offset, count);
+        }
+
+        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            return _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
+        }
+
+        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+        {
+            return _innerStream.WriteAsync(buffer, cancellationToken);
+        }
+
+        public override void Write(ReadOnlySpan<byte> buffer)
+        {
+            _innerStream.Write(buffer);
+        }
+
+        public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+        {
+            return _innerStream.BeginRead(buffer, offset, count, callback, state);
+        }
+
+        public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+        {
+            return _innerStream.BeginWrite(buffer, offset, count, callback, state);
+        }
+
+        public override int EndRead(IAsyncResult asyncResult)
+        {
+            return _innerStream.EndRead(asyncResult);
+        }
+
+        public override void EndWrite(IAsyncResult asyncResult)
+        {
+            _innerStream.EndWrite(asyncResult);
+        }
+
+        public override void CopyTo(Stream destination, int bufferSize)
+        {
+            _innerStream.CopyTo(destination, bufferSize);
+        }
+
+        public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+        {
+            return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);
+        }
+
+        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+        {
+            return _innerStream.ReadAsync(buffer, cancellationToken);
+        }
+
+        public override ValueTask DisposeAsync()
+        {
+            return _innerStream.DisposeAsync();
+        }
+
+        public override int Read(Span<byte> buffer)
+        {
+            return _innerStream.Read(buffer);
+        }
+    }
+}

+ 30 - 0
src/Middleware/HttpLogging/src/HttpLoggingBuilderExtensions.cs

@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.HttpLogging;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    /// <summary>
+    /// Extension methods for the HttpLogging middleware.
+    /// </summary>
+    public static class HttpLoggingBuilderExtensions
+    {
+        /// <summary>
+        /// Adds a middleware that can log HTTP requests and responses.
+        /// </summary>
+        /// <param name="app">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
+        /// <returns>The <see cref="IApplicationBuilder"/>.</returns>
+        public static IApplicationBuilder UseHttpLogging(this IApplicationBuilder app)
+        {
+            if (app == null)
+            {
+                throw new ArgumentNullException(nameof(app));
+            }
+
+            app.UseMiddleware<HttpLoggingMiddleware>();
+            return app;
+        }
+    }
+}

+ 40 - 0
src/Middleware/HttpLogging/src/HttpLoggingExtensions.cs

@@ -0,0 +1,40 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    internal static class HttpLoggingExtensions
+    {
+        private static readonly Action<ILogger, string, Exception?> _requestBody =
+            LoggerMessage.Define<string>(LogLevel.Information, new EventId(3, "RequestBody"), "RequestBody: {Body}");
+
+        private static readonly Action<ILogger, string, Exception?> _responseBody =
+            LoggerMessage.Define<string>(LogLevel.Information, new EventId(4, "ResponseBody"), "ResponseBody: {Body}");
+
+        private static readonly Action<ILogger, Exception?> _decodeFailure =
+            LoggerMessage.Define(LogLevel.Debug, new EventId(5, "DecodeFaulure"), "Decode failure while converting body.");
+
+        private static readonly Action<ILogger, Exception?> _unrecognizedMediaType =
+            LoggerMessage.Define(LogLevel.Debug, new EventId(6, "UnrecognizedMediaType"), "Unrecognized Content-Type for body.");
+
+        public static void RequestLog(this ILogger logger, HttpRequestLog requestLog) => logger.Log(
+            LogLevel.Information,
+            new EventId(1, "RequestLogLog"),
+            requestLog,
+            exception: null,
+            formatter: HttpRequestLog.Callback);
+        public static void ResponseLog(this ILogger logger, HttpResponseLog responseLog) => logger.Log(
+            LogLevel.Information,
+            new EventId(2, "ResponseLog"),
+            responseLog,
+            exception: null,
+            formatter: HttpResponseLog.Callback);
+        public static void RequestBody(this ILogger logger, string body) => _requestBody(logger, body, null);
+        public static void ResponseBody(this ILogger logger, string body) => _responseBody(logger, body, null);
+        public static void DecodeFailure(this ILogger logger, Exception ex) => _decodeFailure(logger, ex);
+        public static void UnrecognizedMediaType(this ILogger logger) => _unrecognizedMediaType(logger, null);
+    }
+}

+ 176 - 0
src/Middleware/HttpLogging/src/HttpLoggingFields.cs

@@ -0,0 +1,176 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    /// <summary>
+    /// Flags used to control which parts of the
+    /// request and response are logged.
+    /// </summary>
+    [Flags]
+    public enum HttpLoggingFields : long
+    {
+        /// <summary>
+        /// No logging.
+        /// </summary>
+        None = 0x0,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request Path, which includes both the <see cref="HttpRequest.Path"/>
+        /// and <see cref="HttpRequest.PathBase"/>.
+        /// <p>
+        /// For example:
+        /// Path: /index
+        /// PathBase: /app
+        /// </p>
+        /// </summary>
+        RequestPath = 0x1,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request <see cref="HttpRequest.QueryString"/>.
+        /// <p>
+        /// For example:
+        /// Query: ?index=1
+        /// </p>
+        /// </summary>
+        RequestQuery = 0x2,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request <see cref="HttpRequest.Protocol"/>.
+        /// <p>
+        /// For example:
+        /// Protocol: HTTP/1.1
+        /// </p>
+        /// </summary>
+        RequestProtocol = 0x4,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request <see cref="HttpRequest.Method"/>.
+        /// <p>
+        /// For example:
+        /// Method: GET
+        /// </p>
+        /// </summary>
+        RequestMethod = 0x8,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request <see cref="HttpRequest.Scheme"/>.
+        /// <p>
+        /// For example:
+        /// Scheme: https
+        /// </p>
+        /// </summary>
+        RequestScheme = 0x10,
+
+        /// <summary>
+        /// Flag for logging the HTTP Response <see cref="HttpResponse.StatusCode"/>.
+        /// <p>
+        /// For example:
+        /// StatusCode: 200
+        /// </p>
+        /// </summary>
+        ResponseStatusCode = 0x20,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request <see cref="HttpRequest.Headers"/>.
+        /// Request Headers are logged as soon as the middleware is invoked.
+        /// Headers are redacted by default with the character '[Redacted]' unless specified in
+        /// the <see cref="HttpLoggingOptions.RequestHeaders"/>.
+        /// <p>
+        /// For example:
+        /// Connection: keep-alive
+        /// My-Custom-Request-Header: [Redacted]
+        /// </p>
+        /// </summary>
+        RequestHeaders = 0x40,
+
+        /// <summary>
+        /// Flag for logging the HTTP Response <see cref="HttpResponse.Headers"/>.
+        /// Response Headers are logged when the <see cref="HttpResponse.Body"/> is written to
+        /// or when <see cref="IHttpResponseBodyFeature.StartAsync(System.Threading.CancellationToken)"/>
+        /// is called.
+        /// Headers are redacted by default with the character '[Redacted]' unless specified in
+        /// the <see cref="HttpLoggingOptions.ResponseHeaders"/>.
+        /// <p>
+        /// For example:
+        /// Content-Length: 16
+        /// My-Custom-Response-Header: [Redacted]
+        /// </p>
+        /// </summary>
+        ResponseHeaders = 0x80,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request <see cref="IHttpRequestTrailersFeature.Trailers"/>.
+        /// Request Trailers are currently not logged.
+        /// </summary>
+        RequestTrailers = 0x100,
+
+        /// <summary>
+        /// Flag for logging the HTTP Response <see cref="IHttpResponseTrailersFeature.Trailers"/>.
+        /// Response Trailers are currently not logged.
+        /// </summary>
+        ResponseTrailers = 0x200,
+
+        /// <summary>
+        /// Flag for logging the HTTP Request <see cref="HttpRequest.Body"/>.
+        /// Logging the request body has performance implications, as it requires buffering
+        /// the entire request body up to <see cref="HttpLoggingOptions.RequestBodyLogLimit"/>.
+        /// </summary>
+        RequestBody = 0x400,
+
+        /// <summary>
+        /// Flag for logging the HTTP Response <see cref="HttpResponse.Body"/>.
+        /// Logging the response body has performance implications, as it requires buffering
+        /// the entire response body up to <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/>.
+        /// </summary>
+        ResponseBody = 0x800,
+
+        /// <summary>
+        /// Flag for logging a collection of HTTP Request properties,
+        /// including <see cref="RequestPath"/>, <see cref="RequestQuery"/>, <see cref="RequestProtocol"/>,
+        /// <see cref="RequestMethod"/>, and <see cref="RequestScheme"/>.
+        /// </summary>
+        RequestProperties = RequestPath | RequestQuery | RequestProtocol | RequestMethod | RequestScheme,
+
+        /// <summary>
+        /// Flag for logging HTTP Request properties and headers.
+        /// Includes <see cref="RequestProperties"/> and <see cref="RequestHeaders"/>
+        /// </summary>
+        RequestPropertiesAndHeaders = RequestProperties | RequestHeaders,
+
+        /// <summary>
+        /// Flag for logging HTTP Response properties and headers.
+        /// Includes <see cref="ResponseStatusCode"/> and <see cref="ResponseHeaders"/>
+        /// </summary>
+        ResponsePropertiesAndHeaders = ResponseStatusCode | ResponseHeaders,
+
+        /// <summary>
+        /// Flag for logging the entire HTTP Request.
+        /// Includes <see cref="RequestPropertiesAndHeaders"/> and <see cref="RequestBody"/>.
+        /// Logging the request body has performance implications, as it requires buffering
+        /// the entire request body up to <see cref="HttpLoggingOptions.RequestBodyLogLimit"/>.
+        /// </summary>
+        Request = RequestPropertiesAndHeaders | RequestBody,
+
+        /// <summary>
+        /// Flag for logging the entire HTTP Response.
+        /// Includes <see cref="ResponsePropertiesAndHeaders"/> and <see cref="ResponseBody"/>.
+        /// Logging the response body has performance implications, as it requires buffering
+        /// the entire response body up to <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/>.
+        /// </summary>
+        Response = ResponseStatusCode | ResponseHeaders | ResponseBody,
+
+        /// <summary>
+        /// Flag for logging both the HTTP Request and Response.
+        /// Includes <see cref="Request"/> and <see cref="Response"/>.
+        /// Logging the request and response body has performance implications, as it requires buffering
+        /// the entire request and response body up to the <see cref="HttpLoggingOptions.RequestBodyLogLimit"/>
+        /// and <see cref="HttpLoggingOptions.ResponseBodyLogLimit"/>.
+        /// </summary>
+        All = Request | Response
+    }
+}

+ 247 - 0
src/Middleware/HttpLogging/src/HttpLoggingMiddleware.cs

@@ -0,0 +1,247 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    /// <summary>
+    /// Middleware that logs HTTP requests and HTTP responses.
+    /// </summary>
+    internal sealed class HttpLoggingMiddleware
+    {
+        private readonly RequestDelegate _next;
+        private readonly ILogger _logger;
+        private readonly IOptionsMonitor<HttpLoggingOptions> _options;
+        private const int DefaultRequestFieldsMinusHeaders = 7;
+        private const int DefaultResponseFieldsMinusHeaders = 2;
+        private const string Redacted = "[Redacted]";
+
+        /// <summary>
+        /// Initializes <see cref="HttpLoggingMiddleware" />.
+        /// </summary>
+        /// <param name="next"></param>
+        /// <param name="options"></param>
+        /// <param name="logger"></param>
+        public HttpLoggingMiddleware(RequestDelegate next, IOptionsMonitor<HttpLoggingOptions> options, ILogger<HttpLoggingMiddleware> logger)
+        {
+            _next = next ?? throw new ArgumentNullException(nameof(next));
+
+            if (options == null)
+            {
+                throw new ArgumentNullException(nameof(options));
+            }
+
+            if (logger == null)
+            {
+                throw new ArgumentNullException(nameof(logger));
+            }
+
+            _options = options;
+            _logger = logger;
+        }
+
+        /// <summary>
+        /// Invokes the <see cref="HttpLoggingMiddleware" />.
+        /// </summary>
+        /// <param name="context"></param>
+        /// <returns></returns>HttpResponseLog.cs
+        public Task Invoke(HttpContext context)
+        {
+            if (!_logger.IsEnabled(LogLevel.Information))
+            {
+                // Logger isn't enabled.
+                return _next(context);
+            }
+
+            return InvokeInternal(context);
+        }
+
+        private async Task InvokeInternal(HttpContext context)
+        {
+            var options = _options.CurrentValue;
+            RequestBufferingStream? requestBufferingStream = null;
+            Stream? originalBody = null;
+
+            if ((HttpLoggingFields.Request & options.LoggingFields) != HttpLoggingFields.None)
+            {
+                var request = context.Request;
+                var list = new List<KeyValuePair<string, string?>>(
+                    request.Headers.Count + DefaultRequestFieldsMinusHeaders);
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestProtocol))
+                {
+                    AddToList(list, nameof(request.Protocol), request.Protocol);
+                }
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestMethod))
+                {
+                    AddToList(list, nameof(request.Method), request.Method);
+                }
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestScheme))
+                {
+                    AddToList(list, nameof(request.Scheme), request.Scheme);
+                }
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestPath))
+                {
+                    AddToList(list, nameof(request.PathBase), request.PathBase);
+                    AddToList(list, nameof(request.Path), request.Path);
+                }
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestQuery))
+                {
+                    AddToList(list, nameof(request.QueryString), request.QueryString.Value);
+                }
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestHeaders))
+                {
+                    FilterHeaders(list, request.Headers, options._internalRequestHeaders);
+                }
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.RequestBody))
+                {
+                    if (MediaTypeHelpers.TryGetEncodingForMediaType(request.ContentType,
+                        options.MediaTypeOptions.MediaTypeStates,
+                        out var encoding))
+                    {
+                        originalBody = request.Body;
+                        requestBufferingStream = new RequestBufferingStream(
+                            request.Body,
+                            options.RequestBodyLogLimit,
+                            _logger,
+                            encoding);
+                        request.Body = requestBufferingStream;
+                    }
+                    else
+                    {
+                        _logger.UnrecognizedMediaType();
+                    }
+                }
+
+                var httpRequestLog = new HttpRequestLog(list);
+
+                _logger.RequestLog(httpRequestLog);
+            }
+
+            ResponseBufferingStream? responseBufferingStream = null;
+            IHttpResponseBodyFeature? originalBodyFeature = null;
+
+            try
+            {
+                var response = context.Response;
+
+                if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseBody))
+                {
+                    originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>()!;
+
+                    // TODO pool these.
+                    responseBufferingStream = new ResponseBufferingStream(originalBodyFeature,
+                        options.ResponseBodyLogLimit,
+                        _logger,
+                        context,
+                        options.MediaTypeOptions.MediaTypeStates,
+                        options);
+                    response.Body = responseBufferingStream;
+                    context.Features.Set<IHttpResponseBodyFeature>(responseBufferingStream);
+                }
+
+                await _next(context);
+
+                if (requestBufferingStream?.HasLogged == false)
+                {
+                    // If the middleware pipeline didn't read until 0 was returned from readasync,
+                    // make sure we log the request body.
+                    requestBufferingStream.LogRequestBody();
+                }
+
+                if (responseBufferingStream == null || responseBufferingStream.FirstWrite == false)
+                {
+                    // No body, write headers here.
+                    LogResponseHeaders(response, options, _logger);
+                }
+
+                if (responseBufferingStream != null)
+                {
+                    var responseBody = responseBufferingStream.GetString(responseBufferingStream.Encoding);
+                    if (!string.IsNullOrEmpty(responseBody))
+                    {
+                        _logger.ResponseBody(responseBody);
+                    }
+                }
+            }
+            finally
+            {
+                responseBufferingStream?.Dispose();
+
+                if (originalBodyFeature != null)
+                {
+                    context.Features.Set(originalBodyFeature);
+                }
+
+                requestBufferingStream?.Dispose();
+
+                if (originalBody != null)
+                {
+                    context.Request.Body = originalBody;
+                }
+            }
+        }
+
+        private static void AddToList(List<KeyValuePair<string, string?>> list, string key, string? value)
+        {
+            list.Add(new KeyValuePair<string, string?>(key, value));
+        }
+
+        public static void LogResponseHeaders(HttpResponse response, HttpLoggingOptions options, ILogger logger)
+        {
+            var list = new List<KeyValuePair<string, string?>>(
+                response.Headers.Count + DefaultResponseFieldsMinusHeaders);
+
+            if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseStatusCode))
+            {
+                list.Add(new KeyValuePair<string, string?>(nameof(response.StatusCode),
+                    response.StatusCode.ToString(CultureInfo.InvariantCulture)));
+            }
+
+            if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseHeaders))
+            {
+                FilterHeaders(list, response.Headers, options._internalResponseHeaders);
+            }
+
+            var httpResponseLog = new HttpResponseLog(list);
+
+            logger.ResponseLog(httpResponseLog);
+        }
+
+        internal static void FilterHeaders(List<KeyValuePair<string, string?>> keyValues,
+            IHeaderDictionary headers,
+            HashSet<string> allowedHeaders)
+        {
+            foreach (var (key, value) in headers)
+            {
+                if (!allowedHeaders.Contains(key))
+                {
+                    // Key is not among the "only listed" headers.
+                    keyValues.Add(new KeyValuePair<string, string?>(key, Redacted));
+                    continue;
+                }
+                keyValues.Add(new KeyValuePair<string, string?>(key, value.ToString()));
+            }
+        }
+    }
+}

+ 78 - 0
src/Middleware/HttpLogging/src/HttpLoggingOptions.cs

@@ -0,0 +1,78 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    /// <summary>
+    /// Options for the <see cref="HttpLoggingMiddleware"/>.
+    /// </summary>
+    public sealed class HttpLoggingOptions
+    {
+        /// <summary>
+        /// Fields to log for the Request and Response. Defaults to logging request and response properties and headers.
+        /// </summary>
+        public HttpLoggingFields LoggingFields { get; set; } = HttpLoggingFields.RequestPropertiesAndHeaders | HttpLoggingFields.ResponsePropertiesAndHeaders;
+
+        /// <summary>
+        /// Request header values that are allowed to be logged.
+        /// <p>
+        /// If a request header is not present in the <see cref="RequestHeaders"/>,
+        /// the header name will be logged with a redacted value.
+        /// </p>
+        /// </summary>
+        public ISet<string> RequestHeaders => _internalRequestHeaders;
+
+        internal HashSet<string> _internalRequestHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            HeaderNames.Accept,
+            HeaderNames.AcceptEncoding,
+            HeaderNames.AcceptLanguage,
+            HeaderNames.Allow,
+            HeaderNames.Connection,
+            HeaderNames.ContentLength,
+            HeaderNames.ContentType,
+            HeaderNames.Host,
+            HeaderNames.UserAgent
+        };
+
+        /// <summary>
+        /// Response header values that are allowed to be logged.
+        /// <p>
+        /// If a response header is not present in the <see cref="ResponseHeaders"/>,
+        /// the header name will be logged with a redacted value.
+        /// </p>
+        /// </summary>
+        public ISet<string> ResponseHeaders => _internalResponseHeaders;
+
+        internal HashSet<string> _internalResponseHeaders = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            HeaderNames.ContentLength,
+            HeaderNames.ContentType,
+            HeaderNames.TransferEncoding
+        };
+
+        /// <summary>
+        /// Options for configuring encodings for a specific media type.
+        /// <p>
+        /// If the request or response do not match the supported media type,
+        /// the response body will not be logged.
+        /// </p>
+        /// </summary>
+        public MediaTypeOptions MediaTypeOptions { get; } = MediaTypeOptions.BuildDefaultMediaTypeOptions();
+
+        /// <summary>
+        /// Maximum request body size to log (in bytes). Defaults to 32 KB.
+        /// </summary>
+        public int RequestBodyLogLimit { get; set; } = 32 * 1024;
+
+        /// <summary>
+        /// Maximum response body size to log (in bytes). Defaults to 32 KB.
+        /// </summary>
+        public int ResponseBodyLogLimit { get; set; } = 32 * 1024;
+    }
+}

+ 35 - 0
src/Middleware/HttpLogging/src/HttpLoggingServicesExtensions.cs

@@ -0,0 +1,35 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.HttpLogging;
+
+namespace Microsoft.Extensions.DependencyInjection
+{
+    /// <summary>
+    /// Extension methods for the HttpLogging middleware.
+    /// </summary>
+    public static class HttpLoggingServicesExtensions
+    {
+        /// <summary>
+        /// Adds HTTP Logging services.
+        /// </summary>
+        /// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
+        /// <param name="configureOptions">A delegate to configure the <see cref="HttpLoggingOptions"/>.</param>
+        /// <returns></returns>
+        public static IServiceCollection AddHttpLogging(this IServiceCollection services, Action<HttpLoggingOptions> configureOptions)
+        {
+            if (services == null)
+            {
+                throw new ArgumentNullException(nameof(services));
+            }
+            if (configureOptions == null)
+            {
+                throw new ArgumentNullException(nameof(configureOptions));
+            }
+
+            services.Configure(configureOptions);
+            return services;
+        }
+    }
+}

+ 74 - 0
src/Middleware/HttpLogging/src/HttpRequestLog.cs

@@ -0,0 +1,74 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    internal sealed class HttpRequestLog : IReadOnlyList<KeyValuePair<string, string?>>
+    {
+        private readonly List<KeyValuePair<string, string?>> _keyValues;
+        private string? _cachedToString;
+
+        internal static readonly Func<object, Exception?, string> Callback = (state, exception) => ((HttpRequestLog)state).ToString();
+
+        public HttpRequestLog(List<KeyValuePair<string, string?>> keyValues)
+        {
+            _keyValues = keyValues;
+        }
+
+        public KeyValuePair<string, string?> this[int index] => _keyValues[index];
+
+        public int Count => _keyValues.Count;
+
+        public IEnumerator<KeyValuePair<string, string?>> GetEnumerator()
+        {
+            var count = _keyValues.Count;
+            for (var i = 0; i < count; i++)
+            {
+                yield return _keyValues[i];
+            }
+        }
+
+        public override string ToString()
+        {
+            if (_cachedToString == null)
+            {
+                // TODO use string.Create instead of a StringBuilder here.
+                var builder = new StringBuilder();
+                var count = _keyValues.Count;
+                builder.Append("Request:");
+                builder.Append(Environment.NewLine);
+
+                for (var i = 0; i < count - 1; i++)
+                {
+                    var kvp = _keyValues[i];
+                    builder.Append(kvp.Key);
+                    builder.Append(": ");
+                    builder.Append(kvp.Value);
+                    builder.Append(Environment.NewLine);
+                }
+
+                if (count > 0)
+                {
+                    var kvp = _keyValues[count - 1];
+                    builder.Append(kvp.Key);
+                    builder.Append(": ");
+                    builder.Append(kvp.Value);
+                }
+
+                _cachedToString = builder.ToString();
+            }
+
+            return _cachedToString;
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+    }
+}

+ 73 - 0
src/Middleware/HttpLogging/src/HttpResponseLog.cs

@@ -0,0 +1,73 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    internal sealed class HttpResponseLog : IReadOnlyList<KeyValuePair<string, string?>>
+    {
+        private readonly List<KeyValuePair<string, string?>> _keyValues;
+        private string? _cachedToString;
+
+        internal static readonly Func<object, Exception?, string> Callback = (state, exception) => ((HttpResponseLog)state).ToString();
+
+        public HttpResponseLog(List<KeyValuePair<string, string?>> keyValues)
+        {
+            _keyValues = keyValues;
+        }
+
+        public KeyValuePair<string, string?> this[int index] => _keyValues[index];
+
+        public int Count => _keyValues.Count;
+
+        public IEnumerator<KeyValuePair<string, string?>> GetEnumerator()
+        {
+            var count = _keyValues.Count;
+            for (var i = 0; i < count; i++)
+            {
+                yield return _keyValues[i];
+            }
+        }
+
+        public override string ToString()
+        {
+            if (_cachedToString == null)
+            {
+                var builder = new StringBuilder();
+                var count = _keyValues.Count;
+                builder.Append("Response:");
+                builder.Append(Environment.NewLine);
+
+                for (var i = 0; i < count - 1; i++)
+                {
+                    var kvp = _keyValues[i];
+                    builder.Append(kvp.Key);
+                    builder.Append(": ");
+                    builder.Append(kvp.Value);
+                    builder.Append(Environment.NewLine);
+                }
+
+                if (count > 0)
+                {
+                    var kvp = _keyValues[count - 1];
+                    builder.Append(kvp.Key);
+                    builder.Append(": ");
+                    builder.Append(kvp.Value);
+                }
+
+                _cachedToString = builder.ToString();
+            }
+
+            return _cachedToString;
+        }
+
+        IEnumerator IEnumerable.GetEnumerator()
+        {
+            return GetEnumerator();
+        }
+    }
+}

+ 70 - 0
src/Middleware/HttpLogging/src/MediaTypeHelpers.cs

@@ -0,0 +1,70 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using Microsoft.Net.Http.Headers;
+using static Microsoft.AspNetCore.HttpLogging.MediaTypeOptions;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    internal static class MediaTypeHelpers
+    {
+        private static readonly List<Encoding> SupportedEncodings = new List<Encoding>()
+        {
+            Encoding.UTF8,
+            Encoding.Unicode,
+            Encoding.ASCII,
+            Encoding.Latin1 // TODO allowed by default? Make this configurable?
+        };
+
+        public static bool TryGetEncodingForMediaType(string contentType, List<MediaTypeState> mediaTypeList, [NotNullWhen(true)] out Encoding? encoding)
+        {
+            encoding = null;
+            if (mediaTypeList == null || mediaTypeList.Count == 0 || string.IsNullOrEmpty(contentType))
+            {
+                return false;
+            }
+
+            var mediaType = new MediaTypeHeaderValue(contentType);
+
+            if (mediaType.Charset.HasValue)
+            {
+                // Create encoding based on charset
+                var requestEncoding = mediaType.Encoding;
+
+                if (requestEncoding != null)
+                {
+                    for (var i = 0; i < SupportedEncodings.Count; i++)
+                    {
+                        if (string.Equals(requestEncoding.WebName,
+                            SupportedEncodings[i].WebName,
+                            StringComparison.OrdinalIgnoreCase))
+                        {
+                            encoding = SupportedEncodings[i];
+                            return true;
+                        }
+                    }
+                }
+            }
+            else
+            {
+                // TODO Binary format https://github.com/dotnet/aspnetcore/issues/31884
+                foreach (var state in mediaTypeList)
+                {
+                    var type = state.MediaTypeHeaderValue;
+                    if (type.MatchesMediaType(mediaType.MediaType))
+                    {
+                        // We always set encoding
+                        encoding = state.Encoding!;
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+    }
+}

+ 127 - 0
src/Middleware/HttpLogging/src/MediaTypeOptions.cs

@@ -0,0 +1,127 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    /// <summary>
+    /// Options for HttpLogging to configure which encoding to use for each media type.
+    /// </summary>
+    public sealed class MediaTypeOptions
+    {
+        private readonly List<MediaTypeState> _mediaTypeStates = new();
+
+        internal MediaTypeOptions()
+        {
+        }
+
+        internal List<MediaTypeState> MediaTypeStates => _mediaTypeStates;
+
+        internal static MediaTypeOptions BuildDefaultMediaTypeOptions()
+        {
+            var options = new MediaTypeOptions();
+            options.AddText("application/json", Encoding.UTF8);
+            options.AddText("application/*+json", Encoding.UTF8);
+            options.AddText("application/xml", Encoding.UTF8);
+            options.AddText("application/*+xml", Encoding.UTF8);
+            options.AddText("text/*", Encoding.UTF8);
+
+            return options;
+        }
+
+        internal void AddText(MediaTypeHeaderValue mediaType)
+        {
+            if (mediaType == null)
+            {
+                throw new ArgumentNullException(nameof(mediaType));
+            }
+
+            mediaType.Encoding ??= Encoding.UTF8;
+
+            _mediaTypeStates.Add(new MediaTypeState(mediaType) { Encoding = mediaType.Encoding });
+        }
+
+        /// <summary>
+        /// Adds a contentType to be used for logging as text.
+        /// </summary>
+        /// <remarks>
+        /// If charset is not specified in the contentType, the encoding will default to UTF-8.
+        /// </remarks>
+        /// <param name="contentType">The content type to add.</param>
+        public void AddText(string contentType)
+        {
+            if (contentType == null)
+            {
+                throw new ArgumentNullException(nameof(contentType));
+            }
+
+            AddText(MediaTypeHeaderValue.Parse(contentType));
+        }
+
+        /// <summary>
+        /// Adds a contentType to be used for logging as text.
+        /// </summary>
+        /// <param name="contentType">The content type to add.</param>
+        /// <param name="encoding">The encoding to use.</param>
+        public void AddText(string contentType, Encoding encoding)
+        {
+            if (contentType == null)
+            {
+                throw new ArgumentNullException(nameof(contentType));
+            }
+
+            if (encoding == null)
+            {
+                throw new ArgumentNullException(nameof(encoding));
+            }
+
+            var mediaType = MediaTypeHeaderValue.Parse(contentType);
+            mediaType.Encoding = encoding;
+            AddText(mediaType);
+        }
+
+        /// <summary>
+        /// Adds a <see cref="MediaTypeHeaderValue"/> to be used for logging as binary.
+        /// </summary>
+        /// <param name="mediaType">The MediaType to add.</param>
+        public void AddBinary(MediaTypeHeaderValue mediaType)
+        {
+            throw new NotSupportedException();
+        }
+
+        /// <summary>
+        /// Adds a content to be used for logging as text.
+        /// </summary>
+        /// <param name="contentType">The content type to add.</param>
+        public void AddBinary(string contentType)
+        {
+            throw new NotSupportedException();
+        }
+
+        /// <summary>
+        /// Clears all MediaTypes.
+        /// </summary>
+        public void Clear()
+        {
+            _mediaTypeStates.Clear();
+        }
+
+        internal readonly struct MediaTypeState
+        {
+            public MediaTypeState(MediaTypeHeaderValue mediaTypeHeaderValue)
+            {
+                MediaTypeHeaderValue = mediaTypeHeaderValue;
+                Encoding = null;
+                IsBinary = false;
+            }
+
+            public MediaTypeHeaderValue MediaTypeHeaderValue { get; }
+            public Encoding? Encoding { get; init; }
+            public bool IsBinary { get; init; }
+        }
+    }
+}

+ 22 - 0
src/Middleware/HttpLogging/src/Microsoft.AspNetCore.HttpLogging.csproj

@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <Description>
+      ASP.NET Core middleware for logging HTTP requests and responses.
+    </Description>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <IsAspNetCoreApp>true</IsAspNetCoreApp>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <IsPackable>false</IsPackable>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
+    <Reference Include="Microsoft.Extensions.Options" />
+    
+    <Compile Include="$(RepoRoot)src\Shared\TaskToApm.cs" Link="Internal\TaskToApm.cs" />
+    <Compile Include="$(SharedSourceRoot)Buffers\**\*.cs" LinkBase="Internal\" />
+  </ItemGroup>
+
+</Project>

+ 3 - 0
src/Middleware/HttpLogging/src/Properties/AssemblyInfo.cs

@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.HttpLogging.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

+ 1 - 0
src/Middleware/HttpLogging/src/PublicAPI.Shipped.txt

@@ -0,0 +1 @@
+#nullable enable

+ 42 - 0
src/Middleware/HttpLogging/src/PublicAPI.Unshipped.txt

@@ -0,0 +1,42 @@
+#nullable enable
+Microsoft.AspNetCore.Builder.HttpLoggingBuilderExtensions
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.Request | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.Response -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.None = 0 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.Request = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestPropertiesAndHeaders | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestBody -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestBody = 1024 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestHeaders = 64 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestMethod = 8 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestPath = 1 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestProperties = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestPath | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestQuery | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestProtocol | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestMethod | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestScheme -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestPropertiesAndHeaders = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestProperties | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestHeaders -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestProtocol = 4 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestQuery = 2 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestScheme = 16 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.RequestTrailers = 256 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.Response = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponsePropertiesAndHeaders | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponseBody -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponseBody = 2048 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponseHeaders = 128 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponsePropertiesAndHeaders = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponseStatusCode | Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponseHeaders -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponseStatusCode = 32 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.ResponseTrailers = 512 -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.HttpLoggingOptions() -> void
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.LoggingFields.get -> Microsoft.AspNetCore.HttpLogging.HttpLoggingFields
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.LoggingFields.set -> void
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.MediaTypeOptions.get -> Microsoft.AspNetCore.HttpLogging.MediaTypeOptions!
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.RequestBodyLogLimit.get -> int
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.RequestBodyLogLimit.set -> void
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.RequestHeaders.get -> System.Collections.Generic.ISet<string!>!
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.ResponseBodyLogLimit.get -> int
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.ResponseBodyLogLimit.set -> void
+Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions.ResponseHeaders.get -> System.Collections.Generic.ISet<string!>!
+Microsoft.AspNetCore.HttpLogging.MediaTypeOptions
+Microsoft.AspNetCore.HttpLogging.MediaTypeOptions.AddBinary(Microsoft.Net.Http.Headers.MediaTypeHeaderValue! mediaType) -> void
+Microsoft.AspNetCore.HttpLogging.MediaTypeOptions.AddBinary(string! contentType) -> void
+Microsoft.AspNetCore.HttpLogging.MediaTypeOptions.AddText(string! contentType) -> void
+Microsoft.AspNetCore.HttpLogging.MediaTypeOptions.AddText(string! contentType, System.Text.Encoding! encoding) -> void
+Microsoft.AspNetCore.HttpLogging.MediaTypeOptions.Clear() -> void
+Microsoft.Extensions.DependencyInjection.HttpLoggingServicesExtensions
+static Microsoft.AspNetCore.Builder.HttpLoggingBuilderExtensions.UseHttpLogging(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
+static Microsoft.Extensions.DependencyInjection.HttpLoggingServicesExtensions.AddHttpLogging(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.HttpLogging.HttpLoggingOptions!>! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!

+ 116 - 0
src/Middleware/HttpLogging/src/RequestBufferingStream.cs

@@ -0,0 +1,116 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Pipelines;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    internal sealed class RequestBufferingStream : BufferingStream
+    {
+        private Encoding _encoding;
+        private readonly int _limit;
+
+        public bool HasLogged { get; private set; }
+
+        public RequestBufferingStream(Stream innerStream, int limit, ILogger logger, Encoding encoding)
+            : base(innerStream, logger)
+        {
+            _logger = logger;
+            _limit = limit;
+            _innerStream = innerStream;
+            _encoding = encoding;
+        }
+
+        public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
+        {
+            var res = await _innerStream.ReadAsync(destination, cancellationToken);
+
+            WriteToBuffer(destination.Slice(0, res).Span);
+
+            return res;
+        }
+
+        public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            var res = await _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
+
+            WriteToBuffer(buffer.AsSpan(offset, res));
+
+            return res;
+        }
+
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            var res = _innerStream.Read(buffer, offset, count);
+
+            WriteToBuffer(buffer.AsSpan(offset, res));
+
+            return res;
+        }
+
+        private void WriteToBuffer(ReadOnlySpan<byte> span)
+        {
+            // get what was read into the buffer
+            var remaining = _limit - _bytesBuffered;
+
+            if (remaining == 0)
+            {
+                return;
+            }
+
+            if (span.Length == 0 && !HasLogged)
+            {
+                // Done reading, log the string.
+                LogRequestBody();
+                return;
+            }
+
+            var innerCount = Math.Min(remaining, span.Length);
+
+            if (span.Slice(0, innerCount).TryCopyTo(_tailMemory.Span))
+            {
+                _tailBytesBuffered += innerCount;
+                _bytesBuffered += innerCount;
+                _tailMemory = _tailMemory.Slice(innerCount);
+            }
+            else
+            {
+                BuffersExtensions.Write(this, span.Slice(0, innerCount));
+            }
+
+            if (_limit - _bytesBuffered == 0 && !HasLogged)
+            {
+                LogRequestBody();
+            }
+        }
+
+        public void LogRequestBody()
+        {
+            var requestBody = GetString(_encoding);
+            if (requestBody != null)
+            {
+                _logger.RequestBody(requestBody);
+            }
+            HasLogged = true;
+        }
+
+        public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+        {
+            return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state);
+        }
+
+        public override int EndRead(IAsyncResult asyncResult)
+        {
+            return TaskToApm.End<int>(asyncResult);
+        }
+    }
+}

+ 177 - 0
src/Middleware/HttpLogging/src/ResponseBufferingStream.cs

@@ -0,0 +1,177 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Pipelines;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+using static Microsoft.AspNetCore.HttpLogging.MediaTypeOptions;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    /// <summary>
+    /// Stream that buffers reads 
+    /// </summary>
+    internal sealed class ResponseBufferingStream : BufferingStream, IHttpResponseBodyFeature
+    {
+        private readonly IHttpResponseBodyFeature _innerBodyFeature;
+        private readonly int _limit;
+        private PipeWriter? _pipeAdapter;
+
+        private readonly HttpContext _context;
+        private readonly List<MediaTypeState> _encodings;
+        private readonly HttpLoggingOptions _options;
+        private Encoding? _encoding;
+
+        private static readonly StreamPipeWriterOptions _pipeWriterOptions = new StreamPipeWriterOptions(leaveOpen: true);
+
+        internal ResponseBufferingStream(IHttpResponseBodyFeature innerBodyFeature,
+            int limit,
+            ILogger logger,
+            HttpContext context,
+            List<MediaTypeState> encodings,
+            HttpLoggingOptions options)
+            : base(innerBodyFeature.Stream, logger)
+        {
+            _innerBodyFeature = innerBodyFeature;
+            _innerStream = innerBodyFeature.Stream;
+            _limit = limit;
+            _context = context;
+            _encodings = encodings;
+            _options = options;
+        }
+
+        public bool FirstWrite { get; private set; }
+
+        public Stream Stream => this;
+
+        public PipeWriter Writer => _pipeAdapter ??= PipeWriter.Create(Stream, _pipeWriterOptions);
+
+        public Encoding? Encoding { get => _encoding; }
+
+        public override void Write(byte[] buffer, int offset, int count)
+        {
+            Write(buffer.AsSpan(offset, count));
+        }
+
+        public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
+        {
+            return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state);
+        }
+
+        public override void EndWrite(IAsyncResult asyncResult)
+        {
+            TaskToApm.End(asyncResult);
+        }
+
+        public override void Write(ReadOnlySpan<byte> span)
+        {
+            var remaining = _limit - _bytesBuffered;
+            var innerCount = Math.Min(remaining, span.Length);
+
+            OnFirstWrite();
+
+            if (innerCount > 0)
+            {    
+                if (span.Slice(0, innerCount).TryCopyTo(_tailMemory.Span))
+                {
+                    _tailBytesBuffered += innerCount;
+                    _bytesBuffered += innerCount;
+                    _tailMemory = _tailMemory.Slice(innerCount);
+                }
+                else
+                {
+                    BuffersExtensions.Write(this, span.Slice(0, innerCount));
+                }
+            }
+
+            _innerStream.Write(span);
+        }
+
+        public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            await WriteAsync(new Memory<byte>(buffer, offset, count), cancellationToken);
+        }
+
+        public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+        {
+            var remaining = _limit - _bytesBuffered;
+            var innerCount = Math.Min(remaining, buffer.Length);
+
+            OnFirstWrite();
+
+            if (innerCount > 0)
+            {
+                if (_tailMemory.Length - innerCount > 0)
+                {
+                    buffer.Slice(0, innerCount).CopyTo(_tailMemory);
+                    _tailBytesBuffered += innerCount;
+                    _bytesBuffered += innerCount;
+                    _tailMemory = _tailMemory.Slice(innerCount);
+                }
+                else
+                {
+                    BuffersExtensions.Write(this, buffer.Span);
+                }
+            }
+
+            await _innerStream.WriteAsync(buffer, cancellationToken);
+        }
+
+        private void OnFirstWrite()
+        {
+            if (!FirstWrite)
+            {
+                // Log headers as first write occurs (headers locked now)
+                HttpLoggingMiddleware.LogResponseHeaders(_context.Response, _options, _logger);
+
+                MediaTypeHelpers.TryGetEncodingForMediaType(_context.Response.ContentType, _encodings, out _encoding);
+                FirstWrite = true;
+            }
+        }
+
+        public void DisableBuffering()
+        {
+            _innerBodyFeature.DisableBuffering();
+        }
+
+        public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
+        {
+            OnFirstWrite();
+            return _innerBodyFeature.SendFileAsync(path, offset, count, cancellation);
+        }
+
+        public Task StartAsync(CancellationToken token = default)
+        {
+            OnFirstWrite();
+            return _innerBodyFeature.StartAsync(token);
+        }
+
+        public async Task CompleteAsync()
+        {
+            await _innerBodyFeature.CompleteAsync();
+        }
+
+        public override void Flush()
+        {
+            OnFirstWrite();
+            base.Flush();
+        }
+
+        public override Task FlushAsync(CancellationToken cancellationToken)
+        {
+            OnFirstWrite();
+            return base.FlushAsync(cancellationToken);
+        }
+    }
+}

+ 867 - 0
src/Middleware/HttpLogging/test/HttpLoggingMiddlewareTests.cs

@@ -0,0 +1,867 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    public class HttpLoggingMiddlewareTests : LoggedTest
+    {
+        public static TheoryData BodyData
+        {
+            get
+            {
+                var variations = new TheoryData<string>();
+                variations.Add("Hello World");
+                variations.Add(new string('a', 4097));
+                variations.Add(new string('b', 10000));
+                variations.Add(new string('あ', 10000));
+                return variations;
+            }
+        }
+
+        [Fact]
+        public void Ctor_ThrowsExceptionsWhenNullArgs()
+        {
+            Assert.Throws<ArgumentNullException>(() => new HttpLoggingMiddleware(
+                null,
+                CreateOptionsAccessor(),
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>()));
+
+            Assert.Throws<ArgumentNullException>(() => new HttpLoggingMiddleware(c =>
+            {
+                return Task.CompletedTask;
+            },
+            null,
+            LoggerFactory.CreateLogger<HttpLoggingMiddleware>()));
+
+            Assert.Throws<ArgumentNullException>(() => new HttpLoggingMiddleware(c =>
+            {
+                return Task.CompletedTask;
+            },
+            CreateOptionsAccessor(),
+            null));
+        }
+
+        [Fact]
+        public async Task NoopWhenLoggingDisabled()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.None;
+
+            var middleware = new HttpLoggingMiddleware(
+                c =>
+                {
+                    c.Response.StatusCode = 200;
+                    return Task.CompletedTask;
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Protocol = "HTTP/1.0";
+            httpContext.Request.Method = "GET";
+            httpContext.Request.Scheme = "http";
+            httpContext.Request.Path = new PathString("/foo");
+            httpContext.Request.PathBase = new PathString("/foo");
+            httpContext.Request.QueryString = new QueryString("?foo");
+            httpContext.Request.Headers["Connection"] = "keep-alive";
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
+
+            await middleware.Invoke(httpContext);
+
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Method: GET"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("QueryString: ?foo"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
+        }
+
+        [Fact]
+        public async Task DefaultRequestInfoOnlyHeadersAndRequestInfo()
+        {
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                    }
+                },
+                CreateOptionsAccessor(),
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Protocol = "HTTP/1.0";
+            httpContext.Request.Method = "GET";
+            httpContext.Request.Scheme = "http";
+            httpContext.Request.Path = new PathString("/foo");
+            httpContext.Request.PathBase = new PathString("/foo");
+            httpContext.Request.QueryString = new QueryString("?foo");
+            httpContext.Request.Headers["Connection"] = "keep-alive";
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Method: GET"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("QueryString: ?foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task RequestLogsAllRequestInfo()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.Request;
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                    }
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Protocol = "HTTP/1.0";
+            httpContext.Request.Method = "GET";
+            httpContext.Request.Scheme = "http";
+            httpContext.Request.Path = new PathString("/foo");
+            httpContext.Request.PathBase = new PathString("/foo");
+            httpContext.Request.QueryString = new QueryString("?foo");
+            httpContext.Request.Headers["Connection"] = "keep-alive";
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Method: GET"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("QueryString: ?foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task RequestPropertiesLogs()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestProperties;
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                    }
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Protocol = "HTTP/1.0";
+            httpContext.Request.Method = "GET";
+            httpContext.Request.Scheme = "http";
+            httpContext.Request.Path = new PathString("/foo");
+            httpContext.Request.PathBase = new PathString("/foo");
+            httpContext.Request.QueryString = new QueryString("?foo");
+            httpContext.Request.Headers["Connection"] = "keep-alive";
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Method: GET"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("QueryString: ?foo"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task RequestHeadersLogs()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestHeaders;
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                    }
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.Protocol = "HTTP/1.0";
+            httpContext.Request.Method = "GET";
+            httpContext.Request.Scheme = "http";
+            httpContext.Request.Path = new PathString("/foo");
+            httpContext.Request.PathBase = new PathString("/foo");
+            httpContext.Request.QueryString = new QueryString("?foo");
+            httpContext.Request.Headers["Connection"] = "keep-alive";
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("test"));
+
+            await middleware.Invoke(httpContext);
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Protocol: HTTP/1.0"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Method: GET"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Scheme: http"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Path: /foo"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("PathBase: /foo"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("QueryString: ?foo"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task UnknownRequestHeadersRedacted()
+        {
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                    }
+                },
+                CreateOptionsAccessor(),
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            httpContext.Request.Headers["foo"] = "bar";
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("foo: [Redacted]"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("foo: bar"));
+        }
+
+        [Fact]
+        public async Task CanConfigureRequestAllowList()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.RequestHeaders.Clear();
+            options.CurrentValue.RequestHeaders.Add("foo");
+            var middleware = new HttpLoggingMiddleware(
+                 c =>
+                 {
+                     return Task.CompletedTask;
+                 },
+                 options,
+                 LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            // Header on the default allow list.
+            httpContext.Request.Headers["Connection"] = "keep-alive";
+
+            httpContext.Request.Headers["foo"] = "bar";
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("foo: bar"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("foo: [Redacted]"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Connection: [Redacted]"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Connection: keep-alive"));
+        }
+
+        [Theory]
+        [MemberData(nameof(BodyData))]
+        public async Task RequestBodyReadingWorks(string expected)
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                    }
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(expected));
+
+            await middleware.Invoke(httpContext);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
+        }
+
+        [Fact]
+        public async Task RequestBodyReadingLimitLongCharactersWorks()
+        {
+            var input = string.Concat(new string('あ', 5));
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
+            options.CurrentValue.RequestBodyLogLimit = 4;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    var count = 0;
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                        count += res;
+                    }
+
+                    Assert.Equal(15, count);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
+
+            await middleware.Invoke(httpContext);
+            var expected = input.Substring(0, options.CurrentValue.RequestBodyLogLimit / 3);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Equals("RequestBody: " + expected));
+        }
+
+        [Fact]
+        public async Task RequestBodyReadingLimitWorks()
+        {
+            var input = string.Concat(new string('a', 60000), new string('b', 3000));
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    var count = 0;
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                        count += res;
+                    }
+
+                    Assert.Equal(63000, count);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
+
+            await middleware.Invoke(httpContext);
+            var expected = input.Substring(0, options.CurrentValue.ResponseBodyLogLimit);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Equals("RequestBody: " + expected));
+        }
+
+        [Fact]
+        public async Task PartialReadBodyStillLogs()
+        {
+            var input = string.Concat(new string('a', 60000), new string('b', 3000));
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    var res = await c.Request.Body.ReadAsync(arr);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(input));
+
+            await middleware.Invoke(httpContext);
+            var expected = input.Substring(0, 4096);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Equals("RequestBody: " + expected));
+        }
+
+        [Theory]
+        [InlineData("text/plain")]
+        [InlineData("text/html")]
+        [InlineData("application/json")]
+        [InlineData("application/xml")]
+        [InlineData("application/entity+json")]
+        [InlineData("application/entity+xml")]
+        public async Task VerifyDefaultMediaTypeHeaders(string contentType)
+        {
+            // media headers that should work.
+            var expected = new string('a', 1000);
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                    }
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.ContentType = contentType;
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(expected));
+
+            await middleware.Invoke(httpContext);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
+        }
+
+        [Theory]
+        [InlineData("application/invalid")]
+        [InlineData("multipart/form-data")]
+        public async Task RejectedContentTypes(string contentType)
+        {
+            // media headers that should work.
+            var expected = new string('a', 1000);
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    var count = 0;
+
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                        count += res;
+                    }
+
+                    Assert.Equal(1000, count);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.ContentType = contentType;
+            httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(expected));
+
+            await middleware.Invoke(httpContext);
+
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains(expected));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Unrecognized Content-Type for body."));
+        }
+
+        [Fact]
+        public async Task DifferentEncodingsWork()
+        {
+            var encoding = Encoding.Unicode;
+            var expected = new string('a', 1000);
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.RequestBody;
+            options.CurrentValue.MediaTypeOptions.Clear();
+            options.CurrentValue.MediaTypeOptions.AddText("text/plain", encoding);
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    var arr = new byte[4096];
+                    var count = 0;
+                    while (true)
+                    {
+                        var res = await c.Request.Body.ReadAsync(arr);
+                        if (res == 0)
+                        {
+                            break;
+                        }
+                        count += res;
+                    }
+
+                    Assert.Equal(2000, count);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+            httpContext.Request.ContentType = "text/plain";
+            httpContext.Request.Body = new MemoryStream(encoding.GetBytes(expected));
+
+            await middleware.Invoke(httpContext);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
+        }
+
+        [Fact]
+        public async Task DefaultResponseInfoOnlyHeadersAndRequestInfo()
+        {
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    c.Response.StatusCode = 200;
+                    c.Response.Headers[HeaderNames.TransferEncoding] = "test";
+                    c.Response.ContentType = "text/plain";
+                    await c.Response.WriteAsync("test");
+                },
+                CreateOptionsAccessor(),
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task ResponseInfoLogsAll()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.Response;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    c.Response.StatusCode = 200;
+                    c.Response.Headers[HeaderNames.TransferEncoding] = "test";
+                    c.Response.ContentType = "text/plain";
+                    await c.Response.WriteAsync("test");
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+
+        [Fact]
+        public async Task StatusCodeLogs()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseStatusCode;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    c.Response.StatusCode = 200;
+                    c.Response.Headers["Server"] = "Kestrel";
+                    c.Response.ContentType = "text/plain";
+                    await c.Response.WriteAsync("test");
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Server: Kestrel"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task ResponseHeadersLogs()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseHeaders;
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    c.Response.StatusCode = 200;
+                    c.Response.Headers[HeaderNames.TransferEncoding] = "test";
+                    c.Response.ContentType = "text/plain";
+                    await c.Response.WriteAsync("test");
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task ResponseHeadersRedacted()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseHeaders;
+
+            var middleware = new HttpLoggingMiddleware(
+                c =>
+                {
+                    c.Response.Headers["Test"] = "Kestrel";
+                    return Task.CompletedTask;
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Test: [Redacted]"));
+        }
+
+        [Fact]
+        public async Task AllowedResponseHeadersModify()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseHeaders;
+            options.CurrentValue.ResponseHeaders.Clear();
+            options.CurrentValue.ResponseHeaders.Add("Test");
+
+            var middleware = new HttpLoggingMiddleware(
+                c =>
+                {
+                    c.Response.Headers["Test"] = "Kestrel";
+                    c.Response.Headers["Server"] = "Kestrel";
+                    return Task.CompletedTask;
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Test: Kestrel"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Server: [Redacted]"));
+        }
+
+        [Theory]
+        [MemberData(nameof(BodyData))]
+        public async Task ResponseBodyWritingWorks(string expected)
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseBody;
+            var middleware = new HttpLoggingMiddleware(
+                c =>
+                {
+                    c.Response.ContentType = "text/plain";
+                    return c.Response.WriteAsync(expected);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
+        }
+
+        [Fact]
+        public async Task ResponseBodyWritingLimitWorks()
+        {
+            var input = string.Concat(new string('a', 30000), new string('b', 3000));
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseBody;
+            var middleware = new HttpLoggingMiddleware(
+                c =>
+                {
+                    c.Response.ContentType = "text/plain";
+                    return c.Response.WriteAsync(input);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+
+            var expected = input.Substring(0, options.CurrentValue.ResponseBodyLogLimit);
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains(expected));
+        }
+
+        [Fact]
+        public async Task FirstWriteResponseHeadersLogged()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.Response;
+
+            var writtenHeaders = new TaskCompletionSource<object>();
+            var letBodyFinish = new TaskCompletionSource<object>();
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    c.Response.StatusCode = 200;
+                    c.Response.Headers[HeaderNames.TransferEncoding] = "test";
+                    c.Response.ContentType = "text/plain";
+                    await c.Response.WriteAsync("test");
+                    writtenHeaders.SetResult(null);
+                    await letBodyFinish.Task;
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            var middlewareTask = middleware.Invoke(httpContext);
+
+            await writtenHeaders.Task;
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+
+            letBodyFinish.SetResult(null);
+
+            await middlewareTask;
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Body: test"));
+        }
+
+        [Fact]
+        public async Task StartAsyncResponseHeadersLogged()
+        {
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.Response;
+
+            var writtenHeaders = new TaskCompletionSource<object>();
+            var letBodyFinish = new TaskCompletionSource<object>();
+
+            var middleware = new HttpLoggingMiddleware(
+                async c =>
+                {
+                    c.Response.StatusCode = 200;
+                    c.Response.Headers[HeaderNames.TransferEncoding] = "test";
+                    c.Response.ContentType = "text/plain";
+                    await c.Response.StartAsync();
+                    writtenHeaders.SetResult(null);
+                    await letBodyFinish.Task;
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            var middlewareTask = middleware.Invoke(httpContext);
+
+            await writtenHeaders.Task;
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("StatusCode: 200"));
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Transfer-Encoding: test"));
+            Assert.DoesNotContain(TestSink.Writes, w => w.Message.Contains("Body: test"));
+
+            letBodyFinish.SetResult(null);
+
+            await middlewareTask;
+        }
+
+        [Fact]
+        public async Task UnrecognizedMediaType()
+        {
+            var expected = "Hello world";
+            var options = CreateOptionsAccessor();
+            options.CurrentValue.LoggingFields = HttpLoggingFields.ResponseBody;
+            var middleware = new HttpLoggingMiddleware(
+                c =>
+                {
+                    c.Response.ContentType = "foo/*";
+                    return c.Response.WriteAsync(expected);
+                },
+                options,
+                LoggerFactory.CreateLogger<HttpLoggingMiddleware>());
+
+            var httpContext = new DefaultHttpContext();
+
+            await middleware.Invoke(httpContext);
+
+            Assert.Contains(TestSink.Writes, w => w.Message.Contains("Unrecognized Content-Type for body."));
+        }
+
+        private IOptionsMonitor<HttpLoggingOptions> CreateOptionsAccessor()
+        {
+            var options = new HttpLoggingOptions();
+            var optionsAccessor = Mock.Of<IOptionsMonitor<HttpLoggingOptions>>(o => o.CurrentValue == options);
+            return optionsAccessor;
+        }
+    }
+}

+ 67 - 0
src/Middleware/HttpLogging/test/HttpLoggingOptionsTests.cs

@@ -0,0 +1,67 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Xunit;
+
+namespace Microsoft.AspNetCore.HttpLogging
+{
+    public class HttpLoggingOptionsTests
+    {
+        [Fact]
+        public void DefaultsMediaTypes()
+        {
+            var options = new HttpLoggingOptions();
+            var defaultMediaTypes = options.MediaTypeOptions.MediaTypeStates;
+            Assert.Equal(5, defaultMediaTypes.Count);
+
+            Assert.Contains(defaultMediaTypes, w => w.MediaTypeHeaderValue.MediaType.Equals("application/json"));
+            Assert.Contains(defaultMediaTypes, w => w.MediaTypeHeaderValue.MediaType.Equals("application/*+json"));
+            Assert.Contains(defaultMediaTypes, w => w.MediaTypeHeaderValue.MediaType.Equals("application/xml"));
+            Assert.Contains(defaultMediaTypes, w => w.MediaTypeHeaderValue.MediaType.Equals("application/*+xml"));
+            Assert.Contains(defaultMediaTypes, w => w.MediaTypeHeaderValue.MediaType.Equals("text/*"));
+        }
+
+        [Fact]
+        public void CanAddMediaTypesString()
+        {
+            var options = new HttpLoggingOptions();
+            options.MediaTypeOptions.AddText("test/*");
+
+            var defaultMediaTypes = options.MediaTypeOptions.MediaTypeStates;
+            Assert.Equal(6, defaultMediaTypes.Count);
+
+            Assert.Contains(defaultMediaTypes, w => w.MediaTypeHeaderValue.MediaType.Equals("test/*"));
+        }
+
+        [Fact]
+        public void CanAddMediaTypesWithCharset()
+        {
+            var options = new HttpLoggingOptions();
+            options.MediaTypeOptions.AddText("test/*; charset=ascii");
+
+            var defaultMediaTypes = options.MediaTypeOptions.MediaTypeStates;
+            Assert.Equal(6, defaultMediaTypes.Count);
+
+            Assert.Contains(defaultMediaTypes, w => w.MediaTypeHeaderValue.Encoding.WebName.Equals("us-ascii"));
+        }
+
+        [Fact]
+        public void CanClearMediaTypes()
+        {
+            var options = new HttpLoggingOptions();
+            options.MediaTypeOptions.Clear();
+            Assert.Empty(options.MediaTypeOptions.MediaTypeStates);
+        }
+
+        [Fact]
+        public void HeadersAreCaseInsensitive()
+        {
+            var options = new HttpLoggingOptions();
+            options.RequestHeaders.Clear();
+            options.RequestHeaders.Add("Test");
+            options.RequestHeaders.Add("test");
+
+            Assert.Single(options.RequestHeaders);
+        }
+    }
+}

+ 12 - 0
src/Middleware/HttpLogging/test/Microsoft.AspNetCore.HttpLogging.Tests.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.HttpLogging" />
+    <Reference Include="Microsoft.AspNetCore.TestHost" />
+  </ItemGroup>
+
+</Project>

+ 1 - 1
src/Middleware/HttpsPolicy/src/HttpsRedirectionOptions.cs

@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using Microsoft.AspNetCore.Http;

+ 3 - 0
src/Middleware/Middleware.slnf

@@ -31,6 +31,9 @@
       "src\\Middleware\\HostFiltering\\sample\\HostFilteringSample.csproj",
       "src\\Middleware\\HostFiltering\\src\\Microsoft.AspNetCore.HostFiltering.csproj",
       "src\\Middleware\\HostFiltering\\test\\Microsoft.AspNetCore.HostFiltering.Tests.csproj",
+      "src\\Middleware\\HttpLogging\\samples\\HttpLogging.Sample\\HttpLogging.Sample.csproj",
+      "src\\Middleware\\HttpLogging\\src\\Microsoft.AspNetCore.HttpLogging.csproj",
+      "src\\Middleware\\HttpLogging\\test\\Microsoft.AspNetCore.HttpLogging.Tests.csproj",
       "src\\Middleware\\HttpOverrides\\sample\\HttpOverridesSample.csproj",
       "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
       "src\\Middleware\\HttpOverrides\\test\\Microsoft.AspNetCore.HttpOverrides.Tests.csproj",

+ 1 - 0
src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj

@@ -18,6 +18,7 @@
     <Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" />
     <Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
     <Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
+    <Compile Include="$(SharedSourceRoot)Buffers\**\*.cs" LinkBase="Internal\Infrastructure\PipeWriterHelpers" />
     <Compile Include="$(SharedSourceRoot)runtime\*.cs" Link="Shared\runtime\%(Filename)%(Extension)" />
     <Compile Include="$(SharedSourceRoot)runtime\Http2\**\*.cs" Link="Shared\runtime\Http2\%(Filename)%(Extension)" />
     <Compile Include="$(SharedSourceRoot)runtime\Http3\**\*.cs" Link="Shared\runtime\Http3\%(Filename)%(Extension)" />

+ 0 - 0
src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegment.cs → src/Shared/Buffers/BufferSegment.cs


+ 0 - 0
src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/BufferSegmentStack.cs → src/Shared/Buffers/BufferSegmentStack.cs