PrometheusExporter: Project cleanup and middleware fixes (#2414)

This commit is contained in:
Mikel Blanchard 2021-09-28 12:53:22 -07:00 committed by GitHub
parent f47ea55a20
commit dc24fe1485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 732 additions and 384 deletions

View File

@ -12,13 +12,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Hosting\OpenTelemetry.Extensions.Hosting.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" /> <ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Jaeger\OpenTelemetry.Exporter.Jaeger.csproj" /> <ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Jaeger\OpenTelemetry.Exporter.Jaeger.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Zipkin\OpenTelemetry.Exporter.Zipkin.csproj" /> <ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Zipkin\OpenTelemetry.Exporter.Zipkin.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.AspNetCore\OpenTelemetry.Instrumentation.AspNetCore.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Hosting\OpenTelemetry.Extensions.Hosting.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" /> <ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.OpenTelemetryProtocol\OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Prometheus\OpenTelemetry.Exporter.Prometheus.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.AspNetCore\OpenTelemetry.Instrumentation.AspNetCore.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Instrumentation.Http\OpenTelemetry.Instrumentation.Http.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -23,7 +23,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using OpenTelemetry;
using OpenTelemetry.Exporter; using OpenTelemetry.Exporter;
using OpenTelemetry.Instrumentation.AspNetCore; using OpenTelemetry.Instrumentation.AspNetCore;
using OpenTelemetry.Metrics; using OpenTelemetry.Metrics;
@ -34,8 +33,6 @@ namespace Examples.AspNetCore
{ {
public class Startup public class Startup
{ {
private MeterProvider meterProvider;
public Startup(IConfiguration configuration) public Startup(IConfiguration configuration)
{ {
this.Configuration = configuration; this.Configuration = configuration;
@ -59,9 +56,9 @@ namespace Examples.AspNetCore
} }
}); });
// Switch between Zipkin/Jaeger by setting UseExporter in appsettings.json. // Switch between Zipkin/Jaeger/OTLP by setting UseExporter in appsettings.json.
var exporter = this.Configuration.GetValue<string>("UseExporter").ToLowerInvariant(); var tracingExporter = this.Configuration.GetValue<string>("UseTracingExporter").ToLowerInvariant();
switch (exporter) switch (tracingExporter)
{ {
case "jaeger": case "jaeger":
services.AddOpenTelemetryTracing((builder) => builder services.AddOpenTelemetryTracing((builder) => builder
@ -117,15 +114,24 @@ namespace Examples.AspNetCore
break; break;
} }
// TODO: Add IServiceCollection.AddOpenTelemetryMetrics extension method var metricsExporter = this.Configuration.GetValue<string>("UseMetricsExporter").ToLowerInvariant();
var providerBuilder = Sdk.CreateMeterProviderBuilder() services.AddOpenTelemetryMetrics(builder =>
.AddAspNetCoreInstrumentation(); {
builder.AddAspNetCoreInstrumentation();
// TODO: Add configuration switch for Prometheus and OTLP export switch (metricsExporter)
providerBuilder {
.AddConsoleExporter(); case "prometheus":
builder.AddPrometheusExporter();
this.meterProvider = providerBuilder.Build(); break;
case "otlp":
builder.AddOtlpExporter();
break;
default:
builder.AddConsoleExporter();
break;
}
});
} }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
@ -146,6 +152,12 @@ namespace Examples.AspNetCore
app.UseRouting(); app.UseRouting();
var metricsExporter = this.Configuration.GetValue<string>("UseMetricsExporter").ToLowerInvariant();
if (metricsExporter == "prometheus")
{
app.UseOpenTelemetryPrometheusScrapingEndpoint();
}
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllers(); endpoints.MapControllers();

View File

@ -7,7 +7,8 @@
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"UseExporter": "console", "UseTracingExporter": "console",
"UseMetricsExporter": "console",
"UseLogging": true, "UseLogging": true,
"Jaeger": { "Jaeger": {
"ServiceName": "jaeger-test", "ServiceName": "jaeger-test",

View File

@ -49,7 +49,11 @@ namespace Examples.Console
*/ */
using var meterProvider = Sdk.CreateMeterProviderBuilder() using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddSource("TestMeter") .AddSource("TestMeter")
.AddPrometheusExporter(opt => opt.Url = $"http://localhost:{port}/metrics/") .AddPrometheusExporter(opt =>
{
opt.StartHttpListener = true;
opt.HttpListenerPrefixes = new string[] { $"http://*:{port}/" };
})
.Build(); .Build();
ObservableGauge<long> gauge = MyMeter.CreateObservableGauge<long>( ObservableGauge<long> gauge = MyMeter.CreateObservableGauge<long>(

View File

@ -0,0 +1,22 @@
// <copyright file="AssemblyInfo.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.Runtime.CompilerServices;
#if SIGNED
[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")]
#else
[assembly: InternalsVisibleTo("Benchmarks")]
#endif

View File

@ -18,13 +18,13 @@ using System;
using System.Diagnostics.Tracing; using System.Diagnostics.Tracing;
using OpenTelemetry.Internal; using OpenTelemetry.Internal;
namespace OpenTelemetry.Exporter.Prometheus.Implementation namespace OpenTelemetry.Exporter.Prometheus
{ {
/// <summary> /// <summary>
/// EventSource events emitted from the project. /// EventSource events emitted from the project.
/// </summary> /// </summary>
[EventSource(Name = "OpenTelemetry-Exporter-Prometheus")] [EventSource(Name = "OpenTelemetry-Exporter-Prometheus")]
internal class PrometheusExporterEventSource : EventSource internal sealed class PrometheusExporterEventSource : EventSource
{ {
public static PrometheusExporterEventSource Log = new PrometheusExporterEventSource(); public static PrometheusExporterEventSource Log = new PrometheusExporterEventSource();

View File

@ -0,0 +1,194 @@
// <copyright file="PrometheusExporterExtensions.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.Globalization;
using System.IO;
using System.Threading.Tasks;
using OpenTelemetry.Metrics;
using static OpenTelemetry.Exporter.Prometheus.PrometheusMetricBuilder;
namespace OpenTelemetry.Exporter.Prometheus
{
/// <summary>
/// Helper to write metrics collection from exporter in Prometheus format.
/// </summary>
internal static class PrometheusExporterExtensions
{
private const string PrometheusCounterType = "counter";
private const string PrometheusGaugeType = "gauge";
private const string PrometheusHistogramType = "histogram";
private const string PrometheusHistogramSumPostFix = "_sum";
private const string PrometheusHistogramCountPostFix = "_count";
private const string PrometheusHistogramBucketPostFix = "_bucket";
private const string PrometheusHistogramBucketLabelPositiveInfinity = "+Inf";
private const string PrometheusHistogramBucketLabelLessThan = "le";
/// <summary>
/// Serialize metrics to prometheus format.
/// </summary>
/// <param name="exporter"><see cref="PrometheusExporter"/>.</param>
/// <param name="writer">StreamWriter to write to.</param>
/// <returns><see cref="Task"/> to await the operation.</returns>
public static async Task WriteMetricsCollection(this PrometheusExporter exporter, StreamWriter writer)
{
foreach (var metric in exporter.Metrics)
{
var builder = new PrometheusMetricBuilder()
.WithName(metric.Name)
.WithDescription(metric.Description);
switch (metric.MetricType)
{
case MetricType.LongSum:
{
builder = builder.WithType(PrometheusCounterType);
WriteLongSumMetrics(metric, builder);
break;
}
case MetricType.DoubleSum:
{
builder = builder.WithType(PrometheusCounterType);
WriteDoubleSumMetrics(metric, builder);
break;
}
case MetricType.LongGauge:
{
builder = builder.WithType(PrometheusGaugeType);
WriteLongGaugeMetrics(metric, builder);
break;
}
case MetricType.DoubleGauge:
{
builder = builder.WithType(PrometheusGaugeType);
WriteDoubleGaugeMetrics(metric, builder);
break;
}
case MetricType.Histogram:
{
builder = builder.WithType(PrometheusHistogramType);
WriteHistogramMetrics(metric, builder);
break;
}
}
await builder.Write(writer).ConfigureAwait(false);
}
}
private static void WriteLongSumMetrics(Metric metric, PrometheusMetricBuilder builder)
{
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.LongValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
}
private static void WriteDoubleSumMetrics(Metric metric, PrometheusMetricBuilder builder)
{
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.DoubleValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
}
private static void WriteLongGaugeMetrics(Metric metric, PrometheusMetricBuilder builder)
{
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.LongValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
}
private static void WriteDoubleGaugeMetrics(Metric metric, PrometheusMetricBuilder builder)
{
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.DoubleValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
}
private static void WriteHistogramMetrics(Metric metric, PrometheusMetricBuilder builder)
{
/*
* For Histogram we emit one row for Sum, Count and as
* many rows as number of buckets.
* myHistogram_sum{tag1="value1",tag2="value2"} 258330 1629860660991
* myHistogram_count{tag1="value1",tag2="value2"} 355 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="0"} 0 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="5"} 2 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="10"} 4 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="25"} 6 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="50"} 12 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="75"} 19 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="100"} 26 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="250"} 65 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="500"} 128 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="1000"} 241 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="+Inf"} 355 1629860660991
*/
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilderSum = builder.AddValue();
metricValueBuilderSum.WithName(metric.Name + PrometheusHistogramSumPostFix);
metricValueBuilderSum = metricValueBuilderSum.WithValue(metricPoint.DoubleValue);
metricValueBuilderSum.AddLabels(metricPoint.Keys, metricPoint.Values);
var metricValueBuilderCount = builder.AddValue();
metricValueBuilderCount.WithName(metric.Name + PrometheusHistogramCountPostFix);
metricValueBuilderCount = metricValueBuilderCount.WithValue(metricPoint.LongValue);
metricValueBuilderCount.AddLabels(metricPoint.Keys, metricPoint.Values);
long totalCount = 0;
for (int i = 0; i < metricPoint.ExplicitBounds.Length + 1; i++)
{
totalCount += metricPoint.BucketCounts[i];
var metricValueBuilderBuckets = builder.AddValue();
metricValueBuilderBuckets.WithName(metric.Name + PrometheusHistogramBucketPostFix);
metricValueBuilderBuckets = metricValueBuilderBuckets.WithValue(totalCount);
metricValueBuilderBuckets.AddLabels(metricPoint.Keys, metricPoint.Values);
var bucketName = i == metricPoint.ExplicitBounds.Length ?
PrometheusHistogramBucketLabelPositiveInfinity : metricPoint.ExplicitBounds[i].ToString(CultureInfo.InvariantCulture);
metricValueBuilderBuckets.WithLabel(PrometheusHistogramBucketLabelLessThan, bucketName);
}
}
}
private static void AddLabels(this PrometheusMetricValueBuilder valueBuilder, string[] keys, object[] values)
{
if (keys != null)
{
for (int i = 0; i < keys.Length; i++)
{
valueBuilder.WithLabel(keys[i], values[i].ToString());
}
}
}
}
}

View File

@ -19,14 +19,13 @@ using System.IO;
using System.Net; using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using OpenTelemetry.Exporter.Prometheus.Implementation;
namespace OpenTelemetry.Exporter namespace OpenTelemetry.Exporter.Prometheus
{ {
/// <summary> /// <summary>
/// A HTTP listener used to expose Prometheus metrics. /// A HTTP listener used to expose Prometheus metrics.
/// </summary> /// </summary>
public class PrometheusExporterMetricsHttpServer : IDisposable internal sealed class PrometheusExporterMetricsHttpServer : IDisposable
{ {
private readonly PrometheusExporter exporter; private readonly PrometheusExporter exporter;
private readonly HttpListener httpListener = new HttpListener(); private readonly HttpListener httpListener = new HttpListener();
@ -42,7 +41,22 @@ namespace OpenTelemetry.Exporter
public PrometheusExporterMetricsHttpServer(PrometheusExporter exporter) public PrometheusExporterMetricsHttpServer(PrometheusExporter exporter)
{ {
this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter)); this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter));
this.httpListener.Prefixes.Add(exporter.Options.Url);
if ((exporter.Options.HttpListenerPrefixes?.Count ?? 0) <= 0)
{
throw new ArgumentException("No HttpListenerPrefixes were specified on PrometheusExporterOptions.");
}
string path = exporter.Options.ScrapeEndpointPath ?? PrometheusExporterOptions.DefaultScrapeEndpointPath;
if (!path.StartsWith("/"))
{
path = $"/{path}";
}
foreach (string prefix in exporter.Options.HttpListenerPrefixes)
{
this.httpListener.Prefixes.Add($"{prefix.TrimEnd('/')}{path}");
}
} }
/// <summary> /// <summary>
@ -87,16 +101,6 @@ namespace OpenTelemetry.Exporter
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases the unmanaged resources used by this class and optionally releases the managed resources.
/// </summary>
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{ {
if (this.httpListener != null && this.httpListener.IsListening) if (this.httpListener != null && this.httpListener.IsListening)
{ {
@ -116,26 +120,21 @@ namespace OpenTelemetry.Exporter
{ {
var ctxTask = this.httpListener.GetContextAsync(); var ctxTask = this.httpListener.GetContextAsync();
ctxTask.Wait(this.tokenSource.Token); ctxTask.Wait(this.tokenSource.Token);
var ctx = ctxTask.Result; var ctx = ctxTask.Result;
ctx.Response.StatusCode = 200; if (!this.exporter.TryEnterSemaphore())
ctx.Response.ContentType = PrometheusMetricBuilder.ContentType; {
ctx.Response.StatusCode = 429;
ctx.Response.Close();
}
using var output = ctx.Response.OutputStream; Task.Run(() => this.ProcessExportRequest(ctx));
using var writer = new StreamWriter(output);
this.exporter.Collect(Timeout.Infinite);
this.exporter.WriteMetricsCollection(writer);
} }
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
{ {
PrometheusExporterEventSource.Log.CanceledExport(ex); PrometheusExporterEventSource.Log.CanceledExport(ex);
} }
catch (Exception ex)
{
PrometheusExporterEventSource.Log.FailedExport(ex);
}
finally finally
{ {
try try
@ -149,5 +148,31 @@ namespace OpenTelemetry.Exporter
} }
} }
} }
private async Task ProcessExportRequest(HttpListenerContext context)
{
try
{
using var writer = new StreamWriter(context.Response.OutputStream);
try
{
this.exporter.Collect(Timeout.Infinite);
await this.exporter.WriteMetricsCollection(writer).ConfigureAwait(false);
}
finally
{
await writer.FlushAsync().ConfigureAwait(false);
}
}
catch (Exception ex)
{
PrometheusExporterEventSource.Log.FailedExport(ex);
}
finally
{
this.exporter.ReleaseSemaphore();
}
}
} }
} }

View File

@ -0,0 +1,112 @@
// <copyright file="PrometheusExporterMiddleware.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>
#if NETCOREAPP3_1_OR_GREATER
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using OpenTelemetry.Metrics;
namespace OpenTelemetry.Exporter.Prometheus
{
/// <summary>
/// ASP.NET Core middleware for exposing a Prometheus metrics scraping endpoint.
/// </summary>
internal sealed class PrometheusExporterMiddleware
{
private readonly PrometheusExporter exporter;
/// <summary>
/// Initializes a new instance of the <see cref="PrometheusExporterMiddleware"/> class.
/// </summary>
/// <param name="meterProvider"><see cref="MeterProvider"/>.</param>
/// <param name="next"><see cref="RequestDelegate"/>.</param>
public PrometheusExporterMiddleware(MeterProvider meterProvider, RequestDelegate next)
{
if (meterProvider == null)
{
throw new ArgumentNullException(nameof(meterProvider));
}
if (!meterProvider.TryFindExporter(out PrometheusExporter exporter))
{
throw new ArgumentException("A PrometheusExporter could not be found configured on the provided MeterProvider.");
}
this.exporter = exporter;
}
/// <summary>
/// Invoke.
/// </summary>
/// <param name="httpContext"> context. </param>
/// <returns>Task. </returns>
public async Task InvokeAsync(HttpContext httpContext)
{
Debug.Assert(httpContext != null, "httpContext should not be null");
var response = httpContext.Response;
if (!this.exporter.TryEnterSemaphore())
{
response.StatusCode = 429;
return;
}
try
{
this.exporter.Collect(Timeout.Infinite);
await WriteMetricsToResponse(this.exporter, response).ConfigureAwait(false);
}
catch (Exception ex)
{
if (!response.HasStarted)
{
response.StatusCode = 500;
}
PrometheusExporterEventSource.Log.FailedExport(ex);
}
finally
{
this.exporter.ReleaseSemaphore();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static async Task WriteMetricsToResponse(PrometheusExporter exporter, HttpResponse response)
{
response.StatusCode = 200;
response.ContentType = PrometheusMetricBuilder.ContentType;
using var writer = new StreamWriter(response.Body);
try
{
await exporter.WriteMetricsCollection(writer).ConfigureAwait(false);
}
finally
{
await writer.FlushAsync().ConfigureAwait(false);
}
}
}
}
#endif

View File

@ -13,16 +13,18 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// </copyright> // </copyright>
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace OpenTelemetry.Exporter.Prometheus.Implementation namespace OpenTelemetry.Exporter.Prometheus
{ {
internal class PrometheusMetricBuilder internal sealed class PrometheusMetricBuilder
{ {
public const string ContentType = "text/plain; version = 0.0.4"; public const string ContentType = "text/plain; version = 0.0.4";
@ -89,7 +91,7 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation
return val; return val;
} }
public void Write(StreamWriter writer) public async Task Write(StreamWriter writer)
{ {
// https://prometheus.io/docs/instrumenting/exposition_formats/ // https://prometheus.io/docs/instrumenting/exposition_formats/
@ -111,10 +113,10 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation
// and the line feed characters have to be escaped as \\ and \n, respectively. // and the line feed characters have to be escaped as \\ and \n, respectively.
// Only one HELP line may exist for any given metric name. // Only one HELP line may exist for any given metric name.
writer.Write("# HELP "); await writer.WriteAsync("# HELP ").ConfigureAwait(false);
writer.Write(this.name); await writer.WriteAsync(this.name).ConfigureAwait(false);
writer.Write(GetSafeMetricDescription(this.description)); await writer.WriteAsync(GetSafeMetricDescription(this.description)).ConfigureAwait(false);
writer.Write("\n"); await writer.WriteAsync("\n").ConfigureAwait(false);
} }
if (!string.IsNullOrEmpty(this.type)) if (!string.IsNullOrEmpty(this.type))
@ -126,11 +128,11 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation
// before the first sample is reported for that metric name. If there is no TYPE // before the first sample is reported for that metric name. If there is no TYPE
// line for a metric name, the type is set to untyped. // line for a metric name, the type is set to untyped.
writer.Write("# TYPE "); await writer.WriteAsync("# TYPE ").ConfigureAwait(false);
writer.Write(this.name); await writer.WriteAsync(this.name).ConfigureAwait(false);
writer.Write(" "); await writer.WriteAsync(" ").ConfigureAwait(false);
writer.Write(this.type); await writer.WriteAsync(this.type).ConfigureAwait(false);
writer.Write("\n"); await writer.WriteAsync("\n").ConfigureAwait(false);
} }
// The remaining lines describe samples (one per line) using the following syntax (EBNF): // The remaining lines describe samples (one per line) using the following syntax (EBNF):
@ -144,7 +146,7 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation
foreach (var m in this.values) foreach (var m in this.values)
{ {
// metric_name and label_name carry the usual Prometheus expression language restrictions. // metric_name and label_name carry the usual Prometheus expression language restrictions.
writer.Write(m.Name != null ? GetSafeMetricName(m.Name) : this.name); await writer.WriteAsync(m.Name != null ? GetSafeMetricName(m.Name) : this.name).ConfigureAwait(false);
// label_value can be any sequence of UTF-8 characters, but the backslash // label_value can be any sequence of UTF-8 characters, but the backslash
// (\, double-quote ("}, and line feed (\n) characters have to be escaped // (\, double-quote ("}, and line feed (\n) characters have to be escaped
@ -152,26 +154,26 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation
if (m.Labels.Count > 0) if (m.Labels.Count > 0)
{ {
writer.Write(@"{"); await writer.WriteAsync(@"{").ConfigureAwait(false);
writer.Write(string.Join(",", m.Labels.Select(x => GetLabelAndValue(x.Item1, x.Item2)))); await writer.WriteAsync(string.Join(",", m.Labels.Select(x => GetLabelAndValue(x.Item1, x.Item2)))).ConfigureAwait(false);
writer.Write(@"}"); await writer.WriteAsync(@"}").ConfigureAwait(false);
} }
// value is a float represented as required by Go's ParseFloat() function. In addition to // value is a float represented as required by Go's ParseFloat() function. In addition to
// standard numerical values, Nan, +Inf, and -Inf are valid values representing not a number, // standard numerical values, Nan, +Inf, and -Inf are valid values representing not a number,
// positive infinity, and negative infinity, respectively. // positive infinity, and negative infinity, respectively.
writer.Write(" "); await writer.WriteAsync(" ").ConfigureAwait(false);
writer.Write(m.Value.ToString(CultureInfo.InvariantCulture)); await writer.WriteAsync(m.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
writer.Write(" "); await writer.WriteAsync(" ").ConfigureAwait(false);
// The timestamp is an int64 (milliseconds since epoch, i.e. 1970-01-01 00:00:00 UTC, excluding // The timestamp is an int64 (milliseconds since epoch, i.e. 1970-01-01 00:00:00 UTC, excluding
// leap seconds), represented as required by Go's ParseInt() function. // leap seconds), represented as required by Go's ParseInt() function.
writer.Write(now); await writer.WriteAsync(now).ConfigureAwait(false);
// Prometheus' text-based format is line oriented. Lines are separated // Prometheus' text-based format is line oriented. Lines are separated
// by a line feed character (\n). The last line must end with a line // by a line feed character (\n). The last line must end with a line
// feed character. Empty lines are ignored. // feed character. Empty lines are ignored.
writer.Write("\n"); await writer.WriteAsync("\n").ConfigureAwait(false);
} }
static string GetLabelAndValue(string label, string value) static string GetLabelAndValue(string label, string value)
@ -239,7 +241,7 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation
return result; return result;
} }
internal class PrometheusMetricValueBuilder internal sealed class PrometheusMetricValueBuilder
{ {
public readonly ICollection<Tuple<string, string>> Labels = new List<Tuple<string, string>>(); public readonly ICollection<Tuple<string, string>> Labels = new List<Tuple<string, string>>();
public double Value; public double Value;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks> <TargetFrameworks>netcoreapp3.1;net461</TargetFrameworks>
<Description>Prometheus exporter for OpenTelemetry .NET</Description> <Description>Prometheus exporter for OpenTelemetry .NET</Description>
<PackageTags>$(PackageTags);prometheus;metrics</PackageTags> <PackageTags>$(PackageTags);prometheus;metrics</PackageTags>
<MinVerTagPrefix>core-</MinVerTagPrefix> <MinVerTagPrefix>core-</MinVerTagPrefix>
@ -26,8 +26,8 @@
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Api\Internal\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" /> <Compile Include="$(RepoRoot)\src\OpenTelemetry.Api\Internal\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="$(MicrosoftAspNetCoreHttpAbstractionsPkgVer)" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -15,6 +15,8 @@
// </copyright> // </copyright>
using System; using System;
using System.Threading;
using OpenTelemetry.Exporter.Prometheus;
using OpenTelemetry.Metrics; using OpenTelemetry.Metrics;
namespace OpenTelemetry.Exporter namespace OpenTelemetry.Exporter
@ -28,7 +30,10 @@ namespace OpenTelemetry.Exporter
{ {
internal readonly PrometheusExporterOptions Options; internal readonly PrometheusExporterOptions Options;
internal Batch<Metric> Metrics; internal Batch<Metric> Metrics;
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
private readonly PrometheusExporterMetricsHttpServer metricsHttpServer;
private Func<int, bool> funcCollect; private Func<int, bool> funcCollect;
private bool disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PrometheusExporter"/> class. /// Initializes a new instance of the <see cref="PrometheusExporter"/> class.
@ -37,12 +42,25 @@ namespace OpenTelemetry.Exporter
public PrometheusExporter(PrometheusExporterOptions options) public PrometheusExporter(PrometheusExporterOptions options)
{ {
this.Options = options; this.Options = options;
if (options.StartHttpListener)
{
try
{
this.metricsHttpServer = new PrometheusExporterMetricsHttpServer(this);
this.metricsHttpServer.Start();
}
catch (Exception ex)
{
throw new InvalidOperationException("PrometheusExporter http listener could not be started.", ex);
}
}
} }
public Func<int, bool> Collect public Func<int, bool> Collect
{ {
get => this.funcCollect; get => this.funcCollect;
set { this.funcCollect = value; } set => this.funcCollect = value;
} }
public override ExportResult Export(in Batch<Metric> metrics) public override ExportResult Export(in Batch<Metric> metrics)
@ -50,5 +68,31 @@ namespace OpenTelemetry.Exporter
this.Metrics = metrics; this.Metrics = metrics;
return ExportResult.Success; return ExportResult.Success;
} }
internal bool TryEnterSemaphore()
{
return this.semaphore.Wait(0);
}
internal void ReleaseSemaphore()
{
this.semaphore.Release();
}
protected override void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
this.metricsHttpServer?.Dispose();
this.semaphore.Dispose();
}
this.disposed = true;
}
base.Dispose(disposing);
}
} }
} }

View File

@ -0,0 +1,59 @@
// <copyright file="PrometheusExporterApplicationBuilderExtensions.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>
#if NETCOREAPP3_1_OR_GREATER
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Exporter;
using OpenTelemetry.Exporter.Prometheus;
using OpenTelemetry.Metrics;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Provides extension methods for <see cref="IApplicationBuilder"/> to add Prometheus Scraper Endpoint.
/// </summary>
public static class PrometheusExporterApplicationBuilderExtensions
{
/// <summary>
/// Adds OpenTelemetry Prometheus scraping endpoint middleware to an
/// <see cref="IApplicationBuilder"/> instance.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add
/// middleware to.</param>
/// <param name="meterProvider">Optional <see cref="MeterProvider"/>
/// containing a <see cref="PrometheusExporter"/> otherwise the primary
/// SDK provider will be resolved using application services.</param>
/// <returns>A reference to the <see cref="IApplicationBuilder"/> instance after the operation has completed.</returns>
public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, MeterProvider meterProvider = null)
{
var options = app.ApplicationServices.GetOptions<PrometheusExporterOptions>();
string path = options.ScrapeEndpointPath ?? PrometheusExporterOptions.DefaultScrapeEndpointPath;
if (!path.StartsWith("/"))
{
path = $"/{path}";
}
return app.Map(
new PathString(path),
builder => builder.UseMiddleware<PrometheusExporterMiddleware>(meterProvider ?? app.ApplicationServices.GetRequiredService<MeterProvider>()));
}
}
}
#endif

