From 268ad49643bc8149f3ab8893c4bfba335396beda Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 09:08:15 +0000 Subject: [PATCH 1/6] Improve error handling and reduce error potential across services - Fix infinite spin-loops in ObtainLock and WaitForAuthUnlockAsync by adding exponential backoff and deadline timeouts to prevent CPU spin and application hangs when Redis is slow or locks are contended - Wrap async void Twitch monitor event handlers (Online/Update/Offline) in try-catch to prevent unhandled exceptions from crashing the process - Guard GetAuthAsync against NullReferenceException when no active auth entry exists in the database; throws with a clear diagnostic message - Fix AddOrUpdateAsync deadlock: wrap the update path in try/finally so the SemaphoreSlim is always released even when SaveChanges throws - Guard RemoveAsync against NullReferenceException when the entity is not found by FindAsync - Add null check for streamUser in StreamUpdateConsumer before accessing Username and other properties - Add null check for monitorUser in MonitorModule.GetStreamUserAsync and throw ArgumentException with a user-visible message - Fix GuildUpdated NullReferenceException by switching from instance .Equals() to static string.Equals() for nullable IconUrl comparison - Add per-guild try-catch in DiscordMemberLiveConsumer so one failing guild does not prevent notifications for all remaining mutual guilds - Dispose CancellationTokenSource in SendDiscordMessage using 'using' to prevent a resource leak on every notification send - Replace bare catch{} blocks in InteractionHandler with typed catches that log warnings/errors for better observability https://claude.ai/code/session_01DGjiqffs4cNf5LX7A3YnLx --- .../Discord/DiscordMemberLiveConsumer.cs | 9 ++ .../Consumers/Streams/StreamOnlineConsumer.cs | 17 ++- .../Consumers/Streams/StreamUpdateConsumer.cs | 7 ++ .../InteractionHandler.cs | 13 ++- .../LiveBotDiscordEventHandlers.cs | 4 +- .../Modules/MonitorModule.cs | 3 + LiveBot.Repository/ModelRepository.cs | 17 ++- LiveBot.Watcher.Twitch/TwitchMonitor.cs | 110 +++++++++++++----- 8 files changed, 137 insertions(+), 43 deletions(-) diff --git a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs index 60cfada..6323d98 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs @@ -70,6 +70,9 @@ public async Task Consume(ConsumeContext context) foreach (var guild in mutualGuilds) { + try + { + if (guild?.Id == null) continue; var discordGuild = await _work.GuildRepository.SingleOrDefaultAsync(i => i.DiscordId == guild.Id); @@ -241,6 +244,12 @@ public async Task Consume(ConsumeContext context) ); } } + + } // end per-guild try + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled error processing member live notification for guild {GuildId}", guild?.Id); + } } } } diff --git a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOnlineConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOnlineConsumer.cs index 69ecb8e..f656e0c 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOnlineConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOnlineConsumer.cs @@ -289,10 +289,22 @@ private StreamNotification CreateStreamNotification(ILiveBotStream stream, Strea private async Task ObtainLock(string recordId, Guid lockGuid, TimeSpan lockTimeout) { + var deadline = DateTime.UtcNow.Add(lockTimeout); + int delayMs = 50; bool obtainedLock; do { obtainedLock = await _cache.ObtainLockAsync(recordId: recordId, identifier: lockGuid, expiryTime: lockTimeout); + if (!obtainedLock) + { + if (DateTime.UtcNow >= deadline) + { + _streamOnlineLogger.LogWarning("Timed out waiting for lock on {RecordId} after {Timeout}s", recordId, lockTimeout.TotalSeconds); + return false; + } + await Task.Delay(delayMs); + delayMs = Math.Min(delayMs * 2, 1000); + } } while (!obtainedLock); @@ -542,14 +554,13 @@ private double CalculateNotificationDelay(StreamNotification streamNotification, private async Task SendDiscordMessage(SocketTextChannel channel, string notificationMessage, Embed embed, TimeSpan lockTimeout) { - CancellationTokenSource cancellationToken = new(); - cancellationToken.CancelAfter((int)lockTimeout.TotalMilliseconds); + using var cts = new CancellationTokenSource((int)lockTimeout.TotalMilliseconds); var messageRequestOptions = new RequestOptions() { RetryMode = RetryMode.AlwaysFail, Timeout = (int)lockTimeout.TotalMilliseconds, - CancelToken = cancellationToken.Token + CancelToken = cts.Token }; return await channel.SendMessageAsync(text: notificationMessage, embed: embed, options: messageRequestOptions); diff --git a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs index 3d83f89..5eab7cd 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs @@ -41,6 +41,13 @@ public async Task Consume(ConsumeContext context) var streamUser = await _work.UserRepository.SingleOrDefaultAsync(i => i.ServiceType == stream.ServiceType && i.SourceID == user.Id); var streamSubscriptions = await _work.SubscriptionRepository.FindAsync(i => i.User == streamUser); + if (streamUser == null) + { + _logger.LogWarning("StreamUser not found for {UserId} on {ServiceType} during stream update; skipping", + user.Id, stream.ServiceType); + return; + } + StreamGame streamGame; if (game.Id == "0" || string.IsNullOrEmpty(game.Id)) { diff --git a/LiveBot.Discord.SlashCommands/InteractionHandler.cs b/LiveBot.Discord.SlashCommands/InteractionHandler.cs index 5a35e0b..547a2ec 100644 --- a/LiveBot.Discord.SlashCommands/InteractionHandler.cs +++ b/LiveBot.Discord.SlashCommands/InteractionHandler.cs @@ -46,7 +46,10 @@ internal async Task ReadyAsync(DiscordSocketClient client) _logger.LogInformation(message: "Finished registering AdminModule with shard {ShardId}", client.ShardId); RegisteredCommands = true; } - catch { } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to register AdminModule on shard {ShardId}", client.ShardId); + } } } @@ -65,8 +68,9 @@ internal async Task HandleInteraction(SocketInteraction interaction) await Task.CompletedTask; } - catch + catch (Exception ex) { + _logger.LogError(ex, "Unhandled exception executing interaction {InteractionType}", interaction.Type); // If Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original // response, or at least let the user know that something went wrong during the command execution. if (interaction.Type is InteractionType.ApplicationCommand) @@ -89,7 +93,10 @@ internal async Task InteractionExecuted(ICommandInfo commandInfo, IInteractionCo await context.Interaction.FollowupAsync(ephemeral: true, embed: embed); } - catch { } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to send error followup for {CommandName}", commandInfo?.Name ?? "Unknown"); + } } var logLevel = LogLevel.Information; diff --git a/LiveBot.Discord.SlashCommands/LiveBotDiscordEventHandlers.cs b/LiveBot.Discord.SlashCommands/LiveBotDiscordEventHandlers.cs index cb6c4aa..602f42c 100644 --- a/LiveBot.Discord.SlashCommands/LiveBotDiscordEventHandlers.cs +++ b/LiveBot.Discord.SlashCommands/LiveBotDiscordEventHandlers.cs @@ -73,8 +73,8 @@ public async Task GuildJoined(SocketGuild guild) public async Task GuildUpdated(SocketGuild beforeGuild, SocketGuild afterGuild) { if ( - beforeGuild.Name.Equals(afterGuild.Name, StringComparison.InvariantCultureIgnoreCase) && - beforeGuild.IconUrl.Equals(afterGuild.IconUrl, StringComparison.InvariantCultureIgnoreCase) + string.Equals(beforeGuild.Name, afterGuild.Name, StringComparison.InvariantCultureIgnoreCase) && + string.Equals(beforeGuild.IconUrl, afterGuild.IconUrl, StringComparison.InvariantCultureIgnoreCase) ) return; var context = new DiscordGuildUpdate { GuildId = afterGuild.Id, GuildName = afterGuild.Name, IconUrl = afterGuild.IconUrl }; diff --git a/LiveBot.Discord.SlashCommands/Modules/MonitorModule.cs b/LiveBot.Discord.SlashCommands/Modules/MonitorModule.cs index fbc46b3..c2f4832 100644 --- a/LiveBot.Discord.SlashCommands/Modules/MonitorModule.cs +++ b/LiveBot.Discord.SlashCommands/Modules/MonitorModule.cs @@ -612,6 +612,9 @@ private ILiveBotMonitor GetMonitor(Uri uri) private async Task GetStreamUserAsync(ILiveBotMonitor monitor, Uri uri) { var monitorUser = await monitor.GetUser(profileURL: uri.AbsoluteUri); + if (monitorUser == null) + throw new ArgumentException($"Could not find a user for the provided URL: {Format.EscapeUrl(uri.AbsoluteUri)}"); + StreamUser streamUser = new() { ServiceType = monitorUser.ServiceType, diff --git a/LiveBot.Repository/ModelRepository.cs b/LiveBot.Repository/ModelRepository.cs index a325a69..152063e 100644 --- a/LiveBot.Repository/ModelRepository.cs +++ b/LiveBot.Repository/ModelRepository.cs @@ -234,11 +234,18 @@ public virtual async Task AddOrUpdateAsync(TEntity entity, Expression @@ -254,6 +261,8 @@ public async Task RemoveAsync(long Id) { await syncLock.WaitAsync().ConfigureAwait(false); TEntity entity = await DbSet.FindAsync(Id).ConfigureAwait(false); + if (entity == null) + return; DbSet.Remove(entity); await Context.SaveChangesAsync().ConfigureAwait(false); } diff --git a/LiveBot.Watcher.Twitch/TwitchMonitor.cs b/LiveBot.Watcher.Twitch/TwitchMonitor.cs index 8d933e6..95d9a27 100644 --- a/LiveBot.Watcher.Twitch/TwitchMonitor.cs +++ b/LiveBot.Watcher.Twitch/TwitchMonitor.cs @@ -124,52 +124,70 @@ public void Monitor_OnServiceStarted(object? sender, OnServiceStartedArgs e) public async void Monitor_OnStreamOnline(object? sender, OnStreamOnlineArgs e) { if (!IsWatcher) return; - - ILiveBotUser? user = await GetUserById(e.Stream.UserId); - if (user == null) + try + { + ILiveBotUser? user = await GetUserById(e.Stream.UserId); + if (user == null) + { + _logger.LogWarning("Could not find user {UserId} for online event", e.Stream.UserId); + return; + } + + ILiveBotGame game = await GetGame(e.Stream.GameId); + ILiveBotStream stream = new TwitchStream(BaseURL, ServiceType, e.Stream, user, game); + + await PublishStreamOnline(stream); + } + catch (Exception ex) { - _logger.LogWarning("Could not find user {UserId} for online event", e.Stream.UserId); - return; + _logger.LogError(ex, "Unhandled exception in Monitor_OnStreamOnline for user {UserId}", e.Stream.UserId); } - - ILiveBotGame game = await GetGame(e.Stream.GameId); - ILiveBotStream stream = new TwitchStream(BaseURL, ServiceType, e.Stream, user, game); - - await PublishStreamOnline(stream); } public async void Monitor_OnStreamUpdate(object? sender, OnStreamUpdateArgs e) { if (!IsWatcher) return; - - ILiveBotUser? user = await GetUserById(e.Stream.UserId); - if (user == null) + try { - _logger.LogWarning("Could not find user {UserId} for update event", e.Stream.UserId); - return; + ILiveBotUser? user = await GetUserById(e.Stream.UserId); + if (user == null) + { + _logger.LogWarning("Could not find user {UserId} for update event", e.Stream.UserId); + return; + } + + ILiveBotGame game = await GetGame(e.Stream.GameId); + ILiveBotStream stream = new TwitchStream(BaseURL, ServiceType, e.Stream, user, game); + + await PublishStreamUpdate(stream); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception in Monitor_OnStreamUpdate for user {UserId}", e.Stream.UserId); } - - ILiveBotGame game = await GetGame(e.Stream.GameId); - ILiveBotStream stream = new TwitchStream(BaseURL, ServiceType, e.Stream, user, game); - - await PublishStreamUpdate(stream); } public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) { if (!IsWatcher) return; - - ILiveBotUser? user = await GetUserById(e.Stream.UserId); - if (user == null) + try { - _logger.LogWarning("Could not find user {UserId} for offline event", e.Stream.UserId); - return; + ILiveBotUser? user = await GetUserById(e.Stream.UserId); + if (user == null) + { + _logger.LogWarning("Could not find user {UserId} for offline event", e.Stream.UserId); + return; + } + + ILiveBotGame game = await GetGame(e.Stream.GameId); + ILiveBotStream stream = new TwitchStream(BaseURL, ServiceType, e.Stream, user, game); + + await PublishStreamOffline(stream); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception in Monitor_OnStreamOffline for user {UserId}", e.Stream.UserId); } - - ILiveBotGame game = await GetGame(e.Stream.GameId); - ILiveBotStream stream = new TwitchStream(BaseURL, ServiceType, e.Stream, user, game); - - await PublishStreamOffline(stream); } #endregion Events @@ -350,15 +368,27 @@ public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) #region Misc Functions /// - /// Wait for an Auth lock to not be in place + /// Wait for an Auth lock to not be in place, with exponential backoff and a timeout /// /// private async Task WaitForAuthUnlockAsync() { + var deadline = DateTime.UtcNow.AddSeconds(30); + int delayMs = 100; bool authLocked; do { authLocked = await _cache.CheckForLockAsync(recordId: _authCacheName); + if (authLocked) + { + if (DateTime.UtcNow >= deadline) + { + _logger.LogWarning("Timed out waiting for auth lock to be released after 30s; proceeding anyway"); + return; + } + await Task.Delay(delayMs); + delayMs = Math.Min(delayMs * 2, 2000); + } } while (authLocked); } @@ -378,6 +408,12 @@ private async Task GetAuthAsync() { var tempAuth = await _work.AuthRepository.SingleOrDefaultAsync(i => i.ServiceType == ServiceType && i.ClientId == ClientId && i.Expired == false); + if (tempAuth == null) + { + _logger.LogError("No active auth entry found for {ServiceType} with ClientId {ClientId}", ServiceType, ClientId); + throw new InvalidOperationException($"No active auth entry found for {ServiceType} with ClientId {ClientId}"); + } + auth = new TwitchAuth { Id = tempAuth.Id, @@ -448,10 +484,22 @@ public async Task UpdateAuth(bool force = false) if (oldAuth.Expired || oldAuth.ExpiresAt <= DateTime.UtcNow || force) { + var lockDeadline = DateTime.UtcNow.AddSeconds(30); + int lockDelayMs = 100; do { // Obtain a lock for a maximum of 30 seconds obtainedLock = await _cache.ObtainLockAsync(recordId: _authCacheName, identifier: lockGuid, expiryTime: TimeSpan.FromSeconds(30)); + if (!obtainedLock) + { + if (DateTime.UtcNow >= lockDeadline) + { + _logger.LogWarning("Timed out waiting for auth lock; skipping token refresh"); + break; + } + await Task.Delay(lockDelayMs); + lockDelayMs = Math.Min(lockDelayMs * 2, 2000); + } } while (!obtainedLock); From 9bc26ade9f5e4d443e399df255948d5620693dac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 15:40:26 +0000 Subject: [PATCH 2/6] Fix StreamUpdateConsumer null checks ordering and user null guard The streamUser null check was placed after SubscriptionRepository.FindAsync which already passed streamUser as a query parameter - a null streamUser would cause EF to throw or return unexpected results. Also add a null guard for `user` itself, since GetUserById can return null when the Twitch API does not find the account. Reorder to: resolve user -> guard null user -> resolve streamUser -> guard null streamUser -> query subscriptions. https://claude.ai/code/session_01DGjiqffs4cNf5LX7A3YnLx --- .../Consumers/Streams/StreamUpdateConsumer.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs index 5eab7cd..2e04674 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs @@ -33,13 +33,19 @@ public async Task Consume(ConsumeContext context) if (monitor == null) return; - ILiveBotUser user = stream.User ?? await monitor.GetUserById(stream.UserId); + ILiveBotUser? user = stream.User ?? await monitor.GetUserById(stream.UserId); + if (user == null) + { + _logger.LogWarning("Could not resolve user for stream {StreamId} on {ServiceType} during stream update; skipping", + stream.Id, stream.ServiceType); + return; + } + ILiveBotGame game = stream.Game ?? await monitor.GetGame(stream.GameId); Expression> templateGamePredicate = (i => i.ServiceType == stream.ServiceType && i.SourceId == "0"); var templateGame = await _work.GameRepository.SingleOrDefaultAsync(templateGamePredicate); var streamUser = await _work.UserRepository.SingleOrDefaultAsync(i => i.ServiceType == stream.ServiceType && i.SourceID == user.Id); - var streamSubscriptions = await _work.SubscriptionRepository.FindAsync(i => i.User == streamUser); if (streamUser == null) { @@ -48,6 +54,8 @@ public async Task Consume(ConsumeContext context) return; } + var streamSubscriptions = await _work.SubscriptionRepository.FindAsync(i => i.User == streamUser); + StreamGame streamGame; if (game.Id == "0" || string.IsNullOrEmpty(game.Id)) { From 1557064a05257c4ac21ed7cfd6cba629c64ed7b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 15:46:29 +0000 Subject: [PATCH 3/6] Second pass: fix async fire-and-forget, null dereferences, and missing error isolation - Fix ConsolidateRoleMentions: List.ForEach with async delegate is effectively async void - exceptions silently swallowed and removes not awaited. Replace with a proper await foreach loop. - Fix GetSubscriptionEmbed: guild.GetRole() returns null for deleted roles. Calling .Name on the result and OrderBy(i => i.Name) would NRE. Filter null roles out after the Select. - Fix EscapeSpecialDiscordCharacters: Format.Sanitize throws on null input. Guard with null/empty check returning string.Empty instead. - Fix GetStreamEmbed: game.Name and stream.StreamURL can be null/empty. Discord.NET throws ArgumentException on empty embed field values. Fall back to '[Not Set]' and user.ProfileURL respectively. - Add per-item try-catch in DiscordGuildDeleteConsumer subscription loop and channel loop so one failed removal does not abort cleanup for all remaining subscriptions/channels. - Add per-item try-catch in DiscordChannelDeleteConsumer subscription loop for the same reason. - Guard Queues.QueueURL, QueueUsername, and QueuePassword against null env vars. Previously they would silently pass null to MassTransit, producing cryptic connection/auth failures at runtime. https://claude.ai/code/session_01DGjiqffs4cNf5LX7A3YnLx --- LiveBot.Core/Repository/Static/Queues.cs | 13 +++++++-- .../Discord/DiscordChannelDeleteConsumer.cs | 17 +++++++---- .../Discord/DiscordGuildDeleteConsumer.cs | 28 +++++++++++++++---- .../Helpers/MonitorUtils.cs | 8 ++++-- .../Helpers/NotificationHelpers.cs | 8 ++++-- 5 files changed, 55 insertions(+), 19 deletions(-) diff --git a/LiveBot.Core/Repository/Static/Queues.cs b/LiveBot.Core/Repository/Static/Queues.cs index ba9ca4a..3798455 100644 --- a/LiveBot.Core/Repository/Static/Queues.cs +++ b/LiveBot.Core/Repository/Static/Queues.cs @@ -6,11 +6,18 @@ public class Queues { public static string QueueURL { - get => $"rabbitmq://{Environment.GetEnvironmentVariable("RabbitMQ_URL")}"; + get + { + var host = Environment.GetEnvironmentVariable("RabbitMQ_URL") + ?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_URL' is not set."); + return $"rabbitmq://{host}"; + } } - public static readonly string QueueUsername = Environment.GetEnvironmentVariable("RabbitMQ_Username"); - public static readonly string QueuePassword = Environment.GetEnvironmentVariable("RabbitMQ_Password"); + public static readonly string QueueUsername = Environment.GetEnvironmentVariable("RabbitMQ_Username") + ?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_Username' is not set."); + public static readonly string QueuePassword = Environment.GetEnvironmentVariable("RabbitMQ_Password") + ?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_Password' is not set."); public static readonly ushort PrefetchCount = 32; diff --git a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordChannelDeleteConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordChannelDeleteConsumer.cs index 4d370f9..3910634 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordChannelDeleteConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordChannelDeleteConsumer.cs @@ -26,11 +26,18 @@ public async Task Consume(ConsumeContext context) var subscriptions = await _work.SubscriptionRepository.FindAsync(i => i.DiscordChannel.DiscordId == message.ChannelId && i.DiscordGuild.DiscordId == message.GuildId); foreach (var subscription in subscriptions) { - _logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because channel was delete {GuildId} {ChannelId} {ChannelName} - {SubscriptionId}", subscription.User.Username, subscription.User.ServiceType, channel.DiscordGuild.DiscordId, channel.DiscordId, channel.Name, subscription.Id); - var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == subscription); - foreach (var roleToMention in rolesToMention) - await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id); - await _work.SubscriptionRepository.RemoveAsync(subscription.Id); + try + { + _logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because channel was delete {GuildId} {ChannelId} {ChannelName} - {SubscriptionId}", subscription.User.Username, subscription.User.ServiceType, channel.DiscordGuild.DiscordId, channel.DiscordId, channel.Name, subscription.Id); + var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == subscription); + foreach (var roleToMention in rolesToMention) + await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id); + await _work.SubscriptionRepository.RemoveAsync(subscription.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove Stream Subscription {SubscriptionId} for channel {ChannelId}", subscription.Id, channel.DiscordId); + } } var guildConfig = await _work.GuildConfigRepository.SingleOrDefaultAsync(i => i.DiscordGuild.DiscordId == message.GuildId); diff --git a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordGuildDeleteConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordGuildDeleteConsumer.cs index fd497db..c2c1489 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordGuildDeleteConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordGuildDeleteConsumer.cs @@ -33,16 +33,32 @@ public async Task Consume(ConsumeContext context) // Remove Stream Subscriptions for this Guild foreach (var streamSubscription in streamSubscriptions) { - _logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because have left the Guild {GuildId} - {SubscriptionId}", streamSubscription.User.Username, streamSubscription.User.ServiceType, discordGuild.DiscordId, streamSubscription.Id); - var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == streamSubscription); - foreach (var roleToMention in rolesToMention) - await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id); - await _work.SubscriptionRepository.RemoveAsync(streamSubscription.Id); + try + { + _logger.LogInformation("Removing Stream Subscription for {Username} on {ServiceType} because have left the Guild {GuildId} - {SubscriptionId}", streamSubscription.User.Username, streamSubscription.User.ServiceType, discordGuild.DiscordId, streamSubscription.Id); + var rolesToMention = await _work.RoleToMentionRepository.FindAsync(i => i.StreamSubscription == streamSubscription); + foreach (var roleToMention in rolesToMention) + await _work.RoleToMentionRepository.RemoveAsync(roleToMention.Id); + await _work.SubscriptionRepository.RemoveAsync(streamSubscription.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove Stream Subscription {SubscriptionId} for guild {GuildId}", streamSubscription.Id, discordGuild.DiscordId); + } } // Remove Discord Channels for this Guild foreach (var discordChannel in discordChannels) - await _work.ChannelRepository.RemoveAsync(discordChannel.Id); + { + try + { + await _work.ChannelRepository.RemoveAsync(discordChannel.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove Discord Channel {ChannelId} for guild {GuildId}", discordChannel.DiscordId, discordGuild.DiscordId); + } + } // Remove Discord Guild await _work.GuildRepository.RemoveAsync(discordGuild.Id); diff --git a/LiveBot.Discord.SlashCommands/Helpers/MonitorUtils.cs b/LiveBot.Discord.SlashCommands/Helpers/MonitorUtils.cs index a433ae1..ad7cf61 100644 --- a/LiveBot.Discord.SlashCommands/Helpers/MonitorUtils.cs +++ b/LiveBot.Discord.SlashCommands/Helpers/MonitorUtils.cs @@ -45,7 +45,10 @@ internal static Embed GetSubscriptionEmbed(SocketGuild guild, StreamSubscription { var roles = new List(); if (subscription.RolesToMention.Any()) - roles = subscription.RolesToMention.Select(i => guild.GetRole(i.DiscordRoleId)).ToList(); + roles = subscription.RolesToMention + .Select(i => guild.GetRole(i.DiscordRoleId)) + .Where(r => r != null) + .ToList(); var roleStrings = new List(); foreach (var role in roles.OrderBy(i => i.Name)) @@ -86,7 +89,8 @@ internal static async Task ConsolidateRoleMentions(IUnitOfWork work, Strea if (currentRolesToMention.Any()) { var rolesToDelete = currentRolesToMention.Where(i => !roleIds.Contains(i.DiscordRoleId)).ToList(); - rolesToDelete.ForEach(async i => await work.RoleToMentionRepository.RemoveAsync(i.Id)); + foreach (var roleToDelete in rolesToDelete) + await work.RoleToMentionRepository.RemoveAsync(roleToDelete.Id); RolesUpdated = true; } foreach (var roleId in roleIds) diff --git a/LiveBot.Discord.SlashCommands/Helpers/NotificationHelpers.cs b/LiveBot.Discord.SlashCommands/Helpers/NotificationHelpers.cs index ff4d4fe..8ea72b2 100644 --- a/LiveBot.Discord.SlashCommands/Helpers/NotificationHelpers.cs +++ b/LiveBot.Discord.SlashCommands/Helpers/NotificationHelpers.cs @@ -10,8 +10,10 @@ namespace LiveBot.Discord.SlashCommands.Helpers { public static class NotificationHelpers { - public static string EscapeSpecialDiscordCharacters(string input) + public static string EscapeSpecialDiscordCharacters(string? input) { + if (string.IsNullOrEmpty(input)) + return string.Empty; return Format.Sanitize(input); } @@ -121,10 +123,10 @@ public static Embed GetStreamEmbed(ILiveBotStream stream, ILiveBotUser user, ILi .WithThumbnailUrl(user.AvatarURL); // Add Game field - builder.AddField(name: "Game", value: game.Name, inline: true); + builder.AddField(name: "Game", value: string.IsNullOrWhiteSpace(game.Name) ? "[Not Set]" : game.Name, inline: true); // Add Stream URL field - builder.AddField(name: "Stream", value: stream.StreamURL, inline: true); + builder.AddField(name: "Stream", value: string.IsNullOrWhiteSpace(stream.StreamURL) ? user.ProfileURL : stream.StreamURL, inline: true); // Add Status Field //builder.AddField(name: "Status", value: "", inline: false); From 02e30c302761702cbb4d8e0c9ec02118845dfbe2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 06:04:21 +0000 Subject: [PATCH 4/6] =?UTF-8?q?Optimize:=20eliminate=20redundant=20DB=20qu?= =?UTF-8?q?eries,=20O(n=C2=B2)=20loop,=20and=20repeated=20env=20reads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StreamUpdateConsumer: - Move subscriptions check before game-related DB queries so that if a user has no subscriptions, the game lookup and potential AddOrUpdate are skipped entirely — saves 1-2 DB round-trips per event when there are no active subscriptions. - Remove the second SingleOrDefaultAsync call after AddOrUpdateAsync in the else branch. The returned game entity was assigned to a local variable that was never read again in the method, making it a completely wasted DB query on every stream-update event. DiscordMemberLiveConsumer: - Replace List + Any(i => i.Id == guild.Id) deduplication with a HashSet. The previous approach was O(n) per guild inside a nested shard/guild loop — O(n²) overall. HashSet.Add() returns false for duplicates, making the dedup O(1) per guild. TwitchMonitor.UpdateUsers: - Remove redundant ContainsKey + [key]=value / else TryAdd branching on ConcurrentDictionary. The indexer assignment (dict[key] = value) already handles both add and update atomically, eliminating an extra dictionary lookup per user on every user-refresh cycle. Queues: - Cache the RabbitMQ host string in a private static readonly field instead of calling Environment.GetEnvironmentVariable on every QueueURL property access. https://claude.ai/code/session_01DGjiqffs4cNf5LX7A3YnLx --- LiveBot.Core/Repository/Static/Queues.cs | 13 ++++--------- .../Discord/DiscordMemberLiveConsumer.cs | 6 +++--- .../Consumers/Streams/StreamUpdateConsumer.cs | 15 ++++++--------- LiveBot.Watcher.Twitch/TwitchMonitor.cs | 9 +-------- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/LiveBot.Core/Repository/Static/Queues.cs b/LiveBot.Core/Repository/Static/Queues.cs index 3798455..5ec4d91 100644 --- a/LiveBot.Core/Repository/Static/Queues.cs +++ b/LiveBot.Core/Repository/Static/Queues.cs @@ -4,15 +4,10 @@ namespace LiveBot.Core.Repository.Static { public class Queues { - public static string QueueURL - { - get - { - var host = Environment.GetEnvironmentVariable("RabbitMQ_URL") - ?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_URL' is not set."); - return $"rabbitmq://{host}"; - } - } + private static readonly string _queueHost = Environment.GetEnvironmentVariable("RabbitMQ_URL") + ?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_URL' is not set."); + + public static string QueueURL => $"rabbitmq://{_queueHost}"; public static readonly string QueueUsername = Environment.GetEnvironmentVariable("RabbitMQ_Username") ?? throw new InvalidOperationException("Required environment variable 'RabbitMQ_Username' is not set."); diff --git a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs index 6323d98..2a4ef5f 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Discord/DiscordMemberLiveConsumer.cs @@ -57,14 +57,14 @@ public async Task Consume(ConsumeContext context) if (userGame == null) return; + var seenGuildIds = new HashSet(); var mutualGuilds = new List(); foreach (DiscordSocketClient shard in _client.Shards) { foreach (SocketGuild guild in shard.Guilds) { - if (guild.GetUser(user.Id) != null) - if (!mutualGuilds.Any(i => i.Id == guild.Id)) - mutualGuilds.Add(guild); + if (guild.GetUser(user.Id) != null && seenGuildIds.Add(guild.Id)) + mutualGuilds.Add(guild); } } diff --git a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs index 2e04674..f7c33a4 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamUpdateConsumer.cs @@ -43,8 +43,6 @@ public async Task Consume(ConsumeContext context) ILiveBotGame game = stream.Game ?? await monitor.GetGame(stream.GameId); - Expression> templateGamePredicate = (i => i.ServiceType == stream.ServiceType && i.SourceId == "0"); - var templateGame = await _work.GameRepository.SingleOrDefaultAsync(templateGamePredicate); var streamUser = await _work.UserRepository.SingleOrDefaultAsync(i => i.ServiceType == stream.ServiceType && i.SourceID == user.Id); if (streamUser == null) @@ -56,9 +54,14 @@ public async Task Consume(ConsumeContext context) var streamSubscriptions = await _work.SubscriptionRepository.FindAsync(i => i.User == streamUser); - StreamGame streamGame; + if (!streamSubscriptions.Any()) + return; + + // Ensure the game record exists in the database + Expression> templateGamePredicate = (i => i.ServiceType == stream.ServiceType && i.SourceId == "0"); if (game.Id == "0" || string.IsNullOrEmpty(game.Id)) { + var templateGame = await _work.GameRepository.SingleOrDefaultAsync(templateGamePredicate); if (templateGame == null) { StreamGame newStreamGame = new StreamGame @@ -69,9 +72,7 @@ public async Task Consume(ConsumeContext context) ThumbnailURL = "" }; await _work.GameRepository.AddOrUpdateAsync(newStreamGame, templateGamePredicate); - templateGame = await _work.GameRepository.SingleOrDefaultAsync(templateGamePredicate); } - streamGame = templateGame; } else { @@ -83,12 +84,8 @@ public async Task Consume(ConsumeContext context) ThumbnailURL = game.ThumbnailURL }; await _work.GameRepository.AddOrUpdateAsync(newStreamGame, i => i.ServiceType == stream.ServiceType && i.SourceId == stream.GameId); - streamGame = await _work.GameRepository.SingleOrDefaultAsync(i => i.ServiceType == stream.ServiceType && i.SourceId == stream.GameId); } - if (!streamSubscriptions.Any()) - return; - bool hasValidSubscriptions = false; foreach (StreamSubscription streamSubscription in streamSubscriptions) diff --git a/LiveBot.Watcher.Twitch/TwitchMonitor.cs b/LiveBot.Watcher.Twitch/TwitchMonitor.cs index 95d9a27..029a1d0 100644 --- a/LiveBot.Watcher.Twitch/TwitchMonitor.cs +++ b/LiveBot.Watcher.Twitch/TwitchMonitor.cs @@ -573,14 +573,7 @@ public async Task UpdateUsers() foreach (User user in users.Users) { var twitchUser = new TwitchUser(BaseURL, ServiceType, user); - if (_userCache.ContainsKey(user.Id)) - { - _userCache[user.Id] = twitchUser; - } - else - { - _userCache.TryAdd(user.Id, twitchUser); - } + _userCache[user.Id] = twitchUser; await _cache.SetListItemAsync(recordId: _userCacheName, fieldName: twitchUser.Id, data: twitchUser); try { From 25dfc7621f21d1f27205c4f3ae0f3ac730de9ee0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 06:14:57 +0000 Subject: [PATCH 5/6] Fix auth: prevent infinite recursion, stop timer object churn, fix async void MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Infinite recursion in all API_Get* credential error handlers: Every method (API_GetGame, API_GetUserByLogin, API_GetUserById, API_GetUsersById, API_GetUserByURL, API_GetStream) caught InvalidCredentialException/BadScopeException and called itself without passing the retryCount through. A persistent auth failure would recurse indefinitely until stack overflow. Each credential catch block now propagates retryCount and returns null once ApiRetryCount is exhausted, matching the existing BadGateway behaviour. SetupAuthTimer - two issues fixed together: 1. A new System.Timers.Timer was allocated on every call (every auth refresh cycle), with the old one left for GC. The timer is now created once and reused; subsequent calls simply update the Interval and restart it. 2. The Elapsed handler used `async (sender, e) => await UpdateAuth()` which is async void — an unhandled exception from UpdateAuth would propagate to the thread pool and terminate the process. Replaced with a fire-and-forget pattern using ContinueWith(OnlyOnFaulted) to log any unhandled faults without crashing. UpdateAuth - oldAuth expiry uses UpdateAsync instead of AddOrUpdateAsync: The old token was fetched from the DB moments before. Using AddOrUpdateAsync to mark it expired searches for a match and would insert a duplicate row if the token somehow wasn't found. UpdateAsync is semantically correct and avoids the unnecessary upsert predicate evaluation. https://claude.ai/code/session_01DGjiqffs4cNf5LX7A3YnLx --- LiveBot.Watcher.Twitch/TwitchMonitor.cs | 83 +++++++++++++++++-------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/LiveBot.Watcher.Twitch/TwitchMonitor.cs b/LiveBot.Watcher.Twitch/TwitchMonitor.cs index 029a1d0..cbaa2d2 100644 --- a/LiveBot.Watcher.Twitch/TwitchMonitor.cs +++ b/LiveBot.Watcher.Twitch/TwitchMonitor.cs @@ -216,9 +216,13 @@ public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) catch (Exception e) when (e is InvalidCredentialException || e is BadScopeException) { _logger.LogError(exception: e, message: "Error getting {ServiceType} Game", ServiceType); - await UpdateAuth(force: true); - await Task.Delay(RetryDelay); - return await API_GetGame(gameId); + if (retryCount <= ApiRetryCount) + { + await UpdateAuth(force: true); + await Task.Delay(RetryDelay); + return await API_GetGame(gameId, retryCount + 1); + } + return null; } } @@ -244,9 +248,13 @@ public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) catch (Exception e) when (e is InvalidCredentialException || e is BadScopeException) { _logger.LogError(exception: e, message: "Error getting {ServiceType} User by Login", ServiceType); - await UpdateAuth(force: true); - await Task.Delay(RetryDelay); - return await API_GetUserByLogin(username); + if (retryCount <= ApiRetryCount) + { + await UpdateAuth(force: true); + await Task.Delay(RetryDelay); + return await API_GetUserByLogin(username, retryCount + 1); + } + return null; } } @@ -272,9 +280,13 @@ public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) catch (Exception e) when (e is InvalidCredentialException || e is BadScopeException) { _logger.LogError(exception: e, message: "Error getting {ServiceType} User by Id", ServiceType); - await UpdateAuth(force: true); - await Task.Delay(RetryDelay); - return await API_GetUserById(userId); + if (retryCount <= ApiRetryCount) + { + await UpdateAuth(force: true); + await Task.Delay(RetryDelay); + return await API_GetUserById(userId, retryCount + 1); + } + return null; } } @@ -299,9 +311,13 @@ public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) catch (Exception e) when (e is InvalidCredentialException || e is BadScopeException) { _logger.LogError(exception: e, message: "Error getting {ServiceType} Users by Id", ServiceType); - await UpdateAuth(force: true); - await Task.Delay(RetryDelay); - return await API_GetUsersById(userIdList); + if (retryCount <= ApiRetryCount) + { + await UpdateAuth(force: true); + await Task.Delay(RetryDelay); + return await API_GetUsersById(userIdList, retryCount + 1); + } + return null; } } @@ -326,9 +342,13 @@ public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) catch (Exception e) when (e is InvalidCredentialException || e is BadScopeException) { _logger.LogError(exception: e, message: "Error getting {ServiceType} User by URL", ServiceType); - await UpdateAuth(force: true); - await Task.Delay(RetryDelay); - return await API_GetUserByURL(url); + if (retryCount <= ApiRetryCount) + { + await UpdateAuth(force: true); + await Task.Delay(RetryDelay); + return await API_GetUserByURL(url, retryCount + 1); + } + return null; } } @@ -357,9 +377,13 @@ public async void Monitor_OnStreamOffline(object? sender, OnStreamOfflineArgs e) catch (Exception e) when (e is InvalidCredentialException || e is BadScopeException) { _logger.LogError(exception: e, message: "Error getting {ServiceType} Stream", ServiceType); - await UpdateAuth(force: true); - await Task.Delay(RetryDelay); - return await API_GetStream(user); + if (retryCount <= ApiRetryCount) + { + await UpdateAuth(force: true); + await Task.Delay(RetryDelay); + return await API_GetStream(user, retryCount + 1); + } + return null; } } @@ -522,7 +546,7 @@ public async Task UpdateAuth(bool force = false) await _cache.SetRecordAsync(recordId: _authCacheName, data: newAuth, expiryTime: timeToExpire.Duration()); oldAuth.Expired = true; - await _work.AuthRepository.AddOrUpdateAsync(oldAuth, i => i.ServiceType == ServiceType && i.ClientId == ClientId && i.AccessToken == oldAuth.AccessToken); + await _work.AuthRepository.UpdateAsync(oldAuth); AccessToken = newAuth.AccessToken; _logger.LogDebug("{ServiceType} Expiration time: {ExpirationSeconds}", ServiceType, refreshResponse.ExpiresIn < 1800 ? 1800 : refreshResponse.ExpiresIn); @@ -549,16 +573,25 @@ public async Task UpdateAuth(bool force = false) private void SetupAuthTimer(TimeSpan timeSpan) { - RefreshAuthTimer?.Stop(); - if (timeSpan.TotalSeconds < 1800) timeSpan = TimeSpan.FromSeconds(1800); - RefreshAuthTimer = new System.Timers.Timer(timeSpan.Duration().TotalMilliseconds) + if (RefreshAuthTimer == null) { - AutoReset = false - }; - RefreshAuthTimer.Elapsed += async (sender, e) => await UpdateAuth(); + RefreshAuthTimer = new System.Timers.Timer { AutoReset = false }; + RefreshAuthTimer.Elapsed += (sender, e) => + { + _ = UpdateAuth().ContinueWith(t => + _logger.LogError(t.Exception, "Unhandled exception in auth refresh timer for {ServiceType}", ServiceType), + TaskContinuationOptions.OnlyOnFaulted); + }; + } + else + { + RefreshAuthTimer.Stop(); + } + + RefreshAuthTimer.Interval = timeSpan.Duration().TotalMilliseconds; RefreshAuthTimer.Start(); } From 55aaf5a17a45e2c1b79b47a51afc44092b07b4f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Feb 2026 04:33:27 +0000 Subject: [PATCH 6/6] Reduce log noise: align IsDebug handling and downgrade high-frequency Info logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DatabaseSetup.cs (Twitch Watcher) hardcoded MinimumLevel.Information() regardless of the IsDebug flag, while LiveBotSetupHelpers.cs (Discord) correctly switches to LogEventLevel.Debug when IsDebug=true. This meant debug-mode logging was silently disabled for the entire Twitch Watcher service. Both services now use the same conditional: IsDebug ? LogEventLevel.Debug : LogEventLevel.Information Serilog replaces the .NET logging pipeline entirely, so the Logging section in appsettings.json has no effect — the Serilog MinimumLevel is the only gate that matters. StreamOfflineConsumer: "No subscriptions found" was LogInformation. This fires for every stream-offline event where the user has no LiveBot subscription, which is the common case for most monitored streams. Downgraded to LogDebug. TwitchMonitor.UpdateAuth: "Updating Auth" and "Successfully set AccessToken" were both LogInformation. These fire on every scheduled auth refresh cycle and on every InvalidCredentialException retry (now up to ApiRetryCount times). Downgraded to LogDebug so they only surface when IsDebug=true. https://claude.ai/code/session_01DGjiqffs4cNf5LX7A3YnLx --- .../Consumers/Streams/StreamOfflineConsumer.cs | 2 +- LiveBot.Watcher.Twitch/DatabaseSetup.cs | 2 +- LiveBot.Watcher.Twitch/TwitchMonitor.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOfflineConsumer.cs b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOfflineConsumer.cs index c313808..f2c98da 100644 --- a/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOfflineConsumer.cs +++ b/LiveBot.Discord.SlashCommands/Consumers/Streams/StreamOfflineConsumer.cs @@ -39,7 +39,7 @@ public async Task Consume(ConsumeContext context) var streamSubscriptions = await GetStreamSubscriptionsAsync(streamUser); if (!streamSubscriptions.Any()) { - _streamOfflineLogger.LogInformation("No subscriptions found for {Username} ({UserId})", + _streamOfflineLogger.LogDebug("No subscriptions found for {Username} ({UserId})", user.Username, user.Id); return; } diff --git a/LiveBot.Watcher.Twitch/DatabaseSetup.cs b/LiveBot.Watcher.Twitch/DatabaseSetup.cs index 5481f76..3a27e4e 100644 --- a/LiveBot.Watcher.Twitch/DatabaseSetup.cs +++ b/LiveBot.Watcher.Twitch/DatabaseSetup.cs @@ -25,7 +25,7 @@ public static WebApplicationBuilder SetupLiveBot(this WebApplicationBuilder buil builder.Host.UseSerilog((ctx, lc) => lc - .MinimumLevel.Information() + .MinimumLevel.Is(IsDebug ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information) .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.DatadogLogs(apiKey: apiKey, source: source, service: service, host: hostname, tags: tags) .Enrich.FromLogContext() diff --git a/LiveBot.Watcher.Twitch/TwitchMonitor.cs b/LiveBot.Watcher.Twitch/TwitchMonitor.cs index cbaa2d2..6593084 100644 --- a/LiveBot.Watcher.Twitch/TwitchMonitor.cs +++ b/LiveBot.Watcher.Twitch/TwitchMonitor.cs @@ -480,13 +480,13 @@ public async Task GetAndSetActiveAuth() public async Task UpdateAuth(bool force = false) { - _logger.LogInformation(message: "Updating Auth for {ServiceType} with Client Id {ClientId}", ServiceType, ClientId); + _logger.LogDebug(message: "Updating Auth for {ServiceType} with Client Id {ClientId}", ServiceType, ClientId); if (!IsWatcher) { try { var activeAuth = await GetAndSetActiveAuth(); - _logger.LogInformation(message: "Successfully set AccessToken for {ServiceType} with Client Id {ClientId} to active auth", ServiceType, ClientId); + _logger.LogDebug(message: "Successfully set AccessToken for {ServiceType} with Client Id {ClientId} to active auth", ServiceType, ClientId); // Trigger it 5 minutes before expiration time to be safe var timeToExpire = activeAuth.ExpiresAt - DateTime.UtcNow;