Skip to content

Commit

Permalink
Added unhandled exception tracing to logger
Browse files Browse the repository at this point in the history
  • Loading branch information
jschick04 committed Dec 3, 2024
1 parent cc4f59b commit def382c
Showing 1 changed file with 69 additions and 20 deletions.
89 changes: 69 additions & 20 deletions src/EventLogExpert.UI/Services/DebugLogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,47 @@
using Fluxor;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Runtime.ExceptionServices;

namespace EventLogExpert.UI.Services;

public sealed class DebugLogService : ITraceLogger, IFileLogger
public sealed partial class DebugLogService : ITraceLogger, IFileLogger, IDisposable
{
private const long MaxLogSize = 10 * 1024 * 1024;

private static readonly ReaderWriterLockSlim s_loggingFileLock = new();

private readonly FileLocationOptions _fileLocationOptions;
private readonly IState<SettingsState> _settingsState;
private readonly Lock _firstChanceLock = new();
private readonly IStateSelection<SettingsState, LogLevel> _logLevelState;

private bool _firstChanceLoggingEnabled;

public DebugLogService(FileLocationOptions fileLocationOptions, IState<SettingsState> settingsState)
public DebugLogService(
FileLocationOptions fileLocationOptions,
IStateSelection<SettingsState, LogLevel> logLevelState)
{
_fileLocationOptions = fileLocationOptions;
_settingsState = settingsState;
_logLevelState = logLevelState;

_logLevelState.Select(state => state.Config.LogLevel);

// HACK: Constructor is called before SettingsState gets LogLevel from preferences
// Injecting preferences means we check the saved preferences every time Trace is called, or
// we save the preference on init and an app restart is required to update the logging level
_logLevelState.StateChanged += (_, _) =>
{
if (_firstChanceLoggingEnabled)
{
AppDomain.CurrentDomain.FirstChanceException -= OnFirstChanceException;
}

InitTracing();
};

InitTracing();

AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
}

public async Task ClearAsync()
Expand All @@ -41,6 +65,16 @@ public async Task ClearAsync()
}
}

public void Dispose()
{
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;

if (_firstChanceLoggingEnabled)
{
AppDomain.CurrentDomain.FirstChanceException -= OnFirstChanceException;
}
}

public async IAsyncEnumerable<string> LoadAsync()
{
s_loggingFileLock.EnterReadLock();
Expand All @@ -60,7 +94,7 @@ public async IAsyncEnumerable<string> LoadAsync()

public void Trace(string message, LogLevel level = LogLevel.Information)
{
if (level < _settingsState.Value.Config.LogLevel) { return; }
if (level < _logLevelState.Value) { return; }

string output = $"[{DateTime.Now:o}] [{Environment.CurrentManagedThreadId}] [{level}] {message}";

Expand All @@ -84,6 +118,8 @@ public void Trace(string message, LogLevel level = LogLevel.Information)

private void InitTracing()
{
_firstChanceLoggingEnabled = false;

// Set up tracing to a file
var fileInfo = new FileInfo(_fileLocationOptions.LoggingPath);

Expand All @@ -95,24 +131,37 @@ private void InitTracing()
// Disabling first chance exception logging unless LogLevel is at Trace level
// since it is noisy and is logging double information for exceptions that are being handled.
// This also saves any potential performance hit for when we aren't worried about tracking first chance exceptions.
if (_settingsState.Value.Config.LogLevel > LogLevel.Trace) { return; }
if (_logLevelState.Value > LogLevel.Trace) { return; }

var firstChanceSemaphore = new SemaphoreSlim(1);
_firstChanceLoggingEnabled = true;

// Trace all exceptions
AppDomain.CurrentDomain.FirstChanceException += (o, args) =>
AppDomain.CurrentDomain.FirstChanceException += OnFirstChanceException;
}

private void OnFirstChanceException(object? sender, FirstChanceExceptionEventArgs e)
{
// Unless we're already tracing one.
//
// When an instance of EventLogExpert is launched by double-clicking an evtx file
// and no databases are present, we get into a condition where we're trying to trace
// a first-chance exception, and the act of tracing causes another first-chance
// exception, which we then try to trace, until we hit a stack overflow.

if (!_firstChanceLock.TryEnter(100)) { return; }

try
{
// Unless we're already tracing one.
//
// When an instance of EventLogExpert is launched by double-clicking an evtx file
// and no databases are present, we get into a condition where we're trying to trace
// a first-chance exception, and the act of tracing causes another first-chance
// exception, which we then try to trace, until we hit a stack overflow.
if (firstChanceSemaphore.Wait(100))
{
Trace($"{args.Exception}", LogLevel.Trace);
firstChanceSemaphore.Release();
}
};
Trace($"{e.Exception}", LogLevel.Trace);
}
finally
{
_firstChanceLock.Exit();
}
}

private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Trace($"Unhandled Exception: {e.ExceptionObject}", LogLevel.Critical);
}
}

0 comments on commit def382c

Please sign in to comment.