diff --git a/.editorconfig b/.editorconfig index c897d8a93..aebbfc80f 100644 --- a/.editorconfig +++ b/.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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce029a36f..db38582f4 100644 --- a/CONTRIBUTING.md +++ b/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 diff --git a/Directory.Packages.props b/Directory.Packages.props index 8aa6398e7..3964b45d7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,6 +15,9 @@ + + + diff --git a/README.md b/README.md index 1daebcf5f..c308a6bfd 100644 --- a/README.md +++ b/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 diff --git a/docfx/logging.md b/docfx/logging.md new file mode 100644 index 000000000..9c0693412 --- /dev/null +++ b/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. diff --git a/src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs b/src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs deleted file mode 100644 index bc1248dc0..000000000 --- a/src/Renci.SshNet/Abstractions/DiagnosticAbstraction.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.ComponentModel; -using System.Diagnostics; - -namespace Renci.SshNet.Abstractions -{ - /// - /// Provides access to the internals of SSH.NET. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class DiagnosticAbstraction - { - /// - /// The instance used by SSH.NET. - /// - /// - /// - /// Currently, the library only traces events when compiled in Debug mode. - /// - /// - /// Configuration on .NET Core must be done programmatically, e.g. - /// - /// 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")); - /// - /// - /// - /// On .NET Framework, it is possible to configure via App.config, e.g. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// ]]> - /// - /// - /// - public static readonly TraceSource Source = new TraceSource("SshNet.Logging"); - - /// - /// Logs a message to at the - /// level. - /// - /// The message to log. - /// The trace event type. - [Conditional("DEBUG")] - public static void Log(string text, TraceEventType type = TraceEventType.Verbose) - { - Source.TraceEvent(type, - System.Environment.CurrentManagedThreadId, - text); - } - } -} diff --git a/src/Renci.SshNet/BaseClient.cs b/src/Renci.SshNet/BaseClient.cs index 3757878b4..876710db9 100644 --- a/src/Renci.SshNet/BaseClient.cs +++ b/src/Renci.SshNet/BaseClient.cs @@ -4,7 +4,8 @@ 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 @@ public abstract class BaseClient : IBaseClient /// 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 @@ private protected BaseClient(ConnectionInfo connectionInfo, bool ownsConnectionI _connectionInfo = connectionInfo; _ownsConnectionInfo = ownsConnectionInfo; _serviceFactory = serviceFactory; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); _keepAliveInterval = Timeout.InfiniteTimeSpan; } @@ -343,7 +346,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken) /// The method was called after the client was disposed. public void Disconnect() { - DiagnosticAbstraction.Log("Disconnecting client."); + _logger.LogInformation("Disconnecting client."); CheckDisposed(); @@ -442,7 +445,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - DiagnosticAbstraction.Log("Disposing client."); + _logger.LogDebug("Disposing client."); Disconnect(); diff --git a/src/Renci.SshNet/Channels/Channel.cs b/src/Renci.SshNet/Channels/Channel.cs index ca8e910fa..4c3569cef 100644 --- a/src/Renci.SshNet/Channels/Channel.cs +++ b/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 @@ internal abstract class Channel : IChannel 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 @@ protected Channel(ISession session, uint localChannelNumber, uint localWindowSiz LocalChannelNumber = localChannelNumber; LocalPacketSize = localPacketSize; LocalWindowSize = localWindowSize; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); session.ChannelWindowAdjustReceived += OnChannelWindowAdjust; session.ChannelDataReceived += OnChannelData; @@ -555,7 +558,7 @@ protected virtual void Close() 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); } } } diff --git a/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs b/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs index 7b00c3bae..71108e309 100644 --- a/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs +++ b/src/Renci.SshNet/Channels/ChannelDirectTcpip.cs @@ -3,6 +3,8 @@ 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 @@ internal sealed class ChannelDirectTcpip : ClientChannel, IChannelDirectTcpip public ChannelDirectTcpip(ISession session, uint localChannelNumber, uint localWindowSize, uint localPacketSize) : base(session, localChannelNumber, localWindowSize, localPacketSize) { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -157,8 +160,7 @@ private void ShutdownSocket(SocketShutdown how) } catch (SocketException ex) { - // TODO: log as warning - DiagnosticAbstraction.Log("Failure shutting down socket: " + ex); + _logger.LogInformation(ex, "Failure shutting down socket"); } } } diff --git a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs index 2cc0c16f8..29808e2b4 100644 --- a/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs +++ b/src/Renci.SshNet/Channels/ChannelForwardedTcpip.cs @@ -5,6 +5,8 @@ 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 @@ internal ChannelForwardedTcpip(ISession session, remoteWindowSize, remotePacketSize) { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -142,8 +146,7 @@ private void ShutdownSocket(SocketShutdown how) } catch (SocketException ex) { - // TODO: log as warning - DiagnosticAbstraction.Log("Failure shutting down socket: " + ex); + _logger.LogInformation(ex, "Failure shutting down socket"); } } } diff --git a/src/Renci.SshNet/Common/Extensions.cs b/src/Renci.SshNet/Common/Extensions.cs index cf4deb077..2f3ae6ac0 100644 --- a/src/Renci.SshNet/Common/Extensions.cs +++ b/src/Renci.SshNet/Common/Extensions.cs @@ -351,5 +351,12 @@ internal static bool IsConnected(this Socket socket) return socket.Connected; } + + internal static string Join(this IEnumerable 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); + } } } diff --git a/src/Renci.SshNet/Connection/ConnectorBase.cs b/src/Renci.SshNet/Connection/ConnectorBase.cs index c36fae6df..ebea9aa80 100644 --- a/src/Renci.SshNet/Connection/ConnectorBase.cs +++ b/src/Renci.SshNet/Connection/ConnectorBase.cs @@ -4,6 +4,8 @@ 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 @@ protected ConnectorBase(ISocketFactory socketFactory) /// An error occurred trying to establish the connection. 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 @@ protected async Task SocketConnectAsync(EndPoint endPoint, CancellationT { 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 diff --git a/src/Renci.SshNet/ForwardedPortDynamic.cs b/src/Renci.SshNet/ForwardedPortDynamic.cs index 7d0e5af96..5b8ff9d29 100644 --- a/src/Renci.SshNet/ForwardedPortDynamic.cs +++ b/src/Renci.SshNet/ForwardedPortDynamic.cs @@ -7,6 +7,8 @@ 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 /// public class ForwardedPortDynamic : ForwardedPort { + private readonly ILogger _logger; private ForwardedPortStatus _status; /// @@ -72,6 +75,7 @@ public ForwardedPortDynamic(string host, uint port) BoundHost = host; BoundPort = port; _status = ForwardedPortStatus.Stopped; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -409,8 +413,7 @@ private void InternalStop(TimeSpan timeout) 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."); } } diff --git a/src/Renci.SshNet/ForwardedPortLocal.cs b/src/Renci.SshNet/ForwardedPortLocal.cs index ec030be44..50d84dc74 100644 --- a/src/Renci.SshNet/ForwardedPortLocal.cs +++ b/src/Renci.SshNet/ForwardedPortLocal.cs @@ -3,7 +3,8 @@ 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 /// public partial class ForwardedPortLocal : ForwardedPort { + private readonly ILogger _logger; private ForwardedPortStatus _status; private bool _isDisposed; private Socket _listener; @@ -101,6 +103,7 @@ public ForwardedPortLocal(string boundHost, uint boundPort, string host, uint po Host = host; Port = port; _status = ForwardedPortStatus.Stopped; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -387,8 +390,7 @@ private void InternalStop(TimeSpan timeout) 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."); } } diff --git a/src/Renci.SshNet/ForwardedPortRemote.cs b/src/Renci.SshNet/ForwardedPortRemote.cs index 07b234541..f51a3d82b 100644 --- a/src/Renci.SshNet/ForwardedPortRemote.cs +++ b/src/Renci.SshNet/ForwardedPortRemote.cs @@ -3,6 +3,8 @@ 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 /// 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 @@ public ForwardedPortRemote(IPAddress boundHostAddress, uint boundPort, IPAddress HostAddress = hostAddress; Port = port; _status = ForwardedPortStatus.Stopped; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); } /// @@ -208,8 +212,7 @@ protected override void StopPort(TimeSpan timeout) 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; diff --git a/src/Renci.SshNet/Renci.SshNet.csproj b/src/Renci.SshNet/Renci.SshNet.csproj index d00f773fa..2936c0a7f 100644 --- a/src/Renci.SshNet/Renci.SshNet.csproj +++ b/src/Renci.SshNet/Renci.SshNet.csproj @@ -36,6 +36,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Renci.SshNet/Security/KeyExchange.cs b/src/Renci.SshNet/Security/KeyExchange.cs index 96eb326ca..1331098b0 100644 --- a/src/Renci.SshNet/Security/KeyExchange.cs +++ b/src/Renci.SshNet/Security/KeyExchange.cs @@ -3,7 +3,8 @@ 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 /// public abstract class KeyExchange : Algorithm, IKeyExchange { + private readonly ILogger _logger; private CipherInfo _clientCipherInfo; private CipherInfo _serverCipherInfo; private HashInfo _clientHashInfo; @@ -61,6 +63,11 @@ public byte[] ExchangeHash /// public event EventHandler HostKeyReceived; + private protected KeyExchange() + { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); + } + /// public virtual void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage) { @@ -71,12 +78,23 @@ public virtual void Start(Session session, KeyExchangeInitMessage message, bool 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 @@ from a in message.EncryptionAlgorithmsClientToServer 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 @@ from a in message.EncryptionAlgorithmsServerToClient 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 @@ from a in message.MacAlgorithmsClientToServer 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 @@ from a in message.MacAlgorithmsServerToClient 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 @@ from a in message.CompressionAlgorithmsClientToServer 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 @@ public Cipher CreateServerCipher(out bool isAead) 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 @@ public Cipher CreateClientCipher(out bool isAead) 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 @@ public HashAlgorithm CreateServerHash(out bool isEncryptThenMAC) 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 @@ public HashAlgorithm CreateClientHash(out bool isEncryptThenMAC) 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 @@ public Compressor CreateCompressor() 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 @@ public Compressor CreateDecompressor() 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(); diff --git a/src/Renci.SshNet/ServiceFactory.cs b/src/Renci.SshNet/ServiceFactory.cs index d238b6eac..0b8e27f67 100644 --- a/src/Renci.SshNet/ServiceFactory.cs +++ b/src/Renci.SshNet/ServiceFactory.cs @@ -4,7 +4,8 @@ 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 @@ internal sealed partial class ServiceFactory : IServiceFactory /// private const int PartialSuccessLimit = 5; + private readonly ILogger _logger; + + internal ServiceFactory() + { + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); + } + /// /// Creates an . /// @@ -152,7 +160,7 @@ public ISftpFileReader CreateSftpFileReader(string fileName, ISftpSession sftpSe 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); diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index 63560b685..639a7dbba 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -5,10 +5,14 @@ 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 @@ public class Session : ISession /// private readonly IServiceFactory _serviceFactory; private readonly ISocketFactory _socketFactory; + private readonly ILogger _logger; /// /// Holds an object that is used to ensure only a single thread can read from @@ -288,13 +293,28 @@ public bool IsConnected } } + private byte[] _sessionId; + /// /// Gets the session id. /// /// /// The session id, or if the client has not been authenticated. /// - 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; } /// /// Gets the client init message. @@ -535,6 +555,7 @@ internal Session(ConnectionInfo connectionInfo, IServiceFactory serviceFactory, ConnectionInfo = connectionInfo; _serviceFactory = serviceFactory; _socketFactory = socketFactory; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); _messageListenerCompleted = new ManualResetEvent(initialState: true); } @@ -577,7 +598,7 @@ public void Connect() 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 @@ public async Task ConnectAsync(CancellationToken cancellationToken) 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 @@ public async Task ConnectAsync(CancellationToken cancellationToken) /// 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 @@ internal void SendMessage(Message message) 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 @@ private bool TrySendMessage(Message message) } 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 @@ private void TrySendDisconnect(DisconnectReason reasonCode, string message) /// message. 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 @@ internal void OnKeyExchangeInitReceived(KeyExchangeInitMessage message) { _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 @@ internal void OnKeyExchangeInitReceived(KeyExchangeInitMessage message) 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 @@ private Message LoadMessage(byte[] data, int offset, int count) 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 } /// @@ -1951,7 +1974,7 @@ private void SocketDisconnectAndDispose() { 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 @@ private void SocketDisconnectAndDispose() } 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 @@ private void RaiseError(Exception exp) { 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 @@ private void RaiseError(Exception exp) 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 @@ protected virtual void Dispose(bool disposing) if (disposing) { - DiagnosticAbstraction.Log(string.Format("[{0}] Disposing session.", ToHex(SessionId))); + _logger.LogDebug("[{SessionId}] Disposing session.", SessionIdHex); Disconnect(); diff --git a/src/Renci.SshNet/Sftp/SftpFileReader.cs b/src/Renci.SshNet/Sftp/SftpFileReader.cs index 40e13edd4..3c86fc31b 100644 --- a/src/Renci.SshNet/Sftp/SftpFileReader.cs +++ b/src/Renci.SshNet/Sftp/SftpFileReader.cs @@ -4,6 +4,8 @@ using System.Runtime.ExceptionServices; using System.Threading; +using Microsoft.Extensions.Logging; + using Renci.SshNet.Abstractions; using Renci.SshNet.Common; @@ -22,6 +24,7 @@ internal sealed class SftpFileReader : ISftpFileReader private readonly ManualResetEvent _readAheadCompleted; private readonly Dictionary _queue; private readonly WaitHandle[] _waitHandles; + private readonly ILogger _logger; /// /// Holds the size of the file, when available. @@ -68,6 +71,7 @@ public SftpFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, i _readAheadCompleted = new ManualResetEvent(initialState: false); _disposingWaitHandle = new ManualResetEvent(initialState: false); _waitHandles = _sftpSession.CreateWaitHandleArray(_disposingWaitHandle, _semaphore.AvailableWaitHandle); + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(); StartReadAhead(); } @@ -266,7 +270,7 @@ private void Dispose(bool disposing) } catch (Exception ex) { - DiagnosticAbstraction.Log("Failure closing handle: " + ex); + _logger.LogInformation(ex, "Failure closing handle"); } } } diff --git a/src/Renci.SshNet/SshNetLoggingConfiguration.cs b/src/Renci.SshNet/SshNetLoggingConfiguration.cs new file mode 100644 index 000000000..4add75c9f --- /dev/null +++ b/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 +{ + /// + /// Allows configuring the logging for internal logs of SSH.NET. + /// + public static class SshNetLoggingConfiguration + { + internal static ILoggerFactory LoggerFactory { get; private set; } = NullLoggerFactory.Instance; + + /// + /// Initializes the logging for SSH.NET. + /// + /// The logger factory. + public static void InitializeLogging(ILoggerFactory loggerFactory) + { + ThrowHelper.ThrowIfNull(loggerFactory); + LoggerFactory = loggerFactory; + } + } +} diff --git a/src/Renci.SshNet/SubsystemSession.cs b/src/Renci.SshNet/SubsystemSession.cs index 5ddd80718..bb3ddfc9b 100644 --- a/src/Renci.SshNet/SubsystemSession.cs +++ b/src/Renci.SshNet/SubsystemSession.cs @@ -4,7 +4,8 @@ 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 @@ internal abstract class SubsystemSession : ISubsystemSession 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 @@ protected SubsystemSession(ISession session, string subsystemName, int operation _session = session; _subsystemName = subsystemName; + _logger = SshNetLoggingConfiguration.LoggerFactory.CreateLogger(GetType()); OperationTimeout = operationTimeout; } @@ -180,7 +183,7 @@ protected void RaiseError(Exception error) { _exception = error; - DiagnosticAbstraction.Log("Raised exception: " + error); + _logger.LogInformation(error, "Raised exception"); _ = _errorOccuredWaitHandle?.Set(); diff --git a/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj b/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj index 06ae83ea6..d9f89ac45 100644 --- a/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj +++ b/test/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj @@ -18,6 +18,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs index 4be520af6..7c095b867 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs +++ b/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 InstanceLazy = new Lazy(() => 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 @@ public static InfrastructureFixture Instance 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 @@ public async Task InitializeAsync() .WithHostname("renci-ssh-tests-server") .WithImage(_sshServerImage) .WithPortBinding(22, true) + .WithLogger(containerLogger) .Build(); await _sshServer.StartAsync(); diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs index d08578e4e..6838e54d7 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs +++ b/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; /// /// The SSH Server host name. @@ -58,13 +57,10 @@ public SshUser User 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); } /// @@ -85,18 +81,5 @@ protected void CreateTestFile(string fileName, int size) } } } - - 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"); - } } }