From b949dcaf4e16f0c60fdffbca91fbde1f452233e1 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:33:13 +0300 Subject: [PATCH] [dotnet] Possibility to output internal log messages to file (#13249) * INitial implementation * With context * Update HttpCommandExecutor.cs * Nullable handlers * Don't capture logger * Log message issuer * Simplify things * Continue * Update nunit adapter to work with dotnet 7 and be more friendly with windows * Rename to LogEventLevel * Typo * Introduce LogContextManager * Typo again * Rename to Timestamp * Make ILogger as static field * Support hierarchical contexts * Rename to EmitMessage * Do not emit message to parent context * Deep copy of loggers and handlers per context * Make fields private * Static works with current log context * Create context with minimum level * Set minimum level for context * Rename to WithHandler * Set minimum level per issuer * Simplify getting internal logger * Use DateTimeOffset * Docs for log event level * Docs for ILogger * Docs for Logger * Docs for others * Make ILogger interface as internal * Revert "Make ILogger interface as internal" This reverts commit 3cf6e489dc5aaaa7dc78a54d22d97230ff6b0b11. * First test * Update LogTest.cs * Fix build error * Info minimum log level by default * Remove unnecessary log call in ChromeDriver * Adjust log levels in console output * Make it length fixed * Make webdriver assembly internals visible to tests * Make ILogger hidden from user * More tests for log context * Init * Rename back to AddHandler * Make format script happy? * Make format script happy? * Rename back to SetLevel * Console handler by default * Output logs to stderr * New api to mange log handlers * Use logging in DriverFactory * Revert "Use logging in DriverFactory" This reverts commit e3255a6217851882ef772165fb01bff1df8c244f. * Verbose driver creation in tests * Search driver type in loaded assemblies * Decalare internals visible to in csproj to not conflict with bazel * Clean specific assembly name for driver type * Old school using to make bazel happy * Fix targeting packs for test targets * It works * Small clean up * Lock in ctor * Dispose at process exit * Remove redundant Clone for log handlers * Dispose log handlers when context finishes * Lock writing to the disk globally * Fix new list of log handlers for context * Don't lock in ctor * Add docs * Change format of datetime in file log * Thread safe disposing * Add finilizer * Docs for finilizer * Add tests * Recreating missing dirs --- .../Internal/Logging/ConsoleLogHandler.cs | 9 -- .../Internal/Logging/FileLogHandler.cs | 94 +++++++++++++++++++ .../webdriver/Internal/Logging/ILogHandler.cs | 6 -- .../webdriver/Internal/Logging/LogContext.cs | 26 +++-- .../Internal/Logging/FileLogHandlerTest.cs | 43 +++++++++ 5 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs create mode 100644 dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs diff --git a/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs b/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs index 83759ac09dee6..30f63dd625673 100644 --- a/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs +++ b/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs @@ -36,14 +36,5 @@ public void Handle(LogEvent logEvent) { Console.Error.WriteLine($"{logEvent.Timestamp:HH:mm:ss.fff} {_levels[(int)logEvent.Level]} {logEvent.IssuedBy.Name}: {logEvent.Message}"); } - - /// - /// Creates a new instance of the class. - /// - /// A new instance of the class. - public ILogHandler Clone() - { - return this; - } } } diff --git a/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs b/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs new file mode 100644 index 0000000000000..915ea43870863 --- /dev/null +++ b/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; + +namespace OpenQA.Selenium.Internal.Logging +{ + /// + /// Represents a log handler that writes log events to a file. + /// + public class FileLogHandler : ILogHandler, IDisposable + { + // performance trick to avoid expensive Enum.ToString() with fixed length + private static readonly string[] _levels = { "TRACE", "DEBUG", " INFO", " WARN", "ERROR" }; + + private FileStream _fileStream; + private StreamWriter _streamWriter; + + private readonly object _lockObj = new object(); + private bool _isDisposed; + + /// + /// Initializes a new instance of the class with the specified file path. + /// + /// The path of the log file. + public FileLogHandler(string path) + { + if (string.IsNullOrEmpty(path)) throw new ArgumentException("File log path cannot be null or empty.", nameof(path)); + + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + _fileStream = File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + _fileStream.Seek(0, SeekOrigin.End); + _streamWriter = new StreamWriter(_fileStream, System.Text.Encoding.UTF8) + { + AutoFlush = true + }; + } + + /// + /// Handles a log event by writing it to the log file. + /// + /// The log event to handle. + public void Handle(LogEvent logEvent) + { + lock (_lockObj) + { + _streamWriter.WriteLine($"{logEvent.Timestamp:r} {_levels[(int)logEvent.Level]} {logEvent.IssuedBy.Name}: {logEvent.Message}"); + } + } + + /// + /// Disposes the file log handler and releases any resources used. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Finalizes the file log handler instance. + /// + ~FileLogHandler() + { + Dispose(false); + } + + /// + /// Disposes the file log handler and releases any resources used. + /// + /// A flag indicating whether to dispose managed resources. + protected virtual void Dispose(bool disposing) + { + lock (_lockObj) + { + if (!_isDisposed) + { + if (disposing) + { + _streamWriter?.Dispose(); + _streamWriter = null; + _fileStream?.Dispose(); + _fileStream = null; + } + + _isDisposed = true; + } + } + } + } +} diff --git a/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs b/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs index 44c3ae67f6377..9c0365e0881e4 100644 --- a/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs +++ b/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs @@ -28,11 +28,5 @@ public interface ILogHandler /// /// The log event to handle. void Handle(LogEvent logEvent); - - /// - /// Creates a clone of the log handler. - /// - /// A clone of the log handler. - ILogHandler Clone(); } } diff --git a/dotnet/src/webdriver/Internal/Logging/LogContext.cs b/dotnet/src/webdriver/Internal/Logging/LogContext.cs index 6b19059b5cd5a..57713c23edc53 100644 --- a/dotnet/src/webdriver/Internal/Logging/LogContext.cs +++ b/dotnet/src/webdriver/Internal/Logging/LogContext.cs @@ -60,19 +60,16 @@ public ILogContext CreateContext(LogEventLevel minimumLevel) loggers = new ConcurrentDictionary(_loggers.Select(l => new KeyValuePair(l.Key, new Logger(l.Value.Issuer, minimumLevel)))); } - IList handlers = null; + var context = new LogContext(minimumLevel, this, loggers, null); if (Handlers != null) { - handlers = new List(Handlers.Select(h => h.Clone())); - } - else - { - handlers = new List(); + foreach (var handler in Handlers) + { + context.Handlers.Add(handler); + } } - var context = new LogContext(minimumLevel, this, loggers, Handlers); - Log.CurrentContext = context; return context; @@ -137,6 +134,19 @@ public ILogContext SetLevel(Type issuer, LogEventLevel level) public void Dispose() { + // Dispose log handlers associated with this context + // if they are hot handled by parent context + if (Handlers != null && _parentLogContext != null && _parentLogContext.Handlers != null) + { + foreach (var logHandler in Handlers) + { + if (!_parentLogContext.Handlers.Contains(logHandler)) + { + (logHandler as IDisposable)?.Dispose(); + } + } + } + Log.CurrentContext = _parentLogContext; } } diff --git a/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs b/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs new file mode 100644 index 0000000000000..072582723b154 --- /dev/null +++ b/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using System; +using System.IO; + +namespace OpenQA.Selenium.Internal.Logging +{ + public class FileLogHandlerTest + { + [Test] + [TestCase(null)] + [TestCase("")] + public void ShouldNotAcceptIncorrectPath(string path) + { + var act = () => new FileLogHandler(path); + + Assert.That(act, Throws.ArgumentException); + } + + [Test] + public void ShouldHandleLogEvent() + { + var tempFile = Path.GetTempFileName(); + + try + { + using (var fileLogHandler = new FileLogHandler(tempFile)) + { + fileLogHandler.Handle(new LogEvent(typeof(FileLogHandlerTest), DateTimeOffset.Now, LogEventLevel.Info, "test message")); + } + + Assert.That(File.ReadAllText(tempFile), Does.Contain("test message")); + } + catch (Exception) + { + throw; + } + finally + { + File.Delete(tempFile); + } + } + } +}