diff --git a/LANCommander.Server/Endpoints/SaveEndpoints.cs b/LANCommander.Server/Endpoints/SaveEndpoints.cs index ea2c7363..0d27bf62 100644 --- a/LANCommander.Server/Endpoints/SaveEndpoints.cs +++ b/LANCommander.Server/Endpoints/SaveEndpoints.cs @@ -4,7 +4,6 @@ using LANCommander.Server.Data.Models; using LANCommander.Server.Extensions; using LANCommander.Server.Services; using Microsoft.AspNetCore.Mvc; -using Serilog; using System.DirectoryServices.AccountManagement; using System.Security.Claims; using File = System.IO.File; @@ -30,6 +29,7 @@ public static class SaveEndpoints public static async Task GetAsync( ClaimsPrincipal userPrincipal, + [FromServices] ILogger logger, [FromServices] UserService userService, [FromServices] GameSaveService saveService) { @@ -37,7 +37,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } @@ -50,6 +50,7 @@ public static class SaveEndpoints public static async Task GetByIdAsync( Guid id, ClaimsPrincipal userPrincipal, + [FromServices] ILogger logger, [FromServices] UserService userService, [FromServices] GameSaveService saveService) { @@ -57,7 +58,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } @@ -73,6 +74,7 @@ public static class SaveEndpoints public static async Task DeleteByIdAsync( Guid id, ClaimsPrincipal userPrincipal, + [FromServices] ILogger logger, [FromServices] UserService userService, [FromServices] GameSaveService saveService) { @@ -80,7 +82,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } @@ -98,6 +100,7 @@ public static class SaveEndpoints public static async Task GetSavesByGameAsync( Guid gameId, ClaimsPrincipal userPrincipal, + [FromServices] ILogger logger, [FromServices] UserService userService, [FromServices] GameSaveService saveService) { @@ -105,7 +108,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } @@ -118,6 +121,7 @@ public static class SaveEndpoints public static async Task GetLatestSaveByGameAsync( Guid gameId, ClaimsPrincipal userPrincipal, + [FromServices] ILogger logger, [FromServices] IMapper mapper, [FromServices] UserService userService, [FromServices] GameSaveService saveService) @@ -126,7 +130,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } @@ -144,6 +148,7 @@ public static class SaveEndpoints public static async Task DownloadLatestSaveByGameAsync( Guid gameId, ClaimsPrincipal userPrincipal, + [FromServices] ILogger logger, [FromServices] UserService userService, [FromServices] GameSaveService saveService) { @@ -151,7 +156,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } @@ -178,6 +183,7 @@ public static class SaveEndpoints public static async Task DownloadSaveByIdAsync( Guid id, ClaimsPrincipal userPrincipal, + [FromServices] ILogger logger, [FromServices] UserService userService, [FromServices] GameSaveService saveService) { @@ -185,7 +191,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } @@ -209,6 +215,7 @@ public static class SaveEndpoints Guid gameId, ClaimsPrincipal userPrincipal, HttpContext httpContext, + [FromServices] ILogger logger, [FromServices] SettingsProvider settingsProvider, [FromServices] IMapper mapper, [FromServices] UserService userService, @@ -220,7 +227,7 @@ public static class SaveEndpoints if (user == null) { - Log.Error("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); + logger.LogError("Could not find user from claim principal: {UserName}", userPrincipal?.Identity?.Name); return TypedResults.Unauthorized(); } diff --git a/LANCommander.Server/Hubs/LoggingHub.cs b/LANCommander.Server/Hubs/LoggingHub.cs index 2e4a4016..037c89e7 100644 --- a/LANCommander.Server/Hubs/LoggingHub.cs +++ b/LANCommander.Server/Hubs/LoggingHub.cs @@ -1,7 +1,4 @@ -using LANCommander.SDK; -using LANCommander.Server.Services; -using Microsoft.AspNetCore.SignalR; -using Serilog.Events; +using Microsoft.AspNetCore.SignalR; namespace LANCommander.Server.Hubs { @@ -9,14 +6,13 @@ namespace LANCommander.Server.Hubs { public override async Task OnConnectedAsync() { - Clients.Caller.SendAsync("Log", "Connected to server logging provider!", LogEventLevel.Information, DateTime.Now); + await Clients.Caller.SendAsync("Log", "Connected to server logging provider!", LogLevel.Information, DateTime.Now); await base.OnConnectedAsync(); } - public static async Task Log(IHubContext context, string message, LogEvent logEvent) + public static async Task Log(IHubContext context, string message, LogLevel logLevel, DateTime timestamp) { - - await context.Clients.All.SendAsync("Log", message, logEvent.Level, logEvent.Timestamp.DateTime); + await context.Clients.All.SendAsync("Log", message, logLevel, timestamp); } } } \ No newline at end of file diff --git a/LANCommander.Server/LANCommander.Server.csproj b/LANCommander.Server/LANCommander.Server.csproj index f407a557..c0f837fd 100644 --- a/LANCommander.Server/LANCommander.Server.csproj +++ b/LANCommander.Server/LANCommander.Server.csproj @@ -85,7 +85,6 @@ - @@ -113,12 +112,6 @@ - - - - - - diff --git a/LANCommander.Server/Logging/SignalRLoggerProvider.cs b/LANCommander.Server/Logging/SignalRLoggerProvider.cs new file mode 100644 index 00000000..c29c3d1d --- /dev/null +++ b/LANCommander.Server/Logging/SignalRLoggerProvider.cs @@ -0,0 +1,55 @@ +using LANCommander.Server.Hubs; +using Microsoft.AspNetCore.SignalR; + +namespace LANCommander.Server.Logging; + +public class SignalRLoggerProvider(IServiceProvider serviceProvider, LogLevel minimumLevel = LogLevel.Information) : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) => new SignalRLogger(serviceProvider, minimumLevel); + + public void Dispose() + { + } + + private class SignalRLogger(IServiceProvider serviceProvider, LogLevel minimumLevel) : ILogger + { + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly LogLevel _minimumLevel = minimumLevel; + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => logLevel >= _minimumLevel; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + if (exception != null) + message += Environment.NewLine + exception; + + // Use a background task to avoid blocking + _ = Task.Run(async () => + { + try + { + var hubContext = _serviceProvider.GetService>(); + if (hubContext != null) + { + await hubContext.Clients.All.SendAsync("Log", message, logLevel, DateTime.Now); + } + } + catch + { + // Silently fail if SignalR is unavailable + } + }); + } + } +} diff --git a/LANCommander.Server/Logging/TerminalColor.cs b/LANCommander.Server/Logging/TerminalColor.cs index 14275574..3aa4ed07 100644 --- a/LANCommander.Server/Logging/TerminalColor.cs +++ b/LANCommander.Server/Logging/TerminalColor.cs @@ -1,7 +1,4 @@ -using Serilog.Events; -using System.Drawing.Text; - -namespace LANCommander.Server.Logging +namespace LANCommander.Server.Logging { public static class TerminalColor { diff --git a/LANCommander.Server/Program.cs b/LANCommander.Server/Program.cs index e86dabdf..855846fb 100644 --- a/LANCommander.Server/Program.cs +++ b/LANCommander.Server/Program.cs @@ -1,4 +1,3 @@ -using Serilog; using LANCommander.Server.UI; using LANCommander.Server.Startup; @@ -26,9 +25,11 @@ builder.AddDatabase(args); builder.Services.AddHealthChecks(); -Log.Debug("Building Application"); var app = builder.Build(); +var logger = app.Services.GetRequiredService>(); +logger.LogDebug("Building Application"); + app.UseDatabase(args); app.ValidateSettings(); @@ -42,9 +43,8 @@ app.UseMiddlewares(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - Log.Debug("App has been run in a development environment"); + logger.LogDebug("App has been run in a development environment"); app.UseMigrationsEndPoint(); - } else { diff --git a/LANCommander.Server/Startup/ApplicationSettings.cs b/LANCommander.Server/Startup/ApplicationSettings.cs index 5b7b9a9c..8b9d76bb 100644 --- a/LANCommander.Server/Startup/ApplicationSettings.cs +++ b/LANCommander.Server/Startup/ApplicationSettings.cs @@ -1,9 +1,5 @@ -using LANCommander.SDK; using LANCommander.SDK.Extensions; using Microsoft.Extensions.Options; -using Serilog; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace LANCommander.Server.Startup; @@ -25,10 +21,11 @@ public static class ApplicationSettings { var settingsProvider = app.Services.GetRequiredService>(); var settings = app.Services.GetRequiredService>(); + var logger = app.Services.GetRequiredService>(); if (settings.Value.Server.Authentication.TokenSecret.Length < 16) { - Log.Debug("JWT token secret is too short. Regenerating..."); + logger.LogDebug("JWT token secret is too short. Regenerating..."); settingsProvider.Update(s => { s.Server.Authentication.TokenSecret = Guid.NewGuid().ToString(); diff --git a/LANCommander.Server/Startup/Authentication.cs b/LANCommander.Server/Startup/Authentication.cs index af1b6342..49afb6c9 100644 --- a/LANCommander.Server/Startup/Authentication.cs +++ b/LANCommander.Server/Startup/Authentication.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; -using Serilog; namespace LANCommander.Server.Startup; @@ -47,8 +46,7 @@ public static class Authentication } catch (Exception ex) { - Log.Error(ex, "Authentication Provider {Name} could not be registered", - authenticationProvider.Name); + Console.WriteLine($"Error: Authentication Provider {authenticationProvider.Name} could not be registered: {ex.Message}"); } } @@ -114,7 +112,7 @@ public static class Authentication { context.Response.Redirect("/Login"); - Log.Error(context.Failure, "OIDC authentication failed"); + Console.WriteLine($"Error: OIDC authentication failed: {context.Failure?.Message}"); await context.Response.CompleteAsync(); }; @@ -163,7 +161,7 @@ public static class Authentication { context.Response.Redirect("/Login"); - Log.Error(context.Failure, "OAuth authentication failed"); + Console.WriteLine($"Error: OAuth authentication failed: {context.Failure?.Message}"); await context.Response.CompleteAsync(); }; diff --git a/LANCommander.Server/Startup/Database.cs b/LANCommander.Server/Startup/Database.cs index 1863ed8c..478b88c5 100644 --- a/LANCommander.Server/Startup/Database.cs +++ b/LANCommander.Server/Startup/Database.cs @@ -10,7 +10,6 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Octokit; -using Serilog; namespace LANCommander.Server.Startup; @@ -65,7 +64,7 @@ public static class Database if (File.Exists(dataSource)) { - Log.Information("Migrations pending, database will be backed up to {BackupName}", backupName); + logger.LogInformation("Migrations pending, database will be backed up to {BackupName}", backupName); File.Copy(dataSource, backupName); } } diff --git a/LANCommander.Server/Startup/Endpoints.cs b/LANCommander.Server/Startup/Endpoints.cs index 593facbe..68b7ef3c 100644 --- a/LANCommander.Server/Startup/Endpoints.cs +++ b/LANCommander.Server/Startup/Endpoints.cs @@ -1,5 +1,4 @@ using LANCommander.Server.Endpoints; -using Serilog; namespace LANCommander.Server.Startup; @@ -7,8 +6,6 @@ public static class Endpoints { public static void AddControllers(this WebApplicationBuilder builder) { - Log.Debug("Initializing Controllers"); - builder.Services.AddControllers().AddJsonOptions(static x => { x.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles; @@ -17,8 +14,6 @@ public static class Endpoints public static WebApplication MapEndpoints(this WebApplication app) { - Log.Debug("Registering Endpoints"); - app.UseEndpoints(endpoints => { endpoints.MapAuthenticationEndpoints(); diff --git a/LANCommander.Server/Startup/Identity.cs b/LANCommander.Server/Startup/Identity.cs index d3c2d4e0..e81d4587 100644 --- a/LANCommander.Server/Startup/Identity.cs +++ b/LANCommander.Server/Startup/Identity.cs @@ -4,7 +4,6 @@ using LANCommander.Server.Services; using LANCommander.Server.Services.Models; using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Tokens; -using Serilog; namespace LANCommander.Server.Startup; @@ -15,7 +14,6 @@ public static class Identity var settings = new Settings.Settings(); builder.Configuration.Bind(settings); - Log.Debug("Initializing Identity"); builder.Services.AddIdentityCore((options) => { options.SignIn.RequireConfirmedAccount = false; diff --git a/LANCommander.Server/Startup/Logger.cs b/LANCommander.Server/Startup/Logger.cs index 9f3f922b..1a847292 100644 --- a/LANCommander.Server/Startup/Logger.cs +++ b/LANCommander.Server/Startup/Logger.cs @@ -1,14 +1,6 @@ -using Elastic.Serilog.Sinks; -using LANCommander.Server.Configuration; -using LANCommander.Server.Hubs; -using LANCommander.Server.Parsers; -using LANCommander.Server.Services.Models; +using LANCommander.Server.Logging; using LANCommander.Server.Settings.Enums; using Microsoft.Extensions.Options; -using Serilog; -using Serilog.Events; -using Serilog.Filters; -using Serilog.Sinks.AspNetCore.App.SignalR.Extensions; namespace LANCommander.Server.Startup; @@ -16,73 +8,49 @@ public static class Logger { public static WebApplicationBuilder AddLogger(this WebApplicationBuilder builder) { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .WriteTo.Console() - .CreateBootstrapLogger(); - - builder.Services.AddSerilogHub(); - - builder.Services.AddSerilog((serviceProvider, config) => + builder.Services.AddLogging((loggingBuilder) => { - config.MinimumLevel.Verbose(); - config.Enrich.FromLogContext(); - + var serviceProvider = builder.Services.BuildServiceProvider(); var settings = serviceProvider.GetRequiredService>(); - + + // Configure filters if (settings.Value.Server.Logs.IgnorePings) - config.Filter.ByExcluding(Matching.WithProperty("RequestPath", v => v.StartsWith("/api/Ping", StringComparison.OrdinalIgnoreCase))); + { + loggingBuilder.AddFilter((category, level) => + { + // Filter out ping-related logs if IgnorePings is enabled + if (category != null && category.Contains("Ping", StringComparison.OrdinalIgnoreCase)) + return false; + return true; + }); + } foreach (var provider in settings.Value.Server.Logs.Providers) { - LogEventLevel minimumLevel = provider.MinimumLevel switch - { - LogLevel.Trace => LogEventLevel.Verbose, - LogLevel.Debug => LogEventLevel.Debug, - LogLevel.Warning => LogEventLevel.Warning, - LogLevel.Information => LogEventLevel.Information, - LogLevel.Error => LogEventLevel.Error, - LogLevel.Critical => LogEventLevel.Fatal, - _ => LogEventLevel.Information - }; - + var minimumLevel = provider.MinimumLevel; + switch (provider.Type) { case LoggingProviderType.Console: - config.WriteTo.Console(restrictedToMinimumLevel: minimumLevel); + loggingBuilder.AddConsole(); + loggingBuilder.SetMinimumLevel(minimumLevel); break; - - case LoggingProviderType.SignalR: - config.WriteTo.SignalR( - serviceProvider, - (context, message, logEvent) => LoggingHub.Log(context, message, logEvent)); - break; - - case LoggingProviderType.File: - config.WriteTo.File( - Path.Combine(provider.ConnectionString,"log-.txt"), - rollingInterval: (RollingInterval)(int)(provider.ArchiveEvery ?? LogInterval.Day), - restrictedToMinimumLevel: minimumLevel); - break; - - case LoggingProviderType.Seq: - try - { - var options = ConnectionStringBinder.Bind(provider.ConnectionString); - config.WriteTo.Seq( - restrictedToMinimumLevel: minimumLevel, - serverUrl: options.ServerUrl, - apiKey: options.ApiKey); - } - catch (Exception ex) - { - Log.Error(ex, "Could not bind Seq connection string"); - } + case LoggingProviderType.SignalR: + loggingBuilder.Services.AddSingleton(sp => + new SignalRLoggerProvider(sp, minimumLevel)); break; - + + case LoggingProviderType.File: + loggingBuilder.AddFile(provider.ConnectionString, minimumLevel); + break; + + case LoggingProviderType.Seq: case LoggingProviderType.ElasticSearch: - config.WriteTo.Elasticsearch([new Uri(provider.ConnectionString)], restrictedToMinimumLevel: minimumLevel); + // Note: Standard .NET logging doesn't have built-in support for Seq or Elasticsearch + // Users will need to add these providers manually if needed + builder.Logging.AddDebug(); + Console.WriteLine($"Warning: {provider.Type} logging provider is not supported in standard .NET logging. Consider using a third-party logging library."); break; } } @@ -90,4 +58,55 @@ public static class Logger return builder; } + + private static ILoggingBuilder AddFile(this ILoggingBuilder builder, string logDirectory, LogLevel minimumLevel) + => builder.AddProvider(new FileLoggerProvider(logDirectory, minimumLevel)); + + private class FileLogger(string logDirectory, string categoryName, LogLevel minimumLevel) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => logLevel >= minimumLevel; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var logFilePath = Path.Combine( + logDirectory, + $"log-{DateTime.Now:yyyy-MM-dd}.txt"); + + var message = formatter(state, exception); + var logMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{logLevel}] [{categoryName}] {message}"; + + if (exception != null) + logMessage += Environment.NewLine + exception; + + try + { + lock (logDirectory) + { + Directory.CreateDirectory(logDirectory); + File.AppendAllText(logFilePath, logMessage + Environment.NewLine); + } + } + catch + { + // Silently fail if logging to file fails + } + } + } + + private class FileLoggerProvider(string logDirectory, LogLevel minimumLevel) : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) => new FileLogger(logDirectory, categoryName, minimumLevel); + + public void Dispose() { } + } } \ No newline at end of file diff --git a/LANCommander.Server/Startup/Razor.cs b/LANCommander.Server/Startup/Razor.cs index 8496cd5d..ae663dbf 100644 --- a/LANCommander.Server/Startup/Razor.cs +++ b/LANCommander.Server/Startup/Razor.cs @@ -1,13 +1,9 @@ -using Serilog; - namespace LANCommander.Server.Startup; public static class Razor { public static WebApplicationBuilder AddRazor(this WebApplicationBuilder builder) { - Log.Debug("Configuring MVC and Blazor"); - builder.Services .AddMvc(static options => options.EnableEndpointRouting = false) .AddRazorOptions(static options => diff --git a/LANCommander.Server/Startup/Services.cs b/LANCommander.Server/Startup/Services.cs index ff2c2bac..9566e6b4 100644 --- a/LANCommander.Server/Startup/Services.cs +++ b/LANCommander.Server/Startup/Services.cs @@ -4,7 +4,6 @@ using LANCommander.Server.Providers; using LANCommander.Server.Services.Abstractions; using LANCommander.Server.Services.Extensions; using LANCommander.UI.Extensions; -using Serilog; namespace LANCommander.Server.Startup; @@ -12,8 +11,6 @@ public static class Services { public static WebApplicationBuilder AddLANCommanderServices(this WebApplicationBuilder builder) { - Log.Debug("Registering services"); - builder.Services.AddLANCommanderClient(); builder.Services.AddLANCommanderServer(); builder.Services.AddLANCommanderImportExport(); diff --git a/LANCommander.Server/UI/Pages/Settings/Logs/Components/LogViewer.razor b/LANCommander.Server/UI/Pages/Settings/Logs/Components/LogViewer.razor index bf775079..dd6ecd56 100644 --- a/LANCommander.Server/UI/Pages/Settings/Logs/Components/LogViewer.razor +++ b/LANCommander.Server/UI/Pages/Settings/Logs/Components/LogViewer.razor @@ -1,6 +1,5 @@ @using LANCommander.Server.Logging @using Microsoft.AspNetCore.SignalR.Client -@using Serilog.Events @using XtermBlazor @inject ILogger Logger @inject MessageService MessageService @@ -52,7 +51,7 @@ .WithUrl(NavigationManager.ToAbsoluteUri("/logging")) .Build(); - HubConnection.On("Log", (message, level, timestamp) => + HubConnection.On("Log", (message, level, timestamp) => { var parts = new string[] { @@ -74,16 +73,16 @@ await HubConnection.StartAsync(); } - string GetColorCode(LogEventLevel level) + string GetColorCode(Microsoft.Extensions.Logging.LogLevel level) { return level switch { - LogEventLevel.Verbose => TerminalColor.BrightBlack, - LogEventLevel.Debug => TerminalColor.BrightCyan, - LogEventLevel.Information => TerminalColor.BrightGreen, - LogEventLevel.Warning => TerminalColor.BrightYellow, - LogEventLevel.Error => TerminalColor.BrightRed, - LogEventLevel.Fatal => TerminalColor.BrightMagenta, + Microsoft.Extensions.Logging.LogLevel.Trace => TerminalColor.BrightBlack, + Microsoft.Extensions.Logging.LogLevel.Debug => TerminalColor.BrightCyan, + Microsoft.Extensions.Logging.LogLevel.Information => TerminalColor.BrightGreen, + Microsoft.Extensions.Logging.LogLevel.Warning => TerminalColor.BrightYellow, + Microsoft.Extensions.Logging.LogLevel.Error => TerminalColor.BrightRed, + Microsoft.Extensions.Logging.LogLevel.Critical => TerminalColor.BrightMagenta, _ => TerminalColor.Default, }; }