View File

@ -1,192 +0,0 @@
// <copyright file="PrometheusExporterExtensions.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.Globalization;
using System.IO;
using System.Text;
using OpenTelemetry.Exporter.Prometheus.Implementation;
using OpenTelemetry.Metrics;
using static OpenTelemetry.Exporter.Prometheus.Implementation.PrometheusMetricBuilder;
namespace OpenTelemetry.Exporter
{
/// <summary>
/// Helper to write metrics collection from exporter in Prometheus format.
/// </summary>
public static class PrometheusExporterExtensions
{
private const string PrometheusCounterType = "counter";
private const string PrometheusGaugeType = "gauge";
private const string PrometheusHistogramType = "histogram";
private const string PrometheusHistogramSumPostFix = "_sum";
private const string PrometheusHistogramCountPostFix = "_count";
private const string PrometheusHistogramBucketPostFix = "_bucket";
private const string PrometheusHistogramBucketLabelPositiveInfinity = "+Inf";
private const string PrometheusHistogramBucketLabelLessThan = "le";
/// <summary>
/// Serialize to Prometheus Format.
/// </summary>
/// <param name="exporter">Prometheus Exporter.</param>
/// <param name="writer">StreamWriter to write to.</param>
public static void WriteMetricsCollection(this PrometheusExporter exporter, StreamWriter writer)
{
foreach (var metric in exporter.Metrics)
{
var builder = new PrometheusMetricBuilder()
.WithName(metric.Name)
.WithDescription(metric.Description);
switch (metric.MetricType)
{
case MetricType.LongSum:
{
builder = builder.WithType(PrometheusCounterType);
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.LongValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
builder.Write(writer);
break;
}
case MetricType.DoubleSum:
{
builder = builder.WithType(PrometheusCounterType);
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.DoubleValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
builder.Write(writer);
break;
}
case MetricType.LongGauge:
{
builder = builder.WithType(PrometheusGaugeType);
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.LongValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
builder.Write(writer);
break;
}
case MetricType.DoubleGauge:
{
builder = builder.WithType(PrometheusGaugeType);
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilder = builder.AddValue();
metricValueBuilder = metricValueBuilder.WithValue(metricPoint.DoubleValue);
metricValueBuilder.AddLabels(metricPoint.Keys, metricPoint.Values);
}
builder.Write(writer);
break;
}
case MetricType.Histogram:
{
/*
* For Histogram we emit one row for Sum, Count and as
* many rows as number of buckets.
* myHistogram_sum{tag1="value1",tag2="value2"} 258330 1629860660991
* myHistogram_count{tag1="value1",tag2="value2"} 355 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="0"} 0 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="5"} 2 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="10"} 4 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="25"} 6 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="50"} 12 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="75"} 19 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="100"} 26 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="250"} 65 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="500"} 128 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="1000"} 241 1629860660991
* myHistogram_bucket{tag1="value1",tag2="value2",le="+Inf"} 355 1629860660991
*/
builder = builder.WithType(PrometheusHistogramType);
foreach (ref var metricPoint in metric.GetMetricPoints())
{
var metricValueBuilderSum = builder.AddValue();
metricValueBuilderSum.WithName(metric.Name + PrometheusHistogramSumPostFix);
metricValueBuilderSum = metricValueBuilderSum.WithValue(metricPoint.DoubleValue);
metricValueBuilderSum.AddLabels(metricPoint.Keys, metricPoint.Values);
var metricValueBuilderCount = builder.AddValue();
metricValueBuilderCount.WithName(metric.Name + PrometheusHistogramCountPostFix);
metricValueBuilderCount = metricValueBuilderCount.WithValue(metricPoint.LongValue);
metricValueBuilderCount.AddLabels(metricPoint.Keys, metricPoint.Values);
long totalCount = 0;
for (int i = 0; i < metricPoint.ExplicitBounds.Length + 1; i++)
{
totalCount += metricPoint.BucketCounts[i];
var metricValueBuilderBuckets = builder.AddValue();
metricValueBuilderBuckets.WithName(metric.Name + PrometheusHistogramBucketPostFix);
metricValueBuilderBuckets = metricValueBuilderBuckets.WithValue(totalCount);
metricValueBuilderBuckets.AddLabels(metricPoint.Keys, metricPoint.Values);
var bucketName = i == metricPoint.ExplicitBounds.Length ?
PrometheusHistogramBucketLabelPositiveInfinity : metricPoint.ExplicitBounds[i].ToString(CultureInfo.InvariantCulture);
metricValueBuilderBuckets.WithLabel(PrometheusHistogramBucketLabelLessThan, bucketName);
}
}
builder.Write(writer);
break;
}
}
}
}
/// <summary>
/// Get Metrics Collection as a string.
/// </summary>
/// <param name="exporter"> Prometheus Exporter. </param>
/// <returns>Metrics serialized to string in Prometheus format.</returns>
public static string GetMetricsCollection(this PrometheusExporter exporter)
{
using var stream = new MemoryStream();
using var writer = new StreamWriter(stream);
WriteMetricsCollection(exporter, writer);
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length);
}
private static void AddLabels(this PrometheusMetricValueBuilder valueBuilder, string[] keys, object[] values)
{
if (keys != null)
{
for (int i = 0; i < keys.Length; i++)
{
valueBuilder.WithLabel(keys[i], values[i].ToString());
}
}
}
}
}

