opentelemetry-dotnet/test/OpenTelemetry.Exporter.Prom.../PrometheusExporterTests.cs

190 lines
7.7 KiB
C#

// <copyright file="PrometheusExporterTests.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
#if !NETFRAMEWORK
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
#endif
using OpenTelemetry.Metrics.Export;
using OpenTelemetry.Trace;
using Xunit;
using Xunit.Abstractions;
namespace OpenTelemetry.Exporter.Prometheus.Tests
{
public class PrometheusExporterTests
{
private const int MetricPushIntervalMsec = 100;
private const int WaitDuration = MetricPushIntervalMsec + 100;
private readonly ITestOutputHelper output;
public PrometheusExporterTests(ITestOutputHelper output)
{
this.output = output;
}
[Fact]
public async Task E2ETestMetricsHttpServerAsync()
{
var promOptions = new PrometheusExporterOptions() { Url = "http://localhost:9184/metrics/" };
var promExporter = new PrometheusExporter(promOptions);
var simpleProcessor = new UngroupedBatcher();
var metricsHttpServer = new PrometheusExporterMetricsHttpServer(promExporter);
try
{
metricsHttpServer.Start();
CollectMetrics(simpleProcessor, promExporter);
}
finally
{
await Task.Delay(WaitDuration);
var client = new HttpClient();
var response = await client.GetAsync("http://localhost:9184/metrics/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseText = response.Content.ReadAsStringAsync().Result;
this.output.WriteLine($"Response from metrics API is \n {responseText}");
this.ValidateResponse(responseText);
metricsHttpServer.Stop();
}
}
#if !NETFRAMEWORK
[Fact]
public async Task E2ETestMiddleware()
{
var promOptions = new PrometheusExporterOptions { Url = "/metrics" };
var promExporter = new PrometheusExporter(promOptions);
var simpleProcessor = new UngroupedBatcher();
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UsePrometheus();
})
.ConfigureServices(services =>
{
services.AddSingleton(promOptions);
services.AddSingleton(promExporter); // Temporary till we figure out metrics configuration
});
var server = new TestServer(builder);
var client = server.CreateClient();
try
{
CollectMetrics(simpleProcessor, promExporter);
}
finally
{
var response = await client.GetAsync("/foo");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
await Task.Delay(WaitDuration);
response = await client.GetAsync("/metrics");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var responseText = await response.Content.ReadAsStringAsync();
this.output.WriteLine($"Response from metrics API is \n {responseText}");
this.ValidateResponse(responseText);
}
}
#endif
private static void CollectMetrics(UngroupedBatcher simpleProcessor, MetricExporter exporter)
{
var meter = Sdk.CreateMeterProvider(mb =>
{
mb.SetMetricProcessor(simpleProcessor);
mb.SetMetricExporter(exporter);
mb.SetMetricPushInterval(TimeSpan.FromMilliseconds(MetricPushIntervalMsec));
}).GetMeter("library1");
var testCounter = meter.CreateInt64Counter("testCounter");
var testMeasure = meter.CreateInt64Measure("testMeasure");
var labels1 = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("dim1", "value1"), new KeyValuePair<string, string>("dim2", "value1"),
};
var labels2 = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("dim1", "value2"), new KeyValuePair<string, string>("dim2", "value2"),
};
var defaultContext = default(SpanContext);
for (int i = 0; i < 10; i++)
{
testCounter.Add(defaultContext, 100, meter.GetLabelSet(labels1));
testCounter.Add(defaultContext, 10, meter.GetLabelSet(labels1));
testCounter.Add(defaultContext, 200, meter.GetLabelSet(labels2));
testCounter.Add(defaultContext, 10, meter.GetLabelSet(labels2));
testMeasure.Record(defaultContext, 10, meter.GetLabelSet(labels1));
testMeasure.Record(defaultContext, 100, meter.GetLabelSet(labels1));
testMeasure.Record(defaultContext, 5, meter.GetLabelSet(labels1));
testMeasure.Record(defaultContext, 500, meter.GetLabelSet(labels1));
}
}
private void ValidateResponse(string responseText)
{
// Validate counters.
Assert.Contains("TYPE testCounter counter", responseText);
Assert.Contains("testCounter{dim1=\"value1\",dim2=\"value1\"}", responseText);
Assert.Contains("testCounter{dim1=\"value2\",dim2=\"value2\"}", responseText);
// Validate measure.
Assert.Contains("# TYPE testMeasure summary", responseText);
// sum is 6150 = 10 * (10+100+5+500)
Assert.Contains("testMeasure_sum{dim1=\"value1\"} 6150", responseText);
// count is 10 * 4
Assert.Contains("testMeasure_count{dim1=\"value1\"} 40", responseText);
// Min is 5
Assert.Contains("testMeasure{dim1=\"value1\",quantile=\"0\"} 5", responseText);
// Max is 500
Assert.Contains("testMeasure{dim1=\"value1\",quantile=\"1\"} 500", responseText);
// TODO: Validate that # TYPE occurs only once per metric.
// Also validate that for every metric+dimension, there is only one row in the response.
// Though the above are Prometheus Server requirements, we haven't enforced it in code.
// This is because we have implemented Prometheus using a Push Controller, where
// we accumulate metrics from each Push into exporter, and is used to construct
// out for /metrics call. Because of this, its possible that multiple Push has occurred
// before Prometheus server makes /metrics call. (i.e Prometheus scrape interval is much more
// than Push interval scenario)
// Once a pull model is implemented, we'll not have this issue and we need to add tests
// at that time.
// If in future, there is a official .NET Prometheus Client library, and OT Exporter
// choses to take a dependency on it, then none of these concerns arise.
}
}
}