diff --git a/OliverBooth/Logging/ColorfulConsoleTarget.cs b/OliverBooth/Logging/ColorfulConsoleTarget.cs
new file mode 100644
index 0000000..b3185f3
--- /dev/null
+++ b/OliverBooth/Logging/ColorfulConsoleTarget.cs
@@ -0,0 +1,39 @@
+using System.Text;
+using NLog;
+using NLog.Targets;
+using LogLevel = NLog.LogLevel;
+
+namespace OliverBooth.Logging;
+
+///
+/// Represents an NLog target which supports colorful output to stdout.
+///
+internal sealed class ColorfulConsoleTarget : TargetWithLayout
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the log target.
+ public ColorfulConsoleTarget(string name)
+ {
+ Name = name;
+ }
+
+ ///
+ 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();
+ }
+}
diff --git a/OliverBooth/Logging/LogFileTarget.cs b/OliverBooth/Logging/LogFileTarget.cs
new file mode 100644
index 0000000..bc8373f
--- /dev/null
+++ b/OliverBooth/Logging/LogFileTarget.cs
@@ -0,0 +1,40 @@
+using System.Text;
+using NLog;
+using NLog.Targets;
+using OliverBooth.Services;
+
+namespace OliverBooth.Logging;
+
+///
+/// Represents an NLog target which writes its output to a log file on disk.
+///
+internal sealed class LogFileTarget : TargetWithLayout
+{
+ private readonly LoggingService _loggingService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the log target.
+ /// The .
+ public LogFileTarget(string name, LoggingService loggingService)
+ {
+ _loggingService = loggingService;
+ Name = name;
+ }
+
+ ///
+ 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();
+ }
+}
diff --git a/OliverBooth/OliverBooth.csproj b/OliverBooth/OliverBooth.csproj
index 13558d6..8a6f2ac 100644
--- a/OliverBooth/OliverBooth.csproj
+++ b/OliverBooth/OliverBooth.csproj
@@ -15,6 +15,8 @@
+
+
diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs
index f244e99..b120cc1 100644
--- a/OliverBooth/Program.cs
+++ b/OliverBooth/Program.cs
@@ -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();
+
builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
builder.Services.AddControllersWithViews();
builder.Services.AddRouting(options => options.LowercaseUrls = true);
diff --git a/OliverBooth/Services/LoggingService.cs b/OliverBooth/Services/LoggingService.cs
new file mode 100644
index 0000000..fb57ec4
--- /dev/null
+++ b/OliverBooth/Services/LoggingService.cs
@@ -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;
+
+///
+/// Represents a class which implements a logging service that supports multiple log targets.
+///
+///
+/// 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 logs/latest.log.
+///
+internal sealed class LoggingService : BackgroundService
+{
+ private const string LogFileName = "logs/latest.log";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LoggingService()
+ {
+ LogFile = new FileInfo(LogFileName);
+ }
+
+ ///
+ /// Gets or sets the log file.
+ ///
+ /// The log file.
+ public FileInfo LogFile { get; set; }
+
+ ///
+ /// Archives any existing log files.
+ ///
+ 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();
+ }
+
+ ///
+ 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();
+ }
+}