Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

metrics improvement #95

Merged
merged 6 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,15 @@ The root URL of the API is available here: https://api.helldivers2.dev
> [!WARNING]
> The root domain of the API recently changed, it's recommended you use the domain above to avoid problems in the future

We also ask that you send us a `User-Agent` header when making requests (if accessing directly from the browser,
the headers sent by those should suffice and you don't need to add anything special).
While this is currently not *required*, we are considering making this required in the future, so adding it now
is the safer option.

We also ask that you include an `X-Application-Contact` header with either a Discord, email or other contact handle
so we know how to reach out to you (see below).

We ask this so we can identify the applications making requests, and so we can reach out in case we notice weird or
incorrect behaviour (or we notice you're generating more traffic than we can handle).
We ask that you send along a `X-Super-Client` header with the name of your application / domain
(e.g. `X-Super-Client: api.helldivers2.dev`) and optionally a `X-Super-Contact` with some form of contact if your site
does not link to any form of contact information we can find. We use this information in case we need to notify our users
of important changes to the API that may cause disruption of service or when additional restrictions would be imposed
on your app (to prevent abuse, unintentional or otherwise).

> [!IMPORTANT]
> While adding `X-Super-Client` and `X-Super-Contact` is currently not required, the `X-Super-Client` header **will**
> be made obligatory in the future, causing clients who don't send it to fail. For more information see #94

### Rate limits
Currently the rate limit is set at 5 requests/10 seconds.
Expand Down
74 changes: 74 additions & 0 deletions src/Helldivers-2-API/Metrics/ClientMetric.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Text.RegularExpressions;

namespace Helldivers.API.Metrics;

/// <summary>
/// Handles the logic for generating the `Client` label in the Prometheus metrics for HTTP requests.
/// </summary>
public static partial class ClientMetric
{
/// <summary>
/// The name of a client that couldn't be identified.
/// </summary>
private const string UNKNOWN_CLIENT_NAME = "Unknown";

/// <summary>
/// The name for clients that are developing locally (e.g. localhost, ...).
/// </summary>
private const string LOCAL_DEVELOPMENT_CLIENT_NAME = "Development";

[GeneratedRegex(@"^[a-z0-9]{24}--.+\.netlify\.app$")]
private static partial Regex IsNetlifyApp();

/// <summary>
/// Extracts the name of the client for the incoming request.
/// </summary>
public static string GetClientName(HttpContext context)
{
// If we have an authenticated user, use their name instead
if (context.User.Identity is { Name: { } name })
return name;

// If the client sends `X-Super-Client` we use that name
if (context.Request.Headers.TryGetValue("X-Super-Client", out var superClient))
if (string.IsNullOrWhiteSpace(superClient) is false)
return superClient!;

if (GetBrowserClientName(context.Request) is { } clientName)
return clientName;

return UNKNOWN_CLIENT_NAME;
}

/// <summary>
/// If the client is a browser it sends specific headers along.
/// Attempt to parse out those headers to get the domain and perform some additional normalization.
///
/// If we can't find any of those headers simply return <c>null</c>
/// </summary>
private static string? GetBrowserClientName(HttpRequest request)
{
string? result = null;
if (string.IsNullOrWhiteSpace(request.Headers.Referer) is false)
result = request.Headers.Referer!;
if (string.IsNullOrWhiteSpace(request.Headers.Origin) is false)
result = request.Headers.Origin!;

if (result is not null && Uri.TryCreate(result, UriKind.Absolute, out var uri))
{
// Localhost etc just return local development for a client name.
if (uri.Host.Equals("localhost", StringComparison.InvariantCultureIgnoreCase))
return LOCAL_DEVELOPMENT_CLIENT_NAME;

// Netlify seems to generate urls with random prefixes like following:
// <24 characters random>--<projectname>.netlify.app
// we normalize to <projectname>.netlify.app
if (IsNetlifyApp().IsMatch(uri.Host))
return uri.Host[26..];

return uri.Host;
}

return result;
}
}
22 changes: 22 additions & 0 deletions src/Helldivers-2-API/Middlewares/RedirectFlyDomainMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Helldivers.API.Middlewares;

/// <summary>
/// Automatically generates a redirect if a user still uses the deprecated `fly.io` URL.
/// </summary>
public class RedirectFlyDomainMiddleware : IMiddleware
{
private const string RedirectDomain = "https://api.helldivers2.dev";

/// <inheritdoc />
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Host.Host.Equals("helldivers-2-dotnet.fly.dev", StringComparison.InvariantCultureIgnoreCase))
{
var url = $"{RedirectDomain}{context.Request.Path}";

context.Response.Redirect(url, permanent: true);
Dismissed Show dismissed Hide dismissed
}

await next(context);
}
}
19 changes: 5 additions & 14 deletions src/Helldivers-2-API/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Helldivers.API.Configuration;
using Helldivers.API.Controllers;
using Helldivers.API.Controllers.V1;
using Helldivers.API.Metrics;
using Helldivers.API.Middlewares;
using Helldivers.Core.Extensions;
using Helldivers.Models;
Expand Down Expand Up @@ -42,6 +43,7 @@

