Prometheus: Expand UseOpenTelemetryPrometheusScrapingEndpoint extension (#3029)

This commit is contained in:
Mikel Blanchard 2022-04-13 13:37:11 -07:00 committed by GitHub
parent f3b7b80173
commit 26c3e0c23c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 286 additions and 39 deletions

View File

@ -16,5 +16,8 @@ OpenTelemetry.Exporter.PrometheusExporterOptions.StartHttpListener.set -> void
OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions
override OpenTelemetry.Exporter.PrometheusExporter.Dispose(bool disposing) -> void
override OpenTelemetry.Exporter.PrometheusExporter.Export(in OpenTelemetry.Batch<OpenTelemetry.Metrics.Metric> metrics) -> OpenTelemetry.ExportResult
static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, OpenTelemetry.Metrics.MeterProvider meterProvider = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder
static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder
static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, OpenTelemetry.Metrics.MeterProvider meterProvider, System.Func<Microsoft.AspNetCore.Http.HttpContext, bool> predicate, string path, System.Action<Microsoft.AspNetCore.Builder.IApplicationBuilder> configureBranchedPipeline) -> Microsoft.AspNetCore.Builder.IApplicationBuilder
static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, string path) -> Microsoft.AspNetCore.Builder.IApplicationBuilder
static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, System.Func<Microsoft.AspNetCore.Http.HttpContext, bool> predicate) -> Microsoft.AspNetCore.Builder.IApplicationBuilder
static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action<OpenTelemetry.Exporter.PrometheusExporterOptions> configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder

View File

