[REFACTOR] Refactored logging & added support for Microsoft.Extensions.Logging. Added test WebApi project

This commit is contained in:
max 2024-11-17 22:51:54 +01:00
parent 58739c2aea
commit 737cbcfd11
31 changed files with 398 additions and 97 deletions

View File

@ -7,14 +7,23 @@
using DotBased.Logging.Serilog; using DotBased.Logging.Serilog;
using DotBased.Logging; using DotBased.Logging;
using DotBased.Utilities;
using Serilog; using Serilog;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
LogService.Initialize(options =>
{
options
.AddSeverityFilter("Program", LogSeverity.Verbose)
.AddSeverityFilter("DotBased.dll", LogSeverity.Verbose);
});
var serilogLogger = SetupSerilog(); var serilogLogger = SetupSerilog();
LogService.AddLogAdapter(new BasedSerilogAdapter(serilogLogger)); LogService.AddLogAdapter(new BasedSerilogAdapter(serilogLogger));
var logger = LogService.RegisterLogger(typeof(Program)); var logger = LogService.RegisterLogger<Program>();
logger.Information("Whoah... Hi!"); logger.Information("Whoah... Hi!, {Param}", "Test!");
var cult = Culture.GetSystemCultures();
Console.ReadKey(); // Hold console app open. Console.ReadKey(); // Hold console app open.
return; return;

View File

@ -18,21 +18,23 @@ public class BasedServerAuthenticationStateProvider : ServerAuthenticationStateP
_config = configuration; _config = configuration;
_localStorage = localStorage; _localStorage = localStorage;
_securityService = securityService; _securityService = securityService;
_logger = LogService.RegisterLogger(typeof(BasedServerAuthenticationStateProvider)); _logger = LogService.RegisterLogger<BasedServerAuthenticationStateProvider>();
} }
private BasedAuthConfiguration _config; private BasedAuthConfiguration _config;
private readonly ProtectedLocalStorage _localStorage; private readonly ProtectedLocalStorage _localStorage;
private readonly SecurityService _securityService; private readonly SecurityService _securityService;
private ILogger _logger; private readonly ILogger _logger;
private readonly AuthenticationState _anonState = new(new ClaimsPrincipal()); private readonly AuthenticationState _anonState = new(new ClaimsPrincipal());
public override async Task<AuthenticationState> GetAuthenticationStateAsync() public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{ {
_logger.Debug("Getting authentication state...");
var sessionIdResult = await _localStorage.GetAsync<string>(BasedAuthDefaults.StorageKey); var sessionIdResult = await _localStorage.GetAsync<string>(BasedAuthDefaults.StorageKey);
if (!sessionIdResult.Success || sessionIdResult.Value == null) if (!sessionIdResult.Success || sessionIdResult.Value == null)
return _anonState; return _anonState;
_logger.Debug("Found state [{State}], getting session from {Service}", sessionIdResult.Value, nameof(SecurityService));
var stateResult = await _securityService.GetAuthenticationStateFromSessionAsync(sessionIdResult.Value); var stateResult = await _securityService.GetAuthenticationStateFromSessionAsync(sessionIdResult.Value);
return stateResult is { Success: true, Value: not null } ? stateResult.Value : _anonState; return stateResult is { Success: true, Value: not null } ? stateResult.Value : _anonState;
} }

View File

@ -18,6 +18,7 @@ public static class DotBasedAuthDependencyInjection
var Configuration = new BasedAuthConfiguration(); var Configuration = new BasedAuthConfiguration();
configurationAction?.Invoke(Configuration); configurationAction?.Invoke(Configuration);
services.AddSingleton<BasedAuthConfiguration>(Configuration); services.AddSingleton<BasedAuthConfiguration>(Configuration);
if (Configuration.AuthDataRepositoryType == null) if (Configuration.AuthDataRepositoryType == null)
throw new ArgumentNullException(nameof(Configuration.AuthDataRepositoryType), $"No '{nameof(IAuthDataRepository)}' configured!"); throw new ArgumentNullException(nameof(Configuration.AuthDataRepositoryType), $"No '{nameof(IAuthDataRepository)}' configured!");

View File

