diff --git a/code/portaloggy.sln b/code/portaloggy.sln new file mode 100644 index 0000000..9936045 --- /dev/null +++ b/code/portaloggy.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "portaloggy", "portaloggy\portaloggy.csproj", "{67A83ECB-9B90-47E1-B56A-BAFE8DB64B1D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "portaloggy.Tester", "portaloggy.Tester\portaloggy.Tester.csproj", "{4BE8310F-EC55-443C-A5AF-3C2D065E314C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {67A83ECB-9B90-47E1-B56A-BAFE8DB64B1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67A83ECB-9B90-47E1-B56A-BAFE8DB64B1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67A83ECB-9B90-47E1-B56A-BAFE8DB64B1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67A83ECB-9B90-47E1-B56A-BAFE8DB64B1D}.Release|Any CPU.Build.0 = Release|Any CPU + {4BE8310F-EC55-443C-A5AF-3C2D065E314C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4BE8310F-EC55-443C-A5AF-3C2D065E314C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4BE8310F-EC55-443C-A5AF-3C2D065E314C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4BE8310F-EC55-443C-A5AF-3C2D065E314C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EF3D422B-47DF-4283-A055-D1A59E923175} + EndGlobalSection +EndGlobal diff --git a/code/portaloggy/Constants.cs b/code/portaloggy/Constants.cs new file mode 100644 index 0000000..a9a21b2 --- /dev/null +++ b/code/portaloggy/Constants.cs @@ -0,0 +1,15 @@ +namespace portaloggy +{ + public class Constants + { + public const string LOG_SEVERITY_NONE = "NONE"; + public const string LOG_SEVERITY_TRACE = "TRACE"; + public const string LOG_SEVERITY_DEBUG = "DEBUG"; + public const string LOG_SEVERITY_INFO = "INFO"; + public const string LOG_SEVERITY_WARN = "WARN"; + public const string LOG_SEVERITY_ERROR = "ERROR"; + public const string LOG_SEVERITY_FATAL = "FATAL"; + public const string LOG_SEVERITY_SUCCESS = "SUCCESS"; + public const string LOG_SEVERITY_ANCHOR = "ANCHOR"; + } +} diff --git a/code/portaloggy/GenericPrettifier.cs b/code/portaloggy/GenericPrettifier.cs new file mode 100644 index 0000000..61fc6ea --- /dev/null +++ b/code/portaloggy/GenericPrettifier.cs @@ -0,0 +1,52 @@ +namespace portaloggy +{ + public static class GenericPrettifier + { + public static string GetPrettifiedSeverity(string severity = null) + { + var calculatedSeverity = severity ?? Constants.LOG_SEVERITY_NONE; + + if(calculatedSeverity.Length > 5) + { + calculatedSeverity = calculatedSeverity.Substring(0, 5); + } + + return calculatedSeverity.PadLeft(5); + } + + public static string GetPrettifiedSource(string knownCallerMemberName, string knownCallerFilePath, int knownCallerLineNumber, bool isFileNameToClassTypeTranslationEnabled = true) + { + var computedSource = string.Empty; + var sourceFile = new FileInfo(knownCallerFilePath); + + if(isFileNameToClassTypeTranslationEnabled) + { + computedSource = $"{sourceFile.Name.Substring(0, sourceFile.Name.Length - sourceFile.Extension.Length)}"; + } + else + { + computedSource = $"{sourceFile.Name}"; + } + + if(computedSource.Length>20) + { + computedSource = computedSource.Substring(computedSource.Length - 17); + computedSource = $"...{computedSource}"; + } + + computedSource = computedSource.PadRight(20); + + if(knownCallerLineNumber > 0) + { + var formattedLineNumber = knownCallerLineNumber.ToString().PadRight(4); + computedSource = $"{computedSource}:{formattedLineNumber}"; + } + else + { + computedSource = computedSource.PadRight(25); + } + + return computedSource; + } + } +} diff --git a/code/portaloggy/ILogger.cs b/code/portaloggy/ILogger.cs new file mode 100644 index 0000000..7ccd1dc --- /dev/null +++ b/code/portaloggy/ILogger.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; + +namespace portaloggy +{ + /// + /// Universal logger. Use everywhere. + /// + public interface ILogger + { + /// + /// Logs a message. + /// + /// Your message. + /// Optionally specify severity. + /// Optionally give exception. + void Log( + string message, + string severity = null, + Exception exception = null, + [CallerMemberName] string memberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0); + } +} diff --git a/code/portaloggy/LoggerExtensions.cs b/code/portaloggy/LoggerExtensions.cs new file mode 100644 index 0000000..854e017 --- /dev/null +++ b/code/portaloggy/LoggerExtensions.cs @@ -0,0 +1,112 @@ +using System.Runtime.CompilerServices; + +namespace portaloggy +{ + public static class LoggerExtensions + { + /// + /// Trace messages are debug messages which are too many to even store. + /// + /// + /// For example, you're checking a directory for new files every 5 seconds, and you generate 5 log message for + /// starting, running, ending and result of the operation. You run this over 5 directories. That's 25 messages + /// per 5 seconds. You don't need to log this unless you're really after some serious bug that you're hunting. + /// Designating this kind of log message as trace allows us to ignore it, as it would baloon our log files. + /// + public static void Trace( + this ILogger logger, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger.Log(message, Constants.LOG_SEVERITY_TRACE, null, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Debug messages that are likely not going to be enabled/collected from a production build, or will be only + /// enabled temporarily to hunt down a bug. Use , if + /// you're expecting to have so many messages, that it would make the log output basically unreadable. + /// + public static void Debug( + this ILogger logger, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger.Log(message, Constants.LOG_SEVERITY_DEBUG, null, callerMemberName, callerFilePath, callerLineNumber); + } + + public static void Info( + this ILogger logger, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger.Log(message, Constants.LOG_SEVERITY_INFO, null, callerMemberName, callerFilePath, callerLineNumber); + } + + public static void Warning( + this ILogger logger, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger.Log(message, Constants.LOG_SEVERITY_WARN, null, callerMemberName, callerFilePath, callerLineNumber); + } + + public static void Success( + this ILogger logger, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger.Log(message, Constants.LOG_SEVERITY_SUCCESS, null, callerMemberName, callerFilePath, callerLineNumber); + } + + public static void Error( + this ILogger logger, + string message, + Exception ex = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger.Log(message, Constants.LOG_SEVERITY_ERROR, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Irrecoverable error after which the process or application will die. + /// + public static void Fatal( + this ILogger logger, + string message, + Exception ex = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + logger.Log(message, Constants.LOG_SEVERITY_FATAL, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Creates a log line that carries keywords, such as #specificScenario, to allow machine-readable identification + /// of log line in log output. Your message value is prefixed with a ":::". + /// + public static void Anchor( + this ILogger logger, + string message, + params string[] keywords) + { + try + { + logger.Log($"Anchored log line {string.Join(", ", keywords)}:::{message}", Constants.LOG_SEVERITY_ANCHOR); + } + catch { } + } + } +} diff --git a/code/portaloggy/Loggers/AggregatedLogger.cs b/code/portaloggy/Loggers/AggregatedLogger.cs new file mode 100644 index 0000000..9874fff --- /dev/null +++ b/code/portaloggy/Loggers/AggregatedLogger.cs @@ -0,0 +1,37 @@ +using System.Runtime.CompilerServices; + +namespace portaloggy +{ + /// + /// Allows logging into multiple loggers simultaneously. + /// + public class AggregatedLogger : ILogger + { + private readonly ICollection loggers; + + public AggregatedLogger(params ILogger[] loggers) + { + if (loggers == null) + { + throw new ArgumentNullException(nameof(loggers)); + } + else if (loggers.Length == 0) + { + throw new ArgumentException(nameof(loggers)); + } + + this.loggers = loggers; + } + + public void Log(string message, string severity = null, Exception exception = null, [CallerMemberName] string memberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + lock(this.loggers) + { + foreach(ILogger logger in this.loggers) + { + logger.Log(message, severity, exception, memberName, callerFilePath, sourceLineNumber); + } + } + } + } +} diff --git a/code/portaloggy/Loggers/ConsoleLogger.cs b/code/portaloggy/Loggers/ConsoleLogger.cs new file mode 100644 index 0000000..7d3cfe3 --- /dev/null +++ b/code/portaloggy/Loggers/ConsoleLogger.cs @@ -0,0 +1,111 @@ +using System.Runtime.CompilerServices; + +namespace portaloggy +{ + public class ConsoleLogger : ILogger + { + /// + /// If enabled, logger will print [TRACE] logs. Warning: There are supposed to be A LOT of those. + /// + public bool IsTraceOutputEnabled = false; + + /// + /// If enabled, logger will print [DEBUG] logs. + /// + public bool IsDebugOutputEnabled = true; + + /// + /// If nabled, source file names will be trated as class types. + /// + public bool IsFileNameToClassTypeTranslationEnabled = true; + + /// + /// If enabled, messages will use different coloring based on their severity. + /// + public bool IsMessageSeverityColoringEnabled = true; + + private object printLocker = new object(); + + public void Log(string message, string severity = null, Exception exception = null, [CallerMemberName] string memberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + try + { + if (severity == Constants.LOG_SEVERITY_TRACE && this.IsTraceOutputEnabled == false) + { + return; + } + else if (severity == Constants.LOG_SEVERITY_DEBUG && this.IsDebugOutputEnabled == false) + { + return; + } + + var processedMessage = exception == null + ? message + : $"{exception.GetType().Name}: {exception.Message}"; + + if (exception != null && !string.IsNullOrWhiteSpace(message)) + { + processedMessage = $"{message} Error: {processedMessage}"; + } + + this.Print(severity, $"[{DateTime.Now:s}] - {GenericPrettifier.GetPrettifiedSource(memberName, callerFilePath, sourceLineNumber, this.IsFileNameToClassTypeTranslationEnabled)} - {GenericPrettifier.GetPrettifiedSeverity(severity)} - {processedMessage ?? "No message."}"); + + if (exception != null) + { + var stacktrace = this.FormatStacktrace(exception); + + if (string.IsNullOrWhiteSpace(stacktrace) == false) + { + Console.WriteLine("--- STACKTRACE ---"); + Console.WriteLine(stacktrace); + } + } + } + catch { } // Ignore. We do not crash because of a failed log. + } + + private void Print(string severity, string message) + { + lock (this.printLocker) + { + if (this.IsMessageSeverityColoringEnabled == false) + { + Console.WriteLine(message); + } + else if (severity == Constants.LOG_SEVERITY_TRACE) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + } + else if(severity == Constants.LOG_SEVERITY_DEBUG) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + } + else if(severity == Constants.LOG_SEVERITY_WARN) + { + Console.ForegroundColor = ConsoleColor.Yellow; + } + else if(severity == Constants.LOG_SEVERITY_ERROR) + { + Console.ForegroundColor = ConsoleColor.Red; + } + else if(severity == Constants.LOG_SEVERITY_FATAL) + { + Console.ForegroundColor = ConsoleColor.Red; + } + else if(severity == Constants.LOG_SEVERITY_SUCCESS) + { + Console.ForegroundColor = ConsoleColor.Green; + } + + Console.WriteLine(message); + Console.ResetColor(); + } + } + + private string FormatStacktrace(Exception ex) + { + var trace = $"\n{ex.StackTrace}"; + return trace; + } + } +} diff --git a/code/portaloggy/portaloggy.csproj b/code/portaloggy/portaloggy.csproj new file mode 100644 index 0000000..43feb73 --- /dev/null +++ b/code/portaloggy/portaloggy.csproj @@ -0,0 +1,14 @@ + + + net8.0 + enable + disable + True + portaloggy + mc + 1.0.1 + $(AssemblyVersion) + A highly-portable, multi-platform, system-agnostic logging abstraction. Use portaloggy.ILogger everywhere. Make use of portaloggy.LoggerExtensions. Already contains ConsoleLogger for dead-simple console logging and AggregatedLogger for simultaneous logging to your own implementation of ILogger. + mc @ 2024 + +