View File

@ -1,4 +1,4 @@
// <copyright file="MeterProviderBuilderExtensions.cs" company="OpenTelemetry Authors"> // <copyright file="PrometheusExporterMeterProviderBuilderExtensions.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors // Copyright The OpenTelemetry Authors
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
@ -19,15 +19,14 @@ using OpenTelemetry.Exporter;
namespace OpenTelemetry.Metrics namespace OpenTelemetry.Metrics
{ {
public static class MeterProviderBuilderExtensions public static class PrometheusExporterMeterProviderBuilderExtensions
{ {
/// <summary> /// <summary>
/// Adds Console exporter to the TracerProvider. /// Adds <see cref="PrometheusExporter"/> to the <see cref="MeterProviderBuilder"/>.
/// </summary> /// </summary>
/// <param name="builder"><see cref="MeterProviderBuilder"/> builder to use.</param> /// <param name="builder"><see cref="MeterProviderBuilder"/> builder to use.</param>
/// <param name="configure">Exporter configuration options.</param> /// <param name="configure">Exporter configuration options.</param>
/// <returns>The instance of <see cref="MeterProviderBuilder"/> to chain the calls.</returns> /// <returns>The instance of <see cref="MeterProviderBuilder"/> to chain the calls.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")]
public static MeterProviderBuilder AddPrometheusExporter(this MeterProviderBuilder builder, Action<PrometheusExporterOptions> configure = null) public static MeterProviderBuilder AddPrometheusExporter(this MeterProviderBuilder builder, Action<PrometheusExporterOptions> configure = null)
{ {
if (builder == null) if (builder == null)
@ -35,14 +34,24 @@ namespace OpenTelemetry.Metrics
throw new ArgumentNullException(nameof(builder)); throw new ArgumentNullException(nameof(builder));
} }
var options = new PrometheusExporterOptions(); if (builder is IDeferredMeterProviderBuilder deferredMeterProviderBuilder)
{
return deferredMeterProviderBuilder.Configure((sp, builder) =>
{
AddPrometheusExporter(builder, sp.GetOptions<PrometheusExporterOptions>(), configure);
});
}
return AddPrometheusExporter(builder, new PrometheusExporterOptions(), configure);
}
private static MeterProviderBuilder AddPrometheusExporter(MeterProviderBuilder builder, PrometheusExporterOptions options, Action<PrometheusExporterOptions> configure = null)
{
configure?.Invoke(options); configure?.Invoke(options);
var exporter = new PrometheusExporter(options); var exporter = new PrometheusExporter(options);
var reader = new BaseExportingMetricReader(exporter); var reader = new BaseExportingMetricReader(exporter);
var metricsHttpServer = new PrometheusExporterMetricsHttpServer(exporter);
metricsHttpServer.Start();
return builder.AddReader(reader); return builder.AddReader(reader);
} }
} }

