Skip to content

Commit

Permalink
Add AddMetricsConsumer helper to TelemetryConsumption (#1899)
Browse files Browse the repository at this point in the history
  • Loading branch information
MihaZupan committed Oct 19, 2022
1 parent be1f6be commit 6b4195a
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 27 deletions.
2 changes: 1 addition & 1 deletion samples/ReverseProxy.Metrics.Sample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddHttpContextAccessor();

// Interface that collects general metrics about the proxy forwarder
services.AddSingleton<IMetricsConsumer<ForwarderMetrics>, ForwarderMetricsConsumer>();
services.AddMetricsConsumer<ForwarderMetricsConsumer>();

// Registration of a consumer to events for proxy forwarder telemetry
services.AddTelemetryConsumer<ForwarderTelemetryConsumer>();
Expand Down
2 changes: 1 addition & 1 deletion src/TelemetryConsumption/EventListenerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private void EnableEventSource(EventSource eventSource)
return;
}

var eventLevel = enableEvents ? EventLevel.Verbose : EventLevel.Critical;
var eventLevel = enableEvents ? EventLevel.Informational : EventLevel.Critical;
var arguments = enableMetrics ? new Dictionary<string, string?> { { "EventCounterIntervalSec", MetricsOptions.Interval.TotalSeconds.ToString() } } : null;

EnableEvents(eventSource, eventLevel, EventKeywords.None, arguments);
Expand Down
109 changes: 109 additions & 0 deletions src/TelemetryConsumption/TelemetryConsumptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,113 @@ public static IServiceCollection AddTelemetryConsumer<TConsumer>(this IServiceCo

return services;
}

/// <summary>
/// Registers a consumer singleton for every IMetricsConsumer interface it implements.
/// </summary>
public static IServiceCollection AddMetricsConsumer(this IServiceCollection services, object consumer)
{
var implementsAny = false;

if (consumer is IMetricsConsumer<ForwarderMetrics> forwarderMetricsConsumer)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton(forwarderMetricsConsumer));
implementsAny = true;
}

if (consumer is IMetricsConsumer<KestrelMetrics> kestrelMetricsConsumer)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton(kestrelMetricsConsumer));
implementsAny = true;
}

if (consumer is IMetricsConsumer<HttpMetrics> httpMetricsConsumer)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton(httpMetricsConsumer));
implementsAny = true;
}

if (consumer is IMetricsConsumer<NameResolutionMetrics> nameResolutionMetricsConsumer)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton(nameResolutionMetricsConsumer));
implementsAny = true;
}

if (consumer is IMetricsConsumer<NetSecurityMetrics> netSecurityMetricsConsumer)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton(netSecurityMetricsConsumer));
implementsAny = true;
}

if (consumer is IMetricsConsumer<SocketsMetrics> socketsMetricsConsumer)
{
services.TryAddEnumerable(ServiceDescriptor.Singleton(socketsMetricsConsumer));
implementsAny = true;
}

if (!implementsAny)
{
throw new ArgumentException("The consumer must implement at least one IMetricsConsumer interface.", nameof(consumer));
}

services.AddTelemetryListeners();

return services;
}

