prometheus-net
128 строк · 5.4 Кб
1using System.Diagnostics;
2using System.Net;
3
4namespace Prometheus;
5
6/// <summary>
7/// Implementation of a Prometheus exporter that serves metrics using HttpListener.
8/// This is a stand-alone exporter for apps that do not already have an HTTP server included.
9/// </summary>
10public class MetricServer : MetricHandler
11{
12private readonly HttpListener _httpListener = new();
13
14/// <summary>
15/// Only requests that match this predicate will be served by the metric server. This allows you to add authorization checks.
16/// By default (if null), all requests are served.
17/// </summary>
18public Func<HttpListenerRequest, bool>? RequestPredicate { get; set; }
19
20public MetricServer(int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : this("+", port, url, registry, useHttps)
21{
22}
23
24public MetricServer(string hostname, int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false)
25{
26var s = useHttps ? "s" : "";
27_httpListener.Prefixes.Add($"http{s}://{hostname}:{port}/{url}");
28
29_registry = registry ?? Metrics.DefaultRegistry;
30}
31
32private readonly CollectorRegistry _registry;
33
34protected override Task StartServer(CancellationToken cancel)
35{
36// This will ensure that any failures to start are nicely thrown from StartServerAsync.
37_httpListener.Start();
38
39// Kick off the actual processing to a new thread and return a Task for the processing thread.
40return Task.Factory.StartNew(delegate
41{
42try
43{
44Thread.CurrentThread.Name = "Metric Server"; //Max length 16 chars (Linux limitation)
45
46while (!cancel.IsCancellationRequested)
47{
48// There is no way to give a CancellationToken to GCA() so, we need to hack around it a bit.
49var getContext = _httpListener.GetContextAsync();
50getContext.Wait(cancel);
51var context = getContext.Result;
52
53// Asynchronously process the request.
54_ = Task.Factory.StartNew(async delegate
55{
56var request = context.Request;
57var response = context.Response;
58
59try
60{
61var predicate = RequestPredicate;
62
63if (predicate != null && !predicate(request))
64{
65// Request rejected by predicate.
66response.StatusCode = (int)HttpStatusCode.Forbidden;
67return;
68}
69
70try
71{
72// We first touch the response.OutputStream only in the callback because touching
73// it means we can no longer send headers (the status code).
74var serializer = new TextSerializer(delegate
75{
76response.ContentType = PrometheusConstants.TextContentTypeWithVersionAndEncoding;
77response.StatusCode = 200;
78return response.OutputStream;
79});
80
81await _registry.CollectAndSerializeAsync(serializer, cancel);
82response.OutputStream.Dispose();
83}
84catch (ScrapeFailedException ex)
85{
86// This can only happen before anything is written to the stream, so it
87// should still be safe to update the status code and report an error.
88response.StatusCode = 503;
89
90if (!string.IsNullOrWhiteSpace(ex.Message))
91{
92using (var writer = new StreamWriter(response.OutputStream))
93writer.Write(ex.Message);
94}
95}
96}
97catch (Exception ex) when (!(ex is OperationCanceledException))
98{
99if (!_httpListener.IsListening)
100return; // We were shut down.
101
102Trace.WriteLine(string.Format("Error in {0}: {1}", nameof(MetricServer), ex));
103
104try
105{
106response.StatusCode = 500;
107}
108catch
109{
110// Might be too late in request processing to set response code, so just ignore.
111}
112}
113finally
114{
115response.Close();
116}
117});
118}
119}
120finally
121{
122_httpListener.Stop();
123// This should prevent any currently processed requests from finishing.
124_httpListener.Close();
125}
126}, TaskCreationOptions.LongRunning);
127}
128}
129