Simplified logging, added logging to file
This commit is contained in:
parent
8d081ed637
commit
e1a39f964f
7 changed files with 74 additions and 82 deletions
|
@ -4,8 +4,6 @@ using System.Reflection;
|
|||
namespace RegexBot;
|
||||
|
||||
static class ModuleLoader {
|
||||
private const string LogName = nameof(ModuleLoader);
|
||||
|
||||
/// <summary>
|
||||
/// Given the instance configuration, loads all appropriate types from file specified in it.
|
||||
/// </summary>
|
||||
|
@ -42,13 +40,12 @@ static class ModuleLoader {
|
|||
where !type.IsAssignableFrom(typeof(RegexbotModule))
|
||||
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
|
||||
select type;
|
||||
k._svcLogging.DoInstanceLog(false, LogName, $"Scanning {asm.GetName().Name}");
|
||||
k._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}");
|
||||
|
||||
var newmods = new List<RegexbotModule>();
|
||||
foreach (var t in eligibleTypes) {
|
||||
var mod = Activator.CreateInstance(t, k)!;
|
||||
k._svcLogging.DoInstanceLog(false, LogName,
|
||||
$"---> Loading module {t.FullName}");
|
||||
k._svcLogging.DoLog(false, nameof(ModuleLoader), $"---> Loading module {t.FullName}");
|
||||
newmods.Add((RegexbotModule)mod);
|
||||
}
|
||||
return newmods;
|
||||
|
|
|
@ -50,19 +50,16 @@ class Program {
|
|||
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
|
||||
e.Cancel = true;
|
||||
|
||||
_main._svcLogging.DoInstanceLog(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal.");
|
||||
_main._svcLogging.DoLog(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal.");
|
||||
|
||||
// 5 seconds of leeway - any currently running tasks will need time to finish executing
|
||||
var closeWait = Task.Delay(5000);
|
||||
|
||||
// TODO periodic task service: stop processing, wait for all tasks to finish
|
||||
// TODO notify services of shutdown
|
||||
|
||||
closeWait.Wait();
|
||||
|
||||
bool success = _main.DiscordClient.StopAsync().Wait(1000);
|
||||
if (!success) _main._svcLogging.DoInstanceLog(false, nameof(RegexBot),
|
||||
"Failed to disconnect cleanly from Discord. Will force shut down.");
|
||||
var finishingTasks = Task.Run(async () => {
|
||||
// TODO periodic task service: stop processing, wait for all tasks to finish
|
||||
// TODO notify services of shutdown
|
||||
await _main.DiscordClient.StopAsync();
|
||||
});
|
||||
|
||||
if (!finishingTasks.Wait(5000))
|
||||
_main._svcLogging.DoLog(false, nameof(RegexBot), "Could not disconnect properly. Exiting...");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,6 @@ public partial class RegexbotClient {
|
|||
|
||||
// Everything's ready to go. Print the welcome message here.
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
|
||||
_svcLogging.DoInstanceLog(false, nameof(RegexBot), $"{nameof(RegexBot)} v{ver}. https://github.com/NoiTheCat/RegexBot");
|
||||
_svcLogging.DoLog(true, nameof(RegexBot), $"{nameof(RegexBot)} v{ver} - https://github.com/NoiTheCat/RegexBot");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,18 +69,21 @@ public abstract class RegexbotModule {
|
|||
protected EntityList GetModerators(ulong guild) => Bot._svcGuildState.DoGetModlist(guild);
|
||||
|
||||
/// <summary>
|
||||
/// Appends a message to the specified guild log.
|
||||
/// Emits a log message to the bot console that is associated with the specified guild.
|
||||
/// </summary>
|
||||
/// /// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
|
||||
protected void Log(ulong guild, string message) => Bot._svcLogging.DoGuildLog(guild, Name, message);
|
||||
protected void Log(SocketGuild guild, string? message) {
|
||||
var gname = guild.Name ?? $"Guild ID {guild.Id}";
|
||||
Bot._svcLogging.DoLog(false, $"{Name}] [{gname}", message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message to the instance log.
|
||||
/// Emits a log message to the bot console and, optionally, the logging webhook.
|
||||
/// </summary>
|
||||
/// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
|
||||
/// <param name="report">
|
||||
/// Specifies if the log message should be sent to the reporting channel.
|
||||
/// Only messages of very high importance should use this option.
|
||||
/// </param>
|
||||
protected void PLog(string message, bool report = false) => Bot._svcLogging.DoInstanceLog(report, Name, message);
|
||||
protected void Log(string message, bool report = false) => Bot._svcLogging.DoLog(report, Name, message);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,30 @@
|
|||
using Discord;
|
||||
using Discord.Webhook;
|
||||
using RegexBot.Data;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace RegexBot.Services.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
|
||||
/// For further information on log types, see documentation under <see cref="Data.BotDatabaseContext"/>.
|
||||
/// Implements program-wide logging.
|
||||
/// </summary>
|
||||
class LoggingService : Service {
|
||||
// NOTE: Service.Log's functionality is implemented here. DO NOT use within this class.
|
||||
private readonly DiscordWebhookClient _instLogWebhook;
|
||||
private readonly string? _logBasePath;
|
||||
|
||||
internal LoggingService(RegexbotClient bot) : base(bot) {
|
||||
_instLogWebhook = new DiscordWebhookClient(bot.Config.InstanceLogTarget);
|
||||
_logBasePath = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
|
||||
+ Path.DirectorySeparatorChar + "logs";
|
||||
try {
|
||||
if (!Directory.Exists(_logBasePath)) Directory.CreateDirectory(_logBasePath);
|
||||
Directory.GetFiles(_logBasePath);
|
||||
} catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) {
|
||||
_logBasePath = null;
|
||||
Output(Name, "Cannot create or access logging directory. File logging will be disabled.");
|
||||
}
|
||||
|
||||
// Discord.Net log handling (client logging option is specified in Program.cs)
|
||||
bot.DiscordClient.Log += DiscordClient_Log;
|
||||
// Let's also do the ready message
|
||||
bot.DiscordClient.Ready +=
|
||||
delegate { DoInstanceLog(true, nameof(RegexBot), "Connected and ready."); return Task.CompletedTask; };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -27,30 +32,45 @@ class LoggingService : Service {
|
|||
/// Only events with high importance are stored. Others are just printed to console.
|
||||
/// </summary>
|
||||
private Task DiscordClient_Log(LogMessage arg) {
|
||||
bool important = arg.Severity != LogSeverity.Info;
|
||||
string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
|
||||
const string logSource = "Discord.Net";
|
||||
var msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
|
||||
if (arg.Exception != null) msg += "\n```\n" + arg.Exception.ToString() + "\n```";
|
||||
|
||||
if (important) DoInstanceLog(true, logSource, msg);
|
||||
else ToConsole(logSource, msg);
|
||||
var important = arg.Severity != LogSeverity.Info;
|
||||
switch (arg.Message) { // Prevent webhook logs for these 'important' Discord.Net messages
|
||||
case "Connecting":
|
||||
case "Connected":
|
||||
case "Ready":
|
||||
case "Disconnecting":
|
||||
case "Disconnected":
|
||||
case "Resumed previous session":
|
||||
case "Failed to resume previous session":
|
||||
important = false;
|
||||
break;
|
||||
}
|
||||
DoLog(important, "Discord.Net", msg);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void ToConsole(string source, string message) {
|
||||
message ??= "(null)";
|
||||
var prefix = $"[{DateTimeOffset.UtcNow:u}] [{source}] ";
|
||||
private void Output(string source, string message) {
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var output = new StringBuilder();
|
||||
var prefix = $"[{now:u}] [{source}] ";
|
||||
foreach (var line in message.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None)) {
|
||||
Console.WriteLine(prefix + line);
|
||||
output.Append(prefix).AppendLine(line);
|
||||
}
|
||||
var outstr = output.ToString();
|
||||
Console.Write(outstr);
|
||||
if (_logBasePath != null) {
|
||||
var filename = _logBasePath + Path.DirectorySeparatorChar + $"{now:yyyy-MM}.log";
|
||||
File.AppendAllText(filename, outstr, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
// Hooked
|
||||
internal void DoInstanceLog(bool report, string source, string? message) {
|
||||
internal void DoLog(bool report, string source, string? message) {
|
||||
message ??= "(null)";
|
||||
ToConsole(source, message);
|
||||
|
||||
Output(source, message);
|
||||
if (report) Task.Run(() => ReportInstanceWebhook(source, message));
|
||||
}
|
||||
|
||||
|
@ -62,24 +82,8 @@ class LoggingService : Service {
|
|||
Description = message
|
||||
};
|
||||
await _instLogWebhook.SendMessageAsync(embeds: new[] { e.Build() });
|
||||
} catch (Discord.Net.HttpException ex) {
|
||||
DoInstanceLog(false, Name, "Failed to send message to reporting channel: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
// Hooked
|
||||
public void DoGuildLog(ulong guild, string source, string message) {
|
||||
message ??= "(null)";
|
||||
try {
|
||||
using var db = new BotDatabaseContext();
|
||||
db.Add(new GuildLogLine() { GuildId = (long)guild, Source = source, Message = message });
|
||||
db.SaveChanges();
|
||||
#if DEBUG
|
||||
ToConsole($"DEBUG {guild} - {source}", message);
|
||||
#endif
|
||||
} catch (Exception ex) {
|
||||
// Stack trace goes to console only.
|
||||
DoInstanceLog(false, Name, "Error when storing guild log line: " + ex.ToString());
|
||||
DoLog(false, Name, "Failed to send message to reporting channel: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,10 +27,7 @@ class ModuleStateService : Service {
|
|||
}
|
||||
|
||||
private async Task RefreshGuildState(SocketGuild arg) {
|
||||
bool success = await ProcessConfiguration(arg.Id);
|
||||
|
||||
if (success) BotClient._svcLogging.DoInstanceLog(false, GuildLogSource, $"Configuration refreshed for guild ID {arg.Id}.");
|
||||
else BotClient._svcLogging.DoGuildLog(arg.Id, GuildLogSource, "Configuration was not refreshed due to errors.");
|
||||
if (await ProcessConfiguration(arg.Id)) Log($"Configuration refreshed for server {arg.Id}.");
|
||||
}
|
||||
|
||||
private Task RemoveGuildData(SocketGuild arg) {
|
||||
|
@ -79,7 +76,7 @@ class ModuleStateService : Service {
|
|||
throw new InvalidCastException("Configuration is not valid JSON.");
|
||||
}
|
||||
} catch (Exception ex) when (ex is JsonReaderException or InvalidCastException) {
|
||||
BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource, $"A problem exists within the guild configuration: {ex.Message}");
|
||||
Log($"Error loading configuration for server ID {guildId}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -90,25 +87,19 @@ class ModuleStateService : Service {
|
|||
|
||||
// Create guild state objects for all existing modules
|
||||
var newStates = new Dictionary<Type, object?>();
|
||||
foreach (var mod in BotClient.Modules) {
|
||||
var t = mod.GetType();
|
||||
var tn = t.Name;
|
||||
foreach (var module in BotClient.Modules) {
|
||||
var t = module.GetType();
|
||||
try {
|
||||
try {
|
||||
var state = await mod.CreateGuildStateAsync(guildId, guildConf[tn]!);
|
||||
newStates.Add(t, state);
|
||||
} catch (Exception ex) when (ex is not ModuleLoadException) {
|
||||
Log("Unhandled exception while initializing guild state for module:\n" +
|
||||
$"Module: {tn} | " +
|
||||
$"Guild: {guildId} ({BotClient.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" +
|
||||
$"```\n{ex}\n```", true);
|
||||
BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource,
|
||||
"An internal error occurred when attempting to load new configuration.");
|
||||
return false;
|
||||
}
|
||||
var state = await module.CreateGuildStateAsync(guildId, guildConf[module.Name]!);
|
||||
newStates.Add(t, state);
|
||||
} catch (ModuleLoadException ex) {
|
||||
BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource,
|
||||
$"{tn} has encountered an issue with its configuration: {ex.Message}");
|
||||
Log($"{guildId}: Error reading configuration regarding {module.Name}: {ex.Message}");
|
||||
return false;
|
||||
} catch (Exception ex) when (ex is not ModuleLoadException) {
|
||||
Log("Unhandled exception while initializing guild state for module:\n" +
|
||||
$"Module: {module.Name} | " +
|
||||
$"Guild: {guildId} ({BotClient.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" +
|
||||
$"```\n{ex}\n```", true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,5 +19,5 @@ internal abstract class Service {
|
|||
/// </summary>
|
||||
/// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
|
||||
/// <param name="report">Specify if the log message should be sent to a reporting channel.</param>
|
||||
protected void Log(string message, bool report = false) => BotClient._svcLogging.DoInstanceLog(report, Name, message);
|
||||
protected void Log(string message, bool report = false) => BotClient._svcLogging.DoLog(report, Name, message);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue