diff --git a/Common/Common.csproj b/Common/Common.csproj index 345380b..5d87ed2 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -8,6 +8,9 @@ + + + diff --git a/Common/Logging/ILogger.cs b/Common/Logging/ILogger.cs index 766525f..a229e54 100644 --- a/Common/Logging/ILogger.cs +++ b/Common/Logging/ILogger.cs @@ -10,7 +10,7 @@ public interface ILogger /// /// Event that is thrown whenever a new log entry was entered with any Log method. /// - event EventHandler NewLogEntry; + event EventHandler? NewLogEntry; /// /// Initialize the Logger once before using it. diff --git a/Common/Logging/LogLevel.cs b/Common/Logging/LogLevel.cs index bd02a1c..f6c6775 100644 --- a/Common/Logging/LogLevel.cs +++ b/Common/Logging/LogLevel.cs @@ -2,13 +2,14 @@ /// /// Level of a log entry. +/// Listed from lowest priority to highest priority. /// public enum LogLevel { Undefined = 0, - Error = 1, - Warn = 2, + Trace = 1, + Debug = 2, Info = 3, - Debug = 4, - Trace = 5 + Warn = 4, + Error = 5 } diff --git a/Common/Logging/SerilogLogger.cs b/Common/Logging/SerilogLogger.cs new file mode 100644 index 0000000..cb99569 --- /dev/null +++ b/Common/Logging/SerilogLogger.cs @@ -0,0 +1,168 @@ +using Common.Logging; +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace Common.Logger; + +/// +/// Implements a Logger that uses the Serilog package to create log entries and distributes them into different sinks. +/// Implemented Sinks: File (rolling file), Debug (). +/// +public class SerilogLogger : Logging.ILogger +{ + private readonly LoggingLevelSwitch loggingLevelSwitch; + public event EventHandler? NewLogEntry; + + /// + /// Constructor of a Logger that uses the Serilog package to create log entries and distributes them into different sinks. + /// + /// + /// path where the logfile shall be created. + /// rolling file is created with daily interval. date is automatically added to file name. + /// Example: "C:/log/log-.txt" + /// + /// + /// if true the log entries are written to . + /// Minimum level is ignored for this log entries, therefore every log entry will be logged. + /// + public SerilogLogger(string logFilePath, bool logToDebug) + { + // create logging level switch and initialized with lowest level + this.loggingLevelSwitch = new LoggingLevelSwitch() { MinimumLevel = Serilog.Events.LogEventLevel.Verbose }; + + // create logger instance + if (logToDebug) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Debug() + .WriteTo.File(logFilePath, + rollingInterval: RollingInterval.Day, + levelSwitch: loggingLevelSwitch, + outputTemplate: "{Message:lj}") // outputTemplate = plain message, formatting is done inside log entry + .CreateLogger(); + } + else + { + Log.Logger = new LoggerConfiguration() + .WriteTo.File(logFilePath, + rollingInterval: RollingInterval.Day, + levelSwitch: loggingLevelSwitch, + outputTemplate: "{Message:lj}") // outputTemplate = plain message, formatting is done inside log entry + .CreateLogger(); + } + } + + public void Initialize() + { + this.Initialize(LogLevel.Trace); + } + + public void Initialize(LogLevel minimumLogLevel) + { + this.SetMinimumLogLevel(minimumLogLevel); + // Nothing else to do for this logger + } + + public void SetMinimumLogLevel(LogLevel newMinimumLogLevel) + { + this.loggingLevelSwitch.MinimumLevel = this.ConvertGenericToSerilogLogLevel(newMinimumLogLevel); + } + + private LogEventLevel ConvertGenericToSerilogLogLevel(LogLevel newMinimumLogLevel) => newMinimumLogLevel switch + { + LogLevel.Undefined => LogEventLevel.Verbose, + LogLevel.Trace => LogEventLevel.Verbose, + LogLevel.Debug => LogEventLevel.Debug, + LogLevel.Info => LogEventLevel.Information, + LogLevel.Warn => LogEventLevel.Warning, + LogLevel.Error => LogEventLevel.Error, + _ => throw new NotImplementedException($"'{nameof(ConvertGenericToSerilogLogLevel)}()' does not contain an entry for {nameof(LogLevel)} {newMinimumLogLevel}"), + }; + + public void StopLogging() + { + Log.CloseAndFlush(); + } + + private void CreateLogEntry(LogEntry logEntry) + { + var serilogEventLevel = this.ConvertGenericToSerilogLogLevel(logEntry.LogLevel); + // formatting is done inside logEntry.ToString() method => therefore log plain message text with right category + switch (serilogEventLevel) + { + case LogEventLevel.Verbose: + Log.Verbose(logEntry.ToString()); + break; + + case LogEventLevel.Debug: + Log.Debug(logEntry.ToString()); + break; + + case LogEventLevel.Information: + Log.Information(logEntry.ToString()); + break; + + case LogEventLevel.Warning: + Log.Warning(logEntry.ToString()); + break; + + case LogEventLevel.Error: + Log.Error(logEntry.ToString()); + break; + + case LogEventLevel.Fatal: + Log.Error(logEntry.ToString()); + break; + + default: + throw new NotImplementedException($"'{nameof(CreateLogEntry)}()' does not contain an implementation for {nameof(LogEventLevel)} {serilogEventLevel}."); + } + } + + + #region Logging methods + public void LogTrace(string message, string category = "") + { + this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Trace, Message = message }); + } + + public void LogDebug(string message, string category = "") + { + this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Debug, Message = message }); + } + + public void LogInfo(string message, string category = "") + { + this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Info, Message = message }); + } + + public void LogWarn(string message, string category = "") + { + this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Warn, Message = message }); + } + + public void LogError(string message, string category = "") + { + this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Error, Message = message }); + } + + public void LogException(Exception exception, string category = "") + { + this.CreateLogEntry(new LogEntry { Category = category, LogLevel = LogLevel.Error, Exception = exception }); + } + + public void LogException(Exception exception, string message, string category = "") + { + this.CreateLogEntry( + new LogEntry + { + Category = category, + LogLevel = LogLevel.Error, + Message = message, + Exception = exception + }); + } + #endregion + +} diff --git a/MultiTerm.Wpf/App.xaml.cs b/MultiTerm.Wpf/App.xaml.cs index cc8e2ae..b02790e 100644 --- a/MultiTerm.Wpf/App.xaml.cs +++ b/MultiTerm.Wpf/App.xaml.cs @@ -1,10 +1,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MultiTerm.Core.ViewModel; -using Common.StartupHelpers; using System.Windows; -using MultiTerm.Core.Common; using MultiTerm.Core.Helpers; +using Common.Logging; +using Common.Logger; namespace MultiTerm.Wpf; @@ -19,6 +19,7 @@ public partial class App : Application .ConfigureServices((hostContext, services) => { services.AddSingleton(); + services.AddSingleton(new SerilogLogger("C:/log/multiterm-log-.txt", true)); // viewmodels services.AddSingleton(); @@ -33,6 +34,11 @@ public partial class App : Application { await AppHost!.StartAsync(); + // create logger and initialize + var logger = AppHost.Services.GetRequiredService(); + logger.Initialize(LogLevel.Trace); + logger.LogInfo("Application started.", nameof(App)); + // instanciate startup form and show var startupForm = AppHost.Services.GetRequiredService(); startupForm.Show(); @@ -42,6 +48,11 @@ public partial class App : Application protected override async void OnExit(ExitEventArgs e) { + // log application exit and stop logger (if still available) + var logger = AppHost!.Services.GetRequiredService(); + logger?.LogInfo("Application exited.", nameof(App)); + logger?.StopLogging(); + await AppHost!.StopAsync(); base.OnExit(e); }