Asp.NET Core middleware runs too early in the pipeline
I've been debugging for a couple of days trying to figure out why the app-metrics endpoints only worked on my dev box, and not in our test environment. Well, it turns out it was related to the fact that in our dev-environment the application does not run on a naked domain. So IIS is set up to host the application at *:80/App.Name/, and since the App.Metrics middleware ran before the IIS middleware (that get's added automatically by calling Host.CreateDefaultBuilder), the paths had not been corrected. So it was literally impossible to call the app-metrics endpoints, becuase you couldn't call our application with /metrics. Only /App.Name/metrics. This is obviously very confusing, cause right after the app-metrics middleware the paths gets fixed, so logging in the configured part of the pipeline did not reveal the issue.
I suggest moving the endpoints to using endpoint-routing as well. I did that for our project, here's sample code to that effect:
using App.Metrics;
using App.Metrics.AspNetCore;
using App.Metrics.AspNetCore.Endpoints;
using App.Metrics.Formatters;
using App.Metrics.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Metrics endpoint route builder extensions.
/// </summary>
public static class MetricsEndpointRouteBuilderExtensions
{
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns environment info.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapEnvInfo(this IEndpointRouteBuilder builder)
=> builder.MapEnvInfo(getFormatter: null);
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns environment info.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <param name="pattern">The route pattern.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapEnvInfo(this IEndpointRouteBuilder builder, string pattern)
=> builder.MapEnvInfo(pattern, getFormatter: null);
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns environment info.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="formatter">An optional <see cref="IEnvOutputFormatter"/> to format the output.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapEnvInfo(this IEndpointRouteBuilder builder, string pattern = "/infoz", IEnvOutputFormatter? formatter = null)
=> builder.MapEnvInfo(pattern, getFormatter: _ => formatter);
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns environment info.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="getFormatter">An optional function to get a <see cref="IEnvOutputFormatter"/> to format the output.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapEnvInfo(this IEndpointRouteBuilder builder, string pattern = "/infoz", Func<IServiceProvider, IEnvOutputFormatter?>? getFormatter = null)
{
return builder.MapGet(pattern, ctx =>
{
var environmentInfoProvider = ctx.RequestServices.GetRequiredService<EnvironmentInfoProvider>();
var writer = GetEnvInfoResponseWriter(ctx.RequestServices, getFormatter?.Invoke(ctx.RequestServices));
return writer.WriteAsync(ctx, environmentInfoProvider.Build(), ctx.RequestAborted);
});
static IEnvResponseWriter GetEnvInfoResponseWriter(IServiceProvider serviceProvider, IEnvOutputFormatter? formatter = null)
{
var formatters = serviceProvider.GetRequiredService<IReadOnlyCollection<IEnvOutputFormatter>>();
if (formatter != null)
{
var responseWriter = new DefaultEnvResponseWriter(formatter, formatters);
return responseWriter;
}
var options = serviceProvider.GetRequiredService<IOptions<MetricEndpointsOptions>>();
return new DefaultEnvResponseWriter(options.Value.EnvInfoEndpointOutputFormatter, formatters);
}
}
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns metrics.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapMetrics(this IEndpointRouteBuilder builder)
=> builder.MapMetrics(getFormatter: null);
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns metrics.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <param name="pattern">The route pattern.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapMetrics(this IEndpointRouteBuilder builder, string pattern)
=> builder.MapMetrics(pattern, getFormatter: null);
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns metrics.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="formatter">An optional <see cref="IMetricsOutputFormatter"/> to format the output.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapMetrics(this IEndpointRouteBuilder builder, string pattern = "/metrics", IMetricsOutputFormatter? formatter = null)
=> builder.MapMetrics(pattern, getFormatter: _ => formatter);
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns metrics.
/// </summary>
/// <typeparam name="TFormatter">Formatter type.</typeparam>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <param name="pattern">The route pattern.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapMetrics<TFormatter>(this IEndpointRouteBuilder builder, string pattern = "/metrics")
where TFormatter : IMetricsOutputFormatter
=> builder.MapMetrics(pattern, getFormatter: GetFormatter<TFormatter>);
/// <summary>
/// Adds a <see cref="RouteEndpoint"/> to the <see cref="IEndpointRouteBuilder"/> that matches HTTP GET requests for the specified pattern and returns metrics.
/// </summary>
/// <param name="builder">The <see cref="IEndpointRouteBuilder"/> to add the endpoint to.</param>
/// <param name="pattern">The route pattern.</param>
/// <param name="getFormatter">An optional function to get a <see cref="IMetricsOutputFormatter"/> to format the output.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static IEndpointConventionBuilder MapMetrics(this IEndpointRouteBuilder builder, string pattern = "/metrics", Func<IServiceProvider, IMetricsOutputFormatter?>? getFormatter = null)
{
return builder.MapGet(pattern, ctx =>
{
var metrics = ctx.RequestServices.GetRequiredService<IMetrics>();
var writer = GetMetricsTextResponseWriter(ctx.RequestServices, getFormatter?.Invoke(ctx.RequestServices));
return writer.WriteAsync(ctx, metrics.Snapshot.Get(), ctx.RequestAborted);
});
static IMetricsResponseWriter GetMetricsTextResponseWriter(IServiceProvider serviceProvider, IMetricsOutputFormatter? formatter = null)
{
var formatters = serviceProvider.GetRequiredService<IReadOnlyCollection<IMetricsOutputFormatter>>();
if (formatter != null)
{
var responseWriter = new DefaultMetricsResponseWriter(formatter, formatters);
return responseWriter;
}
var options = serviceProvider.GetRequiredService<IOptions<MetricEndpointsOptions>>();
return new DefaultMetricsResponseWriter(options.Value.MetricsTextEndpointOutputFormatter, formatters);
}
}
private static IMetricsOutputFormatter? GetFormatter<TFormatter>(IServiceProvider services)
where TFormatter : IMetricsOutputFormatter
{
var fromDi = services.GetService<TFormatter>();
if (fromDi is not null)
{
return fromDi;
}
var metrics = services.GetRequiredService<IMetricsRoot>();
return metrics.OutputMetricsFormatters.OfType<TFormatter>().FirstOrDefault();
}
}
}