diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index d354bac..6418f99 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -9,6 +9,7 @@ using HellionChat.Code; using HellionChat.GameFunctions.Types; using HellionChat.Resources; using HellionChat.Util; +using Microsoft.Extensions.Logging; namespace HellionChat; @@ -19,6 +20,7 @@ internal sealed class AutoTellTabsService : IDisposable private readonly Plugin _plugin; private readonly MessageManager _messageManager; private readonly MessageStore _store; + private readonly ILogger _logger; private readonly object _tempTabsLock = new(); // Hard cap on pinned TempTabs so the sidebar doesn't inflate over years @@ -29,11 +31,17 @@ internal sealed class AutoTellTabsService : IDisposable private bool _initialized; - internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store) + internal AutoTellTabsService( + Plugin plugin, + MessageManager messageManager, + MessageStore store, + ILogger logger + ) { _plugin = plugin; _messageManager = messageManager; _store = store; + _logger = logger; } // Derived from the tab list on read. Pin/Unpin/Promote/Logout simply @@ -67,7 +75,7 @@ internal sealed class AutoTellTabsService : IDisposable private void RehydratePinnedTabs() { var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool); - Plugin.LogProxy.Debug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found"); + _logger.LogDebug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found"); foreach (var tab in Plugin.Config.Tabs) { @@ -76,7 +84,7 @@ internal sealed class AutoTellTabsService : IDisposable if (tab.TellTarget is null || !tab.TellTarget.IsSet()) { - Plugin.LogProxy.Warning( + _logger.LogWarning( $"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget " + $"(Name={tab.TellTarget?.Name ?? ""} World={tab.TellTarget?.World ?? 0}). " + "Chat input on this tab will be empty until the partner sends a tell or you /tell manually." @@ -93,7 +101,7 @@ internal sealed class AutoTellTabsService : IDisposable // sees the recent conversation, not a blank tab. PreloadHistory(tab, tab.TellTarget.Name, tab.TellTarget.World, Guid.Empty); - Plugin.LogProxy.Debug( + _logger.LogDebug( $"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}" ); } @@ -130,7 +138,7 @@ internal sealed class AutoTellTabsService : IDisposable if (partner == null) { // Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases) - Plugin.LogProxy.Warning( + _logger.LogWarning( $"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " + $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " + $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " @@ -361,7 +369,7 @@ internal sealed class AutoTellTabsService : IDisposable catch (Exception ex) { // Non-fatal: tab still spawns with visible error notice instead of silent history loss - Plugin.LogProxy.Error(ex, "[AutoTellTabs] History preload failed"); + _logger.LogError(ex, "[AutoTellTabs] History preload failed"); tab.Messages.AddPrune( MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError), MessageManager.MessageDisplayLimit @@ -456,7 +464,7 @@ internal sealed class AutoTellTabsService : IDisposable { if (!tab.IsTempTab || tab.IsPinned) { - Plugin.LogProxy.Debug( + _logger.LogDebug( $"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}" ); return false; @@ -472,7 +480,7 @@ internal sealed class AutoTellTabsService : IDisposable } tab.IsPinned = true; - Plugin.LogProxy.Debug( + _logger.LogDebug( $"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}" ); _plugin.SaveConfig(); @@ -495,7 +503,7 @@ internal sealed class AutoTellTabsService : IDisposable } tab.IsPinned = false; - Plugin.LogProxy.Debug("[Pin] Unpinned tab '{tab.Name}'"); + _logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name); _plugin.SaveConfig(); } @@ -509,9 +517,7 @@ internal sealed class AutoTellTabsService : IDisposable tab.IsTempTab = false; tab.IsPinned = false; tab.TellTarget = TellTarget.Empty(); - Plugin.LogProxy.Debug( - $"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)" - ); + _logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)"); _plugin.SaveConfig(); } } diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index 64f3bd1..fc11a2f 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -14,6 +14,7 @@ using HellionChat.Util; using Lumina.Text.Expressions; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; +using Microsoft.Extensions.Logging; namespace HellionChat; @@ -22,6 +23,7 @@ internal class MessageManager : IAsyncDisposable internal const int MessageDisplayLimit = 10_000; private Plugin Plugin { get; } + private readonly ILogger _logger; internal MessageStore Store { get; } private Dictionary Formats { get; } = []; @@ -48,11 +50,21 @@ internal class MessageManager : IAsyncDisposable // AutoTellTabsService to spawn or refresh temp tabs without coupling. public event Action? MessageProcessed; - internal unsafe MessageManager(Plugin plugin) + internal unsafe MessageManager( + Plugin plugin, + ILogger logger, + ILoggerFactory loggerFactory + ) { Plugin = plugin; + _logger = logger; - Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil, Plugin.LogProxy); + Store = new MessageStore( + DatabasePath(), + Plugin.PlatformUtil, + loggerFactory.CreateLogger(), + loggerFactory + ); PendingMessageThread = new Thread(() => ProcessPendingMessages(PendingThreadCancellationToken.Token) @@ -91,7 +103,7 @@ internal class MessageManager : IAsyncDisposable await Task.Delay(100); if (PendingMessageThread.IsAlive) - Plugin.LogProxy.Warning( + _logger.LogWarning( "PendingMessageThread did not observe cancellation within 10s. " + "Worker remains on background thread; next plugin reload releases it." ); @@ -137,7 +149,7 @@ internal class MessageManager : IAsyncDisposable } catch (Exception ex) { - Plugin.LogProxy.Error(ex, "Error processing pending message"); + _logger.LogError(ex, "Error processing pending message"); } } else @@ -182,12 +194,12 @@ internal class MessageManager : IAsyncDisposable // Mark failed messages as deleted to prevent retry attempts var failedIds = messages.FailedMessageIds(); - Plugin.LogProxy.Info( + _logger.LogInformation( $"Marking {failedIds.Count} messages as deleted due to parse failures" ); foreach (var msgId in messages.FailedMessageIds()) { - Plugin.LogProxy.Debug($"Marking message '{msgId}' as deleted due to parse failure"); + _logger.LogDebug($"Marking message '{msgId}' as deleted due to parse failure"); Store.DeleteMessage(msgId); } } @@ -203,13 +215,13 @@ internal class MessageManager : IAsyncDisposable } catch (Exception ex) { - Plugin.LogProxy.Error(ex, "Error in FilterAllTabs"); + _logger.LogError(ex, "Error in FilterAllTabs"); } // v1.4.9 R3 profiling: Information so the xllog tail surfaces this // without a Debug filter. Belt-and-suspenders for future plugin-load // regressions; remains in place after Sub-Task 3.4 Befund. - Plugin.LogProxy.Information($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms"); + _logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms"); }); } @@ -264,7 +276,7 @@ internal class MessageManager : IAsyncDisposable } catch (Exception ex) { - Plugin.LogProxy.Error(ex, "Error in ContentIdResolver"); + _logger.LogError(ex, "Error in ContentIdResolver"); } } diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs index 21d314a..0cfe180 100644 --- a/HellionChat/MessageStore.cs +++ b/HellionChat/MessageStore.cs @@ -9,6 +9,7 @@ using MessagePack; using MessagePack.Formatters; using MessagePack.Resolvers; using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; using Encoding = System.Text.Encoding; namespace HellionChat; @@ -179,7 +180,8 @@ internal class MessageStore : IDisposable } private readonly IPlatformUtil _platformUtil; - private readonly IPluginLogProxy _logger; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; // Readiness gate for the FTS5 full-text index. Volatile so the DbViewer's // per-frame IsFtsIndexBuilt read sees the flip the moment the bulk-insert @@ -197,11 +199,17 @@ internal class MessageStore : IDisposable // own SqliteConnection via OpenSecondaryConnection. private readonly object _readLock = new(); - internal MessageStore(string dbPath, IPlatformUtil platformUtil, IPluginLogProxy logger) + internal MessageStore( + string dbPath, + IPlatformUtil platformUtil, + ILogger logger, + ILoggerFactory loggerFactory + ) { DbPath = dbPath; _platformUtil = platformUtil; _logger = logger; + _loggerFactory = loggerFactory; Connection = Connect(); Migrate(); InitFtsReadyCache(); @@ -246,7 +254,7 @@ internal class MessageStore : IDisposable conn.Open(); ApplyPragmas(conn); connectSw.Stop(); - _logger.Information($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms"); + _logger.LogInformation($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms"); return conn; } @@ -290,12 +298,12 @@ internal class MessageStore : IDisposable migration(); migrateSw.Stop(); - _logger.Information($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms"); + _logger.LogInformation($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms"); } private void Migrate0() { - _logger.Information("Running migration 0: Creating tables"); + _logger.LogInformation("Running migration 0: Creating tables"); Connection.Execute( @" CREATE TABLE IF NOT EXISTS messages ( @@ -322,7 +330,7 @@ internal class MessageStore : IDisposable private void Migrate1() { - _logger.Information("Running migration 1: Adding Deleted column"); + _logger.LogInformation("Running migration 1: Adding Deleted column"); Connection.Execute( @" ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false; @@ -334,7 +342,7 @@ internal class MessageStore : IDisposable private void Migrate2() { - _logger.Information("Running migration 2: Adding Channel generated column"); + _logger.LogInformation("Running migration 2: Adding Channel generated column"); Connection.Execute( @" ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL; @@ -362,13 +370,15 @@ internal class MessageStore : IDisposable private void Migrate3() { - _logger.Information("Running migration 3: Fix log kinds to fit the new format"); + _logger.LogInformation("Running migration 3: Fix log kinds to fit the new format"); // Recovery for partially-applied Migrate3: schema already in target // shape but user_version was never bumped -- just record and exit. if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code")) { - _logger.Information("Migration 3: schema already migrated, only bumping user_version"); + _logger.LogInformation( + "Migration 3: schema already migrated, only bumping user_version" + ); SetMigrationVersion(3); return; } @@ -398,7 +408,7 @@ internal class MessageStore : IDisposable private void Migrate4() { - _logger.Information("Running migration 4: Add FTS5 virtual table for full-text search"); + _logger.LogInformation("Running migration 4: Add FTS5 virtual table for full-text search"); // Standalone FTS5 table (no content='messages' linking, no content_rowid). // messages.Id is BLOB-PK (Guid), which is incompatible with FTS5's @@ -422,7 +432,7 @@ internal class MessageStore : IDisposable private void SetMigrationVersion(int version) { - _logger.Information($"Setting version {version}"); + _logger.LogInformation($"Setting version {version}"); using var cmd = Connection.CreateCommand(); // PRAGMA does not accept SQLite parameter bindings; version is a // compile-time int from the migration sequence, never user input. @@ -837,7 +847,7 @@ internal class MessageStore : IDisposable // Privacy filter -- drop disallowed ChatTypes before they reach storage. if (!Plugin.Config.IsAllowedForStorage(message.Code.Type)) { - _logger.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}"); + _logger.LogTrace($"Privacy filter dropped message: ChatType={message.Code.Type}"); return; } @@ -941,7 +951,10 @@ internal class MessageStore : IDisposable if (to is not null) cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds()); - return new MessageEnumerator(cmd.ExecuteReader(), _logger); + return new MessageEnumerator( + cmd.ExecuteReader(), + _loggerFactory.CreateLogger() + ); } } @@ -993,7 +1006,10 @@ internal class MessageStore : IDisposable cmd.Parameters.AddWithValue("$Count", count); - return new MessageEnumerator(cmd.ExecuteReader(), _logger); + return new MessageEnumerator( + cmd.ExecuteReader(), + _loggerFactory.CreateLogger() + ); } } @@ -1033,7 +1049,10 @@ internal class MessageStore : IDisposable cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing); var collected = new List(); - using var enumerator = new MessageEnumerator(cmd.ExecuteReader(), _logger); + using var enumerator = new MessageEnumerator( + cmd.ExecuteReader(), + _loggerFactory.CreateLogger() + ); foreach (var message in enumerator) { if (!ChunkUtil.MatchesSender(message, senderName, senderWorld)) @@ -1145,7 +1164,10 @@ internal class MessageStore : IDisposable ((DateTimeOffset)before).ToUnixTimeMilliseconds() ); - return new MessageEnumerator(cmd.ExecuteReader(), _logger); + return new MessageEnumerator( + cmd.ExecuteReader(), + _loggerFactory.CreateLogger() + ); } } @@ -1198,7 +1220,10 @@ internal class MessageStore : IDisposable cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page); cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage); - return new MessageEnumerator(cmd.ExecuteReader(), _logger); + return new MessageEnumerator( + cmd.ExecuteReader(), + _loggerFactory.CreateLogger() + ); } } @@ -1219,14 +1244,14 @@ internal class MessageStore : IDisposable } } -internal class MessageEnumerator(DbDataReader reader, IPluginLogProxy logger) +internal class MessageEnumerator(DbDataReader reader, ILogger logger) : IEnumerable, IDisposable, IAsyncDisposable { private const int MaxErrorLogs = 10; - private readonly IPluginLogProxy _logger = logger; + private readonly ILogger _logger = logger; private readonly List FailedIds = []; private int FailedCount; public bool DidError => FailedCount > 0; @@ -1247,10 +1272,10 @@ internal class MessageEnumerator(DbDataReader reader, IPluginLogProxy logger) catch (Exception e) { if (FailedCount < MaxErrorLogs) - _logger.Error($"Exception while reading message '{id}' from database: {e}"); + _logger.LogError($"Exception while reading message '{id}' from database: {e}"); FailedCount++; if (FailedCount == MaxErrorLogs) - _logger.Error("Further parsing errors will not be logged"); + _logger.LogError("Further parsing errors will not be logged"); if (id != Guid.Empty) FailedIds.Add(id); diff --git a/HellionChat/PluginHostFactory.cs b/HellionChat/PluginHostFactory.cs index a4c3786..af59833 100644 --- a/HellionChat/PluginHostFactory.cs +++ b/HellionChat/PluginHostFactory.cs @@ -121,7 +121,11 @@ internal static class PluginHostFactory sp.GetRequiredService() )); - services.AddSingleton(sp => new MessageManager(sp.GetRequiredService())); + services.AddSingleton(sp => new MessageManager( + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService() + )); // AutoTellTabsService pulls MessageStore through MessageManager.Store // because MessageStore is still allocated inside MessageManager.ctor @@ -131,7 +135,12 @@ internal static class PluginHostFactory { var pluginRef = sp.GetRequiredService(); var manager = sp.GetRequiredService(); - return new AutoTellTabsService(pluginRef, manager, manager.Store); + return new AutoTellTabsService( + pluginRef, + manager, + manager.Store, + sp.GetRequiredService>() + ); }); // -----------------------------------------------------------------