Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix deadlock in DiscordWebhookSink #1018

Merged
merged 1 commit into from
Mar 31, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 100 additions & 77 deletions src/Modix.Services/Utilities/DiscordWebhookSink.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using Discord.Webhook;
using Modix.Services.CodePaste;
Expand All @@ -8,104 +11,124 @@
using Serilog.Core;
using Serilog.Events;

namespace Modix.Services.Utilities
namespace Modix.Services.Utilities;

public sealed class DiscordWebhookSink : ILogEventSink, IAsyncDisposable
{
public sealed class DiscordWebhookSink
: ILogEventSink,
IDisposable
private readonly Lazy<CodePasteService> _codePasteService;
private readonly DiscordWebhookClient _discordWebhookClient;
private readonly IFormatProvider _formatProvider;
private readonly JsonSerializerSettings _jsonSerializerSettings;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly Task _logEventProcessorTask;
private readonly BlockingCollection<LogEvent> _logEventQueue;

public DiscordWebhookSink(
ulong webhookId,
string webhookToken,
IFormatProvider formatProvider,
Lazy<CodePasteService> codePasteService)
{
private readonly Lazy<CodePasteService> _codePasteService;
private readonly DiscordWebhookClient _discordWebhookClient;
private readonly IFormatProvider _formatProvider;
private readonly JsonSerializerSettings _jsonSerializerSettings;
public DiscordWebhookSink(
ulong webhookId,
string webhookToken,
IFormatProvider formatProvider,
Lazy<CodePasteService> codePasteService)
{
_codePasteService = codePasteService;
_discordWebhookClient = new DiscordWebhookClient(webhookId, webhookToken);
_formatProvider = formatProvider;
_codePasteService = codePasteService;
_discordWebhookClient = new DiscordWebhookClient(webhookId, webhookToken);
_formatProvider = formatProvider;

_jsonSerializerSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new ExceptionContractResolver()
};
}
public void Emit(LogEvent logEvent)
_jsonSerializerSettings = new JsonSerializerSettings
{
const int DiscordStringTruncateLength = 1000;

var formattedMessage = logEvent.RenderMessage(_formatProvider);
Formatting = Formatting.Indented,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
ContractResolver = new ExceptionContractResolver()
};

_cancellationTokenSource = new CancellationTokenSource();
_logEventQueue = [];
_logEventProcessorTask = Task.Run(ProcessLogEventItemsAsync, _cancellationTokenSource.Token);
}

var message = new EmbedBuilder()
.WithAuthor("DiscordLogger")
.WithTitle("Modix")
.WithTimestamp(DateTimeOffset.UtcNow)
.WithColor(Color.Red);
public void Emit(LogEvent logEvent)
=> _logEventQueue.Add(logEvent);

public async Task ProcessLogEventItemsAsync()
{
foreach (var logEvent in _logEventQueue.GetConsumingEnumerable(_cancellationTokenSource.Token))
{
try
{
var messagePayload = $"{formattedMessage}\n{logEvent.Exception?.Message}";
const int DiscordStringTruncateLength = 1000;

message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName($"LogLevel: {logEvent.Level}")
.WithValue(Format.Code(messagePayload.TruncateTo(DiscordStringTruncateLength))));
var formattedMessage = logEvent.RenderMessage(_formatProvider);

var eventAsJson = JsonConvert.SerializeObject(logEvent, _jsonSerializerSettings);
var message = new EmbedBuilder()
.WithAuthor("DiscordLogger")
.WithTitle("Modix")
.WithTimestamp(DateTimeOffset.UtcNow)
.WithColor(Color.Red);

var url = _codePasteService.Value.UploadCodeAsync(eventAsJson).GetAwaiter().GetResult();
try
{
var messagePayload = $"{formattedMessage}\n{logEvent.Exception?.Message}";

message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName("Full Log Event")
.WithValue($"[view on paste.mod.gg]({url})"));
}
catch (Exception ex)
{
Console.WriteLine($"Unable to upload log event. {ex}");
message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName($"LogLevel: {logEvent.Level}")
.WithValue(Format.Code(messagePayload.TruncateTo(DiscordStringTruncateLength))));

var eventAsJson = JsonConvert.SerializeObject(logEvent, _jsonSerializerSettings);

var url = await _codePasteService.Value.UploadCodeAsync(eventAsJson);

var stackTracePayload = $"{formattedMessage}\n{logEvent.Exception?.ToString().TruncateTo(DiscordStringTruncateLength)}".TruncateTo(DiscordStringTruncateLength);
message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName("Full Log Event")
.WithValue($"[view on paste.mod.gg]({url})"));
}
catch (Exception ex)
{
Console.WriteLine($"Unable to upload log event. {ex}");

message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName("Stack Trace")
.WithValue(Format.Code(stackTracePayload)));
var stackTracePayload = $"{formattedMessage}\n{logEvent.Exception?.ToString().TruncateTo(DiscordStringTruncateLength)}".TruncateTo(DiscordStringTruncateLength);

message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName("Upload Failure Exception")
.WithValue(Format.Code($"{ex.ToString().TruncateTo(DiscordStringTruncateLength)}")));
message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName("Stack Trace")
.WithValue(Format.Code(stackTracePayload)));

message.AddField(new EmbedFieldBuilder()
.WithIsInline(false)
.WithName("Upload Failure Exception")
.WithValue(Format.Code($"{ex.ToString().TruncateTo(DiscordStringTruncateLength)}")));
}

await _discordWebhookClient.SendMessageAsync(string.Empty, embeds: [message.Build()], username: "Modix Logger");
}
catch
{
// Catching all exceptions as to not crash the processor thread
// Wait an arbitrary amount of time before trying to process the next item.
await Task.Delay(10000);
}
_discordWebhookClient.SendMessageAsync(string.Empty, embeds: new[] { message.Build() }, username: "Modix Logger");
}

public void Dispose()
=> _discordWebhookClient.Dispose();
}

public static class DiscordWebhookSinkExtensions
public async ValueTask DisposeAsync()
{
public static LoggerConfiguration DiscordWebhookSink(this LoggerSinkConfiguration config, ulong id, string token, LogEventLevel minLevel, Lazy<CodePasteService> codePasteService)
{
return config.Sink(new DiscordWebhookSink(id, token, null, codePasteService), minLevel);
}
_discordWebhookClient.Dispose();
await _cancellationTokenSource.CancelAsync();
await _logEventProcessorTask;
_cancellationTokenSource.Dispose();
}
}

public static class LoggingExtensions
{
public static string TruncateTo(this string str, int length)
{
if (str.Length < length)
{
return str;
}
public static class DiscordWebhookSinkExtensions
{
public static LoggerConfiguration DiscordWebhookSink(this LoggerSinkConfiguration config, ulong id, string token, LogEventLevel minLevel, Lazy<CodePasteService> codePasteService)
=> config.Sink(new DiscordWebhookSink(id, token, null, codePasteService), minLevel);
}

return str.Substring(0, length);
}
}
public static class LoggingExtensions
{
public static string TruncateTo(this string str, int length)
=> str.Length < length
? str
: str[..length];
}
Loading