@ -2,6 +2,10 @@
## Unreleased
* Added `IApplicationBuilder` extension methods to help with Prometheus
middleware configuration on ASP.NET Core
([#3029](https://github.com/open-telemetry/opentelemetry-dotnet/pull/3029))
## 1.2.0-rc5
Released 2022-Apr-12
@ -19,7 +23,7 @@ Released 2022-Mar-04
Released 2022-Feb-02
* Update default `httpListenerPrefixes` for PrometheusExporter to be `http://localhost:9464/`.
([2783](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2783))
([#2783](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2783))
## 1.2.0-rc1

View File

@ -21,12 +21,14 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Exporter;
using OpenTelemetry.Exporter.Prometheus;
using OpenTelemetry.Internal;
using OpenTelemetry.Metrics;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Provides extension methods for <see cref="IApplicationBuilder"/> to add Prometheus Scraper Endpoint.
/// Provides extension methods for <see cref="IApplicationBuilder"/> to add
/// Prometheus scraping endpoint.
/// </summary>
public static class PrometheusExporterApplicationBuilderExtensions
{
@ -34,25 +36,118 @@ namespace Microsoft.AspNetCore.Builder
/// Adds OpenTelemetry Prometheus scraping endpoint middleware to an
/// <see cref="IApplicationBuilder"/> instance.
/// </summary>
/// <remarks>Note: A branched pipeline is created for the route
/// specified by <see
/// cref="PrometheusExporterOptions.ScrapeEndpointPath"/>.</remarks>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add
/// middleware to.</param>
/// <returns>A reference to the original <see
/// cref="IApplicationBuilder"/> for chaining calls.</returns>
public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app)
=> UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: null, path: null, configureBranchedPipeline: null);
/// <summary>
/// Adds OpenTelemetry Prometheus scraping endpoint middleware to an
/// <see cref="IApplicationBuilder"/> instance.
/// </summary>
/// <remarks>Note: A branched pipeline is created for the supplied
/// <paramref name="path"/>.</remarks>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add
/// middleware to.</param>
/// <param name="path">Path to use for the branched pipeline.</param>
/// <returns>A reference to the original <see
/// cref="IApplicationBuilder"/> for chaining calls.</returns>
public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, string path)
{
Guard.ThrowIfNull(path);
return UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: null, path: path, configureBranchedPipeline: null);
}
/// <summary>
/// Adds OpenTelemetry Prometheus scraping endpoint middleware to an
/// <see cref="IApplicationBuilder"/> instance.
/// </summary>
/// <remarks>Note: A branched pipeline is created for the supplied
/// <paramref name="predicate"/>.</remarks>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add
/// middleware to.</param>
/// <param name="predicate">Predicate for deciding if a given
/// <see cref="HttpContext"/> should be branched.</param>
/// <returns>A reference to the original <see
/// cref="IApplicationBuilder"/> for chaining calls.</returns>
public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, Func<HttpContext, bool> predicate)
{
Guard.ThrowIfNull(predicate);
return UseOpenTelemetryPrometheusScrapingEndpoint(app, meterProvider: null, predicate: predicate, path: null, configureBranchedPipeline: null);
}
/// <summary>
/// Adds OpenTelemetry Prometheus scraping endpoint middleware to an
/// <see cref="IApplicationBuilder"/> instance.
/// </summary>
/// <remarks>Note: A branched pipeline is created based on the <paramref
/// name="predicate"/> or <paramref name="path"/>. If neither <paramref
/// name="predicate"/> nor <paramref name="path"/> are provided then
/// <see cref="PrometheusExporterOptions.ScrapeEndpointPath"/> is
/// used.</remarks>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add
/// middleware to.</param>
/// <param name="meterProvider">Optional <see cref="MeterProvider"/>
/// containing a <see cref="PrometheusExporter"/> otherwise the primary
/// SDK provider will be resolved using application services.</param>
/// <returns>A reference to the <see cref="IApplicationBuilder"/> instance after the operation has completed.</returns>
public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, MeterProvider meterProvider = null)
/// <param name="predicate">Optional predicate for deciding if a given
/// <see cref="HttpContext"/> should be branched. If supplied <paramref
/// name="path"/> is ignored.</param>
/// <param name="path">Optional path to use for the branched pipeline.
/// Ignored if <paramref name="predicate"/> is supplied.</param>
/// <param name="configureBranchedPipeline">Optional callback to
/// configure the branched pipeline. Called before registration of the
/// Prometheus middleware.</param>
/// <returns>A reference to the original <see
/// cref="IApplicationBuilder"/> for chaining calls.</returns>
public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(
this IApplicationBuilder app,
MeterProvider meterProvider,
Func<HttpContext, bool> predicate,
string path,
Action<IApplicationBuilder> configureBranchedPipeline)
{
var options = app.ApplicationServices.GetOptions<PrometheusExporterOptions>();
// Note: Order is important here. MeterProvider is accessed before
// GetOptions<PrometheusExporterOptions> so that any changes made to
// PrometheusExporterOptions in deferred AddPrometheusExporter
// configure actions are reflected.
meterProvider ??= app.ApplicationServices.GetRequiredService<MeterProvider>();
string path = options.ScrapeEndpointPath ?? PrometheusExporterOptions.DefaultScrapeEndpointPath;
if (!path.StartsWith("/"))
if (predicate == null)
{
path = $"/{path}";
if (path == null)
{
var options = app.ApplicationServices.GetOptions<PrometheusExporterOptions>();
path = options.ScrapeEndpointPath ?? PrometheusExporterOptions.DefaultScrapeEndpointPath;
}
if (!path.StartsWith("/"))
{
path = $"/{path}";
}
return app.Map(
new PathString(path),
builder =>
{
configureBranchedPipeline?.Invoke(builder);
builder.UseMiddleware<PrometheusExporterMiddleware>(meterProvider);
});
}
return app.Map(
new PathString(path),
builder => builder.UseMiddleware<PrometheusExporterMiddleware>(meterProvider ?? app.ApplicationServices.GetRequiredService<MeterProvider>()));
return app.MapWhen(
context => predicate(context),
builder =>
{
configureBranchedPipeline?.Invoke(builder);
builder.UseMiddleware<PrometheusExporterMiddleware>(meterProvider);
});
}
}
}

View File

