feat: add NLog

This commit is contained in:
Oliver Booth 2023-08-06 15:56:08 +01:00
parent d24f9d3996
commit 518ea1b933
Signed by: oliverbooth
GPG Key ID: 725DB725A0D9EE61
5 changed files with 184 additions and 0 deletions

View File

@ -0,0 +1,39 @@
using System.Text;
using NLog;
using NLog.Targets;
using LogLevel = NLog.LogLevel;
namespace OliverBooth.Logging;
/// <summary>
/// Represents an NLog target which supports colorful output to stdout.
/// </summary>
internal sealed class ColorfulConsoleTarget : TargetWithLayout
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorfulConsoleTarget" /> class.
/// </summary>
/// <param name="name">The name of the log target.</param>
public ColorfulConsoleTarget(string name)
{
Name = name;
}
/// <inheritdoc />
protected override void Write(LogEventInfo logEvent)
{
var message = new StringBuilder();
message.Append(Layout.Render(logEvent));
if (logEvent.Level == LogLevel.Warn)
Console.ForegroundColor = ConsoleColor.Yellow;
else if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Fatal)
Console.ForegroundColor = ConsoleColor.Red;
if (logEvent.Exception is { } exception)
message.Append($": {exception}");
Console.WriteLine(message);
Console.ResetColor();
}
}

View File

@ -0,0 +1,40 @@
using System.Text;
using NLog;
using NLog.Targets;
using OliverBooth.Services;
namespace OliverBooth.Logging;
/// <summary>
/// Represents an NLog target which writes its output to a log file on disk.
/// </summary>
internal sealed class LogFileTarget : TargetWithLayout
{
private readonly LoggingService _loggingService;
/// <summary>
/// Initializes a new instance of the <see cref="LogFileTarget" /> class.
/// </summary>
/// <param name="name">The name of the log target.</param>
/// <param name="loggingService">The <see cref="LoggingService" />.</param>
public LogFileTarget(string name, LoggingService loggingService)
{
_loggingService = loggingService;
Name = name;
}
/// <inheritdoc />
protected override void Write(LogEventInfo logEvent)
{
_loggingService.ArchiveLogFilesAsync(false).GetAwaiter().GetResult();
using FileStream stream = _loggingService.LogFile.Open(FileMode.Append, FileAccess.Write);
using var writer = new StreamWriter(stream, Encoding.UTF8);
writer.Write(Layout.Render(logEvent));
if (logEvent.Exception is { } exception)
writer.Write($": {exception}");
writer.WriteLine();
}
}

View File

@ -15,6 +15,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.9"/>
<PackageReference Include="NLog" Version="5.2.3"/>
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3"/>
<PackageReference Include="X10D" Version="3.2.2"/>
<PackageReference Include="X10D.Hosting" Version="3.2.2"/>
</ItemGroup>

View File

@ -1,5 +1,13 @@
using NLog.Extensions.Logging;
using OliverBooth.Services;
using X10D.Hosting.DependencyInjection;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddNLog();
builder.Services.AddHostedSingleton<LoggingService>();
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
builder.Services.AddControllersWithViews();
builder.Services.AddRouting(options => options.LowercaseUrls = true);

View File

@ -0,0 +1,95 @@
using System.IO.Compression;
using NLog;
using NLog.Config;
using NLog.Layouts;
using OliverBooth.Logging;
using LogLevel = NLog.LogLevel;
namespace OliverBooth.Services;
/// <summary>
/// Represents a class which implements a logging service that supports multiple log targets.
/// </summary>
/// <remarks>
/// This class implements a logging structure similar to that of Minecraft, where historic logs are compressed to a .gz and
/// the latest log is found in <c>logs/latest.log</c>.
/// </remarks>
internal sealed class LoggingService : BackgroundService
{
private const string LogFileName = "logs/latest.log";
/// <summary>
/// Initializes a new instance of the <see cref="LoggingService" /> class.
/// </summary>
public LoggingService()
{
LogFile = new FileInfo(LogFileName);
}
/// <summary>
/// Gets or sets the log file.
/// </summary>
/// <value>The log file.</value>
public FileInfo LogFile { get; set; }
/// <summary>
/// Archives any existing log files.
/// </summary>
public async Task ArchiveLogFilesAsync(bool archiveToday = true)
{
var latestFile = new FileInfo(LogFile.FullName);
if (!latestFile.Exists) return;
DateTime lastWrite = latestFile.LastWriteTime;
string lastWriteDate = $"{lastWrite:yyyy-MM-dd}";
var version = 0;
string name;
if (!archiveToday && lastWrite.Date == DateTime.Today) return;
while (File.Exists(name = Path.Combine(LogFile.Directory!.FullName, $"{lastWriteDate}-{++version}.log.gz")))
{
// body ignored
}
await using (FileStream source = latestFile.OpenRead())
{
await using FileStream output = File.Create(name);
await using var gzip = new GZipStream(output, CompressionMode.Compress);
await source.CopyToAsync(gzip);
}
latestFile.Delete();
}
/// <inheritdoc />
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
LogFile.Directory?.Create();
LogManager.Setup(builder => builder.SetupExtensions(extensions =>
{
extensions.RegisterLayoutRenderer("TheTime", info => info.TimeStamp.ToString("HH:mm:ss"));
extensions.RegisterLayoutRenderer("ServiceName", info => info.LoggerName);
}));
Layout? layout = Layout.FromString("[${TheTime} ${level:uppercase=true}] [${ServiceName}] ${message}");
var config = new LoggingConfiguration();
var fileLogger = new LogFileTarget("FileLogger", this) { Layout = layout };
var consoleLogger = new ColorfulConsoleTarget("ConsoleLogger") { Layout = layout };
#if DEBUG
LogLevel minLevel = LogLevel.Debug;
#else
LogLevel minLevel = LogLevel.Info;
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ENABLE_DEBUG_LOGGING")))
minLevel = LogLevel.Debug;
#endif
config.AddRule(minLevel, LogLevel.Fatal, consoleLogger);
config.AddRule(minLevel, LogLevel.Fatal, fileLogger);
LogManager.Configuration = config;
return ArchiveLogFilesAsync();
}
}