Skip to content

Commit a7a91d1

Browse files
committed
Add metrics factory
1 parent ccb861b commit a7a91d1

14 files changed

+274
-20
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.Metrics;
5+
using Microsoft.Extensions.Metrics;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Microsoft.AspNetCore.Hosting.Abstractions.MetricsPrototype;
9+
10+
internal class DefaultMetricsFactory : IMetricsFactory
11+
{
12+
private readonly IOptions<MetricsOptions> _options;
13+
14+
public DefaultMetricsFactory(IOptions<MetricsOptions> options)
15+
{
16+
_options = options;
17+
}
18+
19+
public Meter CreateMeter(string name)
20+
{
21+
// TODO: Configure meter with default tags.
22+
return new Meter(name);
23+
}
24+
25+
public Meter CreateMeter(MeterOptions options)
26+
{
27+
// TODO: Configure meter with default tags.
28+
return new Meter(options.Name, options.Version);
29+
}
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.Metrics;
5+
6+
namespace Microsoft.Extensions.Metrics;
7+
8+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
9+
#pragma warning disable RS0016 // Add public types and members to the declared API
10+
public sealed class MetricsOptions
11+
{
12+
public string? ContainerId { get; set; }
13+
public IDictionary<string, string> DefaultTags { get; } = new Dictionary<string, string>();
14+
}
15+
16+
public sealed class MeterOptions
17+
{
18+
public required string Name { get; set; }
19+
public string? Version { get; set; }
20+
}
21+
22+
public interface IMetricsFactory
23+
{
24+
Meter CreateMeter(string name);
25+
Meter CreateMeter(MeterOptions options);
26+
}
27+
#pragma warning restore RS0016 // Add public types and members to the declared API
28+
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Hosting.Abstractions.MetricsPrototype;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
using Microsoft.Extensions.Metrics;
7+
8+
namespace Microsoft.Extensions.DependencyInjection;
9+
10+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
11+
#pragma warning disable RS0016 // Add public types and members to the declared API
12+
public static class MetricsServiceExtensions
13+
{
14+
public static IServiceCollection AddMetrics(this IServiceCollection services)
15+
{
16+
ArgumentNullException.ThrowIfNull(services);
17+
18+
services.TryAddSingleton<IMetricsFactory, DefaultMetricsFactory>();
19+
20+
return services;
21+
}
22+
23+
public static IServiceCollection AddMetrics(this IServiceCollection services, Action<MetricsOptions> configure)
24+
{
25+
ArgumentNullException.ThrowIfNull(services);
26+
27+
services.AddMetrics();
28+
services.Configure(configure);
29+
30+
return services;
31+
}
32+
}
33+
#pragma warning restore RS0016 // Add public types and members to the declared API
34+
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member

src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options
6262
#pragma warning restore CS0618 // Type or member is obsolete
6363

6464
services.Configure<GenericWebHostServiceOptions>(options =>
65-
{
66-
// Set the options
67-
options.WebHostOptions = webHostOptions;
68-
// Store and forward any startup errors
69-
options.HostingStartupExceptions = _hostingStartupErrors;
70-
});
65+
{
66+
// Set the options
67+
options.WebHostOptions = webHostOptions;
68+
// Store and forward any startup errors
69+
options.HostingStartupExceptions = _hostingStartupErrors;
70+
});
7171

7272
// REVIEW: This is bad since we don't own this type. Anybody could add one of these and it would mess things up
7373
// We need to flow this differently
@@ -80,6 +80,9 @@ public GenericWebHostBuilder(IHostBuilder builder, WebHostBuilderOptions options
8080
services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
8181
services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();
8282

83+
services.AddMetrics();
84+
services.TryAddSingleton<HostingMetrics>();
85+
8386
// IMPORTANT: This needs to run *before* direct calls on the builder (like UseStartup)
8487
_hostingStartupWebHostBuilder?.ConfigureServices(webhostContext, services);
8588

src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
2626
IApplicationBuilderFactory applicationBuilderFactory,
2727
IEnumerable<IStartupFilter> startupFilters,
2828
IConfiguration configuration,
29-
IWebHostEnvironment hostingEnvironment)
29+
IWebHostEnvironment hostingEnvironment,
30+
HostingMetrics hostingMetrics)
3031
{
3132
Options = options.Value;
3233
Server = server;
@@ -40,6 +41,7 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
4041
StartupFilters = startupFilters;
4142
Configuration = configuration;
4243
HostingEnvironment = hostingEnvironment;
44+
HostingMetrics = hostingMetrics;
4345
}
4446