// Register the rate limiting middleware.
builder.Services.AddTransient<RateLimitMiddleware>();
builder.Services.AddTransient<RedirectFlyDomainMiddleware>();

// Register the memory cache, used in the rate limiting middleware.
builder.Services.AddMemoryCache();
Expand Down Expand Up @@ -181,23 +183,12 @@

var app = builder.Build();

app.UseMiddleware<RedirectFlyDomainMiddleware>();

// Track telemetry for Prometheus (Fly.io metrics)
app.UseHttpMetrics(options =>
{
options.AddCustomLabel("Client", context =>
{
if (context.User.Identity is { Name: { } name })
return name;

// TODO: document custom header that clients can send to identify themselves.

if (string.IsNullOrWhiteSpace(context.Request.Headers.Referer) is false)
return context.Request.Headers.Referer!;
if (string.IsNullOrWhiteSpace(context.Request.Headers.Origin) is false)
return context.Request.Headers.Origin!;

return "Unknown";
});
options.AddCustomLabel("Client", ClientMetric.GetClientName);
});

// Use response compression for smaller payload sizes
Expand Down
6 changes: 3 additions & 3 deletions src/Helldivers-2-Sync/Hosted/ArrowHeadSyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ public sealed partial class ArrowHeadSyncService(
StorageFacade storage
) : BackgroundService
{
private static readonly Histogram SteamSyncMetric =
Metrics.CreateHistogram("helldivers_sync_steam", "All Steam synchronizations");
private static readonly Histogram ArrowHeadSyncMetric =
Metrics.CreateHistogram("helldivers_sync_arrowhead", "All ArrowHead synchronizations");

#region Source generated logging

Expand All @@ -49,7 +49,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
try
{
using var _ = SteamSyncMetric.NewTimer();
using var _ = ArrowHeadSyncMetric.NewTimer();
await using var scope = scopeFactory.CreateAsyncScope();

await SynchronizeAsync(scope.ServiceProvider, cancellationToken);
Expand Down
6 changes: 3 additions & 3 deletions src/Helldivers-2-Sync/Hosted/SteamSyncService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public sealed partial class SteamSyncService(
IServiceScopeFactory scopeFactory
) : BackgroundService
{
private static readonly Histogram ArrowHeadSyncMetric =
Metrics.CreateHistogram("helldivers_sync_arrowhead", "All ArrowHead synchronizations");
private static readonly Histogram SteamSyncMetric =
Metrics.CreateHistogram("helldivers_sync_steam", "All Steam synchronizations");

#region Source generated logging

Expand All @@ -35,7 +35,7 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
try
{
using var _ = ArrowHeadSyncMetric.NewTimer();
using var _ = SteamSyncMetric.NewTimer();
await using var scope = scopeFactory.CreateAsyncScope();

var feed = await scope
Expand Down
1 change: 0 additions & 1 deletion src/Helldivers-2-Sync/Services/ArrowHeadApiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Helldivers.Sync.Configuration;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text.Json;

namespace Helldivers.Sync.Services;
Expand Down