File: Middleware\Internal\LoggingStream.cs
Web Access
Project: src\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
 
internal sealed 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 Task FlushAsync(CancellationToken cancellationToken)
    {
        return _inner.FlushAsync(cancellationToken);
    }
 
    public override int Read(byte[] buffer, int offset, int count)
    {
        int read = _inner.Read(buffer, offset, count);
        if (count > 0)
        {
            Log("Read", new ReadOnlySpan<byte>(buffer, offset, read));
        }
        return read;
    }
 
    public override int Read(Span<byte> destination)
    {
        int read = _inner.Read(destination);
        if (!destination.IsEmpty)
        {
            Log("Read", destination.Slice(0, read));
        }
        return read;
    }
 
    public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        int read = await _inner.ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
        if (count > 0)
        {
            Log("ReadAsync", new ReadOnlySpan<byte>(buffer, offset, read));
        }
        return read;
    }
 
    public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
    {
        int read = await _inner.ReadAsync(destination, cancellationToken);
        if (!destination.IsEmpty)
        {
            Log("ReadAsync", destination.Span.Slice(0, read));
        }
        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", new ReadOnlySpan<byte>(buffer, offset, count));
        _inner.Write(buffer, offset, count);
    }
 
    public override void Write(ReadOnlySpan<byte> source)
    {
        Log("Write", source);
        _inner.Write(source);
    }
 
    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        Log("WriteAsync", new ReadOnlySpan<byte>(buffer, offset, count));
        return _inner.WriteAsync(buffer, offset, count, cancellationToken);
    }
 
    public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
    {
        Log("WriteAsync", source.Span);
        return _inner.WriteAsync(source, cancellationToken);
    }
 
    private void Log(string method, ReadOnlySpan<byte> buffer)
    {
        if (!_logger.IsEnabled(LogLevel.Debug))
        {
            return;
        }
 
        var builder = new StringBuilder();
        builder.Append(method);
        builder.Append('[');
        builder.Append(buffer.Length);
        builder.Append(']');
 
        if (buffer.Length > 0)
        {
            builder.AppendLine();
        }
 
        // A maximum of 2 8 byte segments are written at once with a space between them, meaning 17 characters can be written
        // ........ ........
        Span<char> charBuilder = stackalloc char[17];
        var charBuilderIndex = 0;
 
        // Write the hex
        for (int i = 0; i < buffer.Length; i++)
        {
            builder.Append(CultureInfo.InvariantCulture, $"{buffer[i]:X2}");
            builder.Append(' ');
 
            var bufferChar = (char)buffer[i];
            if (char.IsControl(bufferChar))
            {
                charBuilder[charBuilderIndex++] = '.';
            }
            else
            {
                charBuilder[charBuilderIndex++] = bufferChar;
            }
 
            if ((i + 1) % 16 == 0)
            {
                builder.Append("  ");
                builder.Append(charBuilder);
                if (i != buffer.Length - 1)
                {
                    builder.AppendLine();
                }
                charBuilder.Clear();
                charBuilderIndex = 0;
            }
            else if ((i + 1) % 8 == 0)
            {
                builder.Append(' ');
                charBuilder[charBuilderIndex++] = ' ';
            }
        }
 
        // Different than charBuffer.Length since charBuffer contains an extra " " after the 8th byte.
        var numBytesInLastLine = buffer.Length % 16;
 
        if (numBytesInLastLine > 0)
        {
            // 2 (between hex and char blocks) + num bytes left (3 per byte)
            var padLength = 2 + (3 * (16 - numBytesInLastLine));
            // extra for space after 8th byte
            if (numBytesInLastLine < 8)
            {
                padLength++;
            }
 
            builder.Append(' ', padLength);
            builder.Append(charBuilder.Slice(0, charBuilderIndex));
        }
 
        _logger.LogDebug(builder.ToString());
    }
 
    // The below APM methods call the underlying Read/WriteAsync methods which will still be logged.
    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);
    }
 
    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);
    }
}