4547
public GenericWebHostServiceOptions Options { get; }
@@ -55,6 +57,7 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
5557
public IEnumerable<IStartupFilter> StartupFilters { get; }
5658
public IConfiguration Configuration { get; }
5759
public IWebHostEnvironment HostingEnvironment { get; }
60+
public HostingMetrics HostingMetrics { get; }
5861

5962
public async Task StartAsync(CancellationToken cancellationToken)
6063
{
@@ -153,7 +156,7 @@ static string ExpandPorts(string ports, string scheme)
153156
application = ErrorPageBuilder.BuildErrorPageApplication(HostingEnvironment.ContentRootFileProvider, Logger, showDetailedErrors, ex);
154157
}
155158

156-
var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory);
159+
var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory, HostingMetrics);
157160

158161
await Server.StartAsync(httpApplication, cancellationToken);
159162
HostingEventSource.Log.ServerReady();

src/Hosting/Hosting/src/Internal/HostingApplication.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ public HostingApplication(
2323
DiagnosticListener diagnosticSource,
2424
ActivitySource activitySource,
2525
DistributedContextPropagator propagator,
26-
IHttpContextFactory httpContextFactory)
26+
IHttpContextFactory httpContextFactory,
27+
HostingMetrics metrics)
2728
{
2829
_application = application;
29-
_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource, activitySource, propagator);
30+
_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource, activitySource, propagator, metrics);
3031
if (httpContextFactory is DefaultHttpContextFactory factory)
3132
{
3233
_defaultHttpContextFactory = factory;

src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,29 @@ internal sealed class HostingApplicationDiagnostics
2828
private readonly ActivitySource _activitySource;
2929
private readonly DiagnosticListener _diagnosticListener;
3030
private readonly DistributedContextPropagator _propagator;
31+
private readonly HostingMetrics _metrics;
3132
private readonly ILogger _logger;
3233

3334
public HostingApplicationDiagnostics(
3435
ILogger logger,
3536
DiagnosticListener diagnosticListener,
3637
ActivitySource activitySource,
37-
DistributedContextPropagator propagator)
38+
DistributedContextPropagator propagator,
39+
HostingMetrics metrics)
3840
{
3941
_logger = logger;
4042
_diagnosticListener = diagnosticListener;
4143
_activitySource = activitySource;
4244
_propagator = propagator;
45+
_metrics = metrics;
4346
}
4447

