Skip to content

Commit

Permalink
Improve telemetry (#39)
Browse files Browse the repository at this point in the history
Mark 404, 403, 401, 429 as successful
Drop request durations for websockets
  • Loading branch information
AlexMacocian authored Nov 28, 2023
1 parent 53fa62c commit 9a7b1e7
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 22 deletions.
7 changes: 7 additions & 0 deletions GuildWarsPartySearch/Attributes/OptionsNameAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace GuildWarsPartySearch.Server.Attributes;

[AttributeUsage(AttributeTargets.Class)]
public sealed class OptionsNameAttribute : Attribute
{
public string? Name { get; set; }
}
8 changes: 8 additions & 0 deletions GuildWarsPartySearch/Config.Debug.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,13 @@
},
"ApplicationInsights": {
"InstrumentationKey": "[AZURE_INSIGHTS_INSTRUMENTATIONKEY]"
},
"TelemetryOptions": {
"SuccessfulStatusCodes": [
401,
403,
404,
429
]
}
}
8 changes: 8 additions & 0 deletions GuildWarsPartySearch/Config.Release.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,13 @@
},
"ApplicationInsights": {
"InstrumentationKey": "[AZURE_INSIGHTS_INSTRUMENTATIONKEY]"
},
"TelemetryOptions": {
"SuccessfulStatusCodes": [
401,
403,
404,
429
]
}
}
28 changes: 28 additions & 0 deletions GuildWarsPartySearch/Extensions/WebApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using GuildWarsPartySearch.Server.Attributes;
using System.Core.Extensions;
using System.Extensions;

namespace GuildWarsPartySearch.Server.Extensions;

public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder ConfigureExtended<TOptions>(this WebApplicationBuilder builder)
where TOptions : class, new()
{
builder.ThrowIfNull()
.Services.Configure<TOptions>(builder.Configuration.GetSection(GetOptionsName<TOptions>()));
return builder;
}

