Added operation diagnostic data

To assist with troubleshooting
This commit is contained in:
Noikoio 2019-12-11 19:14:03 -08:00
parent b31813c210
commit 3fc13efc57
9 changed files with 253 additions and 46 deletions

View file

@ -18,7 +18,7 @@ Class BirthdayRoleUpdate
''' Does processing on all available guilds at once. ''' Does processing on all available guilds at once.
''' </summary> ''' </summary>
Public Overrides Async Function OnTick() As Task Public Overrides Async Function OnTick() As Task
Dim tasks As New List(Of Task(Of Integer)) Dim tasks As New List(Of Task)
For Each guild In BotInstance.DiscordClient.Guilds For Each guild In BotInstance.DiscordClient.Guilds
Dim t = ProcessGuildAsync(guild) Dim t = ProcessGuildAsync(guild)
tasks.Add(t) tasks.Add(t)
@ -32,6 +32,7 @@ Class BirthdayRoleUpdate
Select task.Exception Select task.Exception
Log($"Encountered {exs.Count} errors during bulk guild processing.") Log($"Encountered {exs.Count} errors during bulk guild processing.")
For Each iex In exs For Each iex In exs
' TODO probably not a good idea
Log(iex.ToString()) Log(iex.ToString())
Next Next
End Try End Try
@ -53,7 +54,10 @@ Class BirthdayRoleUpdate
' TODO metrics for role sets, unsets, announcements - and I mentioned this above too ' TODO metrics for role sets, unsets, announcements - and I mentioned this above too
End Function End Function
Private Async Function ProcessGuildAsync(guild As SocketGuild) As Task(Of Integer) ''' <summary>
''' Main function where actual guild processing occurs.
''' </summary>
Private Async Function ProcessGuildAsync(guild As SocketGuild) As Task
' Gather required information ' Gather required information
Dim tz As String Dim tz As String
Dim users As IEnumerable(Of GuildUserSettings) Dim users As IEnumerable(Of GuildUserSettings)
@ -61,15 +65,19 @@ Class BirthdayRoleUpdate
Dim channel As SocketTextChannel = Nothing Dim channel As SocketTextChannel = Nothing
Dim announce As (String, String) Dim announce As (String, String)
Dim announceping As Boolean Dim announceping As Boolean
Dim op As OperationStatus
If Not BotInstance.GuildCache.ContainsKey(guild.Id) Then Return 0 ' guild not yet fully loaded; skip processing ' Skip processing of guild if local info has not yet been loaded
If Not BotInstance.GuildCache.ContainsKey(guild.Id) Then Return
' Lock once to grab all info
Dim gs = BotInstance.GuildCache(guild.Id) Dim gs = BotInstance.GuildCache(guild.Id)
With gs With gs
tz = .TimeZone tz = .TimeZone
users = .Users users = .Users
announce = .AnnounceMessages announce = .AnnounceMessages
announceping = .AnnouncePing announceping = .AnnouncePing
op = .OperationLog
If .AnnounceChannelId.HasValue Then channel = guild.GetTextChannel(gs.AnnounceChannelId.Value) If .AnnounceChannelId.HasValue Then channel = guild.GetTextChannel(gs.AnnounceChannelId.Value)
If .RoleId.HasValue Then role = guild.GetRole(gs.RoleId.Value) If .RoleId.HasValue Then role = guild.GetRole(gs.RoleId.Value)
@ -81,55 +89,57 @@ Class BirthdayRoleUpdate
' Set birthday roles, get list of users that had the role added ' Set birthday roles, get list of users that had the role added
' But first check if we are able to do so. Letting all requests fail instead will lead to rate limiting. ' But first check if we are able to do so. Letting all requests fail instead will lead to rate limiting.
Dim correctRoleSettings = HasCorrectRoleSettings(guild, role) Dim roleCheck = CheckCorrectRoleSettings(guild, role)
Dim gotForbidden = False If Not roleCheck.Item1 Then
SyncLock op
op(OperationType.BirthdayRole) = New OperationInfo(New Exception(roleCheck.Item2))
op(OperationType.BirthdayAnnounce) = Nothing
End SyncLock
Return
End If
Dim announceNames As IEnumerable(Of SocketGuildUser) = Nothing Dim announcementList As IEnumerable(Of SocketGuildUser)
If correctRoleSettings Then ' Do actual role updating
Try Try
announceNames = Await UpdateGuildBirthdayRoles(guild, role, birthdays) announcementList = Await UpdateGuildBirthdayRoles(guild, role, birthdays)
SyncLock op
op(OperationType.BirthdayRole) = New OperationInfo()
End SyncLock
Catch ex As Discord.Net.HttpException Catch ex As Discord.Net.HttpException
If ex.HttpCode = HttpStatusCode.Forbidden Then SyncLock op
gotForbidden = True op(OperationType.BirthdayRole) = New OperationInfo(ex)
Else op(OperationType.BirthdayAnnounce) = Nothing
End SyncLock
If ex.HttpCode <> HttpStatusCode.Forbidden Then
' Send unusual exceptions to calling method
Throw Throw
End If End If
Return
End Try End Try
End If
' Update warning flag If announcementList.Count <> 0 Then
Dim updateError = Not correctRoleSettings Or gotForbidden Await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList, op)
' Quit now if the warning flag was set. Announcement data is not available.
If updateError Then Return 0
If announceNames.Count <> 0 Then
' Send out announcement message
Await AnnounceBirthdaysAsync(announce, announceping, channel, announceNames)
End If End If
Return announceNames.Count
End Function End Function
''' <summary> ''' <summary>
''' Checks if the bot may be allowed to alter roles. ''' Checks if the bot may be allowed to alter roles.
''' </summary> ''' </summary>
Private Function HasCorrectRoleSettings(guild As SocketGuild, role As SocketRole) As Boolean Private Function CheckCorrectRoleSettings(guild As SocketGuild, role As SocketRole) As (Boolean, String)
If role Is Nothing Then If role Is Nothing Then
' Designated role not found or defined in guild Return (False, "Designated role not found or defined in guild")
Return False
End If End If
If Not guild.CurrentUser.GuildPermissions.ManageRoles Then If Not guild.CurrentUser.GuildPermissions.ManageRoles Then
' Bot user cannot manage roles Return (False, "Bot does not contain Manage Roles permission")
Return False
End If End If
' Check potential role order conflict ' Check potential role order conflict
If role.Position >= guild.CurrentUser.Hierarchy Then If role.Position >= guild.CurrentUser.Hierarchy Then
' Target role is at or above bot's highest role. Return (False, "Targeted role is at or above bot's highest rank")
Return False
End If End If
Return True Return (True, "Success")
End Function End Function
''' <summary> ''' <summary>
@ -221,8 +231,14 @@ Class BirthdayRoleUpdate
Private Async Function AnnounceBirthdaysAsync(announce As (String, String), Private Async Function AnnounceBirthdaysAsync(announce As (String, String),
announcePing As Boolean, announcePing As Boolean,
c As SocketTextChannel, c As SocketTextChannel,
names As IEnumerable(Of SocketGuildUser)) As Task names As IEnumerable(Of SocketGuildUser),
If c Is Nothing Then Return op As OperationStatus) As Task
If c Is Nothing Then
SyncLock op
op(OperationType.BirthdayAnnounce) = New OperationInfo("Announcement channel missing or undefined")
End SyncLock
Return
End If
Dim announceMsg As String Dim announceMsg As String
If names.Count = 1 Then If names.Count = 1 Then
@ -252,9 +268,13 @@ Class BirthdayRoleUpdate
Try Try
Await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString())) Await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString()))
SyncLock op
op(OperationType.BirthdayAnnounce) = New OperationInfo()
End SyncLock
Catch ex As Discord.Net.HttpException Catch ex As Discord.Net.HttpException
' Ignore SyncLock op
' TODO keep tabs on this somehow for troubleshooting purposes op(OperationType.BirthdayAnnounce) = New OperationInfo(ex)
End SyncLock
End Try End Try
End Function End Function
End Class End Class

View file

@ -10,6 +10,7 @@ Class BirthdayBot
Private ReadOnly _cmdsListing As ListingCommands Private ReadOnly _cmdsListing As ListingCommands
Private ReadOnly _cmdsHelp As HelpInfoCommands Private ReadOnly _cmdsHelp As HelpInfoCommands
Private ReadOnly _cmdsMods As ManagerCommands Private ReadOnly _cmdsMods As ManagerCommands
Private ReadOnly _cmdsDiag As DiagnosticCommands
Private WithEvents Client As DiscordShardedClient Private WithEvents Client As DiscordShardedClient
Private ReadOnly _worker As BackgroundServiceRunner Private ReadOnly _worker As BackgroundServiceRunner
@ -49,15 +50,21 @@ Class BirthdayBot
For Each item In _cmdsMods.Commands For Each item In _cmdsMods.Commands
_dispatchCommands.Add(item.Item1, item.Item2) _dispatchCommands.Add(item.Item1, item.Item2)
Next Next
_cmdsDiag = New DiagnosticCommands(Me, conf)
For Each item In _cmdsDiag.Commands
_dispatchCommands.Add(item.Item1, item.Item2)
Next
End Sub End Sub
Public Async Function Start() As Task Public Async Function Start() As Task
Await Client.LoginAsync(TokenType.Bot, Config.BotToken) Await Client.LoginAsync(TokenType.Bot, Config.BotToken)
Await Client.StartAsync() Await Client.StartAsync()
#If Not DEBUG Then
Log("Background processing", "Delaying start") Log("Background processing", "Delaying start")
Await Task.Delay(90000) ' TODO don't keep doing this Await Task.Delay(90000) ' TODO don't keep doing this
Log("Background processing", "Delay complete") Log("Background processing", "Delay complete")
#End If
_worker.Start() _worker.Start()
Await Task.Delay(-1) Await Task.Delay(-1)
@ -85,8 +92,8 @@ Class BirthdayBot
Return Task.CompletedTask Return Task.CompletedTask
End Function End Function
Private Async Function SetStatus() As Task Handles Client.ShardConnected Private Async Function SetStatus(shard As DiscordSocketClient) As Task Handles Client.ShardConnected
Await Client.SetGameAsync(CommandPrefix + "help") Await shard.SetGameAsync(CommandPrefix + "help")
End Function End Function
Private Async Function Dispatch(msg As SocketMessage) As Task Handles Client.MessageReceived Private Async Function Dispatch(msg As SocketMessage) As Task Handles Client.MessageReceived

View file

@ -4,7 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RootNamespace>BirthdayBot</RootNamespace> <RootNamespace>BirthdayBot</RootNamespace>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.3.4</Version> <Version>1.3.6</Version>
<Authors>Noi</Authors> <Authors>Noi</Authors>
<Company /> <Company />
<Description>Discord bot for birthday reminders.</Description> <Description>Discord bot for birthday reminders.</Description>

View file

@ -7,6 +7,8 @@ Imports System.IO
''' </summary> ''' </summary>
Class Configuration Class Configuration
Public ReadOnly Property BotToken As String Public ReadOnly Property BotToken As String
Public ReadOnly Property LogWebhook As String
Public ReadOnly Property DiagnosticChannel As ULong
Public ReadOnly Property DBotsToken As String Public ReadOnly Property DBotsToken As String
Public ReadOnly Property DatabaseSettings As Database Public ReadOnly Property DatabaseSettings As Database
@ -23,11 +25,19 @@ Class Configuration
End If End If
Dim jc = JObject.Parse(File.ReadAllText(confPath)) Dim jc = JObject.Parse(File.ReadAllText(confPath))
BotToken = jc("BotToken").Value(Of String)()
BotToken = jc("BotToken")?.Value(Of String)()
If String.IsNullOrWhiteSpace(BotToken) Then If String.IsNullOrWhiteSpace(BotToken) Then
Throw New Exception("'BotToken' must be specified.") Throw New Exception("'BotToken' must be specified.")
End If End If
LogWebhook = jc("LogWebhook")?.Value(Of String)()
If String.IsNullOrWhiteSpace(LogWebhook) Then
Throw New Exception("'LogWebhook' must be specified.")
End If
DiagnosticChannel = jc("DiagnosticChannel").Value(Of ULong)()
Dim dbj = jc("DBotsToken") Dim dbj = jc("DBotsToken")
If dbj IsNot Nothing Then If dbj IsNot Nothing Then
DBotsToken = dbj.Value(Of String)() DBotsToken = dbj.Value(Of String)()
@ -35,7 +45,7 @@ Class Configuration
DBotsToken = Nothing DBotsToken = Nothing
End If End If
Dim sqlcs = jc("SqlConnectionString").Value(Of String)() Dim sqlcs = jc("SqlConnectionString")?.Value(Of String)()
If String.IsNullOrWhiteSpace(sqlcs) Then If String.IsNullOrWhiteSpace(sqlcs) Then
Throw New Exception("'SqlConnectionString' must be specified.") Throw New Exception("'SqlConnectionString' must be specified.")
End If End If

View file

@ -7,7 +7,7 @@ Imports NpgsqlTypes
''' Holds various pieces of state information for a guild the bot is operating in. ''' Holds various pieces of state information for a guild the bot is operating in.
''' Includes, among other things, a copy of the guild's settings and a list of all known users with birthdays. ''' Includes, among other things, a copy of the guild's settings and a list of all known users with birthdays.
''' </summary> ''' </summary>
Friend Class GuildStateInformation Class GuildStateInformation
Public ReadOnly Property GuildId As ULong Public ReadOnly Property GuildId As ULong
Private ReadOnly _db As Database Private ReadOnly _db As Database
Private _bdayRole As ULong? Private _bdayRole As ULong?
@ -19,9 +19,10 @@ Friend Class GuildStateInformation
Private _announceMsgPl As String Private _announceMsgPl As String
Private _announcePing As Boolean Private _announcePing As Boolean
Private ReadOnly _userCache As Dictionary(Of ULong, GuildUserSettings) Private ReadOnly _userCache As Dictionary(Of ULong, GuildUserSettings)
Public ReadOnly Property OperationLog As OperationStatus
''' <summary> ''' <summary>
''' Gets a list of cached users. Use sparingly. ''' Gets a list of cached registered user information.
''' </summary> ''' </summary>
Public ReadOnly Property Users As IEnumerable(Of GuildUserSettings) Public ReadOnly Property Users As IEnumerable(Of GuildUserSettings)
Get Get
@ -115,6 +116,9 @@ Friend Class GuildStateInformation
' Called by LoadSettingsAsync. Double-check ordinals when changes are made. ' Called by LoadSettingsAsync. Double-check ordinals when changes are made.
Private Sub New(reader As DbDataReader, dbconfig As Database) Private Sub New(reader As DbDataReader, dbconfig As Database)
_db = dbconfig _db = dbconfig
OperationLog = New OperationStatus()
GuildId = CULng(reader.GetInt64(0)) GuildId = CULng(reader.GetInt64(0))
' Weird: if using a ternary operator with a ULong?, Nothing resolves to 0 despite Option Strict On. ' Weird: if using a ternary operator with a ULong?, Nothing resolves to 0 despite Option Strict On.
If Not reader.IsDBNull(1) Then If Not reader.IsDBNull(1) Then

