diff --git a/src/Reporting/Reporting.sln b/src/Reporting/Reporting.sln
index c326be56..596636bf 100644
--- a/src/Reporting/Reporting.sln
+++ b/src/Reporting/Reporting.sln
@@ -142,6 +142,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MetricsInfluxDB2SandboxMvc"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App.Metrics.Reporting.InfluxDB2.Facts", "test\App.Metrics.Reporting.InfluxDB2.Facts\App.Metrics.Reporting.InfluxDB2.Facts.csproj", "{C4B12D2E-75D3-4800-B116-77851E4D1A2E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App.Metrics.Formatters.Prometheus.Facts", "test\App.Metrics.Formatters.Prometheus.Facts\App.Metrics.Formatters.Prometheus.Facts.csproj", "{5E6D167B-E6D1-4853-A54C-DD53CB3416EF}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -316,6 +318,10 @@ Global
{C4B12D2E-75D3-4800-B116-77851E4D1A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4B12D2E-75D3-4800-B116-77851E4D1A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4B12D2E-75D3-4800-B116-77851E4D1A2E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5E6D167B-E6D1-4853-A54C-DD53CB3416EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5E6D167B-E6D1-4853-A54C-DD53CB3416EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5E6D167B-E6D1-4853-A54C-DD53CB3416EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5E6D167B-E6D1-4853-A54C-DD53CB3416EF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -382,6 +388,7 @@ Global
{8B32FBD8-2F3F-40EF-A672-8E1E9E16148F} = {9D00C681-A88C-450E-B83E-E3BFF942F82A}
{74C314BC-394A-4A09-B739-03D4BE86DBB0} = {9D00C681-A88C-450E-B83E-E3BFF942F82A}
{C4B12D2E-75D3-4800-B116-77851E4D1A2E} = {3B064A0E-B6B6-41A3-BFC1-F4734E16EBA3}
+ {5E6D167B-E6D1-4853-A54C-DD53CB3416EF} = {DC95E9C9-286C-45D6-BE58-83E3F42DB96D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {492C97B1-706A-4357-B3F7-EFB6AFB1F6C6}
diff --git a/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/App.Metrics.Formatters.Prometheus.Facts.csproj b/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/App.Metrics.Formatters.Prometheus.Facts.csproj
new file mode 100644
index 00000000..430c961c
--- /dev/null
+++ b/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/App.Metrics.Formatters.Prometheus.Facts.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ netcoreapp3.1
+
+
+
+
+
+
+
diff --git a/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/TestClock.cs b/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/TestClock.cs
new file mode 100644
index 00000000..43bb7fae
--- /dev/null
+++ b/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/TestClock.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Globalization;
+
+namespace App.Metrics.Formatters.Prometheus.Facts
+{
+ sealed class TestClock : IClock
+ {
+ public event EventHandler Advanced;
+
+ public long Nanoseconds { get; private set; }
+
+ public long Seconds => TimeUnit.Nanoseconds.ToSeconds(Nanoseconds);
+
+ public DateTime UtcDateTime => new DateTime(Nanoseconds / 100L, DateTimeKind.Utc);
+
+ public void Advance(TimeUnit unit, long value)
+ {
+ Nanoseconds += unit.ToNanoseconds(value);
+ Advanced?.Invoke(this, EventArgs.Empty);
+ }
+
+ public string FormatTimestamp(DateTime timestamp) { return timestamp.ToString("yyyy-MM-ddTHH:mm:ss.ffffK", CultureInfo.InvariantCulture); }
+ }
+}
\ No newline at end of file
diff --git a/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/TestMetricsPrometheusTextOutputFormatter.cs b/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/TestMetricsPrometheusTextOutputFormatter.cs
new file mode 100644
index 00000000..547e9afd
--- /dev/null
+++ b/src/Reporting/test/App.Metrics.Formatters.Prometheus.Facts/TestMetricsPrometheusTextOutputFormatter.cs
@@ -0,0 +1,230 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using App.Metrics.Apdex;
+using App.Metrics.BucketHistogram;
+using App.Metrics.BucketTimer;
+using App.Metrics.Counter;
+using App.Metrics.Gauge;
+using App.Metrics.Histogram;
+using App.Metrics.Meter;
+using App.Metrics.ReservoirSampling;
+using App.Metrics.ReservoirSampling.ExponentialDecay;
+using App.Metrics.Timer;
+using FluentAssertions;
+using Xunit;
+
+namespace App.Metrics.Formatters.Prometheus.Facts
+{
+ public class TestMetricsPrometheusTextOutputFormatter
+ {
+ private readonly IClock _clock = new TestClock();
+ private readonly IReservoir _defaultReservoir = new DefaultForwardDecayingReservoir();
+
+ private readonly MetricsPrometheusTextOutputFormatter _metricsPrometheusTextOutputFormatter =
+ new MetricsPrometheusTextOutputFormatter();
+
+ private readonly DateTime _timestamp = new DateTime(2017, 1, 1, 1, 1, 1, DateTimeKind.Utc);
+
+ [Fact]
+ public async Task Apdex_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_apdex apdex description\n# TYPE test_apdex gauge\ntest_apdex 0\n\n";
+
+ var apdex = new DefaultApdexMetric(_defaultReservoir, _clock, false);
+ var apdexValueSource = new ApdexValueSource(
+ "apdex",
+ ConstantValue.Provider(apdex.Value),
+ MetricTags.Empty,
+ description: "apdex description");
+
+ var output = await GetFormatterOutput(CreateValueSource(apdexScores: apdexValueSource));
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public async Task Counter_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_counter counter_description\n# TYPE test_counter gauge\ntest_counter 0\n\n";
+
+ var counter = new DefaultCounterMetric();
+ var counterValueSource = new CounterValueSource("counter", ConstantValue.Provider(counter.Value),
+ Unit.Calls,
+ MetricTags.Empty,
+ description: "counter_description");
+
+ var output = await GetFormatterOutput(CreateValueSource(counters: counterValueSource));
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public async Task Bucket_histogram_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_bucket_histogram bucket histogram description\n# TYPE test_bucket_histogram histogram\ntest_bucket_histogram_sum 0\ntest_bucket_histogram_count 0\ntest_bucket_histogram_bucket{le=\"0.5\"} 0\ntest_bucket_histogram_bucket{le=\"+Inf\"} 0\n\n";
+ var bucketHistogram = new DefaultBucketHistogramMetric(new[] {0.5});
+ var bucketHistogramValueSource = new BucketHistogramValueSource("bucket_histogram",
+ ConstantValue.Provider(bucketHistogram.Value),
+ Unit.Calls,
+ MetricTags.Empty,
+ "bucket histogram description");
+ var metricsContextValueSource = CreateValueSource(bucketHistograms: bucketHistogramValueSource);
+
+ var output = await GetFormatterOutput(metricsContextValueSource);
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public async Task Bucket_timer_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_bucket_timer bucket timer description\n# TYPE test_bucket_timer histogram\ntest_bucket_timer_sum 0\ntest_bucket_timer_count 0\ntest_bucket_timer_bucket{le=\"0.5\"} 0\ntest_bucket_timer_bucket{le=\"+Inf\"} 0\n\n";
+ var bucketHistogram = new DefaultBucketHistogramMetric(new[] {0.5});
+ var bucketTimer = new DefaultBucketTimerMetric(bucketHistogram,_clock,TimeUnit.Milliseconds);
+ var bucketTimerValueSource = new BucketTimerValueSource(
+ "bucket_timer",
+ ConstantValue.Provider(bucketTimer.Value),
+ Unit.Calls,
+ TimeUnit.Milliseconds,
+ TimeUnit.Milliseconds,
+ MetricTags.Empty,
+ "bucket timer description");
+ var metricsContextValueSource = CreateValueSource(bucketTimers: bucketTimerValueSource);
+
+ var output = await GetFormatterOutput(metricsContextValueSource);
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public async Task Gauge_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_gauge gauge description\n# TYPE test_gauge gauge\ntest_gauge 1\n\n";
+ var gauge = new FunctionGauge(() => 1);
+ var gaugeValueSource = new GaugeValueSource(
+ "gauge",
+ ConstantValue.Provider(gauge.Value),
+ Unit.None,
+ MetricTags.Empty,
+ description: "gauge description");
+ var metricsContextValueSource = CreateValueSource(gauges: gaugeValueSource);
+
+ var output = await GetFormatterOutput(metricsContextValueSource);
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public async Task Histogram_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_histogram histogram description\n# TYPE test_histogram summary\ntest_histogram_sum 0\ntest_histogram_count 0\ntest_histogram{quantile=\"0.5\"} 0\ntest_histogram{quantile=\"0.75\"} 0\ntest_histogram{quantile=\"0.95\"} 0\ntest_histogram{quantile=\"0.99\"} 0\n\n";
+ var histogram = new DefaultHistogramMetric(_defaultReservoir);
+ var histogramValueSource = new HistogramValueSource(
+ "histogram",
+ ConstantValue.Provider(histogram.Value),
+ Unit.None,
+ MetricTags.Empty,
+ description: "histogram description");
+
+ var metricsContextValueSource = CreateValueSource(histograms: histogramValueSource);
+
+ var output = await GetFormatterOutput(metricsContextValueSource);
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public async Task Meter_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_meter_total meter description\n# TYPE test_meter_total counter\ntest_meter_total 0\n\n";
+ var clock = new TestClock();
+ var meter = new DefaultMeterMetric(clock);
+
+ var meterValueSource = new MeterValueSource(
+ "meter",
+ ConstantValue.Provider(meter.Value),
+ Unit.None,
+ TimeUnit.Milliseconds,
+ MetricTags.Empty,
+ description: "meter description");
+
+ var metricsContextValueSource = CreateValueSource(meters: meterValueSource);
+
+ var output = await GetFormatterOutput(metricsContextValueSource);
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ [Fact]
+ public async Task Timer_output_contains_description()
+ {
+ const string expected =
+ "# HELP test_timer timer description\n# TYPE test_timer summary\ntest_timer_sum 0\ntest_timer_count 0\ntest_timer{quantile=\"0.5\"} 0\ntest_timer{quantile=\"0.75\"} 0\ntest_timer{quantile=\"0.95\"} 0\ntest_timer{quantile=\"0.99\"} 0\n\n";
+ var clock = new TestClock();
+ var timer = new DefaultTimerMetric(_defaultReservoir, clock);
+ var timerValueSource = new TimerValueSource(
+ "timer",
+ ConstantValue.Provider(timer.Value),
+ Unit.None,
+ TimeUnit.Milliseconds,
+ TimeUnit.Milliseconds,
+ MetricTags.Empty,
+ description: "timer description");
+
+ // Act
+ var metricsContextValueSource = CreateValueSource(timers: timerValueSource);
+
+ var output = await GetFormatterOutput(metricsContextValueSource);
+ output.Should().BeEquivalentTo(expected);
+ }
+
+ private async Task GetFormatterOutput(MetricsContextValueSource metricsContextValueSource)
+ {
+ await using var stream = new MemoryStream();
+ await _metricsPrometheusTextOutputFormatter.WriteAsync(stream,
+ new MetricsDataValueSource(_timestamp,
+ new[] {metricsContextValueSource}));
+
+ var output = Encoding.UTF8.GetString(stream.ToArray());
+ return output;
+ }
+
+ private static MetricsContextValueSource CreateValueSource(
+ string context="test",
+ GaugeValueSource gauges = null,
+ CounterValueSource counters = null,
+ MeterValueSource meters = null,
+ HistogramValueSource histograms = null,
+ BucketHistogramValueSource bucketHistograms = null,
+ TimerValueSource timers = null,
+ BucketTimerValueSource bucketTimers = null,
+ ApdexValueSource apdexScores = null)
+ {
+ var gaugeValues = gauges != null ? new[] {gauges} : Enumerable.Empty();
+ var counterValues = counters != null ? new[] {counters} : Enumerable.Empty();
+ var meterValues = meters != null ? new[] {meters} : Enumerable.Empty();
+ var histogramValues = histograms != null ? new[] {histograms} : Enumerable.Empty();
+ var bucketHistogramValues = bucketHistograms != null
+ ? new[] {bucketHistograms}
+ : Enumerable.Empty();
+ var timerValues = timers != null ? new[] {timers} : Enumerable.Empty();
+ var bucketTimerValues =
+ bucketTimers != null ? new[] {bucketTimers} : Enumerable.Empty();
+ var apdexScoreValues = apdexScores != null ? new[] {apdexScores} : Enumerable.Empty();
+
+ return new MetricsContextValueSource(
+ context,
+ gaugeValues,
+ counterValues,
+ meterValues,
+ histogramValues,
+ bucketHistogramValues,
+ timerValues,
+ bucketTimerValues,
+ apdexScoreValues);
+ }
+ }
+}
\ No newline at end of file