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); + } + } + } +}