private static string GetOptionsName<TOptions>()
{
var maybeAttribute = typeof(TOptions).GetCustomAttributes(false).OfType<OptionsNameAttribute>().FirstOrDefault();
if (maybeAttribute is not null &&
maybeAttribute.Name?.IsNullOrWhiteSpace() is false)
{
return maybeAttribute.Name;
}

return typeof(TOptions).Name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ private static async Task ProcessWebSocketRequest(WebSocketRouteBase route, Http
route.WebSocket = webSocket;
route.Context = httpContext;
await route.SocketAccepted(httpContext.RequestAborted);
httpContext.Request.Scheme = httpContext.Request.Scheme == "https" ? "wss" : "ws";
await HandleWebSocket(webSocket, route, httpContext.RequestAborted);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", httpContext.RequestAborted);
}
Expand Down
23 changes: 13 additions & 10 deletions GuildWarsPartySearch/Launch/ServerConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using GuildWarsPartySearch.Server.Services.Feed;
using GuildWarsPartySearch.Server.Services.Lifetime;
using GuildWarsPartySearch.Server.Services.PartySearch;
using GuildWarsPartySearch.Server.Telemetry;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Options;
using System.Core.Extensions;
Expand Down Expand Up @@ -62,23 +64,24 @@ public static ILoggingBuilder SetupLogging(this ILoggingBuilder builder)

public static WebApplicationBuilder SetupOptions(this WebApplicationBuilder builder)
{
builder.ThrowIfNull()
.Services.Configure<EnvironmentOptions>(builder.Configuration.GetSection(nameof(EnvironmentOptions)))
.Configure<ContentOptions>(builder.Configuration.GetSection(nameof(ContentOptions)))
.Configure<PartySearchTableOptions>(builder.Configuration.GetSection(nameof(PartySearchTableOptions)))
.Configure<StorageAccountOptions>(builder.Configuration.GetSection(nameof(StorageAccountOptions)))
.Configure<ServerOptions>(builder.Configuration.GetSection(nameof(ServerOptions)))
.Configure<IpRateLimitOptions>(builder.Configuration.GetSection(nameof(IpRateLimitOptions)))
.Configure<IpRateLimitPolicies>(builder.Configuration.GetSection(nameof(IpRateLimitPolicies)));

return builder;
return builder.ThrowIfNull()
.ConfigureExtended<EnvironmentOptions>()
.ConfigureExtended<ContentOptions>()
.ConfigureExtended<PartySearchTableOptions>()
.ConfigureExtended<StorageAccountOptions>()
.ConfigureExtended<ServerOptions>()
.ConfigureExtended<IpRateLimitOptions>()
.ConfigureExtended<IpRateLimitPolicies>()
.ConfigureExtended<TelemetryOptions>();
}

public static IServiceCollection SetupServices(this IServiceCollection services)
{
services.ThrowIfNull();

services.AddApplicationInsightsTelemetry();
services.AddSingleton<ITelemetryInitializer, Mark4xxAsSuccessfulTelemetryInitializer>();
services.AddApplicationInsightsTelemetryProcessor<WebSocketTelemetryProcessor>();
services.AddMemoryCache();
services.AddInMemoryRateLimiting();
services.AddScoped<ApiKeyProtected>();
Expand Down
9 changes: 8 additions & 1 deletion GuildWarsPartySearch/Options/ContentOptions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
namespace GuildWarsPartySearch.Server.Options;
using System.Text.Json.Serialization;

namespace GuildWarsPartySearch.Server.Options;

public sealed class ContentOptions : IAzureBlobStorageOptions
{
[JsonPropertyName(nameof(UpdateFrequency))]
public TimeSpan UpdateFrequency { get; set; } = TimeSpan.FromMinutes(5);

[JsonPropertyName(nameof(StagingFolder))]
public string StagingFolder { get; set; } = "Content";

[JsonPropertyName(nameof(ContainerName))]
public string ContainerName { get; set; } = default!;
}
5 changes: 4 additions & 1 deletion GuildWarsPartySearch/Options/EnvironmentOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
namespace GuildWarsPartySearch.Server.Options;
using System.Text.Json.Serialization;

namespace GuildWarsPartySearch.Server.Options;

public sealed class EnvironmentOptions
{
[JsonPropertyName(nameof(Name))]
public string? Name { get; init; }
}
5 changes: 4 additions & 1 deletion GuildWarsPartySearch/Options/PartySearchTableOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
namespace GuildWarsPartySearch.Server.Options;
using System.Text.Json.Serialization;

namespace GuildWarsPartySearch.Server.Options;

public class PartySearchTableOptions : IAzureTableStorageOptions
{
[JsonPropertyName(nameof(TableName))]
public string TableName { get; set; } = default!;
}
10 changes: 5 additions & 5 deletions GuildWarsPartySearch/Options/ServerOptions.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;

namespace GuildWarsPartySearch.Server.Options;

public sealed class ServerOptions
{
[JsonProperty(nameof(Certificate))]
[JsonPropertyName(nameof(Certificate))]
public string? Certificate { get; set; }

[JsonProperty(nameof(ApiKey))]
[JsonPropertyName(nameof(ApiKey))]
public string? ApiKey { get; set; }

[JsonProperty(nameof(InactivityTimeout))]
[JsonPropertyName(nameof(InactivityTimeout))]
public TimeSpan? InactivityTimeout { get; set; }

[JsonProperty(nameof(HeartbeatFrequency))]
[JsonPropertyName(nameof(HeartbeatFrequency))]
public TimeSpan? HeartbeatFrequency { get; set; }
}
8 changes: 4 additions & 4 deletions GuildWarsPartySearch/Options/StorageAccountOptions.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using Newtonsoft.Json;
using System.Text.Json.Serialization;

namespace GuildWarsPartySearch.Server.Options;

public sealed class StorageAccountOptions
{
[JsonProperty(nameof(TableName))]
[JsonPropertyName(nameof(TableName))]
public string? TableName { get; set; }

[JsonProperty(nameof(ConnectionString))]
[JsonPropertyName(nameof(ConnectionString))]
public string? ConnectionString { get; set; }

[JsonProperty(nameof(ContainerName))]
[JsonPropertyName(nameof(ContainerName))]
public string? ContainerName { get; set; }
}
9 changes: 9 additions & 0 deletions GuildWarsPartySearch/Options/TelemetryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json.Serialization;

namespace GuildWarsPartySearch.Server.Options;

public sealed class TelemetryOptions
{
[JsonPropertyName(nameof(SuccessfulStatusCodes))]
public List<int>? SuccessfulStatusCodes { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using GuildWarsPartySearch.Server.Options;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.Options;
using System.Core.Extensions;

namespace GuildWarsPartySearch.Server.Telemetry;

public sealed class Mark4xxAsSuccessfulTelemetryInitializer : ITelemetryInitializer
{
private readonly TelemetryOptions telemetryOptions;

public Mark4xxAsSuccessfulTelemetryInitializer(
IOptions<TelemetryOptions> options)
{
this.telemetryOptions = options.ThrowIfNull().Value;
}

public void Initialize(ITelemetry telemetry)
{
if (telemetry is not RequestTelemetry requestTelemetry)
{
return;
}

if (!int.TryParse(requestTelemetry.ResponseCode, out var statusCode))
{
return;
}

if (this.telemetryOptions.SuccessfulStatusCodes?.Contains(statusCode) is true)
{
requestTelemetry.Success = true;
}

return;
}
}
31 changes: 31 additions & 0 deletions GuildWarsPartySearch/Telemetry/WebSocketTelemetryProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using System.Core.Extensions;

namespace GuildWarsPartySearch.Server.Telemetry;

public sealed class WebSocketTelemetryProcessor : ITelemetryProcessor
{
private readonly ITelemetryProcessor next;

public WebSocketTelemetryProcessor(ITelemetryProcessor next)
{
this.next = next.ThrowIfNull();
}

public void Process(ITelemetry item)
{
if (item is not RequestTelemetry request)
{
return;
}

if (request.Url.Scheme is "ws" or "wss")
{
request.Duration = TimeSpan.Zero;
}

this.next.Process(item);
}
}

0 comments on commit 9a7b1e7

Please sign in to comment.