forked from prometheus-net/prometheus-net
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMetricServerMiddleware.cs
127 lines (107 loc) · 5.38 KB
/
MetricServerMiddleware.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
using Microsoft.AspNetCore.Http;
using System.Net.Http.Headers;
namespace Prometheus;
/// <summary>
/// Prometheus metrics export middleware for ASP.NET Core.
///
/// You should use IApplicationBuilder.UseMetricServer extension method instead of using this class directly.
/// </summary>
public sealed class MetricServerMiddleware
{
public MetricServerMiddleware(RequestDelegate next, Settings settings)
{
_registry = settings.Registry ?? Metrics.DefaultRegistry;
_enableOpenMetrics = settings.EnableOpenMetrics;
}
public sealed class Settings
{
/// <summary>
/// Where do we take the metrics from. By default, we will take them from the global singleton registry.
/// </summary>
public CollectorRegistry? Registry { get; set; }
/// <summary>
/// Whether we support the OpenMetrics exposition format. Required to publish exemplars. Defaults to enabled.
/// Use of OpenMetrics also requires that the client negotiate the OpenMetrics format via the HTTP "Accept" request header.
/// </summary>
public bool EnableOpenMetrics { get; set; } = true;
}
private readonly CollectorRegistry _registry;
private readonly bool _enableOpenMetrics;
private sealed record ProtocolNegotiationResult
{
public ExpositionFormat ExpositionFormat { get; }
public string ContentType { get; }
public ProtocolNegotiationResult(ExpositionFormat expositionFormat, string contentType)
{
ExpositionFormat = expositionFormat;
ContentType = contentType;
}
}
private IEnumerable<MediaTypeWithQualityHeaderValue> ExtractAcceptableMediaTypes(string acceptHeaderValue)
{
var candidates = acceptHeaderValue.Split(',');
foreach (var candidate in candidates)
{
// It is conceivably possible that some value is invalid - we filter them out here and only return valid values.
// A common case is a missing/empty "Accept" header, in which case we just get 1 candidate of empty string (which is invalid).
if (MediaTypeWithQualityHeaderValue.TryParse(candidate, out var mediaType))
yield return mediaType;
}
}
private ProtocolNegotiationResult NegotiateComminucationProtocol(HttpRequest request)
{
var acceptHeaderValues = request.Headers.Accept.ToString();
// We allow the "Accept" HTTP header to be overridden by the "accept" query string parameter.
// This is mainly for development purposes (to make it easier to request OpenMetrics format via browser URL bar).
if (request.Query.TryGetValue("accept", out var acceptValuesFromQuery))
acceptHeaderValues = string.Join(",", acceptValuesFromQuery);
foreach (var candidate in ExtractAcceptableMediaTypes(acceptHeaderValues)
.OrderByDescending(mt => mt.Quality.GetValueOrDefault(1)))
{
if (candidate.MediaType == PrometheusConstants.TextContentType)
{
// The first preference is the text format. Fall throgh to the default case.
break;
}
else if (_enableOpenMetrics && candidate.MediaType == PrometheusConstants.OpenMetricsContentType)
{
return new ProtocolNegotiationResult(ExpositionFormat.OpenMetricsText, PrometheusConstants.OpenMetricsContentTypeWithVersionAndEncoding);
}
}
return new ProtocolNegotiationResult(ExpositionFormat.PrometheusText, PrometheusConstants.TextContentTypeWithVersionAndEncoding);
}
public async Task Invoke(HttpContext context)
{
var response = context.Response;
try
{
var negotiationResult = NegotiateComminucationProtocol(context.Request);
Stream GetResponseBodyStream()
{
// We first touch the response.Body only in the callback here because touching it means we can no longer send headers (the status code).
// The collection logic will delay calling this method until it is reasonably confident that nothing will go wrong will the collection.
response.ContentType = negotiationResult.ContentType;
response.StatusCode = StatusCodes.Status200OK;
return response.Body;
}
var serializer = new TextSerializer(GetResponseBodyStream(), negotiationResult.ExpositionFormat);
await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted);
}
catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
{
// The scrape was cancalled by the client. This is fine. Just swallow the exception to not generate pointless spam.
}
catch (ScrapeFailedException ex)
{
// This can only happen before any serialization occurs, in the pre-collect callbacks.
// So it should still be safe to update the status code and write an error message.
response.StatusCode = StatusCodes.Status503ServiceUnavailable;
if (!string.IsNullOrWhiteSpace(ex.Message))
{
using (var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding,
bufferSize: -1, leaveOpen: true))
await writer.WriteAsync(ex.Message);
}
}
}
}