RegexBot/Kerobot/Services/Logging/LoggingService.cs
2018-06-06 16:01:54 -07:00

192 lines
7.5 KiB
C#

using Discord;
using Npgsql;
using NpgsqlTypes;
using System;
using System.Threading.Tasks;
namespace Kerobot.Services.Logging
{
/// <summary>
/// Implements logging for the whole program.
/// </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 global instance log table
async Task CreateGlobalTable()
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
await CreateDatabaseTablesAsync(db);
}
CreateGlobalTable().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 severity are placed in the log. 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)
{
// Note: Using external method here!
await Kerobot.InstanceLogAsync(true, logSource, msg);
}
else
{
FormatToConsole(DateTimeOffset.UtcNow, logSource, msg);
}
}
const string TableLog = "program_log";
public override async Task CreateDatabaseTablesAsync(NpgsqlConnection db)
{
using (var c = db.CreateCommand())
{
c.CommandText = $"create table if not exists {TableLog} ("
+ "log_id serial primary key, "
+ "log_timestamp timestamptz not null, "
+ "log_source text not null, "
+ "message text not null"
+ ")";
await c.ExecuteNonQueryAsync();
}
}
private async Task TableInsertAsync(NpgsqlConnection db, DateTimeOffset timestamp, string source, string message)
{
using (var c = db.CreateCommand())
{
c.CommandText = $"insert into {TableLog} (log_timestamp, log_source, message) values"
+ "(@Ts, @Src, @Msg)";
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();
}
}
/// <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
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(null))
{
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
}
}
catch (Exception ex)
{
// This is not good. Resorting to plain console write to report the issue.
// Let's hope a warning reaches the reporting channel.
Console.WriteLine("!!! Error during recording to instance log: " + ex.Message);
Console.WriteLine(ex.StackTrace);
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
{
using (var db = await Kerobot.GetOpenNpgsqlConnectionAsync(guild))
{
await TableInsertAsync(db, DateTimeOffset.UtcNow, source, message);
}
}
catch (Exception ex)
{
// Probably a bad 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);
}
}
}
}