From 32d49870c62bdeba845ddad904269bf2e73fc55a Mon Sep 17 00:00:00 2001 From: Simon Schmid Date: Thu, 29 Sep 2022 18:40:44 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 8 + .gitignore | 18 +++ Sherlog.sln | 56 +++++++ src/Directory.Build.props | 43 ++++++ src/Directory.Build.targets | 16 ++ .../src/AbstractTcpSocketAppender.cs | 80 ++++++++++ src/Sherlog.Appenders/src/ConsoleAppender.cs | 29 ++++ .../src/FileWriterAppender.cs | 30 ++++ .../src/Sherlog.Appenders.csproj | 16 ++ src/Sherlog.Appenders/src/SosMaxAppender.cs | 28 ++++ .../src/TcpSocketAppender.cs | 11 ++ .../src/ColorCodeFormatter.cs | 51 ++++++ src/Sherlog.Formatters/src/LogFormatter.cs | 4 + .../src/LogMessageFormatter.cs | 12 ++ .../src/Sherlog.Formatters.csproj | 12 ++ .../src/TimestampFormatter.cs | 14 ++ .../tests/LogMessageFormatterTests.cs | 24 +++ .../tests/Sherlog.Formatters.Tests.csproj | 23 +++ src/Sherlog/src/LogLevel.cs | 14 ++ src/Sherlog/src/Logger.cs | 46 ++++++ src/Sherlog/src/LoggerStatic.cs | 64 ++++++++ src/Sherlog/src/Sherlog.csproj | 8 + src/Sherlog/tests/LoggerTests.cs | 93 +++++++++++ src/Sherlog/tests/Sherlog.Tests.csproj | 22 +++ src/Sherlog/tests/SherlogTests.cs | 145 ++++++++++++++++++ 25 files changed, 867 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 Sherlog.sln create mode 100644 src/Directory.Build.props create mode 100644 src/Directory.Build.targets create mode 100644 src/Sherlog.Appenders/src/AbstractTcpSocketAppender.cs create mode 100644 src/Sherlog.Appenders/src/ConsoleAppender.cs create mode 100644 src/Sherlog.Appenders/src/FileWriterAppender.cs create mode 100644 src/Sherlog.Appenders/src/Sherlog.Appenders.csproj create mode 100644 src/Sherlog.Appenders/src/SosMaxAppender.cs create mode 100644 src/Sherlog.Appenders/src/TcpSocketAppender.cs create mode 100644 src/Sherlog.Formatters/src/ColorCodeFormatter.cs create mode 100644 src/Sherlog.Formatters/src/LogFormatter.cs create mode 100644 src/Sherlog.Formatters/src/LogMessageFormatter.cs create mode 100644 src/Sherlog.Formatters/src/Sherlog.Formatters.csproj create mode 100644 src/Sherlog.Formatters/src/TimestampFormatter.cs create mode 100644 src/Sherlog.Formatters/tests/LogMessageFormatterTests.cs create mode 100644 src/Sherlog.Formatters/tests/Sherlog.Formatters.Tests.csproj create mode 100644 src/Sherlog/src/LogLevel.cs create mode 100644 src/Sherlog/src/Logger.cs create mode 100644 src/Sherlog/src/LoggerStatic.cs create mode 100644 src/Sherlog/src/Sherlog.csproj create mode 100644 src/Sherlog/tests/LoggerTests.cs create mode 100644 src/Sherlog/tests/Sherlog.Tests.csproj create mode 100644 src/Sherlog/tests/SherlogTests.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7279087 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset=utf-8 +end_of_line=lf +trim_trailing_whitespace=true +insert_final_newline=true +indent_style=space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e73764 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# OS +.DS_Store +Thumbs.db + +# Solution +obj +bin +*.userprefs +*.user +TestResults + +# IDEs +.vs +.vscode +.idea + +# Project +build diff --git a/Sherlog.sln b/Sherlog.sln new file mode 100644 index 0000000..36b82b2 --- /dev/null +++ b/Sherlog.sln @@ -0,0 +1,56 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sherlog", "Sherlog", "{5BAFDDD4-9388-4E43-8CBB-C648243AE78F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sherlog", "src\Sherlog\src\Sherlog.csproj", "{5E53385B-6709-4DE3-BD63-44C7C6506189}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sherlog.Tests", "src\Sherlog\tests\Sherlog.Tests.csproj", "{DC32B1D6-8292-47DC-9900-B33EA52C01C6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sherlog.Appenders", "Sherlog.Appenders", "{AFF34632-3CAD-4034-A277-2CEA95A70612}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sherlog.Appenders", "src\Sherlog.Appenders\src\Sherlog.Appenders.csproj", "{0A087C52-6165-4CC1-9BF3-9866D1D8AC92}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sherlog.Formatters", "Sherlog.Formatters", "{B6A57558-6B16-4C20-B7A3-258DF1FC28E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sherlog.Formatters", "src\Sherlog.Formatters\src\Sherlog.Formatters.csproj", "{A90D7628-BCBC-47CB-B70D-C199FD2387E1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sherlog.Formatters.Tests", "src\Sherlog.Formatters\tests\Sherlog.Formatters.Tests.csproj", "{834C76D3-F2AC-4281-98F1-CCB8EB6B3D2D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5E53385B-6709-4DE3-BD63-44C7C6506189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E53385B-6709-4DE3-BD63-44C7C6506189}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E53385B-6709-4DE3-BD63-44C7C6506189}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E53385B-6709-4DE3-BD63-44C7C6506189}.Release|Any CPU.Build.0 = Release|Any CPU + {DC32B1D6-8292-47DC-9900-B33EA52C01C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DC32B1D6-8292-47DC-9900-B33EA52C01C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DC32B1D6-8292-47DC-9900-B33EA52C01C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC32B1D6-8292-47DC-9900-B33EA52C01C6}.Release|Any CPU.Build.0 = Release|Any CPU + {0A087C52-6165-4CC1-9BF3-9866D1D8AC92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A087C52-6165-4CC1-9BF3-9866D1D8AC92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A087C52-6165-4CC1-9BF3-9866D1D8AC92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A087C52-6165-4CC1-9BF3-9866D1D8AC92}.Release|Any CPU.Build.0 = Release|Any CPU + {A90D7628-BCBC-47CB-B70D-C199FD2387E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A90D7628-BCBC-47CB-B70D-C199FD2387E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A90D7628-BCBC-47CB-B70D-C199FD2387E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A90D7628-BCBC-47CB-B70D-C199FD2387E1}.Release|Any CPU.Build.0 = Release|Any CPU + {834C76D3-F2AC-4281-98F1-CCB8EB6B3D2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {834C76D3-F2AC-4281-98F1-CCB8EB6B3D2D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {834C76D3-F2AC-4281-98F1-CCB8EB6B3D2D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {834C76D3-F2AC-4281-98F1-CCB8EB6B3D2D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5E53385B-6709-4DE3-BD63-44C7C6506189} = {5BAFDDD4-9388-4E43-8CBB-C648243AE78F} + {DC32B1D6-8292-47DC-9900-B33EA52C01C6} = {5BAFDDD4-9388-4E43-8CBB-C648243AE78F} + {0A087C52-6165-4CC1-9BF3-9866D1D8AC92} = {AFF34632-3CAD-4034-A277-2CEA95A70612} + {A90D7628-BCBC-47CB-B70D-C199FD2387E1} = {B6A57558-6B16-4C20-B7A3-258DF1FC28E5} + {834C76D3-F2AC-4281-98F1-CCB8EB6B3D2D} = {B6A57558-6B16-4C20-B7A3-258DF1FC28E5} + EndGlobalSection +EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..15e2838 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,43 @@ + + + + false + net6.0 + netstandard2.1 + net6.0 + true + default + en-US + + + + + + + + $(DefaultItemExcludes);$(MSBuildProjectDirectory)/obj/**/* + $(DefaultItemExcludes);$(MSBuildProjectDirectory)/bin/**/* + + + + $(MSBuildProjectDirectory)/obj/container/ + $(MSBuildProjectDirectory)/bin/container/ + + + + $(MSBuildStartupDirectory)/build/Managed/UnityEditor.dll + $(MSBuildStartupDirectory)/build/Managed/UnityEngine.dll + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 0000000..dd40977 --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,16 @@ + + + + Simon Schmid + MIT + icon.png + https://github.com/sschmid/DesperateDevs + Desperate Devs + $(AssemblyName) + Simon Schmid + DesperateDevs, Desperate, Devs + https://github.com/sschmid/DesperateDevs + git + + + diff --git a/src/Sherlog.Appenders/src/AbstractTcpSocketAppender.cs b/src/Sherlog.Appenders/src/AbstractTcpSocketAppender.cs new file mode 100644 index 0000000..1f50aa9 --- /dev/null +++ b/src/Sherlog.Appenders/src/AbstractTcpSocketAppender.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Net; +using TCPeasy; + +namespace Sherlog.Appenders +{ + public abstract class AbstractTcpSocketAppender + { + readonly List _history = new List(); + + AbstractTcpSocket _socket; + + public void Connect(IPAddress ip, int port) + { + var client = new TcpClientSocket(); + _socket = client; + client.OnConnected += _ => OnConnected(); + client.Connect(ip, port); + } + + public void Listen(int port) + { + var server = new TcpServerSocket(); + _socket = server; + server.OnClientConnected += (_, _) => OnConnected(); + server.Listen(port); + } + + public void Disconnect() => _socket.Disconnect(); + + public void Send(Logger logger, LogLevel logLevel, string message) + { + if (IsSocketReady()) + _socket.Send(SerializeMessage(logger, logLevel, message)); + else + _history.Add(new HistoryEntry(logger, logLevel, message)); + } + + bool IsSocketReady() + { + if (_socket != null) + { + if (_socket is TcpServerSocket server) + return server.Count > 0; + + if (_socket is TcpClientSocket client) + return client.IsConnected; + } + + return false; + } + + void OnConnected() + { + if (_history.Count > 0) + { + foreach (var entry in _history) + Send(entry.Logger, entry.LogLevel, entry.Message); + + _history.Clear(); + } + } + + protected abstract byte[] SerializeMessage(Logger logger, LogLevel logLevel, string message); + + class HistoryEntry + { + public readonly Logger Logger; + public readonly LogLevel LogLevel; + public readonly string Message; + + public HistoryEntry(Logger logger, LogLevel logLevel, string message) + { + Logger = logger; + LogLevel = logLevel; + Message = message; + } + } + } +} diff --git a/src/Sherlog.Appenders/src/ConsoleAppender.cs b/src/Sherlog.Appenders/src/ConsoleAppender.cs new file mode 100644 index 0000000..4f336be --- /dev/null +++ b/src/Sherlog.Appenders/src/ConsoleAppender.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System; + +namespace Sherlog.Appenders +{ + public class ConsoleAppender + { + readonly Dictionary _consoleColors; + + public ConsoleAppender(Dictionary consoleColors) + { + _consoleColors = consoleColors; + } + + public void WriteLine(Logger logger, LogLevel logLevel, string message) + { + if (_consoleColors.TryGetValue(logLevel, out var color)) + { + Console.ForegroundColor = color; + Console.WriteLine(message); + Console.ResetColor(); + } + else + { + Console.WriteLine(message); + } + } + } +} diff --git a/src/Sherlog.Appenders/src/FileWriterAppender.cs b/src/Sherlog.Appenders/src/FileWriterAppender.cs new file mode 100644 index 0000000..8076f0a --- /dev/null +++ b/src/Sherlog.Appenders/src/FileWriterAppender.cs @@ -0,0 +1,30 @@ +using System.IO; + +namespace Sherlog.Appenders +{ + public class FileWriterAppender + { + readonly object _lock = new object(); + readonly string _filePath; + + public FileWriterAppender(string filePath) => _filePath = filePath; + + public void WriteLine(Logger logger, LogLevel logLevel, string message) + { + lock (_lock) + { + using var writer = new StreamWriter(_filePath, true); + writer.WriteLine(message); + } + } + + public void ClearFile() + { + lock (_lock) + { + using var writer = new StreamWriter(_filePath, false); + writer.Write(string.Empty); + } + } + } +} diff --git a/src/Sherlog.Appenders/src/Sherlog.Appenders.csproj b/src/Sherlog.Appenders/src/Sherlog.Appenders.csproj new file mode 100644 index 0000000..7b6e7c1 --- /dev/null +++ b/src/Sherlog.Appenders/src/Sherlog.Appenders.csproj @@ -0,0 +1,16 @@ + + + + $(DefaultTargetFramework) + 1.0.0 + + + + + + + + + + + diff --git a/src/Sherlog.Appenders/src/SosMaxAppender.cs b/src/Sherlog.Appenders/src/SosMaxAppender.cs new file mode 100644 index 0000000..9715fb4 --- /dev/null +++ b/src/Sherlog.Appenders/src/SosMaxAppender.cs @@ -0,0 +1,28 @@ +using System.Text; + +namespace Sherlog.Appenders +{ + public class SosMaxAppender : AbstractTcpSocketAppender + { + protected override byte[] SerializeMessage(Logger logger, LogLevel logLevel, string message) => + Encoding.UTF8.GetBytes(FormatLogMessage(logLevel.ToString(), message)); + + string FormatLogMessage(string logLevel, string message) + { + var lines = message.Split('\n'); + return lines.Length == 1 + ? $"!SOS{ReplaceXmlSymbols(message)}\0" + : $"!SOS{MultilineMessage(lines[0], message)}\0"; + } + + string MultilineMessage(string title, string message) => + $"{ReplaceXmlSymbols(title)}{ReplaceXmlSymbols(message.Substring(message.IndexOf('\n') + 1))}"; + + string ReplaceXmlSymbols(string str) => str + .Replace("<", "<") + .Replace(">", ">") + .Replace("<", "") + .Replace(">", "]]>") + .Replace("&", ""); + } +} diff --git a/src/Sherlog.Appenders/src/TcpSocketAppender.cs b/src/Sherlog.Appenders/src/TcpSocketAppender.cs new file mode 100644 index 0000000..26caac5 --- /dev/null +++ b/src/Sherlog.Appenders/src/TcpSocketAppender.cs @@ -0,0 +1,11 @@ +using System.Text; +using TCPeasy; + +namespace Sherlog.Appenders +{ + public class TcpSocketAppender : AbstractTcpSocketAppender + { + protected override byte[] SerializeMessage(Logger logger, LogLevel logLevel, string message) => + TcpMessageParser.WrapMessage(Encoding.UTF8.GetBytes(message)); + } +} diff --git a/src/Sherlog.Formatters/src/ColorCodeFormatter.cs b/src/Sherlog.Formatters/src/ColorCodeFormatter.cs new file mode 100644 index 0000000..d6cbb4e --- /dev/null +++ b/src/Sherlog.Formatters/src/ColorCodeFormatter.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; + +namespace Sherlog.Formatters +{ + public class ColorCodeFormatter + { + // ANSI COLOR escape codes for colors and other things. + // You can change the color of foreground and background plus bold, italic, underline etc + // For a complete list see http://en.wikipedia.org/wiki/ANSI_escape_code#Colors + + public const string Reset = "0m"; + public const string Esc = "\x1B["; + + public const string NoBackground = ""; + public const string BlackForeground = "30m"; + public const string BlackBackground = "40m"; + public const string RedForeground = "31m"; + public const string RedBackground = "41m"; + public const string GreenForeground = "32m"; + public const string GreenBackground = "42m"; + public const string YellowForeground = "33m"; + public const string YellowBackground = "43m"; + public const string BlueForeground = "34m"; + public const string BlueBackground = "44m"; + public const string MagentaForeground = "35m"; + public const string MagentaBackground = "45m"; + public const string CyanForeground = "36m"; + public const string CyanBackground = "46m"; + public const string WhiteForeground = "37m"; + public const string WhiteBackground = "47m"; + + public class Color + { + public string Foreground; + public string Background; + } + + public readonly Dictionary Colors = new Dictionary + { + {LogLevel.Trace, new Color {Foreground = WhiteForeground, Background = CyanBackground}}, + {LogLevel.Debug, new Color {Foreground = BlueForeground, Background = NoBackground}}, + {LogLevel.Info, new Color {Foreground = GreenForeground, Background = NoBackground}}, + {LogLevel.Warn, new Color {Foreground = YellowForeground, Background = NoBackground}}, + {LogLevel.Error, new Color {Foreground = WhiteForeground, Background = RedBackground}}, + {LogLevel.Fatal, new Color {Foreground = WhiteForeground, Background = MagentaBackground}} + }; + + public string FormatMessage(Logger logger, LogLevel logLevel, string message) => + $"{Esc}{Colors[logLevel].Background}{Esc}{Colors[logLevel].Foreground}{message}{Esc}{Reset}"; + } +} diff --git a/src/Sherlog.Formatters/src/LogFormatter.cs b/src/Sherlog.Formatters/src/LogFormatter.cs new file mode 100644 index 0000000..80db315 --- /dev/null +++ b/src/Sherlog.Formatters/src/LogFormatter.cs @@ -0,0 +1,4 @@ +namespace Sherlog.Formatters +{ + public delegate string LogFormatter(Logger logger, LogLevel logLevel, string message); +} diff --git a/src/Sherlog.Formatters/src/LogMessageFormatter.cs b/src/Sherlog.Formatters/src/LogMessageFormatter.cs new file mode 100644 index 0000000..a7d7f35 --- /dev/null +++ b/src/Sherlog.Formatters/src/LogMessageFormatter.cs @@ -0,0 +1,12 @@ +namespace Sherlog.Formatters +{ + public class LogMessageFormatter + { + readonly string _format; + + public LogMessageFormatter(string format = "[{1}] {0}: {2}") => _format = format; + + public string FormatMessage(Logger logger, LogLevel logLevel, string message) => + string.Format(_format, logger.Name, logLevel, message); + } +} diff --git a/src/Sherlog.Formatters/src/Sherlog.Formatters.csproj b/src/Sherlog.Formatters/src/Sherlog.Formatters.csproj new file mode 100644 index 0000000..93f900d --- /dev/null +++ b/src/Sherlog.Formatters/src/Sherlog.Formatters.csproj @@ -0,0 +1,12 @@ + + + + $(DefaultTargetFramework) + 1.0.0 + + + + + + + diff --git a/src/Sherlog.Formatters/src/TimestampFormatter.cs b/src/Sherlog.Formatters/src/TimestampFormatter.cs new file mode 100644 index 0000000..2fd4e87 --- /dev/null +++ b/src/Sherlog.Formatters/src/TimestampFormatter.cs @@ -0,0 +1,14 @@ +using System; + +namespace Sherlog.Formatters +{ + public class TimestampFormatter + { + readonly string _timeFormat; + + public TimestampFormatter(string timeFormat = "{0:yyyy/MM/dd/hh:mm:ss:fff}") => _timeFormat = timeFormat; + + public string FormatMessage(Logger logger, LogLevel logLevel, string message) => + $"{string.Format(_timeFormat, DateTime.Now)} {message}"; + } +} diff --git a/src/Sherlog.Formatters/tests/LogMessageFormatterTests.cs b/src/Sherlog.Formatters/tests/LogMessageFormatterTests.cs new file mode 100644 index 0000000..aac2d7a --- /dev/null +++ b/src/Sherlog.Formatters/tests/LogMessageFormatterTests.cs @@ -0,0 +1,24 @@ +using FluentAssertions; +using Xunit; + +namespace Sherlog.Formatters.Tests +{ + public class LogMessageFormatterTests + { + readonly LogMessageFormatter _formatter; + readonly Logger _logger; + + public LogMessageFormatterTests() + { + _formatter = new LogMessageFormatter("[{1}] {0}: {2}"); + _logger = new Logger("TestLogger"); + } + + [Fact] + public void FormatsString() + { + _formatter.FormatMessage(_logger, LogLevel.Debug, "test message") + .Should().Be("[Debug] TestLogger: test message"); + } + } +} diff --git a/src/Sherlog.Formatters/tests/Sherlog.Formatters.Tests.csproj b/src/Sherlog.Formatters/tests/Sherlog.Formatters.Tests.csproj new file mode 100644 index 0000000..6d4d75c --- /dev/null +++ b/src/Sherlog.Formatters/tests/Sherlog.Formatters.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(DefaultTestTargetFramework) + false + false + + + + + + + + + + + + + + + + + diff --git a/src/Sherlog/src/LogLevel.cs b/src/Sherlog/src/LogLevel.cs new file mode 100644 index 0000000..5dd9e5a --- /dev/null +++ b/src/Sherlog/src/LogLevel.cs @@ -0,0 +1,14 @@ +namespace Sherlog +{ + public enum LogLevel + { + On, + Trace, + Debug, + Info, + Warn, + Error, + Fatal, + Off + } +} diff --git a/src/Sherlog/src/Logger.cs b/src/Sherlog/src/Logger.cs new file mode 100644 index 0000000..8ed6f9d --- /dev/null +++ b/src/Sherlog/src/Logger.cs @@ -0,0 +1,46 @@ +using System; + +namespace Sherlog +{ + public delegate void LogDelegate(Logger logger, LogLevel logLevel, string message); + + public partial class Logger + { + public event LogDelegate OnLog; + + public readonly string Name; + + public LogLevel LogLevel; + + public Logger(string name) => Name = name; + + public void Trace(string message) => Log(LogLevel.Trace, message); + public void Debug(string message) => Log(LogLevel.Debug, message); + public void Info (string message) => Log(LogLevel.Info, message); + public void Warn (string message) => Log(LogLevel.Warn, message); + public void Error(string message) => Log(LogLevel.Error, message); + public void Fatal(string message) => Log(LogLevel.Fatal, message); + + public void Assert(bool condition, string message) + { + if (!condition) + throw new SherlogAssertException(message); + } + +#if SHERLOG_OFF + [System.Diagnostics.Conditional("false")] +#endif + void Log(LogLevel logLvl, string message) + { + if (logLvl >= LogLevel) + OnLog?.Invoke(this, logLvl, message); + } + + public void Reset() => OnLog = null; + } + + public class SherlogAssertException : Exception + { + public SherlogAssertException(string message) : base(message) { } + } +} diff --git a/src/Sherlog/src/LoggerStatic.cs b/src/Sherlog/src/LoggerStatic.cs new file mode 100644 index 0000000..745cdd4 --- /dev/null +++ b/src/Sherlog/src/LoggerStatic.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; + +namespace Sherlog +{ + public partial class Logger + { + public static LogLevel GlobalLogLevel + { + get => _globalLogLevel; + set + { + _globalLogLevel = value; + foreach (var logger in Loggers.Values) + logger.LogLevel = value; + } + } + + static readonly Dictionary Loggers = new Dictionary(); + + static LogLevel _globalLogLevel; + static LogDelegate _appenders; + + public static void AddAppender(LogDelegate appender) + { + _appenders += appender; + foreach (var logger in Loggers.Values) + logger.OnLog += appender; + } + + public static void RemoveAppender(LogDelegate appender) + { + _appenders -= appender; + foreach (var logger in Loggers.Values) + logger.OnLog -= appender; + } + + public static Logger GetLogger(Type type) => GetLogger(type.FullName); + + public static Logger GetLogger(string name) + { + if (!Loggers.TryGetValue(name, out var logger)) + { + logger = new Logger(name) + { + LogLevel = GlobalLogLevel + }; + logger.OnLog += _appenders; + Loggers.Add(name, logger); + } + + return logger; + } + + public static void ClearLoggers() => Loggers.Clear(); + + public static void ClearAppenders() + { + _appenders = null; + foreach (var logger in Loggers.Values) + logger.OnLog = null; + } + } +} diff --git a/src/Sherlog/src/Sherlog.csproj b/src/Sherlog/src/Sherlog.csproj new file mode 100644 index 0000000..e0e0bf2 --- /dev/null +++ b/src/Sherlog/src/Sherlog.csproj @@ -0,0 +1,8 @@ + + + + $(DefaultTargetFramework) + 1.0.0 + + + diff --git a/src/Sherlog/tests/LoggerTests.cs b/src/Sherlog/tests/LoggerTests.cs new file mode 100644 index 0000000..7a29fa8 --- /dev/null +++ b/src/Sherlog/tests/LoggerTests.cs @@ -0,0 +1,93 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Sherlog.Tests +{ + public class LoggerTests + { + const string Message = "test message"; + + readonly Logger _logger; + + public LoggerTests() + { + _logger = new Logger("TestLogger"); + } + + [Theory] + [InlineData(LogLevel.On, true, true, true, true, true, true)] + [InlineData(LogLevel.Trace, true, true, true, true, true, true)] + [InlineData(LogLevel.Debug, false, true, true, true, true, true)] + [InlineData(LogLevel.Info, false, false, true, true, true, true)] + [InlineData(LogLevel.Warn, false, false, false, true, true, true)] + [InlineData(LogLevel.Error, false, false, false, false, true, true)] + [InlineData(LogLevel.Fatal, false, false, false, false, false, true)] + [InlineData(LogLevel.Off, false, false, false, false, false, false)] + public void LogLevels(LogLevel logLevel, bool trace, bool debug, bool info, bool warn, bool error, bool fatal) + { + _logger.LogLevel = logLevel; + AssertLogLevel(_logger.Trace, LogLevel.Trace, trace); + AssertLogLevel(_logger.Debug, LogLevel.Debug, debug); + AssertLogLevel(_logger.Info, LogLevel.Info, info); + AssertLogLevel(_logger.Warn, LogLevel.Warn, warn); + AssertLogLevel(_logger.Error, LogLevel.Error, error); + AssertLogLevel(_logger.Fatal, LogLevel.Fatal, fatal); + + void AssertLogLevel(Action logMethod, LogLevel logLvl, bool shouldLog) + { + var didLog = false; + var eventLogLevel = LogLevel.Off; + string eventMessage = null; + Logger eventLogger = null; + _logger.OnLog += (logger, level, msg) => + { + didLog = true; + eventLogger = logger; + eventLogLevel = level; + eventMessage = msg; + }; + + logMethod(Message); + + didLog.Should().Be(shouldLog); + + if (shouldLog) + { + eventLogger.Should().BeSameAs(_logger); + eventMessage.Should().Be(Message); + eventLogLevel.Should().Be(logLvl); + } + else + { + eventMessage.Should().BeNull(); + eventLogLevel.Should().Be(LogLevel.Off); + eventLogger.Should().BeNull(); + } + } + } + + [Fact] + public void AssertDoesNotThrowWhenConditionIsTrue() + { + _logger.Assert(true, "success"); + } + + [Fact] + public void AssertThrowsWhenConditionIsFalse() + { + FluentActions.Invoking(() => _logger.Assert(false, "fail")) + .Should().Throw(); + } + + [Fact] + public void ResetsOnLog() + { + var didLog = 0; + _logger.OnLog += (logger, level, s) => didLog += 1; + _logger.Reset(); + _logger.Info("test message"); + didLog.Should().Be(0); + } + } +} diff --git a/src/Sherlog/tests/Sherlog.Tests.csproj b/src/Sherlog/tests/Sherlog.Tests.csproj new file mode 100644 index 0000000..c4a17cf --- /dev/null +++ b/src/Sherlog/tests/Sherlog.Tests.csproj @@ -0,0 +1,22 @@ + + + + $(DefaultTestTargetFramework) + false + false + + + + + + + + + + + + + + + + diff --git a/src/Sherlog/tests/SherlogTests.cs b/src/Sherlog/tests/SherlogTests.cs new file mode 100644 index 0000000..b3d72e9 --- /dev/null +++ b/src/Sherlog/tests/SherlogTests.cs @@ -0,0 +1,145 @@ +using System; +using FluentAssertions; +using Xunit; + +namespace Sherlog.Tests +{ + public class SherlogTests : IDisposable + { + [Fact] + public void CreatesNewLogger() + { + var logger = Logger.GetLogger("TestLogger"); + logger.Should().NotBeNull(); + logger.GetType().Should().BeSameAs(typeof(Logger)); + logger.Name.Should().Be("TestLogger"); + logger.LogLevel.Should().Be(LogLevel.On); + } + + [Fact] + public void CreatesNewLoggerForType() + { + var logger = Logger.GetLogger(typeof(SherlogTests)); + logger.Should().NotBeNull(); + logger.GetType().Should().BeSameAs(typeof(Logger)); + logger.Name.Should().Be("Sherlog.Tests.SherlogTests"); + logger.LogLevel.Should().Be(LogLevel.On); + } + + [Fact] + public void ReturnsSameLoggerWhenNameIsEqual() + { + var logger1 = Logger.GetLogger("TestLogger"); + var logger2 = Logger.GetLogger("TestLogger"); + logger1.Should().BeSameAs(logger2); + } + + [Fact] + public void ReturnsSameLoggerWhenTypeIsEqual() + { + var logger1 = Logger.GetLogger(typeof(SherlogTests)); + var logger2 = Logger.GetLogger(typeof(SherlogTests)); + logger1.Should().BeSameAs(logger2); + } + + [Fact] + public void ClearsCreatedLoggers() + { + var logger1 = Logger.GetLogger("TestLogger"); + Logger.ClearLoggers(); + var logger2 = Logger.GetLogger("TestLogger"); + logger1.Should().NotBeSameAs(logger2); + } + + [Fact] + public void CreatesNewLoggerWithGlobalLogLevel() + { + Logger.GlobalLogLevel = LogLevel.Error; + var logger = Logger.GetLogger("TestLogger"); + logger.LogLevel.Should().Be(LogLevel.Error); + } + + [Fact] + public void SetsGlobalLogLevelOnCreatedLogger() + { + var logger = Logger.GetLogger("TestLogger"); + logger.LogLevel.Should().Be(LogLevel.On); + Logger.GlobalLogLevel = LogLevel.Error; + logger.LogLevel.Should().Be(LogLevel.Error); + } + + [Fact] + public void CreatesNewLoggerWithGlobalAppender() + { + var appenderLogLevel = LogLevel.Off; + var appenderMessage = string.Empty; + Logger.AddAppender((log, logLevel, message) => + { + appenderLogLevel = logLevel; + appenderMessage = message; + }); + + var appenderLogLevel2 = LogLevel.Off; + var appenderMessage2 = string.Empty; + Logger.AddAppender((log, logLevel, message) => + { + appenderLogLevel2 = logLevel; + appenderMessage2 = message; + }); + + var logger = Logger.GetLogger("TestLogger"); + logger.Info("test message"); + + appenderLogLevel.Should().Be(LogLevel.Info); + appenderMessage.Should().Be("test message"); + appenderLogLevel2.Should().Be(LogLevel.Info); + appenderMessage2.Should().Be("test message"); + } + + [Fact] + public void AddsAppenderOnCreatedLogger() + { + var logger = Logger.GetLogger("TestLogger"); + var didLog = false; + Logger.AddAppender((log, logLevel, message) => didLog = true); + logger.Info("test message"); + didLog.Should().BeTrue(); + } + + [Fact] + public void RemovesAppenderOnCreatedLogger() + { + var didLog = false; + LogDelegate appender = (log, logLevel, message) => didLog = true; + Logger.AddAppender(appender); + var logger = Logger.GetLogger("TestLogger"); + Logger.RemoveAppender(appender); + logger.Info("test message"); + didLog.Should().BeFalse(); + } + + [Fact] + public void ClearsGlobalAppendersOnCreatedLogger() + { + var appenderLogLevel = LogLevel.Off; + var appenderMessage = string.Empty; + Logger.AddAppender((log, logLevel, message) => + { + appenderLogLevel = logLevel; + appenderMessage = message; + }); + var logger = Logger.GetLogger("TestLogger"); + Logger.ClearAppenders(); + logger.Info("test message"); + appenderLogLevel.Should().Be(LogLevel.Off); + appenderMessage.Should().Be(string.Empty); + } + + public void Dispose() + { + Logger.GlobalLogLevel = LogLevel.On; + Logger.ClearAppenders(); + Logger.ClearLoggers(); + } + } +}