Skip to content

Commit

Permalink
[dotnet] Possibility to output internal log messages to file (#13249)
Browse files Browse the repository at this point in the history
* 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 3cf6e48.

* 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 e3255a6.

* 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
  • Loading branch information
nvborisenko authored Dec 7, 2023
1 parent 1bcb948 commit b949dca
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 23 deletions.
9 changes: 0 additions & 9 deletions dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}

/// <summary>
/// Creates a new instance of the <see cref="ConsoleLogHandler"/> class.
/// </summary>
/// <returns>A new instance of the <see cref="ConsoleLogHandler"/> class.</returns>
public ILogHandler Clone()
{
return this;
}
}
}
94 changes: 94 additions & 0 deletions dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.IO;

namespace OpenQA.Selenium.Internal.Logging
{
/// <summary>
/// Represents a log handler that writes log events to a file.
/// </summary>
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;

/// <summary>
/// Initializes a new instance of the <see cref="FileLogHandler"/> class with the specified file path.
/// </summary>
/// <param name="path">The path of the log file.</param>
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
};
}

/// <summary>
/// Handles a log event by writing it to the log file.
/// </summary>
/// <param name="logEvent">The log event to handle.</param>
public void Handle(LogEvent logEvent)
{
lock (_lockObj)
{
_streamWriter.WriteLine($"{logEvent.Timestamp:r} {_levels[(int)logEvent.Level]} {logEvent.IssuedBy.Name}: {logEvent.Message}");
}
}

/// <summary>
/// Disposes the file log handler and releases any resources used.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Finalizes the file log handler instance.
/// </summary>
~FileLogHandler()
{
Dispose(false);
}

/// <summary>
/// Disposes the file log handler and releases any resources used.
/// </summary>
/// <param name="disposing">A flag indicating whether to dispose managed resources.</param>
protected virtual void Dispose(bool disposing)
{
lock (_lockObj)
{
if (!_isDisposed)
{
if (disposing)
{
_streamWriter?.Dispose();
_streamWriter = null;
_fileStream?.Dispose();
_fileStream = null;
}

_isDisposed = true;
}
}
}
}
}
6 changes: 0 additions & 6 deletions dotnet/src/webdriver/Internal/Logging/ILogHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,5 @@ public interface ILogHandler
/// </summary>
/// <param name="logEvent">The log event to handle.</param>
void Handle(LogEvent logEvent);

/// <summary>
/// Creates a clone of the log handler.
/// </summary>
/// <returns>A clone of the log handler.</returns>
ILogHandler Clone();
}
}
26 changes: 18 additions & 8 deletions dotnet/src/webdriver/Internal/Logging/LogContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,16 @@ public ILogContext CreateContext(LogEventLevel minimumLevel)
loggers = new ConcurrentDictionary<Type, ILogger>(_loggers.Select(l => new KeyValuePair<Type, ILogger>(l.Key, new Logger(l.Value.Issuer, minimumLevel))));
}

IList<ILogHandler> handlers = null;
var context = new LogContext(minimumLevel, this, loggers, null);

if (Handlers != null)
{
handlers = new List<ILogHandler>(Handlers.Select(h => h.Clone()));
}
else
{
handlers = new List<ILogHandler>();
foreach (var handler in Handlers)
{
context.Handlers.Add(handler);
}
}

var context = new LogContext(minimumLevel, this, loggers, Handlers);

Log.CurrentContext = context;

return context;
Expand Down Expand Up @@ -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;
}
}
Expand Down
43 changes: 43 additions & 0 deletions dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}

0 comments on commit b949dca

Please sign in to comment.