View File

@ -1,60 +0,0 @@
// <copyright file="PrometheusExporterMiddleware.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>
#if NETSTANDARD2_0
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace OpenTelemetry.Exporter
{
/// <summary>
/// A middleware used to expose Prometheus metrics.
/// </summary>
public class PrometheusExporterMiddleware
{
private readonly PrometheusExporter exporter;
/// <summary>
/// Initializes a new instance of the <see cref="PrometheusExporterMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the pipeline.</param>
/// <param name="exporter">The <see cref="PrometheusExporter"/> instance.</param>
public PrometheusExporterMiddleware(RequestDelegate next, PrometheusExporter exporter)
{
this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter));
}
/// <summary>
/// Invoke.
/// </summary>
/// <param name="httpContext"> context. </param>
/// <returns>Task. </returns>
public Task InvokeAsync(HttpContext httpContext)
{
if (httpContext is null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var result = this.exporter.GetMetricsCollection();
return httpContext.Response.WriteAsync(result);
}
}
}
#endif

View File

@ -14,16 +14,40 @@
// limitations under the License. // limitations under the License.
// </copyright> // </copyright>
using System.Collections.Generic;
namespace OpenTelemetry.Exporter namespace OpenTelemetry.Exporter
{ {
/// <summary> /// <summary>
/// Options to run prometheus exporter. /// <see cref="PrometheusExporter"/> options.
/// </summary> /// </summary>
public class PrometheusExporterOptions public class PrometheusExporterOptions
{ {
internal const string DefaultScrapeEndpointPath = "/metrics";
#if NETCOREAPP3_1_OR_GREATER
/// <summary> /// <summary>
/// Gets or sets the port to listen to. Typically it ends with /metrics like http://localhost:9184/metrics/. /// Gets or sets a value indicating whether or not an http listener
/// should be started. Default value: False.
/// </summary> /// </summary>
public string Url { get; set; } = "http://localhost:9184/metrics/"; public bool StartHttpListener { get; set; }
#else
/// <summary>
/// Gets or sets a value indicating whether or not an http listener
/// should be started. Default value: True.
/// </summary>
public bool StartHttpListener { get; set; } = true;
#endif
/// <summary>
/// Gets or sets the prefixes to use for the http listener. Default
/// value: http://*:80/.
/// </summary>
public IReadOnlyCollection<string> HttpListenerPrefixes { get; set; } = new string[] { "http://*:80/" };
/// <summary>
/// Gets or sets the path to use for the scraping endpoint. Default value: /metrics.
/// </summary>
public string ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath;
} }
} }

