Compare commits

...

7 Commits

17 changed files with 213 additions and 192 deletions

View File

@ -9,7 +9,7 @@ var serilogLogger = SetupSerilog();
LogService.AddLogAdapter(new SerilogAdapter(serilogLogger));
var logger = LogService.RegisterLogger(nameof(Program));
var logger = LogService.RegisterLogger(typeof(Program));
logger.Trace("Test TRACE log! {StringValue} {AnotherValue}", "WOW", "W0W");
logger.Debug("Test DEBUG log! {IntVal}", 69);

View File

@ -15,19 +15,19 @@ public class SerilogAdapter : LogAdapterBase
_messageTemplateParser = new MessageTemplateParser();
}
public const string SampleTemplate = "[{Timestamp:HH:mm:ss} - {Caller} -> {AsmCaller}] | {Level:u3}] {Message:lj}{NewLine}{Exception}";
public const string SampleTemplate = "[{Timestamp:HH:mm:ss} - {Caller}] | {Level:u3}] {Message:lj}{NewLine}{Exception}";
private readonly global::Serilog.ILogger _serilogLogger;
private readonly MessageTemplateParser _messageTemplateParser;
public override void HandleLog(object? sender, LogCapsule? capsule)
public override void HandleLog(object? processor, LogCapsule? capsule)
{
if (capsule == null)
return;
var baseLogger = capsule.Logger as Logger;
var logger = _serilogLogger
.ForContext("AsmCaller", baseLogger?.CallingAsmInfo.AssemblyName ?? "Static")
.ForContext("Caller", baseLogger?.Identifier);
.ForContext("Assembly", capsule.Logger.Caller.AssemblyName)
.ForContext("Source", capsule.Logger.Caller.Source)
.ForContext("Caller", capsule.Logger.Caller.Name);
var template = _messageTemplateParser.Parse(capsule.Message);
IEnumerable<LogEventProperty>? properties = null;
@ -39,6 +39,7 @@ public class SerilogAdapter : LogAdapterBase
switch (capsule.Severity)
{
case LogSeverity.Trace:
default:
logger.Write(new LogEvent(capsule.TimeStamp, LogEventLevel.Verbose, null, template, properties ?? ArraySegment<LogEventProperty>.Empty, ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom()));
break;
case LogSeverity.Debug:

View File

@ -1,9 +0,0 @@
namespace DotBased;
public class Based
{
void Test()
{
}
}

View File

@ -1,5 +1,8 @@
namespace DotBased.Extensions;
/// <summary>
/// Some simple extensions used for the string class
/// </summary>
public static class StringExtensions
{
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s);

View File

@ -1,5 +1,8 @@
namespace DotBased.Logging;
/// <summary>
/// ILogger interface used by <see cref="LoggerBase"/>
/// </summary>
public interface ILogger
{
public void Trace(string message, params object?[]? parameters);

View File

@ -1,5 +1,8 @@
namespace DotBased.Logging;
/// <summary>
/// The base for creating log adpaters.
/// </summary>
public abstract class LogAdapterBase
{
public LogAdapterBase(string adapterName)
@ -10,14 +13,17 @@ public abstract class LogAdapterBase
internal readonly EventHandler<LogCapsule> HandleLogEvent;
public string Id { get; } = Guid.NewGuid().ToString();
/// <summary>
/// The name this adapter has.
/// </summary>
public string AdapterName { get; }
/*private string[] GetMessageProperties(string message)
{
return [];
}*/
public abstract void HandleLog(object? sender, LogCapsule? capsule);
/// <summary>
/// Handle the incomming <see cref="LogCapsule"/> that the <see cref="LogProcessor"/> sends.
/// </summary>
/// <param name="processor">The log processor that has processed this log</param>
/// <param name="capsule">The log capsule, which contains the log information</param>
public abstract void HandleLog(object? processor, LogCapsule? capsule);
public override int GetHashCode() => HashCode.Combine(Id, AdapterName);

View File

@ -1,7 +1,13 @@
namespace DotBased.Logging;
/// <summary>
/// This will contain all the log event information that the log adapter will receive.
/// </summary>
public class LogCapsule
{
/// <summary>
/// The log serverty this log event is being logged.
/// </summary>
public LogSeverity Severity { get; set; }
public string Message { get; set; } = string.Empty;
public Exception? Exception { get; set; }
@ -13,5 +19,5 @@ public class LogCapsule
/// <summary>
/// The logger that generated this capsule
/// </summary>
public ILogger Logger { get; set; }
public LoggerBase Logger { get; set; }
}

View File

@ -1,9 +1,18 @@
namespace DotBased.Logging;
/// <summary>
/// Options for loggers, processor and <see cref="LogService"/>.
/// </summary>
public class LogOptions
{
/// <summary>
/// The severty the logger will log
/// </summary>
public LogSeverity Severity { get; set; } = LogSeverity.Trace;
/// <summary>
/// The function that will build and return the <see cref="LoggerBase"/> when calling <see cref="LogService.RegisterLogger"/>, so a custom logger based on the <see cref="LoggerBase"/> can be used.
/// </summary>
public Func<CallerInformation, Action<LogCapsule>, LoggerBase> LoggerBuilder { get; set; } =
(identifier, sendEvent) => new Logger(identifier, ref sendEvent);
}

View File

@ -1,5 +1,8 @@
namespace DotBased.Logging;
/// <summary>
/// Log processor, this class runs a task that send the logs (<see cref="LogCapsule"/>) to all adapters that are registered in the <see cref="LogService"/> class.
/// </summary>
public class LogProcessor : IDisposable
{
public LogProcessor()
@ -21,8 +24,11 @@ public class LogProcessor : IDisposable
private readonly ManualResetEvent _threadShutdownEvent = new ManualResetEvent(false);
/// <summary>
/// Stop the LogProcessor, the processor cannot be resumed after stopped!
/// Stop the LogProcessor
/// </summary>
/// <remarks>
/// The processor cannot be resumed after it is stopped!
/// </remarks>
public void Stop()
{
_threadShutdownEvent.Set();
@ -54,7 +60,7 @@ public class LogProcessor : IDisposable
if (_threadShutdownEvent.WaitOne(0))
break;
if (_processorQueue.Any())
if (_processorQueue.Count != 0)
{
var capsule = _processorQueue.Dequeue();
if (LogService.ShouldLog(LogService.Options.Severity, capsule.Severity))
@ -79,7 +85,7 @@ public class LogProcessor : IDisposable
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("==================================================================================");
Console.ForegroundColor = oldColor;
//TODO: Write info to disk.
//TODO: Write to disk.
}
}
}

View File

@ -1,51 +1,104 @@
using System.Collections.ObjectModel;
using System.Reflection;
namespace DotBased.Logging;
/// <summary>
/// Main log service class, handles the loggers, log processor and adapters.
/// </summary>
public static class LogService
{
// TODO: Future: add middlewares and chanagable log processor
static LogService()
{
Options = new LogOptions();
_loggerSendEvent = LogProcessor.IncommingLogHandlerEvent;
LoggerSendEvent = LogProcessor.IncommingLogHandlerEvent;
}
public static bool ShouldLog(LogSeverity maxSeverity, LogSeverity severity) => maxSeverity <= severity;
public static LogOptions Options { get; private set; }
public static LogProcessor LogProcessor { get; private set; } = new LogProcessor();
private static HashSet<LogAdapterBase> Adapters { get; } = new HashSet<LogAdapterBase>();
private static HashSet<ILogger> Loggers { get; } = new HashSet<ILogger>();
private static HashSet<LoggerBase> Loggers { get; } = new HashSet<LoggerBase>();
/// <summary>
/// Internal communication between loggers and processor
/// Action for internal communication between loggers and processor
/// </summary>
private static Action<LogCapsule> _loggerSendEvent;
private static readonly Action<LogCapsule> LoggerSendEvent;
/// <summary>
/// Register a logger that will be used in a class and will live as long as the class.
/// This will get the calling assembly and will pass that through ther log adapters.
/// </summary>
/// <example>
/// <code>
/// public class Program
/// {
/// public Program
/// {
/// logger = LogService.RegisterLogger(nameof(Program));
/// }
/// private ILogger logger;
/// }
/// </code>
/// </example>
/// <param name="callerType">The type that called the function</param>
/// <returns>The configured <see cref="ILogger"/> implementation that will be configuered in the <see cref="LogOptions.LoggerBuilder"/> at the <see cref="LogService"/> class</returns>
public static ILogger RegisterLogger(Type callerType)
{
var logger = Options.LoggerBuilder.Invoke(new CallerInformation(callerType), LoggerSendEvent);
Loggers.Add(logger);
return logger;
}
public static bool UnregisterLogger(LoggerBase logger) => Loggers.Remove(logger);
public static ReadOnlyCollection<LoggerBase> GetLoggers => new ReadOnlyCollection<LoggerBase>(Loggers.ToList());
/// <summary>
/// Add a log adapter to the service.
/// </summary>
/// <param name="logAdapter">The log adapter based on <see cref="LogAdapterBase"/></param>
public static void AddLogAdapter(LogAdapterBase logAdapter)
{
LogProcessor.LogProcessed += logAdapter.HandleLogEvent;
Adapters.Add(logAdapter);
}
public static ILogger RegisterLogger(string identifier)
/// <summary>
/// Removes the log adapter from the service.
/// </summary>
/// <param name="adapter">The adapter to remove</param>
/// <returns>True if the adapter is succesfully removed otherwise false.</returns>
public static bool RemoveLogAdapter(LogAdapterBase adapter)
{
var asm = Assembly.GetCallingAssembly();
var logger = new Logger(identifier, CallingAssemblyInfo.LoadFromAsm(asm), ref _loggerSendEvent);
Loggers.Add(logger);
return logger;
if (!Adapters.Contains(adapter)) return false;
LogProcessor.LogProcessed -= adapter.HandleLogEvent;
return Adapters.Remove(adapter);
}
public static ReadOnlyCollection<LogAdapterBase> GetAdapters =>
new ReadOnlyCollection<LogAdapterBase>(Adapters.ToList());
}
public struct CallingAssemblyInfo
public readonly struct CallerInformation
{
private CallingAssemblyInfo(Assembly asm)
public CallerInformation(Type type)
{
var asmName = asm.GetName();
AssemblyName = asmName.Name ?? "Unknown";
AssemblyFullName = asmName.FullName;
}
public static CallingAssemblyInfo LoadFromAsm(Assembly asm) => new CallingAssemblyInfo(asm);
Name = type.Name;
Source = type.FullName ?? type.GUID.ToString();
Namespace = type.Namespace ?? string.Empty;
SourceAssembly = type.Assembly;
var asmName = SourceAssembly.GetName();
AssemblyName = asmName.Name ?? "Unknown";
AssemblyFullname = asmName.FullName;
}
public string Name { get; }
public string Source { get; }
public string Namespace { get; }
public Assembly SourceAssembly { get; }
public string AssemblyName { get; }
public string AssemblyFullName { get; set; }
public string AssemblyFullname { get; }
}

View File

@ -1,25 +1,16 @@
namespace DotBased.Logging;
public class Logger : ILogger
/// <summary>
/// Main logger, this class is the default logger that the <see cref="LogService.RegisterLogger"/> function will return.
/// </summary>
public class Logger(CallerInformation caller, ref Action<LogCapsule> logProcessorHandler) : LoggerBase(caller, ref logProcessorHandler)
{
public Logger(string identifier, CallingAssemblyInfo asmInfo, ref Action<LogCapsule> logProcessorHandler)
{
Identifier = identifier;
CallingAsmInfo = asmInfo;
_processLog = logProcessorHandler;
}
public string Identifier { get; }
public CallingAssemblyInfo CallingAsmInfo { get; }
private readonly Action<LogCapsule> _processLog;
public void Log(LogCapsule capsule)
{
_processLog(capsule);
ProcessLog(capsule);
}
public void Trace(string message, params object?[]? parameters)
public override void Trace(string message, params object?[]? parameters)
{
Log(new LogCapsule()
{
@ -31,7 +22,7 @@ public class Logger : ILogger
});
}
public void Debug(string message, params object?[]? parameters)
public override void Debug(string message, params object?[]? parameters)
{
Log(new LogCapsule()
{
@ -43,7 +34,7 @@ public class Logger : ILogger
});
}
public void Information(string message, params object?[]? parameters)
public override void Information(string message, params object?[]? parameters)
{
Log(new LogCapsule()
{
@ -55,7 +46,7 @@ public class Logger : ILogger
});
}
public void Warning(string message, params object?[]? parameters)
public override void Warning(string message, params object?[]? parameters)
{
Log(new LogCapsule()
{
@ -67,7 +58,7 @@ public class Logger : ILogger
});
}
public void Error(Exception exception, string message, params object?[]? parameters)
public override void Error(Exception exception, string message, params object?[]? parameters)
{
Log(new LogCapsule()
{
@ -80,7 +71,7 @@ public class Logger : ILogger
});
}
public void Fatal(Exception exception, string message, params object?[]? parameters)
public override void Fatal(Exception exception, string message, params object?[]? parameters)
{
Log(new LogCapsule()
{
@ -93,5 +84,5 @@ public class Logger : ILogger
});
}
public override int GetHashCode() => HashCode.Combine(Identifier, CallingAsmInfo.AssemblyFullName);
public override int GetHashCode() => HashCode.Combine(Caller.Source, Caller.AssemblyFullname);
}

View File

@ -0,0 +1,20 @@
namespace DotBased.Logging;
/// <summary>
/// Base for creating loggers
/// </summary>
/// <param name="caller">The caller information</param>
/// <param name="logProcessorHandler">The handler where the logs can be send to</param>
public abstract class LoggerBase(CallerInformation caller, ref Action<LogCapsule> logProcessorHandler) : ILogger
{
public CallerInformation Caller { get; } = caller;
internal readonly Action<LogCapsule> ProcessLog = logProcessorHandler;
public abstract void Trace(string message, params object?[]? parameters);
public abstract void Debug(string message, params object?[]? parameters);
public abstract void Information(string message, params object?[]? parameters);
public abstract void Warning(string message, params object?[]? parameters);
public abstract void Error(Exception exception, string message, params object?[]? parameters);
public abstract void Fatal(Exception exception, string message, params object?[]? parameters);
}

View File

@ -1,91 +0,0 @@
using System.Text;
namespace DotBased.Logging;
public class SimpleConsoleLogAdapter : LogAdapterBase
{
public SimpleConsoleLogAdapter(string adapterName) : base(adapterName)
{
Console.OutputEncoding = Encoding.UTF8;
}
private readonly ConsoleColor _defaultColor = Console.ForegroundColor;
private const ConsoleColor TimestampColor = ConsoleColor.DarkBlue;
private const ConsoleColor BrackedColor = ConsoleColor.Gray;
private const ConsoleColor MessageColor = ConsoleColor.DarkGray;
private ConsoleColor _severityColor = ConsoleColor.Cyan;
public override void HandleLog(object? sender, LogCapsule? capsule)
{
if (capsule == null) return;
Console.ForegroundColor = BrackedColor;
Console.Write("[");
Console.ForegroundColor = TimestampColor;
Console.Write($"{capsule.TimeStamp}");
Console.ForegroundColor = BrackedColor;
Console.Write("] ");
_severityColor = capsule.Severity.ToConsoleColor();
WriteSeverity(capsule.Severity);
if (capsule.Severity is LogSeverity.Error or LogSeverity.Fatal)
WriteException(capsule);
else
WriteMessage(capsule);
Console.ForegroundColor = _defaultColor;
}
private void WriteSeverity(LogSeverity severity)
{
Console.ForegroundColor = BrackedColor;
Console.Write("[");
Console.ForegroundColor = _severityColor;
Console.Write($"{severity}");
Console.ForegroundColor = BrackedColor;
Console.Write("] ");
}
private void WriteMessage(LogCapsule capsule)
{
if (capsule.Parameters == null || !capsule.Parameters.Any())
{
Console.ForegroundColor = MessageColor;
Console.WriteLine($"{capsule.Message}");
}
else
{
}
}
private void WriteException(LogCapsule capsule)
{
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine("\u23F7");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("==============================================================================================");
WriteMessage(capsule);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(capsule.Exception);
Console.WriteLine("==============================================================================================");
Console.ForegroundColor = _defaultColor;
}
}
public static class LogSeverityExt
{
public static ConsoleColor ToConsoleColor(this LogSeverity severity)
{
var color = severity switch
{
LogSeverity.Trace or LogSeverity.Info => ConsoleColor.White,
LogSeverity.Debug => ConsoleColor.Magenta,
LogSeverity.Warning => ConsoleColor.Yellow,
LogSeverity.Error => ConsoleColor.Red,
LogSeverity.Fatal => ConsoleColor.DarkRed,
_ => ConsoleColor.White
};
return color;
}
}

44
DotBased/Result.cs Normal file
View File

@ -0,0 +1,44 @@
namespace DotBased;
/// <summary>
/// Simple result class for returning a result state or a message and a exception.
/// </summary>
public class Result(bool success, string message, Exception? exception)
{
public bool Success { get; set; } = success;
public string Message { get; set; } = message;
public Exception? Exception { get; set; } = exception;
public static Result Ok() => new Result(true, string.Empty, null);
public static Result Failed(string message, Exception? exception = null) => new Result(false, message, exception);
}
public class Result<TValue>(bool success, string message, TValue? value, Exception? exception) : Result(success, message, exception)
{
public TValue? Value { get; set; } = value;
public static Result<TValue> Ok(TValue value) => new Result<TValue>(true, string.Empty, value, null);
public new static Result<TValue> Failed(string message, Exception? exception = null) =>
new Result<TValue>(false, message, default, exception);
}
public class ListResult<TItem>(bool success, string message, int totalCount, IEnumerable<TItem>? items, Exception? exception) : Result(success, message, exception)
{
public readonly IReadOnlyList<TItem> Items = items != null ? new List<TItem>(items) : new List<TItem>();
/// <summary>
/// The amount of items that this result contains.
/// </summary>
public int Count => Items.Count;
/// <summary>
/// The total amount of item that is available.
/// </summary>
public int TotalCount { get; } = totalCount;
public static ListResult<TItem> Ok(IEnumerable<TItem> items, int totalCount = -1) =>
new ListResult<TItem>(true, string.Empty, totalCount, items, null);
public new static ListResult<TItem> Failed(string message, Exception? exception = null) =>
new ListResult<TItem>(false, message, -1,null, exception);
}

View File

@ -1,5 +1,8 @@
namespace DotBased.Utilities;
/// <summary>
/// This class has some generator functions.
/// </summary>
public static class Generator
{
private static readonly Random Random = new Random();

View File

@ -1,10 +1,19 @@
namespace DotBased.Utilities;
/// <summary>
/// Suffix functions for multiple types of values
/// </summary>
public static class Suffix
{
private static readonly string[] SizeSuffixes =
["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
/// <summary>
/// Converts the bytes to the memory suffix.
/// </summary>
/// <param name="bytes">The bytes to convert</param>
/// <param name="decimalPlaces">How manay decimal places will be placed</param>
/// <returns>The suffixed bytes in the correct format</returns>
public static string BytesToSizeSuffix(long bytes, int decimalPlaces = 1)
{
if (decimalPlaces < 0)

View File

@ -1,34 +1 @@
# DotBased info
## Logging
In most logging frameworks you will encounter all or some of the following log levels:
- TRACE
- DEBUG
- INFO
- WARN
- ERROR
- FATAL
The names of some of those give you a hint on what they are about. However, lets discuss each of them in greater detail.
TRACE the most fine-grained information only used in rare cases where you need the full visibility of what is happening in your application and inside the third-party libraries that you use. You can expect the TRACE logging level to be very verbose. You can use it for example to annotate each step in the algorithm or each individual query with parameters in your code.
DEBUG less granular compared to the TRACE level, but it is more than you will need in everyday use. The DEBUG log level should be used for information that may be needed for diagnosing issues and troubleshooting or when running application in the test environment for the purpose of making sure everything is running correctly
INFO the standard log level indicating that something happened, the application entered a certain state, etc. For example, a controller of your authorization API may include an INFO log level with information on which user requested authorization if the authorization was successful or not. The information logged using the INFO log level should be purely informative and not looking into them on a regular basis shouldnt result in missing any important information.
WARN the log level that indicates that something unexpected happened in the application, a problem, or a situation that might disturb one of the processes. But that doesnt mean that the application failed. The WARN level should be used in situations that are unexpected, but the code can continue the work. For example, a parsing error occurred that resulted in a certain document not being processed.
ERROR the log level that should be used when the application hits an issue preventing one or more functionalities from properly functioning. The ERROR log level can be used when one of the payment systems is not available, but there is still the option to check out the basket in the e-commerce application or when your social media logging option is not working for some reason.
FATAL the log level that tells that the application encountered an event or entered a state in which one of the crucial business functionality is no longer working. A FATAL log level may be used when the application is not able to connect to a crucial data store like a database or all the payment systems are not available and users cant checkout their baskets in your e-commerce.
To summarize what we know about each of the logging level:
Log Level Importance
- Fatal One or more key business functionalities are not working and the whole system doesnt fulfill the business functionalities.
- Error One or more functionalities are not working, preventing some functionalities from working correctly.
- Warn Unexpected behavior happened inside the application, but it is continuing its work and the key business features are operating as expected.
- Info An event happened, the event is purely informative and can be ignored during normal operations.
- Debug A log level used for events considered to be useful during software debugging when more granular information is needed.
- Trace A log level describing events showing step by step execution of your code that can be ignored during the standard operation, but may be useful during extended debugging sessions.
# DotBased