/// <summary>
/// Registers a consumer singleton for every IMetricsConsumer interface it implements.
/// </summary>
public static IServiceCollection AddMetricsConsumer<TConsumer>(this IServiceCollection services)
where TConsumer : class
{
var implementsAny = false;

if (typeof(IMetricsConsumer<ForwarderMetrics>).IsAssignableFrom(typeof(TConsumer)))
{
services.AddSingleton(services => (IMetricsConsumer<ForwarderMetrics>)services.GetRequiredService<TConsumer>());
implementsAny = true;
}

if (typeof(IMetricsConsumer<KestrelMetrics>).IsAssignableFrom(typeof(TConsumer)))
{
services.AddSingleton(services => (IMetricsConsumer<KestrelMetrics>)services.GetRequiredService<TConsumer>());
implementsAny = true;
}

if (typeof(IMetricsConsumer<HttpMetrics>).IsAssignableFrom(typeof(TConsumer)))
{
services.AddSingleton(services => (IMetricsConsumer<HttpMetrics>)services.GetRequiredService<TConsumer>());
implementsAny = true;
}

if (typeof(IMetricsConsumer<NameResolutionMetrics>).IsAssignableFrom(typeof(TConsumer)))
{
services.AddSingleton(services => (IMetricsConsumer<NameResolutionMetrics>)services.GetRequiredService<TConsumer>());
implementsAny = true;
}

if (typeof(IMetricsConsumer<NetSecurityMetrics>).IsAssignableFrom(typeof(TConsumer)))
{
services.AddSingleton(services => (IMetricsConsumer<NetSecurityMetrics>)services.GetRequiredService<TConsumer>());
implementsAny = true;
}

if (typeof(IMetricsConsumer<SocketsMetrics>).IsAssignableFrom(typeof(TConsumer)))
{
services.AddSingleton(services => (IMetricsConsumer<SocketsMetrics>)services.GetRequiredService<TConsumer>());
implementsAny = true;
}

if (!implementsAny)
{
throw new ArgumentException("TConsumer must implement at least one IMetricsConsumer interface.", nameof(TConsumer));
}

services.TryAddSingleton<TConsumer>();

services.AddTelemetryListeners();

return services;
}
}
78 changes: 53 additions & 25 deletions test/ReverseProxy.FunctionalTests/TelemetryConsumptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public enum RegistrationApproach
Manual
}