View File

@ -1,46 +0,0 @@
// <copyright file="PrometheusRouteBuilderExtensions.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>
#if NETSTANDARD2_0
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace OpenTelemetry.Exporter.Prometheus
{
/// <summary>
/// Provides extension methods for <see cref="IApplicationBuilder"/> to add Prometheus Scraper Endpoint.
/// </summary>
public static class PrometheusRouteBuilderExtensions
{
private const string DefaultPath = "/metrics";
/// <summary>
/// Use prometheus extension.
/// </summary>
/// <param name="app">The <see cref="IApplicationBuilder"/> to add middleware to.</param>
/// <returns>A reference to the <see cref="IApplicationBuilder"/> instance after the operation has completed.</returns>
public static IApplicationBuilder UsePrometheus(this IApplicationBuilder app)
{
var options = app.ApplicationServices.GetService(typeof(PrometheusExporterOptions)) as PrometheusExporterOptions;
var path = new PathString(options?.Url ?? DefaultPath);
return app.Map(
new PathString(path),
builder => builder.UseMiddleware<PrometheusExporterMiddleware>());
}
}
}
#endif

View File

@ -7,17 +7,90 @@
* [Get Prometheus](https://prometheus.io/docs/introduction/first_steps/) * [Get Prometheus](https://prometheus.io/docs/introduction/first_steps/)
## Installation ## Steps to enable OpenTelemetry.Exporter.Prometheus
### Step 1: Install Package
```shell ```shell
dotnet add package OpenTelemetry.Exporter.Prometheus dotnet add package OpenTelemetry.Exporter.Prometheus
``` ```
### Step 2: Configure OpenTelemetry MeterProvider
* When using OpenTelemetry.Extensions.Hosting package on .NET Core 3.1+:
```csharp
services.AddOpenTelemetryMetrics(builder =>
{
builder.AddPrometheusExporter();
});
```
* Or configure directly:
Call the `AddPrometheusExporter` `MeterProviderBuilder` extension to
register the Prometheus exporter.
```csharp
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddPrometheusExporter()
.Build();
```
### Step 3: Configure Prometheus Scraping Endpoint
* On .NET Core 3.1+ register Prometheus scraping middleware using the
`UseOpenTelemetryPrometheusScrapingEndpoint` extension:
```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseOpenTelemetryPrometheusScrapingEndpoint();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
```
* On .NET Framework an http listener is automatically started which will respond
to scraping requests. See the [Options Properties](#options-properties)
section for details on the settings available. This may also be turned on in
.NET Core (it is OFF by default) when the ASP.NET Core pipeline is not
available for middleware registration.
## Configuration ## Configuration
You can configure the `PrometheusExporter` by following the directions below: You can configure the `PrometheusExporter` through `PrometheusExporterOptions`.
* `Url`: The url to listen to. Typically it ends with `/metrics` like `http://localhost:9184/metrics/`. ## Options Properties
The `PrometheusExporter` can be configured using the `PrometheusExporterOptions`
properties:
* `StartHttpListener`: Set to `true` to start an http listener which will
respond to Prometheus scrape requests using the `HttpListenerPrefixes` and
`ScrapeEndpointPath` options.
Defaults:
* On .NET Framework this is `true` by default.
* On .NET Core 3.1+ this is `false` by default. Users running ASP.NET Core
should use the `UseOpenTelemetryPrometheusScrapingEndpoint` extension to
register the scraping middleware instead of using the listener.
* `HttpListenerPrefixes`: Defines the prefixes which will be used by the
listener when `StartHttpListener` is `true`. The default value is an array of
one element: `http://*:80/`. You may specify multiple endpoints.
For details see:
[HttpListenerPrefixCollection.Add(String)](https://docs.microsoft.com/dotnet/api/system.net.httplistenerprefixcollection.add)
* `ScrapeEndpointPath`: Defines the path for the Prometheus scrape endpoint for
either the http listener or the middleware registered by
`UseOpenTelemetryPrometheusScrapingEndpoint`. Default value: `/metrics`.
See See
[`TestPrometheusExporter.cs`](../../examples/Console/TestPrometheusExporter.cs) [`TestPrometheusExporter.cs`](../../examples/Console/TestPrometheusExporter.cs)

View File

@ -65,6 +65,8 @@ namespace OpenTelemetry.Metrics
} }
} }
internal BaseExporter<Metric> Exporter => this.exporter;
protected ExportModes SupportedExportModes => this.supportedExportModes; protected ExportModes SupportedExportModes => this.supportedExportModes;
internal override void SetParentProvider(BaseProvider parentProvider) internal override void SetParentProvider(BaseProvider parentProvider)