View file

@ -0,0 +1,75 @@
Imports Discord.WebSocket
Imports Discord.Webhook
Imports System.Text
''' <summary>
''' Implements the command used by global bot moderators to get operation info for each guild.
''' </summary>
Class DiagnosticCommands
Inherits CommandsCommon
Private ReadOnly _webhook As DiscordWebhookClient
Private ReadOnly _diagChannel As ULong
Sub New(inst As BirthdayBot, db As Configuration)
MyBase.New(inst, db)
_webhook = New DiscordWebhookClient(db.LogWebhook)
_diagChannel = db.DiagnosticChannel
End Sub
Public Overrides ReadOnly Property Commands As IEnumerable(Of (String, CommandHandler))
Get
Return New List(Of (String, CommandHandler)) From {
("diag", AddressOf CmdDiag),
("checkme", AddressOf CmdCheckme)
}
End Get
End Property
' Dumps all known guild information to the given webhook
Private Async Function CmdDiag(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
' Ignore if not in the correct channel
If reqChannel.Id <> _diagChannel Then Return
' Requires two parameters: (cmd) (guild id)
If param.Length <> 2 Then
Await reqChannel.SendMessageAsync(":x: Usage: (command) (guild ID)")
Return
End If
Dim rgid As ULong
If Not ULong.TryParse(param(1), rgid) Then
Await reqChannel.SendMessageAsync(":x: Cannot parse numeric guild ID")
Return
End If
Dim guild = Instance.DiscordClient.GetGuild(rgid)
If guild Is Nothing Then
Await reqChannel.SendMessageAsync(":x: Guild is not known to the bot")
End If
Dim gi = Instance.GuildCache(rgid)
If gi Is Nothing Then
Await reqChannel.SendMessageAsync(":x: Guild is known, but information is not available.")
End If
Await reqChannel.SendMessageAsync(":white_check_mark: Compiling info and sending to webhook.")
Dim report As New StringBuilder
report.AppendLine("=-=-=-=-GUILD INFORMATION-=-=-=-=")
' TODO dump config info
report.AppendLine($"{guild.Id}: {guild.Name}")
report.AppendLine($"User count: {guild.Users.Count}")
report.AppendLine("---")
SyncLock gi.OperationLog
report.Append(gi.OperationLog.GetDiagStrings())
End SyncLock
report.AppendLine("**Note**: Full stack traces for captured exceptions are printed to console.")
Await _webhook.SendMessageAsync(report.ToString())
End Function
Private Async Function CmdCheckme(param As String(), reqChannel As SocketTextChannel, reqUser As SocketGuildUser) As Task
Await _webhook.SendMessageAsync($"{reqUser.Username}#{reqUser.Discriminator}: {reqChannel.Guild.Id} checkme")
End Function
End Class

View file

@ -0,0 +1,37 @@
''' <summary>
''' Information regarding a single type of operation.
''' </summary>
Class OperationInfo
''' <summary>
''' The time in which the respective operation was attempted.
''' </summary>
ReadOnly Property Timestamp As DateTimeOffset
''' <summary>
''' Any exception encountered during the respective operation.
''' </summary>
''' <returns>Nothing/null if the previous given operation was a success.</returns>
ReadOnly Property Exception As Exception
''' <summary>
''' Creates an instance containing a success status.
''' </summary>
Sub New()
Timestamp = DateTimeOffset.UtcNow
End Sub
''' <summary>
''' Creates an instance containing a captured exception
''' </summary>
Sub New(ex As Exception)
Me.New()
Exception = ex
End Sub
''' <summary>
''' Creates an instance containing a custom error message
''' </summary>
Sub New(message As String)
Me.New()
Exception = New Exception(message)
End Sub
End Class

