From 717efe67817cd6f7283affd93d0a62f67ae572ce Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Fri, 19 Dec 2025 09:35:23 +0300 Subject: [PATCH] feat: add logs formatting --- GSS2.Test/ConsoleFormatter.cs | 111 ++++++++++++++++++++++++++++++++ GSS2.Test/FileLogger.cs | 46 +++++++++++++ GSS2.Test/FileLoggerProvider.cs | 14 ++++ GSS2.Test/LibCameraLogSink.cs | 88 +++++++++++++++++++++++++ GSS2.Test/Program.cs | 8 +++ GSS2.Test/appsettings.json | 13 +++- 6 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 GSS2.Test/ConsoleFormatter.cs create mode 100644 GSS2.Test/FileLogger.cs create mode 100644 GSS2.Test/FileLoggerProvider.cs create mode 100644 GSS2.Test/LibCameraLogSink.cs diff --git a/GSS2.Test/ConsoleFormatter.cs b/GSS2.Test/ConsoleFormatter.cs new file mode 100644 index 0000000..2818c56 --- /dev/null +++ b/GSS2.Test/ConsoleFormatter.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace GSS2.Test; + +public sealed class ConsoleFormatter : Microsoft.Extensions.Logging.Console.ConsoleFormatter +{ + private static class Ansi + { + public const string Reset = "\x1b[0m"; + + public static string Color(ConsoleColor color) => color switch + { + ConsoleColor.Black => "\x1b[30m", + ConsoleColor.DarkRed => "\x1b[31m", + ConsoleColor.DarkGreen => "\x1b[32m", + ConsoleColor.DarkYellow => "\x1b[33m", + ConsoleColor.DarkBlue => "\x1b[34m", + ConsoleColor.DarkMagenta => "\x1b[35m", + ConsoleColor.DarkCyan => "\x1b[36m", + ConsoleColor.Gray => "\x1b[37m", + + ConsoleColor.Red => "\x1b[91m", + ConsoleColor.Green => "\x1b[92m", + ConsoleColor.Yellow => "\x1b[93m", + ConsoleColor.Blue => "\x1b[94m", + ConsoleColor.Magenta => "\x1b[95m", + ConsoleColor.Cyan => "\x1b[96m", + ConsoleColor.White => "\x1b[97m", + + _ => Reset + }; + } + + public ConsoleFormatter() : base(nameof(ConsoleFormatter)) + { } + + public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) + { + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var level = logEntry.LogLevel switch + { + LogLevel.Trace => "TRACE", + LogLevel.Debug => "DEBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "ERROR", + LogLevel.Critical => "CRIT", + _ => "NONE", + }; + var category = logEntry.Category; + var eventId = logEntry.EventId.Id; + var eventName = logEntry.EventId.Name; + var message = logEntry.Formatter?.Invoke(logEntry.State, logEntry.Exception); + + if (Console.IsOutputRedirected) + { + textWriter.WriteLine($"[{timestamp}] [{level}] [{category}] {message}"); + } + else + { + var levelColor = Ansi.Color(GetLogLevelColor(logEntry.LogLevel)); + var categoryColor = Ansi.Color(ConsoleColor.Blue); + + textWriter.Write('['); + textWriter.Write(timestamp); + textWriter.Write("] ["); + textWriter.Write(levelColor); + textWriter.Write(level); + textWriter.Write(Ansi.Reset); + textWriter.Write("] ["); + textWriter.Write(categoryColor); + textWriter.Write(category); + textWriter.Write(Ansi.Reset); + textWriter.Write("] ["); + textWriter.Write(eventId); + textWriter.Write("] ["); + textWriter.Write(eventName); + textWriter.Write("] "); + + textWriter.WriteLine(message); + } + + if (logEntry.Exception != null) + { + if (!Console.IsOutputRedirected) + Console.ForegroundColor = ConsoleColor.DarkRed; + + textWriter.WriteLine(logEntry.Exception); + + if (!Console.IsOutputRedirected) + Console.ResetColor(); + } + } + + private static void WriteColored(TextWriter writer, string text, ConsoleColor color) + { + var originalColor = Console.ForegroundColor; + Console.ForegroundColor = color; + writer.Write(text); + Console.ForegroundColor = originalColor; + } + private static ConsoleColor GetLogLevelColor(LogLevel level) => level switch + { + LogLevel.Trace => ConsoleColor.DarkGray, + LogLevel.Debug => ConsoleColor.Gray, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Error => ConsoleColor.Red, + LogLevel.Critical => ConsoleColor.DarkRed, + _ => Console.ForegroundColor + }; +} diff --git a/GSS2.Test/FileLogger.cs b/GSS2.Test/FileLogger.cs new file mode 100644 index 0000000..d15ae60 --- /dev/null +++ b/GSS2.Test/FileLogger.cs @@ -0,0 +1,46 @@ +namespace GSS2.Test; + +public sealed class FileLogger : ILogger +{ + private readonly string _category; + private readonly string _filePath; + private static readonly object _lock = new(); + + public FileLogger(string category, string filePath) + { + _category = category; + _filePath = filePath; + } + +#pragma warning disable CS8633 // Nullability in constraints for type parameter doesn't match the constraints for type parameter in implicitly implemented interface method'. + public IDisposable? BeginScope(TState state) => null; +#pragma warning restore CS8633 // Nullability in constraints for type parameter doesn't match the constraints for type parameter in implicitly implemented interface method'. + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var level = logLevel switch + { + LogLevel.Trace => "TRACE", + LogLevel.Debug => "DEBUG", + LogLevel.Information => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "ERROR", + LogLevel.Critical => "CRIT", + _ => "NONE", + }; + var message = formatter(state, exception); + + var line = $"[{timestamp}] [{level}] [{_category}] [{eventId.Id}] [{eventId.Name}] {message}"; + + lock (_lock) + { + File.AppendAllText(_filePath, line + Environment.NewLine); + + if (exception != null) + File.AppendAllText(_filePath, exception + Environment.NewLine); + } + } +} diff --git a/GSS2.Test/FileLoggerProvider.cs b/GSS2.Test/FileLoggerProvider.cs new file mode 100644 index 0000000..2c367e4 --- /dev/null +++ b/GSS2.Test/FileLoggerProvider.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Logging; + +namespace GSS2.Test; + +public sealed class FileLoggerProvider : ILoggerProvider +{ + private readonly string _filePath; + + public FileLoggerProvider(string filePath) => _filePath = filePath; + + public ILogger CreateLogger(string categoryName) => new FileLogger(categoryName, _filePath); + + public void Dispose() { } +} diff --git a/GSS2.Test/LibCameraLogSink.cs b/GSS2.Test/LibCameraLogSink.cs new file mode 100644 index 0000000..79b4483 --- /dev/null +++ b/GSS2.Test/LibCameraLogSink.cs @@ -0,0 +1,88 @@ +using LibCameraSharp; + +using Microsoft.Extensions.Logging; + +namespace GSS2.Test; + +public sealed class LibCameraLogSink : IDisposable +{ + private readonly ILogger _logger; + private readonly ILogger _loggerCamera; + private readonly ILogger _loggerRPI; + private readonly ILogger _loggerDeviceEnumerator; + private readonly ILogger _loggerIPAProxy; + private readonly ILogger _loggerWRAPPER; + private bool _disposedValue; + + public LibCameraLogSink(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger("LibCamera"); + _loggerCamera = loggerFactory.CreateLogger("LibCamera-Camera"); + _loggerRPI = loggerFactory.CreateLogger("LibCamera-RPI"); + _loggerDeviceEnumerator = loggerFactory.CreateLogger("LibCamera-DeviceEnumerator"); + _loggerIPAProxy = loggerFactory.CreateLogger("LibCamera-IPAProxy"); + _loggerWRAPPER = loggerFactory.CreateLogger("LibCamera-WRAPPER"); + Log.Initialize(); + Log.SetLevel("Camera", Log.LogLevelEnum.DEBUG); + Log.SetLevel("RPI", Log.LogLevelEnum.DEBUG); + Log.SetLevel("DeviceEnumerator", Log.LogLevelEnum.DEBUG); + Log.SetLevel("IPAProxy", Log.LogLevelEnum.DEBUG); + Log.SetLevel("WRAPPER", Log.LogLevelEnum.DEBUG); + Log.LogReceived += OnLogReceived; + } + + private void OnLogReceived(object? sender, Log.LogEventArgs e) + { + if (_disposedValue) + return; + + try + { + var level = MapLogLevel(e.Level); + + var logger = e.Category switch + { + "Camera" => _loggerCamera, + "RPI" => _loggerRPI, + "DeviceEnumerator" => _loggerDeviceEnumerator, + "IPAProxy" => _loggerIPAProxy, + "WRAPPER" => _loggerWRAPPER, + _ => null + }; + + if (logger is not null && logger.IsEnabled(level)) + logger.Log(level, new EventId(0, $"{e.ThreadId} {e.File}"), e, null, static (state, _) => $"{state.Message}"); + else if (logger is null && _logger.IsEnabled(level)) + _logger.Log(level, new EventId(0, $"{e.ThreadId} {e.File}"), e, null, static (state, _) => $"[{state.Category}] {state.Message}"); + } + catch + { } + } + private static LogLevel MapLogLevel(Log.LogLevelEnum level) => level switch + { + Log.LogLevelEnum.DEBUG => LogLevel.Debug, + Log.LogLevelEnum.INFO => LogLevel.Information, + Log.LogLevelEnum.WARN => LogLevel.Warning, + Log.LogLevelEnum.ERROR => LogLevel.Error, + Log.LogLevelEnum.FATAL => LogLevel.Critical, + _ => LogLevel.None + }; + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Log.LogReceived -= OnLogReceived; + } + _disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/GSS2.Test/Program.cs b/GSS2.Test/Program.cs index e7db71c..ed0215e 100644 --- a/GSS2.Test/Program.cs +++ b/GSS2.Test/Program.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using GSS2.Core; using GSS2.Core.Analysis.AmineContent.Database; +using Microsoft.Extensions.Logging.Console; namespace GSS2.Test; @@ -10,6 +11,12 @@ public class Program { var builder = Host.CreateApplicationBuilder(args); + builder.Logging.ClearProviders(); + builder.Logging.AddConsole(options => options.FormatterName = nameof(ConsoleFormatter)); + builder.Logging.AddConsoleFormatter(); + builder.Logging.AddProvider(new FileLoggerProvider("app.log")); + builder.Services.AddSingleton(); + builder.Services.AddAmineContentCalibrationContext(builder.Configuration); builder.Services.AddAmineContentResultsContext(builder.Configuration); @@ -21,6 +28,7 @@ public class Program builder.Services.AddHostedService(); var host = builder.Build(); + host.Services.GetRequiredService(); using (var scope = host.Services.CreateScope()) { diff --git a/GSS2.Test/appsettings.json b/GSS2.Test/appsettings.json index 4deb7fa..214205d 100644 --- a/GSS2.Test/appsettings.json +++ b/GSS2.Test/appsettings.json @@ -2,7 +2,18 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore.Migrations": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning", + "GSS2.Core.Hardware.LightsService": "Information", + "GSS2.Core.Hardware.IlluminatorService":"Information", + "GSS2.Core.Hardware.CameraService":"Information", + "LibCamera": "Information", + "LibCamera-Camera": "Information", + "LibCamera-RPI": "Information", + "LibCamera-DeviceEnumerator": "Information", + "LibCamera-IPAProxy": "Information", + "LibCamera-WRAPPER": "Information" } }, "ConnectionStrings": {