@ -45,8 +45,26 @@ dotnet add package OpenTelemetry.Exporter.Prometheus
```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseOpenTelemetryPrometheusScrapingEndpoint();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
```
Overloads of the `UseOpenTelemetryPrometheusScrapingEndpoint` extension are
provided to change the path or for more advanced configuration a predicate
function can be used:
```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseOpenTelemetryPrometheusScrapingEndpoint(
context => context.Request.Path == "/internal/metrics"
&& context.Connection.LocalPort == 5067);
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();

View File

@ -18,7 +18,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@ -36,12 +38,156 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests
private static readonly string MeterName = Utils.GetCurrentMethodName();
[Fact]
public async Task PrometheusExporterMiddlewareIntegration()
public Task PrometheusExporterMiddlewareIntegration()
{
var host = await new HostBuilder()
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint());
}
[Fact]
public Task PrometheusExporterMiddlewareIntegration_Options()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics_options",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
services => services.Configure<PrometheusExporterOptions>(o => o.ScrapeEndpointPath = "metrics_options"));
}
[Fact]
public Task PrometheusExporterMiddlewareIntegration_OptionsFallback()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
services => services.Configure<PrometheusExporterOptions>(o => o.ScrapeEndpointPath = null));
}
[Fact]
public Task PrometheusExporterMiddlewareIntegration_OptionsViaAddPrometheusExporter()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics_from_AddPrometheusExporter",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(),
configureOptions: o => o.ScrapeEndpointPath = "/metrics_from_AddPrometheusExporter");
}
[Fact]
public Task PrometheusExporterMiddlewareIntegration_PathOverride()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics_override",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint("/metrics_override"));
}
[Fact]
public Task PrometheusExporterMiddlewareIntegration_Predicate()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics_predicate?enabled=true",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(httpcontext => httpcontext.Request.Path == "/metrics_predicate" && httpcontext.Request.Query["enabled"] == "true"));
}
[Fact]
public Task PrometheusExporterMiddlewareIntegration_MixedPredicateAndPath()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics_predicate",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(
meterProvider: null,
predicate: httpcontext => httpcontext.Request.Path == "/metrics_predicate",
path: "/metrics_path",
configureBranchedPipeline: branch => branch.Use((context, next) =>
{
context.Response.Headers.Add("X-MiddlewareExecuted", "true");
return next();
})),
services => services.Configure<PrometheusExporterOptions>(o => o.ScrapeEndpointPath = "/metrics_options"),
validateResponse: rsp =>
{
if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable<string> headers))
{
headers = Array.Empty<string>();
}
Assert.Equal("true", headers.FirstOrDefault());
});
}
[Fact]
public Task PrometheusExporterMiddlewareIntegration_MixedPath()
{
return RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics_path",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(
meterProvider: null,
predicate: null,
path: "/metrics_path",
configureBranchedPipeline: branch => branch.Use((context, next) =>
{
context.Response.Headers.Add("X-MiddlewareExecuted", "true");
return next();
})),
services => services.Configure<PrometheusExporterOptions>(o => o.ScrapeEndpointPath = "/metrics_options"),
validateResponse: rsp =>
{
if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable<string> headers))
{
headers = Array.Empty<string>();
}
Assert.Equal("true", headers.FirstOrDefault());
});
}
[Fact]
public async Task PrometheusExporterMiddlewareIntegration_MeterProvider()
{
using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(MeterName)
.AddPrometheusExporter()
.Build();
await RunPrometheusExporterMiddlewareIntegrationTest(
"/metrics",
app => app.UseOpenTelemetryPrometheusScrapingEndpoint(
meterProvider: meterProvider,
predicate: null,
path: null,
configureBranchedPipeline: null),
registerMeterProvider: false).ConfigureAwait(false);
}
private static async Task RunPrometheusExporterMiddlewareIntegrationTest(
string path,
Action<IApplicationBuilder> configure,
Action<IServiceCollection> configureServices = null,
Action<HttpResponseMessage> validateResponse = null,
bool registerMeterProvider = true,
Action<PrometheusExporterOptions> configureOptions = null)
{
using var host = await new HostBuilder()
.ConfigureWebHost(webBuilder => webBuilder
.UseTestServer()
.UseStartup<Startup>())
.ConfigureServices(services =>
{
if (registerMeterProvider)
{
services.AddOpenTelemetryMetrics(builder => builder
.AddMeter(MeterName)
.AddPrometheusExporter(o =>
{
configureOptions?.Invoke(o);
if (o.StartHttpListener)
{
throw new InvalidOperationException("StartHttpListener should be false on .NET Core 3.1+.");
}
}));
}
configureServices?.Invoke(services);
})
.Configure(configure))
.StartAsync();
var tags = new KeyValuePair<string, object>[]
@ -58,7 +204,7 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests
counter.Add(100.18D, tags);
counter.Add(0.99D, tags);
using var response = await host.GetTestClient().GetAsync("/metrics").ConfigureAwait(false);
using var response = await host.GetTestClient().GetAsync(path).ConfigureAwait(false);
var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
@ -80,35 +226,16 @@ namespace OpenTelemetry.Exporter.Prometheus.Tests
var index = content.LastIndexOf(' ');
Assert.Equal('\n', content[content.Length - 1]);
Assert.Equal('\n', content[^1]);
var timestamp = long.Parse(content.Substring(index, content.Length - index - 1));
Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp);
validateResponse?.Invoke(response);
await host.StopAsync().ConfigureAwait(false);
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOpenTelemetryMetrics(builder => builder
.AddMeter(MeterName)
.AddPrometheusExporter(o =>
{
if (o.StartHttpListener)
{
throw new InvalidOperationException("StartHttpListener should be false on .NET Core 3.1+.");
}
}));
}
public void Configure(IApplicationBuilder app)
{
app.UseOpenTelemetryPrometheusScrapingEndpoint();
}
}
}
}
#endif