From 98d13ebbbb5ce021a961bc624d86a366c838130c Mon Sep 17 00:00:00 2001
From: flashwave <me@flash.moe>
Date: Mon, 28 Apr 2025 12:29:11 +0000
Subject: [PATCH] Improved logging system.

---
 .gitignore                                    |  1 +
 SharpChat.Flashii/FlashiiClient.cs            | 22 ++++++-
 SharpChat.Flashii/SharpChat.Flashii.csproj    |  4 ++
 SharpChat.MariaDB/MariaDBMessageStorage.cs    | 12 ++--
 SharpChat.MariaDB/MariaDBMigrations.cs        |  6 +-
 SharpChat.MariaDB/MariaDBStorage.cs           |  9 ++-
 SharpChat.MariaDB/SharpChat.MariaDB.csproj    |  1 +
 SharpChat.SQLite/SQLiteConnection.cs          |  1 -
 SharpChat.SQLite/SQLiteMessageStorage.cs      | 12 ++--
 SharpChat.SQLite/SQLiteMigrations.cs          |  6 +-
 SharpChat.SQLite/SQLiteStorage.cs             |  9 ++-
 SharpChat.SQLite/SharpChat.SQLite.csproj      |  1 +
 .../C2SPacketHandlers/AuthC2SPacketHandler.cs |  7 +-
 .../SendMessageC2SPacketHandler.cs            |  4 --
 .../ShutdownRestartClientCommand.cs           |  3 +-
 SharpChat/Connection.cs                       | 44 +++----------
 SharpChat/Context.cs                          | 17 +++--
 SharpChat/Program.cs                          | 65 +++++++++++++++----
 SharpChat/SharpChat.csproj                    |  1 +
 SharpChat/SharpChatWebSocketServer.cs         | 28 ++++----
 SharpChat/SockChatServer.cs                   | 53 ++++++++++++---
 SharpChatCommon/Configuration/CachedValue.cs  |  1 -
 SharpChatCommon/Logger.cs                     | 33 ----------
 SharpChatCommon/SharpChatCommon.csproj        |  4 ++
 24 files changed, 202 insertions(+), 142 deletions(-)
 delete mode 100644 SharpChatCommon/Logger.cs

diff --git a/.gitignore b/.gitignore
index 215570f..8b2dbd5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ sharpchat.db
 sharpchat.db-wal
 sharpchat.db-shm
 SharpChat/version.txt
+logs/
 
 # User-specific files
 *.suo
diff --git a/SharpChat.Flashii/FlashiiClient.cs b/SharpChat.Flashii/FlashiiClient.cs
index c4675c2..ffac01b 100644
--- a/SharpChat.Flashii/FlashiiClient.cs
+++ b/SharpChat.Flashii/FlashiiClient.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.Logging;
 using SharpChat.Auth;
 using SharpChat.Bans;
 using SharpChat.Configuration;
@@ -5,10 +6,11 @@ using System.Net;
 using System.Security.Cryptography;
 using System.Text;
 using System.Text.Json;
+using ZLogger;
 
 namespace SharpChat.Flashii;
 
-public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, BansClient {
+public class FlashiiClient(ILogger logger, HttpClient httpClient, Config config) : AuthClient, BansClient {
     private const string DEFAULT_BASE_URL = "https://flashii.net/_sockchat";
     private readonly CachedValue<string> BaseURL = config.ReadCached("url", DEFAULT_BASE_URL);
 
@@ -28,6 +30,9 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
     private const string AUTH_VERIFY_SIG = "verify#{0}#{1}#{2}";
 
     public async Task<AuthResult> AuthVerify(IPAddress remoteAddr, string scheme, string token) {
+        logger.ZLogInformation($"Verifying authentication data for {remoteAddr}...");
+        logger.ZLogTrace($"AuthVerify({remoteAddr}, {scheme}, {token})");
+
         string remoteAddrStr = remoteAddr.ToString();
 
         HttpRequestMessage request = new(HttpMethod.Post, string.Format(AUTH_VERIFY_URL, BaseURL)) {
@@ -42,6 +47,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         };
 
         using HttpResponseMessage response = await httpClient.SendAsync(request);
+        logger.ZLogTrace($"AuthVerify() -> HTTP {response.StatusCode}");
         response.EnsureSuccessStatusCode();
 
         using Stream stream = await response.Content.ReadAsStreamAsync();
@@ -55,6 +61,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
     private const string AUTH_BUMP_USERS_ONLINE_URL = "{0}/bump";
 
     public async Task AuthBumpUsersOnline(IEnumerable<(IPAddress remoteAddr, string userId)> entries) {
+        logger.ZLogInformation($"Bumping online users list...");
         if(!entries.Any())
             return;
 
@@ -80,6 +87,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         };
 
         using HttpResponseMessage response = await httpClient.SendAsync(request);
+        logger.ZLogTrace($"AuthBumpUsersOnline() -> HTTP {response.StatusCode}");
         response.EnsureSuccessStatusCode();
     }
 
@@ -95,6 +103,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         IPAddress? issuerRemoteAddr = null,
         string? issuerUserId = null
     ) {
+        logger.ZLogInformation($"Creating ban of kind {kind} with duration {duration} for {remoteAddr}/{userId} issued by {issuerRemoteAddr}/{issuerUserId}...");
         if(duration <= TimeSpan.Zero || kind != BanKind.User)
             return;
 
@@ -133,7 +142,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         };
 
         using HttpResponseMessage response = await httpClient.SendAsync(request);
-
+        logger.ZLogTrace($"BanCreate() -> HTTP {response.StatusCode}");
         response.EnsureSuccessStatusCode();
     }
 
@@ -158,6 +167,8 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
             target = iabi.Address.ToString();
         } else throw new ArgumentException("info argument is set to unsupported implementation", nameof(info));
 
+        logger.ZLogInformation($"Revoking ban of kind {info.Kind} issued on {target}...");
+
         string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
         string url = string.Format(BANS_REVOKE_URL, BaseURL, Uri.EscapeDataString(type), Uri.EscapeDataString(target), Uri.EscapeDataString(now));
         string sig = string.Format(BANS_REVOKE_SIG, now, type, target);
