Prometheus: Expand UseOpenTelemetryPrometheusScrapingEndpoint extension (#3029)
This commit is contained in:
parent
f3b7b80173
commit
26c3e0c23c
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue