diff --git a/EonaCat.Logger/Enums.cs b/EonaCat.Logger/Enums.cs index b176f3a..6b99454 100644 --- a/EonaCat.Logger/Enums.cs +++ b/EonaCat.Logger/Enums.cs @@ -1,4 +1,6 @@ -namespace EonaCat.Logger +using Microsoft.Extensions.Logging; + +namespace EonaCat.Logger { // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/License for full license details. @@ -14,4 +16,84 @@ CRITICAL = 6, TRACE = 7 } + + public static class LogTypeConverter + { + public static ELogType FromLogLevel(this LogLevel logLevel) + { + switch (logLevel) + { + case LogLevel.None: + return ELogType.NONE; + case LogLevel.Error: + return ELogType.ERROR; + case LogLevel.Debug: + return ELogType.DEBUG; + case LogLevel.Critical: + return ELogType.CRITICAL; + case LogLevel.Warning: + return ELogType.WARNING; + case LogLevel.Trace: + return ELogType.TRACE; + default: + return ELogType.INFO; + } + } + + public static ELogType FromSeverity(this ESeverity logLevel) + { + switch (logLevel) + { + case ESeverity.Debug: + return ELogType.DEBUG; + case ESeverity.Warn: + return ELogType.WARNING; + case ESeverity.Emergency: + return ELogType.CRITICAL; + case ESeverity.Critical: + return ELogType.CRITICAL; + case ESeverity.Alert: + return ELogType.ERROR; + case ESeverity.Error: + return ELogType.ERROR; + default: + return ELogType.INFO; + } + } + } + + /// + /// Message severity. + /// + public enum ESeverity + { + /// + /// Debug messages. + /// + Debug = 0, + /// + /// Informational messages. + /// + Info = 1, + /// + /// Warning messages. + /// + Warn = 2, + /// + /// Error messages. + /// + Error = 3, + /// + /// Alert messages. + /// + Alert = 4, + /// + /// Critical messages. + /// + Critical = 5, + /// + /// Emergency messages. + /// + Emergency = 6 + } } \ No newline at end of file diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index 7c9e819..3c8a49e 100644 --- a/EonaCat.Logger/EonaCat.Logger.csproj +++ b/EonaCat.Logger/EonaCat.Logger.csproj @@ -8,7 +8,7 @@ net6.0; icon.ico - 1.0.0 + 1.0.1 EonaCat (Jeroen Saey) true EonaCat (Jeroen Saey) @@ -23,6 +23,8 @@ True LICENSE True + EonaCat.Logger + git diff --git a/EonaCat.Logger/EonaCatCoreLogger/Extensions/FileLoggerFactoryExtensions.cs b/EonaCat.Logger/EonaCatCoreLogger/Extensions/FileLoggerFactoryExtensions.cs index b6ca567..077e0ea 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Extensions/FileLoggerFactoryExtensions.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Extensions/FileLoggerFactoryExtensions.cs @@ -16,20 +16,20 @@ namespace EonaCat.Logger.Extensions /// Adds a file logger named 'File' to the factory. /// /// The to use. - public static ILoggingBuilder AddFile(this ILoggingBuilder builder) + public static ILoggingBuilder AddEonaCatFileLogger(this ILoggingBuilder builder) { builder.Services.AddSingleton(); return builder; } /// - /// Adds a file logger named 'File' to the factory. + /// Adds the EonaCat File Logger named 'EonaCatFileLogger' to the factory. /// /// The to use. /// Sets the filename prefix to use for log files - public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filenamePrefix) + public static ILoggingBuilder AddEonaCatFileLogger(this ILoggingBuilder builder, string filenamePrefix) { - builder.AddFile(options => options.FileNamePrefix = filenamePrefix); + builder.AddEonaCatFileLogger(options => options.FileNamePrefix = filenamePrefix); return builder; } @@ -38,13 +38,13 @@ namespace EonaCat.Logger.Extensions /// /// The to use. /// Configure an instance of the to set logging options - public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action configure) + public static ILoggingBuilder AddEonaCatFileLogger(this ILoggingBuilder builder, Action configure) { if (configure == null) { throw new ArgumentNullException(nameof(configure)); } - builder.AddFile(); + builder.AddEonaCatFileLogger(); builder.Services.Configure(configure); return builder; diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs index 1321b56..550bb60 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs @@ -15,6 +15,7 @@ namespace EonaCat.Logger private int _fileSizeLimit = 200 * 1024 * 1024; private int _retainedFileCountLimit = 50; private int _maxRolloverFiles = 10; + private int _maxTries = 3; public static string DefaultPath => AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; @@ -55,6 +56,20 @@ namespace EonaCat.Logger } } + /// + /// Gets or sets the max times to try to write to the file. + /// Defaults 3. + /// + public int MaxWriteTries + { + get => _maxTries; + + set + { + _maxTries = value; + } + } + /// /// Gets or sets a strictly positive value representing the maximum retained file rollovers or null for no limit. /// Defaults to 10. diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs index 43a54b1..cc575c9 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -1,9 +1,11 @@ using EonaCat.Logger.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime; using System.Threading; using System.Threading.Tasks; @@ -23,9 +25,37 @@ namespace EonaCat.Logger private readonly int _maxFileSize; private readonly int _maxRetainedFiles; private readonly int _maxRolloverFiles; + private readonly int _maxTries; private int _rollOverCount = 0; + private static readonly object _writeLock = new object(); + private string _logFile; - public string LogFile { get; private set; } + /// + /// The file to which log messages should be appended. + /// + public string LogFile + { + get + { + return _logFile; + } + set + { + _logFile = value; + + if (!string.IsNullOrEmpty(_logFile)) + { + string dir = Path.GetDirectoryName(_logFile); + if (!string.IsNullOrEmpty(dir)) + { + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + } + } + } + } /// /// Creates an instance of the @@ -39,6 +69,7 @@ namespace EonaCat.Logger _maxFileSize = loggerOptions.FileSizeLimit; _maxRetainedFiles = loggerOptions.RetainedFileCountLimit; _maxRolloverFiles = loggerOptions.MaxRolloverFiles; + _maxTries = loggerOptions.MaxWriteTries; } /// @@ -77,11 +108,31 @@ namespace EonaCat.Logger } } - using (StreamWriter streamWriter = File.AppendText(LogFile)) + lock (_writeLock) { - foreach (LogMessage item in group) + int tries = 0; + bool completed = false; + + while (!completed) { - await streamWriter.WriteAsync(item.Message); + try + { + System.IO.StreamWriter file = new System.IO.StreamWriter(LogFile, true); + foreach (LogMessage item in group) + { + file.WriteAsync(item.Message); + } + + file.Close(); + completed = true; + } + catch (Exception exc) + { + tries++; + Task.Delay(100); + if (tries >= _maxTries) + throw; + } } } } diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs index 72f8238..ac15f3f 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using EonaCat.logger.Managers; +using Microsoft.Extensions.Logging; using System; using System.Text; @@ -25,35 +26,21 @@ namespace EonaCat.Logger.Internal public bool IsEnabled(LogLevel logLevel) { - if (logLevel == LogLevel.None) - { - return false; - } - return true; + return logLevel != LogLevel.None; } public void Log(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - if (!IsEnabled(logLevel)) - { - return; - } + if (!IsEnabled(logLevel)) return; - StringBuilder builder = new StringBuilder(); - builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz")); - builder.Append(" ["); - builder.Append(logLevel.ToString()); - builder.Append("] "); - builder.Append(_category); - builder.Append(": "); - builder.AppendLine(formatter(state, exception)); + var message = LogHelper.FormatMessageWithHeader(new Managers.LoggerSettings(), logLevel.FromLogLevel(), formatter(state, exception)) + Environment.NewLine; if (exception != null) - { - builder.AppendLine(exception.ToString()); + { + message = exception.FormatExceptionToMessage() + Environment.NewLine; } - _provider.AddMessage(timestamp, builder.ToString()); + _provider.AddMessage(timestamp, message); } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs index 6eb1561..25ebe75 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs @@ -90,7 +90,7 @@ namespace EonaCat.Logger.Internal } catch { - //cancellation token canceled or CompleteAdding called + // cancellation token canceled or CompleteAdding called } } } @@ -128,6 +128,12 @@ namespace EonaCat.Logger.Internal public void Dispose() { Stop(); + GC.SuppressFinalize(this); + } + + ~BatchingLoggerProvider() + { + Dispose(); } public ILogger CreateLogger(string categoryName) diff --git a/EonaCat.Logger/EonaCatCoreLogger/Models/EonaCatLogMessage.cs b/EonaCat.Logger/EonaCatCoreLogger/Models/EonaCatLogMessage.cs index 216c21a..80df403 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Models/EonaCatLogMessage.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Models/EonaCatLogMessage.cs @@ -9,13 +9,13 @@ namespace EonaCatLogger.EonaCatCoreLogger.Models public class EonaCatLogMessage { - public DateTime DateTime { get; internal set; } - public string Message { get; internal set; } - public ELogType LogType { get; internal set; } + public DateTime DateTime { get; set; } + public string Message { get; set; } + public ELogType LogType { get; set; } public override string ToString() { - return $"[{EnumHelper.ToString(LogType)}] [{DateTime.ToString(Constants.DateTimeFormats.LOGGING)}] {Message}"; + return $"[{DateTime.ToString(Constants.DateTimeFormats.LOGGING)}] [{EnumHelper.ToString(LogType)}] {Message}"; } } } \ No newline at end of file diff --git a/EonaCat.Logger/Exceptions/EonaCatLoggerAssertionException.cs b/EonaCat.Logger/Exceptions/EonaCatLoggerAssertionException.cs new file mode 100644 index 0000000..810ec68 --- /dev/null +++ b/EonaCat.Logger/Exceptions/EonaCatLoggerAssertionException.cs @@ -0,0 +1,9 @@ +using System; + +namespace EonaCat.Logger.Exceptions +{ + public class EonaCatLoggerAssertionException : Exception + { + public EonaCatLoggerAssertionException(string message) : base(message) { } + } +} diff --git a/EonaCat.Logger/Managers/ColorSchema.cs b/EonaCat.Logger/Managers/ColorSchema.cs new file mode 100644 index 0000000..0c65962 --- /dev/null +++ b/EonaCat.Logger/Managers/ColorSchema.cs @@ -0,0 +1,74 @@ +using System; + +namespace EonaCat.Logger.Managers +{ + /// + /// Colors to use when writing to the console. + /// + public class ColorSchema + { + private ConsoleColor currentForeground = Console.ForegroundColor; + + /// + /// The color to use for debug messages. Default is dark gray on black. + /// + public ColorScheme Debug = new ColorScheme(ConsoleColor.DarkGray, ConsoleColor.Black); + + /// + /// The color to use for informational messages. Default is gray on black. + /// + public ColorScheme Info = new ColorScheme(ConsoleColor.Gray, ConsoleColor.Black); + + /// + /// The color to use for warning messages. Default is dark red on black. + /// + public ColorScheme Warning = new ColorScheme(ConsoleColor.DarkRed, ConsoleColor.Black); + + /// + /// The color to use for error messages. Default is red on black. + /// + public ColorScheme Error = new ColorScheme(ConsoleColor.Red, ConsoleColor.Black); + + /// + /// The color to use for alert messages. Default is dark yellow on black. + /// + public ColorScheme Traffic = new ColorScheme(ConsoleColor.DarkYellow, ConsoleColor.Black); + + /// + /// The color to use for critical messages. Default is yellow on black. + /// + public ColorScheme Critical = new ColorScheme(ConsoleColor.Yellow, ConsoleColor.Black); + + /// + /// The color to use for emergency messages. Default is white on red. + /// + public ColorScheme Trace = new ColorScheme(ConsoleColor.White, ConsoleColor.Red); + } + + /// + /// Color scheme for logging messages. + /// + public class ColorScheme + { + /// + /// Foreground color. + /// + public ConsoleColor Foreground = Console.ForegroundColor; + + /// + /// Background color. + /// + public ConsoleColor Background = Console.BackgroundColor; + + /// + /// Instantiates a new color scheme. + /// + /// Foreground color. + /// Background color. + public ColorScheme(ConsoleColor foreground, ConsoleColor background) + { + Foreground = foreground; + Background = background; + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Managers/EFileLoggingMode.cs b/EonaCat.Logger/Managers/EFileLoggingMode.cs new file mode 100644 index 0000000..aa887b3 --- /dev/null +++ b/EonaCat.Logger/Managers/EFileLoggingMode.cs @@ -0,0 +1,8 @@ +namespace EonaCat.Logger.Managers +{ + public enum EFileLoggingMode + { + Enabled = 0, + Disabled = 1, + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Managers/ILogManager.cs b/EonaCat.Logger/Managers/ILogManager.cs new file mode 100644 index 0000000..755b223 --- /dev/null +++ b/EonaCat.Logger/Managers/ILogManager.cs @@ -0,0 +1,7 @@ +namespace EonaCat.Logger.Managers +{ + public interface ILogManager + { + void Write(string message, ELogType logType = ELogType.INFO, ELogType? logLevel = null); + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Managers/LogHelper.cs b/EonaCat.Logger/Managers/LogHelper.cs new file mode 100644 index 0000000..3de259a --- /dev/null +++ b/EonaCat.Logger/Managers/LogHelper.cs @@ -0,0 +1,292 @@ +using EonaCat.Logger; +using EonaCat.Logger.Managers; +using EonaCat.Logger.Syslog; +using Microsoft.Extensions.Logging; +using System; +using System.Collections; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EonaCat.logger.Managers +{ + public static class LogHelper + { + private static readonly object _fileLock = new object(); + + /// + /// Format a message with the specified header + /// + /// Logger settings + /// logtype for the formatted message + /// The actual message to format with the header + /// + public static string FormatMessageWithHeader(LoggerSettings settings, ELogType logType, string currentMessage) + { + if (string.IsNullOrWhiteSpace(currentMessage)) + { + return currentMessage; + } + + if (settings == null) + { + return "[EonaCatLogger]" + " " + currentMessage; + } + + string header = settings.HeaderFormat; + if (header.Contains("{ts}")) + { + header = header.Replace("{ts}", DateTime.Now.ToUniversalTime().ToString(settings.TimestampFormat)); + } + + if (header.Contains("{host}")) + { + header = header.Replace("{host}", Dns.GetHostName()); + } + + if (header.Contains("{thread}")) + { + header = header.Replace("{thread}", Thread.CurrentThread.ManagedThreadId.ToString()); + } + + if (header.Contains("{sev}")) + { + header = header.Replace("{sev}", logType.ToString()); + } + + string fullMessage = "[EonaCatLogger]" + " " + header + " " + currentMessage; + return fullMessage; + } + + /// + /// Formats a given exception as a string + /// + /// exception + /// The name of the module which called the code (optional) + /// The name of the method which waws called in code (optional) + /// + public static string FormatExceptionToMessage(this Exception exception, string module = null, string method = null) + { + if (exception == null) return string.Empty; + var st = new StackTrace(exception, true); + var frame = st.GetFrame(0); + int fileLine = frame.GetFileLineNumber(); + string filename = frame.GetFileName(); + + string message = + Environment.NewLine + + "--- [EonaCatLogger] Exception details ---" + Environment.NewLine + + (!string.IsNullOrEmpty(module) ? " Module : " + module + Environment.NewLine : "") + + (!string.IsNullOrEmpty(method) ? " Method : " + method + Environment.NewLine : "") + + " Type : " + exception.GetType().ToString() + Environment.NewLine; + + if (exception.Data != null && exception.Data.Count > 0) + { + message += " Data : " + Environment.NewLine; + foreach (DictionaryEntry curr in exception.Data) + { + message += " | " + curr.Key + ": " + curr.Value + Environment.NewLine; + } + } + else + { + message += " Data : (none)" + Environment.NewLine; + } + + message += + " Inner : "; + + if (exception.InnerException == null) message += "(null)" + Environment.NewLine; + else + { + message += exception.InnerException.GetType().ToString() + Environment.NewLine; + message += + " Message : " + exception.InnerException.Message + Environment.NewLine + + " Source : " + exception.InnerException.Source + Environment.NewLine + + " StackTrace : " + exception.InnerException.StackTrace + Environment.NewLine + + " ToString : " + exception.InnerException.ToString() + Environment.NewLine; + + if (exception.InnerException.Data != null && exception.InnerException.Data.Count > 0) + { + message += " Data : " + Environment.NewLine; + foreach (DictionaryEntry curr in exception.Data) + { + message += " | " + curr.Key + ": " + curr.Value + Environment.NewLine; + } + } + else + { + message += " Data : (none)" + Environment.NewLine; + } + } + + message += + " Message : " + exception.Message + Environment.NewLine + + " Source : " + exception.Source + Environment.NewLine + + " StackTrace : " + exception.StackTrace + Environment.NewLine + + " Line : " + fileLine + Environment.NewLine + + " File : " + filename + Environment.NewLine + + " ToString : " + exception.ToString() + Environment.NewLine + + "---"; + return message; + } + + internal static void SendConsole(LoggerSettings settings, ELogType logType, string message) + { + if (settings == null) return; + if (!settings.EnableConsole) return; + if (string.IsNullOrWhiteSpace(message)) return; + + if (settings.EnableColors) + { + ConsoleColor prevForeground = Console.ForegroundColor; + ConsoleColor prevBackground = Console.BackgroundColor; + + if (settings.Colors != null) + { + switch (logType) + { + case ELogType.DEBUG: + Console.ForegroundColor = settings.Colors.Debug.Foreground; + Console.BackgroundColor = settings.Colors.Debug.Background; + break; + case ELogType.INFO: + Console.ForegroundColor = settings.Colors.Info.Foreground; + Console.BackgroundColor = settings.Colors.Info.Background; + break; + case ELogType.WARNING: + Console.ForegroundColor = settings.Colors.Warning.Foreground; + Console.BackgroundColor = settings.Colors.Warning.Background; + break; + case ELogType.ERROR: + Console.ForegroundColor = settings.Colors.Error.Foreground; + Console.BackgroundColor = settings.Colors.Error.Background; + break; + case ELogType.TRAFFIC: + Console.ForegroundColor = settings.Colors.Traffic.Foreground; + Console.BackgroundColor = settings.Colors.Traffic.Background; + break; + case ELogType.CRITICAL: + Console.ForegroundColor = settings.Colors.Critical.Foreground; + Console.BackgroundColor = settings.Colors.Critical.Background; + break; + case ELogType.TRACE: + Console.ForegroundColor = settings.Colors.Trace.Foreground; + Console.BackgroundColor = settings.Colors.Trace.Background; + break; + } + } + + Console.WriteLine(message); + Console.ForegroundColor = prevForeground; + Console.BackgroundColor = prevBackground; + } + else + { + Console.WriteLine(message); + } + } + + internal static void SendFile(ILogger logger, LoggerSettings settings, ELogType logType, string message, int maxTries = 3) + { + lock (_fileLock) + { + if (logger == null) return; + if (settings == null) return; + if (!settings.EnableFileLogging) return; + if (string.IsNullOrWhiteSpace(message)) return; + //logger.LogInformation(message); + + int tries = 0; + bool completed = false; + while (!completed) + { + try + { + System.IO.StreamWriter file = new System.IO.StreamWriter($"{settings.FileLoggerOptions.LogDirectory}{Path.DirectorySeparatorChar}{settings.FileLoggerOptions.FileNamePrefix}_{DateTime.Now.ToString("yyyyMMdd")}.log", true); + file.Write(message + Environment.NewLine); + file.Close(); + completed = true; + } + catch (Exception exc) + { + tries++; + Task.Delay(100); + if (tries >= maxTries) + throw; + } + } + return; + + if (logType == ELogType.CRITICAL) + { + logger.LogCritical(message); + } + else if (logType == ELogType.DEBUG) + { + logger.LogDebug(message); + } + else if (logType == ELogType.ERROR) + { + logger.LogError(message); + } + else if (logType == ELogType.INFO) + { + logger.LogInformation(message); + } + else if (logType == ELogType.TRACE) + { + logger.LogTrace(message); + } + else if (logType == ELogType.TRAFFIC) + { + logger.LogTrace($"[TRAFFIC] {message}"); + } + else if (logType == ELogType.WARNING) + { + logger.LogWarning(message); + } + } + } + + internal static void SendToSysLogServers(LoggerSettings settings, string message) + { + if (settings == null) return; + if (!settings.SendToSyslogServers) return; + if (settings.SysLogServers == null) return; + if (!settings.SysLogServers.Any()) return; + if (string.IsNullOrWhiteSpace(message)) return; + + byte[] data = Encoding.UTF8.GetBytes(message); + + var sysLogServers = settings.SysLogServers.ToList(); + foreach (SyslogServer server in sysLogServers) + { + lock (server.SendLock) + { + if (string.IsNullOrWhiteSpace(server.Hostname)) + { + Console.WriteLine("Server hostname not specified, skipping syslog server"); + } + if (server.Port < 0) + { + Console.WriteLine("Server port must be zero or greater, skipping syslog server"); + } + + try + { + server.Udp.Send(data, data.Length); + } + catch (Exception) + { + + } + } + } + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Managers/LogManager.cs b/EonaCat.Logger/Managers/LogManager.cs index caf95af..d1b616f 100644 --- a/EonaCat.Logger/Managers/LogManager.cs +++ b/EonaCat.Logger/Managers/LogManager.cs @@ -1,13 +1,17 @@ -using EonaCat.Extensions; +using EonaCat.logger.Managers; +using EonaCat.Logger.Exceptions; using EonaCat.Logger.Extensions; using EonaCat.Logger.Helpers; +using EonaCat.Logger.Syslog; using EonaCatLogger.EonaCatCoreLogger.Models; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; -using System.Text; +using System.Net; using System.Threading; using System.Threading.Tasks; @@ -16,11 +20,15 @@ namespace EonaCat.Logger.Managers // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/License for full license details. - public class LogManager : IDisposable + public partial class LogManager : ILogManager, IDisposable { private readonly object _batton = new object(); private DateTime _logDate; + public event LogDelegate OnLog; + public delegate void LogDelegate(LogManager logger, ELogType logLevel, EonaCatLogMessage message); + + public ELogType LogType; private ILoggerProvider LoggerProvider { get; set; } private ILoggerFactory LoggerFactory { get; set; } private ILogger Logger { get; set; } @@ -29,13 +37,50 @@ namespace EonaCat.Logger.Managers public bool IsRunning { get; private set; } public StreamWriter Output { get; private set; } - public string LogFolderPath { get; set; } = "logs"; - public FileLoggerOptions FileLoggerOptions { get; private set; } - public int FileSizeLimit { get; set; } = 200 * 1024 * 1024; + public string CategoryName { get; set; } - private LogManager Instance { get; set; } + + public readonly string Id; private bool _disposed; + private static LogManager _instance; + private LoggerSettings _settings; + private CancellationTokenSource _TokenSource = new CancellationTokenSource(); + private CancellationToken _Token; + + /// + /// Default Logger Instance with it's default configuration + /// + public static LogManager Instance => InstanceInit(); + + /// + /// Logging settings. + /// + public LoggerSettings Settings + { + get + { + if (_settings == null) + { + _settings = new LoggerSettings(); + } + return _settings; + } + + set + { + _settings = value; + } + } + + private static LogManager InstanceInit() + { + if (_instance == null) + { + _instance = new LogManager(null, id: "EonaCat"); + } + return _instance; + } protected virtual void Dispose(bool disposing) { @@ -49,47 +94,20 @@ namespace EonaCat.Logger.Managers if (disposing) { StopLogging(); + _TokenSource.Cancel(); } _disposed = true; } } - public void Dispose() - { - Dispose(true); - } - - public static async Task DownloadLogAsync(HttpResponse response, string logFile, long limit) - { - using (FileStream fileStream = new FileStream(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - response.ContentType = "text/plain"; - response.Headers.Add("Content-Disposition", "attachment;filename=" + Path.GetFileName(logFile)); - - if (limit > fileStream.Length) - { - limit = fileStream.Length; - } - - using (OffsetStream offsetStream = new OffsetStream(fileStream, 0, limit)) - { - using (Stream stream = response.Body) - { - offsetStream.CopyTo(stream); - - if (fileStream.Length > limit) - { - byte[] buffer = Encoding.UTF8.GetBytes("####___EonaCatLogger_TRUNCATED___####"); - await stream.WriteAsync(buffer, 0, buffer.Length); - } - } - } - } - } - private void StartNewLog() { + if (_TokenSource.IsCancellationRequested) + { + return; + } + DateTime now = DateTime.Now; if (IsRunning && now.Date > _logDate.Date) @@ -99,143 +117,157 @@ namespace EonaCat.Logger.Managers IsRunning = true; IServiceCollection serviceCollection = new ServiceCollection(); - CreateDefaultFileLoggerOptions(); - - serviceCollection.AddLogging(builder => builder.AddFile(configuration => + serviceCollection.AddLogging(builder => builder.AddEonaCatFileLogger(configuration => { - configuration.BackgroundQueueSize = FileLoggerOptions.BackgroundQueueSize; - configuration.BatchSize = FileLoggerOptions.BatchSize; - configuration.FileNamePrefix = FileLoggerOptions.FileNamePrefix; - configuration.FileSizeLimit = FileLoggerOptions.FileSizeLimit; - configuration.FlushPeriod = FileLoggerOptions.FlushPeriod; - configuration.IsEnabled = FileLoggerOptions.IsEnabled; - configuration.LogDirectory = FileLoggerOptions.LogDirectory; - configuration.RetainedFileCountLimit = FileLoggerOptions.RetainedFileCountLimit; + configuration = Settings.FileLoggerOptions; })); var serviceProvider = serviceCollection.BuildServiceProvider(); LoggerProvider = serviceProvider.GetService(); LoggerFactory = serviceProvider.GetService(); + CategoryName = CategoryName ?? string.Empty; Logger = LoggerFactory.CreateLogger(CategoryName); - if (!Directory.Exists(FileLoggerOptions.LogDirectory)) + if (!Directory.Exists(Settings.FileLoggerOptions.LogDirectory)) { - Directory.CreateDirectory(FileLoggerOptions.LogDirectory); + Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory); } _logDate = now; - Write(now, "EonaCatLogger started."); + Write(now, $"{DllInfo.ApplicationName} started."); } - private void CreateDefaultFileLoggerOptions() + public void Assert(bool condition, string message) { - if (FileLoggerOptions == null) + if (!condition) { - FileLoggerOptions = new FileLoggerOptions - { - LogDirectory = LogFolderPath, - FileSizeLimit = FileSizeLimit - }; - } - } - - private void WriteToFile(EonaCatLogMessage EonaCatLogMessage) - { - if (EonaCatLogMessage.LogType == ELogType.CRITICAL) - { - Instance.Logger.LogCritical(EonaCatLogMessage.Message); - } - else if (EonaCatLogMessage.LogType == ELogType.DEBUG) - { - Instance.Logger.LogDebug(EonaCatLogMessage.Message); - } - else if (EonaCatLogMessage.LogType == ELogType.ERROR) - { - Instance.Logger.LogError(EonaCatLogMessage.Message); - } - else if (EonaCatLogMessage.LogType == ELogType.INFO) - { - Instance.Logger.LogInformation(EonaCatLogMessage.Message); - } - else if (EonaCatLogMessage.LogType == ELogType.TRACE) - { - Instance.Logger.LogTrace(EonaCatLogMessage.Message); - } - else if (EonaCatLogMessage.LogType == ELogType.TRAFFIC) - { - Instance.Logger.LogTrace($"[TRAFFIC] {EonaCatLogMessage.Message}"); - } - else if (EonaCatLogMessage.LogType == ELogType.WARNING) - { - Instance.Logger.LogWarning(EonaCatLogMessage.Message); + throw new EonaCatLoggerAssertionException(message); } } private void Write(DateTime dateTime, string message, ELogType logType = ELogType.INFO) { - var EonaCatMessage = new EonaCatLogMessage { DateTime = dateTime, Message = message, LogType = logType }; - WriteToFile(EonaCatMessage); + if (string.IsNullOrWhiteSpace(message)) return; + if (logType < ELogType.INFO) return; + + string remainder = ""; + string currentMessage; + + if (message.Length > _settings.MaxMessageLength) + { + currentMessage = message.Substring(0, _settings.MaxMessageLength); + remainder = message.Substring(_settings.MaxMessageLength, (message.Length - _settings.MaxMessageLength)); + } + else + { + currentMessage = message; + } + + var fullMessage = LogHelper.FormatMessageWithHeader(_settings, logType, currentMessage); + + LogHelper.SendConsole(_settings, logType, fullMessage); + + LogHelper.SendFile(Logger, _settings, logType, fullMessage); + + LogHelper.SendToSysLogServers(_settings, fullMessage); + + if (!string.IsNullOrEmpty(remainder)) + { + Write(dateTime, remainder, logType); + } + + var EonaCatMessage = new EonaCatLogMessage { DateTime = dateTime, Message = currentMessage, LogType = logType }; + OnLog?.Invoke(this, logType, EonaCatMessage); } - private LogManager() - { - // Do nothing - } + public void Reset() => OnLog = null; - public LogManager(FileLoggerOptions fileLoggerOptions) + public LogManager(LoggerSettings settings, string serverIp, int serverPort, string id = "EonaCatLogger") { - FileLoggerOptions = fileLoggerOptions; + if (string.IsNullOrEmpty(serverIp)) throw new ArgumentNullException(nameof(serverIp)); + if (serverPort < 0) throw new ArgumentException("Server port must be zero or greater."); + + settings.SysLogServers = new List(); + settings.SysLogServers.Add(new SyslogServer(serverIp, serverPort)); + + Id = id; + Settings = settings; SetupLogManager(); } - private void SetupLogManager() + public LogManager(LoggerSettings settings, string id = "EonaCatLogger") { - AppDomain.CurrentDomain.ProcessExit += ProcessExit; - - Instance = this; - - _logDate = DateTime.Now; - - StartNewLog(); + Id = id; + Settings = settings; + SetupFileLogger(settings, null, true); + SetupLogManager(); } - private void ProcessExit(object sender, EventArgs e) + private void SetupFileLogger(LoggerSettings settings = null, string logFolder = null, bool defaultPrefix = true) { - Instance?.StopLogging(); - Thread.Sleep(1000); - } + if (settings == null) + { + // Create default loggingSettings + Settings = settings; + settings = Settings; + } - private void StopLogging() - { - Write(DateTime.Now, "EonaCatLogger stopped."); - IsRunning = false; - } - - public LogManager(string logFolder = null, bool defaultPrefix = true) - { - CreateDefaultFileLoggerOptions(); + if (!settings.EnableFileLogging) return; + if (logFolder != null) { - FileLoggerOptions.LogDirectory = logFolder; + Settings.FileLoggerOptions.LogDirectory = logFolder; } if (defaultPrefix) { - FileLoggerOptions.FileNamePrefix = "EonaCat"; + Settings.FileLoggerOptions.FileNamePrefix = "EonaCat"; } else { - FileLoggerOptions.FileNamePrefix = string.Empty; + Settings.FileLoggerOptions.FileNamePrefix = string.Empty; } + } + private void SetupLogManager() + { + _Token = _TokenSource.Token; + AppDomain.CurrentDomain.ProcessExit += ProcessExit; + + Console.CancelKeyPress += new ConsoleCancelEventHandler(Console_CancelKeyPress); + + _logDate = DateTime.Now; + } + + void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + Dispose(true); + } + + private void ProcessExit(object sender, EventArgs e) + { + Dispose(true); + } + + private void StopLogging() + { + IsRunning = false; + Write(DateTime.Now, $"{DllInfo.ApplicationName} stopped."); + Task.Delay(500); + } + + public LogManager(string logFolder = null, bool defaultPrefix = true) + { + SetupFileLogger(null, logFolder, defaultPrefix); SetupLogManager(); } - public void Write(Exception exception) + public void Write(Exception exception, string module = null, string method = null, bool criticalException = false) { - Write(exception.ToString()); + if (exception != null) return; + Write(exception.FormatExceptionToMessage(module, method), criticalException ? ELogType.CRITICAL : ELogType.ERROR); } public void Write(string message, ELogType logType = ELogType.INFO, ELogType? logLevel = null) @@ -256,7 +288,7 @@ namespace EonaCat.Logger.Managers StartNewLog(); } - Write(now, $"{DllInfo.ApplicationName}: {message}", logType); + Write(now, message, logType); } } } @@ -269,8 +301,12 @@ namespace EonaCat.Logger.Managers { File.Delete(CurrentLogFile); } - StartNewLog(); } } + + void IDisposable.Dispose() + { + Dispose(true); + } } } \ No newline at end of file diff --git a/EonaCat.Logger/Managers/LoggerSettings.cs b/EonaCat.Logger/Managers/LoggerSettings.cs new file mode 100644 index 0000000..bd5c571 --- /dev/null +++ b/EonaCat.Logger/Managers/LoggerSettings.cs @@ -0,0 +1,173 @@ +using System.IO; +using System; +using System.Collections.Generic; +using EonaCat.Logger.Syslog; +using System.Runtime; + +namespace EonaCat.Logger.Managers +{ + /// + /// Logger settings. + /// + public class LoggerSettings + { + /// + /// Header format. Provide a string that specifies how the preamble of each message should be structured. You can use variables including: + /// {ts}: UTC timestamp + /// {host}: Hostname + /// {thread}: Thread ID + /// {sev}: Severity + /// Default: {ts} {host} {thread} {sev} + /// A space will be inserted between the header and the message. + /// + public string HeaderFormat + { + get + { + return _HeaderFormat; + } + set + { + if (string.IsNullOrEmpty(value)) _HeaderFormat = ""; + else _HeaderFormat = value; + } + } + + /// + /// Timestamp format. + /// + public string TimestampFormat + { + get + { + return _TimestampFormat; + } + set + { + if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(HeaderFormat)); + _TimestampFormat = value; + } + } + + /// + /// Enable or disable console logging. + /// Settings this to true will first validate if a console exists. + /// If a console is not available, it will be set to false. + /// + public bool EnableConsole + { + get + { + return _EnableConsole; + } + set + { + if (value) _EnableConsole = ConsoleExists(); + else _EnableConsole = false; + } + } + + /// + /// Minimum severity required to send a message. + /// + public ESeverity MinimumSeverity { get; set; } = ESeverity.Debug; + + /// + /// Enable or disable use of color for console messages. + /// + public bool EnableColors { get; set; } = true; + + /// + /// Colors to use for console messages based on message severity. + /// + public ColorSchema Colors + { + get + { + return _colors; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(Colors)); + } + _colors = value; + } + } + + public bool SendToSyslogServers { get; set; } + + public List SysLogServers { get; set; } = new List { new SyslogServer("127.0.0.1", 514) }; + + /// + /// Determines if the fileLogging is enabled + /// + public bool EnableFileLogging { get; set; } = true; + + private FileLoggerOptions _fileLoggerOptions; + + /// + /// FileLogger settings. + /// + public FileLoggerOptions FileLoggerOptions + { + get + { + if (_fileLoggerOptions == null) + { + _fileLoggerOptions = CreateDefaultFileLoggerOptions(); + } + return _fileLoggerOptions; + } + + set + { + _fileLoggerOptions = value; + } + } + + private FileLoggerOptions CreateDefaultFileLoggerOptions() + { + return new FileLoggerOptions(); + } + + /// + /// Maximum message length. Must be greater than or equal to 32. Default is 1024. + /// + public int MaxMessageLength + { + get + { + return _MaxMessageLength; + } + set + { + if (value < 32) throw new ArgumentException("Maximum message length must be at least 32."); + _MaxMessageLength = value; + } + } + + private string _HeaderFormat = "{ts} {host} {thread} {sev}"; + private string _TimestampFormat = "yyyy-MM-dd HH:mm:ss"; + private bool _EnableConsole = true; + private int _MaxMessageLength = 1024; + private ColorSchema _colors = new ColorSchema(); + + + private bool ConsoleExists() + { + try + { + bool test1 = Environment.UserInteractive; + bool test2 = Console.WindowHeight > 0; + return test1 && test2; + } + catch (Exception) + { + return false; + } + } + + } +} \ No newline at end of file diff --git a/EonaCat.Logger/Syslog/SyslogServer.cs b/EonaCat.Logger/Syslog/SyslogServer.cs new file mode 100644 index 0000000..f044b14 --- /dev/null +++ b/EonaCat.Logger/Syslog/SyslogServer.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text; + +namespace EonaCat.Logger.Syslog +{ + /// + /// Syslog server. + /// + public class SyslogServer + { + /// + /// Hostname. + /// + public string Hostname + { + get + { + return _Hostname; + } + set + { + if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(Hostname)); + _Hostname = value; + + SetUdp(); + } + } + + /// + /// UDP port. + /// + public int Port + { + get + { + return _Port; + } + set + { + if (value < 0) throw new ArgumentException("Port must be zero or greater."); + _Port = value; + + SetUdp(); + } + } + + /// + /// IP:port of the server. + /// + public string IpPort + { + get + { + return _Hostname + ":" + _Port; + } + } + + internal readonly object SendLock = new object(); + internal UdpClient Udp = null; + private string _Hostname = "127.0.0.1"; + private int _Port = 514; + + /// + /// Instantiate the object. + /// + public SyslogServer() + { + } + + /// + /// Instantiate the object. + /// + /// Hostname. + /// Port. + public SyslogServer(string hostname = "127.0.0.1", int port = 514) + { + Hostname = hostname; + Port = port; + } + + private void SetUdp() + { + Udp = null; + Udp = new UdpClient(_Hostname, _Port); + } + } +} diff --git a/README.md b/README.md index 4422e84..a5da14c 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,12 @@ EonaCat Logger -Log application information to log files \ No newline at end of file +Log application information to log files + + +Be sure the following dependencies are added: + +Microsoft.Extensions.Logging.Abstractions +Microsoft.Extensions.DependencyInjection.Abstractions +Microsoft.Extensions.DependencyInjection +Microsoft.Extensions.Logging \ No newline at end of file