@@ -172,6 +183,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         if(response.StatusCode == HttpStatusCode.NotFound)
             return false;
 
+        logger.ZLogTrace($"BanRevoke() -> HTTP {response.StatusCode}");
         response.EnsureSuccessStatusCode();
 
         return response.StatusCode == HttpStatusCode.NoContent;
@@ -184,6 +196,8 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         userIdOrName ??= "0";
         remoteAddr ??= IPAddress.None;
 
+        logger.ZLogInformation($"Requesting ban info for {remoteAddr}/{userIdOrName}...");
+
         string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
         bool usingUserName = string.IsNullOrEmpty(userIdOrName) || userIdOrName.Any(c => c is < '0' or > '9');
         string remoteAddrStr = remoteAddr.ToString();
@@ -198,6 +212,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         };
 
         using HttpResponseMessage response = await httpClient.SendAsync(request);
+        logger.ZLogTrace($"BanGet() -> HTTP {response.StatusCode}");
         response.EnsureSuccessStatusCode();
 
         using Stream stream = await response.Content.ReadAsStreamAsync();
@@ -214,6 +229,8 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
     private const string BANS_LIST_SIG = "list#{0}";
 
     public async Task<BanInfo[]> BanGetList() {
+        logger.ZLogInformation($"Requesting ban list...");
+
         string now = DateTimeOffset.Now.ToUnixTimeSeconds().ToString();
         string url = string.Format(BANS_LIST_URL, BaseURL, Uri.EscapeDataString(now));
         string sig = string.Format(BANS_LIST_SIG, now);
@@ -225,6 +242,7 @@ public class FlashiiClient(HttpClient httpClient, Config config) : AuthClient, B
         };
 
         using HttpResponseMessage response = await httpClient.SendAsync(request);
+        logger.ZLogTrace($"BanGetList() -> HTTP {response.StatusCode}");
         response.EnsureSuccessStatusCode();
 
         using Stream stream = await response.Content.ReadAsStreamAsync();
diff --git a/SharpChat.Flashii/SharpChat.Flashii.csproj b/SharpChat.Flashii/SharpChat.Flashii.csproj
index a4d46fe..b536779 100644
--- a/SharpChat.Flashii/SharpChat.Flashii.csproj
+++ b/SharpChat.Flashii/SharpChat.Flashii.csproj
@@ -6,6 +6,10 @@
     <Nullable>enable</Nullable>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="ZLogger" Version="2.5.10" />
+  </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\SharpChatCommon\SharpChatCommon.csproj" />
   </ItemGroup>
diff --git a/SharpChat.MariaDB/MariaDBMessageStorage.cs b/SharpChat.MariaDB/MariaDBMessageStorage.cs
index b9b1ad6..417f3d9 100644
--- a/SharpChat.MariaDB/MariaDBMessageStorage.cs
+++ b/SharpChat.MariaDB/MariaDBMessageStorage.cs
@@ -1,10 +1,12 @@
+using Microsoft.Extensions.Logging;
 using MySqlConnector;
 using SharpChat.Messages;
 using System.Text.Json;
+using ZLogger;
 
 namespace SharpChat.MariaDB;
 
-public class MariaDBMessageStorage(MariaDBStorage storage) : MessageStorage {
+public class MariaDBMessageStorage(MariaDBStorage storage, ILogger logger) : MessageStorage {
     public async Task LogMessage(
         long id,
         string type,
@@ -36,7 +38,7 @@ public class MariaDBMessageStorage(MariaDBStorage storage) : MessageStorage {
                 new MySqlParameter("sender_perms", MariaDBUserPermissionsConverter.To(senderPerms))
             );
         } catch(MySqlException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in LogMessage(): {ex}");
         }
     }
 
@@ -48,7 +50,7 @@ public class MariaDBMessageStorage(MariaDBStorage storage) : MessageStorage {
                 new MySqlParameter("id", msg.Id)
             );
         } catch(MySqlException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in DeleteMessage(): {ex}");
         }
     }
 
@@ -92,7 +94,7 @@ public class MariaDBMessageStorage(MariaDBStorage storage) : MessageStorage {
                     return evt;
             }
         } catch(MySqlException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in GetMessage(): {ex}");
         }
 
         return null;
@@ -126,7 +128,7 @@ public class MariaDBMessageStorage(MariaDBStorage storage) : MessageStorage {
                     msgs.Add(evt);
             }
         } catch(MySqlException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in GetMessages(): {ex}");
         }
 
         msgs.Reverse();
diff --git a/SharpChat.MariaDB/MariaDBMigrations.cs b/SharpChat.MariaDB/MariaDBMigrations.cs
index ddae353..dd90c81 100644
--- a/SharpChat.MariaDB/MariaDBMigrations.cs
+++ b/SharpChat.MariaDB/MariaDBMigrations.cs
@@ -1,15 +1,17 @@
+using Microsoft.Extensions.Logging;
 using MySqlConnector;
+using ZLogger;
 
 namespace SharpChat.MariaDB;
 
