RegexBot/Kerobot/Services/Logging/LoggingService.cs
Noikoio 47a738ddbc Remove per-guild schema
All guild data will now be stored within the same table, instead of
a new schema being created for each guild. This avoids issues with
missing schemas.
This likely wasn't a good idea to begin with.
2018-12-18 16:21:35 -08:00

194 lines
7.9 KiB
C#

using System;
using System.Threading.Tasks;
using Discord;
using NpgsqlTypes;
namespace Kerobot.Services.Logging
{
/// <summary>
/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
/// Instance logs are messages of varying importance to the bot operator. Guild logs are messages that can be seen
/// by moderators of a particular guild. All log messages are backed by database.
/// Instance logs are stored as guild ID 0.
/// </summary>
class LoggingService : Service
{
// Note: Service.Log's functionality is implemented here. Don't use it within this class.
// If necessary, use DoInstanceLogAsync instead.
internal LoggingService(Kerobot kb) : base(kb)
{
// Create logging table
CreateDatabaseTablesAsync().Wait();
// Discord.Net log handling (client logging option is specified in Program.cs)
kb.DiscordClient.Log += DiscordClient_Log;
// Ready message too
kb.DiscordClient.Ready +=
async delegate { await DoInstanceLogAsync(true, "Kerobot", "Connected and ready."); };
}
/// <summary>
/// Discord.Net logging events handled here.
/// Only events with high importance are kept. Others are just printed to console.
/// </summary>
private async Task DiscordClient_Log(LogMessage arg)
{
var ts = DateTimeOffset.UtcNow;
bool important = arg.Severity > LogSeverity.Info;
string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
const string logSource = "Discord.Net";
if (important) await DoInstanceLogAsync(true, logSource, msg);
else FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
}
#region Database
const string TableLog = "program_log";
private async Task CreateDatabaseTablesAsync()
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
c.CommandText = $"create table if not exists {TableLog} ("
+ "log_id serial primary key, "
+ "guild_id bigint not null, "
+ "log_timestamp timestamptz not null, "
+ "log_source text not null, "
+ "message text not null"
+ ")";
await c.ExecuteNonQueryAsync();
}
using (var c = db.CreateCommand())
{
c.CommandText = "create index if not exists " +
$"{TableLog}_guildid_idx on {TableLog} guild_id";
await c.ExecuteNonQueryAsync();
}
}
}
private async Task TableInsertAsync(ulong guildId, DateTimeOffset timestamp, string source, string message)
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync())
{
using (var c = db.CreateCommand())
{
c.CommandText = $"insert into {TableLog} (guild_id, log_timestamp, log_source, message) values"
+ "(@Gid, @Ts, @Src, @Msg)";
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = guildId;
c.Parameters.Add("@Ts", NpgsqlDbType.TimestampTZ).Value = timestamp;
c.Parameters.Add("@Src", NpgsqlDbType.Text).Value = source;
c.Parameters.Add("@Msg", NpgsqlDbType.Text).Value = message;
c.Prepare();
await c.ExecuteNonQueryAsync();
}
}
}
#endregion
/// <summary>
/// All console writes originate here.
/// Takes incoming details of a log message. Formats the incoming information in a
/// consistent format before writing out the result to console.
/// </summary>
private void FormatToConsole(DateTimeOffset timestamp, string source, string message)
{
var prefix = $"[{timestamp:u}] [{source}] ";
foreach (var line in message.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None))
{
Console.WriteLine(prefix + line);
}
}
/// <summary>
/// See <see cref="Kerobot.InstanceLogAsync(bool, string, string)"/>
/// </summary>
public async Task DoInstanceLogAsync(bool report, string source, string message)
{
FormatToConsole(DateTimeOffset.UtcNow, source, message);
Exception insertException = null;
try
{
await TableInsertAsync(0, DateTimeOffset.UtcNow, source, message);
}
catch (Exception ex)
{
// Not good. Resorting to plain console write to report the error.
Console.WriteLine("!!! Error during recording to instance log: " + ex.Message);
Console.WriteLine(ex.StackTrace);
// Attempt to pass this error to the reporting channel.
insertException = ex;
}
// Report to logging channel if necessary and possible
var (g, c) = Kerobot.Config.InstanceLogReportTarget;
if ((insertException != null || report) &&
g != 0 && c != 0 && Kerobot.DiscordClient.ConnectionState == ConnectionState.Connected)
{
var ch = Kerobot.DiscordClient.GetGuild(g)?.GetTextChannel(c);
if (ch == null) return; // not connected, or channel doesn't exist.
if (insertException != null)
{
// Attempt to report instance logging failure to the reporting channel
try
{
EmbedBuilder e = new EmbedBuilder()
{
Footer = new EmbedFooterBuilder() { Text = Name },
Timestamp = DateTimeOffset.UtcNow,
Description = "Error during recording to instance log.\nCheck the console.",
Color = Color.DarkRed
};
await ch.SendMessageAsync("", embed: e.Build());
}
catch
{
return; // Give up
}
}
if (report)
{
try
{
EmbedBuilder e = new EmbedBuilder()
{
Footer = new EmbedFooterBuilder() { Text = source },
Timestamp = DateTimeOffset.UtcNow,
Description = message
};
await ch.SendMessageAsync("", embed: e.Build());
}
catch (Discord.Net.HttpException ex)
{
await DoInstanceLogAsync(false, Name, "Failed to send message to reporting channel: " + ex.Message);
}
}
}
}
/// <summary>
/// See <see cref="Kerobot.GuildLogAsync(ulong, string, string)"/>
/// </summary>
public async Task DoGuildLogAsync(ulong guild, string source, string message)
{
try
{
await TableInsertAsync(guild, DateTimeOffset.UtcNow, source, message);
}
catch (Exception ex)
{
// This is probably a terrible idea, but...
await DoInstanceLogAsync(true, this.Name, "Failed to store guild log item: " + ex.Message);
// Stack trace goes to console only.
FormatToConsole(DateTime.UtcNow, this.Name, ex.StackTrace);
}
}
}
}