View file

@ -0,0 +1,49 @@
Imports System.Text
''' <summary>
''' Holds information regarding previous operations done on a guild and their most recent success/error status.
''' </summary>
Class OperationStatus
Private ReadOnly _log As New Dictionary(Of OperationType, OperationInfo)
Default Public Property Item(otype As OperationType) As OperationInfo
Get
Dim o As OperationInfo = Nothing
If Not _log.TryGetValue(otype, o) Then
Return Nothing
End If
Return o
End Get
Set(value As OperationInfo)
If value Is Nothing Then
_log.Remove(otype)
Else
_log(otype) = value
End If
End Set
End Property
''' <summary>
''' Prepares known information in a displayable format.
''' </summary>
Public Function GetDiagStrings() As String
Dim report As New StringBuilder
For Each otype As OperationType In [Enum].GetValues(GetType(OperationType))
Dim prefix = $"`{[Enum].GetName(GetType(OperationType), otype)}`: "
Dim info = Item(otype)
If info Is Nothing Then
report.AppendLine(prefix + "No data")
Continue For
End If
prefix += info.Timestamp.ToString("u") + " "
If info.Exception Is Nothing Then
report.AppendLine(prefix + "Success")
Else
Log("OperationStatus", prefix + info.Exception.ToString())
report.AppendLine(prefix + info.Exception.Message)
End If
Next
Return report.ToString()
End Function
End Class

View file

@ -0,0 +1,5 @@
Enum OperationType
BirthdayRole
BirthdayAnnounce
CommandDispatch
End Enum