-public class MariaDBMigrations(MariaDBConnection conn) {
+public class MariaDBMigrations(ILogger logger, MariaDBConnection conn) {
     private async Task DoMigration(string name, Func<Task> action) {
         bool done = await conn.RunQueryValue<long>(
             "SELECT COUNT(*) FROM sqc_migrations WHERE migration_name = @name",
             new MySqlParameter("name", name)
         ) > 0;
         if(!done) {
-            Logger.Write($"Running migration '{name}'...");
+            logger.ZLogInformation($@"Running migration ""{name}""...");
             await action();
             await conn.RunCommand(
                 "INSERT INTO sqc_migrations (migration_name) VALUES (@name)",
diff --git a/SharpChat.MariaDB/MariaDBStorage.cs b/SharpChat.MariaDB/MariaDBStorage.cs
index 71ccea5..399141f 100644
--- a/SharpChat.MariaDB/MariaDBStorage.cs
+++ b/SharpChat.MariaDB/MariaDBStorage.cs
@@ -1,10 +1,12 @@
+using Microsoft.Extensions.Logging;
 using MySqlConnector;
 using SharpChat.Configuration;
 using SharpChat.Messages;
+using ZLogger;
 
 namespace SharpChat.MariaDB;
 
-public class MariaDBStorage(string connString) : Storage {
+public class MariaDBStorage(ILogger logger, string connString) : Storage {
     public async Task<MariaDBConnection> CreateConnection() {
         MySqlConnection conn = new(connString);
         await conn.OpenAsync();
@@ -12,12 +14,13 @@ public class MariaDBStorage(string connString) : Storage {
     }
 
     public MessageStorage CreateMessageStorage() {
-        return new MariaDBMessageStorage(this);
+        return new MariaDBMessageStorage(this, logger);
     }
 
     public async Task UpgradeStorage() {
+        logger.ZLogInformation($"Upgrading storage...");
         using MariaDBConnection conn = await CreateConnection();
-        await new MariaDBMigrations(conn).RunMigrations();
+        await new MariaDBMigrations(logger, conn).RunMigrations();
     }
 
     public static string BuildConnectionString(Config config) {
diff --git a/SharpChat.MariaDB/SharpChat.MariaDB.csproj b/SharpChat.MariaDB/SharpChat.MariaDB.csproj
index 25c0291..21b366a 100644
--- a/SharpChat.MariaDB/SharpChat.MariaDB.csproj
+++ b/SharpChat.MariaDB/SharpChat.MariaDB.csproj
@@ -8,6 +8,7 @@
 
   <ItemGroup>
     <PackageReference Include="MySqlConnector" Version="2.4.0" />
+    <PackageReference Include="ZLogger" Version="2.5.10" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/SharpChat.SQLite/SQLiteConnection.cs b/SharpChat.SQLite/SQLiteConnection.cs
index e611c04..1d9da94 100644
--- a/SharpChat.SQLite/SQLiteConnection.cs
+++ b/SharpChat.SQLite/SQLiteConnection.cs
@@ -1,4 +1,3 @@
-using System.Data;
 using System.Data.Common;
 using System.Data.SQLite;
 using NativeSQLiteConnection = System.Data.SQLite.SQLiteConnection;
diff --git a/SharpChat.SQLite/SQLiteMessageStorage.cs b/SharpChat.SQLite/SQLiteMessageStorage.cs
index 7e10924..c5bae50 100644
--- a/SharpChat.SQLite/SQLiteMessageStorage.cs
+++ b/SharpChat.SQLite/SQLiteMessageStorage.cs
@@ -1,13 +1,15 @@
+using Microsoft.Extensions.Logging;
 using SharpChat.Messages;
 using System.Data;
 using System.Data.Common;
 using System.Data.SQLite;
 using System.Text;
 using System.Text.Json;
+using ZLogger;
 
 namespace SharpChat.SQLite;
 
-public class SQLiteMessageStorage(SQLiteConnection conn) : MessageStorage {
+public class SQLiteMessageStorage(ILogger logger, SQLiteConnection conn) : MessageStorage {
     public async Task LogMessage(
         long id,
         string type,
@@ -37,7 +39,7 @@ public class SQLiteMessageStorage(SQLiteConnection conn) : MessageStorage {
                 new SQLiteParameter("data", data == null ? "{}" : JsonSerializer.SerializeToUtf8Bytes(data))
             );
         } catch(SQLiteException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in LogMessage(): {ex}");
         }
     }
 
@@ -48,7 +50,7 @@ public class SQLiteMessageStorage(SQLiteConnection conn) : MessageStorage {
                 new SQLiteParameter("id", msg.Id)
             );
         } catch(SQLiteException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in DeleteMessage(): {ex}");
         }
     }
 
@@ -86,7 +88,7 @@ public class SQLiteMessageStorage(SQLiteConnection conn) : MessageStorage {
                     return evt;
             }
         } catch(SQLiteException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in GetMessage(): {ex}");
         }
 
         return null;
@@ -116,7 +118,7 @@ public class SQLiteMessageStorage(SQLiteConnection conn) : MessageStorage {
                     msgs.Add(evt);
             }
         } catch(SQLiteException ex) {
-            Logger.Write(ex);
+            logger.ZLogError($"Error in GetMessages(): {ex}");
         }
 
         msgs.Reverse();
diff --git a/SharpChat.SQLite/SQLiteMigrations.cs b/SharpChat.SQLite/SQLiteMigrations.cs
index 1dcc464..234c7a6 100644
--- a/SharpChat.SQLite/SQLiteMigrations.cs
+++ b/SharpChat.SQLite/SQLiteMigrations.cs
@@ -1,12 +1,16 @@
+using Microsoft.Extensions.Logging;
+using ZLogger;
+
 namespace SharpChat.SQLite;
 
-public class SQLiteMigrations(SQLiteConnection conn) {
+public class SQLiteMigrations(ILogger logger, SQLiteConnection conn) {
     public async Task RunMigrations() {
         long currentVersion = await conn.RunQueryValue<long>("PRAGMA user_version");
         long version = currentVersion;
 
         async Task doMigration(int expect, Func<Task> action) {
             if(version < expect) {
+                logger.ZLogInformation($"Upgrading to version {version}...");
                 await action();
                 ++version;
             }
diff --git a/SharpChat.SQLite/SQLiteStorage.cs b/SharpChat.SQLite/SQLiteStorage.cs
index b0eec3f..af22136 100644
--- a/SharpChat.SQLite/SQLiteStorage.cs
+++ b/SharpChat.SQLite/SQLiteStorage.cs
@@ -1,22 +1,25 @@
+using Microsoft.Extensions.Logging;
 using SharpChat.Configuration;
 using SharpChat.Messages;
 using System.Data.SQLite;
+using ZLogger;
 using NativeSQLiteConnection = System.Data.SQLite.SQLiteConnection;
 
 namespace SharpChat.SQLite;
 
-public class SQLiteStorage(string connString) : Storage, IDisposable {
+public class SQLiteStorage(ILogger logger, string connString) : Storage, IDisposable {
     public const string MEMORY = "file::memory:?cache=shared";
     public const string DEFAULT = "sharpchat.db";
 
     public SQLiteConnection Connection { get; } = new SQLiteConnection(new NativeSQLiteConnection(connString).OpenAndReturn());
 
     public MessageStorage CreateMessageStorage() {
-        return new SQLiteMessageStorage(Connection);
+        return new SQLiteMessageStorage(logger, Connection);
     }
 
     public async Task UpgradeStorage() {
-        await new SQLiteMigrations(Connection).RunMigrations();
+        logger.ZLogInformation($"Upgrading storage...");
+        await new SQLiteMigrations(logger, Connection).RunMigrations();
     }
 
     public static string BuildConnectionString(Config config) {
diff --git a/SharpChat.SQLite/SharpChat.SQLite.csproj b/SharpChat.SQLite/SharpChat.SQLite.csproj
index b47e6ba..13d5133 100644
--- a/SharpChat.SQLite/SharpChat.SQLite.csproj
+++ b/SharpChat.SQLite/SharpChat.SQLite.csproj
@@ -8,6 +8,7 @@
 
   <ItemGroup>
     <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
+    <PackageReference Include="ZLogger" Version="2.5.10" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs
index 7c4d1d4..1928faf 100644
--- a/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs
+++ b/SharpChat/C2SPacketHandlers/AuthC2SPacketHandler.cs
@@ -5,6 +5,7 @@ using SharpChat.Configuration;
 using SharpChat.Messages;
 using SharpChat.Snowflake;
 using SharpChat.SockChat.S2CPackets;
+using ZLogger;
 
 namespace SharpChat.C2SPacketHandlers;
 
@@ -47,7 +48,7 @@ public class AuthC2SPacketHandler(
 
             BanInfo? banInfo = await bansClient.BanGet(authResult.UserId, ctx.Connection.RemoteAddress);
             if(banInfo is not null) {
-                Logger.Write($"<{ctx.Connection.Id}> User is banned.");
+                ctx.Connection.Logger.ZLogInformation($"User {authResult.UserId} is banned.");
                 await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Banned, banInfo.IsPermanent ? DateTimeOffset.MaxValue : banInfo.ExpiresAt));
                 ctx.Connection.Dispose();
                 return;
@@ -139,12 +140,12 @@ public class AuthC2SPacketHandler(
                 ctx.Chat.ContextAccess.Release();
             }
         } catch(AuthFailedException ex) {
-            Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
+            ctx.Connection.Logger.ZLogWarning($"Failed to authenticate (expected): {ex}");
             await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.AuthInvalid));
             ctx.Connection.Dispose();
             throw;
         } catch(Exception ex) {
-            Logger.Write($"<{ctx.Connection.Id}> Failed to authenticate: {ex}");
+            ctx.Connection.Logger.ZLogError($"Failed to authenticate (unexpected): {ex}");
             await ctx.Connection.Send(new AuthFailS2CPacket(AuthFailS2CPacket.Reason.Exception));
             ctx.Connection.Dispose();
             throw;
diff --git a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs
index d757ecb..c69d137 100644
--- a/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs
+++ b/SharpChat/C2SPacketHandlers/SendMessageC2SPacketHandler.cs
@@ -58,10 +58,6 @@ public class SendMessageC2SPacketHandler(
 
             messageText = messageText.Trim();
 
-#if DEBUG
-            Logger.Write($"<{ctx.Connection.Id} {user.UserName}> {messageText}");
-#endif
-
             if(messageText.StartsWith('/')) {
                 ClientCommandContext context = new(messageText, ctx.Chat, user, ctx.Connection, channel);
                 foreach(ClientCommand cmd in Commands)
diff --git a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs
index 3c47045..d589f9d 100644
--- a/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs
+++ b/SharpChat/ClientCommands/ShutdownRestartClientCommand.cs
@@ -1,4 +1,5 @@
 using SharpChat.SockChat.S2CPackets;
+using ZLogger;
 
 namespace SharpChat.ClientCommands;
 
@@ -18,7 +19,7 @@ public class ShutdownRestartClientCommand(CancellationTokenSource cancellationTo
         if(cancellationTokenSource.IsCancellationRequested)
             return;
 
-        Logger.Write("Shutdown requested through Sock Chat command...");
+        ctx.Connection.Logger.ZLogInformation($"Shutdown requested through Sock Chat command...");
 
         if(ctx.NameEquals("restart"))
             foreach(Connection conn in ctx.Chat.Connections)
diff --git a/SharpChat/Connection.cs b/SharpChat/Connection.cs
index ccd3d27..aa74e8b 100644
--- a/SharpChat/Connection.cs
+++ b/SharpChat/Connection.cs
@@ -1,48 +1,28 @@
 using Fleck;
+using Microsoft.Extensions.Logging;
 using SharpChat.SockChat;
 using System.Net;
 
 namespace SharpChat;
 
-public class Connection : IDisposable {
-    public const int ID_LENGTH = 20;
+public class Connection(ILogger logger, IWebSocketConnection sock, IPEndPoint remoteEndPoint) : IDisposable {
+    public static readonly TimeSpan SessionTimeOut = TimeSpan.FromMinutes(5);
 
-#if DEBUG
-    public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(1);
-#else
-    public static TimeSpan SessionTimeOut { get; } = TimeSpan.FromMinutes(5);
-#endif
+    public ILogger Logger { get; } = logger;
+    public IWebSocketConnection Socket { get; } = sock;
+    public IPEndPoint RemoteEndPoint { get; } = remoteEndPoint;
 
-    public IWebSocketConnection Socket { get; }
-
-    public string Id { get; }
     public bool IsDisposed { get; private set; }
     public DateTimeOffset LastPing { get; set; } = DateTimeOffset.Now;
     public User? User { get; set; }
 
     private int CloseCode { get; set; } = 1000;
 
-    public IPAddress RemoteAddress { get; }
-    public ushort RemotePort { get; }
+    public IPAddress RemoteAddress => RemoteEndPoint.Address;
+    public ushort RemotePort => (ushort)RemoteEndPoint.Port;
 
     public bool IsAlive => !IsDisposed && !HasTimedOut;
 
-    public Connection(IWebSocketConnection sock) {
-        Socket = sock;
-        Id = RNG.SecureRandomString(ID_LENGTH);
-
-        if(!IPAddress.TryParse(sock.ConnectionInfo.ClientIpAddress, out IPAddress? addr))
-            throw new Exception("Unable to parse remote address?????");
-
-        if(IPAddress.IsLoopback(addr)
-            && sock.ConnectionInfo.Headers.TryGetValue("X-Real-IP", out string? addrStr)
-            && IPAddress.TryParse(addrStr, out IPAddress? realAddr))
-            addr = realAddr;
-
-        RemoteAddress = addr;
-        RemotePort = (ushort)sock.ConnectionInfo.ClientPort;
-    }
-
     public async Task Send(S2CPacket packet) {
         if(!Socket.IsAvailable)
             return;
@@ -79,12 +59,4 @@ public class Connection : IDisposable {
         IsDisposed = true;
         Socket.Close(CloseCode);
     }
-
-    public override string ToString() {
-        return Id;
-    }
-
-    public override int GetHashCode() {
-        return Id.GetHashCode();
-    }
 }
diff --git a/SharpChat/Context.cs b/SharpChat/Context.cs
index fa41251..05d2dd2 100644
--- a/SharpChat/Context.cs
+++ b/SharpChat/Context.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.Logging;
 using SharpChat.Channels;
 using SharpChat.Events;
 using SharpChat.Messages;
@@ -6,6 +7,7 @@ using SharpChat.SockChat;
 using SharpChat.SockChat.S2CPackets;
 using System.Dynamic;
 using System.Net;
+using ZLogger;
 
 namespace SharpChat;
 
@@ -14,6 +16,9 @@ public class Context {
 
     public readonly SemaphoreSlim ContextAccess = new(1, 1);
 
+    public ILoggerFactory LoggerFactory { get; }
+    private readonly ILogger Logger;
+
     public SnowflakeGenerator SnowflakeGenerator { get; } = new();
     public RandomSnowflake RandomSnowflake { get; }
 
@@ -25,7 +30,9 @@ public class Context {
     public Dictionary<string, RateLimiter> UserRateLimiters { get; } = [];
     public Dictionary<string, Channel> UserLastChannel { get; } = [];
 
-    public Context(MessageStorage msgs) {
+    public Context(ILoggerFactory logFactory, MessageStorage msgs) {
+        LoggerFactory = logFactory;
+        Logger = logFactory.CreateLogger("ctx");
         Messages = msgs;
         RandomSnowflake = new(SnowflakeGenerator);
     }
@@ -86,15 +93,15 @@ public class Context {
     public async Task Update() {
         foreach(Connection conn in Connections)
             if(!conn.IsDisposed && conn.HasTimedOut) {
+                conn.Logger.ZLogInformation($"Nuking connection associated with user {conn.User?.UserId ?? "no-one"}");
                 conn.Dispose();
-                Logger.Write($"Nuked connection {conn.Id} associated with {conn.User}.");
             }
 
         Connections.RemoveWhere(conn => conn.IsDisposed);
 
         foreach(User user in Users)
             if(!Connections.Any(conn => conn.User == user)) {
-                Logger.Write($"Timing out {user} (no more connections).");
+                Logger.ZLogInformation($"Timing out user {user.UserId} (no more connections).");
                 await HandleDisconnect(user, UserDisconnectS2CPacket.Reason.TimeOut);
             }
     }
@@ -140,8 +147,6 @@ public class Context {
         UserPermissions? perms = null,
         bool silent = false
     ) {
-        ArgumentNullException.ThrowIfNull(user);
-
         bool hasChanged = false;
         string previousName = string.Empty;
 
@@ -313,8 +318,6 @@ public class Context {
     }
 
     public async Task ForceChannel(User user, Channel? chan = null) {
-        ArgumentNullException.ThrowIfNull(user);
-
         if(chan == null && !UserLastChannel.TryGetValue(user.UserId, out chan))
             throw new ArgumentException("no channel???");
 
diff --git a/SharpChat/Program.cs b/SharpChat/Program.cs
index 0437654..39d0e07 100644
--- a/SharpChat/Program.cs
+++ b/SharpChat/Program.cs
@@ -1,9 +1,12 @@
+using Microsoft.Extensions.Logging;
 using SharpChat;
 using SharpChat.Configuration;
 using SharpChat.Flashii;
 using SharpChat.MariaDB;
 using SharpChat.SQLite;
 using System.Text;
+using ZLogger;
+using ZLogger.Providers;
 
 const string CONFIG = "sharpchat.cfg";
 
@@ -21,6 +24,34 @@ if(SharpInfo.IsDebugBuild) {
 } else
     Console.WriteLine(SharpInfo.VersionStringShort.PadLeft(28, ' '));
 
+using ILoggerFactory logFactory = LoggerFactory.Create(logging => {
+    logging.ClearProviders();
+
+#if DEBUG
+    logging.SetMinimumLevel(LogLevel.Trace);
+#else
+    logging.SetMinimumLevel(LogLevel.Information);
+#endif
+
+    logging.AddZLoggerConsole(opts => {
+        opts.OutputEncodingToUtf8 = true;
+        opts.UsePlainTextFormatter(formatter => {
+            formatter.SetPrefixFormatter($"{0} [{1} {2}] ", (in MessageTemplate template, in LogInfo info) => template.Format(info.Timestamp, info.Category, info.LogLevel));
+        });
+    });
+    logging.AddZLoggerRollingFile(opts => {
+        opts.FilePathSelector = (ts, seqNo) => $"logs/{ts.ToLocalTime():yyyy-MM-dd_HH-mm-ss}_{seqNo:000}.json";
+        opts.RollingInterval = RollingInterval.Day;
+        opts.RollingSizeKB = 1024;
+        opts.FileShared = true;
+        opts.UseJsonFormatter(formatter => {
+            formatter.UseUtcTimestamp = true;
+        });
+    });
+});
+
+ILogger logger = logFactory.CreateLogger("main");
+
 using CancellationTokenSource cts = new();
 
 void exitHandler() {
@@ -28,7 +59,7 @@ void exitHandler() {
         return;
 
     cts.Cancel();
-    Logger.Write("Shutdown requested through console...");
+    logger.ZLogInformation($"Shutdown requested through console...");
 }
 
 AppDomain.CurrentDomain.ProcessExit += (sender, ev) => { exitHandler(); };
@@ -40,7 +71,7 @@ string configFile = CONFIG;
 
 // If the config file doesn't exist and we're using the default path, run the converter
 if(!File.Exists(configFile) && configFile == CONFIG) {
-    Logger.Write("Creating example configuration file...");
+    logger.ZLogInformation($"Creating example configuration file...");
 
     using Stream s = new FileStream(CONFIG, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
     s.SetLength(0);
@@ -112,12 +143,12 @@ if(!File.Exists(configFile) && configFile == CONFIG) {
     sw.Flush();
 }
 
-Logger.Write("Initialising configuration...");
+logger.ZLogInformation($"Initialising configuration...");
 using StreamConfig config = StreamConfig.FromPath(configFile);
 
 if(cts.IsCancellationRequested) return;
 
-Logger.Write("Initialising HTTP client...");
+logger.ZLogInformation($"Initialising HTTP client...");
 using HttpClient httpClient = new(new HttpClientHandler() {
     UseProxy = false,
 });
@@ -125,31 +156,37 @@ httpClient.DefaultRequestHeaders.Add("User-Agent", SharpInfo.ProgramName);
 
 if(cts.IsCancellationRequested) return;
 
-Logger.Write("Initialising Flashii client...");
-FlashiiClient flashii = new(httpClient, config.ScopeTo("msz"));
+logger.ZLogInformation($"Initialising Flashii client...");
+FlashiiClient flashii = new(logFactory.CreateLogger("flashii"), httpClient, config.ScopeTo("msz"));
 
 if(cts.IsCancellationRequested) return;
 
-Logger.Write("Initialising storage...");
+logger.ZLogInformation($"Initialising storage...");
 Storage storage = string.IsNullOrWhiteSpace(config.SafeReadValue("mariadb:host", string.Empty))
-    ? new SQLiteStorage(SQLiteStorage.BuildConnectionString(config.ScopeTo("sqlite")))
-    : new MariaDBStorage(MariaDBStorage.BuildConnectionString(config.ScopeTo("mariadb")));
+    ? new SQLiteStorage(logFactory.CreateLogger("sqlite"), SQLiteStorage.BuildConnectionString(config.ScopeTo("sqlite")))
+    : new MariaDBStorage(logFactory.CreateLogger("mariadb"), MariaDBStorage.BuildConnectionString(config.ScopeTo("mariadb")));
 
 try {
     if(cts.IsCancellationRequested) return;
 
-    Logger.Write("Upgrading storage...");
     await storage.UpgradeStorage();
 
     if(cts.IsCancellationRequested) return;
 
-    Logger.Write("Preparing server...");
-    await new SockChatServer(cts, flashii, flashii, storage.CreateMessageStorage(), config.ScopeTo("chat")).Listen(cts.Token);
+    logger.ZLogInformation($"Preparing server...");
+    await new SockChatServer(
+        logFactory,
+        cts,
+        flashii,
+        flashii,
+        storage.CreateMessageStorage(),
+        config.ScopeTo("chat")
+    ).Listen(cts.Token);
 } finally {
     if(storage is IDisposable disp) {
-        Logger.Write("Cleaning storage...");
+        logger.ZLogInformation($"Cleaning storage...");
         disp.Dispose();
     }
 }
 
-Logger.Write("Exiting...");
+logger.ZLogInformation($"Exiting...");
diff --git a/SharpChat/SharpChat.csproj b/SharpChat/SharpChat.csproj
index e530e41..930df1c 100644
--- a/SharpChat/SharpChat.csproj
+++ b/SharpChat/SharpChat.csproj
@@ -17,6 +17,7 @@
 
   <ItemGroup>
     <PackageReference Include="Fleck" Version="1.2.0" />
+    <PackageReference Include="ZLogger" Version="2.5.10" />
   </ItemGroup>
 
   <Target Name="PreBuild" BeforeTargets="PreBuildEvent">
diff --git a/SharpChat/SharpChatWebSocketServer.cs b/SharpChat/SharpChatWebSocketServer.cs
index 701fc34..92f4f0e 100644
--- a/SharpChat/SharpChatWebSocketServer.cs
+++ b/SharpChat/SharpChatWebSocketServer.cs
@@ -1,12 +1,14 @@
 #nullable disable
 
 using Fleck;
+using Microsoft.Extensions.Logging;
 using System.Net;
 using System.Net.Sockets;
 using System.Runtime.InteropServices;
 using System.Security.Authentication;
 using System.Security.Cryptography.X509Certificates;
 using System.Text;
+using ZLogger;
 
 // Near direct reimplementation of Fleck's WebSocketServer with address reusing
 // Fleck's Socket wrapper doesn't provide any way to do this with the normally provided APIs
@@ -15,12 +17,14 @@ using System.Text;
 namespace SharpChat;
 
 public class SharpChatWebSocketServer : IWebSocketServer {
-
+    private readonly ILogger Logger;
     private readonly string _scheme;
     private readonly IPAddress _locationIP;
     private Action<IWebSocketConnection> _config;
 
-    public SharpChatWebSocketServer(string location, bool supportDualStack = true) {
+    public SharpChatWebSocketServer(ILogger logger, string location, bool supportDualStack = true) {
+        Logger = logger;
+
         Uri uri = new(location);
 
         Port = uri.Port;
@@ -79,17 +83,17 @@ public class SharpChatWebSocketServer : IWebSocketServer {
         ListenerSocket.Bind(ipLocal);
         ListenerSocket.Listen(100);
         Port = ((IPEndPoint)ListenerSocket.LocalEndPoint).Port;
-        FleckLog.Info(string.Format("Server started at {0} (actual port {1})", Location, Port));
+        Logger.ZLogInformation($"Server started at {Location} (actual port {Port})");
         if(_scheme == "wss") {
             if(Certificate == null) {
-                FleckLog.Error("Scheme cannot be 'wss' without a Certificate");
+                Logger.ZLogError($"Scheme cannot be 'wss' without a Certificate");
                 return;
             }
 
             // makes dotnet shut up, TLS is handled by NGINX anyway
             // if(EnabledSslProtocols == SslProtocols.None) {
             //     EnabledSslProtocols = SslProtocols.Tls;
-            //     FleckLog.Debug("Using default TLS 1.0 security protocol.");
+            //     Logger.ZLogDebug($"Using default TLS 1.0 security protocol.");
             // }
         }
         ListenForClients();
@@ -98,18 +102,18 @@ public class SharpChatWebSocketServer : IWebSocketServer {
 
     private void ListenForClients() {
         ListenerSocket.Accept(OnClientConnect, e => {
-            FleckLog.Error("Listener socket is closed", e);
+            Logger.ZLogError($"Listener socket is closed: {e}");
             if(RestartAfterListenError) {
-                FleckLog.Info("Listener socket restarting");
+                Logger.ZLogInformation($"Listener socket restarting");
                 try {
                     ListenerSocket.Dispose();
                     Socket socket = new(_locationIP.AddressFamily, SocketType.Stream, ProtocolType.IP);
                     socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
                     ListenerSocket = new SocketWrapper(socket);
                     Start(_config);
-                    FleckLog.Info("Listener socket restarted");
+                    Logger.ZLogInformation($"Listener socket restarted");
                 } catch(Exception ex) {
-                    FleckLog.Error("Listener could not be restarted", ex);
+                    Logger.ZLogError($"Listener could not be restarted: {ex}");
                 }
             }
         });
@@ -118,7 +122,7 @@ public class SharpChatWebSocketServer : IWebSocketServer {
     private void OnClientConnect(ISocket clientSocket) {
         if(clientSocket == null) return; // socket closed
 
-        FleckLog.Debug(string.Format("Client connected from {0}:{1}", clientSocket.RemoteIpAddress, clientSocket.RemotePort.ToString()));
+        Logger.ZLogDebug($"Client connected from {clientSocket.RemoteIpAddress}:{clientSocket.RemotePort}");
         ListenForClients();
 
         WebSocketConnection connection = null;
@@ -154,12 +158,12 @@ public class SharpChatWebSocketServer : IWebSocketServer {
             s => SubProtocolNegotiator.Negotiate(SupportedSubProtocols, s));
 
         if(IsSecure) {
-            FleckLog.Debug("Authenticating Secure Connection");
+            Logger.ZLogDebug($"Authenticating Secure Connection");
             clientSocket
                 .Authenticate(Certificate,
                               EnabledSslProtocols,
                               connection.StartReceiving,
-                              e => FleckLog.Warn("Failed to Authenticate", e));
+                              e => Logger.ZLogWarning($"Failed to Authenticate: {e}"));
         } else {
             connection.StartReceiving();
         }
diff --git a/SharpChat/SockChatServer.cs b/SharpChat/SockChatServer.cs
index d4d49b2..599db67 100644
--- a/SharpChat/SockChatServer.cs
+++ b/SharpChat/SockChatServer.cs
@@ -1,4 +1,4 @@
-using Fleck;
+using Microsoft.Extensions.Logging;
 using SharpChat.Auth;
 using SharpChat.Bans;
 using SharpChat.C2SPacketHandlers;
@@ -7,6 +7,7 @@ using SharpChat.Configuration;
 using SharpChat.Messages;
 using SharpChat.SockChat.S2CPackets;
 using System.Net;
+using ZLogger;
 
 namespace SharpChat;
 
@@ -19,6 +20,8 @@ public class SockChatServer {
 
     public Context Context { get; }
 
+    private readonly ILogger Logger;
+
     private readonly BansClient BansClient;
 
     private readonly CachedValue<ushort> Port;
@@ -34,24 +37,29 @@ public class SockChatServer {
     private static readonly string[] DEFAULT_CHANNELS = ["lounge"];
 
     public SockChatServer(
+        ILoggerFactory logFactory,
         CancellationTokenSource cancellationTokenSource,
         AuthClient authClient,
         BansClient bansClient,
         MessageStorage msgStorage,
         Config config
     ) {
-        Logger.Write("Initialising Sock Chat server...");
+        Logger = logFactory.CreateLogger("sockchat");
+        Logger.ZLogInformation($"Initialising Sock Chat server...");
 
         BansClient = bansClient;
 
+        Logger.ZLogDebug($"Fetching configuration values...");
         Port = config.ReadCached("port", DEFAULT_PORT);
         MaxMessageLength = config.ReadCached("msgMaxLength", DEFAULT_MSG_LENGTH_MAX);
         MaxConnections = config.ReadCached("connMaxCount", DEFAULT_MAX_CONNECTIONS);
         FloodKickLength = config.ReadCached("floodKickLength", DEFAULT_FLOOD_KICK_LENGTH);
         FloodKickExemptRank = config.ReadCached("floodKickExemptRank", DEFAULT_FLOOD_KICK_EXEMPT_RANK);
 
-        Context = new Context(msgStorage ?? throw new ArgumentNullException(nameof(msgStorage)));
+        Logger.ZLogDebug($"Creating context...");
+        Context = new Context(logFactory, msgStorage ?? throw new ArgumentNullException(nameof(msgStorage)));
 
+        Logger.ZLogDebug($"Creating channel list...");
         string[]? channelNames = config.ReadValue("channels", DEFAULT_CHANNELS);
         if(channelNames is not null)
             foreach(string channelName in channelNames) {
@@ -68,6 +76,7 @@ public class SockChatServer {
                 );
             }
 
+        Logger.ZLogDebug($"Registering unauthenticated handlers...");
         GuestHandlers.Add(new AuthC2SPacketHandler(
             authClient,
             bansClient,
@@ -77,11 +86,13 @@ public class SockChatServer {
             MaxConnections
         ));
 
+        Logger.ZLogDebug($"Registering authenticated handlers...");
         AuthedHandlers.AddRange([
             new PingC2SPacketHandler(authClient),
             SendMessageHandler = new SendMessageC2SPacketHandler(Context.RandomSnowflake, MaxMessageLength),
         ]);
 
+        Logger.ZLogDebug($"Registering client commands...");
         SendMessageHandler.AddCommands([
             new AFKClientCommand(),
             new NickClientCommand(),
@@ -105,14 +116,31 @@ public class SockChatServer {
     }
 
     public async Task Listen(CancellationToken cancellationToken) {
-        using IWebSocketServer server = new SharpChatWebSocketServer($"ws://0.0.0.0:{Port}");
+        using SharpChatWebSocketServer server = new(Context.LoggerFactory.CreateLogger("sockchat:server"), $"ws://0.0.0.0:{Port}");
         server.Start(sock => {
+            if(!IPAddress.TryParse(sock.ConnectionInfo.ClientIpAddress, out IPAddress? addr)) {
+                Logger.ZLogError($@"A client attempted to connect with an invalid IP address: ""{sock.ConnectionInfo.ClientIpAddress}""");
+                sock.Close(1011);
+                return;
+            }
+
+            if(IPAddress.IsLoopback(addr) && sock.ConnectionInfo.Headers.TryGetValue("X-Real-IP", out string? addrStr)) {
+                if(IPAddress.TryParse(addrStr, out IPAddress? realAddr))
+                    addr = realAddr;
+                else
+                    Logger.ZLogWarning($@"Connection originated from loopback and supplied an X-Real-IP header, but it could not be parsed: ""{addrStr}""");
+            }
+
+            IPEndPoint endPoint = new(addr, sock.ConnectionInfo.ClientPort);
+
             if(cancellationToken.IsCancellationRequested) {
+                Logger.ZLogInformation($"{endPoint} attepted to connect after shutdown was requested. Connection will be dropped.");
                 sock.Close(1013);
                 return;
             }
 
-            Connection conn = new(sock);
+            ILogger logger = Context.LoggerFactory.CreateLogger($"sockchat:({endPoint})");
+            Connection conn = new(logger, sock, endPoint);
             Context.Connections.Add(conn);
 
             sock.OnOpen = () => OnOpen(conn).Wait();
@@ -121,25 +149,27 @@ public class SockChatServer {
             sock.OnMessage = msg => OnMessage(conn, msg).Wait();
         });
 
-        Logger.Write("Listening...");
+        Logger.ZLogInformation($"Listening...");
         await Task.Delay(Timeout.Infinite, cancellationToken).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
 
+        Logger.ZLogDebug($"Disposing all clients...");
         foreach(Connection conn in Context.Connections)
             conn.Dispose();
     }
 
     private async Task OnOpen(Connection conn) {
-        Logger.Write($"Connection opened from {conn.RemoteAddress}:{conn.RemotePort}");
+        conn.Logger.ZLogInformation($"Connection opened.");
         await Context.SafeUpdate();
     }
 
     private async Task OnError(Connection conn, Exception ex) {
-        Logger.Write($"[{conn.Id} {conn.RemoteAddress}] {ex}");
+        conn.Logger.ZLogError($"Error: {ex.Message}");
+        conn.Logger.ZLogDebug($"{ex}");
         await Context.SafeUpdate();
     }
 
     private async Task OnClose(Connection conn) {
-        Logger.Write($"Connection closed from {conn.RemoteAddress}:{conn.RemotePort}");
+        conn.Logger.ZLogInformation($"Connection closed.");
 
         Context.ContextAccess.Wait();
         try {
@@ -155,6 +185,8 @@ public class SockChatServer {
     }
 
     private async Task OnMessage(Connection conn, string msg) {
+        conn.Logger.ZLogTrace($"Received: {msg}");
+
         await Context.SafeUpdate();
 
         // this doesn't affect non-authed connections?????
@@ -178,8 +210,11 @@ public class SockChatServer {
                     banDuration = TimeSpan.FromSeconds(FloodKickLength);
                     banUser = conn.User;
                     banAddr = conn.RemoteAddress.ToString();
+                    conn.Logger.ZLogWarning($"Exceeded flood limit! Issuing ban with duration {banDuration} on {banAddr}/{banUser.UserId}...");
                 } else if(rateLimiter.IsRisky) {
                     banUser = conn.User;
+                    banAddr = conn.RemoteAddress.ToString();
+                    conn.Logger.ZLogWarning($"About to exceed flood limit! Issueing warning to {banAddr}/{banUser.UserId}...");
                 }
 
                 if(banUser is not null) {
diff --git a/SharpChatCommon/Configuration/CachedValue.cs b/SharpChatCommon/Configuration/CachedValue.cs
index 214d42c..27b5b5b 100644
--- a/SharpChatCommon/Configuration/CachedValue.cs
+++ b/SharpChatCommon/Configuration/CachedValue.cs
@@ -15,7 +15,6 @@ public class CachedValue<T>(Config config, string name, TimeSpan lifetime, T? fa
                 if((now - LastRead) >= lifetime) {
                     LastRead = now;
                     CurrentValue = Config.ReadValue(Name, fallback);
-                    Logger.Debug($"Read {Name} ({CurrentValue})");
                 }
             }
             return (T?)CurrentValue;
diff --git a/SharpChatCommon/Logger.cs b/SharpChatCommon/Logger.cs
deleted file mode 100644
index f5603c1..0000000
--- a/SharpChatCommon/Logger.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Diagnostics;
-using System.Text;
-
-namespace SharpChat;
-
-public static class Logger {
-    public static void Write(string str) {
-        Console.WriteLine(string.Format("[{1}] {0}", str, DateTime.Now));
-    }
-
-    public static void Write(byte[] bytes) {
-        Write(Encoding.UTF8.GetString(bytes));
-    }
-
-    public static void Write(object obj) {
-        Write(obj?.ToString() ?? string.Empty);
-    }
-
-    [Conditional("DEBUG")]
-    public static void Debug(string str) {
-        Write(str);
-    }
-
-    [Conditional("DEBUG")]
-    public static void Debug(byte[] bytes) {
-        Write(bytes);
-    }
-
-    [Conditional("DEBUG")]
-    public static void Debug(object obj) {
-        Write(obj);
-    }
-}
diff --git a/SharpChatCommon/SharpChatCommon.csproj b/SharpChatCommon/SharpChatCommon.csproj
index 969c044..c972a18 100644
--- a/SharpChatCommon/SharpChatCommon.csproj
+++ b/SharpChatCommon/SharpChatCommon.csproj
@@ -7,4 +7,8 @@
     <RootNamespace>SharpChat</RootNamespace>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="ZLogger" Version="2.5.10" />
+  </ItemGroup>
+
 </Project>