Skip to content

Commit

Permalink
Add telemetry
Browse files Browse the repository at this point in the history
  • Loading branch information
austins committed Sep 20, 2024
1 parent dd7175a commit 45b35da
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/DiscordTranslationBot/DiscordTranslationBot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.9.1"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2"/>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0"/>
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0"/>
<PackageReference Include="Refit.HttpClientFactory" Version="7.2.1"/>
<PackageReference Include="Unicode.net" Version="2.0.0"/>
</ItemGroup>
Expand Down
11 changes: 9 additions & 2 deletions src/DiscordTranslationBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@
using DiscordTranslationBot.Extensions;
using DiscordTranslationBot.Mediator;
using DiscordTranslationBot.Providers.Translation;
using DiscordTranslationBot.Telemetry;
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using DiscordEventListener = DiscordTranslationBot.Discord.DiscordEventListener;

var builder = WebApplication.CreateSlimBuilder(args);

// Logging.
builder.Logging.AddSimpleConsole(o => o.TimestampFormat = "HH:mm:ss.fff ");

// Set up configuration.
// Telemetry.
builder.AddTelemetry();

// Configuration.
builder
.Services
.AddOptions<DiscordOptions>()
.Bind(builder.Configuration.GetRequiredSection(DiscordOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();

// Set up services.
// Main services.
builder
.Services
.AddTranslationProviders(builder.Configuration)
Expand Down Expand Up @@ -54,6 +60,7 @@

var app = builder.Build();

app.UseTelemetry();
app.UseRateLimiter();

app
Expand Down
66 changes: 66 additions & 0 deletions src/DiscordTranslationBot/Telemetry/TelemetryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.Extensions.Options;
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

namespace DiscordTranslationBot.Telemetry;

internal static class TelemetryExtensions
{
public static void AddTelemetry(this WebApplicationBuilder builder)
{
var section = builder.Configuration.GetSection(TelemetryOptions.SectionName);
builder.Services.AddOptions<TelemetryOptions>().Bind(section).ValidateDataAnnotations().ValidateOnStart();

var options = section.Get<TelemetryOptions>();
if (options?.Enabled != true)
{
return;
}

var headers = $"X-Seq-ApiKey={options.ApiKey}";

builder.Logging.AddOpenTelemetry(
o => o.AddOtlpExporter(
e =>
{
e.Protocol = OtlpExportProtocol.HttpProtobuf;
e.Endpoint = options.LoggingEndpointUrl!;
e.Headers = headers;
}));

builder
.Services
.AddOpenTelemetry()
.ConfigureResource(
b => b
.AddService(builder.Environment.ApplicationName)
.AddAttributes(
new Dictionary<string, object> { ["environment"] = builder.Environment.EnvironmentName }))
.WithMetrics(b => b.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation().AddPrometheusExporter())
.WithTracing(
b => b
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(
e =>
{
e.Protocol = OtlpExportProtocol.HttpProtobuf;
e.Endpoint = options.TracingEndpointUrl!;
e.Headers = headers;
}));
}

public static void UseTelemetry(this WebApplication app)
{
var options = app.Services.GetRequiredService<IOptions<TelemetryOptions>>();
if (!options.Value.Enabled)
{
return;
}

app.MapPrometheusScrapingEndpoint("/_metrics");
}
}
58 changes: 58 additions & 0 deletions src/DiscordTranslationBot/Telemetry/TelemetryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;

namespace DiscordTranslationBot.Telemetry;

public sealed class TelemetryOptions : IValidatableObject
{
/// <summary>
/// Configuration section name for <see cref="TelemetryOptions" />.
/// </summary>
public const string SectionName = "Telemetry";

/// <summary>
/// Flag indicating whether telemetry is enabled.
/// </summary>
public bool Enabled { get; init; }

/// <summary>
/// The API key for Seq used by <see cref="LoggingEndpointUrl" /> and <see cref="TracingEndpointUrl" />.
/// </summary>
public string? ApiKey { get; init; }

/// <summary>
/// The URL for logging endpoint.
/// </summary>
public Uri? LoggingEndpointUrl { get; init; }

/// <summary>
/// The URL for tracing endpoint.
/// </summary>
public Uri? TracingEndpointUrl { get; init; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Enabled)
{
if (string.IsNullOrWhiteSpace(ApiKey))
{
yield return new ValidationResult(
$"{nameof(TelemetryOptions)}.{nameof(ApiKey)} is required.",
[nameof(ApiKey)]);
}

if (LoggingEndpointUrl?.IsAbsoluteUri != true)
{
yield return new ValidationResult(
$"{nameof(TelemetryOptions)}.{nameof(LoggingEndpointUrl)} is must be an absolute URI.",
[nameof(LoggingEndpointUrl)]);
}

if (TracingEndpointUrl?.IsAbsoluteUri != true)
{
yield return new ValidationResult(
$"{nameof(TelemetryOptions)}.{nameof(TracingEndpointUrl)} is must be an absolute URI.",
[nameof(TracingEndpointUrl)]);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using DiscordTranslationBot.Extensions;
using DiscordTranslationBot.Telemetry;

namespace DiscordTranslationBot.Tests.Unit.Telemetry;

public sealed class TelemetryOptionsTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Valid_Options_ValidatesWithoutErrors(bool enabled)
{
// Arrange
var options = enabled
? new TelemetryOptions
{
Enabled = true,
ApiKey = "apikey",
LoggingEndpointUrl = new Uri("http://localhost:1234"),
TracingEndpointUrl = new Uri("http://localhost:1234")
}
: new TelemetryOptions();

// Act
var isValid = options.TryValidate(out var validationResults);

// Assert
isValid.Should().BeTrue();
validationResults.Should().BeEmpty();
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Invalid_Options_HasValidationErrors(string? stringValue)
{
// Arrange
var options = new TelemetryOptions
{
Enabled = true,
ApiKey = stringValue,
LoggingEndpointUrl = null,
TracingEndpointUrl = null
};

// Act
var isValid = options.TryValidate(out var validationResults);

// Assert
isValid.Should().BeFalse();

validationResults
.Should()
.HaveCount(3)
.And
.ContainSingle(
x => x.ErrorMessage!.Contains($"{nameof(TelemetryOptions)}.{nameof(TelemetryOptions.ApiKey)}"))
.And
.ContainSingle(
x => x.ErrorMessage!.Contains(
$"{nameof(TelemetryOptions)}.{nameof(TelemetryOptions.LoggingEndpointUrl)}"))
.And
.ContainSingle(
x => x.ErrorMessage!.Contains(
$"{nameof(TelemetryOptions)}.{nameof(TelemetryOptions.TracingEndpointUrl)}"));
}
}

0 comments on commit 45b35da

Please sign in to comment.