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