@ -34,11 +34,11 @@ public class MemoryAuthDataRepository : IAuthDataRepository
public async Task<Result<UserModel>> GetUserAsync(string id, string email, string username) public async Task<Result<UserModel>> GetUserAsync(string id, string email, string username)
{ {
UserModel? userModel = null; UserModel? userModel = null;
if (!id.IsNullOrWhiteSpace()) if (!id.IsNullOrEmpty())
userModel = MemoryData.users.FirstOrDefault(u => u.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); userModel = MemoryData.users.FirstOrDefault(u => u.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
if (!email.IsNullOrWhiteSpace()) if (!email.IsNullOrEmpty())
userModel = MemoryData.users.FirstOrDefault(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase)); userModel = MemoryData.users.FirstOrDefault(u => u.Email.Equals(email, StringComparison.OrdinalIgnoreCase));
if (!username.IsNullOrWhiteSpace()) if (!username.IsNullOrEmpty())
userModel = MemoryData.users.FirstOrDefault(u => u.UserName.Equals(username, StringComparison.OrdinalIgnoreCase)); userModel = MemoryData.users.FirstOrDefault(u => u.UserName.Equals(username, StringComparison.OrdinalIgnoreCase));
return userModel != null ? Result<UserModel>.Ok(userModel) : Result<UserModel>.Failed("No user found!"); return userModel != null ? Result<UserModel>.Ok(userModel) : Result<UserModel>.Failed("No user found!");
} }

View File

