diff --git a/samples/SampleApp/Startup.cs b/samples/SampleApp/Startup.cs
index 7ececbb741..97cca8e51e 100644
--- a/samples/SampleApp/Startup.cs
+++ b/samples/SampleApp/Startup.cs
@@ -8,7 +8,7 @@ using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Server.Kestrel;
-using Microsoft.AspNet.Server.Kestrel.Https;
+using Microsoft.AspNet.Server.Kestrel.Filter;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.PlatformAbstractions;
@@ -39,6 +39,8 @@ namespace SampleApp
Console.WriteLine("Could not find certificate at '{0}'. HTTPS is not enabled.", testCertPath);
}
+ app.UseKestrelConnectionLogging();
+
app.Run(async context =>
{
Console.WriteLine("{0} {1}{2}{3}",
diff --git a/src/Microsoft.AspNet.Server.Kestrel.Https/HttpsApplicationBuilderExtensions.cs b/src/Microsoft.AspNet.Server.Kestrel.Https/HttpsApplicationBuilderExtensions.cs
index c595376aa9..e31f0f6482 100644
--- a/src/Microsoft.AspNet.Server.Kestrel.Https/HttpsApplicationBuilderExtensions.cs
+++ b/src/Microsoft.AspNet.Server.Kestrel.Https/HttpsApplicationBuilderExtensions.cs
@@ -4,9 +4,9 @@
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http.Features;
-using Microsoft.AspNet.Server.Kestrel.Filter;
+using Microsoft.AspNet.Server.Kestrel.Https;
-namespace Microsoft.AspNet.Server.Kestrel.Https
+namespace Microsoft.AspNet.Server.Kestrel.Filter
{
public static class HttpsApplicationBuilderExtensions
{
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingConnectionFilter.cs b/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingConnectionFilter.cs
new file mode 100644
index 0000000000..1858715ea9
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingConnectionFilter.cs
@@ -0,0 +1,37 @@
+// 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.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNet.Server.Kestrel.Filter
+{
+ public class LoggingConnectionFilter : IConnectionFilter
+ {
+ private readonly ILogger _logger;
+ private readonly IConnectionFilter _previous;
+
+ public LoggingConnectionFilter(ILogger logger, IConnectionFilter previous)
+ {
+ if (logger == null)
+ {
+ throw new ArgumentNullException(nameof(logger));
+ }
+ if (previous == null)
+ {
+ throw new ArgumentNullException(nameof(previous));
+ }
+
+ _logger = logger;
+ _previous = previous;
+ }
+
+ public async Task OnConnection(ConnectionFilterContext context)
+ {
+ await _previous.OnConnection(context);
+
+ context.Connection = new LoggingStream(context.Connection, _logger);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingFilterApplicationBuilderExtensions.cs b/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingFilterApplicationBuilderExtensions.cs
new file mode 100644
index 0000000000..de0ffab1ac
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingFilterApplicationBuilderExtensions.cs
@@ -0,0 +1,39 @@
+// 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.AspNet.Builder;
+using Microsoft.AspNet.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNet.Server.Kestrel.Filter
+{
+ public static class LoggingFilterApplicationBuilderExtensions
+ {
+ ///
+ /// Emits verbose logs for bytes read from and written to the connection.
+ ///
+ ///
+ public static IApplicationBuilder UseKestrelConnectionLogging(this IApplicationBuilder app)
+ {
+ return app.UseKestrelConnectionLogging(nameof(LoggingConnectionFilter));
+ }
+
+ ///
+ /// Emits verbose logs for bytes read from and written to the connection.
+ ///
+ ///
+ public static IApplicationBuilder UseKestrelConnectionLogging(this IApplicationBuilder app, string loggerName)
+ {
+ var serverInfo = app.ServerFeatures.Get();
+ if (serverInfo != null)
+ {
+ var prevFilter = serverInfo.ConnectionFilter ?? new NoOpConnectionFilter();
+ var loggerFactory = app.ApplicationServices.GetRequiredService();
+ var logger = loggerFactory.CreateLogger(loggerName ?? nameof(LoggingConnectionFilter));
+ serverInfo.ConnectionFilter = new LoggingConnectionFilter(logger, prevFilter);
+ }
+ return app;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingStream.cs b/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingStream.cs
new file mode 100644
index 0000000000..41787c26ca
--- /dev/null
+++ b/src/Microsoft.AspNet.Server.Kestrel/Filter/LoggingStream.cs
@@ -0,0 +1,129 @@
+// 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.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNet.Server.Kestrel.Filter
+{
+ internal class LoggingStream : Stream
+ {
+ private readonly Stream _inner;
+ private readonly ILogger _logger;
+
+ public LoggingStream(Stream inner, ILogger logger)
+ {
+ _inner = inner;
+ _logger = logger;
+ }
+
+ public override bool CanRead
+ {
+ get
+ {
+ return _inner.CanRead;
+ }
+ }
+
+ public override bool CanSeek
+ {
+ get
+ {
+ return _inner.CanSeek;
+ }
+ }
+
+ public override bool CanWrite
+ {
+ get
+ {
+ return _inner.CanWrite;
+ }
+ }
+
+ public override long Length
+ {
+ get
+ {
+ return _inner.Length;
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ return _inner.Position;
+ }
+
+ set
+ {
+ _inner.Position = value;
+ }
+ }
+
+ public override void Flush()
+ {
+ _inner.Flush();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ int read = _inner.Read(buffer, offset, count);
+ Log("Read", read, buffer, offset);
+ return read;
+ }
+
+ public async override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken);
+ Log("ReadAsync", read, buffer, offset);
+ return read;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ return _inner.Seek(offset, origin);
+ }
+
+ public override void SetLength(long value)
+ {
+ _inner.SetLength(value);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ Log("Write", count, buffer, offset);
+ _inner.Write(buffer, offset, count);
+ }
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ Log("WriteAsync", count, buffer, offset);
+ return _inner.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+
+ private void Log(string method, int count, byte[] buffer, int offset)
+ {
+ var builder = new StringBuilder($"{method}[{count}] ");
+
+ // Write the hex
+ for (int i = offset; i < offset + count; i++)
+ {
+ builder.Append(buffer[i].ToString("X2"));
+ builder.Append(" ");
+ }
+ builder.AppendLine();
+ // Write the bytes as if they were ASCII
+ for (int i = offset; i < offset + count; i++)
+ {
+ builder.Append((char)buffer[i]);
+ }
+
+ _logger.LogVerbose(builder.ToString());
+ }
+ }
+}