diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 65f0fe2e3e..e258b67279 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -65,6 +65,7 @@ jobs:
dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }}
dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }}
dotnet pack "src\Discord.Net.Interactions\Discord.Net.Interactions.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }}
+ dotnet pack "src\Discord.Net.OpenTelemetry\Discord.Net.OpenTelemetry.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }}
# dotnet pack "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj" --no-restore --no-build -v minimal -c Release -o ${{ env.ArtifactStagingDirectory }} /p:BuildNumber=${{ env.Suffix }} /p:IsTagBuild=${{ env.IsTagBuild }}
- name: Publish Artifacts
diff --git a/Discord.Net.sln b/Discord.Net.sln
index 48c80d54fe..80b92bee7d 100644
--- a/Discord.Net.sln
+++ b/Discord.Net.sln
@@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.BuildOverrides", "experiment\Discord.Net.BuildOverrides\Discord.Net.BuildOverrides.csproj", "{115F4921-B44D-4F69-996B-69796959C99D}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.OpenTelemetry", "src\Discord.Net.OpenTelemetry\Discord.Net.OpenTelemetry.csproj", "{88D77C2C-547E-41B8-8AFC-D1C089652767}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -244,6 +246,18 @@ Global
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x64.Build.0 = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.ActiveCfg = Release|Any CPU
{115F4921-B44D-4F69-996B-69796959C99D}.Release|x86.Build.0 = Release|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x64.Build.0 = Debug|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Debug|x86.Build.0 = Debug|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|Any CPU.Build.0 = Release|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x64.ActiveCfg = Release|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x64.Build.0 = Release|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x86.ActiveCfg = Release|Any CPU
+ {88D77C2C-547E-41B8-8AFC-D1C089652767}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -264,6 +278,7 @@ Global
{B61AAE66-15CC-40E4-873A-C23E697C3411} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{4A03840B-9EBE-47E3-89AB-E0914DF21AFB} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
{115F4921-B44D-4F69-996B-69796959C99D} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
+ {88D77C2C-547E-41B8-8AFC-D1C089652767} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}
diff --git a/src/Discord.Net.OpenTelemetry/Discord.Net.OpenTelemetry.csproj b/src/Discord.Net.OpenTelemetry/Discord.Net.OpenTelemetry.csproj
new file mode 100644
index 0000000000..f417a0a045
--- /dev/null
+++ b/src/Discord.Net.OpenTelemetry/Discord.Net.OpenTelemetry.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+ net9.0;net8.0;net6.0;net5.0;net461;netstandard2.0;netstandard2.1
+ Discord.OpenTelemetry
+ Discord.Net.OpenTelemetry
+ A Discord.Net extension adding support for the OpenTelemetry (otel) Sdk.
+ 5
+ True
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Discord.Net.OpenTelemetry/Extensions.cs b/src/Discord.Net.OpenTelemetry/Extensions.cs
new file mode 100644
index 0000000000..b90f547cf1
--- /dev/null
+++ b/src/Discord.Net.OpenTelemetry/Extensions.cs
@@ -0,0 +1,41 @@
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+using System;
+
+namespace Discord.OpenTelemetry
+{
+ ///
+ /// An extension class which contains methods to add the Discord.Net OpenTelemetry instrumentation.
+ ///
+ public static class Extensions
+ {
+ private static readonly string[] _sourceNames = ["Discord.Net.WebSocket", "Discord.Net.Audio"];
+
+ ///
+ /// Adds the trace sources of DNet.
+ ///
+ /// The trace provider to add these sources to.
+ /// The provided trace provider to chain calls.
+ ///
+ public static TracerProviderBuilder AddDiscordNetInstrumentation(this TracerProviderBuilder builder)
+ {
+ if (builder is null)
+ throw new ArgumentNullException(nameof(builder));
+ return builder.AddSource(_sourceNames);
+ }
+
+ ///
+ /// Adds the meters of DNet.
+ ///
+ /// The meter provider to add the meters to.
+ /// The provided meter builder to chain calls.
+ ///
+ public static MeterProviderBuilder AddDiscordNetInstrumentation(this MeterProviderBuilder builder)
+ {
+ if (builder is null)
+ throw new ArgumentNullException(nameof(builder));
+ return builder.AddMeter(_sourceNames);
+ }
+ }
+
+}
diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs
index 40ef631dac..1ed7150b88 100644
--- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs
+++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs
@@ -1,14 +1,15 @@
using Discord.API.Voice;
using Discord.Audio.Streams;
using Discord.Logging;
-using Discord.Net;
using Discord.Net.Converters;
using Discord.WebSocket;
+using Discord.WebSocket.Diagnostics;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
@@ -58,6 +59,7 @@ public StreamPair(AudioInStream reader, AudioOutStream writer)
private StopReason _stopReason;
private bool _resuming;
+ public int ClientId { get; }
public SocketGuild Guild { get; }
public DiscordVoiceAPIClient ApiClient { get; private set; }
public int Latency { get; private set; }
@@ -73,13 +75,24 @@ public StreamPair(AudioInStream reader, AudioOutStream writer)
internal AudioClient(SocketGuild guild, int clientId, ulong channelId)
{
Guild = guild;
+ ClientId = clientId;
ChannelId = channelId;
_audioLogger = Discord.LogManager.CreateLogger($"Audio #{clientId}");
ApiClient = new DiscordVoiceAPIClient(guild.Id, Discord.WebSocketProvider, Discord.UdpSocketProvider);
- ApiClient.SentGatewayMessage += async opCode => await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
+ ApiClient.SentGatewayMessage += async opCode =>
+ {
+ AudioMeter.RecordSocketEventSent(opCode, this);
+ await _audioLogger.DebugAsync($"Sent {opCode}").ConfigureAwait(false);
+ };
ApiClient.SentDiscovery += async () => await _audioLogger.DebugAsync("Sent Discovery").ConfigureAwait(false);
- //ApiClient.SentData += async bytes => await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
+ ApiClient.SentData += bytes =>
+ {
+ //await _audioLogger.DebugAsync($"Sent {bytes} Bytes").ConfigureAwait(false);
+ AudioMeter.RecordBytesSent(bytes, this);
+ return Task.CompletedTask;
+ };
+
ApiClient.ReceivedEvent += ProcessMessageAsync;
ApiClient.ReceivedPacket += ProcessPacketAsync;
@@ -87,7 +100,12 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId)
_connection = new ConnectionManager(_stateLock, _audioLogger, ConnectionTimeoutMs,
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
_connection.Connected += () => _connectedEvent.InvokeAsync();
- _connection.Disconnected += (exception, _) => _disconnectedEvent.InvokeAsync(exception);
+ _connection.Disconnected += (exception, reconnect) =>
+ {
+ if (reconnect)
+ AudioMeter.AddAudioReconnect(this);
+ return _disconnectedEvent.InvokeAsync(exception);
+ };
_heartbeatTimes = new ConcurrentQueue();
_keepaliveTimes = new ConcurrentQueue>();
_ssrcMap = new ConcurrentDictionary();
@@ -100,8 +118,16 @@ internal AudioClient(SocketGuild guild, int clientId, ulong channelId)
e.ErrorContext.Handled = true;
};
- LatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false);
- UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false);
+ LatencyUpdated += async (old, val) =>
+ {
+ AudioMeter.RecordSocketLatency((double)val / 1000, this);
+ await _audioLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false);
+ };
+ UdpLatencyUpdated += async (old, val) =>
+ {
+ await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false);
+ AudioMeter.RecordUdpLatency((double)val / 1000, this);
+ };
}
internal Task StartAsync(string url, ulong userId, string sessionId, string token)
@@ -132,6 +158,7 @@ private async Task OnConnectingAsync()
await _audioLogger.DebugAsync($"Connecting ApiClient. Voice server: wss://{_url}").ConfigureAwait(false);
await ApiClient.ConnectAsync($"wss://{_url}?v={DiscordConfig.VoiceAPIVersion}").ConfigureAwait(false);
await _audioLogger.DebugAsync($"Listening on port {ApiClient.UdpPort}").ConfigureAwait(false);
+ AudioMeter.AddAudioConnections(1, this);
if (!_resuming)
{
@@ -151,6 +178,7 @@ private async Task OnDisconnectingAsync(Exception ex)
{
await _audioLogger.DebugAsync("Disconnecting ApiClient").ConfigureAwait(false);
await ApiClient.DisconnectAsync().ConfigureAwait(false);
+ AudioMeter.AddAudioConnections(-1, this);
if (_stopReason == StopReason.Unknown && ex.InnerException is WebSocketException exception)
{
@@ -303,6 +331,9 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
{
_lastMessageTime = Environment.TickCount;
+ var activity = AudioActivity.StartEventReceivedActivity(opCode, this);
+ var watch = Stopwatch.StartNew();
+
try
{
switch (opCode)
@@ -388,7 +419,7 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
_heartbeatTask = RunHeartbeatAsync(_heartbeatInterval, _connection.CancelToken);
_keepaliveTask = RunKeepaliveAsync(_connection.CancelToken);
- _ = _connection.CompleteAsync();
+ _ = _connection.CompleteAsync();
}
break;
default:
@@ -398,11 +429,22 @@ private async Task ProcessMessageAsync(VoiceOpCode opCode, object payload)
}
catch (Exception ex)
{
+ activity?.AddExceptionToActivity(ex);
+ AudioMeter.RecordSocketEventException(ex, opCode, this);
+
await _audioLogger.ErrorAsync($"Error handling {opCode}", ex).ConfigureAwait(false);
}
+ finally
+ {
+ watch.Stop();
+ AudioMeter.RecordSocketEventReceived(watch.Elapsed, opCode, this);
+
+ activity?.Dispose();
+ }
}
private async Task ProcessPacketAsync(byte[] packet)
{
+ AudioMeter.RecordBytesReceived(packet.Length, this);
try
{
if (_connection.State == ConnectionState.Connecting)
diff --git a/src/Discord.Net.WebSocket/Diagnostics/AudioActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/AudioActivity.cs
new file mode 100644
index 0000000000..63bfb790a7
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Diagnostics/AudioActivity.cs
@@ -0,0 +1,32 @@
+using Discord.Audio;
+using Discord.API.Voice;
+using System;
+
+#if NET5_0_OR_GREATER
+using System.Collections.Generic;
+using System.Diagnostics;
+#endif
+
+namespace Discord.WebSocket.Diagnostics
+{
+ public static class AudioActivity
+ {
+#if NET5_0_OR_GREATER
+ private static readonly ActivitySource _source = new("Discord.Net.Audio", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString());
+
+ internal static Activity StartEventReceivedActivity(VoiceOpCode opCode, AudioClient client)
+ {
+ Activity.Current = null; // This activity doesn't have a parent so it have to be explicitly set
+
+ IEnumerable> tags = [
+ .. DiagnosticTags.CreateAudioClientTags(client),
+ .. DiagnosticTags.CreateAudioEventTags(opCode)
+ ];
+ return _source.StartActivity($"process {opCode}", ActivityKind.Consumer, null, tags: tags);
+ }
+
+#else
+ internal static IDisposable StartEventReceivedActivity(VoiceOpCode opCode, AudioClient client) => null;
+#endif
+ }
+}
diff --git a/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs
new file mode 100644
index 0000000000..6c6cad0d1f
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Diagnostics/AudioMeter.cs
@@ -0,0 +1,181 @@
+using System;
+using Discord.Audio;
+using Discord.API.Voice;
+
+#if NET6_0_OR_GREATER
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+#endif
+
+namespace Discord.WebSocket.Diagnostics
+{
+ internal static class AudioMeter
+ {
+#if NET6_0_OR_GREATER
+ private static readonly Meter _meter = new("Discord.Net.Audio", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString());
+
+#if NET7_0_OR_GREATER
+ private static readonly UpDownCounter _socketConnections;
+#endif
+ private static readonly Counter _socketReconnects;
+ private static readonly Histogram _socketLatency;
+
+ private static readonly Counter _socketEventsSentCount;
+ private static readonly Counter _socketEventsReceivedCount;
+ private static readonly Histogram _socketEventsReceivedDuration;
+ private static readonly Counter _socketEventsReceivedExceptions;
+
+ private static readonly Counter _audioBytesReceived;
+ private static readonly Counter _audioBytesSent;
+ private static readonly Histogram _udpLatency;
+
+#if NET9_0_OR_GREATER
+ /*
+ * OTel bucket boundary recommendation for 'http.request.duration':
+ * [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]
+ * (https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration)
+ */
+ private static readonly double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps
+#endif
+
+ static AudioMeter()
+ {
+ // Audio socket
+#if NET7_0_OR_GREATER
+ _socketConnections = _meter.CreateUpDownCounter(
+ name: "discord.audio.connections_count",
+ unit: "Connections",
+ description: "The amount of WebSocket audio connections currently active.");
+#endif
+ _socketReconnects = _meter.CreateCounter(
+ name: "discord.audio.reconnects_count",
+ unit: "Reconnects",
+ description: "The amount WebSocket audio connections reconnecting.");
+ _socketLatency = _meter.CreateHistogram(
+ name: "discord.audio.socket_latency",
+ unit: "Seconds",
+ description: "The latency of the active audio WebSocket connections."
+#if NET9_0_OR_GREATER
+ , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries }
+#endif
+ );
+
+ // Audio socket events
+ _socketEventsSentCount = _meter.CreateCounter(
+ name: "discord.audio.events_sent.count",
+ unit: "Events",
+ description: "The amount of events sent to the audio gateway.");
+ _socketEventsReceivedCount = _meter.CreateCounter(
+ name: "discord.audio.events_received.count",
+ unit: "Events",
+ description: "The amount of events received from the audio gateway.");
+ _socketEventsReceivedDuration = _meter.CreateHistogram(
+ name: "discord.audio.events_received.duration",
+ unit: "Seconds",
+ description: "The duration it took to process events received from the audio gateway.");
+ _socketEventsReceivedExceptions = _meter.CreateCounter(
+ name: "discord.audio.event_received.exception_count",
+ unit: "Exceptions",
+ description: "The amount of exceptions occurred while processing events received from the audio gateway.");
+
+ // UDP data
+ _audioBytesReceived = _meter.CreateCounter(
+ name: "discord.audio.bytes_received",
+ unit: "Bytes",
+ description: "The total amount of bytes received from every UDP audio connection.");
+ _audioBytesSent = _meter.CreateCounter(
+ name: "discord.audio.bytes_sent",
+ unit: "Bytes",
+ description: "The total amount of bytes sent by every UDP audio connection.");
+ _udpLatency = _meter.CreateHistogram(
+ name: "discord.audio.udp_latency",
+ unit: "Seconds",
+ description: "The latency of the open UDP audio 'connections'."
+#if NET9_0_OR_GREATER
+ , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries }
+#endif
+ );
+ }
+
+ internal static void AddAudioConnections(int connections, AudioClient client)
+ {
+#if NET7_0_OR_GREATER
+ _socketConnections.Add(connections, [.. DiagnosticTags.CreateAudioClientTags(client)]);
+#endif
+ }
+
+ internal static void AddAudioReconnect(AudioClient client)
+ {
+ _socketReconnects.Add(1, [.. DiagnosticTags.CreateAudioClientTags(client)]);
+ }
+
+ internal static void RecordSocketLatency(double seconds, AudioClient client)
+ {
+ _socketLatency.Record(seconds, [.. DiagnosticTags.CreateAudioClientTags(client)]);
+ }
+
+ internal static void RecordSocketEventSent(VoiceOpCode op, AudioClient client)
+ {
+ _socketEventsSentCount.Add(1, [
+ .. DiagnosticTags.CreateAudioClientTags(client),
+ .. DiagnosticTags.CreateAudioEventTags(op)
+ ]);
+ }
+
+ internal static void RecordSocketEventReceived(TimeSpan duration, VoiceOpCode op, AudioClient client)
+ {
+ TagList tags = [
+ .. DiagnosticTags.CreateAudioClientTags(client),
+ .. DiagnosticTags.CreateAudioEventTags(op)
+ ];
+ _socketEventsReceivedCount.Add(1, tags);
+ _socketEventsReceivedDuration.Record(duration.TotalSeconds, tags);
+ }
+
+ internal static void RecordSocketEventException(Exception ex, VoiceOpCode op, AudioClient client)
+ {
+ _socketEventsReceivedExceptions.Add(1, [
+ .. DiagnosticTags.CreateAudioClientTags(client),
+ .. DiagnosticTags.CreateAudioEventTags(op),
+ KeyValuePair.Create("exception.type", ex.GetType().ToString()),
+ KeyValuePair.Create("exception.message", ex.Message),
+ KeyValuePair.Create("exception.stacktrace", ex.ToString())
+ ]);
+ }
+
+ internal static void RecordUdpLatency(double seconds, AudioClient client)
+ {
+ _udpLatency.Record(seconds, [.. DiagnosticTags.CreateUdpTags(client)]);
+ }
+
+ internal static void RecordBytesReceived(int amount, AudioClient client)
+ {
+ _audioBytesReceived.Add(amount, [.. DiagnosticTags.CreateUdpTags(client)]);
+ }
+
+ internal static void RecordBytesSent(int amount, AudioClient client)
+ {
+ _audioBytesSent.Add(amount, [.. DiagnosticTags.CreateUdpTags(client)]);
+ }
+#else
+ internal static void AddAudioConnections(int connections, AudioClient client) { }
+
+ internal static void AddAudioReconnect(AudioClient client) { }
+
+ internal static void RecordSocketLatency(double seconds, AudioClient client) { }
+
+ internal static void RecordSocketEventSent(VoiceOpCode op, AudioClient client) { }
+
+ internal static void RecordSocketEventReceived(TimeSpan duration, VoiceOpCode op, AudioClient client) { }
+
+ internal static void RecordSocketEventException(Exception ex, VoiceOpCode op, AudioClient client) { }
+
+ internal static void RecordUdpLatency(double seconds, AudioClient client) { }
+
+ internal static void RecordBytesReceived(int amount, AudioClient client) { }
+
+ internal static void RecordBytesSent(int amount, AudioClient client) { }
+#endif
+ }
+}
diff --git a/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs b/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs
new file mode 100644
index 0000000000..d9bcc1d653
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Diagnostics/BufferedUpDownCounter.cs
@@ -0,0 +1,65 @@
+#if NET7_0_OR_GREATER
+using System.Threading.Tasks;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using System.Collections.ObjectModel;
+
+namespace Discord.WebSocket.Diagnostics
+{
+ ///
+ /// A wrapper around (T is int) which buffers values in cause the instrument isn't enabled yet.
+ ///
+ internal class BufferedUpDownCounter
+ {
+ private readonly Collection<(int value, TagList tags)> _pendingValues = [];
+ private bool _buffering;
+
+ ///
+ /// The instrument this instance will use.
+ ///
+ public UpDownCounter Instrument { get; private set; }
+
+ ///
+ /// Creates a new instance of which buffers .
+ ///
+ /// The instrument to wrap.
+ public BufferedUpDownCounter(UpDownCounter instrument)
+ {
+ Instrument = instrument;
+ }
+
+ ///
+ /// Calls as soon as the instrument is enabled.
+ ///
+ /// The amount to be added.
+ /// Tags to associate with the amount.
+ public void Add(int delta, TagList tags)
+ {
+ if (Instrument.Enabled)
+ {
+ Instrument.Add(delta, tags);
+ }
+ else
+ {
+ _pendingValues.Add((delta, tags));
+ if (!_buffering)
+ {
+ _buffering = true;
+ _ = Task.Run(FlushWhenEnabled);
+ }
+ }
+ }
+
+ private async Task FlushWhenEnabled()
+ {
+ while (!Instrument.Enabled)
+ { await Task.Delay(50); }
+
+ _buffering = false;
+ foreach ((var value, var tags) in _pendingValues)
+ Instrument.Add(value, tags);
+ _pendingValues.Clear();
+ }
+ }
+}
+#endif
diff --git a/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs
new file mode 100644
index 0000000000..38fda7bee4
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Diagnostics/DiagnosticTags.cs
@@ -0,0 +1,45 @@
+#if NET5_0_OR_GREATER
+using Discord.API.Gateway;
+using Discord.API.Voice;
+using Discord.Audio;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.WebSocket.Diagnostics
+{
+ internal static class DiagnosticTags
+ {
+ internal static IEnumerable> CreateSocketClientTags(DiscordSocketClient client) => [
+ KeyValuePair.Create("discord.client.shard_id", client.ShardId),
+ KeyValuePair.Create("discord.client.api_version", $"v{DiscordConfig.APIVersion}"),
+ KeyValuePair.Create("discord.client.gateway_url", client.ApiClient.GatewayUrl)
+ ];
+
+ internal static IEnumerable> CreateEventTags(GatewayOpCode opCode, string type)
+ {
+ IEnumerable> tags = [
+ KeyValuePair.Create("discord.event_op_code", opCode)
+ ];
+ if (!string.IsNullOrEmpty(type))
+ tags = tags.Append(new KeyValuePair("discord.event_op_type", type));
+ return tags;
+ }
+
+ internal static IEnumerable> CreateAudioClientTags(AudioClient client) => [
+ KeyValuePair.Create("discord.audio.client_id", client.ClientId),
+ KeyValuePair.Create("discord.guild_id", client.Guild.Id),
+ KeyValuePair.Create("discord.channel_id", client.ChannelId)
+ ];
+
+ internal static IEnumerable> CreateAudioEventTags(VoiceOpCode opCode) => [
+ KeyValuePair.Create("discord.audio.event_op_code", opCode)
+ ];
+
+ internal static IEnumerable> CreateUdpTags(AudioClient client) => [
+ KeyValuePair.Create("discord.audio.client_port", client.ApiClient.UdpPort),
+ KeyValuePair.Create("discord.audio.server.remote_ip", client.ApiClient.UdpRemoteIp),
+ KeyValuePair.Create("discord.audio.server.remote_port", client.ApiClient.UdpRemotePort)
+ ];
+ }
+}
+#endif
diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs
new file mode 100644
index 0000000000..75bd7f84ec
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Diagnostics/SocketActivity.cs
@@ -0,0 +1,50 @@
+using Discord.API.Gateway;
+using System;
+
+#if NET5_0_OR_GREATER
+using System.Collections.Generic;
+using System.Diagnostics;
+#endif
+
+namespace Discord.WebSocket.Diagnostics
+{
+ internal static class SocketActivity
+ {
+#if NET5_0_OR_GREATER
+ private static readonly ActivitySource _source = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString());
+
+ internal static Activity StartSocketEventActivity(GatewayOpCode opCode, string type, DiscordSocketClient client)
+ {
+ Activity.Current = null; // This activity doesn't have a parent so it have to be explicitly set
+
+ IEnumerable> tags = [
+ .. DiagnosticTags.CreateSocketClientTags(client),
+ .. DiagnosticTags.CreateEventTags(opCode, type),
+ ];
+ return _source.StartActivity($"process {opCode} {type}", ActivityKind.Consumer, null, tags: tags);
+ }
+
+ internal static void AddExceptionToActivity(this Activity activity, Exception ex)
+ {
+#if NET6_0_OR_GREATER
+ activity.SetStatus(ActivityStatusCode.Error, ex.Message);
+#endif
+#if NET9_0_OR_GREATER
+ activity.AddException(ex);
+#else
+ activity.AddEvent(new("exception", tags: new()
+ {
+ { "exception.type", ex.GetType().ToString() },
+ { "exception.message", ex.Message },
+ { "exception.stacktrace", ex.ToString() }
+ }));
+#endif
+ }
+
+#else
+ internal static IDisposable StartSocketEventActivity(GatewayOpCode opCode, string type, DiscordSocketClient client) => null;
+
+ internal static void AddExceptionToActivity(this IDisposable activity, Exception ex) { }
+#endif
+ }
+}
diff --git a/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs
new file mode 100644
index 0000000000..5818d1b961
--- /dev/null
+++ b/src/Discord.Net.WebSocket/Diagnostics/SocketMeter.cs
@@ -0,0 +1,145 @@
+using Discord.API.Gateway;
+using System;
+
+#if NET6_0_OR_GREATER
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+#endif
+
+namespace Discord.WebSocket.Diagnostics
+{
+ internal static class SocketMeter
+ {
+#if NET6_0_OR_GREATER
+ private static readonly Meter _meter = new("Discord.Net.WebSocket", typeof(DiagnosticTags).Assembly.GetName().Version!.ToString());
+
+#if NET7_0_OR_GREATER
+ private static readonly BufferedUpDownCounter _clientShards; // Buffering is especially here required because Add gets called so early where the instrument isn't enabled yet.
+
+ private static readonly BufferedUpDownCounter _socketConnections;
+#endif
+ private static readonly Counter _socketReconnects;
+ private static readonly Histogram _socketConnectionsLatency;
+
+ private static readonly Counter _socketEvents;
+ private static readonly Histogram _socketEventsDuration;
+ private static readonly Counter _socketEventsExceptions;
+
+#if NET9_0_OR_GREATER
+ /*
+ * OTel bucket boundary recommendation for 'http.request.duration':
+ * [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]
+ * (https://github.com/open-telemetry/semantic-conventions/blob/release/v1.23.x/docs/http/http-metrics.md#metric-httpclientrequestduration)
+ */
+ private static readonly double[] _histogramBoundaries = [0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; // Higher resolution in the area from 0.1 to 0.25 in 0.025 steps
+#endif
+
+ static SocketMeter()
+ {
+ // Shard metrics
+#if NET7_0_OR_GREATER
+ _clientShards = new BufferedUpDownCounter(_meter.CreateUpDownCounter(
+ name: "discord.shards_count",
+ unit: "Shards",
+ description: "The amount of shards that currently exists."));
+
+ // Socket client metrics
+ _socketConnections = new BufferedUpDownCounter(_meter.CreateUpDownCounter(
+ name: "discord.socket.connections_count",
+ unit: "Connections",
+ description: "The total amount of WebSocket connections currently connected (should match the amount of 'discord.shards_count')."));
+#endif
+ _socketReconnects = _meter.CreateCounter(
+ name: "discord.socket.reconnects_count",
+ unit: "Reconnects",
+ description: "The amount of WebSocket connections reconnecting.");
+ _socketConnectionsLatency = _meter.CreateHistogram(
+ name: "discord.socket.latency",
+ unit: "Seconds",
+ description: "The latency of the open WebSocket connections."
+#if NET9_0_OR_GREATER
+ , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries }
+#endif
+ );
+
+ // Socket client event metrics
+ _socketEvents = _meter.CreateCounter(
+ name: "discord.events.received_count",
+ unit: "Events",
+ description: "The total amount of events received from the gateway since the application has startet.");
+ _socketEventsDuration = _meter.CreateHistogram(
+ name: "discord.events.duration",
+ unit: "Seconds",
+ description: "The duration to dispatch events received from the gateway."
+#if NET9_0_OR_GREATER
+ , advice: new InstrumentAdvice { HistogramBucketBoundaries = _histogramBoundaries }
+#endif
+ );
+ _socketEventsExceptions = _meter.CreateCounter(
+ name: "discord.events.exceptions_count",
+ unit: "Exceptions",
+ description: "The amount of exceptions occurred while dispatching dispatches sent by the gateway.");
+ }
+
+ internal static void AddClientShards(int shards, DiscordSocketClient client)
+ {
+#if NET7_0_OR_GREATER
+ _clientShards.Add(shards, [.. DiagnosticTags.CreateSocketClientTags(client)]);
+#endif
+ }
+
+ internal static void AddSocketConnections(int connections, DiscordSocketClient client)
+ {
+#if NET7_0_OR_GREATER
+ _socketConnections.Add(connections, [.. DiagnosticTags.CreateSocketClientTags(client)]);
+#endif
+ }
+
+ internal static void AddSocketReconnect(DiscordSocketClient client)
+ {
+ _socketReconnects.Add(1, [.. DiagnosticTags.CreateSocketClientTags(client)]);
+ }
+
+ internal static void RecordConnectionLatency(double seconds, DiscordSocketClient client)
+ {
+ _socketConnectionsLatency.Record(seconds, [.. DiagnosticTags.CreateSocketClientTags(client)]);
+ }
+
+ internal static void RecordSocketEvent(TimeSpan duration, GatewayOpCode opCode, string type, DiscordSocketClient client)
+ {
+ TagList tags = [
+ .. DiagnosticTags.CreateSocketClientTags(client),
+ .. DiagnosticTags.CreateEventTags(opCode, type)
+ ];
+
+ _socketEvents.Add(1, tags);
+ _socketEventsDuration.Record(duration.TotalSeconds, tags);
+ }
+
+ internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketClient client)
+ {
+ TagList tags = [
+ .. DiagnosticTags.CreateSocketClientTags(client),
+ .. DiagnosticTags.CreateEventTags(opCode, type),
+ KeyValuePair.Create("exception.type", ex.GetType().ToString()),
+ KeyValuePair.Create("exception.message", ex.Message),
+ KeyValuePair.Create("exception.stacktrace", ex.ToString()),
+ ];
+ _socketEventsExceptions.Add(1, tags);
+ }
+#else
+ internal static void AddClientShards(int shards, DiscordSocketClient client) { }
+
+ internal static void AddSocketConnections(int connections, DiscordSocketClient client) { }
+
+ internal static void AddSocketReconnect(DiscordSocketClient client) { }
+
+ internal static void RecordConnectionLatency(double seconds, DiscordSocketClient client) { }
+
+ internal static void RecordSocketEvent(TimeSpan duration, GatewayOpCode opCode, string type, DiscordSocketClient client) { }
+
+ internal static void RecordSocketEventException(Exception ex, GatewayOpCode opCode, string type, DiscordSocketClient client) { }
+#endif
+ }
+}
diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
index f33199ba90..197e5b8cc8 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
@@ -46,6 +46,7 @@ internal class DiscordSocketApiClient : DiscordRestApiClient
///
public string GatewayUrl
{
+ get => _gatewayUrl;
set
{
// Makes the sharded client not override the custom value.
diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs
index fdb57e7edd..4424b18f2c 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.EventHandling.cs
@@ -2,26 +2,29 @@
using Discord.API.Gateway;
using Discord.Rest;
using Discord.Utils;
+using Discord.WebSocket.Diagnostics;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using System;
-
-using GameModel = Discord.API.Game;
+using System.Diagnostics;
namespace Discord.WebSocket;
public partial class DiscordSocketClient
{
-
private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string type, object payload)
{
if (seq != null)
_lastSeq = seq.Value;
_lastMessageTime = Environment.TickCount;
+ // An extra Stopwatch is required due to `activity.Duration` cannot be used because its only usable when its stopped but then the metrics won't be associated with this trace when stopped.
+ var activity = SocketActivity.StartSocketEventActivity(opCode, type, this);
+ var watch = Stopwatch.StartNew();
+
try
{
switch (opCode)
@@ -159,7 +162,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
});
_ = _connection.CompleteAsync();
}
- break;
+ break;
case "RESUMED":
{
await _gatewayLogger.DebugAsync("Received Dispatch (RESUMED)").ConfigureAwait(false);
@@ -178,7 +181,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await _gatewayLogger.InfoAsync("Resumed previous session").ConfigureAwait(false);
}
- break;
+ break;
#endregion
#region Guilds
@@ -230,7 +233,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
}
}
}
- break;
+ break;
case "GUILD_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_UPDATE)").ConfigureAwait(false);
@@ -249,7 +252,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_EMOJIS_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_EMOJIS_UPDATE)").ConfigureAwait(false);
@@ -268,7 +271,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_SYNC":
{
await _gatewayLogger.DebugAsync("Ignored Dispatch (GUILD_SYNC)").ConfigureAwait(false);
@@ -291,7 +294,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}*/
}
- break;
+ break;
case "GUILD_DELETE":
{
var data = (payload as JToken).ToObject(_serializer);
@@ -330,7 +333,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
}
}
}
- break;
+ break;
case "GUILD_STICKERS_UPDATE":
{
await _gatewayLogger.DebugAsync($"Received Dispatch (GUILD_STICKERS_UPDATE)").ConfigureAwait(false);
@@ -383,7 +386,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_guildStickerUpdated, nameof(GuildStickerUpdated), before, entityModelPair.Entity);
}
}
- break;
+ break;
#endregion
#region Channels
@@ -423,7 +426,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
if (channel != null)
await TimedInvokeAsync(_channelCreatedEvent, nameof(ChannelCreated), channel).ConfigureAwait(false);
}
- break;
+ break;
case "CHANNEL_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_UPDATE)").ConfigureAwait(false);
@@ -450,7 +453,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "CHANNEL_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_DELETE)").ConfigureAwait(false);
@@ -487,7 +490,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
#region Members
@@ -516,7 +519,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_MEMBER_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_UPDATE)").ConfigureAwait(false);
@@ -560,7 +563,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_MEMBER_REMOVE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBER_REMOVE)").ConfigureAwait(false);
@@ -593,7 +596,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_MEMBERS_CHUNK":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_MEMBERS_CHUNK)").ConfigureAwait(false);
@@ -617,7 +620,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_JOIN_REQUEST_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_JOIN_REQUEST_DELETE)").ConfigureAwait(false);
@@ -639,7 +642,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_guildJoinRequestDeletedEvent, nameof(GuildJoinRequestDeleted), cacheableUser, guild).ConfigureAwait(false);
}
- break;
+ break;
#endregion
#region DM Channels
@@ -660,7 +663,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "CHANNEL_RECIPIENT_REMOVE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (CHANNEL_RECIPIENT_REMOVE)").ConfigureAwait(false);
@@ -683,7 +686,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
@@ -711,7 +714,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_ROLE_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_UPDATE)").ConfigureAwait(false);
@@ -746,7 +749,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_ROLE_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_ROLE_DELETE)").ConfigureAwait(false);
@@ -778,7 +781,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
#region Bans
@@ -807,7 +810,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "GUILD_BAN_REMOVE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (GUILD_BAN_REMOVE)").ConfigureAwait(false);
@@ -833,7 +836,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
#region Messages
@@ -900,7 +903,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
SocketChannelHelper.AddMessage(channel, this, msg);
await TimedInvokeAsync(_messageReceivedEvent, nameof(MessageReceived), msg).ConfigureAwait(false);
}
- break;
+ break;
case "MESSAGE_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_UPDATE)").ConfigureAwait(false);
@@ -986,7 +989,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_messageUpdatedEvent, nameof(MessageUpdated), cacheableBefore, after, channel).ConfigureAwait(false);
}
- break;
+ break;
case "MESSAGE_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE)").ConfigureAwait(false);
@@ -1009,7 +1012,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_messageDeletedEvent, nameof(MessageDeleted), cacheableMsg, cacheableChannel).ConfigureAwait(false);
}
- break;
+ break;
case "MESSAGE_REACTION_ADD":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_ADD)").ConfigureAwait(false);
@@ -1053,7 +1056,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_reactionAddedEvent, nameof(ReactionAdded), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false);
}
- break;
+ break;
case "MESSAGE_REACTION_REMOVE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE)").ConfigureAwait(false);
@@ -1089,7 +1092,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_reactionRemovedEvent, nameof(ReactionRemoved), cacheableMsg, cacheableChannel, reaction).ConfigureAwait(false);
}
- break;
+ break;
case "MESSAGE_REACTION_REMOVE_ALL":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_ALL)").ConfigureAwait(false);
@@ -1110,7 +1113,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_reactionsClearedEvent, nameof(ReactionsCleared), cacheableMsg, cacheableChannel).ConfigureAwait(false);
}
- break;
+ break;
case "MESSAGE_REACTION_REMOVE_EMOJI":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_REACTION_REMOVE_EMOJI)").ConfigureAwait(false);
@@ -1137,7 +1140,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_reactionsRemovedForEmoteEvent, nameof(ReactionsRemovedForEmote), cacheableMsg, cacheableChannel, emote).ConfigureAwait(false);
}
- break;
+ break;
case "MESSAGE_DELETE_BULK":
{
await _gatewayLogger.DebugAsync("Received Dispatch (MESSAGE_DELETE_BULK)").ConfigureAwait(false);
@@ -1166,7 +1169,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_messagesBulkDeletedEvent, nameof(MessagesBulkDeleted), cacheableList, cacheableChannel).ConfigureAwait(false);
}
- break;
+ break;
#endregion
#region Polls
@@ -1222,7 +1225,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_pollVoteAdded, nameof(PollVoteAdded), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId);
}
- break;
+ break;
case "MESSAGE_POLL_VOTE_REMOVE":
{
@@ -1275,7 +1278,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_pollVoteRemoved, nameof(PollVoteRemoved), userCacheable, channelCacheable, messageCacheable, guildCacheable, data.AnswerId);
}
- break;
+ break;
#endregion
@@ -1336,7 +1339,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
user.Update(data);
await TimedInvokeAsync(_presenceUpdated, nameof(PresenceUpdated), user, before, user.Presence).ConfigureAwait(false);
}
- break;
+ break;
case "TYPING_START":
{
await _gatewayLogger.DebugAsync("Received Dispatch (TYPING_START)").ConfigureAwait(false);
@@ -1363,7 +1366,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_userIsTypingEvent, nameof(UserIsTyping), cacheableUser, cacheableChannel).ConfigureAwait(false);
}
- break;
+ break;
#endregion
#region Integrations
@@ -1395,7 +1398,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "INTEGRATION_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_UPDATE)").ConfigureAwait(false);
@@ -1424,7 +1427,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "INTEGRATION_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INTEGRATION_DELETE)").ConfigureAwait(false);
@@ -1449,7 +1452,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
#region Users
@@ -1470,7 +1473,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
#region Voice
@@ -1512,7 +1515,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
//Per g250k, this should always be sent, but apparently not always
user = guild.GetUser(data.UserId)
- ?? (data.Member.IsSpecified ? guild.AddOrUpdateUser(data.Member.Value) : null);
+ ?? (data.Member.IsSpecified ? guild.AddOrUpdateUser(data.Member.Value) : null);
if (user == null)
{
await UnknownGuildUserAsync(type, data.UserId, guild.Id).ConfigureAwait(false);
@@ -1570,7 +1573,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_userVoiceStateUpdatedEvent, nameof(UserVoiceStateUpdated), user, before, after).ConfigureAwait(false);
}
- break;
+ break;
case "VOICE_SERVER_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (VOICE_SERVER_UPDATE)").ConfigureAwait(false);
@@ -1601,7 +1604,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
}
}
- break;
+ break;
case "VOICE_CHANNEL_STATUS_UPDATE":
{
@@ -1619,7 +1622,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_voiceChannelStatusUpdated, nameof(VoiceChannelStatusUpdated), channelCacheable, before, after);
}
- break;
+ break;
#endregion
#region Invites
@@ -1655,7 +1658,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
case "INVITE_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (INVITE_DELETE)").ConfigureAwait(false);
@@ -1678,7 +1681,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
#region Interactions
@@ -1750,7 +1753,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
break;
}
}
- break;
+ break;
case "APPLICATION_COMMAND_CREATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_CREATE)").ConfigureAwait(false);
@@ -1773,7 +1776,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_applicationCommandCreated, nameof(ApplicationCommandCreated), applicationCommand).ConfigureAwait(false);
}
- break;
+ break;
case "APPLICATION_COMMAND_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_UPDATE)").ConfigureAwait(false);
@@ -1796,7 +1799,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_applicationCommandUpdated, nameof(ApplicationCommandUpdated), applicationCommand).ConfigureAwait(false);
}
- break;
+ break;
case "APPLICATION_COMMAND_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (APPLICATION_COMMAND_DELETE)").ConfigureAwait(false);
@@ -1819,7 +1822,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_applicationCommandDeleted, nameof(ApplicationCommandDeleted), applicationCommand).ConfigureAwait(false);
}
- break;
+ break;
#endregion
#region Threads
@@ -1856,7 +1859,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_threadCreated, nameof(ThreadCreated), threadChannel).ConfigureAwait(false);
}
- break;
+ break;
case "THREAD_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_UPDATE)").ConfigureAwait(false);
@@ -1897,7 +1900,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_threadUpdated, nameof(ThreadUpdated), before, threadChannel).ConfigureAwait(false);
}
- break;
+ break;
case "THREAD_DELETE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_DELETE)").ConfigureAwait(false);
@@ -1918,7 +1921,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_threadDeleted, nameof(ThreadDeleted), cacheable).ConfigureAwait(false);
}
- break;
+ break;
case "THREAD_LIST_SYNC":
{
await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_LIST_SYNC)").ConfigureAwait(false);
@@ -1954,7 +1957,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
}
}
}
- break;
+ break;
case "THREAD_MEMBER_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBER_UPDATE)").ConfigureAwait(false);
@@ -1972,7 +1975,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
thread.AddOrUpdateThreadMember(data, thread.Guild.CurrentUser);
}
- break;
+ break;
case "THREAD_MEMBERS_UPDATE":
{
await _gatewayLogger.DebugAsync("Received Dispatch (THREAD_MEMBERS_UPDATE)").ConfigureAwait(false);
@@ -2042,7 +2045,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
}
}
- break;
+ break;
#endregion
#region Stage Channels
@@ -2085,7 +2088,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
return;
}
}
- break;
+ break;
#endregion
#region Guild Scheduled Events
@@ -2107,7 +2110,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_guildScheduledEventCreated, nameof(GuildScheduledEventCreated), newEvent).ConfigureAwait(false);
}
- break;
+ break;
case "GUILD_SCHEDULED_EVENT_UPDATE":
{
await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false);
@@ -2139,7 +2142,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
else
await TimedInvokeAsync(_guildScheduledEventUpdated, nameof(GuildScheduledEventUpdated), beforeCacheable, after).ConfigureAwait(false);
}
- break;
+ break;
case "GUILD_SCHEDULED_EVENT_DELETE":
{
await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false);
@@ -2158,7 +2161,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_guildScheduledEventCancelled, nameof(GuildScheduledEventCancelled), guildEvent).ConfigureAwait(false);
}
- break;
+ break;
case "GUILD_SCHEDULED_EVENT_USER_ADD" or "GUILD_SCHEDULED_EVENT_USER_REMOVE":
{
await _gatewayLogger.DebugAsync($"Received Dispatch ({type})").ConfigureAwait(false);
@@ -2195,7 +2198,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
break;
}
}
- break;
+ break;
#endregion
@@ -2212,7 +2215,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_webhooksUpdated, nameof(WebhooksUpdated), guild, channel);
}
- break;
+ break;
#endregion
@@ -2230,7 +2233,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_auditLogCreated, nameof(AuditLogCreated), auditLog, guild);
}
- break;
+ break;
#endregion
#region Auto Moderation
@@ -2245,7 +2248,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_autoModRuleCreated, nameof(AutoModRuleCreated), rule);
}
- break;
+ break;
case "AUTO_MODERATION_RULE_UPDATE":
{
@@ -2261,7 +2264,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_autoModRuleUpdated, nameof(AutoModRuleUpdated), cacheableBefore, guild.AddOrUpdateAutoModRule(data));
}
- break;
+ break;
case "AUTO_MODERATION_RULE_DELETE":
{
@@ -2273,7 +2276,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_autoModRuleDeleted, nameof(AutoModRuleDeleted), rule);
}
- break;
+ break;
case "AUTO_MODERATION_ACTION_EXECUTION":
{
@@ -2301,14 +2304,14 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
var member = guild.GetUser(data.UserId);
var cacheableUser = new Cacheable(member,
- data.UserId,
- member is not null,
- async () =>
- {
- var model = await ApiClient.GetGuildMemberAsync(data.GuildId, data.UserId);
- return guild.AddOrUpdateUser(model);
- }
- );
+ data.UserId,
+ member is not null,
+ async () =>
+ {
+ var model = await ApiClient.GetGuildMemberAsync(data.GuildId, data.UserId);
+ return guild.AddOrUpdateUser(model);
+ }
+ );
ISocketMessageChannel channel = null;
if (data.ChannelId.IsSpecified)
@@ -2363,7 +2366,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_autoModActionExecuted, nameof(AutoModActionExecuted), guild, action, eventData);
}
- break;
+ break;
#endregion
@@ -2379,7 +2382,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_entitlementCreated, nameof(EntitlementCreated), entitlement);
}
- break;
+ break;
case "ENTITLEMENT_UPDATE":
{
@@ -2403,7 +2406,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_entitlementUpdated, nameof(EntitlementUpdated), cacheableBefore, entitlement);
}
- break;
+ break;
case "ENTITLEMENT_DELETE":
{
@@ -2422,7 +2425,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_entitlementDeleted, nameof(EntitlementDeleted), cacheableEntitlement);
}
- break;
+ break;
case "SUBSCRIPTION_CREATE":
{
@@ -2434,7 +2437,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_subscriptionCreated, nameof(SubscriptionCreated), subscription);
}
- break;
+ break;
case "SUBSCRIPTION_UPDATE":
{
@@ -2458,7 +2461,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_subscriptionUpdated, nameof(SubscriptionUpdated), cacheableBefore, subscription);
}
- break;
+ break;
case "SUBSCRIPTION_DELETE":
{
@@ -2477,7 +2480,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_subscriptionDeleted, nameof(SubscriptionDeleted), cacheableSubscription);
}
- break;
+ break;
#endregion
@@ -2509,7 +2512,7 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
await TimedInvokeAsync(_unknownDispatchReceived, nameof(UnknownDispatchReceived), type, (payload as JToken));
break;
- #endregion
+ #endregion
}
break;
default:
@@ -2526,8 +2529,17 @@ private async Task ProcessMessageAsync(GatewayOpCode opCode, int? seq, string ty
ex.Data["payload_data"] = (payload as JToken).ToString();
}
+ activity?.AddExceptionToActivity(ex);
+ SocketMeter.RecordSocketEventException(ex, opCode, type, this);
+
await _gatewayLogger.ErrorAsync($"Error handling {opCode}{(type != null ? $" ({type})" : "")}", ex).ConfigureAwait(false);
}
- }
+ finally
+ {
+ watch.Stop();
+ SocketMeter.RecordSocketEvent(watch.Elapsed, opCode, type, this);
+ activity?.Dispose();
+ }
+ }
}
diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
index cb7ba9b9d0..dde511d80a 100644
--- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs
@@ -6,7 +6,7 @@
using Discord.Net.WebSockets;
using Discord.Rest;
using Discord.Utils;
-
+using Discord.WebSocket.Diagnostics;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -172,7 +172,12 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie
_connection = new ConnectionManager(_stateLock, _gatewayLogger, config.ConnectionTimeout,
OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x);
_connection.Connected += () => TimedInvokeAsync(_connectedEvent, nameof(Connected));
- _connection.Disconnected += (ex, recon) => TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex);
+ _connection.Disconnected += (ex, recon) =>
+ {
+ if (recon)
+ SocketMeter.AddSocketReconnect(this);
+ return TimedInvokeAsync(_disconnectedEvent, nameof(Disconnected), ex);
+ };
_nextAudioId = 1;
_shardedClient = shardedClient;
@@ -192,7 +197,23 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie
JoinedGuild += async g => await _gatewayLogger.InfoAsync($"Joined {g.Name}").ConfigureAwait(false);
GuildAvailable += async g => await _gatewayLogger.VerboseAsync($"Connected to {g.Name}").ConfigureAwait(false);
GuildUnavailable += async g => await _gatewayLogger.VerboseAsync($"Disconnected from {g.Name}").ConfigureAwait(false);
- LatencyUpdated += async (old, val) => await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false);
+
+ Connected += () =>
+ {
+ SocketMeter.AddSocketConnections(1, this);
+ return Task.CompletedTask;
+ };
+ Disconnected += _ =>
+ {
+ SocketMeter.AddSocketConnections(-1, this);
+ return Task.CompletedTask;
+
+ };
+ LatencyUpdated += async (old, val) =>
+ {
+ SocketMeter.RecordConnectionLatency((double)val / 1000, this);
+ await _gatewayLogger.DebugAsync($"Latency = {val} ms").ConfigureAwait(false);
+ };
GuildAvailable += g =>
{
@@ -205,6 +226,8 @@ private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClie
_largeGuilds = new ConcurrentQueue();
AuditLogCacheSize = config.AuditLogCacheSize;
+
+ SocketMeter.AddClientShards(1, this);
}
private static API.DiscordSocketApiClient CreateApiClient(DiscordSocketConfig config)
=> new DiscordSocketApiClient(config.RestClientProvider, config.WebSocketProvider, DiscordRestConfig.UserAgent, config.GatewayHost,
@@ -220,13 +243,14 @@ internal override void Dispose(bool disposing)
ApiClient?.Dispose();
_stateLock?.Dispose();
}
+
+ SocketMeter.AddClientShards(-1, this);
_isDisposed = true;
}
base.Dispose(disposing);
}
-
internal override async ValueTask DisposeAsync(bool disposing)
{
if (!_isDisposed)
diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs
index cc810d42dd..d1ffda6240 100644
--- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs
+++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs
@@ -48,6 +48,8 @@ internal class DiscordVoiceAPIClient : IDisposable
internal IWebSocketClient WebSocketClient { get; }
public ConnectionState ConnectionState { get; private set; }
+ public string UdpRemoteIp { get; private set; }
+ public int UdpRemotePort { get; private set; }
public ushort UdpPort => _udp.Port;
internal DiscordVoiceAPIClient(ulong guildId, WebSocketProvider webSocketProvider, UdpSocketProvider udpSocketProvider, JsonSerializer serializer = null)
@@ -268,6 +270,8 @@ public async Task SendKeepaliveAsync()
public void SetUdpEndpoint(string ip, int port)
{
+ UdpRemoteIp = ip;
+ UdpRemotePort = port;
_udp.SetDestination(ip, port);
}
#endregion