diff --git a/examples/AspNetCore/Examples.AspNetCore.csproj b/examples/AspNetCore/Examples.AspNetCore.csproj index e067ca3eb..646c8b0d9 100644 --- a/examples/AspNetCore/Examples.AspNetCore.csproj +++ b/examples/AspNetCore/Examples.AspNetCore.csproj @@ -12,13 +12,14 @@ + - - - + + + diff --git a/examples/AspNetCore/Startup.cs b/examples/AspNetCore/Startup.cs index 6d43c6622..d9f4aa733 100644 --- a/examples/AspNetCore/Startup.cs +++ b/examples/AspNetCore/Startup.cs @@ -23,7 +23,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; -using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Instrumentation.AspNetCore; using OpenTelemetry.Metrics; @@ -34,8 +33,6 @@ namespace Examples.AspNetCore { public class Startup { - private MeterProvider meterProvider; - public Startup(IConfiguration configuration) { this.Configuration = configuration; @@ -59,9 +56,9 @@ namespace Examples.AspNetCore } }); - // Switch between Zipkin/Jaeger by setting UseExporter in appsettings.json. - var exporter = this.Configuration.GetValue("UseExporter").ToLowerInvariant(); - switch (exporter) + // Switch between Zipkin/Jaeger/OTLP by setting UseExporter in appsettings.json. + var tracingExporter = this.Configuration.GetValue("UseTracingExporter").ToLowerInvariant(); + switch (tracingExporter) { case "jaeger": services.AddOpenTelemetryTracing((builder) => builder @@ -117,15 +114,24 @@ namespace Examples.AspNetCore break; } - // TODO: Add IServiceCollection.AddOpenTelemetryMetrics extension method - var providerBuilder = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation(); + var metricsExporter = this.Configuration.GetValue("UseMetricsExporter").ToLowerInvariant(); + services.AddOpenTelemetryMetrics(builder => + { + builder.AddAspNetCoreInstrumentation(); - // TODO: Add configuration switch for Prometheus and OTLP export - providerBuilder - .AddConsoleExporter(); - - this.meterProvider = providerBuilder.Build(); + switch (metricsExporter) + { + case "prometheus": + builder.AddPrometheusExporter(); + break; + case "otlp": + builder.AddOtlpExporter(); + break; + default: + builder.AddConsoleExporter(); + break; + } + }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) @@ -146,6 +152,12 @@ namespace Examples.AspNetCore app.UseRouting(); + var metricsExporter = this.Configuration.GetValue("UseMetricsExporter").ToLowerInvariant(); + if (metricsExporter == "prometheus") + { + app.UseOpenTelemetryPrometheusScrapingEndpoint(); + } + app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/examples/AspNetCore/appsettings.json b/examples/AspNetCore/appsettings.json index edd66d9db..bab9c301d 100644 --- a/examples/AspNetCore/appsettings.json +++ b/examples/AspNetCore/appsettings.json @@ -7,7 +7,8 @@ } }, "AllowedHosts": "*", - "UseExporter": "console", + "UseTracingExporter": "console", + "UseMetricsExporter": "console", "UseLogging": true, "Jaeger": { "ServiceName": "jaeger-test", diff --git a/examples/Console/TestPrometheusExporter.cs b/examples/Console/TestPrometheusExporter.cs index 3e717f250..a1f66fd0a 100644 --- a/examples/Console/TestPrometheusExporter.cs +++ b/examples/Console/TestPrometheusExporter.cs @@ -49,7 +49,11 @@ namespace Examples.Console */ using var meterProvider = Sdk.CreateMeterProviderBuilder() .AddSource("TestMeter") - .AddPrometheusExporter(opt => opt.Url = $"http://localhost:{port}/metrics/") + .AddPrometheusExporter(opt => + { + opt.StartHttpListener = true; + opt.HttpListenerPrefixes = new string[] { $"http://*:{port}/" }; + }) .Build(); ObservableGauge gauge = MyMeter.CreateObservableGauge( diff --git a/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs new file mode 100644 index 000000000..36f813dca --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs @@ -0,0 +1,22 @@ +// +// 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. +// +using System.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("Benchmarks")] +#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs index cd577f4f5..0523d120e 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs @@ -18,13 +18,13 @@ using System; using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.Prometheus.Implementation +namespace OpenTelemetry.Exporter.Prometheus { /// /// EventSource events emitted from the project. /// [EventSource(Name = "OpenTelemetry-Exporter-Prometheus")] - internal class PrometheusExporterEventSource : EventSource + internal sealed class PrometheusExporterEventSource : EventSource { public static PrometheusExporterEventSource Log = new PrometheusExporterEventSource(); diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterExtensions.cs new file mode 100644 index 000000000..88e504fa3 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterExtensions.cs @@ -0,0 +1,194 @@ +// +// 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. +// + +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using OpenTelemetry.Metrics; +using static OpenTelemetry.Exporter.Prometheus.PrometheusMetricBuilder; + +namespace OpenTelemetry.Exporter.Prometheus +{ + /// + /// Helper to write metrics collection from exporter in Prometheus format. + /// + 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"; + + /// + /// Serialize metrics to prometheus format. + /// + /// . + /// StreamWriter to write to. + /// to await the operation. + 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()); + } + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMetricsHttpServer.cs similarity index 73% rename from src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs rename to src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMetricsHttpServer.cs index 330764363..909f1ff2e 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMetricsHttpServer.cs @@ -19,14 +19,13 @@ using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; -using OpenTelemetry.Exporter.Prometheus.Implementation; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter.Prometheus { /// /// A HTTP listener used to expose Prometheus metrics. /// - public class PrometheusExporterMetricsHttpServer : IDisposable + internal sealed class PrometheusExporterMetricsHttpServer : IDisposable { private readonly PrometheusExporter exporter; private readonly HttpListener httpListener = new HttpListener(); @@ -42,7 +41,22 @@ namespace OpenTelemetry.Exporter public PrometheusExporterMetricsHttpServer(PrometheusExporter 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}"); + } } /// @@ -87,16 +101,6 @@ namespace OpenTelemetry.Exporter /// public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases the unmanaged resources used by this class and optionally releases the managed resources. - /// - /// to release both managed and unmanaged resources; to release only unmanaged resources. - protected virtual void Dispose(bool disposing) { if (this.httpListener != null && this.httpListener.IsListening) { @@ -116,26 +120,21 @@ namespace OpenTelemetry.Exporter { var ctxTask = this.httpListener.GetContextAsync(); ctxTask.Wait(this.tokenSource.Token); - var ctx = ctxTask.Result; - ctx.Response.StatusCode = 200; - ctx.Response.ContentType = PrometheusMetricBuilder.ContentType; + if (!this.exporter.TryEnterSemaphore()) + { + ctx.Response.StatusCode = 429; + ctx.Response.Close(); + } - using var output = ctx.Response.OutputStream; - using var writer = new StreamWriter(output); - this.exporter.Collect(Timeout.Infinite); - this.exporter.WriteMetricsCollection(writer); + Task.Run(() => this.ProcessExportRequest(ctx)); } } catch (OperationCanceledException ex) { PrometheusExporterEventSource.Log.CanceledExport(ex); } - catch (Exception ex) - { - PrometheusExporterEventSource.Log.FailedExport(ex); - } finally { 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(); + } + } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMiddleware.cs new file mode 100644 index 000000000..f8a584be4 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMiddleware.cs @@ -0,0 +1,112 @@ +// +// 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. +// + +#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 +{ + /// + /// ASP.NET Core middleware for exposing a Prometheus metrics scraping endpoint. + /// + internal sealed class PrometheusExporterMiddleware + { + private readonly PrometheusExporter exporter; + + /// + /// Initializes a new instance of the class. + /// + /// . + /// . + 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; + } + + /// + /// Invoke. + /// + /// context. + /// Task. + 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 diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs index 28e78e59c..751a7be50 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs @@ -13,16 +13,18 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; 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"; @@ -89,7 +91,7 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation return val; } - public void Write(StreamWriter writer) + public async Task Write(StreamWriter writer) { // 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. // Only one HELP line may exist for any given metric name. - writer.Write("# HELP "); - writer.Write(this.name); - writer.Write(GetSafeMetricDescription(this.description)); - writer.Write("\n"); + await writer.WriteAsync("# HELP ").ConfigureAwait(false); + await writer.WriteAsync(this.name).ConfigureAwait(false); + await writer.WriteAsync(GetSafeMetricDescription(this.description)).ConfigureAwait(false); + await writer.WriteAsync("\n").ConfigureAwait(false); } 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 // line for a metric name, the type is set to untyped. - writer.Write("# TYPE "); - writer.Write(this.name); - writer.Write(" "); - writer.Write(this.type); - writer.Write("\n"); + await writer.WriteAsync("# TYPE ").ConfigureAwait(false); + await writer.WriteAsync(this.name).ConfigureAwait(false); + await writer.WriteAsync(" ").ConfigureAwait(false); + await writer.WriteAsync(this.type).ConfigureAwait(false); + await writer.WriteAsync("\n").ConfigureAwait(false); } // 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) { // 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 // (\, 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) { - writer.Write(@"{"); - writer.Write(string.Join(",", m.Labels.Select(x => GetLabelAndValue(x.Item1, x.Item2)))); - writer.Write(@"}"); + await writer.WriteAsync(@"{").ConfigureAwait(false); + await writer.WriteAsync(string.Join(",", m.Labels.Select(x => GetLabelAndValue(x.Item1, x.Item2)))).ConfigureAwait(false); + await writer.WriteAsync(@"}").ConfigureAwait(false); } // 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, // positive infinity, and negative infinity, respectively. - writer.Write(" "); - writer.Write(m.Value.ToString(CultureInfo.InvariantCulture)); - writer.Write(" "); + await writer.WriteAsync(" ").ConfigureAwait(false); + await writer.WriteAsync(m.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + await writer.WriteAsync(" ").ConfigureAwait(false); // 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. - writer.Write(now); + await writer.WriteAsync(now).ConfigureAwait(false); // Prometheus' text-based format is line oriented. Lines are separated // by a line feed character (\n). The last line must end with a line // feed character. Empty lines are ignored. - writer.Write("\n"); + await writer.WriteAsync("\n").ConfigureAwait(false); } static string GetLabelAndValue(string label, string value) @@ -239,7 +241,7 @@ namespace OpenTelemetry.Exporter.Prometheus.Implementation return result; } - internal class PrometheusMetricValueBuilder + internal sealed class PrometheusMetricValueBuilder { public readonly ICollection> Labels = new List>(); public double Value; diff --git a/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj b/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj index b7a7eba84..f69c2c2d9 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net461 + netcoreapp3.1;net461 Prometheus exporter for OpenTelemetry .NET $(PackageTags);prometheus;metrics core- @@ -26,8 +26,8 @@ - - + + diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs index d3b8c6256..66bd48411 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs @@ -15,6 +15,8 @@ // using System; +using System.Threading; +using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Metrics; namespace OpenTelemetry.Exporter @@ -28,7 +30,10 @@ namespace OpenTelemetry.Exporter { internal readonly PrometheusExporterOptions Options; internal Batch Metrics; + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); + private readonly PrometheusExporterMetricsHttpServer metricsHttpServer; private Func funcCollect; + private bool disposed; /// /// Initializes a new instance of the class. @@ -37,12 +42,25 @@ namespace OpenTelemetry.Exporter public PrometheusExporter(PrometheusExporterOptions 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 Collect { get => this.funcCollect; - set { this.funcCollect = value; } + set => this.funcCollect = value; } public override ExportResult Export(in Batch metrics) @@ -50,5 +68,31 @@ namespace OpenTelemetry.Exporter this.Metrics = metrics; 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); + } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterApplicationBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterApplicationBuilderExtensions.cs new file mode 100644 index 000000000..f88fc1b89 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterApplicationBuilderExtensions.cs @@ -0,0 +1,59 @@ +// +// 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. +// + +#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 +{ + /// + /// Provides extension methods for to add Prometheus Scraper Endpoint. + /// + public static class PrometheusExporterApplicationBuilderExtensions + { + /// + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. + /// + /// The to add + /// middleware to. + /// Optional + /// containing a otherwise the primary + /// SDK provider will be resolved using application services. + /// A reference to the instance after the operation has completed. + public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, MeterProvider meterProvider = null) + { + var options = app.ApplicationServices.GetOptions(); + + string path = options.ScrapeEndpointPath ?? PrometheusExporterOptions.DefaultScrapeEndpointPath; + if (!path.StartsWith("/")) + { + path = $"/{path}"; + } + + return app.Map( + new PathString(path), + builder => builder.UseMiddleware(meterProvider ?? app.ApplicationServices.GetRequiredService())); + } + } +} +#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs deleted file mode 100644 index a0c7216b6..000000000 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs +++ /dev/null @@ -1,192 +0,0 @@ -// -// 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. -// - -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 -{ - /// - /// Helper to write metrics collection from exporter in Prometheus format. - /// - 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"; - - /// - /// Serialize to Prometheus Format. - /// - /// Prometheus Exporter. - /// StreamWriter to write to. - 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; - } - } - } - } - - /// - /// Get Metrics Collection as a string. - /// - /// Prometheus Exporter. - /// Metrics serialized to string in Prometheus format. - 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()); - } - } - } - } -} diff --git a/src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMeterProviderBuilderExtensions.cs similarity index 62% rename from src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs rename to src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMeterProviderBuilderExtensions.cs index f7bfb6c33..3be9b5ead 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMeterProviderBuilderExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,15 +19,14 @@ using OpenTelemetry.Exporter; namespace OpenTelemetry.Metrics { - public static class MeterProviderBuilderExtensions + public static class PrometheusExporterMeterProviderBuilderExtensions { /// - /// Adds Console exporter to the TracerProvider. + /// Adds to the . /// /// builder to use. /// Exporter configuration options. /// The instance of to chain the calls. - [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 configure = null) { if (builder == null) @@ -35,14 +34,24 @@ namespace OpenTelemetry.Metrics throw new ArgumentNullException(nameof(builder)); } - var options = new PrometheusExporterOptions(); + if (builder is IDeferredMeterProviderBuilder deferredMeterProviderBuilder) + { + return deferredMeterProviderBuilder.Configure((sp, builder) => + { + AddPrometheusExporter(builder, sp.GetOptions(), configure); + }); + } + + return AddPrometheusExporter(builder, new PrometheusExporterOptions(), configure); + } + + private static MeterProviderBuilder AddPrometheusExporter(MeterProviderBuilder builder, PrometheusExporterOptions options, Action configure = null) + { configure?.Invoke(options); var exporter = new PrometheusExporter(options); var reader = new BaseExportingMetricReader(exporter); - var metricsHttpServer = new PrometheusExporterMetricsHttpServer(exporter); - metricsHttpServer.Start(); return builder.AddReader(reader); } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs deleted file mode 100644 index c692bc850..000000000 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -// 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. -// - -#if NETSTANDARD2_0 - -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; - -namespace OpenTelemetry.Exporter -{ - /// - /// A middleware used to expose Prometheus metrics. - /// - public class PrometheusExporterMiddleware - { - private readonly PrometheusExporter exporter; - - /// - /// Initializes a new instance of the class. - /// - /// The next middleware in the pipeline. - /// The instance. - public PrometheusExporterMiddleware(RequestDelegate next, PrometheusExporter exporter) - { - this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter)); - } - - /// - /// Invoke. - /// - /// context. - /// Task. - 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 diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs index ce4aae423..f12906374 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs @@ -14,16 +14,40 @@ // limitations under the License. // +using System.Collections.Generic; + namespace OpenTelemetry.Exporter { /// - /// Options to run prometheus exporter. + /// options. /// public class PrometheusExporterOptions { + internal const string DefaultScrapeEndpointPath = "/metrics"; + +#if NETCOREAPP3_1_OR_GREATER /// - /// 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. /// - public string Url { get; set; } = "http://localhost:9184/metrics/"; + public bool StartHttpListener { get; set; } +#else + /// + /// Gets or sets a value indicating whether or not an http listener + /// should be started. Default value: True. + /// + public bool StartHttpListener { get; set; } = true; +#endif + + /// + /// Gets or sets the prefixes to use for the http listener. Default + /// value: http://*:80/. + /// + public IReadOnlyCollection HttpListenerPrefixes { get; set; } = new string[] { "http://*:80/" }; + + /// + /// Gets or sets the path to use for the scraping endpoint. Default value: /metrics. + /// + public string ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath; } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs deleted file mode 100644 index 0b38d4734..000000000 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// 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. -// - -#if NETSTANDARD2_0 - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -namespace OpenTelemetry.Exporter.Prometheus -{ - /// - /// Provides extension methods for to add Prometheus Scraper Endpoint. - /// - public static class PrometheusRouteBuilderExtensions - { - private const string DefaultPath = "/metrics"; - - /// - /// Use prometheus extension. - /// - /// The to add middleware to. - /// A reference to the instance after the operation has completed. - 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()); - } - } -} -#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/README.md b/src/OpenTelemetry.Exporter.Prometheus/README.md index ed79f8d2f..29f1468e7 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/README.md +++ b/src/OpenTelemetry.Exporter.Prometheus/README.md @@ -7,17 +7,90 @@ * [Get Prometheus](https://prometheus.io/docs/introduction/first_steps/) -## Installation +## Steps to enable OpenTelemetry.Exporter.Prometheus + +### Step 1: Install Package ```shell 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 -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 [`TestPrometheusExporter.cs`](../../examples/Console/TestPrometheusExporter.cs) diff --git a/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs b/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs index 058d590be..aae8da125 100644 --- a/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs +++ b/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs @@ -65,6 +65,8 @@ namespace OpenTelemetry.Metrics } } + internal BaseExporter Exporter => this.exporter; + protected ExportModes SupportedExportModes => this.supportedExportModes; internal override void SetParentProvider(BaseProvider parentProvider) diff --git a/src/OpenTelemetry/Metrics/CompositeMetricReader.cs b/src/OpenTelemetry/Metrics/CompositeMetricReader.cs index c889bdec0..6720341a1 100644 --- a/src/OpenTelemetry/Metrics/CompositeMetricReader.cs +++ b/src/OpenTelemetry/Metrics/CompositeMetricReader.cs @@ -67,6 +67,8 @@ namespace OpenTelemetry.Metrics return this; } + public Enumerator GetEnumerator() => new Enumerator(this.head); + /// protected override bool ProcessMetrics(Batch metrics, int timeoutMilliseconds) { @@ -159,7 +161,32 @@ namespace OpenTelemetry.Metrics 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; diff --git a/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs b/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs index 416c61197..004cafeb4 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs @@ -120,5 +120,40 @@ namespace OpenTelemetry.Metrics return true; } + + public static bool TryFindExporter(this MeterProvider provider, out T exporter) + where T : BaseExporter + { + 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; + } + } } }