@ -16,7 +16,7 @@ public class SecurityService
_authDataRepository = authDataRepository; _authDataRepository = authDataRepository;
_dataCache = dataCache; _dataCache = dataCache;
_localStorage = localStorage; _localStorage = localStorage;
_logger = LogService.RegisterLogger(typeof(SecurityService)); _logger = LogService.RegisterLogger<SecurityService>();
} }
private readonly IAuthDataRepository _authDataRepository; private readonly IAuthDataRepository _authDataRepository;
@ -26,7 +26,7 @@ public class SecurityService
public async Task<Result<AuthenticationState>> GetAuthenticationStateFromSessionAsync(string id) public async Task<Result<AuthenticationState>> GetAuthenticationStateFromSessionAsync(string id)
{ {
if (id.IsNullOrWhiteSpace()) if (id.IsNullOrEmpty())
return Result<AuthenticationState>.Failed("No valid id!"); return Result<AuthenticationState>.Failed("No valid id!");
AuthenticationStateModel? authStateModel = null; AuthenticationStateModel? authStateModel = null;
var stateCache = _dataCache.RequestSessionState(id); var stateCache = _dataCache.RequestSessionState(id);
@ -75,13 +75,13 @@ public class SecurityService
{ {
UserModel? user = null; UserModel? user = null;
Result<UserModel> usrResult; Result<UserModel> usrResult;
if (!login.UserName.IsNullOrWhiteSpace()) if (!login.UserName.IsNullOrEmpty())
{ {
usrResult = await _authDataRepository.GetUserAsync(string.Empty, string.Empty, login.UserName); usrResult = await _authDataRepository.GetUserAsync(string.Empty, string.Empty, login.UserName);
if (usrResult is { Success: true, Value: not null }) if (usrResult is { Success: true, Value: not null })
user = usrResult.Value; user = usrResult.Value;
} }
else if (!login.Email.IsNullOrWhiteSpace()) else if (!login.Email.IsNullOrEmpty())
{ {
usrResult = await _authDataRepository.GetUserAsync(string.Empty, login.Email, string.Empty); usrResult = await _authDataRepository.GetUserAsync(string.Empty, login.Email, string.Empty);
if (usrResult is { Success: true, Value: not null }) if (usrResult is { Success: true, Value: not null })
@ -114,7 +114,7 @@ public class SecurityService
{ {
try try
{ {
if (state.IsNullOrWhiteSpace()) if (state.IsNullOrEmpty())
return Result.Failed($"Argument {nameof(state)} is empty!"); return Result.Failed($"Argument {nameof(state)} is empty!");
var stateResult = await _authDataRepository.GetAuthenticationStateAsync(state); var stateResult = await _authDataRepository.GetAuthenticationStateAsync(state);

View File

@ -0,0 +1,63 @@
using Microsoft.Extensions.Logging;
namespace DotBased.Logging.MEL;
public class BasedLogger : Microsoft.Extensions.Logging.ILogger
{
public BasedLogger(ILogger logger)
{
basedLogger = logger;
}
private readonly ILogger basedLogger;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
return;
var severity = ConvertLogLevelToSeverity(logLevel);
var capsule = ConstructCapsule(severity, eventId, state, exception, formatter);
basedLogger.Log(capsule);
}
private LogCapsule ConstructCapsule<TState>(LogSeverity severity, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
//TODO: Extract parameters & format
if (state is IEnumerable<KeyValuePair<string, object>> stateEnum)
{
foreach (var prop in stateEnum)
{
}
}
return new LogCapsule()
{
Exception = exception,
Message = formatter.Invoke(state, exception),
Parameters = [],
Severity = severity,
TimeStamp = DateTime.Now,
Logger = basedLogger as LoggerBase ?? throw new NullReferenceException(nameof(basedLogger))
};
}
private LogSeverity ConvertLogLevelToSeverity(LogLevel level)
{
return level switch
{
LogLevel.Trace => LogSeverity.Trace,
LogLevel.Debug => LogSeverity.Debug,
LogLevel.Information => LogSeverity.Info,
LogLevel.Warning => LogSeverity.Warning,
LogLevel.Error => LogSeverity.Error,
LogLevel.Critical => LogSeverity.Fatal,
LogLevel.None => LogSeverity.Ignore,
_ => LogSeverity.Verbose
};
}
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default;
}

View File

@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging;
namespace DotBased.Logging.MEL;
public class BasedLoggerProvider : ILoggerProvider
{
public BasedLoggerProvider(LogOptions options)
{
Options = options;
}
private readonly LogOptions Options;
public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName)
{
return new BasedLogger(Options.LoggerBuilder.Invoke(new LoggerInformation(typeof(BasedLoggerProvider)), categoryName));
}
public void Dispose()
{
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DotBased\DotBased.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using Microsoft.Extensions.Logging;
namespace DotBased.Logging.MEL;
public static class LoggerBuilderExtensions
{
public static ILoggingBuilder AddDotBased(this ILoggingBuilder builder, LogOptions options)
{
if (builder == null)
throw new ArgumentNullException(nameof(builder));
builder.AddProvider(new BasedLoggerProvider(options));
return builder;
}
}

View File

@ -7,7 +7,7 @@ public static class BasedSerilog
/// <summary> /// <summary>
/// Default output template with the extra properties that can be used for serilog sinks. /// Default output template with the extra properties that can be used for serilog sinks.
/// </summary> /// </summary>
public const string OutputTemplate = "[{Timestamp:HH:mm:ss} - {Caller}->{Assembly}] | {Level:u3}] {Message:lj}{NewLine}{Exception}"; public const string OutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3} - {LoggerName}]{NewLine} {Message:lj}{NewLine}{Exception}";
public static LoggerConfiguration UseBasedExtension(this LoggerConfiguration loggerConfiguration) public static LoggerConfiguration UseBasedExtension(this LoggerConfiguration loggerConfiguration)
{ {
@ -20,8 +20,10 @@ public static class BasedSerilog
/// </summary> /// </summary>
public static class ExtraProperties public static class ExtraProperties
{ {
public const string LoggerName = "LoggerName";
public const string AssemblyProp = "Assembly"; public const string AssemblyProp = "Assembly";
public const string SourceProp = "Source"; public const string FullNameProp = "FullName";
public const string NamespaceProp = "Namespace";
public const string CallerProp = "Caller"; public const string CallerProp = "Caller";
} }
} }

View File

@ -16,9 +16,11 @@ public class BasedSerilogAdapter(global::Serilog.ILogger serilogLogger) : LogAda
if (capsule == null) if (capsule == null)
return; return;
var logger = serilogLogger var logger = serilogLogger
.ForContext(BasedSerilog.ExtraProperties.AssemblyProp, capsule.Logger.Caller.AssemblyName) .ForContext(BasedSerilog.ExtraProperties.LoggerName, capsule.Logger.Name)
.ForContext(BasedSerilog.ExtraProperties.SourceProp, capsule.Logger.Caller.Source) .ForContext(BasedSerilog.ExtraProperties.AssemblyProp, capsule.Logger.LoggerInformation.AssemblyName)
.ForContext(BasedSerilog.ExtraProperties.CallerProp, capsule.Logger.Caller.Name); .ForContext(BasedSerilog.ExtraProperties.FullNameProp, capsule.Logger.LoggerInformation.TypeFullName)
.ForContext(BasedSerilog.ExtraProperties.NamespaceProp, capsule.Logger.LoggerInformation.TypeNamespace)
.ForContext(BasedSerilog.ExtraProperties.CallerProp, capsule.Logger.LoggerInformation.TypeName);
var template = _messageTemplateParser.Parse(capsule.Message); var template = _messageTemplateParser.Parse(capsule.Message);
IEnumerable<LogEventProperty>? properties = null; IEnumerable<LogEventProperty>? properties = null;

View File

@ -15,7 +15,7 @@ public class BasedSerilogEnricher : ILogEventEnricher
sourcePropValue = appValue.ToString().Replace("\"", ""); sourcePropValue = appValue.ToString().Replace("\"", "");
var assemblyProperty = propertyFactory.CreateProperty(BasedSerilog.ExtraProperties.AssemblyProp, asmPropValue); var assemblyProperty = propertyFactory.CreateProperty(BasedSerilog.ExtraProperties.AssemblyProp, asmPropValue);
var sourceProperty = propertyFactory.CreateProperty(BasedSerilog.ExtraProperties.SourceProp, sourcePropValue); var sourceProperty = propertyFactory.CreateProperty(BasedSerilog.ExtraProperties.FullNameProp, sourcePropValue);
var callerProperty = propertyFactory.CreateProperty(BasedSerilog.ExtraProperties.CallerProp, sourcePropValue); var callerProperty = propertyFactory.CreateProperty(BasedSerilog.ExtraProperties.CallerProp, sourcePropValue);
logEvent.AddPropertyIfAbsent(assemblyProperty); logEvent.AddPropertyIfAbsent(assemblyProperty);

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
</PropertyGroup> </PropertyGroup>

View File

@ -10,6 +10,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{2156
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.ASP.Auth", "DotBased.ASP.Auth\DotBased.ASP.Auth.csproj", "{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.ASP.Auth", "DotBased.ASP.Auth\DotBased.ASP.Auth.csproj", "{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotBased.Logging.MEL", "DotBased.Logging.MEL\DotBased.Logging.MEL.csproj", "{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{DBDB4538-85D4-45AC-9E0A-A684467AEABA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestWebApi", "TestWebApi\TestWebApi.csproj", "{BADA4BAF-142B-47A8-95FC-B25E1D3D0020}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -32,9 +38,19 @@ Global
{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.Build.0 = Release|Any CPU {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D}.Release|Any CPU.Build.0 = Release|Any CPU
{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D}.Release|Any CPU.Build.0 = Release|Any CPU
{BADA4BAF-142B-47A8-95FC-B25E1D3D0020}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BADA4BAF-142B-47A8-95FC-B25E1D3D0020}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BADA4BAF-142B-47A8-95FC-B25E1D3D0020}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BADA4BAF-142B-47A8-95FC-B25E1D3D0020}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {EBBDAF9A-BFC7-4BDC-8C51-0501B59A1DDC} = {2156FB93-C252-4B33-8A0C-73C82FABB163}
{CBD4111D-F1CA-466A-AC73-9EAB7F235B3D} = {2156FB93-C252-4B33-8A0C-73C82FABB163} {CBD4111D-F1CA-466A-AC73-9EAB7F235B3D} = {2156FB93-C252-4B33-8A0C-73C82FABB163}
{D4D9B584-A524-4CBB-9B61-9CD65ED4AF0D} = {2156FB93-C252-4B33-8A0C-73C82FABB163}
{BADA4BAF-142B-47A8-95FC-B25E1D3D0020} = {DBDB4538-85D4-45AC-9E0A-A684467AEABA}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -8,7 +8,7 @@ namespace DotBased.Collections;
/// </summary> /// </summary>
public class InstanceContainer : IDisposable public class InstanceContainer : IDisposable
{ {
private readonly ILogger _log = LogService.RegisterLogger(typeof(InstanceContainer)); private readonly ILogger _log = LogService.RegisterLogger<InstanceContainer>();
private readonly Dictionary<string, InstanceNode> _tCollection = new Dictionary<string, InstanceNode>(); private readonly Dictionary<string, InstanceNode> _tCollection = new Dictionary<string, InstanceNode>();
/// <summary> /// <summary>
@ -40,7 +40,7 @@ public class InstanceContainer : IDisposable
case null: case null:
break; break;
case IDisposable instance when dispose: case IDisposable instance when dispose:
_log.Debug("Disposing disposable object... (InstanceContainer)"); _log.Debug("Disposing disposable object...");
instance.Dispose(); instance.Dispose();
break; break;
} }

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>default</LangVersion> <LangVersion>default</LangVersion>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -5,5 +5,5 @@ namespace DotBased.Extensions;
/// </summary> /// </summary>
public static class StringExtensions public static class StringExtensions
{ {
public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s); public static bool IsNullOrEmpty(this string s) => string.IsNullOrWhiteSpace(s);
} }

View File

@ -5,6 +5,7 @@ namespace DotBased.Logging;
/// </summary> /// </summary>
public interface ILogger public interface ILogger
{ {
public void Log(LogCapsule capsule);
public void Verbose(string message, params object?[]? parameters); public void Verbose(string message, params object?[]? parameters);
public void Trace(string message, params object?[]? parameters); public void Trace(string message, params object?[]? parameters);
@ -18,4 +19,6 @@ public interface ILogger
public void Error(Exception exception, string message, params object?[]? parameters); public void Error(Exception exception, string message, params object?[]? parameters);
public void Fatal(Exception exception, string message, params object?[]? parameters); public void Fatal(Exception exception, string message, params object?[]? parameters);
public string Name { get; }
} }

View File

@ -1,7 +1,7 @@
namespace DotBased.Logging; namespace DotBased.Logging;
/// <summary> /// <summary>
/// The base for creating log adpaters. /// The base for creating log adapters.
/// </summary> /// </summary>
public abstract class LogAdapterBase public abstract class LogAdapterBase
{ {
@ -19,7 +19,7 @@ public abstract class LogAdapterBase
public string AdapterName { get; } public string AdapterName { get; }
/// <summary> /// <summary>
/// Handle the incomming <see cref="LogCapsule"/> that the <see cref="LogProcessor"/> sends. /// Handle the incoming <see cref="LogCapsule"/> that the <see cref="LogProcessor"/> sends.
/// </summary> /// </summary>
/// <param name="processor">The log processor that has processed this log</param> /// <param name="processor">The log processor that has processed this log</param>
/// <param name="capsule">The log capsule, which contains the log information</param> /// <param name="capsule">The log capsule, which contains the log information</param>

View File

@ -1,3 +1,6 @@
using System.Collections.ObjectModel;
using DotBased.Extensions;
namespace DotBased.Logging; namespace DotBased.Logging;
/// <summary> /// <summary>
@ -5,14 +8,35 @@ namespace DotBased.Logging;
/// </summary> /// </summary>
public class LogOptions public class LogOptions
{ {
public readonly SeverityFilterCollection SeverityFilters = [];
/// <summary> /// <summary>
/// The severty the logger will log /// The severity the logger will log
/// </summary> /// </summary>
public LogSeverity Severity { get; set; } = LogSeverity.Trace; public LogSeverity Severity { get; set; } = LogSeverity.Trace;
/// <summary> /// <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. /// The function that will build and return the <see cref="ILogger"/>.
/// </summary> /// </summary>
public Func<CallerInformation, Action<LogCapsule>, LoggerBase> LoggerBuilder { get; set; } = public Func<LoggerInformation, string, ILogger> LoggerBuilder { get; set; } =
(identifier, sendEvent) => new Logger(identifier, ref sendEvent); (identifier, loggerName) => new Logger(identifier, loggerName);
public LogOptions AddSeverityFilter(string filter, LogSeverity logSeverity)
{
if (filter.IsNullOrEmpty())
return this;
SeverityFilters.Add(new SeverityFilter() { Filter = filter, Severity = logSeverity });
return this;
}
}
public struct SeverityFilter
{
public string Filter { get; set; }
public LogSeverity Severity { get; set; }
}
public class SeverityFilterCollection : KeyedCollection<string, SeverityFilter>
{
protected override string GetKeyForItem(SeverityFilter item) => item.Filter;
} }

View File

@ -8,7 +8,7 @@ public class LogProcessor : IDisposable
public LogProcessor() public LogProcessor()
{ {
_processorQueue = new Queue<LogCapsule>(); _processorQueue = new Queue<LogCapsule>();
IncommingLogHandlerEvent = IncommingLogHandler; IncomingLogHandlerEvent = IncomingLogHandler;
_processorThread = new Thread(ProcessLog) _processorThread = new Thread(ProcessLog)
{ {
IsBackground = true, IsBackground = true,
@ -16,7 +16,7 @@ public class LogProcessor : IDisposable
}; };
_processorThread.Start(); _processorThread.Start();
} }
public readonly Action<LogCapsule> IncommingLogHandlerEvent; public readonly Action<LogCapsule> IncomingLogHandlerEvent;
public event EventHandler<LogCapsule>? LogProcessed; public event EventHandler<LogCapsule>? LogProcessed;
private readonly Queue<LogCapsule> _processorQueue; private readonly Queue<LogCapsule> _processorQueue;
private readonly Thread _processorThread; private readonly Thread _processorThread;
@ -41,10 +41,10 @@ public class LogProcessor : IDisposable
Stop(); Stop();
} }
private void IncommingLogHandler(LogCapsule e) private void IncomingLogHandler(LogCapsule e)
{ {
_processorQueue.Enqueue(e); _processorQueue.Enqueue(e);
// Check is the thread is running, if not wake up the thread. // Check if the thread is running, if not wake up the thread.
if (!_threadSuspendEvent.WaitOne(0)) if (!_threadSuspendEvent.WaitOne(0))
_threadSuspendEvent.Set(); _threadSuspendEvent.Set();
} }
@ -63,7 +63,9 @@ public class LogProcessor : IDisposable
if (_processorQueue.Count != 0) if (_processorQueue.Count != 0)
{ {
var capsule = _processorQueue.Dequeue(); var capsule = _processorQueue.Dequeue();
if (LogService.ShouldLog(LogService.Options.Severity, capsule.Severity)) if (!LogService.CanLog(LogService.Options.Severity, capsule.Severity))
continue;
if (LogService.FilterSeverityLog(capsule))
LogProcessed?.Invoke(this, capsule); LogProcessed?.Invoke(this, capsule);
} }
else else

View File

@ -1,5 +1,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reflection; using System.Reflection;
using DotBased.Extensions;
namespace DotBased.Logging; namespace DotBased.Logging;
@ -8,52 +9,43 @@ namespace DotBased.Logging;
/// </summary> /// </summary>
public static class LogService public static class LogService
{ {
// TODO: Future: add middlewares and changeable log processor
static LogService() static LogService()
{ {
Options = new LogOptions(); Options = new LogOptions();
LoggerSendEvent = LogProcessor.IncommingLogHandlerEvent; LoggerSendEvent = LogProcessor.IncomingLogHandlerEvent;
} }
public static bool ShouldLog(LogSeverity maxSeverity, LogSeverity severity) => maxSeverity <= severity; public static bool CanLog(LogSeverity maxSeverity, LogSeverity severity) => maxSeverity <= severity;
public static LogOptions Options { get; private set; } public static LogOptions Options { get; private set; }
public static LogProcessor LogProcessor { get; private set; } = new LogProcessor(); public static LogProcessor LogProcessor { get; private set; } = new LogProcessor();
private static HashSet<LogAdapterBase> Adapters { get; } = new HashSet<LogAdapterBase>(); private static HashSet<LogAdapterBase> Adapters { get; } = [];
private static HashSet<LoggerBase> Loggers { get; } = new HashSet<LoggerBase>(); private static HashSet<ILogger> Loggers { get; } = [];
/// <summary> /// <summary>
/// Action for internal communication between loggers and processor /// Action for internal communication between loggers and processor
/// </summary> /// </summary>
private static readonly Action<LogCapsule> LoggerSendEvent; internal static readonly Action<LogCapsule> LoggerSendEvent;
/// <summary> public static void Initialize(Action<LogOptions>? options = null)
/// 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); Options = new LogOptions();
options?.Invoke(Options);
}
public static ILogger RegisterLogger(Type? callerType, string name = "")
{
var logger = Options.LoggerBuilder.Invoke(new LoggerInformation(callerType), name);
Loggers.Add(logger); Loggers.Add(logger);
return logger; return logger;
} }
public static bool UnregisterLogger(LoggerBase logger) => Loggers.Remove(logger); /// <summary>
/// Register a logger.
/// </summary>
/// <returns>The configured <see cref="ILogger"/> implementation that will be configured in the <see cref="LogOptions.LoggerBuilder"/> at the <see cref="LogService"/> class</returns>
public static ILogger RegisterLogger<T>() => RegisterLogger(typeof(T), string.Empty);
public static ReadOnlyCollection<LoggerBase> GetLoggers => new ReadOnlyCollection<LoggerBase>(Loggers.ToList()); public static ReadOnlyCollection<ILogger> GetLoggers => new ReadOnlyCollection<ILogger>(Loggers.ToList());
/// <summary> /// <summary>
/// Add a log adapter to the service. /// Add a log adapter to the service.
@ -69,7 +61,7 @@ public static class LogService
/// Removes the log adapter from the service. /// Removes the log adapter from the service.
/// </summary> /// </summary>
/// <param name="adapter">The adapter to remove</param> /// <param name="adapter">The adapter to remove</param>
/// <returns>True if the adapter is succesfully removed otherwise false.</returns> /// <returns>True if the adapter is successfully removed otherwise false.</returns>
public static bool RemoveLogAdapter(LogAdapterBase adapter) public static bool RemoveLogAdapter(LogAdapterBase adapter)
{ {
if (!Adapters.Contains(adapter)) return false; if (!Adapters.Contains(adapter)) return false;
@ -77,28 +69,52 @@ public static class LogService
return Adapters.Remove(adapter); return Adapters.Remove(adapter);
} }
public static ReadOnlyCollection<LogAdapterBase> GetAdapters => public static ReadOnlyCollection<LogAdapterBase> GetAdapters => new ReadOnlyCollection<LogAdapterBase>(Adapters.ToList());
new ReadOnlyCollection<LogAdapterBase>(Adapters.ToList());
}
internal static bool FilterSeverityLog(LogCapsule capsule)
public readonly struct CallerInformation
{ {
public CallerInformation(Type type) if (Options.SeverityFilters.TryGetValue(capsule.Logger.Name, out var namespaceFilter))
{ return CanLog(namespaceFilter.Severity, capsule.Severity);
Name = type.Name; var filterCapsuleNamespace = Options.SeverityFilters.Where(kvp => capsule.Logger.Name.Contains(kvp.Filter)).Select(v => v).ToList();
Source = type.FullName ?? type.GUID.ToString(); if (filterCapsuleNamespace.Count == 0) return true;
Namespace = type.Namespace ?? string.Empty; var filter = filterCapsuleNamespace.FirstOrDefault();
SourceAssembly = type.Assembly; return CanLog(filter.Severity, capsule.Severity);
}
}
var asmName = SourceAssembly.GetName();
AssemblyName = asmName.Name ?? "Unknown"; public readonly struct LoggerInformation
AssemblyFullname = asmName.FullName; {
public LoggerInformation(Type? type)
{
if (type == null)
return;
TypeName = type.Name;
TypeFullName = type.FullName ?? string.Empty;
TypeNamespace = type.Namespace ?? string.Empty;
var module = type.Module;
ModuleName = module.Name;
ModuleScopeName = module.ScopeName;
ModuleFullyQualifiedName = module.FullyQualifiedName;
var assemblyName = type.Assembly.GetName();
AssemblyName = assemblyName.Name ?? (type.Assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute)).FirstOrDefault() as AssemblyTitleAttribute)?.Title ?? string.Empty;
AssemblyFullname = assemblyName.FullName;
if (TypeFullName.IsNullOrEmpty())
TypeFullName = !TypeNamespace.IsNullOrEmpty() ? $"{TypeNamespace}.{TypeName}" : TypeName;
if (TypeNamespace.IsNullOrEmpty())
TypeNamespace = TypeName;
} }
public string Name { get; }
public string Source { get; } public string TypeName { get; } = string.Empty;
public string Namespace { get; } public string TypeFullName { get; } = string.Empty;
public Assembly SourceAssembly { get; } public string TypeNamespace { get; } = string.Empty;
public string AssemblyName { get; } public string AssemblyName { get; } = string.Empty;
public string AssemblyFullname { get; } public string AssemblyFullname { get; } = string.Empty;
public string ModuleName { get; } = string.Empty;
public string ModuleScopeName { get; } = string.Empty;
public string ModuleFullyQualifiedName { get; } = string.Empty;
} }

View File

@ -8,5 +8,6 @@ public enum LogSeverity
Info = 3, Info = 3,
Warning = 4, Warning = 4,
Error = 5, Error = 5,
Fatal = 6 Fatal = 6,
Ignore = 99
} }

View File

@ -3,13 +3,8 @@ namespace DotBased.Logging;
/// <summary> /// <summary>
/// Main logger, this class is the default logger that the <see cref="LogService.RegisterLogger"/> function will return. /// Main logger, this class is the default logger that the <see cref="LogService.RegisterLogger"/> function will return.
/// </summary> /// </summary>
public class Logger(CallerInformation caller, ref Action<LogCapsule> logProcessorHandler) : LoggerBase(caller, ref logProcessorHandler) public class Logger(LoggerInformation loggerInformation, string name) : LoggerBase(loggerInformation, name)
{ {
public void Log(LogCapsule capsule)
{
ProcessLog(capsule);
}
public override void Verbose(string message, params object?[]? parameters) public override void Verbose(string message, params object?[]? parameters)
{ {
Log(new LogCapsule() Log(new LogCapsule()
@ -96,5 +91,5 @@ public class Logger(CallerInformation caller, ref Action<LogCapsule> logProcesso
}); });
} }
public override int GetHashCode() => HashCode.Combine(Caller.Source, Caller.AssemblyFullname); public override int GetHashCode() => HashCode.Combine(LoggerInformation.TypeFullName, LoggerInformation.AssemblyFullname);
} }

View File

@ -1,15 +1,23 @@
using DotBased.Extensions;
namespace DotBased.Logging; namespace DotBased.Logging;
/// <summary> /// <summary>
/// Base for creating loggers /// Base for creating loggers
/// </summary> /// </summary>
/// <param name="caller">The caller information</param> /// <param name="loggerInformation">The caller information</param>
/// <param name="logProcessorHandler">The handler where the logs can be send to</param> public abstract class LoggerBase(LoggerInformation loggerInformation, string name) : ILogger
public abstract class LoggerBase(CallerInformation caller, ref Action<LogCapsule> logProcessorHandler) : ILogger
{ {
public CallerInformation Caller { get; } = caller; public LoggerInformation LoggerInformation { get; } = loggerInformation;
public string Name { get; } = name.IsNullOrEmpty() ? loggerInformation.TypeNamespace : name;
private readonly Action<LogCapsule> ProcessLog = LogService.LoggerSendEvent;
public void Log(LogCapsule capsule)
{
ProcessLog(capsule);
}
internal readonly Action<LogCapsule> ProcessLog = logProcessorHandler;
public abstract void Verbose(string message, params object?[]? parameters); public abstract void Verbose(string message, params object?[]? parameters);
public abstract void Trace(string message, params object?[]? parameters); public abstract void Trace(string message, params object?[]? parameters);

View File

@ -2,11 +2,11 @@ using DotBased.Extensions;
namespace DotBased.Objects; namespace DotBased.Objects;
public class DbObjectAttribute<TValueType> : ObjectAttribute<TValueType> public class DbObjectAttribute<TValueType> : ObjectAttribute<TValueType> where TValueType : IConvertible
{ {
public DbObjectAttribute(string key, TValueType value, string owner) : base(key, value) public DbObjectAttribute(string key, TValueType value, string owner) : base(key, value)
{ {
if (owner.IsNullOrWhiteSpace()) if (owner.IsNullOrEmpty())
throw new ArgumentNullException(nameof(owner), $"The parameter {nameof(owner)} is null or empty! This parameter is required!"); throw new ArgumentNullException(nameof(owner), $"The parameter {nameof(owner)} is null or empty! This parameter is required!");
Owner = owner; Owner = owner;
} }

View File

@ -6,7 +6,7 @@ public class ObjectAttribute<TValueType> : IObjectAttribute<TValueType>
{ {
protected ObjectAttribute(string key, TValueType value) protected ObjectAttribute(string key, TValueType value)
{ {
if (key.IsNullOrWhiteSpace()) if (key.IsNullOrEmpty())
throw new ArgumentNullException(nameof(key), $"The parameter {nameof(key)} is null or empty!"); throw new ArgumentNullException(nameof(key), $"The parameter {nameof(key)} is null or empty!");
Key = key; Key = key;
Value = value; Value = value;

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using DotBased.Logging;
namespace DotBased.Utilities; namespace DotBased.Utilities;
@ -6,6 +7,7 @@ public static class Culture
{ {
private static List<CultureInfo> _sysCultures = new List<CultureInfo>(); private static List<CultureInfo> _sysCultures = new List<CultureInfo>();
private static Dictionary<string, RegionInfo> _regions = new Dictionary<string, RegionInfo>(); private static Dictionary<string, RegionInfo> _regions = new Dictionary<string, RegionInfo>();
private static readonly ILogger _logger = LogService.RegisterLogger(typeof(Culture));
/// <summary> /// <summary>
/// Get all system known cultures. /// Get all system known cultures.
@ -14,6 +16,7 @@ public static class Culture
/// <returns>The list with <see cref="CultureInfo"/>'s the system knows</returns> /// <returns>The list with <see cref="CultureInfo"/>'s the system knows</returns>
public static IEnumerable<CultureInfo> GetSystemCultures() public static IEnumerable<CultureInfo> GetSystemCultures()
{ {
_logger.Debug("Getting system cultures...");
if (_sysCultures.Count == 0) if (_sysCultures.Count == 0)
_sysCultures = CultureInfo.GetCultures(CultureTypes.AllCultures).ToList(); _sysCultures = CultureInfo.GetCultures(CultureTypes.AllCultures).ToList();
return _sysCultures; return _sysCultures;

72
TestWebApi/Program.cs Normal file
View File

@ -0,0 +1,72 @@
using DotBased.Logging;
using DotBased.Logging.MEL;
using DotBased.Logging.Serilog;
using Serilog;
using ILogger = Serilog.ILogger;
var builder = WebApplication.CreateBuilder(args);
LogService.Initialize(options =>
{
options
.AddSeverityFilter("Program", LogSeverity.Verbose);
});
var serilogLogger = SetupSerilog();
LogService.AddLogAdapter(new BasedSerilogAdapter(serilogLogger));
builder.Logging.ClearProviders();
builder.Logging.AddDotBased(LogService.Options);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
ILogger SetupSerilog()
{
var logConfig = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.Console(outputTemplate: BasedSerilog.OutputTemplate);
return logConfig.CreateLogger();
}
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DotBased.Logging.MEL\DotBased.Logging.MEL.csproj" />
<ProjectReference Include="..\DotBased.Logging.Serilog\DotBased.Logging.Serilog.csproj" />
<ProjectReference Include="..\DotBased\DotBased.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,6 @@
@TestWebApi_HostAddress = http://localhost:5044
GET {{TestWebApi_HostAddress}}/weatherforecast/
Accept: application/json
###