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

Add AddMetricsConsumer helper to TelemetryConsumption #1899

Merged
merged 1 commit into from
Oct 19, 2022
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
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't change anything currently, but it can make a significant difference if we ever add debug/trace events to the existing EventSources.
Since this library only knows about the current (informational) events, it would only waste CPU on more verbose events.

If we ever want to consume more verbose events with this library, we will adjust this flag as needed.

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