Skip to content

Commit

Permalink
Fix deadlock in DiscordWebhookSink whenever an error occurs in a cont…
Browse files Browse the repository at this point in the history
…ext where a SynchronizationContext exists (e.g. Blazor)
  • Loading branch information
calledude committed Mar 30, 2024
1 parent 66ebd86 commit 3758697
Showing 1 changed file with 100 additions and 77 deletions.
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];
}

0 comments on commit 3758697

Please sign in to comment.