4548
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4649
public void BeginRequest(HttpContext httpContext, HostingApplication.Context context)
4750
{
4851
long startTimestamp = 0;
4952

50-
if (HostingEventSource.Log.IsEnabled())
53+
if (HostingEventSource.Log.IsEnabled() || _metrics.IsEnabled())
5154
{
5255
context.EventLogEnabled = true;
5356
// To keep the hot path short we defer logging in this function to non-inlines

src/Hosting/Hosting/src/Internal/HostingEventSource.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ internal sealed class HostingEventSource : EventSource
1919
private long _currentRequests;
2020
private long _failedRequests;
2121

22+
public long TotalRequests => Volatile.Read(ref _totalRequests);
23+
public long CurrentRequests => Volatile.Read(ref _currentRequests);
24+
public long FailedRequests => Volatile.Read(ref _failedRequests);
25+
2226
internal HostingEventSource()
2327
: this("Microsoft.AspNetCore.Hosting")
2428
{
@@ -90,23 +94,23 @@ protected override void OnEventCommand(EventCommandEventArgs command)
9094
// This is the convention for initializing counters in the RuntimeEventSource (lazily on the first enable command).
9195
// They aren't disabled afterwards...
9296

93-
_requestsPerSecondCounter ??= new IncrementingPollingCounter("requests-per-second", this, () => Volatile.Read(ref _totalRequests))
97+
_requestsPerSecondCounter ??= new IncrementingPollingCounter("requests-per-second", this, () => TotalRequests)
9498
{
9599
DisplayName = "Request Rate",
96100
DisplayRateTimeScale = TimeSpan.FromSeconds(1)
97101
};
98102

99-
_totalRequestsCounter ??= new PollingCounter("total-requests", this, () => Volatile.Read(ref _totalRequests))
103+
_totalRequestsCounter ??= new PollingCounter("total-requests", this, () => TotalRequests)
100104
{
101105
DisplayName = "Total Requests",
102106
};
103107

104-
_currentRequestsCounter ??= new PollingCounter("current-requests", this, () => Volatile.Read(ref _currentRequests))
108+
_currentRequestsCounter ??= new PollingCounter("current-requests", this, () => CurrentRequests)
105109
{
106110
DisplayName = "Current Requests"
107111
};
108112

109-
_failedRequestsCounter ??= new PollingCounter("failed-requests", this, () => Volatile.Read(ref _failedRequests))
113+
_failedRequestsCounter ??= new PollingCounter("failed-requests", this, () => FailedRequests)
110114
{
111115
DisplayName = "Failed Requests"
112116
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.Metrics;
5+
using Microsoft.Extensions.Metrics;
6+
7+
namespace Microsoft.AspNetCore.Hosting;
8+
9+
internal class HostingMetrics : IDisposable
10+
{
11+
private readonly Meter _meter;
12+
private readonly ObservableCounter<long> _totalRequestsCounter;
13+
private readonly ObservableUpDownCounter<long> _currentRequestsCounter;
14+
private readonly ObservableCounter<long> _failedRequestsCounter;
15+
16+
public HostingMetrics(IMetricsFactory metricsFactory)
17+
{
18+
_meter = metricsFactory.CreateMeter("Microsoft.AspNetCore.Hosting");
19+
20+
_totalRequestsCounter = _meter.CreateObservableCounter<long>(
21+
"total-requests",
22+
() => HostingEventSource.Log.TotalRequests,
23+
description: "Total Requests");
24+
25+
_currentRequestsCounter = _meter.CreateObservableUpDownCounter<long>(
26+
"current-requests",
27+
() => HostingEventSource.Log.CurrentRequests,
28+
description: "Current Requests");
29+
30+
_failedRequestsCounter = _meter.CreateObservableCounter<long>(
31+
"failed-requests",
32+
() => HostingEventSource.Log.FailedRequests,
33+
description: "Failed Requests");
34+
}
35+
36+
public void Dispose()
37+
{
38+
_meter.Dispose();
39+
}
40+
41+
public bool IsEnabled() => _totalRequestsCounter.Enabled || _currentRequestsCounter.Enabled || _failedRequestsCounter.Enabled;
42+
}

src/Hosting/Hosting/src/Internal/WebHost.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
141141
var activitySource = _applicationServices.GetRequiredService<ActivitySource>();
142142
var propagator = _applicationServices.GetRequiredService<DistributedContextPropagator>();
143143
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
144-
var hostingApp = new HostingApplication(application, _logger, diagnosticSource, activitySource, propagator, httpContextFactory);
144+
var hostingMetrics = _applicationServices.GetRequiredService<HostingMetrics>();
145+
var hostingApp = new HostingApplication(application, _logger, diagnosticSource, activitySource, propagator, httpContextFactory, hostingMetrics);
145146
await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);
146147
_startedServer = true;
147148

src/Hosting/Hosting/src/WebHostBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ private IServiceCollection BuildCommonServices(out AggregateException? hostingSt
294294
services.AddOptions();
295295
services.AddLogging();
296296

297+
services.AddMetrics();
298+
services.TryAddSingleton<HostingMetrics>();
299+
297300
services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>();
298301

299302
if (!string.IsNullOrEmpty(_options.StartupAssembly))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.Metrics;
5+
using Microsoft.Extensions.Metrics;
6+
7+
namespace Microsoft.AspNetCore.Hosting.Fakes;
8+
9+
public class TestMetricsFactory : IMetricsFactory
10+
{
11+
private readonly List<Meter> _meters = new List<Meter>();
12+
13+
public IReadOnlyList<Meter> Meters => _meters;
14+
15+
public Meter CreateMeter(string name)
16+
{
17+
var meter = new Meter(name);
18+
_meters.Add(meter);
19+
return meter;
20+
}
21+
22+
public Meter CreateMeter(MeterOptions options)
23+
{
24+
var meter = new Meter(options.Name, options.Version);
25+
_meters.Add(meter);
26+
return meter;
27+
}
28+
}

src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Reflection;
6+
using Microsoft.AspNetCore.Hosting.Fakes;
67
using Microsoft.AspNetCore.Http;
78
using Microsoft.AspNetCore.Http.Features;
89
using Microsoft.Extensions.Logging;
@@ -590,7 +591,8 @@ private static HostingApplication CreateApplication(out FeatureCollection featur
590591
diagnosticListener ?? new NoopDiagnosticListener(),
591592
activitySource ?? new ActivitySource("Microsoft.AspNetCore"),
592593
DistributedContextPropagator.CreateDefaultPropagator(),
593-
httpContextFactory.Object);
594+
httpContextFactory.Object,
595+
new HostingMetrics(new TestMetricsFactory()));
594596

595597
return hostingApplication;
596598
}

0 commit comments

Comments
 (0)