Преглед изворни кода

Replace DiagnosticAbstration with Microsoft.Extensions.Logging.Abstractions (#1509)

* Replace DiagnosticAbstrations with Microsoft.Extensions.Logging.Abstractions

* add documentation

* reduce allocations by SessionId hex conversion

generate the hex string once instead of every log
call and optimize ToHex().

* Update docfx/logging.md

Co-authored-by: Rob Hague <rob.hague00@gmail.com>

* reduce log levels

* hook up testcontainers logging

* drop packet logs further down to trace

* add kex traces

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
mus65 пре 10 месеци
родитељ
комит
70c12467dd

+ 3 - 0
.editorconfig

@@ -704,6 +704,9 @@ dotnet_code_quality.CA1828.api_surface = all
 # Similar to MA0053, but does not support public types and types that define (new) virtual members.
 dotnet_diagnostic.CA1852.severity = none
 
+# CA1848: don't enforce LoggerMessage pattern
+dotnet_diagnostic.CA1848.severity = suggestion
+
 # CA1859: Change return type for improved performance
 #
 # By default, this diagnostic is only reported for private members.

+ 2 - 2
CONTRIBUTING.md

@@ -34,9 +34,9 @@ The repository makes use of continuous integration (CI) with GitHub Actions to v
 
 ## Good to know
 
-### TraceSource logging
+### Logging
 
-The Debug build of SSH.NET contains rudimentary logging functionality via `System.Diagnostics.TraceSource`. See `Renci.SshNet.Abstractions.DiagnosticAbstraction` for usage examples.
+The tests always log to the console. See the [Logging documentation](https://sshnet.github.io/SSH.NET/logging.html) on how to set a custom `ILoggerFactory`.
 
 ### Wireshark
 

+ 3 - 0
Directory.Packages.props

@@ -15,6 +15,9 @@
     <PackageVersion Include="Meziantou.Analyzer" Version="2.0.163" />
     <!-- Must be kept at version 1.0.0 or higher, see https://github.com/sshnet/SSH.NET/pull/1288 for details. -->
     <PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="1.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
     <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
     <PackageVersion Include="MSTest.TestAdapter" Version="3.6.2" />
     <PackageVersion Include="MSTest.TestFramework" Version="3.6.2" />

+ 1 - 0
README.md

@@ -62,6 +62,7 @@ The main types provided by this library are:
 ## Additional Documentation
 
 * [Further examples](https://sshnet.github.io/SSH.NET/examples.html)
+* [Logging](https://sshnet.github.io/SSH.NET/logging.html)
 * [API browser](https://sshnet.github.io/SSH.NET/api/Renci.SshNet.html)
 
 ## Encryption Methods

+ 15 - 0
docfx/logging.md

@@ -0,0 +1,15 @@
+Logging
+=================
+
+SSH.NET uses the [Microsoft.Extensions.Logging](https://learn.microsoft.com/dotnet/core/extensions/logging) API to log diagnostic messages. In order to access the log messages of SSH.NET in your own application for diagnosis, register your own `ILoggerFactory` before using the SSH.NET APIs, for example:
+
+```cs
+ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
+{
+    builder.SetMinimumLevel(LogLevel.Debug);
+    builder.AddConsole();
+});
+
+Renci.SshNet.SshNetLoggingConfiguration.InitializeLogging(loggerFactory);
+
+All messages by SSH.NET are logged under the `Renci.SshNet` category.

+ 0 - 69
src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs

@@ -1,69 +0,0 @@
-using System.ComponentModel;
-using System.Diagnostics;
-
-namespace Renci.SshNet.Abstractions
-{
-    /// <summary>
-    /// Provides access to the <see cref="System.Diagnostics"/> internals of SSH.NET.
-    /// </summary>
-    [EditorBrowsable(EditorBrowsableState.Never)]
-    public static class DiagnosticAbstraction
-    {
-        /// <summary>
-        /// The <see cref="TraceSource"/> instance used by SSH.NET.
-        /// </summary>
-        /// <remarks>
-        /// <para>
-        /// Currently, the library only traces events when compiled in Debug mode.
-        /// </para>
-        /// <para>
-        /// Configuration on .NET Core must be done programmatically, e.g.
-        /// <code>
-        /// DiagnosticAbstraction.Source.Switch = new SourceSwitch("sourceSwitch", "Verbose");
-        /// DiagnosticAbstraction.Source.Listeners.Remove("Default");
-        /// DiagnosticAbstraction.Source.Listeners.Add(new ConsoleTraceListener());
-        /// DiagnosticAbstraction.Source.Listeners.Add(new TextWriterTraceListener("trace.log"));
-        /// </code>
-        /// </para>
-        /// <para>
-        /// On .NET Framework, it is possible to configure via App.config, e.g.
-        /// <code>
-        /// <![CDATA[
-        /// <configuration>
-        ///     <system.diagnostics>
-        ///         <trace autoflush="true"/>
-        ///         <sources>
-        ///             <source name="SshNet.Logging" switchValue="Verbose">
-        ///                 <listeners>
-        ///                     <remove name="Default" />
-        ///                     <add name="console"
-        ///                          type="System.Diagnostics.ConsoleTraceListener" />
-        ///                     <add name="logFile"
-        ///                          type="System.Diagnostics.TextWriterTraceListener"
-        ///                          initializeData="SshNetTrace.log" />
-        ///                 </listeners>
-        ///             </source>
-        ///         </sources>
-        ///     </system.diagnostics>
-        /// </configuration>
-        /// ]]>
-        /// </code>
-        /// </para>
-        /// </remarks>
-        public static readonly TraceSource Source = new TraceSource("SshNet.Logging");
-
-        /// <summary>
-        /// Logs a message to <see cref="Source"/> at the <see cref="TraceEventType.Verbose"/>
-        /// level.
-        /// </summary>
-        /// <param name="text">The message to log.</param>
-        /// <param name="type">The trace event type.</param>
-        [Conditional("DEBUG")]
-        public static void Log(string text, TraceEventType type = TraceEventType.Verbose)
-        {
-            Source.TraceEvent(type,
-                              System.Environment.CurrentManagedThreadId,
-                              text);
-        }
-    }
-}

+ 6 - 3
src/Renci.SshNet/BaseClient.cs

@@ -4,7 +4,8 @@ using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 
-using Renci.SshNet.Abstractions;
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Transport;
 
@@ -20,6 +21,7 @@ namespace Renci.SshNet
         /// </summary>
         private readonly bool _ownsConnectionInfo;
 
+        private readonly ILogger _logger;
         private readonly IServiceFactory _serviceFactory;
         private readonly object _keepAliveLock = new object();
         private TimeSpan _keepAliveInterval;
@@ -190,6 +192,7 @@ namespace Renci.SshNet
             _connectionInfo = connectionInfo;
             _ownsConnectionInfo = ownsConnectionInfo;
             _serviceFactory = serviceFactory;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType());
             _keepAliveInterval = Timeout.InfiniteTimeSpan;
         }
 
@@ -343,7 +346,7 @@ namespace Renci.SshNet
         /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
         public void Disconnect()
         {
-            DiagnosticAbstraction.Log("Disconnecting client.");
+            _logger.LogInformation("Disconnecting client.");
 
             CheckDisposed();
 
@@ -442,7 +445,7 @@ namespace Renci.SshNet
 
             if (disposing)
             {
-                DiagnosticAbstraction.Log("Disposing client.");
+                _logger.LogDebug("Disposing client.");
 
                 Disconnect();
 

+ 5 - 2
src/Renci.SshNet/Channels/Channel.cs

@@ -2,7 +2,8 @@
 using System.Net.Sockets;
 using System.Threading;
 
-using Renci.SshNet.Abstractions;
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages;
 using Renci.SshNet.Messages.Connection;
@@ -18,6 +19,7 @@ namespace Renci.SshNet.Channels
         private readonly Lock _messagingLock = new Lock();
         private readonly uint _initialWindowSize;
         private readonly ISession _session;
+        private readonly ILogger _logger;
         private EventWaitHandle _channelClosedWaitHandle = new ManualResetEvent(initialState: false);
         private EventWaitHandle _channelServerWindowAdjustWaitHandle = new ManualResetEvent(initialState: false);
         private uint? _remoteWindowSize;
@@ -81,6 +83,7 @@ namespace Renci.SshNet.Channels
             LocalChannelNumber = localChannelNumber;
             LocalPacketSize = localPacketSize;
             LocalWindowSize = localWindowSize;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType());
 
             session.ChannelWindowAdjustReceived += OnChannelWindowAdjust;
             session.ChannelDataReceived += OnChannelData;
@@ -555,7 +558,7 @@ namespace Renci.SshNet.Channels
                         var closeWaitResult = _session.TryWait(_channelClosedWaitHandle, ConnectionInfo.ChannelCloseTimeout);
                         if (closeWaitResult != WaitResult.Success)
                         {
-                            DiagnosticAbstraction.Log(string.Format("Wait for channel close not successful: {0:G}.", closeWaitResult));
+                            _logger.LogInformation("Wait for channel close not successful: {CloseWaitResult}", closeWaitResult);
                         }
                     }
                 }

+ 5 - 3
src/Renci.SshNet/Channels/ChannelDirectTcpip.cs

@@ -3,6 +3,8 @@ using System.Net;
 using System.Net.Sockets;
 using System.Threading;
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Connection;
@@ -15,7 +17,7 @@ namespace Renci.SshNet.Channels
     internal sealed class ChannelDirectTcpip : ClientChannel, IChannelDirectTcpip
     {
         private readonly Lock _socketLock = new Lock();
-
+        private readonly ILogger _logger;
         private EventWaitHandle _channelOpen = new AutoResetEvent(initialState: false);
         private EventWaitHandle _channelData = new AutoResetEvent(initialState: false);
         private IForwardedPort _forwardedPort;
@@ -31,6 +33,7 @@ namespace Renci.SshNet.Channels
         public ChannelDirectTcpip(ISession session, uint localChannelNumber, uint localWindowSize, uint localPacketSize)
             : base(session, localChannelNumber, localWindowSize, localPacketSize)
         {
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<ChannelDirectTcpip>();
         }
 
         /// <summary>
@@ -157,8 +160,7 @@ namespace Renci.SshNet.Channels
                 }
                 catch (SocketException ex)
                 {
-                    // TODO: log as warning
-                    DiagnosticAbstraction.Log("Failure shutting down socket: " + ex);
+                    _logger.LogInformation(ex, "Failure shutting down socket");
                 }
             }
         }

+ 5 - 2
src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs

@@ -5,6 +5,8 @@ using System.Net.Sockets;
 using System.Threading;
 #endif
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Connection;
@@ -17,6 +19,7 @@ namespace Renci.SshNet.Channels
     internal sealed class ChannelForwardedTcpip : ServerChannel, IChannelForwardedTcpip
     {
         private readonly Lock _socketShutdownAndCloseLock = new Lock();
+        private readonly ILogger _logger;
         private Socket _socket;
         private IForwardedPort _forwardedPort;
 
@@ -45,6 +48,7 @@ namespace Renci.SshNet.Channels
                    remoteWindowSize,
                    remotePacketSize)
         {
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<ChannelForwardedTcpip>();
         }
 
         /// <summary>
@@ -142,8 +146,7 @@ namespace Renci.SshNet.Channels
                 }
                 catch (SocketException ex)
                 {
-                    // TODO: log as warning
-                    DiagnosticAbstraction.Log("Failure shutting down socket: " + ex);
+                    _logger.LogInformation(ex, "Failure shutting down socket");
                 }
             }
         }

+ 7 - 0
src/Renci.SshNet/Common/Extensions.cs

@@ -351,5 +351,12 @@ namespace Renci.SshNet.Common
 
             return socket.Connected;
         }
+
+        internal static string Join(this IEnumerable<string> values, string separator)
+        {
+            // Used to avoid analyzers asking to "use an overload with a char parameter"
+            // which is not available on all targets.
+            return string.Join(separator, values);
+        }
     }
 }

+ 7 - 2
src/Renci.SshNet/Connection/ConnectorBase.cs

@@ -4,6 +4,8 @@ using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Transport;
@@ -12,11 +14,14 @@ namespace Renci.SshNet.Connection
 {
     internal abstract class ConnectorBase : IConnector
     {
+        private readonly ILogger _logger;
+
         protected ConnectorBase(ISocketFactory socketFactory)
         {
             ThrowHelper.ThrowIfNull(socketFactory);
 
             SocketFactory = socketFactory;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType());
         }
 
         internal ISocketFactory SocketFactory { get; private set; }
@@ -34,7 +39,7 @@ namespace Renci.SshNet.Connection
         /// <exception cref="SocketException">An error occurred trying to establish the connection.</exception>
         protected Socket SocketConnect(EndPoint endPoint, TimeSpan timeout)
         {
-            DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}'.", endPoint));
+            _logger.LogInformation("Initiating connection to '{EndPoint}'.", endPoint);
 
             var socket = SocketFactory.Create(SocketType.Stream, ProtocolType.Tcp);
 
@@ -65,7 +70,7 @@ namespace Renci.SshNet.Connection
         {
             cancellationToken.ThrowIfCancellationRequested();
 
-            DiagnosticAbstraction.Log(string.Format("Initiating connection to '{0}'.", endPoint));
+            _logger.LogInformation("Initiating connection to '{EndPoint}'.", endPoint);
 
             var socket = SocketFactory.Create(SocketType.Stream, ProtocolType.Tcp);
             try

+ 5 - 2
src/Renci.SshNet/ForwardedPortDynamic.cs

@@ -7,6 +7,8 @@ using System.Net.Sockets;
 using System.Text;
 using System.Threading;
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
@@ -19,6 +21,7 @@ namespace Renci.SshNet
     /// </summary>
     public class ForwardedPortDynamic : ForwardedPort
     {
+        private readonly ILogger _logger;
         private ForwardedPortStatus _status;
 
         /// <summary>
@@ -72,6 +75,7 @@ namespace Renci.SshNet
             BoundHost = host;
             BoundPort = port;
             _status = ForwardedPortStatus.Stopped;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<ForwardedPortDynamic>();
         }
 
         /// <summary>
@@ -409,8 +413,7 @@ namespace Renci.SshNet
 
             if (!_pendingChannelCountdown.Wait(timeout))
             {
-                // TODO: log as warning
-                DiagnosticAbstraction.Log("Timeout waiting for pending channels in dynamic forwarded port to close.");
+                _logger.LogInformation("Timeout waiting for pending channels in dynamic forwarded port to close.");
             }
         }
 

+ 5 - 3
src/Renci.SshNet/ForwardedPortLocal.cs

@@ -3,7 +3,8 @@ using System.Net;
 using System.Net.Sockets;
 using System.Threading;
 
-using Renci.SshNet.Abstractions;
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Common;
 
 namespace Renci.SshNet
@@ -13,6 +14,7 @@ namespace Renci.SshNet
     /// </summary>
     public partial class ForwardedPortLocal : ForwardedPort
     {
+        private readonly ILogger _logger;
         private ForwardedPortStatus _status;
         private bool _isDisposed;
         private Socket _listener;
@@ -101,6 +103,7 @@ namespace Renci.SshNet
             Host = host;
             Port = port;
             _status = ForwardedPortStatus.Stopped;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<ForwardedPortLocal>();
         }
 
         /// <summary>
@@ -387,8 +390,7 @@ namespace Renci.SshNet
 
             if (!_pendingChannelCountdown.Wait(timeout))
             {
-                // TODO: log as warning
-                DiagnosticAbstraction.Log("Timeout waiting for pending channels in local forwarded port to close.");
+                _logger.LogInformation("Timeout waiting for pending channels in local forwarded port to close.");
             }
         }
 

+ 5 - 2
src/Renci.SshNet/ForwardedPortRemote.cs

@@ -3,6 +3,8 @@ using System.Globalization;
 using System.Net;
 using System.Threading;
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Common;
 using Renci.SshNet.Messages.Connection;
@@ -14,6 +16,7 @@ namespace Renci.SshNet
     /// </summary>
     public class ForwardedPortRemote : ForwardedPort
     {
+        private readonly ILogger _logger;
         private ForwardedPortStatus _status;
         private bool _requestStatus;
         private EventWaitHandle _globalRequestResponse = new AutoResetEvent(initialState: false);
@@ -97,6 +100,7 @@ namespace Renci.SshNet
             HostAddress = hostAddress;
             Port = port;
             _status = ForwardedPortStatus.Stopped;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<ForwardedPortRemote>();
         }
 
         /// <summary>
@@ -208,8 +212,7 @@ namespace Renci.SshNet
 
             if (!_pendingChannelCountdown.Wait(timeout))
             {
-                // TODO: log as warning
-                DiagnosticAbstraction.Log("Timeout waiting for pending channels in remote forwarded port to close.");
+                _logger.LogInformation("Timeout waiting for pending channels in remote forwarded port to close.");
             }
 
             _status = ForwardedPortStatus.Stopped;

+ 1 - 0
src/Renci.SshNet/Renci.SshNet.csproj

@@ -36,6 +36,7 @@
 
   <ItemGroup>
     <PackageReference Include="BouncyCastle.Cryptography" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
     <PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="all" />
     <PackageReference Include="PolySharp" PrivateAssets="all">
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

+ 99 - 21
src/Renci.SshNet/Security/KeyExchange.cs

@@ -3,7 +3,8 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Security.Cryptography;
 
-using Renci.SshNet.Abstractions;
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Common;
 using Renci.SshNet.Compression;
 using Renci.SshNet.Messages;
@@ -17,6 +18,7 @@ namespace Renci.SshNet.Security
     /// </summary>
     public abstract class KeyExchange : Algorithm, IKeyExchange
     {
+        private readonly ILogger _logger;
         private CipherInfo _clientCipherInfo;
         private CipherInfo _serverCipherInfo;
         private HashInfo _clientHashInfo;
@@ -61,6 +63,11 @@ namespace Renci.SshNet.Security
         /// </summary>
         public event EventHandler<HostKeyEventArgs> HostKeyReceived;
 
+        private protected KeyExchange()
+        {
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType());
+        }
+
         /// <inheritdoc/>
         public virtual void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage)
         {
@@ -71,12 +78,23 @@ namespace Renci.SshNet.Security
                 SendMessage(session.ClientInitMessage);
             }
 
-            // Determine encryption algorithm
+            // Determine client encryption algorithm
             var clientEncryptionAlgorithmName = (from b in session.ConnectionInfo.Encryptions.Keys
                                                  from a in message.EncryptionAlgorithmsClientToServer
                                                  where a == b
                                                  select a).FirstOrDefault();
 
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _logger.LogTrace("[{SessionId}] Encryption client to server: we offer {WeOffer}",
+                    Session.SessionIdHex,
+                    session.ConnectionInfo.Encryptions.Keys.Join(","));
+
+                _logger.LogTrace("[{SessionId}] Encryption client to server: they offer {TheyOffer}",
+                    Session.SessionIdHex,
+                    message.EncryptionAlgorithmsClientToServer.Join(","));
+            }
+
             if (string.IsNullOrEmpty(clientEncryptionAlgorithmName))
             {
                 throw new SshConnectionException("Client encryption algorithm not found", DisconnectReason.KeyExchangeFailed);
@@ -85,11 +103,23 @@ namespace Renci.SshNet.Security
             session.ConnectionInfo.CurrentClientEncryption = clientEncryptionAlgorithmName;
             _clientCipherInfo = session.ConnectionInfo.Encryptions[clientEncryptionAlgorithmName];
 
-            // Determine encryption algorithm
+            // Determine server encryption algorithm
             var serverDecryptionAlgorithmName = (from b in session.ConnectionInfo.Encryptions.Keys
                                                  from a in message.EncryptionAlgorithmsServerToClient
                                                  where a == b
                                                  select a).FirstOrDefault();
+
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _logger.LogTrace("[{SessionId}] Encryption server to client: we offer {WeOffer}",
+                    Session.SessionIdHex,
+                    session.ConnectionInfo.Encryptions.Keys.Join(","));
+
+                _logger.LogTrace("[{SessionId}] Encryption server to client: they offer {TheyOffer}",
+                    Session.SessionIdHex,
+                    message.EncryptionAlgorithmsServerToClient.Join(","));
+            }
+
             if (string.IsNullOrEmpty(serverDecryptionAlgorithmName))
             {
                 throw new SshConnectionException("Server decryption algorithm not found", DisconnectReason.KeyExchangeFailed);
@@ -105,6 +135,18 @@ namespace Renci.SshNet.Security
                                                from a in message.MacAlgorithmsClientToServer
                                                where a == b
                                                select a).FirstOrDefault();
+
+                if (_logger.IsEnabled(LogLevel.Trace))
+                {
+                    _logger.LogTrace("[{SessionId}] MAC client to server: we offer {WeOffer}",
+                        Session.SessionIdHex,
+                        session.ConnectionInfo.HmacAlgorithms.Keys.Join(","));
+
+                    _logger.LogTrace("[{SessionId}] MAC client to server: they offer {TheyOffer}",
+                        Session.SessionIdHex,
+                        message.MacAlgorithmsClientToServer.Join(","));
+                }
+
                 if (string.IsNullOrEmpty(clientHmacAlgorithmName))
                 {
                     throw new SshConnectionException("Client HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
@@ -121,6 +163,18 @@ namespace Renci.SshNet.Security
                                                from a in message.MacAlgorithmsServerToClient
                                                where a == b
                                                select a).FirstOrDefault();
+
+                if (_logger.IsEnabled(LogLevel.Trace))
+                {
+                    _logger.LogTrace("[{SessionId}] MAC server to client: we offer {WeOffer}",
+                        Session.SessionIdHex,
+                        session.ConnectionInfo.HmacAlgorithms.Keys.Join(","));
+
+                    _logger.LogTrace("[{SessionId}] MAC server to client: they offer {TheyOffer}",
+                        Session.SessionIdHex,
+                        message.MacAlgorithmsServerToClient.Join(","));
+                }
+
                 if (string.IsNullOrEmpty(serverHmacAlgorithmName))
                 {
                     throw new SshConnectionException("Server HMAC algorithm not found", DisconnectReason.KeyExchangeFailed);
@@ -135,6 +189,18 @@ namespace Renci.SshNet.Security
                                             from a in message.CompressionAlgorithmsClientToServer
                                             where a == b
                                             select a).FirstOrDefault();
+
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _logger.LogTrace("[{SessionId}] Compression client to server: we offer {WeOffer}",
+                    Session.SessionIdHex,
+                    session.ConnectionInfo.CompressionAlgorithms.Keys.Join(","));
+
+                _logger.LogTrace("[{SessionId}] Compression client to server: they offer {TheyOffer}",
+                    Session.SessionIdHex,
+                    message.CompressionAlgorithmsClientToServer.Join(","));
+            }
+
             if (string.IsNullOrEmpty(compressionAlgorithmName))
             {
                 throw new SshConnectionException("Compression algorithm not found", DisconnectReason.KeyExchangeFailed);
@@ -148,6 +214,18 @@ namespace Renci.SshNet.Security
                                               from a in message.CompressionAlgorithmsServerToClient
                                               where a == b
                                               select a).FirstOrDefault();
+
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _logger.LogTrace("[{SessionId}] Compression server to client: we offer {WeOffer}",
+                    Session.SessionIdHex,
+                    session.ConnectionInfo.CompressionAlgorithms.Keys.Join(","));
+
+                _logger.LogTrace("[{SessionId}] Compression server to client: they offer {TheyOffer}",
+                    Session.SessionIdHex,
+                    message.CompressionAlgorithmsServerToClient.Join(","));
+            }
+
             if (string.IsNullOrEmpty(decompressionAlgorithmName))
             {
                 throw new SshConnectionException("Decompression algorithm not found", DisconnectReason.KeyExchangeFailed);
@@ -190,9 +268,9 @@ namespace Renci.SshNet.Security
 
             serverKey = GenerateSessionKey(SharedKey, ExchangeHash, serverKey, _serverCipherInfo.KeySize / 8);
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} server cipher.",
-                                                    Session.ToHex(Session.SessionId),
-                                                    Session.ConnectionInfo.CurrentServerEncryption));
+            _logger.LogDebug("[{SessionId}] Creating {ServerEncryption} server cipher.",
+                                                    Session.SessionIdHex,
+                                                    Session.ConnectionInfo.CurrentServerEncryption);
 
             // Create server cipher
             return _serverCipherInfo.Cipher(serverKey, serverVector);
@@ -218,9 +296,9 @@ namespace Renci.SshNet.Security
 
             clientKey = GenerateSessionKey(SharedKey, ExchangeHash, clientKey, _clientCipherInfo.KeySize / 8);
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} client cipher.",
-                                                    Session.ToHex(Session.SessionId),
-                                                    Session.ConnectionInfo.CurrentClientEncryption));
+            _logger.LogDebug("[{SessionId}] Creating {ClientEncryption} client cipher.",
+                                                    Session.SessionIdHex,
+                                                    Session.ConnectionInfo.CurrentClientEncryption);
 
             // Create client cipher
             return _clientCipherInfo.Cipher(clientKey, clientVector);
@@ -251,9 +329,9 @@ namespace Renci.SshNet.Security
                                                Hash(GenerateSessionKey(SharedKey, ExchangeHash, 'F', sessionId)),
                                                _serverHashInfo.KeySize / 8);
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} server hmac algorithm.",
-                                                    Session.ToHex(Session.SessionId),
-                                                    Session.ConnectionInfo.CurrentServerHmacAlgorithm));
+            _logger.LogDebug("[{SessionId}] Creating {ServerHmacAlgorithm} server hmac algorithm.",
+                                                    Session.SessionIdHex,
+                                                    Session.ConnectionInfo.CurrentServerHmacAlgorithm);
 
             return _serverHashInfo.HashAlgorithm(serverKey);
         }
@@ -283,9 +361,9 @@ namespace Renci.SshNet.Security
                                                Hash(GenerateSessionKey(SharedKey, ExchangeHash, 'E', sessionId)),
                                                _clientHashInfo.KeySize / 8);
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} client hmac algorithm.",
-                                                    Session.ToHex(Session.SessionId),
-                                                    Session.ConnectionInfo.CurrentClientHmacAlgorithm));
+            _logger.LogDebug("[{SessionId}] Creating {ClientHmacAlgorithm} client hmac algorithm.",
+                                                    Session.SessionIdHex,
+                                                    Session.ConnectionInfo.CurrentClientHmacAlgorithm);
 
             return _clientHashInfo.HashAlgorithm(clientKey);
         }
@@ -303,9 +381,9 @@ namespace Renci.SshNet.Security
                 return null;
             }
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} client compressor.",
-                                                    Session.ToHex(Session.SessionId),
-                                                    Session.ConnectionInfo.CurrentClientCompressionAlgorithm));
+            _logger.LogDebug("[{SessionId}] Creating {CompressionAlgorithm} client compressor.",
+                                                    Session.SessionIdHex,
+                                                    Session.ConnectionInfo.CurrentClientCompressionAlgorithm);
 
             var compressor = _compressorFactory();
 
@@ -327,9 +405,9 @@ namespace Renci.SshNet.Security
                 return null;
             }
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Creating {1} server decompressor.",
-                                                    Session.ToHex(Session.SessionId),
-                                                    Session.ConnectionInfo.CurrentServerCompressionAlgorithm));
+            _logger.LogDebug("[{SessionId}] Creating {ServerCompressionAlgorithm} server decompressor.",
+                                                    Session.SessionIdHex,
+                                                    Session.ConnectionInfo.CurrentServerCompressionAlgorithm);
 
             var decompressor = _decompressorFactory();
 

+ 10 - 2
src/Renci.SshNet/ServiceFactory.cs

@@ -4,7 +4,8 @@ using System.Linq;
 using System.Net.Sockets;
 using System.Text;
 
-using Renci.SshNet.Abstractions;
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Common;
 using Renci.SshNet.Connection;
 using Renci.SshNet.Messages.Transport;
@@ -25,6 +26,13 @@ namespace Renci.SshNet
         /// </summary>
         private const int PartialSuccessLimit = 5;
 
+        private readonly ILogger _logger;
+
+        internal ServiceFactory()
+        {
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<ServiceFactory>();
+        }
+
         /// <summary>
         /// Creates an <see cref="IClientAuthentication"/>.
         /// </summary>
@@ -152,7 +160,7 @@ namespace Renci.SshNet
                 fileSize = null;
                 maxPendingReads = defaultMaxPendingReads;
 
-                DiagnosticAbstraction.Log(string.Format("Failed to obtain size of file. Allowing maximum {0} pending reads: {1}", maxPendingReads, ex));
+                _logger.LogInformation(ex, "Failed to obtain size of file. Allowing maximum {MaxPendingReads} pending reads", maxPendingReads);
             }
 
             return sftpSession.CreateFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);

+ 56 - 34
src/Renci.SshNet/Session.cs

@@ -5,10 +5,14 @@ using System.Globalization;
 using System.Linq;
 using System.Net.Sockets;
 using System.Security.Cryptography;
+#if !NET
 using System.Text;
+#endif
 using System.Threading;
 using System.Threading.Tasks;
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
@@ -75,6 +79,7 @@ namespace Renci.SshNet
         /// </summary>
         private readonly IServiceFactory _serviceFactory;
         private readonly ISocketFactory _socketFactory;
+        private readonly ILogger _logger;
 
         /// <summary>
         /// Holds an object that is used to ensure only a single thread can read from
@@ -288,13 +293,28 @@ namespace Renci.SshNet
             }
         }
 
+        private byte[] _sessionId;
+
         /// <summary>
         /// Gets the session id.
         /// </summary>
         /// <value>
         /// The session id, or <see langword="null"/> if the client has not been authenticated.
         /// </value>
-        public byte[] SessionId { get; private set; }
+        public byte[] SessionId
+        {
+            get
+            {
+                return _sessionId;
+            }
+            private set
+            {
+                _sessionId = value;
+                SessionIdHex = ToHex(value);
+            }
+        }
+
+        internal string SessionIdHex { get; private set; }
 
         /// <summary>
         /// Gets the client init message.
@@ -535,6 +555,7 @@ namespace Renci.SshNet
             ConnectionInfo = connectionInfo;
             _serviceFactory = serviceFactory;
             _socketFactory = socketFactory;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<Session>();
             _messageListenerCompleted = new ManualResetEvent(initialState: true);
         }
 
@@ -577,7 +598,7 @@ namespace Renci.SshNet
                 ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
                 ConnectionInfo.ClientVersion = ClientVersion;
 
-                DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification));
+                _logger.LogInformation("Server version '{ServerIdentification}'.", serverIdentification);
 
                 if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
                 {
@@ -703,7 +724,7 @@ namespace Renci.SshNet
                 ServerVersion = ConnectionInfo.ServerVersion = serverIdentification.ToString();
                 ConnectionInfo.ClientVersion = ClientVersion;
 
-                DiagnosticAbstraction.Log(string.Format("Server version '{0}'.", serverIdentification));
+                _logger.LogInformation("Server version '{ServerIdentification}'.", serverIdentification);
 
                 if (!(serverIdentification.ProtocolVersion.Equals("2.0") || serverIdentification.ProtocolVersion.Equals("1.99")))
                 {
@@ -796,7 +817,7 @@ namespace Renci.SshNet
         /// </remarks>
         public void Disconnect()
         {
-            DiagnosticAbstraction.Log(string.Format("[{0}] Disconnecting session.", ToHex(SessionId)));
+            _logger.LogInformation("[{SessionId}] Disconnecting session.", SessionIdHex);
 
             // send SSH_MSG_DISCONNECT message, clear socket read buffer and dispose it
             Disconnect(DisconnectReason.ByApplication, "Connection terminated by the client.");
@@ -1026,7 +1047,10 @@ namespace Renci.SshNet
                 WaitOnHandle(_keyExchangeCompletedWaitHandle.WaitHandle);
             }
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Sending message '{1}' to server: '{2}'.", ToHex(SessionId), message.GetType().Name, message));
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _logger.LogTrace("[{SessionId}] Sending message {MessageName}({MessageNumber}) to server: '{Message}'.", SessionIdHex, message.MessageName, message.MessageNumber, message.ToString());
+            }
 
             var paddingMultiplier = _clientCipher is null ? (byte)8 : Math.Max((byte)8, _clientCipher.MinimumSize);
             var packetData = message.GetPacket(paddingMultiplier, _clientCompression, _clientEtm || _clientAead);
@@ -1165,12 +1189,12 @@ namespace Renci.SshNet
             }
             catch (SshException ex)
             {
-                DiagnosticAbstraction.Log(string.Format("Failure sending message '{0}' to server: '{1}' => {2}", message.GetType().Name, message, ex));
+                _logger.LogInformation(ex, "Failure sending message {MessageName}({MessageNumber}) to server: '{Message}'", message.MessageName, message.MessageNumber, message.ToString());
                 return false;
             }
             catch (SocketException ex)
             {
-                DiagnosticAbstraction.Log(string.Format("Failure sending message '{0}' to server: '{1}' => {2}", message.GetType().Name, message, ex));
+                _logger.LogInformation(ex, "Failure sending message {MessageName}({MessageNumber}) to server: '{Message}'", message.MessageName, message.MessageNumber, message.ToString());
                 return false;
             }
         }
@@ -1380,7 +1404,7 @@ namespace Renci.SshNet
         /// <param name="message"><see cref="DisconnectMessage"/> message.</param>
         internal void OnDisconnectReceived(DisconnectMessage message)
         {
-            DiagnosticAbstraction.Log(string.Format("[{0}] Disconnect received: {1} {2}.", ToHex(SessionId), message.ReasonCode, message.Description));
+            _logger.LogInformation("[{SessionId}] Disconnect received: {ReasonCode} {MessageDescription}.", SessionIdHex, message.ReasonCode, message.Description);
 
             // transition to disconnecting state to avoid throwing exceptions while cleaning up, and to
             // ensure any exceptions that are raised do not overwrite the SshConnectionException that we
@@ -1475,7 +1499,7 @@ namespace Renci.SshNet
             {
                 _isStrictKex = true;
 
-                DiagnosticAbstraction.Log(string.Format("[{0}] Enabling strict key exchange extension.", ToHex(SessionId)));
+                _logger.LogDebug("[{SessionId}] Enabling strict key exchange extension.", SessionIdHex);
 
                 if (_inboundPacketSequence != 1)
                 {
@@ -1491,7 +1515,7 @@ namespace Renci.SshNet
 
             ConnectionInfo.CurrentKeyExchangeAlgorithm = _keyExchange.Name;
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Performing {1} key exchange.", ToHex(SessionId), ConnectionInfo.CurrentKeyExchangeAlgorithm));
+            _logger.LogDebug("[{SessionId}] Performing {KeyExchangeAlgorithm} key exchange.", SessionIdHex, ConnectionInfo.CurrentKeyExchangeAlgorithm);
 
             _keyExchange.HostKeyReceived += KeyExchange_HostKeyReceived;
 
@@ -1807,34 +1831,33 @@ namespace Renci.SshNet
             var message = _sshMessageFactory.Create(messageType);
             message.Load(data, offset + 1, count - 1);
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Received message '{1}' from server: '{2}'.", ToHex(SessionId), message.GetType().Name, message));
+            if (_logger.IsEnabled(LogLevel.Trace))
+            {
+                _logger.LogTrace("[{SessionId}] Received message {MessageName}({MessageNumber}) from server: '{Message}'.", SessionIdHex, message.MessageName, message.MessageNumber, message.ToString());
+            }
 
             return message;
         }
 
-        private static string ToHex(byte[] bytes, int offset)
+        private static string ToHex(byte[] bytes)
         {
-            var byteCount = bytes.Length - offset;
-
-            var builder = new StringBuilder(bytes.Length * 2);
-
-            for (var i = offset; i < byteCount; i++)
+            if (bytes is null)
             {
-                var b = bytes[i];
-                _ = builder.Append(b.ToString("X2"));
+                return null;
             }
 
-            return builder.ToString();
-        }
+#if NET
+            return Convert.ToHexString(bytes);
+#else
+            var builder = new StringBuilder(bytes.Length * 2);
 
-        internal static string ToHex(byte[] bytes)
-        {
-            if (bytes is null)
+            foreach (var b in bytes)
             {
-                return null;
+                builder.Append(b.ToString("X2"));
             }
 
-            return ToHex(bytes, 0);
+            return builder.ToString();
+#endif
         }
 
         /// <summary>
@@ -1951,7 +1974,7 @@ namespace Renci.SshNet
                         {
                             try
                             {
-                                DiagnosticAbstraction.Log(string.Format("[{0}] Shutting down socket.", ToHex(SessionId)));
+                                _logger.LogDebug("[{SessionId}] Shutting down socket.", SessionIdHex);
 
                                 // Interrupt any pending reads; should be done outside of socket read lock as we
                                 // actually want shutdown the socket to make sure blocking reads are interrupted.
@@ -1963,14 +1986,13 @@ namespace Renci.SshNet
                             }
                             catch (SocketException ex)
                             {
-                                // TODO: log as warning
-                                DiagnosticAbstraction.Log("Failure shutting down socket: " + ex);
+                                _logger.LogInformation(ex, "Failure shutting down socket");
                             }
                         }
 
-                        DiagnosticAbstraction.Log(string.Format("[{0}] Disposing socket.", ToHex(SessionId)));
+                        _logger.LogDebug("[{SessionId}] Disposing socket.", SessionIdHex);
                         _socket.Dispose();
-                        DiagnosticAbstraction.Log(string.Format("[{0}] Disposed socket.", ToHex(SessionId)));
+                        _logger.LogDebug("[{SessionId}] Disposed socket.", SessionIdHex);
                         _socket = null;
                     }
                 }
@@ -2054,7 +2076,7 @@ namespace Renci.SshNet
         {
             var connectionException = exp as SshConnectionException;
 
-            DiagnosticAbstraction.Log(string.Format("[{0}] Raised exception: {1}", ToHex(SessionId), exp));
+            _logger.LogInformation(exp, "[{SessionId}] Raised exception", SessionIdHex);
 
             if (_isDisconnecting)
             {
@@ -2081,7 +2103,7 @@ namespace Renci.SshNet
 
             if (connectionException != null)
             {
-                DiagnosticAbstraction.Log(string.Format("[{0}] Disconnecting after exception: {1}", ToHex(SessionId), exp));
+                _logger.LogInformation(exp, "[{SessionId}] Disconnecting after exception", SessionIdHex);
                 Disconnect(connectionException.DisconnectReason, exp.ToString());
             }
         }
@@ -2154,7 +2176,7 @@ namespace Renci.SshNet
 
             if (disposing)
             {
-                DiagnosticAbstraction.Log(string.Format("[{0}] Disposing session.", ToHex(SessionId)));
+                _logger.LogDebug("[{SessionId}] Disposing session.", SessionIdHex);
 
                 Disconnect();
 

+ 5 - 1
src/Renci.SshNet/Sftp/SftpFileReader.cs

@@ -4,6 +4,8 @@ using System.Globalization;
 using System.Runtime.ExceptionServices;
 using System.Threading;
 
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Abstractions;
 using Renci.SshNet.Common;
 
@@ -22,6 +24,7 @@ namespace Renci.SshNet.Sftp
         private readonly ManualResetEvent _readAheadCompleted;
         private readonly Dictionary<int, BufferedRead> _queue;
         private readonly WaitHandle[] _waitHandles;
+        private readonly ILogger _logger;
 
         /// <summary>
         /// Holds the size of the file, when available.
@@ -68,6 +71,7 @@ namespace Renci.SshNet.Sftp
             _readAheadCompleted = new ManualResetEvent(initialState: false);
             _disposingWaitHandle = new ManualResetEvent(initialState: false);
             _waitHandles = _sftpSession.CreateWaitHandleArray(_disposingWaitHandle, _semaphore.AvailableWaitHandle);
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger<SftpFileReader>();
 
             StartReadAhead();
         }
@@ -266,7 +270,7 @@ namespace Renci.SshNet.Sftp
                     }
                     catch (Exception ex)
                     {
-                        DiagnosticAbstraction.Log("Failure closing handle: " + ex);
+                        _logger.LogInformation(ex, "Failure closing handle");
                     }
                 }
             }

+ 26 - 0
src/Renci.SshNet/SshNetLoggingConfiguration.cs

@@ -0,0 +1,26 @@
+#nullable enable
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+using Renci.SshNet.Common;
+
+namespace Renci.SshNet
+{
+    /// <summary>
+    /// Allows configuring the logging for internal logs of SSH.NET.
+    /// </summary>
+    public static class SshNetLoggingConfiguration
+    {
+        internal static ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance;
+
+        /// <summary>
+        /// Initializes the logging for SSH.NET.
+        /// </summary>
+        /// <param name="loggerFactory">The logger factory.</param>
+        public static void InitializeLogging(ILoggerFactory loggerFactory)
+        {
+            ThrowHelper.ThrowIfNull(loggerFactory);
+            LoggerFactory = loggerFactory;
+        }
+    }
+}

+ 5 - 2
src/Renci.SshNet/SubsystemSession.cs

@@ -4,7 +4,8 @@ using System.Runtime.ExceptionServices;
 using System.Threading;
 using System.Threading.Tasks;
 
-using Renci.SshNet.Abstractions;
+using Microsoft.Extensions.Logging;
+
 using Renci.SshNet.Channels;
 using Renci.SshNet.Common;
 
@@ -22,6 +23,7 @@ namespace Renci.SshNet
         private const int SystemWaitHandleCount = 3;
 
         private readonly string _subsystemName;
+        private readonly ILogger _logger;
         private ISession _session;
         private IChannelSession _channel;
         private Exception _exception;
@@ -84,6 +86,7 @@ namespace Renci.SshNet
 
             _session = session;
             _subsystemName = subsystemName;
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType());
             OperationTimeout = operationTimeout;
         }
 
@@ -180,7 +183,7 @@ namespace Renci.SshNet
         {
             _exception = error;
 
-            DiagnosticAbstraction.Log("Raised exception: " + error);
+            _logger.LogInformation(error, "Raised exception");
 
             _ = _errorOccuredWaitHandle?.Set();
 

+ 2 - 0
test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj

@@ -18,6 +18,8 @@
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>
+    <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Console" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" />
     <PackageReference Include="MSTest.TestAdapter" />
     <PackageReference Include="MSTest.TestFramework" />

+ 16 - 8
test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs

@@ -2,23 +2,27 @@
 using DotNet.Testcontainers.Containers;
 using DotNet.Testcontainers.Images;
 
+using Microsoft.Extensions.Logging;
+
 namespace Renci.SshNet.IntegrationTests.TestsFixtures
 {
     public sealed class InfrastructureFixture : IDisposable
     {
         private InfrastructureFixture()
         {
+            _loggerFactory = LoggerFactory.Create(builder =>
+            {
+                builder.SetMinimumLevel(LogLevel.Debug);
+                builder.AddFilter("testcontainers", LogLevel.Information);
+                builder.AddConsole();
+            });
+
+            SshNetLoggingConfiguration.InitializeLogging(_loggerFactory);
         }
 
-        private static readonly Lazy<InfrastructureFixture> InstanceLazy = new Lazy<InfrastructureFixture>(() => new InfrastructureFixture());
+        public static InfrastructureFixture Instance { get; } = new InfrastructureFixture();
 
-        public static InfrastructureFixture Instance
-        {
-            get
-            {
-                return InstanceLazy.Value;
-            }
-        }
+        private readonly ILoggerFactory _loggerFactory;
 
         private IContainer _sshServer;
 
@@ -34,11 +38,14 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
 
         public async Task InitializeAsync()
         {
+            var containerLogger = _loggerFactory.CreateLogger("testcontainers");
+
             _sshServerImage = new ImageFromDockerfileBuilder()
                 .WithName("renci-ssh-tests-server-image")
                 .WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), Path.Combine("test", "Renci.SshNet.IntegrationTests"))
                 .WithDockerfile("Dockerfile.TestServer")
                 .WithDeleteIfExists(true)
+                .WithLogger(containerLogger)
                 .Build();
 
             await _sshServerImage.CreateAsync();
@@ -47,6 +54,7 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
                 .WithHostname("renci-ssh-tests-server")
                 .WithImage(_sshServerImage)
                 .WithPortBinding(22, true)
+                .WithLogger(containerLogger)
                 .Build();
 
             await _sshServer.StartAsync();

+ 6 - 23
test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs

@@ -1,6 +1,4 @@
-using System.Diagnostics;
-
-using Renci.SshNet.Abstractions;
+using Microsoft.Extensions.Logging;
 
 namespace Renci.SshNet.IntegrationTests.TestsFixtures
 {
@@ -10,6 +8,7 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
     public abstract class IntegrationTestBase
     {
         private readonly InfrastructureFixture _infrastructureFixture;
+        private readonly ILogger _logger;
 
         /// <summary>
         /// The SSH Server host name.
@@ -58,13 +57,10 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
         protected IntegrationTestBase()
         {
             _infrastructureFixture = InfrastructureFixture.Instance;
-            ShowInfrastructureInformation();
-        }
-
-        private void ShowInfrastructureInformation()
-        {
-            Console.WriteLine($"SSH Server host name: {_infrastructureFixture.SshServerHostName}");
-            Console.WriteLine($"SSH Server port: {_infrastructureFixture.SshServerPort}");
+            _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType());
+            _logger.LogDebug("SSH Server: {Host}:{Port}",
+                _infrastructureFixture.SshServerHostName,
+                _infrastructureFixture.SshServerPort);
         }
 
         /// <summary>
@@ -85,18 +81,5 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures
                 }
             }
         }
-
-        protected void EnableTracing()
-        {
-            DiagnosticAbstraction.Source.Switch = new SourceSwitch("sourceSwitch", nameof(SourceLevels.Verbose));
-            DiagnosticAbstraction.Source.Listeners.Remove("Default");
-            DiagnosticAbstraction.Source.Listeners.Add(new ConsoleTraceListener() { Name = "TestConsoleLogger" });
-        }
-
-        protected void DisableTracing()
-        {
-            DiagnosticAbstraction.Source.Switch = new SourceSwitch("sourceSwitch", nameof(SourceLevels.Off));
-            DiagnosticAbstraction.Source.Listeners.Remove("TestConsoleLogger");
-        }
     }
 }