diff --git a/.gitattributes b/.gitattributes
index 1ff0c423..7b17c9b1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -3,6 +3,9 @@
###############################################################################
* text=auto
+# Linux scripts should have Linux file endings.
+*.sh text eol=lf
+
###############################################################################
# Set default behavior for command prompt diff.
#
diff --git a/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs b/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs
new file mode 100644
index 00000000..0fd98f6b
--- /dev/null
+++ b/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs
@@ -0,0 +1,84 @@
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+///
+/// We start up a real(ish) ASP.NET Core web server stack to measure exporter performance end to end.
+///
+[MemoryDiagnoser]
+public class AspNetCoreExporterBenchmarks
+{
+ static AspNetCoreExporterBenchmarks()
+ {
+ // We use the global singleton metrics registry here, so just populate it with some data.
+ // Not too much data - we care more about the overhead and do not want to just inflate the numbers.
+ for (var i = 0; i < 5; i++)
+ Metrics.CreateGauge("dummy_metric_" + i, "For benchmark purposes", "label1", "label2", "label3").WithLabels("1", "2", "3").Inc();
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var builder = new WebHostBuilder().UseStartup();
+ _server = new TestServer(builder);
+ _client = _server.CreateClient();
+
+ // Warmup the ASP.NET Core stack to avoid measuring the ASP.NET Core web server itself in benchmarks.
+ _client.GetAsync("/metrics").GetAwaiter().GetResult();
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _client?.Dispose();
+ _server?.Dispose();
+ }
+
+ private TestServer _server;
+ private HttpClient _client;
+
+ private sealed class EntryPoint
+ {
+#pragma warning disable CA1822 // Mark members as static
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddRouting();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseRouting();
+
+ app.UseHttpMetrics();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapMetrics();
+
+ endpoints.MapGet("ok", context =>
+ {
+ context.Response.StatusCode = 200;
+ return Task.CompletedTask;
+ });
+ });
+ }
+#pragma warning restore CA1822 // Mark members as static
+ }
+
+ [Benchmark]
+ public async Task GetMetrics()
+ {
+ await _client.GetAsync("/metrics");
+ }
+
+ [Benchmark]
+ public async Task Get200Ok()
+ {
+ await _client.GetAsync("/ok");
+ }
+}
diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj
index dc948687..880d2090 100644
--- a/Benchmark.NetCore/Benchmark.NetCore.csproj
+++ b/Benchmark.NetCore/Benchmark.NetCore.csproj
@@ -1,35 +1,38 @@
-
+
-
- Exe
- netcoreapp3.1
- true
+
+ Exe
+ net8.0
+ true
- ..\Resources\prometheus-net.snk
+ ..\Resources\prometheus-net.snk
-
- true
-
+ enable
+ disable
+ True
+ True
+ 1591
-
- true
-
+ true
-
- true
-
+ latest
+ 9999
-
-
-
+ True
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/Benchmark.NetCore/CounterBenchmarks.cs b/Benchmark.NetCore/CounterBenchmarks.cs
new file mode 100644
index 00000000..505ce984
--- /dev/null
+++ b/Benchmark.NetCore/CounterBenchmarks.cs
@@ -0,0 +1,25 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+public class CounterBenchmarks
+{
+ private readonly CollectorRegistry _registry;
+ private readonly MetricFactory _factory;
+ private readonly Counter _counter;
+
+ public CounterBenchmarks()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry);
+
+ _counter = _factory.CreateCounter("gauge", "help text");
+ }
+
+ [Benchmark]
+ public void IncToCurrentTimeUtc()
+ {
+ _counter.IncToCurrentTimeUtc();
+ }
+}
diff --git a/Benchmark.NetCore/ExemplarBenchmarks.cs b/Benchmark.NetCore/ExemplarBenchmarks.cs
new file mode 100644
index 00000000..35672fc3
--- /dev/null
+++ b/Benchmark.NetCore/ExemplarBenchmarks.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+[MemoryDiagnoser]
+//[EventPipeProfiler(EventPipeProfile.CpuSampling)]
+public class ExemplarBenchmarks
+{
+ private readonly CollectorRegistry _registry;
+ private readonly MetricFactory _factory;
+ private readonly Counter _counter;
+
+ public ExemplarBenchmarks()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry);
+
+ // We provide the exemplars manually, without using the default behavior.
+ _factory.ExemplarBehavior = ExemplarBehavior.NoExemplars();
+
+ _counter = _factory.CreateCounter("gauge", "help text");
+ }
+
+ // Just establish a baseline - how much time/memory do we spend if not recording an exemplar.
+ [Benchmark(Baseline = true)]
+ public void Observe_NoExemplar()
+ {
+ _counter.Inc(123);
+ }
+
+ // Just as a sanity check, this should not cost us anything extra and may even be cheaper as we skip the default behavior lookup.
+ [Benchmark]
+ public void Observe_EmptyExemplar()
+ {
+ _counter.Inc(123, Exemplar.None);
+ }
+
+ private static readonly Exemplar.LabelKey CustomLabelKey1 = Exemplar.Key("my_key");
+ private static readonly Exemplar.LabelKey CustomLabelKey2 = Exemplar.Key("my_key2");
+
+ // A manually specified custom exemplar with some arbitrary value.
+ [Benchmark]
+ public void Observe_CustomExemplar()
+ {
+ _counter.Inc(123, Exemplar.From(CustomLabelKey1.WithValue("my_value"), CustomLabelKey2.WithValue("my_value2")));
+ }
+
+ // An exemplar extracted from the current trace context when there is no trace context.
+ [Benchmark]
+ public void Observe_ExemplarFromEmptyTraceContext()
+ {
+ _counter.Inc(123, Exemplar.FromTraceContext());
+ }
+
+ [GlobalSetup(Targets = new[] { nameof(Observe_ExemplarFromTraceContext) })]
+ public void Setup_ExemplarFromTraceContext()
+ {
+ new Activity("test activity").Start();
+
+ if (Activity.Current == null)
+ throw new Exception("Sanity check failed.");
+ }
+
+ // An exemplar extracted from the current trace context when there is a trace context.
+ [Benchmark]
+ public void Observe_ExemplarFromTraceContext()
+ {
+ _counter.Inc(123, Exemplar.FromTraceContext());
+ }
+
+ [GlobalCleanup(Targets = new[] { nameof(Observe_ExemplarFromEmptyTraceContext), nameof(Observe_ExemplarFromTraceContext) })]
+ public void Cleanup()
+ {
+ Activity.Current = null;
+ }
+}
diff --git a/Benchmark.NetCore/GaugeBenchmarks.cs b/Benchmark.NetCore/GaugeBenchmarks.cs
new file mode 100644
index 00000000..6de13456
--- /dev/null
+++ b/Benchmark.NetCore/GaugeBenchmarks.cs
@@ -0,0 +1,25 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+public class GaugeBenchmarks
+{
+ private readonly CollectorRegistry _registry;
+ private readonly MetricFactory _factory;
+ private readonly Gauge _gauge;
+
+ public GaugeBenchmarks()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry);
+
+ _gauge = _factory.CreateGauge("gauge", "help text");
+ }
+
+ [Benchmark]
+ public void SetToCurrenTimeUtc()
+ {
+ _gauge.SetToCurrentTimeUtc();
+ }
+}
diff --git a/Benchmark.NetCore/HttpExporterBenchmarks.cs b/Benchmark.NetCore/HttpExporterBenchmarks.cs
index b2970fbd..fd3729c4 100644
--- a/Benchmark.NetCore/HttpExporterBenchmarks.cs
+++ b/Benchmark.NetCore/HttpExporterBenchmarks.cs
@@ -2,61 +2,62 @@
using Microsoft.AspNetCore.Http;
using Prometheus;
using Prometheus.HttpMetrics;
-using System.Threading.Tasks;
-namespace Benchmark.NetCore
+namespace Benchmark.NetCore;
+
+[MemoryDiagnoser]
+public class HttpExporterBenchmarks
{
- [MemoryDiagnoser]
- public class HttpExporterBenchmarks
- {
- private CollectorRegistry _registry;
- private MetricFactory _factory;
- private HttpInProgressMiddleware _inProgressMiddleware;
- private HttpRequestCountMiddleware _countMiddleware;
- private HttpRequestDurationMiddleware _durationMiddleware;
+ private CollectorRegistry _registry;
+ private MetricFactory _factory;
+ private HttpInProgressMiddleware _inProgressMiddleware;
+ private HttpRequestCountMiddleware _countMiddleware;
+ private HttpRequestDurationMiddleware _durationMiddleware;
- [Params(1000, 10000)]
- public int RequestCount { get; set; }
+ [Params(100_000)]
+ public int RequestCount { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry);
- [GlobalSetup]
- public void Setup()
+ _inProgressMiddleware = new HttpInProgressMiddleware(next => Task.CompletedTask, new HttpInProgressOptions
{
- _registry = Metrics.NewCustomRegistry();
- _factory = Metrics.WithCustomRegistry(_registry);
-
- _inProgressMiddleware = new HttpInProgressMiddleware(next => Task.CompletedTask, new HttpInProgressOptions
- {
- Gauge = _factory.CreateGauge("in_progress", "help")
- });
- _countMiddleware = new HttpRequestCountMiddleware(next => Task.CompletedTask, new HttpRequestCountOptions
- {
- Counter = _factory.CreateCounter("count", "help")
- });
- _durationMiddleware = new HttpRequestDurationMiddleware(next => Task.CompletedTask, new HttpRequestDurationOptions
- {
- Histogram = _factory.CreateHistogram("duration", "help")
- });
- }
-
- [Benchmark]
- public async Task HttpInProgress()
+ Gauge = _factory.CreateGauge("in_progress", "help")
+ });
+ _countMiddleware = new HttpRequestCountMiddleware(next => Task.CompletedTask, new HttpRequestCountOptions
{
- for (var i = 0; i < RequestCount; i++)
- await _inProgressMiddleware.Invoke(new DefaultHttpContext());
- }
-
- [Benchmark]
- public async Task HttpRequestCount()
+ Counter = _factory.CreateCounter("count", "help")
+ });
+ _durationMiddleware = new HttpRequestDurationMiddleware(next => Task.CompletedTask, new HttpRequestDurationOptions
{
- for (var i = 0; i < RequestCount; i++)
- await _countMiddleware.Invoke(new DefaultHttpContext());
- }
+ Histogram = _factory.CreateHistogram("duration", "help")
+ });
+ }
- [Benchmark]
- public async Task HttpRequestDuration()
- {
- for (var i = 0; i < RequestCount; i++)
- await _durationMiddleware.Invoke(new DefaultHttpContext());
- }
+ // Reuse the same HttpContext for different requests, to not count its overhead in the benchmark.
+ private static readonly DefaultHttpContext _httpContext = new();
+
+ [Benchmark]
+ public async Task HttpInProgress()
+ {
+ for (var i = 0; i < RequestCount; i++)
+ await _inProgressMiddleware.Invoke(_httpContext);
+ }
+
+ [Benchmark]
+ public async Task HttpRequestCount()
+ {
+ for (var i = 0; i < RequestCount; i++)
+ await _countMiddleware.Invoke(_httpContext);
+ }
+
+ [Benchmark]
+ public async Task HttpRequestDuration()
+ {
+ for (var i = 0; i < RequestCount; i++)
+ await _durationMiddleware.Invoke(_httpContext);
}
}
\ No newline at end of file
diff --git a/Benchmark.NetCore/LabelBenchmarks.cs b/Benchmark.NetCore/LabelBenchmarks.cs
deleted file mode 100644
index d8694e6c..00000000
--- a/Benchmark.NetCore/LabelBenchmarks.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using BenchmarkDotNet.Attributes;
-using Prometheus;
-
-namespace Benchmark.NetCore
-{
- [MemoryDiagnoser]
- public class LabelBenchmarks
- {
- // Metric -> Variant -> Label values
- private static readonly string[][][] _labelValueRows;
-
- private const int _metricCount = 10;
- private const int _variantCount = 10;
- private const int _labelCount = 5;
-
- static LabelBenchmarks()
- {
- _labelValueRows = new string[_metricCount][][];
-
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
- {
- var variants = new string[_variantCount][];
- _labelValueRows[metricIndex] = variants;
-
- for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
- {
- var values = new string[_labelCount];
- _labelValueRows[metricIndex][variantIndex] = values;
-
- for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++)
- values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}";
- }
- }
- }
-
- private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry();
- private readonly Counter[] _metrics;
-
- public LabelBenchmarks()
- {
- _metrics = new Counter[_metricCount];
-
- var factory = Metrics.WithCustomRegistry(_registry);
-
- // Just use 1st variant for the keys (all we care about are that there is some name-like value in there).
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
- _metrics[metricIndex] = factory.CreateCounter($"metric{metricIndex:D2}", "", _labelValueRows[metricIndex][0]);
- }
-
- ///
- /// Increments an unlabelled Collector instance for a single metric.
- ///
- [Benchmark]
- public void WithoutLabels_OneMetric_OneSeries()
- {
- _metrics[0].Inc();
- }
-
- ///
- /// Increments unlabelled Collector instances for a multiple metrics.
- ///
- [Benchmark]
- public void WithoutLabels_ManyMetrics_OneSeries()
- {
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
- _metrics[metricIndex].Inc();
- }
-
- ///
- /// Increments a labelled Collector.Child instance for a single metric.
- ///
- [Benchmark]
- public void WithLabels_OneMetric_OneSeries()
- {
- _metrics[0].Labels(_labelValueRows[0][0]).Inc();
- }
-
- ///
- /// Increments labelled Collector.Child instances for one metric with multiple different sets of labels.
- ///
- [Benchmark]
- public void WithLabels_OneMetric_ManySeries()
- {
- for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
- _metrics[0].Labels(_labelValueRows[0][variantIndex]).Inc();
- }
-
- ///
- /// Increments a labelled Collector.Child instance for multiple metrics.
- ///
- [Benchmark]
- public void WithLabels_ManyMetrics_OneSeries()
- {
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
- _metrics[metricIndex].Labels(_labelValueRows[metricIndex][0]).Inc();
- }
-
- ///
- /// Increments labelled Collector.Child instances for multiple metrics with multiple different sets of labels.
- ///
- [Benchmark]
- public void WithLabels_ManyMetrics_ManySeries()
- {
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
- for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
- _metrics[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc();
- }
- }
-}
diff --git a/Benchmark.NetCore/LabelSequenceBenchmarks.cs b/Benchmark.NetCore/LabelSequenceBenchmarks.cs
new file mode 100644
index 00000000..5affa893
--- /dev/null
+++ b/Benchmark.NetCore/LabelSequenceBenchmarks.cs
@@ -0,0 +1,19 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+[MemoryDiagnoser]
+public class LabelSequenceBenchmarks
+{
+ private static readonly StringSequence Names3Array = StringSequence.From(new[] { "aaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb", "cccccccccccccc" });
+ private static readonly StringSequence Values3Array = StringSequence.From(new[] { "valueaaaaaaaaaaaaaaaaa", "valuebbbbbbbbbbbbbb", "valuecccccccccccccc" });
+
+ [Benchmark]
+ public void Create_From3Array()
+ {
+ // This is too fast for the benchmark engine, so let's create some additional work by looping through it many times.
+ for (var i = 0; i < 10_000; i++)
+ LabelSequence.From(Names3Array, Values3Array);
+ }
+}
diff --git a/Benchmark.NetCore/ManualDelayer.cs b/Benchmark.NetCore/ManualDelayer.cs
new file mode 100644
index 00000000..20897c7b
--- /dev/null
+++ b/Benchmark.NetCore/ManualDelayer.cs
@@ -0,0 +1,55 @@
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+///
+/// A delayer implementation that only returns from a delay when commanded to.
+///
+internal sealed class ManualDelayer : IDelayer, IDisposable
+{
+ public void BreakAllDelays()
+ {
+ lock (_lock)
+ {
+ _tcs.TrySetResult();
+
+ _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ _delayTask = _tcs.Task;
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_lock)
+ {
+ // If anything is still waiting, it shall not wait no more.
+ _tcs.TrySetResult();
+
+ // If anything will still wait in the future, it shall not wait at all.
+ // Beware of creating spinning loops if you dispose the delayer when something still expects to be delayed.
+ _delayTask = Task.CompletedTask;
+ }
+ }
+
+ private readonly object _lock = new();
+ private Task _delayTask;
+ private TaskCompletionSource _tcs;
+
+ public ManualDelayer()
+ {
+ _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ _delayTask = _tcs.Task;
+ }
+
+ public Task Delay(TimeSpan duration)
+ {
+ lock (_lock)
+ return _delayTask;
+ }
+
+ public Task Delay(TimeSpan duration, CancellationToken cancel)
+ {
+ lock (_lock)
+ return _delayTask.WaitAsync(cancel);
+ }
+}
diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs
new file mode 100644
index 00000000..b24bd49b
--- /dev/null
+++ b/Benchmark.NetCore/MeasurementBenchmarks.cs
@@ -0,0 +1,187 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+///
+/// We take a bunch of measurements of each type of metric and show the cost.
+///
+[MemoryDiagnoser]
+public class MeasurementBenchmarks
+{
+ [Params(100_000)]
+ public int MeasurementCount { get; set; }
+
+ [Params(ExemplarMode.Auto, ExemplarMode.None, ExemplarMode.Provided)]
+ public ExemplarMode Exemplars { get; set; }
+
+ public enum ExemplarMode
+ {
+ ///
+ /// No user-supplied exemplar but the default behavior is allowed to execute (and fail to provide an exemplar).
+ ///
+ Auto,
+
+ ///
+ /// Explicitly indicating that no exemplar is to be used.
+ ///
+ None,
+
+ ///
+ /// Explicitly providing an exemplar.
+ ///
+ Provided
+ }
+
+ private readonly CollectorRegistry _registry;
+ private readonly MetricFactory _factory;
+
+ private readonly Counter.Child _counter;
+ private readonly Gauge.Child _gauge;
+ private readonly Summary.Child _summary;
+ private readonly Histogram.Child _histogram;
+ private readonly Histogram.Child _wideHistogram;
+
+ private readonly Exemplar.LabelKey _traceIdKey = Exemplar.Key("trace_id");
+ private readonly Exemplar.LabelKey _spanIdKey = Exemplar.Key("span_id");
+
+ // We preallocate the exemplar values to avoid measuring the random()->string serialization as part of the benchmark.
+ // What we care about measuring is the overhead of processing the exemplar, not of generating/serializing it.
+ private readonly string _traceIdValue = "7f825eb926a90af6961ace5f9a239945";
+ private readonly string _spanIdValue = "a77603af408a13ec";
+
+ private Exemplar.LabelPair _traceIdLabel;
+ private Exemplar.LabelPair _spanIdLabel;
+
+ ///
+ /// The max value we observe for histograms, to give us coverage of all the histogram buckets
+ /// but not waste 90% of the benchmark on incrementing the +Inf bucket.
+ ///
+ private const int WideHistogramMaxValue = 32 * 1024;
+
+ // Same but for the regular histogram.
+ private readonly int _regularHistogramMaxValue;
+
+ private static readonly string[] labelNames = ["label"];
+
+ public MeasurementBenchmarks()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry);
+
+ // We add a label to each, as labeled usage is the typical usage.
+ var counterTemplate = _factory.CreateCounter("counter", "test counter", labelNames);
+ var gaugeTemplate = _factory.CreateGauge("gauge", "test gauge", labelNames);
+ var summaryTemplate = _factory.CreateSummary("summary", "test summary", labelNames, new SummaryConfiguration
+ {
+ Objectives = new QuantileEpsilonPair[]
+ {
+ new(0.9, 0.1),
+ new(0.95, 0.01),
+ new(0.99, 0.005)
+ }
+ });
+
+ // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default.
+ var regularHistogramBuckets = Prometheus.Histogram.ExponentialBuckets(0.001, 2, 16);
+
+ // Last one is +inf, so take the second-to-last.
+ _regularHistogramMaxValue = (int)regularHistogramBuckets[^2];
+
+ var histogramTemplate = _factory.CreateHistogram("histogram", "test histogram", labelNames, new HistogramConfiguration
+ {
+ Buckets = regularHistogramBuckets
+ });
+
+ var wideHistogramTemplate = _factory.CreateHistogram("wide_histogram", "test histogram", labelNames, new HistogramConfiguration
+ {
+ Buckets = Prometheus.Histogram.LinearBuckets(1, WideHistogramMaxValue / 128, 128)
+ });
+
+ // We cache the children, as is typical usage.
+ _counter = counterTemplate.WithLabels("label value");
+ _gauge = gaugeTemplate.WithLabels("label value");
+ _summary = summaryTemplate.WithLabels("label value");
+ _histogram = histogramTemplate.WithLabels("label value");
+ _wideHistogram = wideHistogramTemplate.WithLabels("label value");
+
+ // We take a single measurement, to warm things up and avoid any first-call impact.
+ _counter.Inc();
+ _gauge.Set(1);
+ _summary.Observe(1);
+ _histogram.Observe(1);
+ _wideHistogram.Observe(1);
+ }
+
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ // There is an unavoidable string->bytes encoding overhead from this.
+ // As it is fixed overhead based on user data size, we pre-encode the strings here to avoid them influencing the benchmark results.
+ // We only preallocate the strings, however (creating the LabelPairs). We still do as much of the exemplar "processing" inline as feasible, to be realistic.
+ _traceIdLabel = _traceIdKey.WithValue(_traceIdValue);
+ _spanIdLabel = _spanIdKey.WithValue(_spanIdValue);
+ }
+
+ [Benchmark]
+ public void Counter()
+ {
+ var exemplarProvider = GetExemplarProvider();
+
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ _counter.Inc(exemplarProvider());
+ }
+ }
+
+ [Benchmark]
+ public void Gauge()
+ {
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ _gauge.Set(i);
+ }
+ }
+
+ [Benchmark]
+ public void Histogram()
+ {
+ var exemplarProvider = GetExemplarProvider();
+
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ var value = i % _regularHistogramMaxValue;
+ _histogram.Observe(value, exemplarProvider());
+ }
+ }
+
+ [Benchmark]
+ public void WideHistogram()
+ {
+ var exemplarProvider = GetExemplarProvider();
+
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ var value = i % WideHistogramMaxValue;
+ _wideHistogram.Observe(value, exemplarProvider());
+ }
+ }
+
+ // Disabled because it is slow and Summary is a legacy metric type that is not recommended for new usage.
+ //[Benchmark]
+ public void Summary()
+ {
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ _summary.Observe(i);
+ }
+ }
+
+ private Func GetExemplarProvider() => Exemplars switch
+ {
+ ExemplarMode.Auto => () => null,
+ ExemplarMode.None => () => Exemplar.None,
+ ExemplarMode.Provided => () => Exemplar.From(_traceIdLabel, _spanIdLabel),
+ _ => throw new NotImplementedException(),
+ };
+}
diff --git a/Benchmark.NetCore/MeterAdapterBenchmarks.cs b/Benchmark.NetCore/MeterAdapterBenchmarks.cs
new file mode 100644
index 00000000..b3abf76f
--- /dev/null
+++ b/Benchmark.NetCore/MeterAdapterBenchmarks.cs
@@ -0,0 +1,95 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+using SDM = System.Diagnostics.Metrics;
+
+namespace Benchmark.NetCore;
+
+///
+/// Equivalent of MeasurementBenchmarks, except we publish the data via .NET Meters API and convert via MeterAdapter.
+///
+[MemoryDiagnoser]
+//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)]
+public class MeterAdapterBenchmarks
+{
+ [Params(100_000)]
+ public int MeasurementCount { get; set; }
+
+ private readonly SDM.Meter _meter = new("prometheus-net benchmark");
+ private readonly SDM.Counter _intCounter;
+ private readonly SDM.Counter _floatCounter;
+ private readonly SDM.Histogram _intHistogram;
+ private readonly SDM.Histogram _floatHistogram;
+
+ private readonly CollectorRegistry _registry;
+
+ private readonly IDisposable _meterAdapter;
+
+ private readonly KeyValuePair _label = new("label", "label value");
+
+ public MeterAdapterBenchmarks()
+ {
+ _intCounter = _meter.CreateCounter("int_counter", description: "This is an integer counter.");
+ _floatCounter = _meter.CreateCounter("float_counter", description: "This is a floating-point counter.");
+ _intHistogram = _meter.CreateHistogram("int_histogram", description: "This is an integer histogram.");
+ _floatHistogram = _meter.CreateHistogram("float_histogram", description: "This is a floating-point histogram.");
+
+ _registry = Metrics.NewCustomRegistry();
+
+ _meterAdapter = MeterAdapter.StartListening(new MeterAdapterOptions
+ {
+ InstrumentFilterPredicate = instrument => instrument.Meter == _meter,
+ Registry = _registry,
+ // We resolve a custom set of buckets here, for maximum stress and to avoid easily reused default buckets.
+ // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default.
+ ResolveHistogramBuckets = _ => Histogram.ExponentialBuckets(0.001, 2, 16)
+ });
+
+ // We take a single measurement, to warm things up and avoid any first-call impact.
+ _intCounter.Add(1, _label);
+ _floatCounter.Add(1, _label);
+ _intHistogram.Record(1, _label);
+ _floatHistogram.Record(1, _label);
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _meterAdapter.Dispose();
+ }
+
+ [Benchmark]
+ public void CounterInt()
+ {
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ _intCounter.Add(1, _label);
+ }
+ }
+
+ [Benchmark]
+ public void CounterFloat()
+ {
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ _floatCounter.Add(1, _label);
+ }
+ }
+
+ [Benchmark]
+ public void HistogramInt()
+ {
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ _intHistogram.Record(i, _label);
+ }
+ }
+
+ [Benchmark]
+ public void HistogramFloat()
+ {
+ for (var i = 0; i < MeasurementCount; i++)
+ {
+ _floatHistogram.Record(i, _label);
+ }
+ }
+}
diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs
index eb49c38b..09028762 100644
--- a/Benchmark.NetCore/MetricCreationBenchmarks.cs
+++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs
@@ -1,207 +1,197 @@
using BenchmarkDotNet.Attributes;
using Prometheus;
-namespace Benchmark.NetCore
+namespace Benchmark.NetCore;
+
+///
+/// One pattern advocated by Prometheus documentation is to implement scraping of external systems by
+/// creating a brand new set of metrics for each scrape. So let's benchmark this scenario.
+///
+[MemoryDiagnoser]
+// This seems to need a lot of warmup to stabilize.
+[WarmupCount(50)]
+//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)]
+public class MetricCreationBenchmarks
{
///
- /// One pattern advocated by Prometheus documentation is to implement scraping of external systems by
- /// creating a brand new set of metrics for each scrape. So let's benchmark this scenario.
+ /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results.
///
- [MemoryDiagnoser]
- public class MetricCreationBenchmarks
- {
- ///
- /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results.
- ///
- private const int _metricCount = 10000;
+ private const int _metricCount = 10_000;
- ///
- /// Some benchmarks try to register metrics that already exist.
- ///
- private const int _duplicateCount = 5;
+ ///
+ /// How many times we repeat acquiring and incrementing the same instance.
+ ///
+ [Params(1, 10)]
+ public int RepeatCount { get; set; }
- private const string _help = "arbitrary help message for metric, not relevant for benchmarking";
+ [Params(true, false)]
+ public bool IncludeStaticLabels { get; set; }
- private static readonly string[] _metricNames;
+ private const string _help = "arbitrary help message for metric, not relevant for benchmarking";
- static MetricCreationBenchmarks()
- {
- _metricNames = new string[_metricCount];
+ private static readonly string[] _metricNames;
- for (var i = 0; i < _metricCount; i++)
- _metricNames[i] = $"metric_{i:D4}";
- }
+ static MetricCreationBenchmarks()
+ {
+ _metricNames = new string[_metricCount];
- private CollectorRegistry _registry;
- private MetricFactory _factory;
+ for (var i = 0; i < _metricCount; i++)
+ _metricNames[i] = $"metric_{i:D4}";
+ }
- [IterationSetup]
- public void Setup()
- {
- _registry = Metrics.NewCustomRegistry();
- _factory = Metrics.WithCustomRegistry(_registry);
- }
+ private CollectorRegistry _registry;
+ private IMetricFactory _factory;
+ private IManagedLifetimeMetricFactory _managedLifetimeFactory;
- private static readonly string[] _labelNames = new[] { "foo", "bar", "baz" };
+ [IterationSetup]
+ public void Setup()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry);
- [Benchmark]
- public void Counter_Many()
+ if (IncludeStaticLabels)
{
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateCounter(_metricNames[i], _help, new CounterConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
+ _registry.SetStaticLabels(new Dictionary
+ {
+ { "static_foo", "static_bar" },
+ { "static_foo1", "static_bar" },
+ { "static_foo2", "static_bar" },
+ { "static_foo3", "static_bar" },
+ { "static_foo4", "static_bar" }
+ });
+
+ _factory = _factory.WithLabels(new Dictionary
+ {
+ { "static_gaa", "static_bar" },
+ { "static_gaa1", "static_bar" },
+ { "static_gaa2", "static_bar" },
+ { "static_gaa3", "static_bar" },
+ { "static_gaa4", "static_bar" },
+ { "static_gaa5", "static_bar" },
+ });
}
- [Benchmark]
- public void Gauge_Many()
- {
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateGauge(_metricNames[i], _help, new GaugeConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
- }
+ _managedLifetimeFactory = _factory.WithManagedLifetime(expiresAfter: TimeSpan.FromHours(1));
+ }
- [Benchmark]
- public void Summary_Many()
- {
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateSummary(_metricNames[i], _help, new SummaryConfiguration
- {
- LabelNames = _labelNames
- }).Observe(123);
- }
+ // We use the same strings both for the names and the values.
+ private static readonly string[] _labels = ["foo", "bar", "baz"];
- [Benchmark]
- public void Histogram_Many()
- {
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateHistogram(_metricNames[i], _help, new HistogramConfiguration
- {
- LabelNames = _labelNames
- }).Observe(123);
- }
+ private static readonly CounterConfiguration _counterConfiguration = CounterConfiguration.Default;
+ private static readonly GaugeConfiguration _gaugeConfiguration = GaugeConfiguration.Default;
+ private static readonly SummaryConfiguration _summaryConfiguration = SummaryConfiguration.Default;
+ private static readonly HistogramConfiguration _histogramConfiguration = HistogramConfiguration.Default;
- [Benchmark]
- public void Counter_Many_Duplicates()
+ [Benchmark]
+ public void Counter_ArrayLabels()
+ {
+ for (var i = 0; i < _metricCount; i++)
{
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateCounter(_metricNames[i], _help, new CounterConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
- }
+ var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration);
- [Benchmark]
- public void Gauge_Many_Duplicates()
- {
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateGauge(_metricNames[i], _help, new GaugeConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ metric.WithLabels(_labels).Inc();
}
+ }
- [Benchmark]
- public void Summary_Many_Duplicates()
- {
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateSummary(_metricNames[i], _help, new SummaryConfiguration
- {
- LabelNames = _labelNames
- }).Observe(123);
- }
+ [Benchmark]
+ public void Counter_MemoryLabels()
+ {
+ // The overloads accepting string[] and ROM are functionally equivalent, though any conversion adds some overhead.
+ var labelsMemory = _labels.AsMemory();
- [Benchmark]
- public void Histogram_Many_Duplicates()
+ for (var i = 0; i < _metricCount; i++)
{
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- for (var i = 0; i < _metricCount; i++)
- _factory.CreateHistogram(_metricNames[i], _help, new HistogramConfiguration
- {
- LabelNames = _labelNames
- }).Observe(123);
- }
+ var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration);
- [Benchmark]
- public void Counter()
- {
- _factory.CreateCounter(_metricNames[0], _help, new CounterConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ metric.WithLabels(labelsMemory).Inc();
}
+ }
- [Benchmark]
- public void Gauge()
+ [Benchmark]
+ public void Counter_SpanLabels()
+ {
+ var labelsSpan = _labels.AsSpan();
+
+ for (var i = 0; i < _metricCount; i++)
{
- _factory.CreateGauge(_metricNames[0], _help, new GaugeConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
+ var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration);
+
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ // If code is aware that it is repeating the registration, using the Span overloads offers optimal performance.
+ metric.WithLabels(labelsSpan).Inc();
}
+ }
+
+ [Benchmark]
+ public void Counter_ManagedLifetime()
+ {
+ // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known.
+ var labelsSpan = _labels.AsSpan();
- [Benchmark]
- public void Summary()
+ for (var i = 0; i < _metricCount; i++)
{
- _factory.CreateSummary(_metricNames[0], _help, new SummaryConfiguration
- {
- LabelNames = _labelNames
- }).Observe(123);
+ var metric = _managedLifetimeFactory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration);
+
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ metric.WithLease(static x => x.Inc(), labelsSpan);
}
+ }
- [Benchmark]
- public void Histogram()
- {
- _factory.CreateHistogram(_metricNames[0], _help, new HistogramConfiguration
+ [Benchmark]
+ public void Counter_10Duplicates()
+ {
+ // We try to register the same metric 10 times, which is wasteful but shows us the overhead from doing this.
+
+ for (var dupe = 0; dupe < 10; dupe++)
+ for (var i = 0; i < _metricCount; i++)
{
- LabelNames = _labelNames
- }).Observe(123);
- }
+ var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration);
- [Benchmark]
- public void Counter_Duplicates()
- {
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- _factory.CreateCounter(_metricNames[0], _help, new CounterConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
- }
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ // We do not use the Span overload here to exemplify "naive" code not aware of the repetition.
+ metric.WithLabels(_labels).Inc();
+ }
+ }
- [Benchmark]
- public void Gauge_Duplicates()
+ [Benchmark]
+ public void Gauge()
+ {
+ for (var i = 0; i < _metricCount; i++)
{
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- _factory.CreateGauge(_metricNames[0], _help, new GaugeConfiguration
- {
- LabelNames = _labelNames
- }).Inc();
+ var metric = _factory.CreateGauge(_metricNames[i], _help, _labels, _gaugeConfiguration);
+
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ // We do not use the Span overload here to exemplify "naive" code not aware of the repetition.
+ metric.WithLabels(_labels).Set(repeat);
}
+ }
- [Benchmark]
- public void Summary_Duplicates()
+ // Disabled because it is slow and Summary is a legacy metric type that is not recommended for new usage.
+ //[Benchmark]
+ public void Summary()
+ {
+ for (var i = 0; i < _metricCount; i++)
{
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- _factory.CreateSummary(_metricNames[0], _help, new SummaryConfiguration
- {
- LabelNames = _labelNames
- }).Observe(123);
+ var metric = _factory.CreateSummary(_metricNames[i], _help, _labels, _summaryConfiguration);
+
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ // We do not use the Span overload here to exemplify "naive" code not aware of the repetition.
+ metric.WithLabels(_labels).Observe(123);
}
+ }
- [Benchmark]
- public void Histogram_Duplicates()
+ [Benchmark]
+ public void Histogram()
+ {
+ for (var i = 0; i < _metricCount; i++)
{
- for (var dupe = 0; dupe < _duplicateCount; dupe++)
- _factory.CreateHistogram(_metricNames[0], _help, new HistogramConfiguration
- {
- LabelNames = _labelNames
- }).Observe(123);
+ var metric = _factory.CreateHistogram(_metricNames[i], _help, _labels, _histogramConfiguration);
+
+ for (var repeat = 0; repeat < RepeatCount; repeat++)
+ // We do not use the Span overload here to exemplify "naive" code not aware of the repetition.
+ metric.WithLabels(_labels).Observe(123);
}
}
}
diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs
new file mode 100644
index 00000000..883ad131
--- /dev/null
+++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs
@@ -0,0 +1,181 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+///
+/// Here we try to ensure that creating/using expiring metrics does not impose too heavy of a performance burden or create easily identifiable memory leaks.
+///
+[MemoryDiagnoser]
+// This seems to need a lot of warmup to stabilize.
+[WarmupCount(50)]
+// This seems to need a lot of iterations to stabilize.
+[IterationCount(50)]
+//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)]
+public class MetricExpirationBenchmarks
+{
+ ///
+ /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results.
+ ///
+ private const int _metricCount = 25_000;
+
+ ///
+ /// If true, we preallocate a lifetime for every metric, so the benchmark only measures the actual usage
+ /// of the metric and not the first-lease setup, as these two components of a metric lifetime can have different impact in different cases.
+ ///
+ [Params(true, false)]
+ public bool PreallocateLifetime { get; set; }
+
+ private const string _help = "arbitrary help message for metric, not relevant for benchmarking";
+
+ private static readonly string[] _metricNames;
+
+ static MetricExpirationBenchmarks()
+ {
+ _metricNames = new string[_metricCount];
+
+ for (var i = 0; i < _metricCount; i++)
+ _metricNames[i] = $"metric_{i:D4}";
+ }
+
+ private CollectorRegistry _registry;
+ private IManagedLifetimeMetricFactory _factory;
+
+ // We use the same strings both for the names and the values.
+ private static readonly string[] _labels = ["foo", "bar", "baz"];
+
+ private ManualDelayer _delayer;
+
+ private readonly ManagedLifetimeMetricHandle[] _counters = new ManagedLifetimeMetricHandle[_metricCount];
+
+ [IterationSetup]
+ public void Setup()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry)
+ // We enable lifetime management but set the expiration timer very high to avoid expiration in the middle of the benchmark.
+ .WithManagedLifetime(expiresAfter: TimeSpan.FromHours(24));
+
+ _delayer = new();
+
+ for (var i = 0; i < _metricCount; i++)
+ {
+ var counter = CreateCounter(_metricNames[i], _help, _labels);
+ _counters[i] = counter;
+
+ // Both the usage and the lifetime allocation matter but we want to bring them out separately in the benchmarks.
+ if (PreallocateLifetime)
+ counter.AcquireRefLease(out _, _labels).Dispose();
+ }
+ }
+
+ [IterationCleanup]
+ public void Cleanup()
+ {
+ // Ensure that all metrics are marked as expired, so the expiration processing logic destroys them all.
+ // This causes some extra work during cleanup but on the other hand, it ensures good isolation between iterations, so fine.
+ foreach (var counter in _counters)
+ counter.SetAllKeepaliveTimestampsToDistantPast();
+
+ // Twice and with some sleep time, just for good measure.
+ // BenchmarkDotNet today does not support async here, so we do a sleep to let the reaper thread process things.
+ _delayer.BreakAllDelays();
+ Thread.Sleep(millisecondsTimeout: 5);
+ }
+
+ private ManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labels)
+ {
+ var counter = (ManagedLifetimeMetricHandle)_factory.CreateCounter(name, help, labels);
+
+ // We use a breakable delayer to ensure that we can control when the metric expiration logic runs, so one iteration
+ // of the benchmark does not start to interfere with another iteration just because some timers are left running.
+ counter.Delayer = _delayer;
+
+ return counter;
+ }
+
+ [Benchmark]
+ public void Use_AutoLease_Once()
+ {
+ for (var i = 0; i < _metricCount; i++)
+ {
+ var wrapper = _counters[i].WithExtendLifetimeOnUse();
+
+ // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed.
+ // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists).
+ wrapper.WithLabels(_labels).Inc();
+ }
+ }
+
+ [Benchmark]
+ public void Use_AutoLease_With10Duplicates()
+ {
+ for (var dupe = 0; dupe < 10; dupe++)
+ for (var i = 0; i < _metricCount; i++)
+ {
+ var wrapper = _counters[i].WithExtendLifetimeOnUse();
+
+ // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed.
+ // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists).
+ wrapper.WithLabels(_labels).Inc();
+ }
+ }
+
+ [Benchmark]
+ public void Use_AutoLease_Once_With10Repeats()
+ {
+ for (var i = 0; i < _metricCount; i++)
+ {
+ var wrapper = _counters[i].WithExtendLifetimeOnUse();
+
+ for (var repeat = 0; repeat < 10; repeat++)
+ // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed.
+ // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists).
+ wrapper.WithLabels(_labels).Inc();
+ }
+ }
+
+ [Benchmark(Baseline = true)]
+ public void Use_ManualLease()
+ {
+ // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known.
+ var labelValues = _labels.AsSpan();
+
+ for (var i = 0; i < _metricCount; i++)
+ {
+ using var lease = _counters[i].AcquireLease(out var instance, labelValues);
+ instance.Inc();
+ }
+ }
+
+ [Benchmark]
+ public void Use_ManualRefLease()
+ {
+ // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known.
+ var labelValues = _labels.AsSpan();
+
+ for (var i = 0; i < _metricCount; i++)
+ {
+ using var lease = _counters[i].AcquireRefLease(out var instance, labelValues);
+ instance.Inc();
+ }
+ }
+
+ private static void IncrementCounter(ICounter counter)
+ {
+ counter.Inc();
+ }
+
+ [Benchmark]
+ public void Use_WithLease()
+ {
+ // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known.
+ var labelValues = _labels.AsSpan();
+
+ // Reuse the delegate.
+ Action incrementCounterAction = IncrementCounter;
+
+ for (var i = 0; i < _metricCount; i++)
+ _counters[i].WithLease(incrementCounterAction, labelValues);
+ }
+}
diff --git a/Benchmark.NetCore/MetricLookupBenchmarks.cs b/Benchmark.NetCore/MetricLookupBenchmarks.cs
new file mode 100644
index 00000000..0c4f9572
--- /dev/null
+++ b/Benchmark.NetCore/MetricLookupBenchmarks.cs
@@ -0,0 +1,111 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+///
+/// This benchmark aims to show that accessing preexisting metrics using various label combinations is fast and does not allocate memory.
+///
+[MemoryDiagnoser]
+public class MetricLookupBenchmarks
+{
+ // Metric -> Variant -> Label values
+ private static readonly string[][][] _labelValueRows;
+
+ private const int _metricCount = 10;
+ private const int _variantCount = 10;
+ private const int _labelCount = 5;
+
+ static MetricLookupBenchmarks()
+ {
+ _labelValueRows = new string[_metricCount][][];
+
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ {
+ var variants = new string[_variantCount][];
+ _labelValueRows[metricIndex] = variants;
+
+ for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
+ {
+ var values = new string[_labelCount];
+ _labelValueRows[metricIndex][variantIndex] = values;
+
+ for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++)
+ values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}";
+ }
+ }
+ }
+
+ private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry();
+ private readonly Counter[] _metrics;
+
+ public MetricLookupBenchmarks()
+ {
+ _metrics = new Counter[_metricCount];
+
+ var factory = Metrics.WithCustomRegistry(_registry);
+
+ // Just use 1st variant for the keys (all we care about are that there is some name-like value in there).
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ _metrics[metricIndex] = factory.CreateCounter($"metric{metricIndex:D2}", "", _labelValueRows[metricIndex][0]);
+ }
+
+ ///
+ /// Increments an unlabelled Collector instance for a single metric.
+ ///
+ [Benchmark]
+ public void WithoutLabels_OneMetric_OneSeries()
+ {
+ _metrics[0].Inc();
+ }
+
+ ///
+ /// Increments unlabelled Collector instances for a multiple metrics.
+ ///
+ [Benchmark]
+ public void WithoutLabels_ManyMetrics_OneSeries()
+ {
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ _metrics[metricIndex].Inc();
+ }
+
+ ///
+ /// Increments a labelled Collector.Child instance for a single metric.
+ ///
+ [Benchmark]
+ public void WithLabels_OneMetric_OneSeries()
+ {
+ _metrics[0].WithLabels(_labelValueRows[0][0]).Inc();
+ }
+
+ ///
+ /// Increments labelled Collector.Child instances for one metric with multiple different sets of labels.
+ ///
+ [Benchmark]
+ public void WithLabels_OneMetric_ManySeries()
+ {
+ for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
+ _metrics[0].WithLabels(_labelValueRows[0][variantIndex]).Inc();
+ }
+
+ ///
+ /// Increments a labelled Collector.Child instance for multiple metrics.
+ ///
+ [Benchmark]
+ public void WithLabels_ManyMetrics_OneSeries()
+ {
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ _metrics[metricIndex].WithLabels(_labelValueRows[metricIndex][0]).Inc();
+ }
+
+ ///
+ /// Increments labelled Collector.Child instances for multiple metrics with multiple different sets of labels.
+ ///
+ [Benchmark]
+ public void WithLabels_ManyMetrics_ManySeries()
+ {
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
+ _metrics[metricIndex].WithLabels(_labelValueRows[metricIndex][variantIndex]).Inc();
+ }
+}
diff --git a/Benchmark.NetCore/MetricPusherBenchmarks.cs b/Benchmark.NetCore/MetricPusherBenchmarks.cs
index a4ca6f2d..83943efa 100644
--- a/Benchmark.NetCore/MetricPusherBenchmarks.cs
+++ b/Benchmark.NetCore/MetricPusherBenchmarks.cs
@@ -1,86 +1,78 @@
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using Prometheus;
using tester;
-namespace Benchmark.NetCore
+namespace Benchmark.NetCore;
+
+// NB! This benchmark requires the Tester project to be running and the MetricPusherTester module to be active (to receive the data).
+// If there is no tester listening, the results will be overly good because the runtime is under less I/O load.
+[MemoryDiagnoser]
+[SimpleJob(RunStrategy.Monitoring, warmupCount: 0)]
+public class MetricPusherBenchmarks
{
- // NB! This benchmark requires the Tester project to be running and the MetricPusherTester module to be active (to receive the data).
- // If there is no tester listening, the results will be overly good because the runtime is under less I/O load.
- [MemoryDiagnoser]
- [SimpleJob(RunStrategy.Monitoring, warmupCount:0)]
- public class MetricPusherBenchmarks
+ private static string MetricPusherUrl = $"http://localhost:{TesterConstants.TesterPort}";
+
+ [GlobalSetup]
+ public async Task GlobalSetup()
{
- private static string MetricPusherUrl = $"http://localhost:{TesterConstants.TesterPort}";
+ // Verify that there is a MetricPusher listening on Tester.
- [GlobalSetup]
- public async Task GlobalSetup()
+ using (var client = new HttpClient())
{
- // Verify that there is a MetricPusher listening on Tester.
-
- using (var client = new HttpClient())
+ try
{
- try
- {
- var result = await client.GetAsync(MetricPusherUrl);
- result.EnsureSuccessStatusCode();
- }
- catch
- {
- throw new Exception("You must start the Tester.NetCore project and configure it to use MetricPusherTester in its Program.cs before running this benchmark.");
- }
+ var result = await client.GetAsync(MetricPusherUrl);
+ result.EnsureSuccessStatusCode();
+ }
+ catch
+ {
+ throw new Exception("You must start the Tester.NetCore project and configure it to use MetricPusherTester in its Program.cs before running this benchmark.");
}
}
+ }
+
-
- [Benchmark]
- public async Task PushTest()
+ [Benchmark]
+ public async Task PushTest()
+ {
+ var registry = Metrics.NewCustomRegistry();
+ var factory = Metrics.WithCustomRegistry(registry);
+
+ var pusher = new MetricPusher(new MetricPusherOptions
{
- var registry = Metrics.NewCustomRegistry();
- var factory = Metrics.WithCustomRegistry(registry);
-
- var pusher = new MetricPusher(new MetricPusherOptions
- {
- Endpoint = MetricPusherUrl,
- Registry = registry,
- IntervalMilliseconds = 30,
- Job = "job"
- });
- pusher.Start();
+ Endpoint = MetricPusherUrl,
+ Registry = registry,
+ IntervalMilliseconds = 30,
+ Job = "job"
+ });
+ pusher.Start();
- var counters = new List();
- for (int i = 0; i < 1000; i++)
- {
- var counter = factory.CreateCounter($"Counter{i}", String.Empty);
- counters.Add(counter);
- }
+ var counters = new List();
+ for (int i = 0; i < 1000; i++)
+ {
+ var counter = factory.CreateCounter($"Counter{i}", String.Empty);
+ counters.Add(counter);
+ }
- var ct = new CancellationTokenSource();
- var incTask = Task.Run(async () =>
+ var ct = new CancellationTokenSource();
+ var incTask = Task.Run(async () =>
+ {
+ while (!ct.IsCancellationRequested)
{
- while (!ct.IsCancellationRequested)
+ foreach (var counter in counters)
{
- foreach (var counter in counters)
- {
- counter.Inc();
- }
-
- await Task.Delay(30);
+ counter.Inc();
}
- });
- await Task.Delay(5000);
- ct.Cancel();
- await incTask;
+ await Task.Delay(30);
+ }
+ });
- pusher.Stop();
- }
+ await Task.Delay(5000);
+ ct.Cancel();
+ await incTask;
+
+ pusher.Stop();
}
}
\ No newline at end of file
diff --git a/Benchmark.NetCore/Program.cs b/Benchmark.NetCore/Program.cs
index 9daa49ab..94822efb 100644
--- a/Benchmark.NetCore/Program.cs
+++ b/Benchmark.NetCore/Program.cs
@@ -1,14 +1,13 @@
using BenchmarkDotNet.Running;
-namespace Benchmark.NetCore
+namespace Benchmark.NetCore;
+
+internal class Program
{
- internal class Program
+ private static void Main(string[] args)
{
- private static void Main(string[] args)
- {
- // Give user possibility to choose which benchmark to run.
- // Can be overridden from the command line with the --filter option.
- new BenchmarkSwitcher(typeof(Program).Assembly).Run(args);
- }
+ // Give user possibility to choose which benchmark to run.
+ // Can be overridden from the command line with the --filter option.
+ new BenchmarkSwitcher(typeof(Program).Assembly).Run(args);
}
}
diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs
new file mode 100644
index 00000000..eb95dc2d
--- /dev/null
+++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs
@@ -0,0 +1,285 @@
+using System.Diagnostics.Metrics;
+using BenchmarkDotNet.Attributes;
+using OpenTelemetry.Metrics;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+/*
+BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2)
+AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
+.NET SDK 8.0.100
+ [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
+ DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
+ Job-PPCGVJ : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2
+
+
+| Method | Job | MaxIterationCount | Mean | Error | StdDev | Allocated |
+|------------------------------ |----------- |------------------ |------------:|---------:|---------:|----------:|
+| PromNetCounter | DefaultJob | Default | 230.4 μs | 1.20 μs | 1.12 μs | - |
+| PromNetHistogram | DefaultJob | Default | 956.7 μs | 3.28 μs | 3.07 μs | - |
+| OTelCounter | DefaultJob | Default | 10,998.2 μs | 35.54 μs | 31.51 μs | 11 B |
+| OTelHistogram | DefaultJob | Default | 12,110.3 μs | 17.08 μs | 14.26 μs | 11 B |
+| PromNetHistogramForAdHocLabel | Job-PPCGVJ | 16 | 716.2 μs | 30.20 μs | 26.77 μs | 664000 B |
+| OTelHistogramForAdHocLabel | Job-PPCGVJ | 16 | 350.5 μs | 1.91 μs | 1.79 μs | 96000 B |
+*/
+
+///
+/// We compare pure measurement (not serializing the data) with prometheus-net SDK and OpenTelemetry .NET SDK.
+///
+///
+/// Design logic:
+/// * Metrics are initialized once on application startup.
+/// * Metrics typically measure "sessions" - there are sets of metrics that are related through shared identifiers and a shared lifetime (e.g. HTTP request),
+/// with all the identifiers for the metrics created when the sesison is initialized (e.g. when the HTTP connection is established).
+/// * Metrics typically are also used report SLI (Service Level Indicator); these involve emitting a lot of unique dimension values, for example: CustomerId.
+///
+/// Excluded from measurement:
+/// * Meter setup (because meters are created once on application setup and not impactful later).
+/// * Test data generation (session numbers and identifier strings) as it is SDK-neutral.
+///
+/// We have a separate benchmark to compare the setup stage (just re-runs the setup logic in measured phase).
+///
+/// We also do not benchmark "observable" metrics that are only polled at time of collection.
+/// Both SDKs support it as an optimization (though OpenTelemetry forces it for counters) but let's try keep the logic here simple and exclude it for now.
+///
+[MemoryDiagnoser]
+public class SdkComparisonBenchmarks
+{
+ // Unique sets of label/tag values per metric. You can think of each one as a "session" we are reporting data for.
+ private const int TimeseriesPerMetric = 100;
+
+ private static readonly string[] LabelNames = new[] { "environment", "server", "session_id" };
+ private const string Label1Value = "production";
+ private const string Label2Value = "hkhk298599-qps010-n200";
+
+ // How many observations we take during a single benchmark invocation (for each timeseries).
+ private const int ObservationCount = 1000;
+
+ private static readonly string[] SessionIds = new string[TimeseriesPerMetric];
+
+ static SdkComparisonBenchmarks()
+ {
+ for (var i = 0; i < SessionIds.Length; i++)
+ SessionIds[i] = Guid.NewGuid().ToString();
+ }
+
+ ///
+ /// Contains all the context that gets initialized at iteration setup time.
+ ///
+ /// This data set is:
+ /// 1) Not included in the performance measurements.
+ /// 2) Reused for each invocation that is part of the same iteration.
+ ///
+ private abstract class MetricsContext : IDisposable
+ {
+ ///
+ /// Records an observation with all the counter-type metrics for each session.
+ ///
+ public abstract void ObserveCounter(double value);
+
+ ///
+ /// Records an observation with all the histogram-type metrics for each session.
+ ///
+ public abstract void ObserveHistogram(double value);
+
+ ///
+ /// Records an observation with one random label value as ad-hoc using a Histogram.
+ ///
+ public abstract void ObserveHistogramWithAnAdHocLabelValue(double value);
+
+ public virtual void Dispose() { }
+ }
+
+ private sealed class PrometheusNetMetricsContext : MetricsContext
+ {
+ private readonly List _counterInstances = new(TimeseriesPerMetric);
+ private readonly List _histogramInstances = new(TimeseriesPerMetric);
+ private readonly Histogram _histogramForAdHocLabels;
+
+ private readonly IMetricServer _server;
+
+ public PrometheusNetMetricsContext()
+ {
+ var registry = Metrics.NewCustomRegistry();
+ var factory = Metrics.WithCustomRegistry(registry);
+
+ // Do not emit any exemplars in this benchmark, as they are not yet equally supported by the SDKs.
+ factory.ExemplarBehavior = ExemplarBehavior.NoExemplars();
+
+ var counter = factory.CreateCounter("counter", "", LabelNames);
+
+ for (var i = 0; i < TimeseriesPerMetric; i++)
+ _counterInstances.Add(counter.WithLabels(Label1Value, Label2Value, SessionIds[i]));
+
+ var histogram = factory.CreateHistogram("histogram", "", LabelNames);
+
+ _histogramForAdHocLabels = factory.CreateHistogram("histogramForAdHocLabels", "", LabelNames);
+
+ for (var i = 0; i < TimeseriesPerMetric; i++)
+ _histogramInstances.Add(histogram.WithLabels(Label1Value, Label2Value, SessionIds[i]));
+
+ // `AddPrometheusHttpListener` of OpenTelemetry creates an HttpListener.
+ // Start an equivalent listener for Prometheus to ensure a fair comparison.
+ // We listen on 127.0.0.1 to avoid firewall prompts (randomly chosen port - we do not expect to receive any traffic).
+ _server = new MetricServer("127.0.0.1", port: 8051);
+ _server.Start();
+ }
+
+ public override void ObserveCounter(double value)
+ {
+ foreach (var counter in _counterInstances)
+ counter.Inc(value);
+ }
+
+ public override void ObserveHistogram(double value)
+ {
+ foreach (var histogram in _histogramInstances)
+ histogram.Observe(value);
+ }
+
+ public override void ObserveHistogramWithAnAdHocLabelValue(double value)
+ {
+ _histogramForAdHocLabels.WithLabels(Label1Value, Label2Value, Guid.NewGuid().ToString()).Observe(value);
+ }
+
+ public override void Dispose()
+ {
+ base.Dispose();
+
+ _server.Dispose();
+ }
+ }
+
+ private sealed class OpenTelemetryMetricsContext : MetricsContext
+ {
+ private const string MeterBaseName = "benchmark";
+
+ private readonly Meter _meter;
+ private readonly MeterProvider _provider;
+
+ private readonly Counter _counter;
+ private readonly Histogram _histogram;
+ private readonly Histogram _histogramForAdHocLabels;
+
+ public OpenTelemetryMetricsContext()
+ {
+ // We use a randomized name every time because otherwise there appears to be some "shared state" between benchmark invocations,
+ // at least for the "setup" part which keeps getting slower every time we call it with the same metric name.
+ _meter = new Meter(MeterBaseName + Guid.NewGuid());
+
+ _counter = _meter.CreateCounter("counter");
+
+ _histogram = _meter.CreateHistogram("histogram");
+
+ _histogramForAdHocLabels = _meter.CreateHistogram("histogramForAdHocLabels");
+
+ _provider = OpenTelemetry.Sdk.CreateMeterProviderBuilder()
+ .AddView("histogram", new OpenTelemetry.Metrics.HistogramConfiguration() { RecordMinMax = false })
+ .AddMeter(_meter.Name)
+ .AddPrometheusHttpListener()
+ .Build();
+ }
+
+ public override void ObserveCounter(double value)
+ {
+ for (int i = 0; i < SessionIds.Length; i++)
+ {
+ var tag1 = new KeyValuePair(LabelNames[0], Label1Value);
+ var tag2 = new KeyValuePair(LabelNames[1], Label2Value);
+ var tag3 = new KeyValuePair(LabelNames[2], SessionIds[i]);
+ _counter.Add(value, tag1, tag2, tag3);
+ }
+ }
+
+ public override void ObserveHistogram(double value)
+ {
+ for (int i = 0; i < SessionIds.Length; i++)
+ {
+ var tag1 = new KeyValuePair(LabelNames[0], Label1Value);
+ var tag2 = new KeyValuePair(LabelNames[1], Label2Value);
+ var tag3 = new KeyValuePair(LabelNames[2], SessionIds[i]);
+ _histogram.Record(value, tag1, tag2, tag3);
+ }
+ }
+
+ public override void ObserveHistogramWithAnAdHocLabelValue(double value)
+ {
+ var tag1 = new KeyValuePair(LabelNames[0], Label1Value);
+ var tag2 = new KeyValuePair(LabelNames[1], Label2Value);
+ var tag3 = new KeyValuePair(LabelNames[2], Guid.NewGuid().ToString());
+ _histogramForAdHocLabels.Record(value, tag1, tag2, tag3);
+ }
+
+ public override void Dispose()
+ {
+ base.Dispose();
+
+ _provider.Dispose();
+ }
+ }
+
+ private MetricsContext _context;
+
+ [GlobalSetup(Targets = new string[] { nameof(OTelCounter), nameof(OTelHistogram), nameof(OTelHistogramForAdHocLabel) })]
+ public void OpenTelemetrySetup()
+ {
+ _context = new OpenTelemetryMetricsContext();
+ }
+
+ [GlobalSetup(Targets = new string[] { nameof(PromNetCounter), nameof(PromNetHistogram), nameof(PromNetHistogramForAdHocLabel) })]
+ public void PrometheusNetSetup()
+ {
+ _context = new PrometheusNetMetricsContext();
+ }
+
+ [Benchmark]
+ public void PromNetCounter()
+ {
+ for (var observation = 0; observation < ObservationCount; observation++)
+ _context.ObserveCounter(observation);
+ }
+
+ [Benchmark]
+ public void PromNetHistogram()
+ {
+ for (var observation = 0; observation < ObservationCount; observation++)
+ _context.ObserveHistogram(observation);
+ }
+
+ [Benchmark]
+ [MaxIterationCount(16)] // Need to set a lower iteration count as this benchmarks allocates a lot memory and takes too long to complete with the default number of iterations.
+ public void PromNetHistogramForAdHocLabel()
+ {
+ for (var observation = 0; observation < ObservationCount; observation++)
+ _context.ObserveHistogramWithAnAdHocLabelValue(observation);
+ }
+
+ [Benchmark]
+ public void OTelCounter()
+ {
+ for (var observation = 0; observation < ObservationCount; observation++)
+ _context.ObserveCounter(observation);
+ }
+
+ [Benchmark]
+ public void OTelHistogram()
+ {
+ for (var observation = 0; observation < ObservationCount; observation++)
+ _context.ObserveHistogram(observation);
+ }
+
+ [Benchmark]
+ [MaxIterationCount(16)] // Set the same number of iteration count as the corresponding PromNet benchmark.
+ public void OTelHistogramForAdHocLabel()
+ {
+ for (var observation = 0; observation < ObservationCount; observation++)
+ _context.ObserveHistogramWithAnAdHocLabelValue(observation);
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _context.Dispose();
+ }
+}
diff --git a/Benchmark.NetCore/SerializationBenchmarks.cs b/Benchmark.NetCore/SerializationBenchmarks.cs
index 288c2b6b..96bf2542 100644
--- a/Benchmark.NetCore/SerializationBenchmarks.cs
+++ b/Benchmark.NetCore/SerializationBenchmarks.cs
@@ -1,84 +1,205 @@
-using BenchmarkDotNet.Attributes;
+using System.IO.Pipes;
+using BenchmarkDotNet.Attributes;
using Prometheus;
-using System.IO;
-using System.Threading.Tasks;
-namespace Benchmark.NetCore
+namespace Benchmark.NetCore;
+
+[MemoryDiagnoser]
+//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.CpuSampling)]
+public class SerializationBenchmarks
{
- [MemoryDiagnoser]
- public class SerializationBenchmarks
+ public enum OutputStreamType
{
- // Metric -> Variant -> Label values
- private static readonly string[][][] _labelValueRows;
+ ///
+ /// A null stream that just does nothing and immediately returns.
+ /// Low overhead but also unrealistic in terms of asynchronous I/O behavior.
+ ///
+ Null,
+
+ ///
+ /// A stream that does nothing except yielding the task/thread to take up nonzero time.
+ /// Tries to increase the overhead from async/await task management that might occur.
+ ///
+ Yield,
+
+ ///
+ /// A named pipe connection. Something halfway between null and a real network connection.
+ /// Named pipes appear to be super slow when dealing with small amounts of data, so optimizing
+ /// this scenario is valuable to ensure that we perform well with real network connections that
+ /// may have similar limitations (depending on OS and network stack).
+ ///
+ NamedPipe
+ }
+
+ [Params(OutputStreamType.Null, OutputStreamType.NamedPipe, OutputStreamType.Yield)]
+ public OutputStreamType StreamType { get; set; }
+
+ // Metric -> Variant -> Label values
+ private static readonly string[][][] _labelValueRows;
+
+ private const int _metricCount = 100;
+ private const int _variantCount = 100;
+ private const int _labelCount = 5;
- private const int _metricCount = 100;
- private const int _variantCount = 100;
- private const int _labelCount = 5;
+ private const string _help = "arbitrary help message for metric lorem ipsum dolor golor bolem";
- private const string _help = "arbitrary help message for metric, not relevant for benchmarking";
+ static SerializationBenchmarks()
+ {
+ _labelValueRows = new string[_metricCount][][];
- static SerializationBenchmarks()
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
{
- _labelValueRows = new string[_metricCount][][];
+ var variants = new string[_variantCount][];
+ _labelValueRows[metricIndex] = variants;
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
{
- var variants = new string[_variantCount][];
- _labelValueRows[metricIndex] = variants;
-
- for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
- {
- var values = new string[_labelCount];
- _labelValueRows[metricIndex][variantIndex] = values;
+ var values = new string[_labelCount];
+ _labelValueRows[metricIndex][variantIndex] = values;
- for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++)
- values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}";
- }
+ for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++)
+ values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}";
}
}
+ }
- private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry();
- private readonly Counter[] _counters;
- private readonly Gauge[] _gauges;
- private readonly Summary[] _summaries;
- private readonly Histogram[] _histograms;
+ private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry();
+ private readonly Counter[] _counters;
+ private readonly Gauge[] _gauges;
+ private readonly Summary[] _summaries;
+ private readonly Histogram[] _histograms;
- public SerializationBenchmarks()
- {
- _counters = new Counter[_metricCount];
- _gauges = new Gauge[_metricCount];
- _summaries = new Summary[_metricCount];
- _histograms = new Histogram[_metricCount];
+ public SerializationBenchmarks()
+ {
+ _counters = new Counter[_metricCount];
+ _gauges = new Gauge[_metricCount];
+ _summaries = new Summary[_metricCount];
+ _histograms = new Histogram[_metricCount];
- var factory = Metrics.WithCustomRegistry(_registry);
+ var factory = Metrics.WithCustomRegistry(_registry);
- // Just use 1st variant for the keys (all we care about are that there is some name-like value in there).
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ // Just use 1st variant for the keys (all we care about are that there is some name-like value in there).
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ {
+ _counters[metricIndex] = factory.CreateCounter($"counter{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
+ _gauges[metricIndex] = factory.CreateGauge($"gauge{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
+ _summaries[metricIndex] = factory.CreateSummary($"summary{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
+ _histograms[metricIndex] = factory.CreateHistogram($"histogram{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
+ }
+
+ // Genmerate some sample data so the metrics are not all zero-initialized.
+ var exemplarLabelPair = Exemplar.Key("traceID").WithValue("bar");
+ for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
+ for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
{
- _counters[metricIndex] = factory.CreateCounter($"counter{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
- _gauges[metricIndex] = factory.CreateGauge($"gauge{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
- _summaries[metricIndex] = factory.CreateSummary($"summary{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
- _histograms[metricIndex] = factory.CreateHistogram($"histogram{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]);
+ _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(Exemplar.From(exemplarLabelPair));
+ _gauges[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc();
+ _summaries[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex);
+ _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex, Exemplar.From(exemplarLabelPair));
}
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ if (StreamType == OutputStreamType.Null)
+ {
+ _outputStream = Stream.Null;
}
+ else if (StreamType == OutputStreamType.Yield)
+ {
+ _outputStream = YieldStream.Default;
+ }
+ else if (StreamType == OutputStreamType.NamedPipe)
+ {
+ var pipeName = StartStreamReader();
+
+ var pipeStream = new NamedPipeClientStream(".", pipeName.ToString(), PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
+ pipeStream.Connect(TimeSpan.FromSeconds(1));
+
+ _outputStream = pipeStream;
+ }
+ else
+ throw new NotSupportedException();
+ }
- [GlobalSetup]
- public void GenerateData()
+ // Will be reused by all the iterations - the serializer does not take ownership nor close the stream.
+ private Stream _outputStream;
+
+ // When this is cancelled, the output stream reader will stop listening for new connections and reading data from existing ones.
+ private readonly CancellationTokenSource _outputStreamReaderCts = new();
+
+ // We just read data into it, we do not care about the contents.
+ // While we do not expect concurrent access, it is fine if it does happen because this data is never consumed
+ private static readonly byte[] _readBuffer = new byte[1024];
+
+ ///
+ /// Starts listening on a random port on the loopback interface and returns the name of the created pipe stream.
+ ///
+ private Guid StartStreamReader()
+ {
+ var name = Guid.NewGuid();
+ var server = new NamedPipeServerStream(name.ToString(), PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
+ var cancel = _outputStreamReaderCts.Token;
+
+ _ = Task.Run(async delegate
{
- for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++)
- for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++)
+ try
+ {
+ while (!cancel.IsCancellationRequested)
{
- _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc();
- _gauges[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc();
- _summaries[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex);
- _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex);
+ await server.WaitForConnectionAsync(cancel);
+ Console.WriteLine("Received a connection.");
+
+ try
+ {
+ while (!cancel.IsCancellationRequested)
+ {
+ var bytesRead = await server.ReadAsync(_readBuffer, cancel);
+
+ if (bytesRead == 0)
+ break;
+ }
+ }
+ finally
+ {
+ server.Disconnect();
+ }
}
- }
+ }
+ catch (OperationCanceledException) when (cancel.IsCancellationRequested)
+ {
+ // Expected
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Unexpected exception in output stream reader: {ex}");
+ }
+ finally
+ {
+ server.Dispose();
+ }
+ });
- [Benchmark]
- public async Task CollectAndSerialize()
- {
- await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default);
- }
+ return name;
+ }
+
+ [Benchmark]
+ public async Task CollectAndSerialize()
+ {
+ await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream), default);
+ }
+
+ [Benchmark]
+ public async Task CollectAndSerializeOpenMetrics()
+ {
+ await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream, ExpositionFormat.OpenMetricsText), default);
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ _outputStreamReaderCts.Cancel();
+ _outputStreamReaderCts.Dispose();
}
}
diff --git a/Benchmark.NetCore/StringSequenceBenchmarks.cs b/Benchmark.NetCore/StringSequenceBenchmarks.cs
new file mode 100644
index 00000000..08e61c02
--- /dev/null
+++ b/Benchmark.NetCore/StringSequenceBenchmarks.cs
@@ -0,0 +1,76 @@
+using BenchmarkDotNet.Attributes;
+using Prometheus;
+
+namespace Benchmark.NetCore;
+
+[MemoryDiagnoser]
+public class StringSequenceBenchmarks
+{
+ private static readonly string[] Values3Array = ["aaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb", "cccccccccccccc"];
+ private static readonly ReadOnlyMemory Values3Memory = new(Values3Array);
+
+ private static readonly string[] Values3ArrayPart1 = ["aaaaaaaaaaaaaaaaa"];
+ private static readonly string[] Values3ArrayPart2 = ["bbbbbbbbbbbbbb"];
+ private static readonly string[] Values3ArrayPart3 = ["cccccccccccccc"];
+
+ [Benchmark]
+ public void Create_From3Array()
+ {
+ StringSequence.From(Values3Array);
+ }
+
+ [Benchmark]
+ public void Create_From3Memory()
+ {
+ StringSequence.From(Values3Memory);
+ }
+
+ [Benchmark]
+ public void Create_From3ArrayConcat()
+ {
+ var part1 = StringSequence.From(Values3ArrayPart1);
+ var part2 = StringSequence.From(Values3ArrayPart2);
+ var part3 = StringSequence.From(Values3ArrayPart3);
+
+ part1.Concat(part2).Concat(part3);
+ }
+
+ private static readonly StringSequence FromValues3 = StringSequence.From(Values3Array);
+ private static readonly StringSequence Other = StringSequence.From(new[] { "fooooooooooooooo", "baaaaaaaaaaaaar", "baaaaaaaaaaz" });
+
+ [Benchmark]
+ public void Contains_Positive()
+ {
+ FromValues3.Contains(Values3Array[2]);
+ }
+
+ [Benchmark]
+ public void Contains_Negative()
+ {
+ FromValues3.Contains("a string that is not in there");
+ }
+
+ [Benchmark]
+ public void Equals_Positive()
+ {
+ FromValues3.Equals(FromValues3);
+ }
+
+ [Benchmark]
+ public void Equals_Negative()
+ {
+ FromValues3.Equals(Other);
+ }
+
+ [Benchmark]
+ public void Concat_Empty()
+ {
+ FromValues3.Concat(StringSequence.Empty);
+ }
+
+ [Benchmark]
+ public void Concat_ToEmpty()
+ {
+ StringSequence.Empty.Concat(FromValues3);
+ }
+}
diff --git a/Benchmark.NetCore/SummaryBenchmarks.cs b/Benchmark.NetCore/SummaryBenchmarks.cs
index 1b64983c..1a42a91a 100644
--- a/Benchmark.NetCore/SummaryBenchmarks.cs
+++ b/Benchmark.NetCore/SummaryBenchmarks.cs
@@ -1,80 +1,76 @@
using BenchmarkDotNet.Attributes;
using Prometheus;
-using System;
-using System.IO;
-using System.Threading.Tasks;
-namespace Benchmark.NetCore
+namespace Benchmark.NetCore;
+
+///
+/// Summary can be quite expensive to use due to its quantile measurement logic.
+/// This benchmark helps get a grip on the facts.
+///
+[MemoryDiagnoser]
+public class SummaryBenchmarks
{
- ///
- /// Summary can be quite expensive to use due to its quantile measurement logic.
- /// This benchmark helps get a grip on the facts.
- ///
- [MemoryDiagnoser]
- public class SummaryBenchmarks
+ // Arbitrary but reasonable objectices we might use for a summary.
+ private static readonly QuantileEpsilonPair[] Objectives = new[]
{
- // Arbitrary but reasonable objectices we might use for a summary.
- private static readonly QuantileEpsilonPair[] Objectives = new[]
- {
- new QuantileEpsilonPair(0.5, 0.05),
- new QuantileEpsilonPair(0.9, 0.01),
- new QuantileEpsilonPair(0.95, 0.01),
- new QuantileEpsilonPair(0.99, 0.005)
- };
+ new QuantileEpsilonPair(0.5, 0.05),
+ new QuantileEpsilonPair(0.9, 0.01),
+ new QuantileEpsilonPair(0.95, 0.01),
+ new QuantileEpsilonPair(0.99, 0.005)
+ };
- // We pre-generate some random data that we feed into the benchmark, to avoid measuring data generation.
- private static readonly double[] Values = new double[1 * 1024 * 1024];
+ // We pre-generate some random data that we feed into the benchmark, to avoid measuring data generation.
+ private static readonly double[] Values = new double[1 * 1024 * 1024];
- private static readonly TimeSpan ExportInterval = TimeSpan.FromMinutes(1);
+ private static readonly TimeSpan ExportInterval = TimeSpan.FromMinutes(1);
- static SummaryBenchmarks()
- {
- var rnd = new Random();
+ static SummaryBenchmarks()
+ {
+ var rnd = new Random();
- for (var i = 0; i < Values.Length; i++)
- Values[i] = rnd.NextDouble();
- }
+ for (var i = 0; i < Values.Length; i++)
+ Values[i] = rnd.NextDouble();
+ }
- private CollectorRegistry _registry;
- private MetricFactory _factory;
+ private CollectorRegistry _registry;
+ private MetricFactory _factory;
- [IterationSetup]
- public void Setup()
- {
- _registry = Metrics.NewCustomRegistry();
- _factory = Metrics.WithCustomRegistry(_registry);
- }
+ [IterationSetup]
+ public void Setup()
+ {
+ _registry = Metrics.NewCustomRegistry();
+ _factory = Metrics.WithCustomRegistry(_registry);
+ }
- [Params(1, 10, 100, 1000, 10000)]
- public int MeasurementsPerSecond { get; set; }
+ [Params(1, 10, 100, 1000, 10000)]
+ public int MeasurementsPerSecond { get; set; }
- [Benchmark]
- public async Task Summary_NPerSecond_For10Minutes()
+ [Benchmark]
+ public async Task Summary_NPerSecond_For10Minutes()
+ {
+ var summary = _factory.CreateSummary("metric_name", "help_string", new SummaryConfiguration
{
- var summary = _factory.CreateSummary("metric_name", "help_string", new SummaryConfiguration
- {
- Objectives = Objectives
- });
+ Objectives = Objectives
+ });
- var now = DateTime.UtcNow;
+ var now = DateTime.UtcNow;
- // We start far enough back to cover the entire age range of the summary.
- var t = now - Summary.DefMaxAge;
- var lastExport = t;
+ // We start far enough back to cover the entire age range of the summary.
+ var t = now - Summary.DefMaxAge;
+ var lastExport = t;
- while (t < now)
- {
- for (var i = 0; i < MeasurementsPerSecond; i++)
- summary.Observe(Values[i % Values.Length]);
+ while (t < now)
+ {
+ for (var i = 0; i < MeasurementsPerSecond; i++)
+ summary.Observe(Values[i % Values.Length]);
- t += TimeSpan.FromSeconds(1);
+ t += TimeSpan.FromSeconds(1);
- if (lastExport + ExportInterval <= t)
- {
- lastExport = t;
+ if (lastExport + ExportInterval <= t)
+ {
+ lastExport = t;
- await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default);
- }
+ await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), true, default);
}
}
}
diff --git a/Benchmark.NetCore/YieldStream.cs b/Benchmark.NetCore/YieldStream.cs
new file mode 100644
index 00000000..7670c59e
--- /dev/null
+++ b/Benchmark.NetCore/YieldStream.cs
@@ -0,0 +1,119 @@
+using System.Runtime.CompilerServices;
+
+namespace Benchmark.NetCore;
+
+///
+/// A stream that does nothing except yielding the task/thread to take up nonzero time. Modeled after NullStream.
+///
+internal sealed class YieldStream : Stream
+{
+ public static readonly YieldStream Default = new();
+
+ private YieldStream() { }
+
+ public override bool CanRead => true;
+ public override bool CanWrite => true;
+ public override bool CanSeek => true;
+ public override long Length => 0;
+ public override long Position { get => 0; set { } }
+
+ public override void CopyTo(Stream destination, int bufferSize)
+ {
+ Thread.Yield();
+ }
+
+ public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Yield();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ // Do nothing - we don't want this stream to be closable.
+ }
+
+ public override void Flush()
+ {
+ Thread.Yield();
+ }
+
+ public override async Task FlushAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Yield();
+ }
+
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ Thread.Yield();
+ return 0;
+ }
+
+ public override int Read(Span buffer)
+ {
+ Thread.Yield();
+ return 0;
+ }
+
+ public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Yield();
+ return 0;
+ }
+
+ [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
+ public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Yield();
+ return 0;
+ }
+
+ public override int ReadByte()
+ {
+ Thread.Yield();
+ return -1;
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ Thread.Yield();
+ }
+
+ public override void Write(ReadOnlySpan buffer)
+ {
+ Thread.Yield();
+ }
+
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Yield();
+ }
+
+ [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
+ public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await Task.Yield();
+ }
+
+ public override void WriteByte(byte value)
+ {
+ Thread.Yield();
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ Thread.Yield();
+ return 0;
+ }
+
+ public override void SetLength(long length)
+ {
+ Thread.Yield();
+ }
+}
diff --git a/Docs/MeasurementsBenchmarks.xlsx b/Docs/MeasurementsBenchmarks.xlsx
new file mode 100644
index 00000000..f2b29726
Binary files /dev/null and b/Docs/MeasurementsBenchmarks.xlsx differ
diff --git a/Exemplars.png b/Exemplars.png
new file mode 100644
index 00000000..d199d655
Binary files /dev/null and b/Exemplars.png differ
diff --git a/History b/History
index 42ed855e..2c7d57cb 100644
--- a/History
+++ b/History
@@ -1,3 +1,57 @@
+* 8.2.1
+- Fix occasional "Collection was modified" exception when serializing metrics. #464
+* 8.2.0
+- .WithLabels() & similar now accept ReadOnlyMemory as alternative to string[]. Same behavior, just easier to use if you already have a ReadOnlyMemory.
+- .WithLabels() & similar now accept ReadOnlySpan as alternative to string[]. This enables allocation-free metric instance creation if a metric instance with these labels is already known.
+- Incorporated various optimizations to reduce the required CPU time and allocated memory, including #410, #443 and other contributions.
+- Observation of large histograms is now 10-30% faster on machines that support AVX2 instructions.
+- health checks exposed via ForwardToPrometheus() no longer create a default metric if a custom metric is provided #444
+* 8.1.1
+- Fix bug in .NET Meters API adapter for UpDownCounter, which was incorrectly transformed to Prometheus metrics. #452 and #453
+* 8.1.0
+- Add support for capturing HttpClient metrics from all registered HttpClients (`services.UseHttpClientMetrics()`).
+* 8.0.1
+- Allow ObservableCounter to be reset. Previously, the MeterAdapter output got stuck on its previous maximum if the underlying Meter reset its value to a lower value. Now we transform the value 1:1.
+* 8.0.0
+- Added OpenMetrics exposition format support (#388).
+- Added exemplar support for Counter and Histogram (#388).
+- The ':' character is no longer allowed in metric or label names. For metric names, Prometheus standard practice is to use colon only in recording rules.
+- Publish symbol packages and add Source Link support for easier debugging experience.
+- Fix defect where metrics with different labels could overwrite each other in specific circumstances with multiple metric factories in use (#389).
+- Ensure even harder that MetricPusher flushes the final state before stopping (#383 and #384)
+- Simplify DotNetStats built-in collector code for ease of readability and more best practices (#365, #364)
+- Slightly improve Counter performance under concurrent load.
+- Reduce memory allocations performed during ASP.NET Core HTTP request tracking.
+- By default, EventCounterAdapter will only listen to a small predefined set of general-purpose useful event sources, to minimize resource consumption in the default configuration. A custom event source filter must now be provided to enable listening for additional event sources.
+- EventCounterAdapter will only refresh data every 10 seconds by default, to reduce amount of garbage generated in memory (.NET event counters are very noisy and create many temporary objects).
+- Added `IManagedLifetimeMetricFactory.WithLabels()` to enable simpler label enrichment in scenarios where lifetime-managed metric instances are used.
+* 7.1.0
+- Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier).
+- Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature).
+* 7.0.0
+- .NET Core specific functionality now targeting .NET 6.0 or greater (all previous versions will be end of life by December 2022).
+- Relaxed the API restriction that forbade you to create multiple metrics with the same name but different label names. While this is a Prometheus anti-pattern, it is a standard pattern in other metrics technologies and we will allow it in the name of interoperability.
+- Added IMetricFactory.WithLabels() to define static labels per-factory and reduce the effort needed by users when defining static labels in specific scopes.
+- Added automatic metric expiration feature, to remove timeseries when they are no longer used after a while. This improves interoperability with other metrics technologies such as .NET Meters.
+- (Breaking change) Rewrite of the MeterAdapter functionality to bridge .NET 6 metrics to Prometheus. API remains equivalent, though options class changed. Output format (what metrics are published) is completely different. Now it translates metric names (instead of putting them in labels) and preserves tags (labels) provided via the .NET Meters API. Added sample project to demonstrate functionality. Ref #359
+- (Breaking change) Dropped support for .NET Standard 2.0 - now will target specifically .NET 6.0 and .NET Framework 4.6.2. This was already documented before but there was "unofficial" support also for .NET Standard remaining. This is now removed.
+- (Breaking change) EventCounterAdapter output format changed to translate counters to Prometheus metrics and to improve usability by publishing both counters and gauges from ambiguous event counters.
+- EventCounterAdapter and MeterAdapter are now enabled on startup by default.
+- Added some debug metrics to indicate the number of metric families, instances and timeseries being exported by prometheus-net.
+- Added typical benchmark results to readme for easy reference, and to show relative speed of the simple versus complex instruments.
+- Removed MetricConfiguration.StaticLabels because it had negative performance impact on many code paths that did not use it. The same functionality can be manually implemented in user code. Static labels remain available on registry and factory level.
+- When a before-collection metrics update callback throws an exception, it is now logged and ignored instead of breaking all metrics collection.
+- EventCounterAdapter now defaults to Informational events only, for reduced performance overhead. Can be configured via callback.
+* 6.0.0
+- Dropped library support for unsupported .NET runtime versions (.NET 2.1).
+- If Razor Pages is enabled, the "page" label is now automatically added to the default HTTP server metrics, unless there is a user-defined label already present with this name.
+- The "endpoint" label is now automatically added to the default HTTP server metrics, containing this endpoint routing route pattern of the request, unless there is a user-defined label already present with this name.
+- Added integration with .NET EventCounters, which can now be easily exposed as Promtheus metrics (see EventCounterAdapter class).
+- Added integration with .NET Meters, which can now be easily exposed as Promtheus metrics (see MeterAdapter class).
+- Added ReplaceOnPush option to MetricPusher #305 #322.
+- Minimum .NET Framework version is now 4.6.2.
+* 5.1.0
+- Add support for adding custom label value providers to HTTP server metrics. #347
* 5.0.2
- Bugfix: HttpClientMetrics under concurrent load with long-running requests were leading to broken response streams. #336
* 5.0.1
diff --git a/LocalMetricsCollector/Dockerfile b/LocalMetricsCollector/Dockerfile
new file mode 100644
index 00000000..61ff408b
--- /dev/null
+++ b/LocalMetricsCollector/Dockerfile
@@ -0,0 +1,37 @@
+FROM mcr.microsoft.com/cbl-mariner/base/core:2.0
+
+# gettext is used by run.sh to do env var replacement
+# ca-certificates-microsoft lets us access github.com for download
+RUN tdnf repolist --refresh \
+ && tdnf install -y gettext ca-certificates-microsoft
+
+# Install dependencies
+RUN tdnf install -y \
+ wget \
+ tar \
+ gzip
+
+################### Based on https://github.com/prometheus/prometheus/blob/main/Dockerfile
+
+ARG PROMETHEUS_PACKAGE_NAME=prometheus-2.41.0.linux-amd64
+RUN wget --no-verbose -O prometheus.tar.gz https://github.com/prometheus/prometheus/releases/download/v2.41.0/$PROMETHEUS_PACKAGE_NAME.tar.gz
+RUN tar xvfz prometheus.tar.gz
+RUN cp /$PROMETHEUS_PACKAGE_NAME/prometheus /bin/
+RUN cp /$PROMETHEUS_PACKAGE_NAME/promtool /bin/
+RUN mkdir /usr/share/prometheus
+RUN cp -r /$PROMETHEUS_PACKAGE_NAME/console_libraries/ /usr/share/prometheus/console_libraries/
+RUN cp -r /$PROMETHEUS_PACKAGE_NAME/consoles/ /usr/share/prometheus/consoles/
+
+RUN mkdir -p /prometheus && mkdir -p /etc/prometheus
+
+################### Custom logic start
+
+WORKDIR /app
+
+COPY run.sh .
+RUN chmod +x /app/run.sh
+
+# This is the config template. run.sh will inject env variables into it.
+COPY prometheus.yml .
+
+ENTRYPOINT ["/app/run.sh"]
\ No newline at end of file
diff --git a/LocalMetricsCollector/prometheus.yml b/LocalMetricsCollector/prometheus.yml
new file mode 100644
index 00000000..885d1252
--- /dev/null
+++ b/LocalMetricsCollector/prometheus.yml
@@ -0,0 +1,28 @@
+global:
+ scrape_interval: 10s
+ evaluation_interval: 10s
+ scrape_timeout: 3s
+
+# Local Prometheus collector for testing and dashboard development purposes.
+
+remote_write:
+- url: https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push
+ send_exemplars: true
+ basic_auth:
+ username: $GRAFANA_USER
+ password: $GRAFANA_API_KEY
+
+scrape_configs:
+ - job_name: prometheus-net
+ scheme: http
+ static_configs:
+ - targets:
+ # Most of the samples use 1234
+ - $HOST_IP:1234
+ # Sample.Grpc
+ - $HOST_IP:5025
+ # Sample.Web
+ - $HOST_IP:5099
+ # Sample.Web.MetricExpiration
+ - $HOST_IP:5283
+ # Not supported: Sample.Web.NetFramework because IIS Express is localhost-only.
\ No newline at end of file
diff --git a/LocalMetricsCollector/run.sh b/LocalMetricsCollector/run.sh
new file mode 100644
index 00000000..ab0c4e07
--- /dev/null
+++ b/LocalMetricsCollector/run.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+set -e
+
+# Replace any env variables in the template, yielding the final configuration file.
+envsubst < /app/prometheus.yml > /etc/prometheus/prometheus.yml
+
+# We must listen on 0.0.0.0 here because otherwise the liveness/readiness probes cannot reach us.
+exec /bin/prometheus --web.listen-address=0.0.0.0:$PROMETHEUS_PORT --storage.tsdb.retention.time=5m --storage.tsdb.min-block-duration=2m --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --web.console.libraries=/usr/share/prometheus/console_libraries --web.console.templates=/usr/share/prometheus/consoles --enable-feature=exemplar-storage
\ No newline at end of file
diff --git a/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs b/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs
index 895066a1..e9cbcfea 100644
--- a/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs
+++ b/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs
@@ -1,36 +1,34 @@
-using System;
-using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Builder;
-namespace Prometheus
+namespace Prometheus;
+
+public static class GrpcMetricsMiddlewareExtensions
{
- public static class GrpcMetricsMiddlewareExtensions
+ ///
+ /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests.
+ ///
+ public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app,
+ Action configure)
{
- ///
- /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests.
- ///
- public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app,
- Action configure)
- {
- var options = new GrpcMiddlewareExporterOptions();
- configure?.Invoke(options);
- app.UseGrpcMetrics(options);
- return app;
- }
-
- ///
- /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests.
- ///
- public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app,
- GrpcMiddlewareExporterOptions? options = null)
- {
- options ??= new GrpcMiddlewareExporterOptions();
+ var options = new GrpcMiddlewareExporterOptions();
+ configure?.Invoke(options);
+ app.UseGrpcMetrics(options);
+ return app;
+ }
- if (options.RequestCount.Enabled)
- {
- app.UseMiddleware(options.RequestCount);
- }
+ ///
+ /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests.
+ ///
+ public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app,
+ GrpcMiddlewareExporterOptions? options = null)
+ {
+ options ??= new GrpcMiddlewareExporterOptions();
- return app;
+ if (options.RequestCount.Enabled)
+ {
+ app.UseMiddleware(options.RequestCount);
}
+
+ return app;
}
}
diff --git a/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs b/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs
index 63f6056f..034478ea 100644
--- a/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs
+++ b/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs
@@ -1,13 +1,12 @@
-namespace Prometheus
+namespace Prometheus;
+
+public abstract class GrpcMetricsOptionsBase
{
- public abstract class GrpcMetricsOptionsBase
- {
- public bool Enabled { get; set; } = true;
+ public bool Enabled { get; set; } = true;
- ///
- /// Allows you to override the registry used to create the default metric instance.
- /// Value is ignored if you specify a custom metric instance in the options.
- ///
- public CollectorRegistry? Registry { get; set; }
- }
+ ///
+ /// Allows you to override the registry used to create the default metric instance.
+ /// Value is ignored if you specify a custom metric instance in the options.
+ ///
+ public CollectorRegistry? Registry { get; set; }
}
diff --git a/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs b/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs
index f7d65578..8254fc19 100644
--- a/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs
+++ b/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs
@@ -1,7 +1,6 @@
-namespace Prometheus
+namespace Prometheus;
+
+public sealed class GrpcMiddlewareExporterOptions
{
- public sealed class GrpcMiddlewareExporterOptions
- {
- public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions();
- }
+ public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions();
}
diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs
index 11211fa0..9eba9ec1 100644
--- a/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs
+++ b/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs
@@ -1,37 +1,31 @@
using Microsoft.AspNetCore.Http;
-using System;
-using System.Threading.Tasks;
-namespace Prometheus
+namespace Prometheus;
+
+///
+/// Counts the number of requests to gRPC services.
+///
+internal sealed class GrpcRequestCountMiddleware : GrpcRequestMiddlewareBase, ICounter>
{
- ///
- /// Counts the number of requests to gRPC services.
- ///
- internal sealed class GrpcRequestCountMiddleware : GrpcRequestMiddlewareBase, ICounter>
- {
- private readonly RequestDelegate _next;
+ private readonly RequestDelegate _next;
- public GrpcRequestCountMiddleware(RequestDelegate next, GrpcRequestCountOptions? options)
- : base(options, options?.Counter)
- {
- _next = next ?? throw new ArgumentNullException(nameof(next));
- }
+ public GrpcRequestCountMiddleware(RequestDelegate next, GrpcRequestCountOptions? options)
+ : base(options, options?.Counter)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ }
- public async Task Invoke(HttpContext context)
- {
- CreateChild(context)?.Inc();
+ public async Task Invoke(HttpContext context)
+ {
+ CreateChild(context)?.Inc();
- await _next(context);
- }
+ await _next(context);
+ }
- protected override string[] DefaultLabels => GrpcRequestLabelNames.All;
+ protected override string[] DefaultLabels => GrpcRequestLabelNames.All;
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter(
- "grpc_requests_received_total",
- "Number of gRPC requests received (including those currently being processed).",
- new CounterConfiguration
- {
- LabelNames = labelNames
- });
- }
+ protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter(
+ "grpc_requests_received_total",
+ "Number of gRPC requests received (including those currently being processed).",
+ labelNames);
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs
index d0fcc291..dd986ef2 100644
--- a/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs
+++ b/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs
@@ -1,10 +1,9 @@
-namespace Prometheus
+namespace Prometheus;
+
+public sealed class GrpcRequestCountOptions : GrpcMetricsOptionsBase
{
- public sealed class GrpcRequestCountOptions : GrpcMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Counter { get; set; }
- }
+ ///
+ /// Set this to use a custom metric instead of the default.
+ ///
+ public ICollector? Counter { get; set; }
}
diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs
index ac17708e..95a79886 100644
--- a/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs
+++ b/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs
@@ -1,17 +1,16 @@
-namespace Prometheus
+namespace Prometheus;
+
+///
+/// Reserved label names used in gRPC metrics.
+///
+public static class GrpcRequestLabelNames
{
- ///
- /// Reserved label names used in gRPC metrics.
- ///
- public static class GrpcRequestLabelNames
- {
- public const string Service = "service";
- public const string Method = "method";
+ public const string Service = "service";
+ public const string Method = "method";
- public static readonly string[] All =
- {
- Service,
- Method,
- };
- }
+ public static readonly string[] All =
+ {
+ Service,
+ Method,
+ };
}
diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs
index 380841da..8f6c80d2 100644
--- a/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs
+++ b/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs
@@ -1,101 +1,98 @@
-using Microsoft.AspNetCore.Http;
-using System;
-using System.Linq;
-using Grpc.AspNetCore.Server;
+using Grpc.AspNetCore.Server;
+using Microsoft.AspNetCore.Http;
-namespace Prometheus
+namespace Prometheus;
+
+// Modeled after HttpRequestMiddlewareBase, just with gRPC specific functionality.
+internal abstract class GrpcRequestMiddlewareBase
+ where TCollector : class, ICollector
+ where TChild : class, ICollectorChild
{
- // Modeled after HttpRequestMiddlewareBase, just with gRPC specific functionality.
- internal abstract class GrpcRequestMiddlewareBase
- where TCollector : class, ICollector
- where TChild : class, ICollectorChild
- {
- ///
- /// The set of labels from among the defaults that this metric supports.
- ///
- /// This set will be automatically extended with labels for additional
- /// route parameters when creating the default metric instance.
- ///
- protected abstract string[] DefaultLabels { get; }
+ ///
+ /// The set of labels from among the defaults that this metric supports.
+ ///
+ /// This set will be automatically extended with labels for additional
+ /// route parameters when creating the default metric instance.
+ ///
+ protected abstract string[] DefaultLabels { get; }
- ///
- /// Creates the default metric instance with the specified set of labels.
- /// Only used if the caller does not provide a custom metric instance in the options.
- ///
- protected abstract TCollector CreateMetricInstance(string[] labelNames);
+ ///
+ /// Creates the default metric instance with the specified set of labels.
+ /// Only used if the caller does not provide a custom metric instance in the options.
+ ///
+ protected abstract TCollector CreateMetricInstance(string[] labelNames);
- ///
- /// The factory to use for creating the default metric for this middleware.
- /// Not used if a custom metric is alreaedy provided in options.
- ///
- protected MetricFactory MetricFactory { get; }
+ ///
+ /// The factory to use for creating the default metric for this middleware.
+ /// Not used if a custom metric is alreaedy provided in options.
+ ///
+ protected MetricFactory MetricFactory { get; }
- private readonly TCollector _metric;
+ private readonly TCollector _metric;
+
+ protected GrpcRequestMiddlewareBase(GrpcMetricsOptionsBase? options, TCollector? customMetric)
+ {
+ MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry);
- protected GrpcRequestMiddlewareBase(GrpcMetricsOptionsBase? options, TCollector? customMetric)
+ if (customMetric != null)
{
- MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry);
+ _metric = customMetric;
+ ValidateNoUnexpectedLabelNames();
+ }
+ else
+ {
+ _metric = CreateMetricInstance(DefaultLabels);
+ }
+ }
- if (customMetric != null)
- {
- _metric = customMetric;
- ValidateNoUnexpectedLabelNames();
- }
- else
- {
- _metric = CreateMetricInstance(DefaultLabels);
- }
+ protected TChild? CreateChild(HttpContext context)
+ {
+ var metadata = context.GetEndpoint()?.Metadata?.GetMetadata();
+ if (metadata == null)
+ {
+ // Not a gRPC request
+ return null;
}
- protected TChild? CreateChild(HttpContext context)
+ if (!_metric.LabelNames.Any())
{
- var metadata = context.GetEndpoint()?.Metadata?.GetMetadata();
- if (metadata == null)
- {
- // Not a gRPC request
- return null;
- }
+ return _metric.Unlabelled;
+ }
- if (!_metric.LabelNames.Any())
- {
- return _metric.Unlabelled;
- }
+ return CreateChild(context, metadata);
+ }
- return CreateChild(context, metadata);
- }
+ protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata)
+ {
+ var labelValues = new string[_metric.LabelNames.Length];
- protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata)
+ for (var i = 0; i < labelValues.Length; i++)
{
- var labelValues = new string[_metric.LabelNames.Length];
-
- for (var i = 0; i < labelValues.Length; i++)
+ switch (_metric.LabelNames[i])
{
- switch (_metric.LabelNames[i])
- {
- case GrpcRequestLabelNames.Service:
- labelValues[i] = metadata.Method.ServiceName;
- break;
- case GrpcRequestLabelNames.Method:
- labelValues[i] = metadata.Method.Name;
- break;
- default:
- // Should never reach this point because we validate in ctor.
- throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}");
- }
+ case GrpcRequestLabelNames.Service:
+ labelValues[i] = metadata.Method.ServiceName;
+ break;
+ case GrpcRequestLabelNames.Method:
+ labelValues[i] = metadata.Method.Name;
+ break;
+ default:
+ // Should never reach this point because we validate in ctor.
+ throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}");
}
-
- return _metric.WithLabels(labelValues);
}
- ///
- /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters.
- ///
- private void ValidateNoUnexpectedLabelNames()
- {
- var unexpected = _metric.LabelNames.Except(DefaultLabels);
+ return _metric.WithLabels(labelValues);
+ }
- if (unexpected.Any())
- throw new ArgumentException($"Provided custom gRPC request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}.");
- }
+ ///
+ /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters.
+ ///
+ private void ValidateNoUnexpectedLabelNames()
+ {
+ var unexpected = _metric.LabelNames.Except(DefaultLabels);
+
+ if (unexpected.Any())
+ throw new ArgumentException($"Provided custom gRPC request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}.");
}
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj
index 056035a1..7d1556fa 100644
--- a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj
+++ b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj
@@ -1,38 +1,72 @@
-
- netcoreapp3.1
- Prometheus
-
- false
-
- true
-
- ..\Resources\prometheus-net.snk
-
- preview
- enable
-
- true
- true
- 1701;1702;1705;1591
-
-
-
- bin\Release\netstandard2.0\Prometheus.Grpc.xml
-
-
-
- bin\Debug\netstandard2.0\Prometheus.Grpc.xml
-
-
-
-
-
-
- 2.28.0
-
-
-
+
+ net6.0
+ Prometheus
+
+ false
+
+ true
+ ..\Resources\prometheus-net.snk
+
+ enable
+ enable
+ True
+ True
+ 1591
+
+ latest
+ 9999
+
+ True
+
+
+ true
+ true
+
+
+ prometheus-net.AspNetCore.Grpc
+ sandersaares
+ prometheus-net
+ prometheus-net
+ ASP.NET Core gRPC integration with Prometheus
+ Copyright © prometheus-net developers
+ https://github.com/prometheus-net/prometheus-net
+ prometheus-net-logo.png
+ README.md
+ metrics prometheus aspnetcore
+ MIT
+ True
+ snupkg
+
+
+
+
+ true
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+ 2.48.0
+
+
+
+
+
+
+
diff --git a/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs b/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs
index c8afbf92..187fa7aa 100644
--- a/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs
+++ b/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs
@@ -1,15 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
-namespace Prometheus
+namespace Prometheus;
+
+public static class HealthCheckBuilderExtensions
{
- public static class HealthCheckBuilderExtensions
+ public static IHealthChecksBuilder ForwardToPrometheus(this IHealthChecksBuilder builder, PrometheusHealthCheckPublisherOptions? options = null)
{
- public static IHealthChecksBuilder ForwardToPrometheus(this IHealthChecksBuilder builder, PrometheusHealthCheckPublisherOptions? options = null)
- {
- builder.Services.AddSingleton(provider => new PrometheusHealthCheckPublisher(options));
+ builder.Services.AddSingleton(provider => new PrometheusHealthCheckPublisher(options));
- return builder;
- }
+ return builder;
}
}
diff --git a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj
index 4c175992..68ade9f2 100644
--- a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj
+++ b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj
@@ -1,51 +1,74 @@
- netstandard2.0;netcoreapp3.1
+ net6.0
Prometheus
false
true
-
..\Resources\prometheus-net.snk
- preview
+ enable
enable
-
- true
-
+ True
+ True
+ 1591
+
+ latest
+ 9999
+
+ True
+
+
+ true
+ true
-
- bin\Release\netstandard2.0\Prometheus.AspNetCore.HealthChecks.xml
- true
- 1701;1702;1705;1591
+
+ prometheus-net.AspNetCore.HealthChecks
+ sandersaares
+ prometheus-net
+ prometheus-net
+ ASP.NET Core Health Checks integration with Prometheus
+ Copyright © prometheus-net developers
+ https://github.com/prometheus-net/prometheus-net
+ prometheus-net-logo.png
+ README.md
+ metrics prometheus aspnetcore
+ MIT
+ True
+ snupkg
-
- bin\Debug\netstandard2.0\Prometheus.AspNetCore.HealthChecks.xml
- true
- 1701;1702;1705;1591
+
+
+ true
-
-
-
-
-
+
+ True
+ \
+
+
+ True
+ \
+
-
-
+
+
-
+
+
-
+
+
+
diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs
index 0a8b03f4..95b08115 100644
--- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs
+++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs
@@ -1,43 +1,39 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-namespace Prometheus
+namespace Prometheus;
+
+///
+/// Publishes ASP.NET Core Health Check statuses as Prometheus metrics.
+///
+internal sealed class PrometheusHealthCheckPublisher : IHealthCheckPublisher
{
- ///
- /// Publishes ASP.NET Core Health Check statuses as Prometheus metrics.
- ///
- internal sealed class PrometheusHealthCheckPublisher : IHealthCheckPublisher
- {
- private readonly Gauge _checkStatus;
+ private readonly Gauge _checkStatus;
- public PrometheusHealthCheckPublisher(PrometheusHealthCheckPublisherOptions? options)
- {
- _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().Gauge;
- }
+ public PrometheusHealthCheckPublisher(PrometheusHealthCheckPublisherOptions? options)
+ {
+ _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().GetDefaultGauge();
+ }
- public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
- {
- foreach (var reportEntry in report.Entries)
- _checkStatus.WithLabels(reportEntry.Key).Set(HealthStatusToMetricValue(reportEntry.Value.Status));
+ public Task PublishAsync(HealthReport report, CancellationToken cancellationToken)
+ {
+ foreach (var reportEntry in report.Entries)
+ _checkStatus.WithLabels(reportEntry.Key).Set(HealthStatusToMetricValue(reportEntry.Value.Status));
- return Task.CompletedTask;
- }
+ return Task.CompletedTask;
+ }
- private static double HealthStatusToMetricValue(HealthStatus status)
+ private static double HealthStatusToMetricValue(HealthStatus status)
+ {
+ switch (status)
{
- switch (status)
- {
- case HealthStatus.Unhealthy:
- return 0;
- case HealthStatus.Degraded:
- return 0.5;
- case HealthStatus.Healthy:
- return 1;
- default:
- throw new NotSupportedException($"Unexpected HealthStatus value: {status}");
- }
+ case HealthStatus.Unhealthy:
+ return 0;
+ case HealthStatus.Degraded:
+ return 0.5;
+ case HealthStatus.Healthy:
+ return 1;
+ default:
+ throw new NotSupportedException($"Unexpected HealthStatus value: {status}");
}
}
}
diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs
index bb4cd606..df2c5400 100644
--- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs
+++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs
@@ -1,15 +1,14 @@
-namespace Prometheus
+namespace Prometheus;
+
+public sealed class PrometheusHealthCheckPublisherOptions
{
- public sealed class PrometheusHealthCheckPublisherOptions
- {
- private const string DefaultName = "aspnetcore_healthcheck_status";
- private const string DefaultHelp = "ASP.NET Core health check status (0 == Unhealthy, 0.5 == Degraded, 1 == Healthy)";
+ private const string DefaultName = "aspnetcore_healthcheck_status";
+ private const string DefaultHelp = "ASP.NET Core health check status (0 == Unhealthy, 0.5 == Degraded, 1 == Healthy)";
- public Gauge Gauge { get; set; } =
- Metrics.CreateGauge(DefaultName, DefaultHelp, new GaugeConfiguration
- {
- LabelNames = new string[] { "name" },
- SuppressInitialValue = true
- });
+ public Gauge? Gauge { get; set; }
+
+ public Gauge GetDefaultGauge()
+ {
+ return Metrics.CreateGauge(DefaultName, DefaultHelp, labelNames: new[] { "name" });
}
}
diff --git a/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs
index 6e82e87d..af2416ff 100644
--- a/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs
@@ -1,47 +1,45 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
-using System.Threading.Tasks;
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+///
+/// If routing data is available before executing the inner handler, this routing data is captured
+/// and can be used later by other middlewares that wish not to be affected by runtime changes to routing data.
+///
+///
+/// This is intended to be executed after the .UseRouting() middleware that performs ASP.NET Core 3 endpoint routing.
+///
+/// The captured route data is stored in the context via ICapturedRouteDataFeature.
+///
+internal sealed class CaptureRouteDataMiddleware
{
- ///
- /// If routing data is available before executing the inner handler, this routing data is captured
- /// and can be used later by other middlewares that wish not to be affected by runtime changes to routing data.
- ///
- ///
- /// This is intended to be executed after the .UseRouting() middleware that performs ASP.NET Core 3 endpoint routing.
- ///
- /// The captured route data is stored in the context via ICapturedRouteDataFeature.
- ///
- internal sealed class CaptureRouteDataMiddleware
- {
- private readonly RequestDelegate _next;
+ private readonly RequestDelegate _next;
- public CaptureRouteDataMiddleware(RequestDelegate next)
- {
- _next = next;
- }
+ public CaptureRouteDataMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
- public Task Invoke(HttpContext context)
- {
- TryCaptureRouteData(context);
+ public Task Invoke(HttpContext context)
+ {
+ TryCaptureRouteData(context);
- return _next(context);
- }
+ return _next(context);
+ }
- private static void TryCaptureRouteData(HttpContext context)
- {
- var routeData = context.GetRouteData();
+ private static void TryCaptureRouteData(HttpContext context)
+ {
+ var routeData = context.GetRouteData();
- if (routeData == null || routeData.Values.Count <= 0)
- return;
+ if (routeData == null || routeData.Values.Count <= 0)
+ return;
- var capturedRouteData = new CapturedRouteDataFeature();
+ var capturedRouteData = new CapturedRouteDataFeature();
- foreach (var pair in routeData.Values)
- capturedRouteData.Values.Add(pair.Key, pair.Value);
+ foreach (var pair in routeData.Values)
+ capturedRouteData.Values.Add(pair.Key, pair.Value);
- context.Features.Set(capturedRouteData);
- }
+ context.Features.Set(capturedRouteData);
}
}
diff --git a/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs b/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs
index 847ff518..f2562e2b 100644
--- a/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs
@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Routing;
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+sealed class CapturedRouteDataFeature : ICapturedRouteDataFeature
{
- sealed class CapturedRouteDataFeature : ICapturedRouteDataFeature
- {
- public RouteValueDictionary Values { get; } = new RouteValueDictionary();
- }
+ public RouteValueDictionary Values { get; } = new RouteValueDictionary();
}
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs b/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs
new file mode 100644
index 00000000..830781c9
--- /dev/null
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs
@@ -0,0 +1,22 @@
+using Microsoft.AspNetCore.Http;
+
+namespace Prometheus.HttpMetrics;
+
+public sealed class HttpCustomLabel
+{
+ ///
+ /// Name of the Prometheus label.
+ ///
+ public string LabelName { get; }
+
+ ///
+ /// A method that extracts the label value from the HttpContext of the request being handled.
+ ///
+ public Func LabelValueProvider { get; }
+
+ public HttpCustomLabel(string labelName, Func labelValueProvider)
+ {
+ LabelName = labelName;
+ LabelValueProvider = labelValueProvider;
+ }
+}
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs
index b8e07779..bff49072 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs
@@ -1,38 +1,30 @@
using Microsoft.AspNetCore.Http;
-using System;
-using System.Threading.Tasks;
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+internal sealed class HttpInProgressMiddleware : HttpRequestMiddlewareBase, IGauge>
{
- internal sealed class HttpInProgressMiddleware : HttpRequestMiddlewareBase, IGauge>
- {
- private readonly RequestDelegate _next;
+ private readonly RequestDelegate _next;
- public HttpInProgressMiddleware(RequestDelegate next, HttpInProgressOptions? options)
- : base(options, options?.Gauge)
- {
- _next = next ?? throw new ArgumentNullException(nameof(next));
- }
+ public HttpInProgressMiddleware(RequestDelegate next, HttpInProgressOptions options)
+ : base(options, options?.Gauge)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ }
- public async Task Invoke(HttpContext context)
+ public async Task Invoke(HttpContext context)
+ {
+ // CreateChild() will take care of applying the right labels, no need to think hard about it here.
+ using (CreateChild(context).TrackInProgress())
{
- // In ASP.NET Core 2, we will not have route data, so we cannot record controller/action labels.
- // In ASP.NET Core 3, we will have this data and can record the labels.
- // CreateChild() will take care of applying the right labels, no need to think hard about it here.
- using (CreateChild(context).TrackInProgress())
- {
- await _next(context);
- }
+ await _next(context);
}
+ }
- protected override string[] DefaultLabels => HttpRequestLabelNames.PotentiallyAvailableBeforeExecutingFinalHandler;
+ protected override string[] BaselineLabels => HttpRequestLabelNames.DefaultsAvailableBeforeExecutingFinalHandler;
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge(
- "http_requests_in_progress",
- "The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.",
- new GaugeConfiguration
- {
- LabelNames = labelNames
- });
- }
+ protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge(
+ "http_requests_in_progress",
+ "The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.",
+ labelNames);
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs
index 7e3d30b2..df09ea3d 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs
@@ -1,10 +1,9 @@
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+public sealed class HttpInProgressOptions : HttpMetricsOptionsBase
{
- public sealed class HttpInProgressOptions : HttpMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Gauge { get; set; }
- }
+ ///
+ /// Set this to use a custom metric instead of the default.
+ ///
+ public ICollector? Gauge { get; set; }
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs
index 3d5af486..ab4ca5d7 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs
@@ -1,25 +1,66 @@
-using System.Collections.Generic;
+namespace Prometheus.HttpMetrics;
-namespace Prometheus.HttpMetrics
+public abstract class HttpMetricsOptionsBase
{
- public abstract class HttpMetricsOptionsBase
- {
- public bool Enabled { get; set; } = true;
-
- ///
- /// Additional route parameters to include beyond the defaults (controller/action).
- /// This may be useful if you have, for example, a "version" parameter for API versioning.
- ///
- ///
- /// Metric labels are automatically added for these parameters, unless you provide your
- /// own metric instance in the options (in which case you must add the required labels).
- ///
- public List AdditionalRouteParameters { get; set; } = new List();
-
- ///
- /// Allows you to override the registry used to create the default metric instance.
- /// Value is ignored if you specify a custom metric instance in the options.
- ///
- public CollectorRegistry? Registry { get; set; }
- }
+ public bool Enabled { get; set; } = true;
+
+ ///
+ /// Transforms the label value from it's raw value (e.g. 200, 404) into a compressed
+ /// alternative (e.g. 2xx, 4xx). Setting this to true can be used to reduce the cardinality of metrics produced while still clearly communicating
+ /// success and error conditions (client vs server error). Defaults to false.
+ ///
+ public bool ReduceStatusCodeCardinality { get; set; } = false;
+
+ ///
+ /// Additional route parameters to include beyond the defaults (controller/action).
+ /// This may be useful if you have, for example, a "version" parameter for API versioning.
+ ///
+ ///
+ /// Metric labels are automatically defined for these parameters, unless you provide your
+ /// own metric instance in the options (in which case you must add the required labels).
+ ///
+ public List AdditionalRouteParameters { get; set; } = new List();
+
+ ///
+ /// Additional custom labels to add to the metrics, with values extracted from the HttpContext of incoming requests.
+ ///
+ ///
+ /// Metric labels are automatically defined for these, unless you provide your
+ /// own metric instance in the options (in which case you must add the required labels).
+ ///
+ public List CustomLabels { get; set; } = new List();
+
+ ///
+ /// Allows you to override the registry used to create the default metric instance.
+ ///
+ ///
+ /// Value is ignored if you specify a custom metric instance or metric factory in the options (instance overrides factory overrides registry).
+ ///
+ public CollectorRegistry? Registry { get; set; }
+
+ ///
+ /// Allows you to override the metric factory used to create the default metric instance.
+ ///
+ ///
+ /// Value is ignored if you specify a custom metric instance in the options (instance overrides factory overrides registry).
+ ///
+ public IMetricFactory? MetricFactory { get; set; }
+
+ ///
+ /// Enables custom logic to determine whether an exemplar should be recorded for a specific HTTP request.
+ /// This will be called after request processing has completed and the response has been filled by inner handlers.
+ ///
+ ///
+ /// By default, we always record an exemplar (if an exemplar is available for the given request/response pair).
+ ///
+ public HttpRequestExemplarPredicate ExemplarPredicate { get; set; } = _ => true;
+
+ ///
+ /// If set, the "page" label will be considered one of the built-in default labels.
+ /// This is only enabled if Razor Pages is detected at the middleware setup stage.
+ ///
+ /// The value is ignored if a custom metric is provided (though the user may still add
+ /// the "page" label themselves via AdditionalRouteParameters and it will work).
+ ///
+ internal bool IncludePageLabelInDefaultsInternal { get; set; }
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs
index ffebc8dd..f133f8d0 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs
@@ -1,27 +1,85 @@
-namespace Prometheus.HttpMetrics
+using Microsoft.AspNetCore.Http;
+
+namespace Prometheus.HttpMetrics;
+
+public sealed class HttpMiddlewareExporterOptions
{
- public sealed class HttpMiddlewareExporterOptions
+ public HttpInProgressOptions InProgress { get; set; } = new HttpInProgressOptions();
+ public HttpRequestCountOptions RequestCount { get; set; } = new HttpRequestCountOptions();
+ public HttpRequestDurationOptions RequestDuration { get; set; } = new HttpRequestDurationOptions();
+
+ ///
+ /// Whether to capture metrics for queries to the /metrics endpoint (where metrics are exported by default). Defaults to false.
+ /// This matches against URLs starting with the /metrics string specifically - if you use a custom metrics endpoint, this will not match.
+ ///
+ public bool CaptureMetricsUrl { get; set; }
+
+ ///
+ /// Configures all the different types of metrics to use reduced status code cardinality (using 2xx instead of 200, 201 etc).
+ ///
+ public void ReduceStatusCodeCardinality()
+ {
+ InProgress.ReduceStatusCodeCardinality = true;
+ RequestCount.ReduceStatusCodeCardinality = true;
+ RequestDuration.ReduceStatusCodeCardinality = true;
+ }
+
+ ///
+ /// Adds an additional route parameter to all the HTTP metrics.
+ ///
+ /// Helper method to avoid manually adding it to each one.
+ ///
+ public void AddRouteParameter(HttpRouteParameterMapping mapping)
+ {
+ InProgress.AdditionalRouteParameters.Add(mapping);
+ RequestCount.AdditionalRouteParameters.Add(mapping);
+ RequestDuration.AdditionalRouteParameters.Add(mapping);
+ }
+
+ ///
+ /// Adds a custom label to all the HTTP metrics.
+ ///
+ /// Helper method to avoid manually adding it to each one.
+ ///
+ public void AddCustomLabel(HttpCustomLabel mapping)
+ {
+ InProgress.CustomLabels.Add(mapping);
+ RequestCount.CustomLabels.Add(mapping);
+ RequestDuration.CustomLabels.Add(mapping);
+ }
+
+ ///
+ /// Adds a custom label to all the HTTP metrics.
+ ///
+ /// Helper method to avoid manually adding it to each one.
+ ///
+ public void AddCustomLabel(string labelName, Func valueProvider)
+ {
+ var mapping = new HttpCustomLabel(labelName, valueProvider);
+
+ InProgress.CustomLabels.Add(mapping);
+ RequestCount.CustomLabels.Add(mapping);
+ RequestDuration.CustomLabels.Add(mapping);
+ }
+
+ ///
+ /// Configures the middleware to use a custom metric factory for creating the metrics.
+ /// This provides an easy way to add custom static labels and configure exemplar behavior for all created metrics.
+ ///
+ public void SetMetricFactory(IMetricFactory metricFactory)
+ {
+ InProgress.MetricFactory = metricFactory;
+ RequestCount.MetricFactory = metricFactory;
+ RequestDuration.MetricFactory = metricFactory;
+ }
+
+ ///
+ /// Configures the options that are shared between all metrics exposed by the HTTP server exporter.
+ ///
+ public void ConfigureMeasurements(Action configure)
{
- public HttpInProgressOptions InProgress { get; set; } = new HttpInProgressOptions();
- public HttpRequestCountOptions RequestCount { get; set; } = new HttpRequestCountOptions();
- public HttpRequestDurationOptions RequestDuration { get; set; } = new HttpRequestDurationOptions();
-
- ///
- /// Whether to capture metrics for queries to the /metrics endpoint (where metrics are exported by default). Defaults to false.
- /// This matches against URLs starting with the /metrics string specifically - if you use a custom metrics endpoint, this will not match.
- ///
- public bool CaptureMetricsUrl { get; set; }
-
- ///
- /// Adds an additional route parameter to all the HTTP metrics.
- ///
- /// Helper method to avoid manually adding it to each one.
- ///
- public void AddRouteParameter(HttpRouteParameterMapping mapping)
- {
- InProgress.AdditionalRouteParameters.Add(mapping);
- RequestCount.AdditionalRouteParameters.Add(mapping);
- RequestDuration.AdditionalRouteParameters.Add(mapping);
- }
+ configure(InProgress);
+ configure(RequestCount);
+ configure(RequestDuration);
}
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs
index fae782ff..faba435e 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs
@@ -1,42 +1,38 @@
using Microsoft.AspNetCore.Http;
-using System;
-using System.Threading.Tasks;
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+internal sealed class HttpRequestCountMiddleware : HttpRequestMiddlewareBase, ICounter>
{
- internal sealed class HttpRequestCountMiddleware : HttpRequestMiddlewareBase, ICounter>
+ private readonly RequestDelegate _next;
+ private readonly HttpRequestCountOptions _options;
+
+ public HttpRequestCountMiddleware(RequestDelegate next, HttpRequestCountOptions options)
+ : base(options, options?.Counter)
{
- private readonly RequestDelegate _next;
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ }
- public HttpRequestCountMiddleware(RequestDelegate next, HttpRequestCountOptions? options)
- : base(options, options?.Counter)
+ public async Task Invoke(HttpContext context)
+ {
+ try
{
- _next = next ?? throw new ArgumentNullException(nameof(next));
+ await _next(context);
}
-
- public async Task Invoke(HttpContext context)
+ finally
{
- try
- {
- await _next(context);
- }
- finally
- {
- // We need to record metrics after inner handler execution because routing data in
- // ASP.NET Core 2 is only available *after* executing the next request delegate.
- // So we would not have the right labels if we tried to create the child early on.
- CreateChild(context).Inc();
- }
+ // We pass either null (== use default exemplar provider) or None (== do not record exemplar).
+ Exemplar? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None;
+
+ CreateChild(context).Inc(exemplar);
}
+ }
- protected override string[] DefaultLabels => HttpRequestLabelNames.All;
+ protected override string[] BaselineLabels => HttpRequestLabelNames.Default;
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter(
- "http_requests_received_total",
- "Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.",
- new CounterConfiguration
- {
- LabelNames = labelNames
- });
- }
+ protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter(
+ "http_requests_received_total",
+ "Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.",
+ labelNames);
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs
index d395cea2..e09e0d98 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs
@@ -1,10 +1,9 @@
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+public sealed class HttpRequestCountOptions : HttpMetricsOptionsBase
{
- public sealed class HttpRequestCountOptions : HttpMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Counter { get; set; }
- }
+ ///
+ /// Set this to use a custom metric instead of the default.
+ ///
+ public ICollector? Counter { get; set; }
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs
index 226d0590..820f8da4 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs
@@ -1,47 +1,45 @@
using Microsoft.AspNetCore.Http;
-using System;
-using System.Diagnostics;
-using System.Threading.Tasks;
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+internal sealed class HttpRequestDurationMiddleware : HttpRequestMiddlewareBase, IHistogram>
{
- internal sealed class HttpRequestDurationMiddleware : HttpRequestMiddlewareBase, IHistogram>
+ private readonly RequestDelegate _next;
+ private readonly HttpRequestDurationOptions _options;
+
+ public HttpRequestDurationMiddleware(RequestDelegate next, HttpRequestDurationOptions options)
+ : base(options, options?.Histogram)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ _options = options ?? throw new ArgumentNullException(nameof(options));
+ }
+
+ public async Task Invoke(HttpContext context)
{
- private readonly RequestDelegate _next;
+ var stopWatch = ValueStopwatch.StartNew();
- public HttpRequestDurationMiddleware(RequestDelegate next, HttpRequestDurationOptions? options)
- : base(options, options?.Histogram)
+ try
{
- _next = next ?? throw new ArgumentNullException(nameof(next));
+ await _next(context);
}
-
- public async Task Invoke(HttpContext context)
+ finally
{
- var stopWatch = ValueStopwatch.StartNew();
-
- // We need to write this out in long form instead of using a timer because routing data in
- // ASP.NET Core 2 is only available *after* executing the next request delegate.
- // So we would not have the right labels if we tried to create the child early on.
- try
- {
- await _next(context);
- }
- finally
- {
- CreateChild(context).Observe(stopWatch.GetElapsedTime().TotalSeconds);
- }
+ // We pass either null (== use default exemplar provider) or None (== do not record exemplar).
+ Exemplar? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None;
+
+ CreateChild(context).Observe(stopWatch.GetElapsedTime().TotalSeconds, exemplar);
}
+ }
- protected override string[] DefaultLabels => HttpRequestLabelNames.All;
+ protected override string[] BaselineLabels => HttpRequestLabelNames.Default;
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram(
- "http_request_duration_seconds",
- "The duration of HTTP requests processed by an ASP.NET Core application.",
- new HistogramConfiguration
- {
- // 1 ms to 32K ms buckets
- Buckets = Histogram.ExponentialBuckets(0.001, 2, 16),
- LabelNames = labelNames
- });
- }
+ protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram(
+ "http_request_duration_seconds",
+ "The duration of HTTP requests processed by an ASP.NET Core application.",
+ labelNames,
+ new HistogramConfiguration
+ {
+ // 1 ms to 32K ms buckets
+ Buckets = Histogram.ExponentialBuckets(0.001, 2, 16),
+ });
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs
index ccdd1272..ddda75aa 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs
@@ -1,10 +1,9 @@
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+public sealed class HttpRequestDurationOptions : HttpMetricsOptionsBase
{
- public sealed class HttpRequestDurationOptions : HttpMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Histogram { get; set; }
- }
+ ///
+ /// Set this to use a custom metric instead of the default.
+ ///
+ public ICollector? Histogram { get; set; }
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs
new file mode 100644
index 00000000..91fce1f9
--- /dev/null
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs
@@ -0,0 +1,5 @@
+using Microsoft.AspNetCore.Http;
+
+namespace Prometheus.HttpMetrics;
+
+public delegate bool HttpRequestExemplarPredicate(HttpContext context);
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs
index ff4b21be..d2fbcfdd 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs
@@ -1,37 +1,54 @@
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+///
+/// Label names used by the HTTP request handler metrics system.
+///
+public static class HttpRequestLabelNames
{
- ///
- /// Label names reserved for the use by the HTTP request metrics.
- ///
- public static class HttpRequestLabelNames
+ public const string Code = "code";
+ public const string Method = "method";
+ public const string Controller = "controller";
+ public const string Action = "action";
+
+ // Not reserved for background-compatibility, as it used to be optional and user-supplied.
+ // Conditionally, it may also be automatically added to metrics.
+ public const string Page = "page";
+
+ // Not reserved for background-compatibility, as it used to be optional and user-supplied.
+ public const string Endpoint = "endpoint";
+
+ // All labels that are supported by prometheus-net default logic.
+ // Useful if you want to define a custom metric that extends the default logic, without hardcoding the built-in label names.
+ public static readonly string[] All =
{
- public const string Code = "code";
- public const string Method = "method";
- public const string Controller = "controller";
- public const string Action = "action";
+ Code,
+ Method,
+ Controller,
+ Action,
+ Page,
+ Endpoint
+ };
- public static readonly string[] All =
- {
- Code,
- Method,
- Controller,
- Action
- };
+ // These are reserved and may only be used with the default logic.
+ internal static readonly string[] Default =
+ {
+ Code,
+ Method,
+ Controller,
+ Action
+ };
- internal static readonly string[] PotentiallyAvailableBeforeExecutingFinalHandler =
- {
- // Always available, part of request.
- Method,
- // These two are available only in ASP.NET Core 3.
- Controller,
- Action
- };
+ internal static readonly string[] DefaultsAvailableBeforeExecutingFinalHandler =
+ {
+ Method,
+ Controller,
+ Action
+ };
- // Labels that do not need routing information to be collected.
- internal static readonly string[] NonRouteSpecific =
- {
- Code,
- Method
- };
- }
+ // Labels that do not need routing information to be collected.
+ internal static readonly string[] NonRouteSpecific =
+ {
+ Code,
+ Method
+ };
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs
index 108ae561..0a5cea70 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs
@@ -1,206 +1,329 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
-using System;
-using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+///
+/// This base class performs the data management necessary to associate the correct labels and values
+/// with HTTP request metrics, depending on the options the user has provided for the HTTP metric middleware.
+///
+/// The following labels are supported:
+/// 'code' (HTTP status code)
+/// 'method' (HTTP request method)
+/// 'controller' (The Controller used to fulfill the HTTP request)
+/// 'action' (The Action used to fulfill the HTTP request)
+/// Any other label - from one of:
+/// * HTTP route parameter (if name/mapping specified in options; name need not match).
+/// * custom logic (callback decides value for each request)
+///
+/// The 'code' and 'method' data are taken from the current HTTP context.
+/// 'controller', 'action' and route parameter labels will be taken from the request routing information.
+///
+/// If a custom metric is provided in the options, it must not be missing any labels for explicitly defined
+/// custom route parameters. However, it is permitted to lack any of the default labels (code/method/...).
+///
+internal abstract class HttpRequestMiddlewareBase
+ where TCollector : class, ICollector
+ where TChild : class, ICollectorChild
{
///
- /// This base class performs the data management necessary to associate the correct labels and values
- /// with HTTP request metrics, depending on the options the user has provided for the HTTP metric middleware.
+ /// The set of labels from among the defaults that this metric supports.
///
- /// The following labels are supported:
- /// 'code' (HTTP status code)
- /// 'method' (HTTP request method)
- /// 'controller' (The Controller used to fulfill the HTTP request)
- /// 'action' (The Action used to fulfill the HTTP request)
- /// Any other label - custom HTTP route parameter (if specified in options).
+ /// This set will be automatically extended with labels for additional
+ /// route parameters and custom labels when creating the default metric instance.
///
- /// The 'code' and 'method' data are taken from the current HTTP context.
- /// Other labels will be taken from the request routing information.
- ///
- /// If a custom metric is provided in the options, it must not be missing any labels for explicitly defined
- /// custom route parameters. However, it is permitted to lack any of the default labels (code/method/...).
+ /// It will also be extended by additional built-in logic (page, endpoint).
+ ///
+ protected abstract string[] BaselineLabels { get; }
+
+ ///
+ /// Creates the default metric instance with the specified set of labels.
+ /// Only used if the caller does not provide a custom metric instance in the options.
///
- internal abstract class HttpRequestMiddlewareBase
- where TCollector : class, ICollector
- where TChild : class, ICollectorChild
+ protected abstract TCollector CreateMetricInstance(string[] labelNames);
+
+ ///
+ /// The factory to use for creating the default metric for this middleware.
+ /// Not used if a custom metric is already provided in options.
+ ///
+ protected IMetricFactory MetricFactory { get; }
+
+ private readonly List _additionalRouteParameters;
+ private readonly List _customLabels;
+ private readonly TCollector _metric;
+
+ // For labels that are route parameter mappings.
+ private readonly Dictionary _labelToRouteParameterMap;
+
+ // For labels that use a custom value provider.
+ private readonly Dictionary> _labelToValueProviderMap;
+
+ private readonly bool _labelsRequireRouteData;
+ private readonly bool _reduceStatusCodeCardinality;
+
+ protected HttpRequestMiddlewareBase(HttpMetricsOptionsBase options, TCollector? customMetric)
{
- ///
- /// The set of labels from among the defaults that this metric supports.
- ///
- /// This set will be automatically extended with labels for additional
- /// route parameters when creating the default metric instance.
- ///
- protected abstract string[] DefaultLabels { get; }
-
- ///
- /// Creates the default metric instance with the specified set of labels.
- /// Only used if the caller does not provide a custom metric instance in the options.
- ///
- protected abstract TCollector CreateMetricInstance(string[] labelNames);
-
- ///
- /// The factory to use for creating the default metric for this middleware.
- /// Not used if a custom metric is already provided in options.
- ///
- protected MetricFactory MetricFactory { get; }
-
- private readonly ICollection _additionalRouteParameters;
- private readonly TCollector _metric;
-
- private readonly Dictionary _labelToRouteParameterMap;
-
- private readonly bool _labelsRequireRouteData;
-
- protected HttpRequestMiddlewareBase(HttpMetricsOptionsBase? options, TCollector? customMetric)
- {
- MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry);
+ MetricFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(options.Registry ?? Metrics.DefaultRegistry);
- _additionalRouteParameters = options?.AdditionalRouteParameters ?? new List(0);
+ _additionalRouteParameters = options.AdditionalRouteParameters ?? new List(0);
+ _customLabels = options.CustomLabels ?? new List(0);
- ValidateAdditionalRouteParameterSet();
- _labelToRouteParameterMap = CreateLabelToRouteParameterMap();
+ if (options.IncludePageLabelInDefaultsInternal)
+ AddPageLabelIfNoConflict(customMetric);
- if (customMetric != null)
- {
- _metric = customMetric;
+ AddEndpointLabelIfNoConflict(customMetric);
- ValidateNoUnexpectedLabelNames();
- ValidateAdditionalRouteParametersPresentInMetricLabelNames();
- }
- else
- {
- _metric = CreateMetricInstance(CreateDefaultLabelSet());
- }
+ ValidateMappings();
+ _labelToRouteParameterMap = CreateLabelToRouteParameterMap();
+ _reduceStatusCodeCardinality = options?.ReduceStatusCodeCardinality ?? false;
+ _labelToValueProviderMap = CreateLabelToValueProviderMap();
- _labelsRequireRouteData = _metric.LabelNames.Except(HttpRequestLabelNames.NonRouteSpecific).Any();
- }
+ if (customMetric != null)
+ {
+ _metric = customMetric;
- ///
- /// Creates the metric child instance to use for measurements.
- ///
- ///
- /// Internal for testing purposes.
- ///
- protected internal TChild CreateChild(HttpContext context)
+ ValidateNoUnexpectedLabelNames();
+ ValidateAdditionalRouteParametersPresentInMetricLabelNames();
+ ValidateCustomLabelsPresentInMetricLabelNames();
+ }
+ else
{
- if (!_metric.LabelNames.Any())
- return _metric.Unlabelled;
+ _metric = CreateMetricInstance(CreateDefaultLabelSet());
+ }
- if (!_labelsRequireRouteData)
- return CreateChild(context, null);
+ _labelsRequireRouteData = _metric.LabelNames.Except(HttpRequestLabelNames.NonRouteSpecific).Any();
+ }
- var routeData = context.Features.Get()?.Values;
+ private void AddPageLabelIfNoConflict(TCollector? customMetric)
+ {
+ // We were asked to add the "page" label because Razor Pages was detected.
+ // We will only do this if nothing else has already occupied the "page" label.
+ // If a custom metric is used, we also skip this if it has no "page" label name defined.
+ //
+ // The possible conflicts are:
+ // * an existing route parameter mapping (which works out the same as our logic, so fine)
+ // * custom logic that defines a "page" label (in which case we allow it to win, for backward compatibility).
- // If we have captured route data, we always prefer it.
- // Otherwise, we extract new route data right now.
- if (routeData == null)
- routeData = context.GetRouteData()?.Values;
+ if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Page))
+ return;
- return CreateChild(context, routeData);
- }
+ if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Page))
+ return;
+
+ if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Page))
+ return;
- protected TChild CreateChild(HttpContext context, RouteValueDictionary? routeData)
+ // If we got so far, we are good - all preconditions for adding "page" label exist.
+ _additionalRouteParameters.Add(new HttpRouteParameterMapping("page"));
+ }
+
+ private void AddEndpointLabelIfNoConflict(TCollector? customMetric)
+ {
+ // We always try to add an "endpoint" label with the endpoint routing route pattern.
+ // We will only do this if nothing else has already occupied the "endpoint" label.
+ // If a custom metric is used, we also skip this if it has no "endpoint" label name defined.
+ //
+ // The possible conflicts are:
+ // * an existing route parameter mapping
+ // * custom logic that defines an "endpoint" label
+ //
+ // In case of conflict, we let the user-defined item win.
+
+ if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint))
+ return;
+
+ if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint))
+ return;
+
+ if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Endpoint))
+ return;
+
+ _customLabels.Add(new HttpCustomLabel(HttpRequestLabelNames.Endpoint, context =>
{
- var labelValues = new string[_metric.LabelNames.Length];
+ var endpoint = context.GetEndpoint() as RouteEndpoint;
+ return endpoint?.RoutePattern.RawText ?? "";
+ }));
+ }
+
+ ///
+ /// Creates the metric child instance to use for measurements.
+ ///
+ ///
+ /// Internal for testing purposes.
+ ///
+ protected internal TChild CreateChild(HttpContext context)
+ {
+ if (!_metric.LabelNames.Any())
+ return _metric.Unlabelled;
- for (var i = 0; i < labelValues.Length; i++)
+ if (!_labelsRequireRouteData)
+ return CreateChild(context, null);
+
+ var routeData = context.Features.Get()?.Values;
+
+ // If we have captured route data, we always prefer it.
+ // Otherwise, we extract new route data right now.
+ if (routeData == null)
+ routeData = context.GetRouteData()?.Values;
+
+ return CreateChild(context, routeData);
+ }
+
+ protected TChild CreateChild(HttpContext context, RouteValueDictionary? routeData)
+ {
+ var labelValues = new string[_metric.LabelNames.Length];
+
+ for (var i = 0; i < labelValues.Length; i++)
+ {
+ switch (_metric.LabelNames[i])
{
- switch (_metric.LabelNames[i])
- {
- case HttpRequestLabelNames.Method:
- labelValues[i] = context.Request.Method;
- break;
- case HttpRequestLabelNames.Code:
- labelValues[i] = context.Response.StatusCode.ToString(CultureInfo.InvariantCulture);
- break;
- default:
- // We validate the label set on initialization, so it must be a route parameter if we get to this point.
- var parameterName = _labelToRouteParameterMap[_metric.LabelNames[i]];
+ case HttpRequestLabelNames.Method:
+ labelValues[i] = context.Request.Method;
+ break;
+ case HttpRequestLabelNames.Code:
+ labelValues[i] = _reduceStatusCodeCardinality ? Math.Floor(context.Response.StatusCode / 100.0).ToString("#xx") : context.Response.StatusCode.ToString(CultureInfo.InvariantCulture);
+ break;
+ default:
+ // If we get to this point it must be either:
+ if (_labelToRouteParameterMap.TryGetValue(_metric.LabelNames[i], out var parameterName))
+ {
+ // A mapped route parameter.
labelValues[i] = routeData?[parameterName] as string ?? string.Empty;
- break;
- }
+ }
+ else if (_labelToValueProviderMap.TryGetValue(_metric.LabelNames[i], out var valueProvider))
+ {
+ // A custom label value provider.
+ labelValues[i] = valueProvider(context) ?? string.Empty;
+ }
+ else
+ {
+ // Something we do not have data for.
+ // This can happen if, for example, a custom metric inherits "all" the labels without reimplementing the "when do we add which label"
+ // logic that prometheus-net implements (which is an entirely reasonable design). So it might just add a "page" label when we have no
+ // page information. Instead of rejecting such custom metrics, we just leave the label value empty and carry on.
+ labelValues[i] = "";
+ }
+ break;
}
-
- return _metric.WithLabels(labelValues);
}
+ return _metric.WithLabels(labelValues);
+ }
- ///
- /// Creates the full set of labels supported for the current metric.
- ///
- /// This merges (in unspecified order) the defaults from prometheus-net with any in options.AdditionalRouteParameters.
- ///
- private string[] CreateDefaultLabelSet()
- {
- return DefaultLabels.Concat(_additionalRouteParameters.Select(x => x.LabelName)).ToArray();
- }
- private void ValidateAdditionalRouteParameterSet()
- {
- var parameterNames = _additionalRouteParameters.Select(x => x.ParameterName).ToList();
+ ///
+ /// Creates the set of labels defined on the automatically created metric.
+ ///
+ private string[] CreateDefaultLabelSet()
+ {
+ return BaselineLabels
+ .Concat(_additionalRouteParameters.Select(x => x.LabelName))
+ .Concat(_customLabels.Select(x => x.LabelName))
+ .ToArray();
+ }
- if (parameterNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != parameterNames.Count)
- throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same parameter name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters));
+ ///
+ /// Creates the full set of labels ALLOWED for the current metric.
+ /// This may be a greater set than the labels automatically added to the default metric.
+ ///
+ private string[] CreateAllowedLabelSet()
+ {
+ return HttpRequestLabelNames.All
+ .Concat(_additionalRouteParameters.Select(x => x.LabelName))
+ .Concat(_customLabels.Select(x => x.LabelName))
+ .Distinct() // Some builtins may also exist in the customs, with customs overwriting. That's fine.
+ .ToArray();
+ }
- var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList();
+ private void ValidateMappings()
+ {
+ var routeParameterLabelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList();
- if (labelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != labelNames.Count)
- throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters));
+ if (routeParameterLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != routeParameterLabelNames.Count)
+ throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters));
- if (HttpRequestLabelNames.All.Except(labelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.All.Length)
- throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.All)}");
+ if (HttpRequestLabelNames.Default.Except(routeParameterLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length)
+ throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}");
- var reservedParameterNames = new[] { "action", "controller" };
+ var customLabelNames = _customLabels.Select(x => x.LabelName).ToList();
- if (reservedParameterNames.Except(parameterNames, StringComparer.InvariantCultureIgnoreCase).Count() != reservedParameterNames.Length)
- throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved route parameter name. Reserved route parameter names are: {string.Join(", ", reservedParameterNames)}");
- }
+ if (customLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != customLabelNames.Count)
+ throw new ArgumentException("The set of custom labels contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.CustomLabels));
- private Dictionary CreateLabelToRouteParameterMap()
- {
- var map = new Dictionary(_additionalRouteParameters.Count + 2);
+ if (HttpRequestLabelNames.Default.Except(customLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length)
+ throw new ArgumentException($"The set of custom labels contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}");
- // Defaults are hardcoded.
- map["action"] = "action";
- map["controller"] = "controller";
+ if (customLabelNames.Intersect(routeParameterLabelNames).Any())
+ throw new ArgumentException("The set of custom labels and the set of additional route parameters contain conflicting label names.", nameof(HttpMetricsOptionsBase.CustomLabels));
+ }
- // Any additional ones are merged.
- foreach (var entry in _additionalRouteParameters)
- map[entry.LabelName] = entry.ParameterName;
+ private Dictionary CreateLabelToRouteParameterMap()
+ {
+ var map = new Dictionary(_additionalRouteParameters.Count + 2);
- return map;
- }
+ // Defaults are hardcoded.
+ map["action"] = "action";
+ map["controller"] = "controller";
- ///
- /// Inspects the metric instance to ensure that all required labels are present.
- ///
- ///
- /// If there are mappings to include route parameters in the labels, there must be labels defined for each such parameter.
- /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller.
- ///
- private void ValidateAdditionalRouteParametersPresentInMetricLabelNames()
- {
- var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList();
- var missing = labelNames.Except(_metric.LabelNames);
+ // Any additional ones are merged.
+ foreach (var entry in _additionalRouteParameters)
+ map[entry.LabelName] = entry.ParameterName;
- if (missing.Any())
- throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}.");
- }
+ return map;
+ }
- ///
- /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters.
- ///
- private void ValidateNoUnexpectedLabelNames()
- {
- var allowedLabels = CreateDefaultLabelSet();
- var unexpected = _metric.LabelNames.Except(allowedLabels);
+ private Dictionary> CreateLabelToValueProviderMap()
+ {
+ var map = new Dictionary>(_customLabels.Count);
- if (unexpected.Any())
- throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}.");
- }
+ foreach (var entry in _customLabels)
+ map[entry.LabelName] = entry.LabelValueProvider;
+
+ return map;
+ }
+
+ ///
+ /// Inspects the metric instance to ensure that all required labels are present.
+ ///
+ ///
+ /// If there are mappings to include route parameters in the labels, there must be labels defined for each such parameter.
+ /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller.
+ ///
+ private void ValidateAdditionalRouteParametersPresentInMetricLabelNames()
+ {
+ var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList();
+ var missing = labelNames.Except(_metric.LabelNames);
+
+ if (missing.Any())
+ throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}.");
+ }
+
+ ///
+ /// Inspects the metric instance to ensure that all required labels are present.
+ ///
+ ///
+ /// If there are mappings to include custom labels, there must be label names defined for each such parameter.
+ /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller.
+ ///
+ private void ValidateCustomLabelsPresentInMetricLabelNames()
+ {
+ var labelNames = _customLabels.Select(x => x.LabelName).ToList();
+ var missing = labelNames.Except(_metric.LabelNames);
+
+ if (missing.Any())
+ throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}.");
+ }
+
+ ///
+ /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters.
+ ///
+ private void ValidateNoUnexpectedLabelNames()
+ {
+ var allowedLabels = CreateAllowedLabelSet();
+ var unexpected = _metric.LabelNames.Except(allowedLabels);
+
+ if (unexpected.Any())
+ throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}.");
}
}
diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs
index c1977984..fafab89a 100644
--- a/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs
@@ -1,40 +1,39 @@
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+///
+/// Maps an HTTP route parameter name to a Prometheus label name.
+///
+///
+/// Typically, the parameter name and the label name will be equal.
+/// The purpose of this is to enable capture of route parameters that conflict with built-in label names like "method" (HTTP method).
+///
+public sealed class HttpRouteParameterMapping
{
///
- /// Maps an HTTP route parameter name to a Prometheus label name.
+ /// Name of the HTTP route parameter.
///
- ///
- /// Typically, the parameter name and the label name will be equal.
- /// The purpose of this is to enable capture of route parameters that conflict with built-in label names like "method" (HTTP method).
- ///
- public sealed class HttpRouteParameterMapping
- {
- ///
- /// Name of the HTTP route parameter.
- ///
- public string ParameterName { get; }
-
- ///
- /// Name of the Prometheus label.
- ///
- public string LabelName { get; }
+ public string ParameterName { get; }
- public HttpRouteParameterMapping(string name)
- {
- Collector.ValidateLabelName(name);
+ ///
+ /// Name of the Prometheus label.
+ ///
+ public string LabelName { get; }
- ParameterName = name;
- LabelName = name;
- }
+ public HttpRouteParameterMapping(string name)
+ {
+ Collector.ValidateLabelName(name);
- public HttpRouteParameterMapping(string parameterName, string labelName)
- {
- Collector.ValidateLabelName(labelName);
+ ParameterName = name;
+ LabelName = name;
+ }
- ParameterName = parameterName;
- LabelName = labelName;
- }
+ public HttpRouteParameterMapping(string parameterName, string labelName)
+ {
+ Collector.ValidateLabelName(labelName);
- public static implicit operator HttpRouteParameterMapping(string name) => new HttpRouteParameterMapping(name);
+ ParameterName = parameterName;
+ LabelName = labelName;
}
+
+ public static implicit operator HttpRouteParameterMapping(string name) => new HttpRouteParameterMapping(name);
}
diff --git a/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs b/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs
index dc99e518..d712c9d3 100644
--- a/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs
+++ b/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs
@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Routing;
-namespace Prometheus.HttpMetrics
+namespace Prometheus.HttpMetrics;
+
+interface ICapturedRouteDataFeature
{
- interface ICapturedRouteDataFeature
- {
- RouteValueDictionary Values { get; }
- }
+ RouteValueDictionary Values { get; }
}
diff --git a/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs b/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs
index 32556a15..8f9987b2 100644
--- a/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs
+++ b/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs
@@ -1,56 +1,67 @@
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
using Prometheus.HttpMetrics;
-using System;
-namespace Prometheus
+namespace Prometheus;
+
+public static class HttpMetricsMiddlewareExtensions
{
- public static class HttpMetricsMiddlewareExtensions
+ ///
+ /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests.
+ ///
+ /// Call this after .UseRouting().
+ ///
+ public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app,
+ Action configure)
{
- ///
- /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests.
- ///
- /// If using ASP.NET Core 3 or newer, call this after .UseRouting().
- ///
- public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app,
- Action configure)
- {
- var options = new HttpMiddlewareExporterOptions();
+ var options = new HttpMiddlewareExporterOptions();
+
+ configure?.Invoke(options);
- configure?.Invoke(options);
+ app.UseHttpMetrics(options);
- app.UseHttpMetrics(options);
+ return app;
+ }
+
+ ///
+ /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests.
+ ///
+ /// Call this after .UseRouting().
+ ///
+ public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app,
+ HttpMiddlewareExporterOptions? options = null)
+ {
+ options = options ?? new HttpMiddlewareExporterOptions();
- return app;
+ if (app.ApplicationServices.GetService() != null)
+ {
+ // If Razor Pages is enabled, we will automatically add a "page" route parameter to represent it. We do this only if no custom metric is used.
+ // If a custom metric is used, we still allow "page" label to be present and automatically fill it with the Razor Pages route parameter
+ // unless there is a custom label with this name added, in which case the custom label takes priority.
+
+ options.InProgress.IncludePageLabelInDefaultsInternal = true;
+ options.RequestCount.IncludePageLabelInDefaultsInternal = true;
+ options.RequestDuration.IncludePageLabelInDefaultsInternal = true;
}
- ///
- /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests.
- ///
- /// If using ASP.NET Core 3 or newer, call this after .UseRouting().
- ///
- public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app,
- HttpMiddlewareExporterOptions? options = null)
+ void ApplyConfiguration(IApplicationBuilder builder)
{
- options = options ?? new HttpMiddlewareExporterOptions();
-
- void ApplyConfiguration(IApplicationBuilder builder)
- {
- builder.UseMiddleware();
-
- if (options.InProgress.Enabled)
- builder.UseMiddleware(options.InProgress);
- if (options.RequestCount.Enabled)
- builder.UseMiddleware(options.RequestCount);
- if (options.RequestDuration.Enabled)
- builder.UseMiddleware(options.RequestDuration);
- }
-
- if (options.CaptureMetricsUrl)
- ApplyConfiguration(app);
- else
- app.UseWhen(context => context.Request.Path != "/metrics", ApplyConfiguration);
-
- return app;
+ builder.UseMiddleware();
+
+ if (options.InProgress.Enabled)
+ builder.UseMiddleware(options.InProgress);
+ if (options.RequestCount.Enabled)
+ builder.UseMiddleware(options.RequestCount);
+ if (options.RequestDuration.Enabled)
+ builder.UseMiddleware(options.RequestDuration);
}
+
+ if (options.CaptureMetricsUrl)
+ ApplyConfiguration(app);
+ else
+ app.UseWhen(context => context.Request.Path != "/metrics", ApplyConfiguration);
+
+ return app;
}
}
\ No newline at end of file
diff --git a/Prometheus.AspNetCore/KestrelMetricServer.cs b/Prometheus.AspNetCore/KestrelMetricServer.cs
index 39b06b88..3dca354a 100644
--- a/Prometheus.AspNetCore/KestrelMetricServer.cs
+++ b/Prometheus.AspNetCore/KestrelMetricServer.cs
@@ -1,93 +1,114 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
-using System;
using System.Net;
using System.Security.Cryptography.X509Certificates;
-using System.Threading;
-using System.Threading.Tasks;
-namespace Prometheus
+namespace Prometheus;
+
+///
+/// A stand-alone Kestrel based metric server that saves you the effort of setting up the ASP.NET Core pipeline.
+/// For all practical purposes, this is just a regular ASP.NET Core server that only serves Prometheus requests.
+///
+public sealed class KestrelMetricServer : MetricHandler
{
- ///
- /// A stand-alone Kestrel based metric server that saves you the effort of setting up the ASP.NET Core pipeline.
- /// For all practical purposes, this is just a regular ASP.NET Core server that only serves Prometheus requests.
- ///
- public sealed class KestrelMetricServer : MetricHandler
+ public KestrelMetricServer(int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this("+", port, url, registry, certificate)
+ {
+ }
+
+ public KestrelMetricServer(string hostname, int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this(LegacyOptions(hostname, port, url, registry, certificate))
{
- public KestrelMetricServer(int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this("+", port, url, registry, certificate)
+ }
+
+ private static KestrelMetricServerOptions LegacyOptions(string hostname, int port, string url, CollectorRegistry? registry, X509Certificate2? certificate) =>
+ new KestrelMetricServerOptions
{
- }
+ Hostname = hostname,
+ Port = (ushort)port,
+ Url = url,
+ Registry = registry,
+ TlsCertificate = certificate,
+ };
+
+ public KestrelMetricServer(KestrelMetricServerOptions options)
+ {
+ _hostname = options.Hostname;
+ _port = options.Port;
+ _url = options.Url;
+ _certificate = options.TlsCertificate;
- public KestrelMetricServer(string hostname, int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : base(registry)
+ // We use one callback to apply the legacy settings, and from within this we call the real callback.
+ _configureExporter = settings =>
{
- _hostname = hostname;
- _port = port;
- _url = url;
+ // Legacy setting, may be overridden by ConfigureExporter.
+ settings.Registry = options.Registry;
- _certificate = certificate;
- }
+ if (options.ConfigureExporter != null)
+ options.ConfigureExporter(settings);
+ };
+ }
- private readonly string _hostname;
- private readonly int _port;
- private readonly string _url;
+ private readonly string _hostname;
+ private readonly int _port;
+ private readonly string _url;
- private readonly X509Certificate2? _certificate;
+ private readonly X509Certificate2? _certificate;
- protected override Task StartServer(CancellationToken cancel)
- {
- var s = _certificate != null ? "s" : "";
- var hostAddress = $"http{s}://{_hostname}:{_port}";
-
- // If the caller needs to customize any of this, they can just set up their own web host and inject the middleware.
- var builder = new WebHostBuilder()
- .UseKestrel()
- .UseIISIntegration()
- .Configure(app =>
- {
- // _registry will already be pre-configured by MetricHandler.
- app.UseMetricServer(_url, _registry);
-
- // If there is any URL prefix, we just redirect people going to root URL to our prefix.
- if (!string.IsNullOrWhiteSpace(_url.Trim('/')))
- {
- app.MapWhen(context => context.Request.Path.Value.Trim('/') == "",
- configuration =>
- {
- configuration.Use((context, next) =>
- {
- context.Response.Redirect(_url);
- return Task.CompletedTask;
- });
- });
- }
- });
+ private readonly Action _configureExporter;
- if (_certificate != null)
+ protected override Task StartServer(CancellationToken cancel)
+ {
+ var s = _certificate != null ? "s" : "";
+ var hostAddress = $"http{s}://{_hostname}:{_port}";
+
+ // If the caller needs to customize any of this, they can just set up their own web host and inject the middleware.
+ var builder = new WebHostBuilder()
+ .UseKestrel()
+ .UseIISIntegration()
+ .Configure(app =>
{
- builder = builder.ConfigureServices(services =>
- {
- Action configureEndpoint = options =>
- {
- options.UseHttps(_certificate);
- };
+ app.UseMetricServer(_configureExporter, _url);
- services.Configure(options =>
+ // If there is any URL prefix, we just redirect people going to root URL to our prefix.
+ if (!string.IsNullOrWhiteSpace(_url.Trim('/')))
+ {
+ app.MapWhen(context => context.Request.Path.Value?.Trim('/') == "",
+ configuration =>
{
- options.Listen(IPAddress.Any, _port, configureEndpoint);
+ configuration.Use((HttpContext context, RequestDelegate next) =>
+ {
+ context.Response.Redirect(_url);
+ return Task.CompletedTask;
+ });
});
- });
- }
- else
- {
- builder = builder.UseUrls(hostAddress);
- }
+ }
+ });
- var webHost = builder.Build();
- webHost.Start();
+ if (_certificate != null)
+ {
+ builder = builder.ConfigureServices(services =>
+ {
+ Action configureEndpoint = options =>
+ {
+ options.UseHttps(_certificate);
+ };
- return webHost.WaitForShutdownAsync(cancel);
+ services.Configure(options =>
+ {
+ options.Listen(IPAddress.Any, _port, configureEndpoint);
+ });
+ });
}
+ else
+ {
+ builder = builder.UseUrls(hostAddress);
+ }
+
+ var webHost = builder.Build();
+ webHost.Start();
+
+ return webHost.WaitForShutdownAsync(cancel);
}
}
diff --git a/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs b/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs
new file mode 100644
index 00000000..b5215ca2
--- /dev/null
+++ b/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace Prometheus;
+
+public static class KestrelMetricServerExtensions
+{
+ public static IServiceCollection AddMetricServer(this IServiceCollection services, Action optionsCallback)
+ {
+ return services.AddHostedService(sp =>
+ {
+ var options = new KestrelMetricServerOptions();
+ optionsCallback(options);
+ return new MetricsExporterService(options);
+ });
+ }
+
+ private sealed class MetricsExporterService : BackgroundService
+ {
+ public MetricsExporterService(KestrelMetricServerOptions options)
+ {
+ _options = options;
+ }
+
+ private readonly KestrelMetricServerOptions _options;
+
+ protected override async Task ExecuteAsync(CancellationToken cancel)
+ {
+ using var metricServer = new KestrelMetricServer(_options);
+ metricServer.Start();
+
+ try
+ {
+ // Wait forever, until we are told to stop.
+ await Task.Delay(-1, cancel);
+ }
+ catch (OperationCanceledException) when (cancel.IsCancellationRequested)
+ {
+ // Time to stop.
+ }
+ }
+ }
+
+}
diff --git a/Prometheus.AspNetCore/KestrelMetricServerOptions.cs b/Prometheus.AspNetCore/KestrelMetricServerOptions.cs
new file mode 100644
index 00000000..817eebf5
--- /dev/null
+++ b/Prometheus.AspNetCore/KestrelMetricServerOptions.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Prometheus;
+
+public sealed class KestrelMetricServerOptions
+{
+ ///
+ /// Will listen for requests using this hostname. "+" indicates listen on all hostnames.
+ /// By setting this to "localhost", you can easily prevent access from remote systems.-
+ ///
+ public string Hostname { get; set; } = "+";
+
+ public ushort Port { get; set; }
+ public string Url { get; set; } = "/metrics";
+ public X509Certificate2? TlsCertificate { get; set; }
+
+ // May be overridden by ConfigureExporter.
+ [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it and prefer ConfigureExporter.
+ public CollectorRegistry? Registry { get; set; }
+
+ ///
+ /// Allows metric exporter options to be configured in a flexible manner.
+ /// The callback is called after applying any values in KestrelMetricServerOptions.
+ ///
+ public Action? ConfigureExporter { get; set; }
+}
diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs
index b11fc079..e7ef2af5 100644
--- a/Prometheus.AspNetCore/MetricServerMiddleware.cs
+++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs
@@ -1,62 +1,115 @@
using Microsoft.AspNetCore.Http;
-using System;
-using System.IO;
-using System.Threading.Tasks;
+using System.Net.Http.Headers;
-namespace Prometheus
+namespace Prometheus;
+
+///
+/// Prometheus metrics export middleware for ASP.NET Core.
+///
+/// You should use IApplicationBuilder.UseMetricServer extension method instead of using this class directly.
+///
+public sealed class MetricServerMiddleware
{
- ///
- /// Prometheus metrics export middleware for ASP.NET Core.
- ///
- /// You should use IApplicationBuilder.UseMetricServer extension method instead of using this class directly.
- ///
- public sealed class MetricServerMiddleware
+ public MetricServerMiddleware(RequestDelegate next, Settings settings)
+ {
+ _registry = settings.Registry ?? Metrics.DefaultRegistry;
+ _enableOpenMetrics = settings.EnableOpenMetrics;
+ }
+
+ public sealed class Settings
+ {
+ ///
+ /// Where do we take the metrics from. By default, we will take them from the global singleton registry.
+ ///
+ public CollectorRegistry? Registry { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public bool EnableOpenMetrics { get; set; } = true;
+ }
+
+ private readonly CollectorRegistry _registry;
+ private readonly bool _enableOpenMetrics;
+
+ private readonly record struct ProtocolNegotiationResult(ExpositionFormat ExpositionFormat, string ContentType);
+
+ private static IEnumerable ExtractAcceptableMediaTypes(string acceptHeaderValue)
{
- public MetricServerMiddleware(RequestDelegate next, Settings settings)
+ var candidates = acceptHeaderValue.Split(',');
+
+ foreach (var candidate in candidates)
{
- _registry = settings.Registry ?? Metrics.DefaultRegistry;
+ // 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;
}
+ }
- public sealed class Settings
+ 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)))
{
- public CollectorRegistry? Registry { get; set; }
+ 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);
+ }
}
- private readonly CollectorRegistry _registry;
+ return new ProtocolNegotiationResult(ExpositionFormat.PrometheusText, PrometheusConstants.TextContentTypeWithVersionAndEncoding);
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ var response = context.Response;
- public async Task Invoke(HttpContext context)
+ try
{
- var response = context.Response;
+ var negotiationResult = NegotiateComminucationProtocol(context.Request);
- try
- {
- // We first touch the response.Body only in the callback because touching
- // it means we can no longer send headers (the status code).
- var serializer = new TextSerializer(delegate
- {
- response.ContentType = PrometheusConstants.ExporterContentType;
- response.StatusCode = StatusCodes.Status200OK;
- return response.Body;
- });
-
- await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted);
- }
- catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
+ Stream GetResponseBodyStream()
{
- // The scrape was cancalled by the client. This is fine. Just swallow the exception to not generate pointless spam.
+ // 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;
}
- catch (ScrapeFailedException ex)
+
+ 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))
{
- // 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);
- }
+ using var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding, bufferSize: -1, leaveOpen: true);
+ await writer.WriteAsync(ex.Message);
}
}
}
diff --git a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs
index b1ab57a4..742befb7 100644
--- a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs
+++ b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs
@@ -1,83 +1,131 @@
-using System;
-using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
+using System.ComponentModel;
-namespace Prometheus
+namespace Prometheus;
+
+public static class MetricServerMiddlewareExtensions
{
- public static class MetricServerMiddlewareExtensions
+ private const string DefaultDisplayName = "Prometheus metrics";
+
+ ///
+ /// Starts a Prometheus metrics exporter using endpoint routing.
+ /// The default URL is /metrics, which is a Prometheus convention.
+ /// Use static methods on the class to create your metrics.
+ ///
+ public static IEndpointConventionBuilder MapMetrics(
+ this IEndpointRouteBuilder endpoints,
+ Action configure,
+ string pattern = "/metrics"
+ )
{
+ var pipeline = endpoints
+ .CreateApplicationBuilder()
+ .InternalUseMiddleware(configure)
+ .Build();
+
+ return endpoints
+ .Map(pattern, pipeline)
+ .WithDisplayName(DefaultDisplayName);
+ }
-#if NETCOREAPP3_1
+ ///
+ /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port.
+ /// The default URL is /metrics, which is a Prometheus convention.
+ /// Use static methods on the class to create your metrics.
+ ///
+ public static IApplicationBuilder UseMetricServer(
+ this IApplicationBuilder builder,
+ int port,
+ Action configure,
+ string? url = "/metrics")
+ {
+ // If no URL, use root URL.
+ url ??= "/";
- private const string DefaultDisplayName = "Prometheus metrics";
+ return builder
+ .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(configure)));
- ///
- /// Starts a Prometheus metrics exporter using endpoint routing.
- /// The default URL is /metrics, which is a Prometheus convention.
- /// Use static methods on the class to create your metrics.
- ///
- public static IEndpointConventionBuilder MapMetrics(
- this IEndpointRouteBuilder endpoints,
- string pattern = "/metrics",
- CollectorRegistry? registry = null
- )
+ Func PortMatches()
{
+ return c => c.Connection.LocalPort == port;
+ }
+ }
- var pipeline = endpoints
- .CreateApplicationBuilder()
- .UseMiddleware(
- new MetricServerMiddleware.Settings
- {
- Registry = registry
- }
- )
- .Build();
+ ///
+ /// Starts a Prometheus metrics exporter.
+ /// The default URL is /metrics, which is a Prometheus convention.
+ /// Use static methods on the class to create your metrics.
+ ///
+ public static IApplicationBuilder UseMetricServer(
+ this IApplicationBuilder builder,
+ Action configure,
+ string? url = "/metrics")
+ {
+ if (url != null)
+ return builder.Map(url, b => b.InternalUseMiddleware(configure));
+ else
+ return builder.InternalUseMiddleware(configure);
+ }
- return endpoints
- .Map(pattern, pipeline)
- .WithDisplayName(DefaultDisplayName);
+ #region Legacy methods without the configure action
+ ///
+ /// Starts a Prometheus metrics exporter using endpoint routing.
+ /// The default URL is /metrics, which is a Prometheus convention.
+ /// Use static methods on the class to create your metrics.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it.
+ public static IEndpointConventionBuilder MapMetrics(
+ this IEndpointRouteBuilder endpoints,
+ string pattern = "/metrics",
+ CollectorRegistry? registry = null
+ )
+ {
+ return MapMetrics(endpoints, LegacyConfigure(registry), pattern);
+ }
- }
+ ///
+ /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port.
+ /// The default URL is /metrics, which is a Prometheus convention.
+ /// Use static methods on the class to create your metrics.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it.
+ public static IApplicationBuilder UseMetricServer(
+ this IApplicationBuilder builder,
+ int port,
+ string? url = "/metrics",
+ CollectorRegistry? registry = null)
+ {
+ return UseMetricServer(builder, port, LegacyConfigure(registry), url);
+ }
-#endif
+ ///
+ /// Starts a Prometheus metrics exporter.
+ /// The default URL is /metrics, which is a Prometheus convention.
+ /// Use static methods on the class to create your metrics.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it.
+ public static IApplicationBuilder UseMetricServer(
+ this IApplicationBuilder builder,
+ string? url = "/metrics",
+ CollectorRegistry? registry = null)
+ {
+ return UseMetricServer(builder, LegacyConfigure(registry), url);
+ }
- ///
- /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port.
- /// The default URL is /metrics, which is a Prometheus convention.
- /// Use static methods on the class to create your metrics.
- ///
- public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, int port, string? url = "/metrics", CollectorRegistry? registry = null)
+ private static Action LegacyConfigure(CollectorRegistry? registry) =>
+ (MetricServerMiddleware.Settings settings) =>
{
- return builder
- .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(registry)));
-
- Func PortMatches()
- {
- return c => c.Connection.LocalPort == port;
- }
- }
+ settings.Registry = registry;
+ };
+ #endregion
- ///
- /// Starts a Prometheus metrics exporter.
- /// The default URL is /metrics, which is a Prometheus convention.
- /// Use static methods on the class to create your metrics.
- ///
- public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, string? url = "/metrics", CollectorRegistry? registry = null)
- {
- // If there is a URL to map, map it and re-enter without the URL.
- if (url != null)
- return builder.Map(url, b => b.InternalUseMiddleware(registry));
- else
- return builder.InternalUseMiddleware(registry);
- }
+ private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, Action configure)
+ {
+ var settings = new MetricServerMiddleware.Settings();
+ configure(settings);
- private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, CollectorRegistry? registry = null)
- {
- return builder.UseMiddleware(new MetricServerMiddleware.Settings
- {
- Registry = registry
- });
- }
+ return builder.UseMiddleware(settings);
}
}
diff --git a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj
index aa85ffeb..8a84ff63 100644
--- a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj
+++ b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj
@@ -1,51 +1,75 @@
-
- netstandard2.0;netcoreapp3.1
- Prometheus
-
- false
-
- true
-
- ..\Resources\prometheus-net.snk
-
- preview
- enable
-
- true
-
-
-
- bin\Release\netstandard2.0\Prometheus.AspNetCore.xml
- true
- 1701;1702;1705;1591
-
-
-
- bin\Debug\netstandard2.0\Prometheus.AspNetCore.xml
- true
- 1701;1702;1705;1591
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ net6.0
+ Prometheus
+
+ false
+
+ true
+ ..\Resources\prometheus-net.snk
+
+ enable
+ enable
+ True
+ True
+ 1591
+
+ latest
+ 9999
+
+ True
+
+
+ true
+ true
+
+
+ prometheus-net.AspNetCore
+ andrasm,qed-,lakario,sandersaares
+ prometheus-net
+ prometheus-net
+ ASP.NET Core middleware and stand-alone Kestrel server for exporting metrics to Prometheus
+ Copyright © prometheus-net developers
+ https://github.com/prometheus-net/prometheus-net
+ prometheus-net-logo.png
+ README.md
+ metrics prometheus aspnetcore
+ MIT
+ True
+ snupkg
+
+
+
+
+ true
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Prometheus.NetCore/DiagnosticSourceAdapter.cs b/Prometheus.NetCore/DiagnosticSourceAdapter.cs
deleted file mode 100644
index 098744f5..00000000
--- a/Prometheus.NetCore/DiagnosticSourceAdapter.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-
-namespace Prometheus
-{
- ///
- /// Monitors all DiagnosticSource events and exposes them as Prometheus counters.
- /// The event data is discarded, only the number of occurrences is measured.
- ///
- ///
- /// This is a very coarse data set due to lacking any intelligence on the payload.
- /// Users are recommended to make custom adapters with more detail for specific use cases.
- ///
- public sealed class DiagnosticSourceAdapter : IDisposable
- {
- ///
- /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics.
- /// Dispose of the return value to stop listening.
- ///
- public static IDisposable StartListening() => StartListening(DiagnosticSourceAdapterOptions.Default);
-
- ///
- /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics.
- /// Dispose of the return value to stop listening.
- ///
- public static IDisposable StartListening(DiagnosticSourceAdapterOptions options) => new DiagnosticSourceAdapter(options);
-
- private DiagnosticSourceAdapter(DiagnosticSourceAdapterOptions options)
- {
- _options = options;
- _metric = Metrics.WithCustomRegistry(options.Registry)
- .CreateCounter("diagnostic_events_total", "Total count of events received via the DiagnosticSource infrastructure.", new CounterConfiguration
- {
- LabelNames = new[]
- {
- "source", // Name of the DiagnosticSource
- "event" // Name of the event
- }
- });
-
- var newListenerObserver = new NewListenerObserver(OnNewListener);
- _newListenerSubscription = DiagnosticListener.AllListeners.Subscribe(newListenerObserver);
- }
-
- private readonly DiagnosticSourceAdapterOptions _options;
- private readonly Counter _metric;
-
- private readonly IDisposable _newListenerSubscription;
-
- // listener name -> subscription
- private readonly Dictionary _newEventSubscription = new Dictionary();
- private readonly object _newEventSubscriptionLock = new object();
-
- private void OnNewListener(DiagnosticListener listener)
- {
- lock (_newEventSubscriptionLock)
- {
- if (_newEventSubscription.TryGetValue(listener.Name, out var oldSubscription))
- {
- oldSubscription.Dispose();
- _newEventSubscription.Remove(listener.Name);
- }
-
- if (!_options.ListenerFilterPredicate(listener))
- return;
-
- var listenerName = listener.Name;
- var newEventObserver = new NewEventObserver(kvp => OnEvent(listenerName, kvp.Key, kvp.Value));
- _newEventSubscription[listenerName] = listener.Subscribe(newEventObserver);
- }
- }
-
- private void OnEvent(string listenerName, string eventName, object payload)
- {
- _metric.WithLabels(listenerName, eventName).Inc();
- }
-
- private sealed class NewListenerObserver : IObserver
- {
- private readonly Action _onNewListener;
-
- public NewListenerObserver(Action onNewListener)
- {
- _onNewListener = onNewListener;
- }
-
- public void OnCompleted()
- {
- }
-
- public void OnError(Exception error)
- {
- }
-
- public void OnNext(DiagnosticListener listener)
- {
- _onNewListener(listener);
- }
- }
-
- private sealed class NewEventObserver : IObserver>
- {
- private readonly Action> _onEvent;
-
- public NewEventObserver(Action> onEvent)
- {
- _onEvent = onEvent;
- }
-
- public void OnCompleted()
- {
- }
-
- public void OnError(Exception error)
- {
- }
-
- public void OnNext(KeyValuePair receivedEvent)
- {
- _onEvent(receivedEvent);
- }
- }
-
- public void Dispose()
- {
- _newListenerSubscription.Dispose();
-
- lock (_newEventSubscriptionLock)
- {
- foreach (var subscription in _newEventSubscription.Values)
- subscription.Dispose();
- }
- }
- }
-}
diff --git a/Prometheus.NetCore/DiagnosticSourceAdapterOptions.cs b/Prometheus.NetCore/DiagnosticSourceAdapterOptions.cs
deleted file mode 100644
index 2b910720..00000000
--- a/Prometheus.NetCore/DiagnosticSourceAdapterOptions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-using System.Diagnostics;
-
-namespace Prometheus
-{
- public sealed class DiagnosticSourceAdapterOptions
- {
- internal static readonly DiagnosticSourceAdapterOptions Default = new DiagnosticSourceAdapterOptions();
-
- ///
- /// By default we subscribe to all listeners but this allows you to filter by listener.
- ///
- public Func ListenerFilterPredicate = _ => true;
-
- public CollectorRegistry Registry = Metrics.DefaultRegistry;
- }
-}
diff --git a/Prometheus.NetCore/Prometheus.NetCore.csproj b/Prometheus.NetCore/Prometheus.NetCore.csproj
deleted file mode 100644
index 025735be..00000000
--- a/Prometheus.NetCore/Prometheus.NetCore.csproj
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
- netcoreapp2.1
-
- Prometheus
-
- false
-
- true
-
- ..\Resources\prometheus-net.snk
-
- preview
- enable
-
- true
- true
- 1701;1702;1705;1591
-
-
-
- bin\Release\netcoreapp2.1\Prometheus.NetCore.xml
-
-
-
- bin\Debug\netcoreapp2.1\Prometheus.NetCore.xml
-
-
-
-
-
-
-
-
-
-
diff --git a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs
index ff7a32df..a9d28c9d 100644
--- a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs
+++ b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs
@@ -1,67 +1,73 @@
-using System;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
+using System.Net;
using System.Web.Http;
-namespace Prometheus
+namespace Prometheus;
+
+public static class AspNetMetricServer
{
- public static class AspNetMetricServer
+ private const string RouteNamePrefix = "Prometheus_";
+
+ public sealed class Settings
{
- public sealed class Settings
- {
- public CollectorRegistry Registry { get; set; }
- }
+ public CollectorRegistry? Registry { get; set; }
+ }
- ///
- /// Registers an anonymous instance of the controller to be published on the /metrics URL.
- ///
- public static void RegisterRoutes(HttpConfiguration configuration, Settings settings = null)
- {
- if (configuration == null)
- throw new ArgumentNullException(nameof(configuration));
+ ///
+ /// Registers an anonymous instance of the controller to be published on the /metrics URL.
+ ///
+ public static void RegisterRoutes(HttpConfiguration configuration, Settings? settings = null) =>
+ MapRoute(configuration, "Default", "metrics", settings);
- configuration.Routes.MapHttpRoute(
- name: "Prometheus_Default",
- routeTemplate: "metrics",
- defaults: null,
- constraints: null,
- handler: new Handler(settings?.Registry ?? Metrics.DefaultRegistry));
- }
+ ///
+ /// Registers an anonymous instance of the controller to be published on a given URL path (e.g. "custom/metrics").
+ ///
+ public static void RegisterRoutes(HttpConfiguration configuration, string path, Settings? settings = null) =>
+ MapRoute(configuration, path, path, settings);
+
+ private static void MapRoute(HttpConfiguration configuration, string routeName, string routeTemplate, Settings? settings)
+ {
+ if (configuration == null)
+ throw new ArgumentNullException(nameof(configuration));
- private sealed class Handler : HttpMessageHandler
+ configuration.Routes.MapHttpRoute(
+ name: $"{RouteNamePrefix}{routeName}",
+ routeTemplate: routeTemplate,
+ defaults: null,
+ constraints: null,
+ handler: new Handler(settings?.Registry ?? Metrics.DefaultRegistry));
+ }
+
+ private sealed class Handler : HttpMessageHandler
+ {
+ public Handler(CollectorRegistry registry)
{
- public Handler(CollectorRegistry registry)
- {
- _registry = registry;
- }
+ _registry = registry;
+ }
- private readonly CollectorRegistry _registry;
+ private readonly CollectorRegistry _registry;
- protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- // The ASP.NET PushStreamContent does not have a way to easily handle exceptions that
- // occur before we write to the stream (when we can still return nice error headers).
- // Maybe in a future version this could be improved, as right now exception == aborted connection.
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ // The ASP.NET PushStreamContent does not have a way to easily handle exceptions that
+ // occur before we write to the stream (when we can still return nice error headers).
+ // Maybe in a future version this could be improved, as right now exception == aborted connection.
- var response = new HttpResponseMessage(HttpStatusCode.OK)
+ var response = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new PushStreamContent(async (stream, content, context) =>
{
- Content = new PushStreamContent(async (stream, content, context) =>
+ try
+ {
+ await _registry.CollectAndExportAsTextAsync(stream, ExpositionFormat.PrometheusText, cancellationToken);
+ }
+ finally
{
- try
- {
- await _registry.CollectAndExportAsTextAsync(stream, cancellationToken);
- }
- finally
- {
- stream.Close();
- }
- }, PrometheusConstants.ExporterContentTypeMinimal)
- };
+ stream.Close();
+ }
+ }, PrometheusConstants.ExporterContentTypeValue)
+ };
- return Task.FromResult(response);
- }
+ return Task.FromResult(response);
}
}
}
diff --git a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj
index c4911aab..cdd99c2e 100644
--- a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj
+++ b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj
@@ -1,74 +1,73 @@
-
-
-
-
- Debug
- AnyCPU
- {25C05E15-A8F8-4F36-A35C-4004C0948305}
- Library
- Properties
- Prometheus
- Prometheus.NetFramework.AspNet
- v4.6.1
- 512
- true
- true
- ..\Resources\prometheus-net.snk
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
- true
- true
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
- true
- true
-
-
-
- ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll
-
-
-
-
- ..\packages\Microsoft.AspNet.WebApi.Client.5.2.7\lib\net45\System.Net.Http.Formatting.dll
-
-
- ..\packages\Microsoft.AspNet.WebApi.Core.5.2.7\lib\net45\System.Web.Http.dll
-
-
-
-
-
-
-
-
-
-
- Properties\SolutionAssemblyInfo.cs
-
-
-
-
-
-
-
-
- {ba482927-f0bc-462c-8ba0-c476d93621bc}
- Prometheus.NetStandard
-
-
-
-
\ No newline at end of file
+
+
+
+ netframework462
+ Prometheus
+
+ false
+
+ true
+ ..\Resources\prometheus-net.snk
+
+ enable
+ enable
+ True
+ True
+ 1591
+
+ latest
+ 9999
+
+ True
+
+
+ true
+ true
+
+
+ prometheus-net.NetFramework.AspNet
+ sandersaares
+ prometheus-net
+ prometheus-net
+ ASP.NET Web API exporter for Prometheus
+ Copyright © prometheus-net developers
+ https://github.com/prometheus-net/prometheus-net
+ prometheus-net-logo.png
+ README.md
+ metrics prometheus aspnetcore
+ MIT
+ True
+ snupkg
+
+
+
+
+ true
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Prometheus.NetFramework.AspNet/Usings.cs b/Prometheus.NetFramework.AspNet/Usings.cs
new file mode 100644
index 00000000..e6a9eb40
--- /dev/null
+++ b/Prometheus.NetFramework.AspNet/Usings.cs
@@ -0,0 +1 @@
+global using System.Net.Http;
\ No newline at end of file
diff --git a/Prometheus.NetFramework.AspNet/packages.config b/Prometheus.NetFramework.AspNet/packages.config
deleted file mode 100644
index aeba5de5..00000000
--- a/Prometheus.NetFramework.AspNet/packages.config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Prometheus.NetStandard/ChildBase.cs b/Prometheus.NetStandard/ChildBase.cs
deleted file mode 100644
index b7b762f2..00000000
--- a/Prometheus.NetStandard/ChildBase.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// Base class for labeled instances of metrics (with all label names and label values defined).
- ///
- public abstract class ChildBase : ICollectorChild, IDisposable
- {
- internal ChildBase(Collector parent, Labels labels, Labels flattenedLabels, bool publish)
- {
- _parent = parent;
- Labels = labels;
- FlattenedLabels = flattenedLabels;
- _publish = publish;
- }
-
- ///
- /// Marks the metric as one to be published, even if it might otherwise be suppressed.
- ///
- /// This is useful for publishing zero-valued metrics once you have loaded data on startup and determined
- /// that there is no need to increment the value of the metric.
- ///
- ///
- /// Subclasses must call this when their value is first set, to mark the metric as published.
- ///
- public void Publish()
- {
- Volatile.Write(ref _publish, true);
- }
-
- ///
- /// Marks the metric as one to not be published.
- ///
- /// The metric will be published when Publish() is called or the value is updated.
- ///
- public void Unpublish()
- {
- Volatile.Write(ref _publish, false);
- }
-
- ///
- /// Removes this labeled instance from metrics.
- /// It will no longer be published and any existing measurements/buckets will be discarded.
- ///
- public void Remove()
- {
- _parent.RemoveLabelled(Labels);
- }
-
- public void Dispose() => Remove();
-
- ///
- /// Labels specific to this metric instance, without any inherited static labels.
- /// Internal for testing purposes only.
- ///
- internal Labels Labels { get; }
-
- ///
- /// All labels that materialize on this metric instance, including inherited static labels.
- /// Internal for testing purposes only.
- ///
- internal Labels FlattenedLabels { get; }
-
- private readonly Collector _parent;
-
- private bool _publish;
-
- ///
- /// Collects all the metric data rows from this collector and serializes it using the given serializer.
- ///
- ///
- /// Subclass must check _publish and suppress output if it is false.
- ///
- internal Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel)
- {
- if (!Volatile.Read(ref _publish))
- return Task.CompletedTask;
-
- return CollectAndSerializeImplAsync(serializer, cancel);
- }
-
- // Same as above, just only called if we really need to serialize this metric (if publish is true).
- private protected abstract Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel);
-
- ///
- /// Creates a metric identifier, with an optional name postfix and optional extra labels.
- /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"}
- ///
- protected byte[] CreateIdentifier(string? postfix = null, params (string, string)[] extraLabels)
- {
- var fullName = postfix != null ? $"{_parent.Name}_{postfix}" : _parent.Name;
-
- var labels = FlattenedLabels;
- if (extraLabels?.Length > 0)
- labels = FlattenedLabels.Concat(extraLabels);
-
- if (labels.Count != 0)
- return PrometheusConstants.ExportEncoding.GetBytes($"{fullName}{{{labels.Serialize()}}}");
- else
- return PrometheusConstants.ExportEncoding.GetBytes(fullName);
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/Collector.cs b/Prometheus.NetStandard/Collector.cs
deleted file mode 100644
index 87b1eb7c..00000000
--- a/Prometheus.NetStandard/Collector.cs
+++ /dev/null
@@ -1,211 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// Base class for metrics, defining the basic informative API and the internal API.
- ///
- public abstract class Collector
- {
- ///
- /// The metric name, e.g. http_requests_total.
- ///
- public string Name { get; }
-
- ///
- /// The help text describing the metric for a human audience.
- ///
- public string Help { get; }
-
- ///
- /// Names of the instance-specific labels (name-value pairs) that apply to this metric.
- /// When the values are added to the names, you get a instance.
- ///
- public string[] LabelNames { get; }
-
- internal abstract Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel);
-
- // Used by ChildBase.Remove()
- internal abstract void RemoveLabelled(Labels labels);
-
- private static readonly string[] EmptyLabelNames = new string[0];
-
- private const string ValidMetricNameExpression = "^[a-zA-Z_:][a-zA-Z0-9_:]*$";
- private const string ValidLabelNameExpression = "^[a-zA-Z_:][a-zA-Z0-9_:]*$";
- private const string ReservedLabelNameExpression = "^__.*$";
-
- private static readonly Regex MetricNameRegex = new Regex(ValidMetricNameExpression, RegexOptions.Compiled);
- private static readonly Regex LabelNameRegex = new Regex(ValidLabelNameExpression, RegexOptions.Compiled);
- private static readonly Regex ReservedLabelRegex = new Regex(ReservedLabelNameExpression, RegexOptions.Compiled);
-
- protected Collector(string name, string help, string[]? labelNames)
- {
- labelNames ??= EmptyLabelNames;
-
- if (!MetricNameRegex.IsMatch(name))
- throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'.");
-
- foreach (var labelName in labelNames)
- {
- if (labelName == null)
- throw new ArgumentNullException("Label name was null.");
-
- ValidateLabelName(labelName);
- }
-
- Name = name;
- Help = help;
- LabelNames = labelNames;
- }
-
- internal static void ValidateLabelName(string labelName)
- {
- if (!LabelNameRegex.IsMatch(labelName))
- throw new ArgumentException($"Label name '{labelName}' does not match regex '{ValidLabelNameExpression}'.");
-
- if (ReservedLabelRegex.IsMatch(labelName))
- throw new ArgumentException($"Label name '{labelName}' is not valid - labels starting with double underscore are reserved!");
- }
- }
-
- ///
- /// Base class for metrics collectors, providing common labeled child management functionality.
- ///
- public abstract class Collector : Collector, ICollector
- where TChild : ChildBase
- {
- private readonly ConcurrentDictionary _labelledMetrics = new ConcurrentDictionary();
-
- // Lazy-initialized since not every collector will use a child with no labels.
- private readonly Lazy _unlabelledLazy;
-
- ///
- /// Gets the child instance that has no labels.
- ///
- protected internal TChild Unlabelled => _unlabelledLazy.Value;
-
- // We need it for the ICollector interface but using this is rarely relevant in client code, so keep it obscured.
- TChild ICollector.Unlabelled => Unlabelled;
-
- // This servers a slightly silly but useful purpose: by default if you start typing .La... and trigger Intellisense
- // it will often for whatever reason focus on LabelNames instead of Labels, leading to tiny but persistent frustration.
- // Having WithLabels() instead eliminates the other candidate and allows for a frustration-free typing experience.
- public TChild WithLabels(params string[] labelValues) => Labels(labelValues);
-
- // Discourage it as it can create confusion. But it works fine, so no reason to mark it obsolete, really.
- [EditorBrowsable(EditorBrowsableState.Never)]
- public TChild Labels(params string[] labelValues)
- {
- var key = new Labels(LabelNames, labelValues);
- return GetOrAddLabelled(key);
- }
-
- public void RemoveLabelled(params string[] labelValues)
- {
- var key = new Labels(LabelNames, labelValues);
- _labelledMetrics.TryRemove(key, out _);
- }
-
- internal override void RemoveLabelled(Labels labels)
- {
- _labelledMetrics.TryRemove(labels, out _);
- }
-
- ///
- /// Gets the instance-specific label values of all labelled instances of the collector.
- /// Values of any inherited static labels are not returned in the result.
- ///
- /// Note that during concurrent operation, the set of values returned here
- /// may diverge from the latest set of values used by the collector.
- ///
- public IEnumerable GetAllLabelValues()
- {
- foreach (var labels in _labelledMetrics.Keys)
- {
- if (labels.Count == 0)
- continue; // We do not return the "unlabelled" label set.
-
- // Defensive copy.
- yield return labels.Values.ToArray();
- }
- }
-
- ///
- /// Set of static labels obtained from any hierarchy level (either defined in metric configuration or in registry).
- ///
- private readonly Labels _staticLabels;
-
- private TChild GetOrAddLabelled(Labels key)
- {
- // Don't allocate lambda for GetOrAdd in the common case that the labeled metrics exist.
- if (_labelledMetrics.TryGetValue(key, out var metric))
- return metric;
-
- return _labelledMetrics.GetOrAdd(key, k => NewChild(k, k.Concat(_staticLabels), publish: !_suppressInitialValue));
- }
-
- ///
- /// For tests that want to see what label values were used when metrics were created.
- ///
- internal Labels[] GetAllLabels() => _labelledMetrics.Select(p => p.Key).ToArray();
-
- internal Collector(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue)
- : base(name, help, labelNames)
- {
- _staticLabels = staticLabels;
- _suppressInitialValue = suppressInitialValue;
- _unlabelledLazy = new Lazy(() => GetOrAddLabelled(Prometheus.Labels.Empty));
-
- // Check for label name collisions.
- var allLabelNames = (labelNames ?? new string[0]).Concat(staticLabels.Names).ToList();
- if (allLabelNames.Count() != allLabelNames.Distinct(StringComparer.Ordinal).Count())
- throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", allLabelNames));
-
- _familyHeaderLines = new byte[][]
- {
- PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name} {help}"),
- PrometheusConstants.ExportEncoding.GetBytes($"# TYPE {name} {Type.ToString().ToLowerInvariant()}")
- };
- }
-
- ///
- /// Creates a new instance of the child collector type.
- ///
- private protected abstract TChild NewChild(Labels labels, Labels flattenedLabels, bool publish);
-
- private protected abstract MetricType Type { get; }
-
- private readonly byte[][] _familyHeaderLines;
-
- internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel)
- {
- EnsureUnlabelledMetricCreatedIfNoLabels();
-
- await serializer.WriteFamilyDeclarationAsync(_familyHeaderLines, cancel);
-
- foreach (var child in _labelledMetrics.Values)
- await child.CollectAndSerializeAsync(serializer, cancel);
- }
-
- private readonly bool _suppressInitialValue;
-
- private void EnsureUnlabelledMetricCreatedIfNoLabels()
- {
- // We want metrics to exist even with 0 values if they are supposed to be used without labels.
- // Labelled metrics are created when label values are assigned. However, as unlabelled metrics are lazy-created
- // (they might are optional if labels are used) we might lose them for cases where they really are desired.
-
- // If there are no label names then clearly this metric is supposed to be used unlabelled, so create it.
- // Otherwise, we allow unlabelled metrics to be used if the user explicitly does it but omit them by default.
- if (!_unlabelledLazy.IsValueCreated && !LabelNames.Any())
- GetOrAddLabelled(Prometheus.Labels.Empty);
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/CollectorRegistry.cs b/Prometheus.NetStandard/CollectorRegistry.cs
deleted file mode 100644
index f9aa04a4..00000000
--- a/Prometheus.NetStandard/CollectorRegistry.cs
+++ /dev/null
@@ -1,249 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// Maintains references to a set of collectors, from which data for metrics is collected at data export time.
- ///
- /// Use methods on the class to add metrics to a collector registry.
- ///
- ///
- /// To encourage good concurrency practices, registries are append-only. You can add things to them but not remove.
- /// If you wish to remove things from the registry, create a new registry with only the things you wish to keep.
- ///
- public sealed class CollectorRegistry : ICollectorRegistry
- {
- #region "Before collect" callbacks
- ///
- /// Registers an action to be called before metrics are collected.
- /// This enables you to do last-minute updates to metric values very near the time of collection.
- /// Callbacks will delay the metric collection, so do not make them too long or it may time out.
- ///
- /// The callback will be executed synchronously and should not take more than a few milliseconds.
- /// To execute longer-duration callbacks, register an asynchronous callback (Func<Task>).
- ///
- /// If the callback throws then the entire metric collection will fail.
- /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter.
- ///
- /// If multiple concurrent collections occur, the callback may be called multiple times concurrently.
- ///
- public void AddBeforeCollectCallback(Action callback)
- {
- if (callback == null)
- throw new ArgumentNullException(nameof(callback));
-
- _beforeCollectCallbacks.Add(callback);
- }
-
- ///
- /// Registers an action to be called before metrics are collected.
- /// This enables you to do last-minute updates to metric values very near the time of collection.
- /// Callbacks will delay the metric collection, so do not make them too long or it may time out.
- ///
- /// Asynchronous callbacks will be executed concurrently and may last longer than a few milliseconds.
- ///
- /// If the callback throws then the entire metric collection will fail.
- /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter.
- ///
- /// If multiple concurrent collections occur, the callback may be called multiple times concurrently.
- ///
- public void AddBeforeCollectCallback(Func callback)
- {
- if (callback == null)
- throw new ArgumentNullException(nameof(callback));
-
- _beforeCollectAsyncCallbacks.Add(callback);
- }
-
- private readonly ConcurrentBag _beforeCollectCallbacks = new ConcurrentBag();
- private readonly ConcurrentBag> _beforeCollectAsyncCallbacks = new ConcurrentBag>();
- #endregion
-
- #region Static labels
- ///
- /// The set of static labels that are applied to all metrics in this registry.
- /// Enumeration of the returned collection is thread-safe.
- ///
- public IEnumerable> StaticLabels => _staticLabels;
-
- ///
- /// Defines the set of static labels to apply to all metrics in this registry.
- /// The static labels can only be set once on startup, before adding or publishing any metrics.
- ///
- public void SetStaticLabels(Dictionary labels)
- {
- if (labels == null)
- throw new ArgumentNullException(nameof(labels));
-
- // Read lock is taken when creating metrics, so we know that no metrics can be created while we hold this lock.
- _staticLabelsLock.EnterWriteLock();
-
- try
- {
- if (_staticLabels.Length != 0)
- throw new InvalidOperationException("Static labels have already been defined - you can only do it once per registry.");
-
- if (_collectors.Count != 0)
- throw new InvalidOperationException("Metrics have already been added to the registry - cannot define static labels anymore.");
-
- // Keep the lock for the duration of this method to make sure no publishing happens while we are setting labels.
- lock (_firstCollectLock)
- {
- if (_hasPerformedFirstCollect)
- throw new InvalidOperationException("The metrics registry has already been published - cannot define static labels anymore.");
-
- foreach (var pair in labels)
- {
- if (pair.Key == null)
- throw new ArgumentException("The name of a label cannot be null.");
-
- if (pair.Value == null)
- throw new ArgumentException("The value of a label cannot be null.");
-
- Collector.ValidateLabelName(pair.Key);
- }
-
- _staticLabels = labels.ToArray();
- }
- }
- finally
- {
- _staticLabelsLock.ExitWriteLock();
- }
- }
-
- private KeyValuePair[] _staticLabels = new KeyValuePair[0];
- private readonly ReaderWriterLockSlim _staticLabelsLock = new ReaderWriterLockSlim();
-
- ///
- /// Executes an action while holding a read lock on the set of static labels (provided as parameter).
- ///
- internal TReturn WhileReadingStaticLabels(Func action)
- {
- _staticLabelsLock.EnterReadLock();
-
- try
- {
- var labels = new Labels(_staticLabels.Select(item => item.Key).ToArray(), _staticLabels.Select(item => item.Value).ToArray());
-
- return action(labels);
- }
- finally
- {
- _staticLabelsLock.ExitReadLock();
- }
- }
- #endregion
-
- ///
- /// Collects all metrics and exports them in text document format to the provided stream.
- ///
- /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer.
- ///
- public Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default)
- {
- if (to == null)
- throw new ArgumentNullException(nameof(to));
-
- return CollectAndSerializeAsync(new TextSerializer(to), cancel);
- }
-
-
- // We pass this thing to GetOrAdd to avoid allocating a collector or a closure.
- // This reduces memory usage in situations where the collector is already registered.
- internal readonly struct CollectorInitializer
- where TCollector : Collector
- where TConfiguration : MetricConfiguration
- {
- private readonly Func _createInstance;
- private readonly string _name;
- private readonly string _help;
- private readonly TConfiguration _configuration;
-
- public string Name => _name;
- public TConfiguration Configuration => _configuration;
-
- public CollectorInitializer(Func createInstance,
- string name, string help, TConfiguration configuration)
- {
- _createInstance = createInstance;
- _name = name;
- _help = help;
- _configuration = configuration;
- }
-
- public TCollector CreateInstance(string _) => _createInstance(_name, _help, _configuration);
- }
-
- ///
- /// Adds a collector to the registry, returning an existing instance if one with a matching name was already registered.
- ///
- internal TCollector GetOrAdd(in CollectorInitializer initializer)
- where TCollector : Collector
- where TConfiguration : MetricConfiguration
- {
- var collectorToUse = _collectors.GetOrAdd(initializer.Name, initializer.CreateInstance);
-
- if (!(collectorToUse is TCollector))
- throw new InvalidOperationException("Collector of a different type with the same name is already registered.");
-
- if ((initializer.Configuration.LabelNames?.Length ?? 0) != collectorToUse.LabelNames.Length
- || (!initializer.Configuration.LabelNames?.SequenceEqual(collectorToUse.LabelNames) ?? false))
- throw new InvalidOperationException("Collector matches a previous registration but has a different set of label names.");
-
- return (TCollector)collectorToUse;
- }
-
- private readonly ConcurrentDictionary _collectors = new ConcurrentDictionary();
-
- internal void SetBeforeFirstCollectCallback(Action a)
- {
- lock (_firstCollectLock)
- {
- if (_hasPerformedFirstCollect)
- return; // Avoid keeping a reference to a callback we won't ever use.
-
- _beforeFirstCollectCallback = a;
- }
- }
-
- ///
- /// Allows us to initialize (or not) the registry with the default metrics before the first collection.
- ///
- private Action? _beforeFirstCollectCallback;
- private bool _hasPerformedFirstCollect;
- private readonly object _firstCollectLock = new object();
-
- ///
- /// Collects metrics from all the registered collectors and sends them to the specified serializer.
- ///
- internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel)
- {
- lock (_firstCollectLock)
- {
- if (!_hasPerformedFirstCollect)
- {
- _hasPerformedFirstCollect = true;
- _beforeFirstCollectCallback?.Invoke();
- _beforeFirstCollectCallback = null;
- }
- }
-
- foreach (var callback in _beforeCollectCallbacks)
- callback();
-
- await Task.WhenAll(_beforeCollectAsyncCallbacks.Select(callback => callback(cancel)));
-
- foreach (var collector in _collectors.Values)
- await collector.CollectAndSerializeAsync(serializer, cancel);
-
- await serializer.FlushAsync(cancel);
- }
- }
-}
diff --git a/Prometheus.NetStandard/Counter.cs b/Prometheus.NetStandard/Counter.cs
deleted file mode 100644
index ed84eef3..00000000
--- a/Prometheus.NetStandard/Counter.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- public sealed class Counter : Collector, ICounter
- {
- public sealed class Child : ChildBase, ICounter
- {
- internal Child(Collector parent, Labels labels, Labels flattenedLabels, bool publish)
- : base(parent, labels, flattenedLabels, publish)
- {
- _identifier = CreateIdentifier();
- }
-
- private readonly byte[] _identifier;
-
- private ThreadSafeDouble _value;
-
- private protected override Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel)
- {
- return serializer.WriteMetricAsync(_identifier, Value, cancel);
- }
-
- public void Inc(double increment = 1.0)
- {
- if (increment < 0.0)
- throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease.");
-
- _value.Add(increment);
- Publish();
- }
-
- public void IncTo(double targetValue)
- {
- _value.IncrementTo(targetValue);
- Publish();
- }
-
- public double Value => _value.Value;
- }
-
- private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish)
- {
- return new Child(this, labels, flattenedLabels, publish);
- }
-
- internal Counter(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue)
- : base(name, help, labelNames, staticLabels, suppressInitialValue)
- {
- }
-
- public void Inc(double increment = 1) => Unlabelled.Inc(increment);
- public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue);
- public double Value => Unlabelled.Value;
-
- public void Publish() => Unlabelled.Publish();
- public void Unpublish() => Unlabelled.Unpublish();
-
- private protected override MetricType Type => MetricType.Counter;
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/CounterConfiguration.cs b/Prometheus.NetStandard/CounterConfiguration.cs
deleted file mode 100644
index 2cbc42b7..00000000
--- a/Prometheus.NetStandard/CounterConfiguration.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Prometheus
-{
- public sealed class CounterConfiguration : MetricConfiguration
- {
- internal static readonly CounterConfiguration Default = new CounterConfiguration();
- }
-}
diff --git a/Prometheus.NetStandard/CounterExtensions.cs b/Prometheus.NetStandard/CounterExtensions.cs
deleted file mode 100644
index 0d410fe5..00000000
--- a/Prometheus.NetStandard/CounterExtensions.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- public static class CounterExtensions
- {
- ///
- /// Increments the value of the counter to the current UTC time as a Unix timestamp in seconds.
- /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds.
- /// Operation is ignored if the current value is already greater.
- ///
- public static void IncToCurrentTimeUtc(this ICounter counter)
- {
- counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow));
- }
-
- ///
- /// Increments the value of the counter to a specific moment as the UTC Unix timestamp in seconds.
- /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds.
- /// Operation is ignored if the current value is already greater.
- ///
- public static void IncToTimeUtc(this ICounter counter, DateTimeOffset timestamp)
- {
- counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp));
- }
-
- ///
- /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown.
- /// If an exception filter is specified, only counts exceptions for which the filter returns true.
- ///
- public static void CountExceptions(this ICounter counter, Action wrapped, Func? exceptionFilter = null)
- {
- if (counter == null)
- throw new ArgumentNullException(nameof(counter));
-
- if (wrapped == null)
- throw new ArgumentNullException(nameof(wrapped));
-
- try
- {
- wrapped();
- }
- catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex))
- {
- counter.Inc();
- throw;
- }
- }
-
- ///
- /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown.
- /// If an exception filter is specified, only counts exceptions for which the filter returns true.
- ///
- public static TResult CountExceptions(this ICounter counter, Func wrapped, Func? exceptionFilter = null)
- {
- if (counter == null)
- throw new ArgumentNullException(nameof(counter));
-
- if (wrapped == null)
- throw new ArgumentNullException(nameof(wrapped));
-
- try
- {
- return wrapped();
- }
- catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex))
- {
- counter.Inc();
- throw;
- }
- }
-
- ///
- /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown.
- /// If an exception filter is specified, only counts exceptions for which the filter returns true.
- ///
- public static async Task CountExceptionsAsync(this ICounter counter, Func wrapped, Func? exceptionFilter = null)
- {
- if (counter == null)
- throw new ArgumentNullException(nameof(counter));
-
- if (wrapped == null)
- throw new ArgumentNullException(nameof(wrapped));
-
- try
- {
- await wrapped().ConfigureAwait(false);
- }
- catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex))
- {
- counter.Inc();
- throw;
- }
- }
-
- ///
- /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown.
- /// If an exception filter is specified, only counts exceptions for which the filter returns true.
- ///
- public static async Task CountExceptionsAsync(this ICounter counter, Func> wrapped, Func? exceptionFilter = null)
- {
- if (counter == null)
- throw new ArgumentNullException(nameof(counter));
-
- if (wrapped == null)
- throw new ArgumentNullException(nameof(wrapped));
-
- try
- {
- return await wrapped().ConfigureAwait(false);
- }
- catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex))
- {
- counter.Inc();
- throw;
- }
- }
- }
-}
diff --git a/Prometheus.NetStandard/DelegatingStreamInternal.cs b/Prometheus.NetStandard/DelegatingStreamInternal.cs
deleted file mode 100644
index 24406006..00000000
--- a/Prometheus.NetStandard/DelegatingStreamInternal.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-// Modified for prometheus-net to reduce dependencies.
-
-#nullable enable
-using System.Diagnostics;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace System.Net.Http
-{
- // Forwards all calls to an inner stream except where overridden in a derived class.
- internal abstract class DelegatingStreamInternal : Stream
- {
- private readonly Stream _innerStream;
-
- #region Properties
-
- public override bool CanRead
- {
- get { return _innerStream.CanRead; }
- }
-
- public override bool CanSeek
- {
- get { return _innerStream.CanSeek; }
- }
-
- public override bool CanWrite
- {
- get { return _innerStream.CanWrite; }
- }
-
- public override long Length
- {
- get { return _innerStream.Length; }
- }
-
- public override long Position
- {
- get { return _innerStream.Position; }
- set { _innerStream.Position = value; }
- }
-
- public override int ReadTimeout
- {
- get { return _innerStream.ReadTimeout; }
- set { _innerStream.ReadTimeout = value; }
- }
-
- public override bool CanTimeout
- {
- get { return _innerStream.CanTimeout; }
- }
-
- public override int WriteTimeout
- {
- get { return _innerStream.WriteTimeout; }
- set { _innerStream.WriteTimeout = value; }
- }
-
- #endregion Properties
-
- protected DelegatingStreamInternal(Stream innerStream)
- {
- _innerStream = innerStream;
- }
-
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- _innerStream.Dispose();
- }
- base.Dispose(disposing);
- }
-
-
- public override long Seek(long offset, SeekOrigin origin)
- {
- return _innerStream.Seek(offset, origin);
- }
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- return _innerStream.Read(buffer, offset, count);
- }
-
- public override int ReadByte()
- {
- return _innerStream.ReadByte();
- }
-
- public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- {
- return _innerStream.ReadAsync(buffer, offset, count, cancellationToken);
- }
- public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
- {
- return _innerStream.BeginRead(buffer, offset, count, callback, state);
- }
-
- public override int EndRead(IAsyncResult asyncResult)
- {
- return _innerStream.EndRead(asyncResult);
- }
-
- public override void Flush()
- {
- _innerStream.Flush();
- }
-
- public override Task FlushAsync(CancellationToken cancellationToken)
- {
- return _innerStream.FlushAsync(cancellationToken);
- }
-
- public override void SetLength(long value)
- {
- _innerStream.SetLength(value);
- }
-
- public override void Write(byte[] buffer, int offset, int count)
- {
- _innerStream.Write(buffer, offset, count);
- }
-
- public override void WriteByte(byte value)
- {
- _innerStream.WriteByte(value);
- }
-
- public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- {
- return _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
- }
-
-
- public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state)
- {
- return _innerStream.BeginWrite(buffer, offset, count, callback, state);
- }
-
- public override void EndWrite(IAsyncResult asyncResult)
- {
- _innerStream.EndWrite(asyncResult);
- }
-
- public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
- {
- return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken);
- }
- }
-}
diff --git a/Prometheus.NetStandard/DotNetStats.cs b/Prometheus.NetStandard/DotNetStats.cs
deleted file mode 100644
index c2d3b761..00000000
--- a/Prometheus.NetStandard/DotNetStats.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-
-namespace Prometheus
-{
- ///
- /// Collects basic .NET metrics about the current process. This is not meant to be an especially serious collector,
- /// more of a producer of sample data so users of the library see something when they install it.
- ///
- public sealed class DotNetStats
- {
- ///
- /// Registers the .NET metrics in the specified registry.
- ///
- public static void Register(CollectorRegistry registry)
- {
- var instance = new DotNetStats(registry);
- registry.AddBeforeCollectCallback(instance.UpdateMetrics);
- }
-
- private readonly Process _process;
- private readonly List _collectionCounts = new List();
- private Gauge _totalMemory;
- private Gauge _virtualMemorySize;
- private Gauge _workingSet;
- private Gauge _privateMemorySize;
- private Counter _cpuTotal;
- private Gauge _openHandles;
- private Gauge _startTime;
- private Gauge _numThreads;
-
- private DotNetStats(CollectorRegistry registry)
- {
- _process = Process.GetCurrentProcess();
- var metrics = Metrics.WithCustomRegistry(registry);
-
- var collectionCountsParent = metrics.CreateCounter("dotnet_collection_count_total", "GC collection count", new[] { "generation" });
-
- for (var gen = 0; gen <= GC.MaxGeneration; gen++)
- {
- _collectionCounts.Add(collectionCountsParent.Labels(gen.ToString()));
- }
-
- // Metrics that make sense to compare between all operating systems
- // Note that old versions of pushgateway errored out if different metrics had same name but different help string.
- // This is fixed in newer versions but keep the help text synchronized with the Go implementation just in case.
- // See https://github.com/prometheus/pushgateway/issues/194
- // and https://github.com/prometheus-net/prometheus-net/issues/89
- _startTime = metrics.CreateGauge("process_start_time_seconds", "Start time of the process since unix epoch in seconds.");
- _cpuTotal = metrics.CreateCounter("process_cpu_seconds_total", "Total user and system CPU time spent in seconds.");
-
- _virtualMemorySize = metrics.CreateGauge("process_virtual_memory_bytes", "Virtual memory size in bytes.");
- _workingSet = metrics.CreateGauge("process_working_set_bytes", "Process working set");
- _privateMemorySize = metrics.CreateGauge("process_private_memory_bytes", "Process private memory size");
- _openHandles = metrics.CreateGauge("process_open_handles", "Number of open handles");
- _numThreads = metrics.CreateGauge("process_num_threads", "Total number of threads");
-
- // .net specific metrics
- _totalMemory = metrics.CreateGauge("dotnet_total_memory_bytes", "Total known allocated memory");
-
- var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
- _startTime.Set((_process.StartTime.ToUniversalTime() - epoch).TotalSeconds);
- }
-
- // The Process class is not thread-safe so let's synchronize the updates to avoid data tearing.
- private readonly object _updateLock = new object();
-
- private void UpdateMetrics()
- {
- try
- {
- lock (_updateLock)
- {
- _process.Refresh();
-
- for (var gen = 0; gen <= GC.MaxGeneration; gen++)
- {
- var collectionCount = _collectionCounts[gen];
- collectionCount.Inc(GC.CollectionCount(gen) - collectionCount.Value);
- }
-
- _totalMemory.Set(GC.GetTotalMemory(false));
- _virtualMemorySize.Set(_process.VirtualMemorySize64);
- _workingSet.Set(_process.WorkingSet64);
- _privateMemorySize.Set(_process.PrivateMemorySize64);
- _cpuTotal.Inc(Math.Max(0, _process.TotalProcessorTime.TotalSeconds - _cpuTotal.Value));
- _openHandles.Set(_process.HandleCount);
- _numThreads.Set(_process.Threads.Count);
- }
- }
- catch (Exception)
- {
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/Gauge.cs b/Prometheus.NetStandard/Gauge.cs
deleted file mode 100644
index 7b852cfa..00000000
--- a/Prometheus.NetStandard/Gauge.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- public sealed class Gauge : Collector, IGauge
- {
- public sealed class Child : ChildBase, IGauge
- {
- internal Child(Collector parent, Labels labels, Labels flattenedLabels, bool publish)
- : base(parent, labels, flattenedLabels, publish)
- {
- _identifier = CreateIdentifier();
- }
-
- private readonly byte[] _identifier;
-
- private ThreadSafeDouble _value;
-
- private protected override Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel)
- {
- return serializer.WriteMetricAsync(_identifier, Value, cancel);
- }
-
- public void Inc(double increment = 1)
- {
- _value.Add(increment);
- Publish();
- }
-
- public void Set(double val)
- {
- _value.Value = val;
- Publish();
- }
-
- public void Dec(double decrement = 1)
- {
- Inc(-decrement);
- }
-
- public void IncTo(double targetValue)
- {
- _value.IncrementTo(targetValue);
- Publish();
- }
-
- public void DecTo(double targetValue)
- {
- _value.DecrementTo(targetValue);
- Publish();
- }
-
- public double Value => _value.Value;
- }
-
- private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish)
- {
- return new Child(this, labels, flattenedLabels, publish);
- }
-
- internal Gauge(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue)
- : base(name, help, labelNames, staticLabels, suppressInitialValue)
- {
- }
-
- public void Inc(double increment = 1) => Unlabelled.Inc(increment);
- public void Set(double val) => Unlabelled.Set(val);
- public void Dec(double decrement = 1) => Unlabelled.Dec(decrement);
- public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue);
- public void DecTo(double targetValue) => Unlabelled.DecTo(targetValue);
- public double Value => Unlabelled.Value;
- public void Publish() => Unlabelled.Publish();
- public void Unpublish() => Unlabelled.Unpublish();
-
- private protected override MetricType Type => MetricType.Gauge;
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/GaugeConfiguration.cs b/Prometheus.NetStandard/GaugeConfiguration.cs
deleted file mode 100644
index ca732340..00000000
--- a/Prometheus.NetStandard/GaugeConfiguration.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Prometheus
-{
- public sealed class GaugeConfiguration : MetricConfiguration
- {
- internal static readonly GaugeConfiguration Default = new GaugeConfiguration();
- }
-}
diff --git a/Prometheus.NetStandard/GaugeExtensions.cs b/Prometheus.NetStandard/GaugeExtensions.cs
deleted file mode 100644
index 146794bf..00000000
--- a/Prometheus.NetStandard/GaugeExtensions.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System;
-
-namespace Prometheus
-{
- public static class GaugeExtensions
- {
- ///
- /// Sets the value of the gauge to the current UTC time as a Unix timestamp in seconds.
- /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds.
- ///
- public static void SetToCurrentTimeUtc(this IGauge gauge)
- {
- gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow));
- }
-
- ///
- /// Sets the value of the gauge to a specific moment as the UTC timezone Unix timestamp in seconds.
- /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds.
- ///
- public static void SetToTimeUtc(this IGauge gauge, DateTimeOffset timestamp)
- {
- gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp));
- }
-
- ///
- /// Increments the value of the gauge to the current UTC time as a Unix timestamp in seconds.
- /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds.
- /// Operation is ignored if the current value is already greater.
- ///
- public static void IncToCurrentTimeUtc(this IGauge gauge)
- {
- gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow));
- }
-
- ///
- /// Increments the value of the gauge to a specific moment as the UTC Unix timestamp in seconds.
- /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds.
- /// Operation is ignored if the current value is already greater.
- ///
- public static void IncToTimeUtc(this IGauge gauge, DateTimeOffset timestamp)
- {
- gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp));
- }
-
- private sealed class InProgressTracker : IDisposable
- {
- public InProgressTracker(IGauge gauge)
- {
- _gauge = gauge;
- }
-
- public void Dispose()
- {
- _gauge.Dec();
- }
-
- private readonly IGauge _gauge;
- }
-
- ///
- /// Tracks the number of in-progress operations taking place.
- ///
- /// Calling this increments the gauge. Disposing of the returned instance decrements it again.
- ///
- ///
- /// It is safe to track the sum of multiple concurrent in-progress operations with the same gauge.
- ///
- public static IDisposable TrackInProgress(this IGauge gauge)
- {
- if (gauge == null)
- throw new ArgumentNullException(nameof(gauge));
-
- gauge.Inc();
-
- return new InProgressTracker(gauge);
- }
- }
-}
diff --git a/Prometheus.NetStandard/Histogram.cs b/Prometheus.NetStandard/Histogram.cs
deleted file mode 100644
index 1f8eae2b..00000000
--- a/Prometheus.NetStandard/Histogram.cs
+++ /dev/null
@@ -1,256 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// The histogram is thread-safe but not atomic - the sum of values and total count of events
- /// may not add up perfectly with bucket contents if new observations are made during a collection.
- ///
- public sealed class Histogram : Collector, IHistogram
- {
- private static readonly double[] DefaultBuckets = { .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10 };
- private readonly double[] _buckets;
-
- internal Histogram(string name, string help, string[]? labelNames, Labels staticLabels, bool suppressInitialValue, double[]? buckets)
- : base(name, help, labelNames, staticLabels, suppressInitialValue)
- {
- if (labelNames?.Any(l => l == "le") == true)
- {
- throw new ArgumentException("'le' is a reserved label name");
- }
- _buckets = buckets ?? DefaultBuckets;
-
- if (_buckets.Length == 0)
- {
- throw new ArgumentException("Histogram must have at least one bucket");
- }
-
- if (!double.IsPositiveInfinity(_buckets[_buckets.Length - 1]))
- {
- _buckets = _buckets.Concat(new[] { double.PositiveInfinity }).ToArray();
- }
-
- for (int i = 1; i < _buckets.Length; i++)
- {
- if (_buckets[i] <= _buckets[i - 1])
- {
- throw new ArgumentException("Bucket values must be increasing");
- }
- }
- }
-
- private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish)
- {
- return new Child(this, labels, flattenedLabels, publish);
- }
-
- public sealed class Child : ChildBase, IHistogram
- {
- internal Child(Histogram parent, Labels labels, Labels flattenedLabels, bool publish)
- : base(parent, labels, flattenedLabels, publish)
- {
- _parent = parent;
-
- _upperBounds = _parent._buckets;
- _bucketCounts = new ThreadSafeLong[_upperBounds.Length];
-
- _sumIdentifier = CreateIdentifier("sum");
- _countIdentifier = CreateIdentifier("count");
-
- _bucketIdentifiers = new byte[_upperBounds.Length][];
- for (var i = 0; i < _upperBounds.Length; i++)
- {
- var value = double.IsPositiveInfinity(_upperBounds[i]) ? "+Inf" : _upperBounds[i].ToString(CultureInfo.InvariantCulture);
-
- _bucketIdentifiers[i] = CreateIdentifier("bucket", ("le", value));
- }
- }
-
- private readonly Histogram _parent;
-
- private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D);
- private readonly ThreadSafeLong[] _bucketCounts;
- private readonly double[] _upperBounds;
-
- internal readonly byte[] _sumIdentifier;
- internal readonly byte[] _countIdentifier;
- internal readonly byte[][] _bucketIdentifiers;
-
- private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel)
- {
- // We output sum.
- // We output count.
- // We output each bucket in order of increasing upper bound.
-
- await serializer.WriteMetricAsync(_sumIdentifier, _sum.Value, cancel);
- await serializer.WriteMetricAsync(_countIdentifier, _bucketCounts.Sum(b => b.Value), cancel);
-
- var cumulativeCount = 0L;
-
- for (var i = 0; i < _bucketCounts.Length; i++)
- {
- cumulativeCount += _bucketCounts[i].Value;
-
- await serializer.WriteMetricAsync(_bucketIdentifiers[i], cumulativeCount, cancel);
- }
- }
-
- public double Sum => _sum.Value;
- public long Count => _bucketCounts.Sum(b => b.Value);
-
- public void Observe(double val) => Observe(val, 1);
-
- public void Observe(double val, long count)
- {
- if (double.IsNaN(val))
- {
- return;
- }
-
- for (int i = 0; i < _upperBounds.Length; i++)
- {
- if (val <= _upperBounds[i])
- {
- _bucketCounts[i].Add(count);
- break;
- }
- }
- _sum.Add(val * count);
- Publish();
- }
- }
-
- private protected override MetricType Type => MetricType.Histogram;
-
- public double Sum => Unlabelled.Sum;
- public long Count => Unlabelled.Count;
- public void Observe(double val) => Unlabelled.Observe(val, 1);
- public void Observe(double val, long count) => Unlabelled.Observe(val, count);
- public void Publish() => Unlabelled.Publish();
- public void Unpublish() => Unlabelled.Unpublish();
-
- // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go
- ///
- /// Creates '' buckets, where the lowest bucket has an
- /// upper bound of '' and each following bucket's upper bound is ''
- /// times the previous bucket's upper bound.
- ///
- /// The function throws if '' is 0 or negative, if '' is 0 or negative,
- /// or if '' is less than or equal 1.
- ///
- /// The upper bound of the lowest bucket. Must be positive.
- /// The factor to increase the upper bound of subsequent buckets. Must be greater than 1.
- /// The number of buckets to create. Must be positive.
- public static double[] ExponentialBuckets(double start, double factor, int count)
- {
- if (count <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(count)}");
- if (start <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(start)}");
- if (factor <= 1) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a {nameof(factor)} greater than 1");
-
- // The math we do can make it incur some tiny avoidable error due to floating point gremlins.
- // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double.
- // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot.
-
- var next = (decimal)start;
- var buckets = new double[count];
-
- for (var i = 0; i < buckets.Length; i++)
- {
- buckets[i] = (double)next;
- next *= (decimal)factor;
- }
-
- return buckets;
- }
-
- // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go
- ///
- /// Creates '' buckets, where the lowest bucket has an
- /// upper bound of '' and each following bucket's upper bound is the upper bound of the
- /// previous bucket, incremented by ''
- ///
- /// The function throws if '' is 0 or negative.
- ///
- /// The upper bound of the lowest bucket.
- /// The width of each bucket (distance between lower and upper bound).
- /// The number of buckets to create. Must be positive.
- public static double[] LinearBuckets(double start, double width, int count)
- {
- if (count <= 0) throw new ArgumentException($"{nameof(LinearBuckets)} needs a positive {nameof(count)}");
-
- // The math we do can make it incur some tiny avoidable error due to floating point gremlins.
- // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double.
- // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot.
-
- var next = (decimal)start;
- var buckets = new double[count];
-
- for (var i = 0; i < buckets.Length; i++)
- {
- buckets[i] = (double)next;
- next += (decimal)width;
- }
-
- return buckets;
- }
-
- ///
- /// Divides each power of 10 into N divisions.
- ///
- /// The starting range includes 10 raised to this power.
- /// The ranges end with 10 raised to this power (this no longer starts a new range).
- /// How many divisions to divide each range into.
- ///
- /// For example, with startPower=-1, endPower=2, divisions=4 we would get:
- /// 10^-1 == 0.1 which defines our starting range, giving buckets: 0.25, 0.5, 0.75, 1.0
- /// 10^0 == 1 which is the next range, giving buckets: 2.5, 5, 7.5, 10
- /// 10^1 == 10 which is the next range, giving buckets: 25, 50, 75, 100
- /// 10^2 == 100 which is the end and the top level of the preceding range.
- /// Giving total buckets: 0.25, 0.5, 0.75, 1.0, 2.5, 5, 7.5, 10, 25, 50, 75, 100
- ///
- public static double[] PowersOfTenDividedBuckets(int startPower, int endPower, int divisions)
- {
- if (startPower >= endPower)
- throw new ArgumentException($"{nameof(startPower)} must be less than {nameof(endPower)}.", nameof(startPower));
-
- if (divisions <= 0)
- throw new ArgumentOutOfRangeException($"{nameof(divisions)} must be a positive integer.", nameof(divisions));
-
- var buckets = new List();
-
- for (var powerOfTen = startPower; powerOfTen < endPower; powerOfTen++)
- {
- // This gives us the upper bound (the start of the next range).
- var max = (decimal)Math.Pow(10, powerOfTen + 1);
-
- // Then we just divide it into N divisions and we are done!
- for (var division = 0; division < divisions; division++)
- {
- var bucket = max / divisions * (division + 1);
-
- // The math we do can make it incur some tiny avoidable error due to floating point gremlins.
- // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double.
- // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot.
- var candidate = (double)bucket;
-
- // Depending on the number of divisions, it may be that divisions from different powers overlap.
- // For example, a division into 20 would include:
- // 19th value in the 0th power: 9.5 (10/20*19=9.5)
- // 1st value in the 1st power: 5 (100/20*1 = 5)
- // To avoid this being a problem, we simply constrain all values to be increasing.
- if (buckets.Any() && buckets.Last() >= candidate)
- continue; // Skip this one, it is not greater.
-
- buckets.Add(candidate);
- }
- }
-
- return buckets.ToArray();
- }
- }
-}
diff --git a/Prometheus.NetStandard/HistogramConfiguration.cs b/Prometheus.NetStandard/HistogramConfiguration.cs
deleted file mode 100644
index 893c499d..00000000
--- a/Prometheus.NetStandard/HistogramConfiguration.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Prometheus
-{
- public sealed class HistogramConfiguration : MetricConfiguration
- {
- internal static readonly HistogramConfiguration Default = new HistogramConfiguration();
-
- ///
- /// Custom histogram buckets to use. If null, will use Histogram.DefaultBuckets.
- ///
- public double[]? Buckets { get; set; }
- }
-}
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs
deleted file mode 100644
index 587980c6..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs
+++ /dev/null
@@ -1,111 +0,0 @@
-using System;
-using System.Linq;
-using System.Net.Http;
-
-namespace Prometheus.HttpClientMetrics
-{
- ///
- /// This base class performs the data management necessary to associate the correct labels and values
- /// with HttpClient metrics, depending on the options the user has provided for the HttpClient metric handler.
- ///
- /// The following labels are supported:
- /// 'method' (HTTP request method)
- /// 'host' (The host name of HTTP request)
- /// 'client' (The name of the HttpClient)
- /// 'code' (HTTP response status code)
- ///
- internal abstract class HttpClientDelegatingHandlerBase : DelegatingHandler
- where TCollector : class, ICollector
- where TChild : class, ICollectorChild
- {
- ///
- /// The set of labels from among the defaults that this metric supports.
- ///
- protected abstract string[] DefaultLabels { get; }
-
- ///
- /// The factory to use for creating the default metric for this middleware.
- /// Not used if a custom metric is already provided in options.
- ///
- protected MetricFactory MetricFactory { get; }
-
- ///
- /// Creates the default metric instance with the specified set of labels.
- /// Only used if the caller does not provide a custom metric instance in the options.
- ///
- protected abstract TCollector CreateMetricInstance(string[] labelNames);
-
- // Internal only for tests.
- internal readonly TCollector _metric;
-
- protected HttpClientDelegatingHandlerBase(HttpClientMetricsOptionsBase? options, TCollector? customMetric, HttpClientIdentity identity)
- {
- _identity = identity;
-
- MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry);
-
- if (customMetric != null)
- {
- _metric = customMetric;
-
- ValidateNoUnexpectedLabelNames();
- }
- else
- {
- _metric = CreateMetricInstance(HttpClientRequestLabelNames.All);
- }
- }
-
- private readonly HttpClientIdentity _identity;
-
- ///
- /// Creates the metric child instance to use for measurements.
- ///
- ///
- /// Internal for testing purposes.
- ///
- protected internal TChild CreateChild(HttpRequestMessage request, HttpResponseMessage? response)
- {
- if (!_metric.LabelNames.Any())
- return _metric.Unlabelled;
-
- var labelValues = new string[_metric.LabelNames.Length];
-
- for (var i = 0; i < labelValues.Length; i++)
- {
- switch (_metric.LabelNames[i])
- {
- case HttpClientRequestLabelNames.Method:
- labelValues[i] = request.Method.Method;
- break;
- case HttpClientRequestLabelNames.Host:
- labelValues[i] = request.RequestUri.Host;
- break;
- case HttpClientRequestLabelNames.Client:
- labelValues[i] = _identity.Name;
- break;
- case HttpClientRequestLabelNames.Code:
- labelValues[i] = response != null ? ((int)response.StatusCode).ToString() : "";
- break;
- default:
- // We validate the label set on initialization, so this is impossible.
- throw new NotSupportedException($"Found unsupported label on metric: {_metric.LabelNames[i]}");
- }
- }
-
- return _metric.WithLabels(labelValues);
- }
-
- ///
- /// If we use a custom metric, it should not have labels that are not among the defaults.
- ///
- private void ValidateNoUnexpectedLabelNames()
- {
- var allowedLabels = HttpClientRequestLabelNames.All;
- var unexpected = _metric.LabelNames.Except(allowedLabels);
-
- if (unexpected.Any())
- throw new ArgumentException($"Provided custom HttpClient metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}.");
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientExporterOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientExporterOptions.cs
deleted file mode 100644
index 6f58df20..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientExporterOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- public sealed class HttpClientExporterOptions
- {
- public HttpClientInProgressOptions InProgress { get; set; } = new HttpClientInProgressOptions();
- public HttpClientRequestCountOptions RequestCount { get; set; } = new HttpClientRequestCountOptions();
- public HttpClientRequestDurationOptions RequestDuration { get; set; } = new HttpClientRequestDurationOptions();
- public HttpClientResponseDurationOptions ResponseDuration { get; set; } = new HttpClientResponseDurationOptions();
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientIdentity.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientIdentity.cs
deleted file mode 100644
index 595e1ebe..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientIdentity.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- public sealed class HttpClientIdentity
- {
- public static readonly HttpClientIdentity Default = new HttpClientIdentity("default");
-
- public string Name { get; }
-
- public HttpClientIdentity(string name)
- {
- Name = name;
- }
- }
-}
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressHandler.cs
deleted file mode 100644
index 292b62c5..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressHandler.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus.HttpClientMetrics
-{
- internal sealed class HttpClientInProgressHandler : HttpClientDelegatingHandlerBase, IGauge>
- {
- public HttpClientInProgressHandler(HttpClientInProgressOptions? options, HttpClientIdentity identity)
- : base(options, options?.Gauge, identity)
- {
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- using (CreateChild(request, null).TrackInProgress())
- {
- return await base.SendAsync(request, cancellationToken);
- }
- }
-
- protected override string[] DefaultLabels => HttpClientRequestLabelNames.KnownInAdvance;
-
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge(
- "httpclient_requests_in_progress",
- "Number of requests currently being executed by an HttpClient.",
- new GaugeConfiguration
- {
- LabelNames = labelNames
- });
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressOptions.cs
deleted file mode 100644
index 68853edb..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientInProgressOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- public sealed class HttpClientInProgressOptions : HttpClientMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Gauge { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientMetricsOptionsBase.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientMetricsOptionsBase.cs
deleted file mode 100644
index 95192635..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientMetricsOptionsBase.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- public abstract class HttpClientMetricsOptionsBase
- {
- public bool Enabled { get; set; } = true;
-
- ///
- /// Allows you to override the registry used to create the default metric instance.
- /// Value is ignored if you specify a custom metric instance in the options.
- ///
- public CollectorRegistry? Registry { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountHandler.cs
deleted file mode 100644
index 409f0299..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountHandler.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus.HttpClientMetrics
-{
- internal sealed class HttpClientRequestCountHandler : HttpClientDelegatingHandlerBase, ICounter>
- {
- public HttpClientRequestCountHandler(HttpClientRequestCountOptions? options, HttpClientIdentity identity)
- : base(options, options?.Counter, identity)
- {
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- HttpResponseMessage? response = null;
-
- try
- {
- response = await base.SendAsync(request, cancellationToken);
- return response;
- }
- finally
- {
- CreateChild(request, response).Inc();
- }
- }
-
- protected override string[] DefaultLabels => HttpClientRequestLabelNames.All;
-
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter(
- "httpclient_requests_sent_total",
- "Count of HTTP requests that have been completed by an HttpClient.",
- new CounterConfiguration
- {
- LabelNames = labelNames
- });
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountOptions.cs
deleted file mode 100644
index 5985c2fb..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestCountOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- public sealed class HttpClientRequestCountOptions : HttpClientMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Counter { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationHandler.cs
deleted file mode 100644
index ddec4e7f..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationHandler.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus.HttpClientMetrics
-{
- internal sealed class HttpClientRequestDurationHandler : HttpClientDelegatingHandlerBase, IHistogram>
- {
- public HttpClientRequestDurationHandler(HttpClientRequestDurationOptions? options, HttpClientIdentity identity)
- : base(options, options?.Histogram, identity)
- {
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var stopWatch = ValueStopwatch.StartNew();
-
- HttpResponseMessage? response = null;
-
- try
- {
- // We measure until SendAsync returns - which is when the response HEADERS are seen.
- // The response body may continue streaming for a long time afterwards, which this does not measure.
- response = await base.SendAsync(request, cancellationToken);
- return response;
- }
- finally
- {
- CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds);
- }
- }
-
- protected override string[] DefaultLabels => HttpClientRequestLabelNames.All;
-
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram(
- "httpclient_request_duration_seconds",
- "Duration histogram of HTTP requests performed by an HttpClient.",
- new HistogramConfiguration
- {
- // 1 ms to 32K ms buckets
- Buckets = Histogram.ExponentialBuckets(0.001, 2, 16),
- LabelNames = labelNames
- });
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationOptions.cs
deleted file mode 100644
index 8177e968..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestDurationOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- public sealed class HttpClientRequestDurationOptions : HttpClientMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Histogram { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestLabelNames.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestLabelNames.cs
deleted file mode 100644
index a06a119d..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientRequestLabelNames.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- ///
- /// Label names reserved for the use by the HttpClient metrics.
- ///
- public static class HttpClientRequestLabelNames
- {
- public const string Method = "method";
- public const string Host = "host";
- public const string Client = "client";
- public const string Code = "code";
-
- public static readonly string[] All =
- {
- Method,
- Host,
- Client,
- Code
- };
-
- // The labels known before receiving the response.
- // Everything except the response status code, basically.
- public static readonly string[] KnownInAdvance =
- {
- Method,
- Host,
- Client
- };
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationHandler.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationHandler.cs
deleted file mode 100644
index ff235638..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationHandler.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System;
-using System.IO;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus.HttpClientMetrics
-{
- internal sealed class HttpClientResponseDurationHandler : HttpClientDelegatingHandlerBase, IHistogram>
- {
- public HttpClientResponseDurationHandler(HttpClientResponseDurationOptions? options, HttpClientIdentity identity)
- : base(options, options?.Histogram, identity)
- {
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var stopWatch = ValueStopwatch.StartNew();
-
- var response = await base.SendAsync(request, cancellationToken);
-
- Stream oldStream = await response.Content.ReadAsStreamAsync();
-
- Wrap(response, oldStream, delegate
- {
- CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds);
- });
-
- return response;
- }
-
- protected override string[] DefaultLabels => HttpClientRequestLabelNames.All;
-
- protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram(
- "httpclient_response_duration_seconds",
- "Duration histogram of HTTP requests performed by an HttpClient, measuring the duration until the HTTP response finished being processed.",
- new HistogramConfiguration
- {
- // 1 ms to 32K ms buckets
- Buckets = Histogram.ExponentialBuckets(0.001, 2, 16),
- LabelNames = labelNames
- });
-
- private void Wrap(HttpResponseMessage response, Stream oldStream, Action onEndOfStream)
- {
- var newContent = new StreamContent(new EndOfStreamDetectingStream(oldStream, onEndOfStream));
-
- var oldHeaders = response.Content.Headers;
- var newHeaders = newContent.Headers;
-
-#if NET6_0_OR_GREATER
- foreach (KeyValuePair header in oldHeaders.NonValidated)
- {
- if (header.Value.Count > 1)
- {
- newHeaders.TryAddWithoutValidation(header.Key, header.Value);
- }
- else
- {
- newHeaders.TryAddWithoutValidation(header.Key, header.Value.ToString());
- }
- }
-#else
- foreach (var header in oldHeaders)
- {
- newHeaders.TryAddWithoutValidation(header.Key, header.Value);
- }
-#endif
-
- response.Content = newContent;
- }
-
- private sealed class EndOfStreamDetectingStream : Stream
- {
- public EndOfStreamDetectingStream(Stream inner, Action onEndOfStream)
- {
- _inner = inner;
- _onEndOfStream = onEndOfStream;
- }
-
- private readonly Stream _inner;
- private readonly Action _onEndOfStream;
- private int _sawEndOfStream = 0;
-
- public override void Flush() => _inner.Flush();
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- var read = _inner.Read(buffer, offset, count);
-
- if (read == 0 && buffer.Length != 0)
- {
- SignalCompletion();
- }
-
- return read;
- }
-
- public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- {
- return buffer.Length == 0
- ? _inner.ReadAsync(buffer, offset, count, cancellationToken)
- : ReadAsyncCore(this, _inner.ReadAsync(buffer, offset, count, cancellationToken));
-
- static async Task ReadAsyncCore(EndOfStreamDetectingStream stream, Task readTask)
- {
- int read = await readTask;
-
- if (read == 0)
- {
- stream.SignalCompletion();
- }
-
- return read;
- }
- }
-
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- SignalCompletion();
-
- _inner.Dispose();
- }
- }
-
- private void SignalCompletion()
- {
- if (Interlocked.Exchange(ref _sawEndOfStream, 1) == 0)
- {
- _onEndOfStream();
- }
- }
-
- public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
- public override void SetLength(long value) => _inner.SetLength(value);
- public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count);
- public override bool CanRead => _inner.CanRead;
- public override bool CanSeek => _inner.CanSeek;
- public override bool CanWrite => _inner.CanWrite;
- public override long Length => _inner.Length;
- public override long Position { get => _inner.Position; set => _inner.Position = value; }
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationOptions.cs b/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationOptions.cs
deleted file mode 100644
index 0bd2c68f..00000000
--- a/Prometheus.NetStandard/HttpClientMetrics/HttpClientResponseDurationOptions.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Prometheus.HttpClientMetrics
-{
- public sealed class HttpClientResponseDurationOptions : HttpClientMetricsOptionsBase
- {
- ///
- /// Set this to use a custom metric instead of the default.
- ///
- public ICollector? Histogram { get; set; }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/HttpClientMetricsExtensions.cs b/Prometheus.NetStandard/HttpClientMetricsExtensions.cs
deleted file mode 100644
index 2abcbfd4..00000000
--- a/Prometheus.NetStandard/HttpClientMetricsExtensions.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using Microsoft.Extensions.DependencyInjection;
-using Prometheus.HttpClientMetrics;
-using System;
-
-namespace Prometheus
-{
- public static class HttpClientMetricsExtensions
- {
- ///
- /// Configures the HttpClient pipeline to collect Prometheus metrics.
- ///
- public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, Action configure)
- {
- var options = new HttpClientExporterOptions();
-
- configure?.Invoke(options);
-
- builder.UseHttpClientMetrics(options);
-
- return builder;
- }
-
- ///
- /// Configures the HttpClient pipeline to collect Prometheus metrics.
- ///
- public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, HttpClientExporterOptions? options = null)
- {
- options ??= new HttpClientExporterOptions();
-
- var identity = new HttpClientIdentity(builder.Name);
-
- if (options.InProgress.Enabled)
- {
- builder = builder.AddHttpMessageHandler(x => new HttpClientInProgressHandler(options.InProgress, identity));
- }
-
- if (options.RequestCount.Enabled)
- {
- builder = builder.AddHttpMessageHandler(x => new HttpClientRequestCountHandler(options.RequestCount, identity));
- }
-
- if (options.RequestDuration.Enabled)
- {
- builder = builder.AddHttpMessageHandler(x => new HttpClientRequestDurationHandler(options.RequestDuration, identity));
- }
-
- if (options.ResponseDuration.Enabled)
- {
- builder = builder.AddHttpMessageHandler(x => new HttpClientResponseDurationHandler(options.ResponseDuration, identity));
- }
-
- return builder;
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/ICollector.cs b/Prometheus.NetStandard/ICollector.cs
deleted file mode 100644
index 011f2c49..00000000
--- a/Prometheus.NetStandard/ICollector.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// Child-type-specific interface implemented by all collectors, used to enable substitution in test code.
- ///
- public interface ICollector : ICollector
- where TChild : ICollectorChild
- {
- TChild Unlabelled { get; }
- TChild WithLabels(params string[] labelValues);
- }
-
- ///
- /// Interface implemented by all collectors, used to enable substitution in test code.
- ///
- public interface ICollector
- {
- string Name { get; }
- string Help { get; }
- string[] LabelNames { get; }
- }
-}
diff --git a/Prometheus.NetStandard/ICollectorChild.cs b/Prometheus.NetStandard/ICollectorChild.cs
deleted file mode 100644
index 422ea653..00000000
--- a/Prometheus.NetStandard/ICollectorChild.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Prometheus
-{
- ///
- /// Interface shared by all labelled collector children.
- ///
- public interface ICollectorChild
- {
- }
-}
diff --git a/Prometheus.NetStandard/ICollectorRegistry.cs b/Prometheus.NetStandard/ICollectorRegistry.cs
deleted file mode 100644
index a9a5f176..00000000
--- a/Prometheus.NetStandard/ICollectorRegistry.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// Allows for substitution of CollectorRegistry in tests.
- /// Not used by prometheus-net itself - you cannot provide your own implementation to prometheus-net code, only to your own code.
- ///
- public interface ICollectorRegistry
- {
- void AddBeforeCollectCallback(Action callback);
- void AddBeforeCollectCallback(Func callback);
-
- IEnumerable> StaticLabels { get; }
- void SetStaticLabels(Dictionary labels);
-
- Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default);
- }
-}
diff --git a/Prometheus.NetStandard/ICounter.cs b/Prometheus.NetStandard/ICounter.cs
deleted file mode 100644
index 8803d4be..00000000
--- a/Prometheus.NetStandard/ICounter.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Prometheus
-{
- public interface ICounter : ICollectorChild
- {
- void Inc(double increment = 1);
- void IncTo(double targetValue);
- double Value { get; }
- }
-}
diff --git a/Prometheus.NetStandard/IGauge.cs b/Prometheus.NetStandard/IGauge.cs
deleted file mode 100644
index ac5c50fd..00000000
--- a/Prometheus.NetStandard/IGauge.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Prometheus
-{
- public interface IGauge : ICollectorChild
- {
- void Inc(double increment = 1);
- void Set(double val);
- void Dec(double decrement = 1);
- void IncTo(double targetValue);
- void DecTo(double targetValue);
- double Value { get; }
- }
-}
diff --git a/Prometheus.NetStandard/IHistogram.cs b/Prometheus.NetStandard/IHistogram.cs
deleted file mode 100644
index 83c88dc9..00000000
--- a/Prometheus.NetStandard/IHistogram.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Prometheus
-{
- public interface IHistogram : IObserver
- {
- ///
- /// Observe multiple events with a given value.
- ///
- /// Intended to support high frequency or batch processing use cases utilizing pre-aggregation.
- ///
- /// Measured value.
- /// Number of observations with this value.
- void Observe(double val, long count);
-
- ///
- /// Gets the sum of all observed events.
- ///
- double Sum { get; }
-
- ///
- /// Gets the count of all observed events.
- ///
- long Count { get; }
- }
-}
diff --git a/Prometheus.NetStandard/IMetricFactory.cs b/Prometheus.NetStandard/IMetricFactory.cs
deleted file mode 100644
index e4522638..00000000
--- a/Prometheus.NetStandard/IMetricFactory.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Prometheus
-{
- ///
- /// Allows for substitution of MetricFactory in tests.
- /// Not used by prometheus-net itself - you cannot provide your own implementation to prometheus-net code, only to your own code.
- ///
- public interface IMetricFactory
- {
- Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null);
- Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null);
- Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null);
- Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null);
- }
-}
diff --git a/Prometheus.NetStandard/IMetricServer.cs b/Prometheus.NetStandard/IMetricServer.cs
deleted file mode 100644
index 40c85e67..00000000
--- a/Prometheus.NetStandard/IMetricServer.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// A metric server exposes a Prometheus metric exporter endpoint in the background,
- /// operating independently and serving metrics until it is instructed to stop.
- ///
- public interface IMetricServer : IDisposable
- {
- ///
- /// Starts serving metrics.
- ///
- /// Returns the same instance that was called (for fluent-API-style chaining).
- ///
- IMetricServer Start();
-
- ///
- /// Instructs the metric server to stop and returns a task you can await for it to stop.
- ///
- Task StopAsync();
-
- ///
- /// Instructs the metric server to stop and waits for it to stop.
- ///
- void Stop();
- }
-}
diff --git a/Prometheus.NetStandard/IMetricsSerializer.cs b/Prometheus.NetStandard/IMetricsSerializer.cs
deleted file mode 100644
index 469f3a6e..00000000
--- a/Prometheus.NetStandard/IMetricsSerializer.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// The only purpose this serves is to warn the developer when he might be accidentally introducing
- /// new serialization-time relationships. The serialization code is very tied to the text format and
- /// not intended to be a generic serialization mechanism.
- ///
- internal interface IMetricsSerializer
- {
- ///
- /// Writes the lines that declare the metric family.
- ///
- Task WriteFamilyDeclarationAsync(byte[][] headerLines, CancellationToken cancel);
-
- ///
- /// Writes a single metric in a metric family.
- ///
- Task WriteMetricAsync(byte[] identifier, double value, CancellationToken cancel);
-
- ///
- /// Flushes any pending buffers. Always call this after all your write calls.
- ///
- Task FlushAsync(CancellationToken cancel);
- }
-}
diff --git a/Prometheus.NetStandard/IObserver.cs b/Prometheus.NetStandard/IObserver.cs
deleted file mode 100644
index 70b70393..00000000
--- a/Prometheus.NetStandard/IObserver.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Prometheus
-{
- ///
- /// Implemented by metric types that observe individual events with specific values.
- ///
- public interface IObserver : ICollectorChild
- {
- ///
- /// Observes a single event with the given value.
- ///
- void Observe(double val);
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/ISummary.cs b/Prometheus.NetStandard/ISummary.cs
deleted file mode 100644
index aeed5840..00000000
--- a/Prometheus.NetStandard/ISummary.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Prometheus
-{
- public interface ISummary : IObserver
- {
- }
-}
diff --git a/Prometheus.NetStandard/ITimer.cs b/Prometheus.NetStandard/ITimer.cs
deleted file mode 100644
index b87894f1..00000000
--- a/Prometheus.NetStandard/ITimer.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using System;
-
-namespace Prometheus
-{
- ///
- /// A timer that can be used to observe a duration of elapsed time.
- ///
- /// The observation is made either when ObserveDuration is called or when the instance is disposed of.
- ///
- public interface ITimer : IDisposable
- {
- ///
- /// Observes the duration (in seconds) and returns the observed value.
- ///
- TimeSpan ObserveDuration();
- }
-}
diff --git a/Prometheus.NetStandard/Labels.cs b/Prometheus.NetStandard/Labels.cs
deleted file mode 100644
index 31846bc1..00000000
--- a/Prometheus.NetStandard/Labels.cs
+++ /dev/null
@@ -1,136 +0,0 @@
-using System;
-using System.Linq;
-
-namespace Prometheus
-{
- ///
- /// The set of labels and label values associated with a metric. Used both for export and as keys.
- ///
- ///
- /// Only the values are considered for equality purposes - the caller must ensure that
- /// LabelValues objects with different sets of names are never compared to each other.
- ///
- /// Always use the explicit constructor when creating an instance. This is a struct in order
- /// to reduce heap allocations when dealing with labelled metrics, which has the consequence of
- /// adding a default parameterless constructor. It should not be used.
- ///
- internal struct Labels : IEquatable
- {
- public static readonly Labels Empty = new Labels(new string[0], new string[0]);
-
- public int Count => Names.Length;
-
- public string[] Values { get; }
- public string[] Names { get; }
-
- private readonly int _hashCode;
-
- public Labels(string[] names, string[] values)
- {
- if (names == null)
- throw new ArgumentNullException(nameof(names));
-
- if (values == null)
- throw new ArgumentNullException(nameof(values));
-
- if (names.Length != values.Length)
- throw new ArgumentException("The list of label values must have the same number of elements as the list of label names.");
-
- foreach (var lv in values)
- {
- if (lv == null)
- throw new ArgumentNullException("A label value cannot be null.");
- }
-
- Values = values;
- Names = names;
-
- // Calculating the hash code is fast but we don't need to re-calculate it for each comparison.
- // Labels are fixed - calculate it once up-front and remember the value.
- _hashCode = CalculateHashCode(Values);
- }
-
- public Labels Concat(params (string, string)[] more)
- {
- var moreLabels = new Labels(more.Select(pair => pair.Item1).ToArray(), more.Select(pair => pair.Item2).ToArray());
-
- return Concat(moreLabels);
- }
-
- public Labels Concat(Labels more)
- {
- var allNames = Names.Concat(more.Names).ToArray();
- var allValues = Values.Concat(more.Values).ToArray();
-
- if (allNames.Length != allNames.Distinct(StringComparer.Ordinal).Count())
- throw new InvalidOperationException("The metric instance received multiple copies of the same label.");
-
- return new Labels(allNames, allValues);
- }
-
- private static string EscapeLabelValue(string value)
- {
- return value
- .Replace("\\", @"\\")
- .Replace("\n", @"\n")
- .Replace("\"", @"\""");
- }
-
- ///
- /// Serializes to the labelkey1="labelvalue1",labelkey2="labelvalue2" label string.
- ///
- public string Serialize()
- {
- // Result is cached in child collector - no need to worry about efficiency here.
-
- var labels = Names
- .Zip(Values, (name, value) => $"{name}=\"{EscapeLabelValue(value)}\"");
-
- return string.Join(",", labels);
- }
-
- public bool Equals(Labels other)
- {
- if (_hashCode != other._hashCode) return false;
- if (other.Values.Length != Values.Length) return false;
-
- for (int i = 0; i < Values.Length; i++)
- {
- if (!string.Equals(Values[i], other.Values[i], StringComparison.Ordinal))
- return false;
- }
-
- return true;
- }
-
- public override bool Equals(object obj)
- {
- if (!(obj is Labels))
- {
- return false;
- }
-
- var other = (Labels)obj;
- return Equals(other);
- }
-
- public override int GetHashCode()
- {
- return _hashCode;
- }
-
- private static int CalculateHashCode(string[] values)
- {
- unchecked
- {
- int hashCode = 0;
- for (int i = 0; i < values.Length; i++)
- {
- hashCode ^= (values[i].GetHashCode() * 397);
- }
-
- return hashCode;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/MetricConfiguration.cs b/Prometheus.NetStandard/MetricConfiguration.cs
deleted file mode 100644
index a60d681b..00000000
--- a/Prometheus.NetStandard/MetricConfiguration.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Collections.Generic;
-
-namespace Prometheus
-{
- ///
- /// This class packages the options for creating metrics into a single class (with subclasses per metric type)
- /// for easy extensibility of the API without adding numerous method overloads whenever new options are added.
- ///
- public abstract class MetricConfiguration
- {
- ///
- /// Names of all the label fields that are defined for each instance of the metric.
- /// If null, the metric will be created without any instance-specific labels.
- ///
- /// Before using a metric that uses instance-specific labels, .WithLabels() must be called to provide values for the labels.
- ///
- public string[]? LabelNames { get; set; }
-
- ///
- /// The static labels to apply to all instances of this metric. These labels cannot be later overwritten.
- ///
- public Dictionary? StaticLabels { get; set; }
-
- ///
- /// If true, the metric will not be published until its value is first modified (regardless of the specific value).
- /// This is useful to delay publishing gauges that get their initial values delay-loaded.
- ///
- /// By default, metrics are published as soon as possible - if they do not use labels then they are published on
- /// creation and if they use labels then as soon as the label values are assigned.
- ///
- public bool SuppressInitialValue { get; set; }
- }
-}
diff --git a/Prometheus.NetStandard/MetricFactory.cs b/Prometheus.NetStandard/MetricFactory.cs
deleted file mode 100644
index 0a3b2358..00000000
--- a/Prometheus.NetStandard/MetricFactory.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System;
-using System.Linq;
-
-namespace Prometheus
-{
- ///
- /// Adds metrics to a registry.
- ///
- public sealed class MetricFactory : IMetricFactory
- {
- private readonly CollectorRegistry _registry;
-
- internal MetricFactory(CollectorRegistry registry)
- {
- _registry = registry ?? throw new ArgumentNullException(nameof(registry));
- }
-
- private Labels CreateStaticLabels(MetricConfiguration metricConfiguration)
- {
- return _registry.WhileReadingStaticLabels(registryLabels =>
- {
- if (metricConfiguration.StaticLabels == null)
- return registryLabels;
-
- var metricLabels = new Labels(metricConfiguration.StaticLabels.Keys.ToArray(), metricConfiguration.StaticLabels.Values.ToArray());
-
- return metricLabels.Concat(registryLabels);
- });
- }
-
- ///
- /// Counters only increase in value and reset to zero when the process restarts.
- ///
- public Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null)
- {
- return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer(
- (n, h, config) => new Counter(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue),
- name, help, configuration ?? CounterConfiguration.Default));
- }
-
- ///
- /// Gauges can have any numeric value and change arbitrarily.
- ///
- public Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null)
- {
- return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer(
- (n, h, config) => new Gauge(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue),
- name, help, configuration ?? GaugeConfiguration.Default));
- }
-
- ///
- /// Summaries track the trends in events over time (10 minutes by default).
- ///
- public Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null)
- {
- return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer(
- (n, h, config) => new Summary(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue, config.Objectives, config.MaxAge, config.AgeBuckets, config.BufferSize),
- name, help, configuration ?? SummaryConfiguration.Default));
- }
-
- ///
- /// Histograms track the size and number of events in buckets.
- ///
- public Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null)
- {
- return _registry.GetOrAdd(new CollectorRegistry.CollectorInitializer(
- (n, h, config) => new Histogram(n, h, config.LabelNames, CreateStaticLabels(config), config.SuppressInitialValue, config.Buckets),
- name, help, configuration ?? HistogramConfiguration.Default));
- }
-
- ///
- /// Counters only increase in value and reset to zero when the process restarts.
- ///
- public Counter CreateCounter(string name, string help, params string[] labelNames) =>
- CreateCounter(name, help, new CounterConfiguration
- {
- LabelNames = labelNames
- });
-
- ///
- /// Gauges can have any numeric value and change arbitrarily.
- ///
- public Gauge CreateGauge(string name, string help, params string[] labelNames) =>
- CreateGauge(name, help, new GaugeConfiguration
- {
- LabelNames = labelNames
- });
-
- ///
- /// Summaries track the trends in events over time (10 minutes by default).
- ///
- public Summary CreateSummary(string name, string help, params string[] labelNames) =>
- CreateSummary(name, help, new SummaryConfiguration
- {
- LabelNames = labelNames
- });
-
- ///
- /// Histograms track the size and number of events in buckets.
- ///
- public Histogram CreateHistogram(string name, string help, params string[] labelNames) =>
- CreateHistogram(name, help, new HistogramConfiguration
- {
- LabelNames = labelNames
- });
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/MetricHandler.cs b/Prometheus.NetStandard/MetricHandler.cs
deleted file mode 100644
index 249233d7..00000000
--- a/Prometheus.NetStandard/MetricHandler.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// Base class for various metric server implementations that start an independent exporter in the background.
- /// The expoters may either be pull-based (exposing the Prometheus API) or push-based (actively pushing to PushGateway).
- ///
- public abstract class MetricHandler : IMetricServer, IDisposable
- {
- // The registry that contains the collectors to export metrics from.
- // Subclasses are expected to use this variable to obtain the correct registry.
- protected readonly CollectorRegistry _registry;
-
- // The token is cancelled when the handler is instructed to stop.
- private CancellationTokenSource? _cts = new CancellationTokenSource();
-
- // This is the task started for the purpose of exporting metrics.
- private Task? _task;
-
- protected MetricHandler(CollectorRegistry? registry = null)
- {
- _registry = registry ?? Metrics.DefaultRegistry;
- }
-
- public IMetricServer Start()
- {
- if (_task != null)
- throw new InvalidOperationException("The metric server has already been started.");
-
- if (_cts == null)
- throw new InvalidOperationException("The metric server has already been started and stopped. Create a new server if you want to start it again.");
-
- _task = StartServer(_cts.Token);
- return this;
- }
-
- public async Task StopAsync()
- {
- // Signal the CTS to give a hint to the server thread that it is time to close up shop.
- _cts?.Cancel();
-
- try
- {
- if (_task == null)
- return; // Never started.
-
- // This will re-throw any exception that was caught on the StartServerAsync thread.
- // Perhaps not ideal behavior but hey, if the implementation does not want this to happen
- // it should have caught it itself in the background processing thread.
- await _task.ConfigureAwait(false); // Issue #308
- }
- catch (OperationCanceledException)
- {
- // We'll eat this one, though, since it can easily get thrown by whatever checks the CancellationToken.
- }
- finally
- {
- _cts?.Dispose();
- _cts = null;
- }
- }
-
- public void Stop()
- {
- // This method mainly exists for API compatiblity with prometheus-net v1. But it works, so that's fine.
- StopAsync().GetAwaiter().GetResult();
- }
-
- void IDisposable.Dispose()
- {
- Stop();
- }
-
- protected abstract Task StartServer(CancellationToken cancel);
- }
-}
diff --git a/Prometheus.NetStandard/MetricPusher.cs b/Prometheus.NetStandard/MetricPusher.cs
deleted file mode 100644
index 5da436c0..00000000
--- a/Prometheus.NetStandard/MetricPusher.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// A metric server that regularly pushes metrics to a Prometheus PushGateway.
- ///
- public class MetricPusher : MetricHandler
- {
- private readonly TimeSpan _pushInterval;
- private readonly Uri _targetUrl;
- private readonly Func _httpClientProvider;
-
- public MetricPusher(string endpoint, string job, string? instance = null, long intervalMilliseconds = 1000, IEnumerable>? additionalLabels = null, CollectorRegistry? registry = null) : this(new MetricPusherOptions
- {
- Endpoint = endpoint,
- Job = job,
- Instance = instance,
- IntervalMilliseconds = intervalMilliseconds,
- AdditionalLabels = additionalLabels,
- Registry = registry
- })
- {
- }
-
- public MetricPusher(MetricPusherOptions options) : base(options.Registry)
- {
- if (string.IsNullOrEmpty(options.Endpoint))
- throw new ArgumentNullException(nameof(options.Endpoint));
-
- if (string.IsNullOrEmpty(options.Job))
- throw new ArgumentNullException(nameof(options.Job));
-
- if (options.IntervalMilliseconds <= 0)
- throw new ArgumentException("Interval must be greater than zero", nameof(options.IntervalMilliseconds));
-
- _httpClientProvider = options.HttpClientProvider ?? (() => _singletonHttpClient);
-
- StringBuilder sb = new StringBuilder(string.Format("{0}/job/{1}", options.Endpoint!.TrimEnd('/'), options.Job));
- if (!string.IsNullOrEmpty(options.Instance))
- sb.AppendFormat("/instance/{0}", options.Instance);
-
- if (options.AdditionalLabels != null)
- {
- foreach (var pair in options.AdditionalLabels)
- {
- if (pair == null || string.IsNullOrEmpty(pair.Item1) || string.IsNullOrEmpty(pair.Item2))
- throw new NotSupportedException($"Invalid {nameof(MetricPusher)} additional label: ({pair?.Item1}):({pair?.Item2})");
-
- sb.AppendFormat("/{0}/{1}", pair.Item1, pair.Item2);
- }
- }
-
- if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out _targetUrl))
- {
- throw new ArgumentException("Endpoint must be a valid url", "endpoint");
- }
-
- _pushInterval = TimeSpan.FromMilliseconds(options.IntervalMilliseconds);
- _onError = options.OnError;
- }
-
- private static readonly MediaTypeHeaderValue ContentTypeHeaderValue = new MediaTypeHeaderValue(PrometheusConstants.ExporterContentTypeMinimal);
-
- private static readonly HttpClient _singletonHttpClient = new HttpClient();
-
- private readonly Action? _onError;
-
- protected override Task StartServer(CancellationToken cancel)
- {
- // Start the server processing loop asynchronously in the background.
- return Task.Run(async delegate
- {
- while (true)
- {
- // We schedule approximately at the configured interval. There may be some small accumulation for the
- // part of the loop we do not measure but it is close enough to be acceptable for all practical scenarios.
- var duration = ValueStopwatch.StartNew();
-
- try
- {
- var httpClient = _httpClientProvider();
-
- // We use a copy-pasted implementation of PushStreamContent here to avoid taking a dependency on the old ASP.NET Web API where it lives.
- var response = await httpClient.PostAsync(_targetUrl, new PushStreamContentInternal(async (stream, content, context) =>
- {
- try
- {
- // Do not pass CT because we only want to cancel after pushing, so a flush is always performed.
- await _registry.CollectAndExportAsTextAsync(stream, default);
- }
- finally
- {
- stream.Close();
- }
- }, ContentTypeHeaderValue));
-
- // If anything goes wrong, we want to get at least an entry in the trace log.
- response.EnsureSuccessStatusCode();
- }
- catch (ScrapeFailedException ex)
- {
- // We do not consider failed scrapes a reportable error since the user code that raises the failure should be the one logging it.
- Trace.WriteLine($"Skipping metrics push due to failed scrape: {ex.Message}");
- }
- catch (Exception ex)
- {
- HandleFailedPush(ex);
- }
-
- // We stop only after pushing metrics, to ensure that the latest state is flushed when told to stop.
- if (cancel.IsCancellationRequested)
- break;
-
- var sleepTime = _pushInterval - duration.GetElapsedTime();
-
- // Sleep until the interval elapses or the pusher is asked to shut down.
- if (sleepTime > TimeSpan.Zero)
- {
- try
- {
- await Task.Delay(sleepTime, cancel);
- }
- catch (OperationCanceledException)
- {
- // The task was cancelled.
- // We continue the loop here to ensure final state gets pushed.
- continue;
- }
- }
- }
- });
- }
-
- private void HandleFailedPush(Exception ex)
- {
- if (_onError != null)
- {
- // Asynchronous because we don't trust the callee to be fast.
- Task.Run(() => _onError(ex));
- }
- else
- {
- // If there is no error handler registered, we write to trace to at least hopefully get some attention to the problem.
- Trace.WriteLine(string.Format("Error in MetricPusher: {0}", ex));
- }
- }
- }
-}
diff --git a/Prometheus.NetStandard/MetricPusherOptions.cs b/Prometheus.NetStandard/MetricPusherOptions.cs
deleted file mode 100644
index 676447cb..00000000
--- a/Prometheus.NetStandard/MetricPusherOptions.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-
-namespace Prometheus
-{
- public sealed class MetricPusherOptions
- {
- internal static readonly MetricPusherOptions Default = new MetricPusherOptions();
-
- public string? Endpoint { get; set; }
- public string? Job { get; set; }
- public string? Instance { get; set; }
- public long IntervalMilliseconds { get; set; } = 1000;
- public IEnumerable>? AdditionalLabels { get; set; }
- public CollectorRegistry? Registry { get; set; }
-
- ///
- /// Callback for when a metric push fails.
- ///
- public Action? OnError { get; set; }
-
- ///
- /// If null, a singleton HttpClient will be used.
- ///
- public Func? HttpClientProvider { get; set; }
- }
-}
diff --git a/Prometheus.NetStandard/MetricServer.cs b/Prometheus.NetStandard/MetricServer.cs
deleted file mode 100644
index ab2197f0..00000000
--- a/Prometheus.NetStandard/MetricServer.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- ///
- /// Implementation of a Prometheus exporter that serves metrics using HttpListener.
- /// This is a stand-alone exporter for apps that do not already have an HTTP server included.
- ///
- public class MetricServer : MetricHandler
- {
- private readonly HttpListener _httpListener = new HttpListener();
-
- ///
- /// Only requests that match this predicate will be served by the metric server. This allows you to add authorization checks.
- /// By default (if null), all requests are served.
- ///
- public Func? RequestPredicate { get; set; }
-
- public MetricServer(int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : this("+", port, url, registry, useHttps)
- {
- }
-
- public MetricServer(string hostname, int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : base(registry)
- {
- var s = useHttps ? "s" : "";
- _httpListener.Prefixes.Add($"http{s}://{hostname}:{port}/{url}");
- }
-
- protected override Task StartServer(CancellationToken cancel)
- {
- // This will ensure that any failures to start are nicely thrown from StartServerAsync.
- _httpListener.Start();
-
- // Kick off the actual processing to a new thread and return a Task for the processing thread.
- return Task.Factory.StartNew(delegate
- {
- try
- {
- Thread.CurrentThread.Name = "Metric Server"; //Max length 16 chars (Linux limitation)
-
- while (!cancel.IsCancellationRequested)
- {
- // There is no way to give a CancellationToken to GCA() so, we need to hack around it a bit.
- var getContext = _httpListener.GetContextAsync();
- getContext.Wait(cancel);
- var context = getContext.Result;
-
- // Asynchronously process the request.
- _ = Task.Factory.StartNew(async delegate
- {
- var request = context.Request;
- var response = context.Response;
-
- try
- {
- var predicate = RequestPredicate;
-
- if (predicate != null && !predicate(request))
- {
- // Request rejected by predicate.
- response.StatusCode = (int)HttpStatusCode.Forbidden;
- return;
- }
-
- try
- {
- // We first touch the response.OutputStream only in the callback because touching
- // it means we can no longer send headers (the status code).
- var serializer = new TextSerializer(delegate
- {
- response.ContentType = PrometheusConstants.ExporterContentType;
- response.StatusCode = 200;
- return response.OutputStream;
- });
-
- await _registry.CollectAndSerializeAsync(serializer, cancel);
- response.OutputStream.Dispose();
- }
- catch (ScrapeFailedException ex)
- {
- // This can only happen before anything is written to the stream, so it
- // should still be safe to update the status code and report an error.
- response.StatusCode = 503;
-
- if (!string.IsNullOrWhiteSpace(ex.Message))
- {
- using (var writer = new StreamWriter(response.OutputStream))
- writer.Write(ex.Message);
- }
- }
- }
- catch (Exception ex) when (!(ex is OperationCanceledException))
- {
- if (!_httpListener.IsListening)
- return; // We were shut down.
-
- Trace.WriteLine(string.Format("Error in {0}: {1}", nameof(MetricServer), ex));
-
- try
- {
- response.StatusCode = 500;
- }
- catch
- {
- // Might be too late in request processing to set response code, so just ignore.
- }
- }
- finally
- {
- response.Close();
- }
- });
- }
- }
- finally
- {
- _httpListener.Stop();
- // This should prevent any currently processed requests from finishing.
- _httpListener.Close();
- }
- }, TaskCreationOptions.LongRunning);
- }
- }
-}
diff --git a/Prometheus.NetStandard/MetricType.cs b/Prometheus.NetStandard/MetricType.cs
deleted file mode 100644
index 75ca46a9..00000000
--- a/Prometheus.NetStandard/MetricType.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Prometheus
-{
- internal enum MetricType
- {
- Counter,
- Gauge,
- Summary,
- Histogram
- }
-}
diff --git a/Prometheus.NetStandard/Metrics.cs b/Prometheus.NetStandard/Metrics.cs
deleted file mode 100644
index 0f8665b1..00000000
--- a/Prometheus.NetStandard/Metrics.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-namespace Prometheus
-{
- ///
- /// Static class for easy creation of metrics. Acts as the entry point to the prometheus-net metrics recording API.
- ///
- /// Some built-in metrics are registered by default in the default collector registry. This is mainly to ensure that
- /// the library exports some metrics when installed. If these default metrics are not desired, call
- /// to remove them before registering your own.
- ///
- public static class Metrics
- {
- ///
- /// The default registry where all metrics are registered by default.
- ///
- public static CollectorRegistry DefaultRegistry { get; private set; }
-
- private static MetricFactory _defaultFactory;
-
- ///
- /// Creates a new registry. You may want to use multiple registries if you want to
- /// export different sets of metrics via different exporters (e.g. on different URLs).
- ///
- public static CollectorRegistry NewCustomRegistry() => new CollectorRegistry();
-
- ///
- /// Returns an instance of that you can use to register metrics in a custom registry.
- ///
- public static MetricFactory WithCustomRegistry(CollectorRegistry registry) =>
- new MetricFactory(registry);
-
- ///
- /// Counters only increase in value and reset to zero when the process restarts.
- ///
- public static Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) =>
- _defaultFactory.CreateCounter(name, help, configuration);
-
- ///
- /// Gauges can have any numeric value and change arbitrarily.
- ///
- public static Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) =>
- _defaultFactory.CreateGauge(name, help, configuration);
-
- ///
- /// Summaries track the trends in events over time (10 minutes by default).
- ///
- public static Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) =>
- _defaultFactory.CreateSummary(name, help, configuration);
-
- ///
- /// Histograms track the size and number of events in buckets.
- ///
- public static Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) =>
- _defaultFactory.CreateHistogram(name, help, configuration);
-
- ///
- /// Counters only increase in value and reset to zero when the process restarts.
- ///
- public static Counter CreateCounter(string name, string help, params string[] labelNames) =>
- _defaultFactory.CreateCounter(name, help, labelNames);
-
- ///
- /// Gauges can have any numeric value and change arbitrarily.
- ///
- public static Gauge CreateGauge(string name, string help, params string[] labelNames) =>
- _defaultFactory.CreateGauge(name, help, labelNames);
-
- ///
- /// Summaries track the trends in events over time (10 minutes by default).
- ///
- public static Summary CreateSummary(string name, string help, params string[] labelNames) =>
- _defaultFactory.CreateSummary(name, help, labelNames);
-
- ///
- /// Histograms track the size and number of events in buckets.
- ///
- public static Histogram CreateHistogram(string name, string help, params string[] labelNames) =>
- _defaultFactory.CreateHistogram(name, help, labelNames);
-
- static Metrics()
- {
- DefaultRegistry = new CollectorRegistry();
- DefaultRegistry.SetBeforeFirstCollectCallback(delegate
- {
- // We include some metrics by default, just to give some output when a user first uses the library.
- // These are not designed to be super meaningful/useful metrics.
- DotNetStats.Register(DefaultRegistry);
- });
-
- _defaultFactory = new MetricFactory(DefaultRegistry);
- }
-
- ///
- /// Suppresses the registration of the default sample metrics from the default registry.
- /// Has no effect if not called on startup (it will not remove metrics from a registry already in use).
- ///
- public static void SuppressDefaultMetrics()
- {
- // Only has effect if called before the registry is collected from.
- DefaultRegistry.SetBeforeFirstCollectCallback(delegate { });
- }
- }
-}
\ No newline at end of file
diff --git a/Prometheus.NetStandard/Prometheus.NetStandard.csproj b/Prometheus.NetStandard/Prometheus.NetStandard.csproj
deleted file mode 100644
index d7708cac..00000000
--- a/Prometheus.NetStandard/Prometheus.NetStandard.csproj
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
- netstandard2.0
-
-
- net461;netstandard2.0
-
-
-
- Prometheus
-
- false
-
- true
-
- ..\Resources\prometheus-net.snk
-
- preview
- enable
-
- true
-
- true
- true
- 1701;1702;1705;1591
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Prometheus.NetStandard/PrometheusConstants.cs b/Prometheus.NetStandard/PrometheusConstants.cs
deleted file mode 100644
index 76768197..00000000
--- a/Prometheus.NetStandard/PrometheusConstants.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Text;
-
-namespace Prometheus
-{
- public static class PrometheusConstants
- {
- public const string ExporterContentType = "text/plain; version=0.0.4; charset=utf-8";
-
- // ASP.NET does not want to accept the parameters in PushStreamContent for whatever reason...
- public const string ExporterContentTypeMinimal = "text/plain";
-
- // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never
- // pre-pended to the output stream.
- public static readonly Encoding ExportEncoding = new UTF8Encoding(false);
- }
-}
diff --git a/Prometheus.NetStandard/PushStreamContentInternal.cs b/Prometheus.NetStandard/PushStreamContentInternal.cs
deleted file mode 100644
index e5489d2f..00000000
--- a/Prometheus.NetStandard/PushStreamContentInternal.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-// Simplified for prometheus-net for dependency reduction reasons.
-
-using System.Diagnostics.CodeAnalysis;
-using System.Diagnostics.Contracts;
-using System.IO;
-using System.Net.Http.Headers;
-using System.Threading.Tasks;
-
-namespace System.Net.Http
-{
- ///
- /// Provides an implementation that exposes an output
- /// which can be written to directly. The ability to push data to the output stream differs from the
- /// where data is pulled and not pushed.
- ///
- sealed class PushStreamContentInternal : HttpContent
- {
- private readonly Func _onStreamAvailable;
-
- private static readonly MediaTypeHeaderValue OctetStreamHeaderValue = MediaTypeHeaderValue.Parse("application/octet-stream");
-
- ///
- /// Initializes a new instance of the class with the given .
- ///
- public PushStreamContentInternal(Func onStreamAvailable, MediaTypeHeaderValue mediaType)
- {
- _onStreamAvailable = onStreamAvailable;
- Headers.ContentType = mediaType ?? OctetStreamHeaderValue;
- }
-
- ///
- /// When this method is called, it calls the action provided in the constructor with the output
- /// stream to write to. Once the action has completed its work it closes the stream which will
- /// close this content instance and complete the HTTP request or response.
- ///
- /// The to which to write.
- /// The associated .
- /// A instance that is asynchronously serializing the object's content.
- [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is passed as task result.")]
- protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
- {
- TaskCompletionSource serializeToStreamTask = new TaskCompletionSource();
-
- Stream wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask);
- await _onStreamAvailable(wrappedStream, this, context);
-
- // wait for wrappedStream.Close/Dispose to get called.
- await serializeToStreamTask.Task;
- }
-
- ///
- /// Computes the length of the stream if possible.
- ///
- /// The computed length of the stream.
- /// true if the length has been computed; otherwise false.
- protected override bool TryComputeLength(out long length)
- {
- // We can't know the length of the content being pushed to the output stream.
- length = -1;
- return false;
- }
-
- internal class CompleteTaskOnCloseStream : DelegatingStreamInternal
- {
- private TaskCompletionSource _serializeToStreamTask;
-
- public CompleteTaskOnCloseStream(Stream innerStream, TaskCompletionSource serializeToStreamTask)
- : base(innerStream)
- {
- _serializeToStreamTask = serializeToStreamTask;
- }
-
- [SuppressMessage(
- "Microsoft.Usage",
- "CA2215:Dispose methods should call base class dispose",
- Justification = "See comments, this is intentional.")]
- protected override void Dispose(bool disposing)
- {
- // We don't dispose the underlying stream because we don't own it. Dispose in this case just signifies
- // that the user's action is finished.
- _serializeToStreamTask.TrySetResult(true);
- }
- }
- }
-}
diff --git a/Prometheus.NetStandard/QuantileEpsilonPair.cs b/Prometheus.NetStandard/QuantileEpsilonPair.cs
deleted file mode 100644
index 1ccf40a7..00000000
--- a/Prometheus.NetStandard/QuantileEpsilonPair.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Prometheus
-{
- public readonly struct QuantileEpsilonPair
- {
- public QuantileEpsilonPair(double quantile, double epsilon)
- {
- Quantile = quantile;
- Epsilon = epsilon;
- }
-
- public double Quantile { get; }
- public double Epsilon { get; }
- }
-}
diff --git a/Prometheus.NetStandard/ScrapeFailedException.cs b/Prometheus.NetStandard/ScrapeFailedException.cs
deleted file mode 100644
index 41c2b19c..00000000
--- a/Prometheus.NetStandard/ScrapeFailedException.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-
-namespace Prometheus
-{
- ///
- /// Signals to the metrics server that metrics are currently unavailable. Thrown from "before collect" callbacks.
- /// This causes the entire export operation to fail - even if some metrics are available, they will not be exported.
- ///
- /// The exception message will be delivered as the HTTP response body by the exporter.
- ///
- [Serializable]
- public class ScrapeFailedException : Exception
- {
- public ScrapeFailedException() { }
- public ScrapeFailedException(string message) : base(message) { }
- public ScrapeFailedException(string message, Exception inner) : base(message, inner) { }
- protected ScrapeFailedException(
- System.Runtime.Serialization.SerializationInfo info,
- System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
- }
-}
diff --git a/Prometheus.NetStandard/Summary.cs b/Prometheus.NetStandard/Summary.cs
deleted file mode 100644
index 50e65707..00000000
--- a/Prometheus.NetStandard/Summary.cs
+++ /dev/null
@@ -1,313 +0,0 @@
-using Prometheus.SummaryImpl;
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Prometheus
-{
- public sealed class Summary : Collector, ISummary
- {
- // Label that defines the quantile in a summary.
- private const string QuantileLabel = "quantile";
-
- ///
- /// Client library guidelines say that the summary should default to not measuring quantiles.
- /// https://prometheus.io/docs/instrumenting/writing_clientlibs/#summary
- ///
- internal static readonly QuantileEpsilonPair[] DefObjectivesArray = new QuantileEpsilonPair[0];
-
- // Default Summary quantile values.
- public static readonly IList DefObjectives = new List(DefObjectivesArray);
-
- // Default duration for which observations stay relevant
- public static readonly TimeSpan DefMaxAge = TimeSpan.FromMinutes(10);
-
- // Default number of buckets used to calculate the age of observations
- public static readonly int DefAgeBuckets = 5;
-
- // Standard buffer size for collecting Summary observations
- public static readonly int DefBufCap = 500;
-
- private readonly IReadOnlyList _objectives;
- private readonly TimeSpan _maxAge;
- private readonly int _ageBuckets;
- private readonly int _bufCap;
-
- internal Summary(
- string name,
- string help,
- string[]? labelNames,
- Labels staticLabels,
- bool suppressInitialValue = false,
- IReadOnlyList? objectives = null,
- TimeSpan? maxAge = null,
- int? ageBuckets = null,
- int? bufCap = null)
- : base(name, help, labelNames, staticLabels, suppressInitialValue)
- {
- _objectives = objectives ?? DefObjectivesArray;
- _maxAge = maxAge ?? DefMaxAge;
- _ageBuckets = ageBuckets ?? DefAgeBuckets;
- _bufCap = bufCap ?? DefBufCap;
-
- if (_objectives.Count == 0)
- _objectives = DefObjectivesArray;
-
- if (_maxAge < TimeSpan.Zero)
- throw new ArgumentException($"Illegal max age {_maxAge}");
-
- if (_ageBuckets == 0)
- _ageBuckets = DefAgeBuckets;
-
- if (_bufCap == 0)
- _bufCap = DefBufCap;
-
- if (labelNames?.Any(_ => _ == QuantileLabel) == true)
- throw new ArgumentException($"{QuantileLabel} is a reserved label name");
- }
-
- private protected override Child NewChild(Labels labels, Labels flattenedLabels, bool publish)
- {
- return new Child(this, labels, flattenedLabels, publish);
- }
-
- private protected override MetricType Type => MetricType.Summary;
-
- public sealed class Child : ChildBase, ISummary
- {
- internal Child(Summary parent, Labels labels, Labels flattenedLabels, bool publish)
- : base(parent, labels, flattenedLabels, publish)
- {
- _objectives = parent._objectives;
- _maxAge = parent._maxAge;
- _ageBuckets = parent._ageBuckets;
- _bufCap = parent._bufCap;
-
- _sortedObjectives = new double[_objectives.Count];
- _hotBuf = new SampleBuffer(_bufCap);
- _coldBuf = new SampleBuffer(_bufCap);
- _streamDuration = new TimeSpan(_maxAge.Ticks / _ageBuckets);
- _headStreamExpTime = DateTime.UtcNow.Add(_streamDuration);
- _hotBufExpTime = _headStreamExpTime;
-
- _streams = new QuantileStream[_ageBuckets];
- for (var i = 0; i < _ageBuckets; i++)
- {
- _streams[i] = QuantileStream.NewTargeted(_objectives);
- }
-
- _headStream = _streams[0];
-
- for (var i = 0; i < _objectives.Count; i++)
- {
- _sortedObjectives[i] = _objectives[i].Quantile;
- }
-
- Array.Sort(_sortedObjectives);
-
- _sumIdentifier = CreateIdentifier("sum");
- _countIdentifier = CreateIdentifier("count");
-
- _quantileIdentifiers = new byte[_objectives.Count][];
- for (var i = 0; i < _objectives.Count; i++)
- {
- var value = double.IsPositiveInfinity(_objectives[i].Quantile) ? "+Inf" : _objectives[i].Quantile.ToString(CultureInfo.InvariantCulture);
-
- _quantileIdentifiers[i] = CreateIdentifier(null, ("quantile", value));
- }
- }
-
- private readonly byte[] _sumIdentifier;
- private readonly byte[] _countIdentifier;
- private readonly byte[][] _quantileIdentifiers;
-
- private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel)
- {
- // We output sum.
- // We output count.
- // We output quantiles.
-
- var now = DateTime.UtcNow;
-
- double count;
- double sum;
- var values = new List<(double quantile, double value)>(_objectives.Count);
-
- lock (_bufLock)
- {
- lock (_lock)
- {
- // Swap bufs even if hotBuf is empty to set new hotBufExpTime.
- SwapBufs(now);
- FlushColdBuf();
-
- count = _count;
- sum = _sum;
-
- for (var i = 0; i < _sortedObjectives.Length; i++)
- {
- var quantile = _sortedObjectives[i];
- var value = _headStream.Count == 0 ? double.NaN : _headStream.Query(quantile);
-
- values.Add((quantile, value));
- }
- }
- }
-
- await serializer.WriteMetricAsync(_sumIdentifier, sum, cancel);
- await serializer.WriteMetricAsync(_countIdentifier, count, cancel);
-
- for (var i = 0; i < values.Count; i++)
- await serializer.WriteMetricAsync(_quantileIdentifiers[i], values[i].value, cancel);
- }
-
- // Objectives defines the quantile rank estimates with their respective
- // absolute error. If Objectives[q] = e, then the value reported
- // for q will be the φ-quantile value for some φ between q-e and q+e.
- // The default value is DefObjectives.
- private IReadOnlyList