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
+
+