View File

@ -67,6 +67,8 @@ namespace OpenTelemetry.Metrics
return this; return this;
} }
public Enumerator GetEnumerator() => new Enumerator(this.head);
/// <inheritdoc/> /// <inheritdoc/>
protected override bool ProcessMetrics(Batch<Metric> metrics, int timeoutMilliseconds) protected override bool ProcessMetrics(Batch<Metric> metrics, int timeoutMilliseconds)
{ {
@ -159,7 +161,32 @@ namespace OpenTelemetry.Metrics
this.disposed = true; this.disposed = true;
} }
private class DoublyLinkedListNode public struct Enumerator
{
private DoublyLinkedListNode node;
internal Enumerator(DoublyLinkedListNode node)
{
this.node = node;
this.Current = null;
}
public MetricReader Current { get; private set; }
public bool MoveNext()
{
if (this.node != null)
{
this.Current = this.node.Value;
this.node = this.node.Next;
return true;
}
return false;
}
}
internal class DoublyLinkedListNode
{ {
public readonly MetricReader Value; public readonly MetricReader Value;

View File

@ -120,5 +120,40 @@ namespace OpenTelemetry.Metrics
return true; return true;
} }
public static bool TryFindExporter<T>(this MeterProvider provider, out T exporter)
where T : BaseExporter<Metric>
{
if (provider is MeterProviderSdk meterProviderSdk)
{
return TryFindExporter(meterProviderSdk.Reader, out exporter);
}
exporter = null;
return false;
static bool TryFindExporter(MetricReader reader, out T exporter)
{
if (reader is BaseExportingMetricReader exportingMetricReader)
{
exporter = exportingMetricReader.Exporter as T;
return exporter != null;
}
if (reader is CompositeMetricReader compositeMetricReader)
{
foreach (MetricReader childReader in compositeMetricReader)
{
if (TryFindExporter(childReader, out exporter))
{
return true;
}
}
}
exporter = null;
return false;
}
}
} }
} }