From 7dce08516bb29691cfd05ec27f1b189396c5d2ae Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 23 May 2025 20:01:55 -0500 Subject: [PATCH] feat: add telemetry support with OpenTelemetry and Seq configuration --- .vscode/settings.json | 1 + src/StevesBot.Worker/Discord/Extensions.cs | 2 +- .../Discord/Gateway/DiscordGatewayClient.cs | 4 +- .../Events/UpdatePresenceDiscordEvent.cs | 2 + src/StevesBot.Worker/Program.cs | 3 ++ src/StevesBot.Worker/StevesBot.Worker.csproj | 5 ++ src/StevesBot.Worker/Telemetry/Extensions.cs | 54 +++++++++++++++++++ .../Telemetry/Instrumentation.cs | 13 +++++ src/StevesBot.Worker/Telemetry/SeqOptions.cs | 12 +++++ src/StevesBot.Worker/Usings.cs | 7 +++ src/StevesBot.Worker/appsettings.Example.json | 7 ++- 11 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/StevesBot.Worker/Telemetry/Extensions.cs create mode 100644 src/StevesBot.Worker/Telemetry/Instrumentation.cs create mode 100644 src/StevesBot.Worker/Telemetry/SeqOptions.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index 1bf783c..14ab469 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "cSpell.words": [ "Reconnectable", "Steves", + "stevesbot", "Szalay" ], } \ No newline at end of file diff --git a/src/StevesBot.Worker/Discord/Extensions.cs b/src/StevesBot.Worker/Discord/Extensions.cs index 6ed4d5a..5271625 100644 --- a/src/StevesBot.Worker/Discord/Extensions.cs +++ b/src/StevesBot.Worker/Discord/Extensions.cs @@ -10,7 +10,7 @@ internal static class Extensions var discordOptions = sp.GetRequiredService(); c.BaseAddress = new Uri(discordOptions.ApiUrl); c.DefaultRequestHeaders.Authorization = new("Bot", discordOptions.AppToken); - c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, 0.0.0)"); + c.DefaultRequestHeaders.Add("User-Agent", $"DiscordBot (https://github.com/StevanFreeborn/steves-bot, {Instrumentation.SourceVersion})"); }) .AddStandardResilienceHandler(); diff --git a/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs b/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs index e5289b2..5a39d6a 100644 --- a/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs +++ b/src/StevesBot.Worker/Discord/Gateway/DiscordGatewayClient.cs @@ -1,3 +1,5 @@ +using Activity = StevesBot.Worker.Discord.Gateway.Events.Data.Activity; + namespace StevesBot.Worker.Discord.Gateway; internal sealed class DiscordGatewayClient : IDiscordGatewayClient @@ -201,7 +203,7 @@ internal sealed class DiscordGatewayClient : IDiscordGatewayClient } catch (OperationCanceledException ex) { - _logger.LogInformation(ex, "Receive messages operation canceled: {Message}", ex.Message); + _logger.LogInformation(ex, "Receive messages operation canceled"); } #pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) diff --git a/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs b/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs index 4326b2e..1b30497 100644 --- a/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs +++ b/src/StevesBot.Worker/Discord/Gateway/Events/UpdatePresenceDiscordEvent.cs @@ -1,3 +1,5 @@ +using Activity = StevesBot.Worker.Discord.Gateway.Events.Data.Activity; + namespace StevesBot.Worker.Discord.Gateway.Events; internal sealed record UpdatePresenceDiscordEvent : DiscordEvent diff --git a/src/StevesBot.Worker/Program.cs b/src/StevesBot.Worker/Program.cs index 5b8ecd0..722a736 100644 --- a/src/StevesBot.Worker/Program.cs +++ b/src/StevesBot.Worker/Program.cs @@ -11,6 +11,8 @@ builder.Services.AddSingleton(static sp => return discordOptions; }); +builder.AddTelemetry(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(TimeProvider.System); @@ -23,4 +25,5 @@ builder.Services.AddDiscordGatewayClient(static (client) => builder.Services.AddHostedService(); var host = builder.Build(); + await host.RunAsync(); \ No newline at end of file diff --git a/src/StevesBot.Worker/StevesBot.Worker.csproj b/src/StevesBot.Worker/StevesBot.Worker.csproj index 731550e..581c7c2 100644 --- a/src/StevesBot.Worker/StevesBot.Worker.csproj +++ b/src/StevesBot.Worker/StevesBot.Worker.csproj @@ -8,6 +8,11 @@ + + + + + diff --git a/src/StevesBot.Worker/Telemetry/Extensions.cs b/src/StevesBot.Worker/Telemetry/Extensions.cs new file mode 100644 index 0000000..60c4c4a --- /dev/null +++ b/src/StevesBot.Worker/Telemetry/Extensions.cs @@ -0,0 +1,54 @@ +namespace StevesBot.Worker.Telemetry; + +internal static class Extensions +{ + public static HostApplicationBuilder AddTelemetry(this HostApplicationBuilder builder) + { + builder.Services.AddSingleton(); + + var seq = new SeqOptions(); + builder.Configuration.GetSection(nameof(SeqOptions)).Bind(seq); + + if (seq.IsEnabled is false) + { + return builder; + } + + builder.Services.AddOpenTelemetry() + .ConfigureResource(resource => + { + resource.AddService(Instrumentation.SourceName, Instrumentation.SourceVersion); + resource.AddAttributes(new Dictionary + { + ["service.name"] = Instrumentation.SourceName, + ["service.version"] = Instrumentation.SourceVersion, + ["service.instance.id"] = Environment.MachineName, + ["service.namespace"] = "stevesbot", + ["service.environment"] = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "production", + }); + }) + .WithLogging(lb => + { + lb.AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.Endpoint = new Uri(seq.LogEndpoint); + o.Headers = seq.AuthHeader; + }); + }) + .WithTracing(tb => + { + tb.AddSource(Instrumentation.SourceName); + tb.AddAspNetCoreInstrumentation(); + tb.AddHttpClientInstrumentation(); + tb.AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.Endpoint = new Uri(seq.TraceEndpoint); + o.Headers = seq.AuthHeader; + }); + }); + + return builder; + } +} diff --git a/src/StevesBot.Worker/Telemetry/Instrumentation.cs b/src/StevesBot.Worker/Telemetry/Instrumentation.cs new file mode 100644 index 0000000..9843dc0 --- /dev/null +++ b/src/StevesBot.Worker/Telemetry/Instrumentation.cs @@ -0,0 +1,13 @@ +namespace StevesBot.Worker.Telemetry; + +internal sealed class Instrumentation : IDisposable +{ + public const string SourceName = "StevesBot.Worker"; + public const string SourceVersion = "0.0.0"; + public readonly ActivitySource Source = new(SourceName, SourceVersion); + + public void Dispose() + { + Source.Dispose(); + } +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Telemetry/SeqOptions.cs b/src/StevesBot.Worker/Telemetry/SeqOptions.cs new file mode 100644 index 0000000..cf5b801 --- /dev/null +++ b/src/StevesBot.Worker/Telemetry/SeqOptions.cs @@ -0,0 +1,12 @@ +namespace StevesBot.Worker.Telemetry; + +internal sealed class SeqOptions +{ + public string ServerUrl { get; set; } = string.Empty; + public string ApiKeyHeader { get; set; } = string.Empty; + public string ApiKey { get; set; } = string.Empty; + public bool IsEnabled => !string.IsNullOrWhiteSpace(ServerUrl) && !string.IsNullOrWhiteSpace(ApiKeyHeader) && !string.IsNullOrWhiteSpace(ApiKey); + public string LogEndpoint => $"{ServerUrl}/ingest/otlp/v1/logs"; + public string TraceEndpoint => $"{ServerUrl}/ingest/otlp/v1/traces"; + public string AuthHeader => $"{ApiKeyHeader}={ApiKey}"; +} \ No newline at end of file diff --git a/src/StevesBot.Worker/Usings.cs b/src/StevesBot.Worker/Usings.cs index b4d31d3..2466a9e 100644 --- a/src/StevesBot.Worker/Usings.cs +++ b/src/StevesBot.Worker/Usings.cs @@ -1,3 +1,4 @@ +global using System.Diagnostics; global using System.Net.Http.Json; global using System.Net.WebSockets; global using System.Reflection; @@ -7,6 +8,11 @@ global using System.Text.Json.Serialization; global using Microsoft.Extensions.Options; +global using OpenTelemetry.Exporter; +global using OpenTelemetry.Logs; +global using OpenTelemetry.Resources; +global using OpenTelemetry.Trace; + global using StevesBot.Worker; global using StevesBot.Worker.Discord; global using StevesBot.Worker.Discord.Gateway; @@ -17,5 +23,6 @@ global using StevesBot.Worker.Discord.Rest.Requests; global using StevesBot.Worker.Discord.Rest.Responses; global using StevesBot.Worker.Discord.Shared; global using StevesBot.Worker.Handlers; +global using StevesBot.Worker.Telemetry; global using StevesBot.Worker.Threading; global using StevesBot.Worker.WebSockets; \ No newline at end of file diff --git a/src/StevesBot.Worker/appsettings.Example.json b/src/StevesBot.Worker/appsettings.Example.json index db47611..6b19aad 100644 --- a/src/StevesBot.Worker/appsettings.Example.json +++ b/src/StevesBot.Worker/appsettings.Example.json @@ -9,5 +9,10 @@ "ApiUrl": "ApiUrl", "AppToken": "AppToken", "Intents": 0 + }, + "SeqOptions": { + "ServerUrl": "ServerUrl", + "ApiKeyHeader": "ApiKeyHeader", + "ApiKey": "ApiKey" } -} +} \ No newline at end of file