private static void Register(IServiceCollection services, RegistrationApproach approach)
private static void RegisterTelemetryConsumers(IServiceCollection services, RegistrationApproach approach)
{
if (approach == RegistrationApproach.WithInstanceHelper)
{
Expand Down Expand Up @@ -65,6 +65,30 @@ private static void Register(IServiceCollection services, RegistrationApproach a
}
}

private static void RegisterMetricsConsumers(IServiceCollection services, RegistrationApproach approach)
{
if (approach == RegistrationApproach.WithInstanceHelper)
{
services.AddMetricsConsumer(new MetricsConsumer());
}
else if (approach == RegistrationApproach.WithGenericHelper)
{
services.AddMetricsConsumer<MetricsConsumer>();
}
else if (approach == RegistrationApproach.Manual)
{
services.AddSingleton<MetricsConsumer>();
services.AddSingleton(services => (IMetricsConsumer<ForwarderMetrics>)services.GetRequiredService<MetricsConsumer>());
services.AddSingleton(services => (IMetricsConsumer<KestrelMetrics>)services.GetRequiredService<MetricsConsumer>());
services.AddSingleton(services => (IMetricsConsumer<HttpMetrics>)services.GetRequiredService<MetricsConsumer>());
services.AddSingleton(services => (IMetricsConsumer<NameResolutionMetrics>)services.GetRequiredService<MetricsConsumer>());
services.AddSingleton(services => (IMetricsConsumer<NetSecurityMetrics>)services.GetRequiredService<MetricsConsumer>());
services.AddSingleton(services => (IMetricsConsumer<SocketsMetrics>)services.GetRequiredService<MetricsConsumer>());

services.AddTelemetryListeners();
}
}

private static void VerifyStages(string[] expected, List<(string Stage, DateTime Timestamp)> stages)
{
Assert.Equal(expected, stages.Select(s => s.Stage).ToArray());
Expand All @@ -83,7 +107,7 @@ public async Task TelemetryConsumptionWorks(RegistrationApproach registrationApp
{
var test = new TestEnvironment(
async context => await context.Response.WriteAsync("Foo"),
proxyBuilder => Register(proxyBuilder.Services, registrationApproach),
proxyBuilder => RegisterTelemetryConsumers(proxyBuilder.Services, registrationApproach),
proxyApp => { },
useHttpsOnDestination: true);

Expand Down Expand Up @@ -135,7 +159,7 @@ public async Task NonProxyTelemetryConsumptionWorks(RegistrationApproach registr
{
var test = new TestEnvironment(
async context => await context.Response.WriteAsync("Foo"),
proxyBuilder => Register(proxyBuilder.Services, registrationApproach),
proxyBuilder => RegisterTelemetryConsumers(proxyBuilder.Services, registrationApproach),
proxyApp => { },
useHttpsOnDestination: true);

Expand Down Expand Up @@ -232,39 +256,31 @@ public void OnRequestStart(DateTime timestamp, string scheme, string host, int p
public void OnRequestStop(DateTime timestamp, string connectionId, string requestId, string httpVersion, string path, string method) => AddStage($"{nameof(OnRequestStop)}-Kestrel", timestamp);
}

[Fact]
public async Task MetricsConsumptionWorks()
[Theory]
[InlineData(RegistrationApproach.WithInstanceHelper)]
[InlineData(RegistrationApproach.WithGenericHelper)]
[InlineData(RegistrationApproach.Manual)]
public async Task MetricsConsumptionWorks(RegistrationApproach registrationApproach)
{
MetricsOptions.Interval = TimeSpan.FromMilliseconds(10);

var consumer = new MetricsConsumer();

var test = new TestEnvironment(
async context =>
{
await context.Response.WriteAsync("Foo");
},
proxyBuilder =>
{
var services = proxyBuilder.Services;
services.AddSingleton<IMetricsConsumer<ForwarderMetrics>>(consumer);
services.AddSingleton<IMetricsConsumer<KestrelMetrics>>(consumer);
services.AddSingleton<IMetricsConsumer<HttpMetrics>>(consumer);
services.AddSingleton<IMetricsConsumer<SocketsMetrics>>(consumer);
services.AddSingleton<IMetricsConsumer<NetSecurityMetrics>>(consumer);
services.AddSingleton<IMetricsConsumer<NameResolutionMetrics>>(consumer);
services.AddTelemetryListeners();
},
async context => await context.Response.WriteAsync("Foo"),
proxyBuilder => RegisterMetricsConsumers(proxyBuilder.Services, registrationApproach),
proxyApp => { },
useHttpsOnDestination: true);

var consumerBox = new MetricsConsumer.MetricsConsumerBox();
MetricsConsumer.ScopeInstance.Value = consumerBox;
MetricsConsumer consumer = null;

await test.Invoke(async uri =>
{
var httpClient = new HttpClient();
await httpClient.GetStringAsync(uri);
consumer = consumerBox.Instance;
try
{
// Do some arbitrary DNS work to get metrics, since we're connecting to localhost
Expand Down Expand Up @@ -316,13 +332,25 @@ private sealed class MetricsConsumer :
IMetricsConsumer<NetSecurityMetrics>,
IMetricsConsumer<SocketsMetrics>
{
public readonly ConcurrentQueue<ForwarderMetrics> ProxyMetrics = new ConcurrentQueue<ForwarderMetrics>();
public sealed class MetricsConsumerBox
{
public MetricsConsumer Instance;
}

public static readonly AsyncLocal<MetricsConsumerBox> ScopeInstance = new();

public readonly ConcurrentQueue<ForwarderMetrics> ProxyMetrics = new();
public readonly ConcurrentQueue<KestrelMetrics> KestrelMetrics = new();
public readonly ConcurrentQueue<HttpMetrics> HttpMetrics = new();
public readonly ConcurrentQueue<SocketsMetrics> SocketsMetrics = new();
public readonly ConcurrentQueue<NetSecurityMetrics> NetSecurityMetrics = new();
public readonly ConcurrentQueue<NameResolutionMetrics> NameResolutionMetrics = new();

public MetricsConsumer()
{
ScopeInstance.Value.Instance = this;
}

public void OnMetrics(ForwarderMetrics previous, ForwarderMetrics current) => ProxyMetrics.Enqueue(current);
public void OnMetrics(KestrelMetrics previous, KestrelMetrics current) => KestrelMetrics.Enqueue(current);
public void OnMetrics(SocketsMetrics previous, SocketsMetrics current) => SocketsMetrics.Enqueue(current);
Expand Down

0 comments on commit 6b4195a

Please sign in to comment.