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