diff --git a/NuGet.config b/NuGet.config
index 6c0da1e86..d9c879908 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -3,6 +3,9 @@
+
diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln
index 4715e78c1..9669f7b6c 100644
--- a/OpenTelemetry.sln
+++ b/OpenTelemetry.sln
@@ -158,6 +158,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "trace", "trace", "{5B7FB835
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started", "docs\trace\getting-started\getting-started.csproj", "{BE60E3D5-DE30-4BAB-8E7A-63B21D0E80D7}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metrics", "metrics", "{3277B1C0-BDFE-4460-9B0D-D9A661FB48DB}"
+ ProjectSection(SolutionItems) = preProject
+ docs\metrics\building-your-own-exporter.md = docs\metrics\building-your-own-exporter.md
+ EndProjectSection
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "logs", "logs", "{3862190B-E2C5-418E-AFDC-DB281FB5C705}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MicroserviceExample", "MicroserviceExample", "{4D492D62-5150-45F9-817F-C99562E364E2}"
@@ -200,6 +205,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "exception-reporting", "docs
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "customizing-the-sdk", "docs\trace\customizing-the-sdk\customizing-the-sdk.csproj", "{64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started", "docs\metrics\getting-started\getting-started.csproj", "{DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Exporter.Prometheus", "src\OpenTelemetry.Exporter.Prometheus\OpenTelemetry.Exporter.Prometheus.csproj", "{52158A12-E7EF-45A1-859F-06F9B17410CB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -394,6 +403,14 @@ Global
{64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {52158A12-E7EF-45A1-859F-06F9B17410CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {52158A12-E7EF-45A1-859F-06F9B17410CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {52158A12-E7EF-45A1-859F-06F9B17410CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {52158A12-E7EF-45A1-859F-06F9B17410CB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -411,6 +428,7 @@ Global
{2C7DD1DA-C229-4D9E-9AF0-BCD5CD3E4948} = {7CB2F02E-03FA-4FFF-89A5-C51F107623FD}
{5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1}
{BE60E3D5-DE30-4BAB-8E7A-63B21D0E80D7} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}
+ {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1}
{3862190B-E2C5-418E-AFDC-DB281FB5C705} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1}
{4D492D62-5150-45F9-817F-C99562E364E2} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E}
{07336602-860B-4975-95DD-405D19C00901} = {4D492D62-5150-45F9-817F-C99562E364E2}
@@ -424,6 +442,7 @@ Global
{972396A8-E35B-499C-9BA1-765E9B8822E1} = {77C7929A-2EED-4AA6-8705-B5C443C8AA0F}
{08D29501-F0A3-468F-B18D-BD1821A72383} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}
{64E3D8BB-93AB-4571-93F7-ED8D64DFFD06} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818}
+ {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521}
diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props
index 332011980..3c4b3d4f2 100644
--- a/build/Common.nonprod.props
+++ b/build/Common.nonprod.props
@@ -15,7 +15,9 @@
PreserveNewest
-
+
+
+
+```text
+Export[] 16:38:36.241 16:38:37.233 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 590, Details: Delta=True,Mon=True,Count=59,Sum=590
+Export[] 16:38:37.233 16:38:38.258 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 640, Details: Delta=True,Mon=True,Count=64,Sum=640
+Export[] 16:38:38.258 16:38:39.261 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 640, Details: Delta=True,Mon=True,Count=64,Sum=640
+Export[] 16:38:39.261 16:38:40.266 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 630, Details: Delta=True,Mon=True,Count=63,Sum=630
+Export[] 16:38:40.266 16:38:41.271 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 640, Details: Delta=True,Mon=True,Count=64,Sum=640
+```
+
+
+Congratulations! You are now collecting metrics using OpenTelemetry.
+
+What does the above program do?
+
+The program creates a
+[Meter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meter)
+instance named "TestMeter" and then creates a
+[Counter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#counter)
+instrument from it. This counter is used to repeatedly report metric
+measurements until exited after 10 seconds.
+
+An OpenTelemetry
+[MeterProvider](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meterprovider)
+is configured to subscribe to instruments from the Meter `TestMeter`, and
+aggregate the measurements in-memory. The pre-aggregated metrics are exported
+every 1 second to a `ConsoleExporter`. `ConsoleExporter` simply displays it on
+the console.
diff --git a/docs/metrics/getting-started/getting-started.csproj b/docs/metrics/getting-started/getting-started.csproj
new file mode 100644
index 000000000..9f5b6b79b
--- /dev/null
+++ b/docs/metrics/getting-started/getting-started.csproj
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/examples/AspNetCore/Startup.cs b/examples/AspNetCore/Startup.cs
index 516d9b826..6d43c6622 100644
--- a/examples/AspNetCore/Startup.cs
+++ b/examples/AspNetCore/Startup.cs
@@ -23,8 +23,10 @@ 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;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
@@ -32,6 +34,8 @@ namespace Examples.AspNetCore
{
public class Startup
{
+ private MeterProvider meterProvider;
+
public Startup(IConfiguration configuration)
{
this.Configuration = configuration;
@@ -112,6 +116,16 @@ namespace Examples.AspNetCore
break;
}
+
+ // TODO: Add IServiceCollection.AddOpenTelemetryMetrics extension method
+ var providerBuilder = Sdk.CreateMeterProviderBuilder()
+ .AddAspNetCoreInstrumentation();
+
+ // TODO: Add configuration switch for Prometheus and OTLP export
+ providerBuilder
+ .AddConsoleExporter();
+
+ this.meterProvider = providerBuilder.Build();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
diff --git a/examples/Console/Examples.Console.csproj b/examples/Console/Examples.Console.csproj
index 33a1f6a41..84ecf4f5b 100644
--- a/examples/Console/Examples.Console.csproj
+++ b/examples/Console/Examples.Console.csproj
@@ -36,5 +36,6 @@
+
diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs
index 0e4023302..fd78e75b8 100644
--- a/examples/Console/Program.cs
+++ b/examples/Console/Program.cs
@@ -34,6 +34,7 @@ namespace Examples.Console
/// dotnet run -p Examples.Console.csproj prometheus -i 15 -p 9184 -d 2
/// dotnet run -p Examples.Console.csproj otlp -e "http://localhost:4317"
/// dotnet run -p Examples.Console.csproj zpages
+ /// dotnet run -p Examples.Console.csproj metrics --help
///
/// The above must be run from the project root folder
/// (eg: C:\repos\opentelemetry-dotnet\examples\Console\).
@@ -41,10 +42,12 @@ namespace Examples.Console
/// Arguments from command line.
public static void Main(string[] args)
{
- Parser.Default.ParseArguments(args)
+ Parser.Default.ParseArguments(args)
.MapResult(
(JaegerOptions options) => TestJaegerExporter.Run(options.Host, options.Port),
(ZipkinOptions options) => TestZipkinExporter.Run(options.Uri),
+ (PrometheusOptions options) => TestPrometheusExporter.Run(options.Port, options.DurationInMins),
+ (MetricsOptions options) => TestMetrics.Run(options),
(GrpcNetClientOptions options) => TestGrpcNetClient.Run(),
(HttpClientOptions options) => TestHttpClient.Run(),
(RedisOptions options) => TestRedis.Run(options.Uri),
@@ -55,8 +58,6 @@ namespace Examples.Console
(OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint),
(InMemoryOptions options) => TestInMemoryExporter.Run(options),
errs => 1);
-
- System.Console.ReadLine();
}
}
@@ -79,6 +80,50 @@ namespace Examples.Console
public string Uri { get; set; }
}
+ [Verb("prometheus", HelpText = "Specify the options required to test Prometheus")]
+ internal class PrometheusOptions
+ {
+ [Option('p', "port", Default = 9184, HelpText = "The port to expose metrics. The endpoint will be http://localhost:port/metrics (This is the port from which your Prometheus server scraps metrics from.)", Required = false)]
+ public int Port { get; set; }
+
+ [Option('d', "duration", Default = 2, HelpText = "Total duration in minutes to run the demo.", Required = false)]
+ public int DurationInMins { get; set; }
+ }
+
+ [Verb("metrics", HelpText = "Specify the options required to test Metrics")]
+ internal class MetricsOptions
+ {
+ [Option('d', "IsDelta", HelpText = "Export Delta metrics", Required = false, Default = true)]
+ public bool IsDelta { get; set; }
+
+ [Option('g', "Gauge", HelpText = "Include Observable Gauge.", Required = false)]
+ public bool? FlagGauge { get; set; }
+
+ [Option('u', "UpDownCounter", HelpText = "Include Observable Up/Down Counter.", Required = false)]
+ public bool? FlagUpDownCounter { get; set; }
+
+ [Option('c', "Counter", HelpText = "Include Counter.", Required = false)]
+ public bool? FlagCounter { get; set; }
+
+ [Option('h', "Histogram", HelpText = "Include Histogram.", Required = false)]
+ public bool? FlagHistogram { get; set; }
+
+ [Option("defaultCollection", Default = 500, HelpText = "Default collection period in milliseconds.", Required = false)]
+ public int DefaultCollectionPeriodMilliseconds { get; set; }
+
+ [Option("runtime", Default = 5000, HelpText = "Run time in milliseconds.", Required = false)]
+ public int RunTime { get; set; }
+
+ [Option("tasks", Default = 1, HelpText = "Run # of concurrent tasks.", Required = false)]
+ public int NumTasks { get; set; }
+
+ [Option("maxLoops", Default = 0, HelpText = "Maximum number of loops/iterations per task. (0 = No Limit)", Required = false)]
+ public int MaxLoops { get; set; }
+
+ [Option("useExporter", Default = "console", HelpText = "Options include otlp or console.", Required = false)]
+ public string UseExporter { get; set; }
+ }
+
[Verb("grpc", HelpText = "Specify the options required to test Grpc.Net.Client")]
internal class GrpcNetClientOptions
{
diff --git a/examples/Console/TestGrpcNetClient.cs b/examples/Console/TestGrpcNetClient.cs
index ade965950..a15bd6fbd 100644
--- a/examples/Console/TestGrpcNetClient.cs
+++ b/examples/Console/TestGrpcNetClient.cs
@@ -66,6 +66,7 @@ namespace Examples.Console
}
System.Console.WriteLine("Press Enter key to exit.");
+ System.Console.ReadLine();
return null;
}
diff --git a/examples/Console/TestHttpClient.cs b/examples/Console/TestHttpClient.cs
index 1e08e48d5..78d0047a9 100644
--- a/examples/Console/TestHttpClient.cs
+++ b/examples/Console/TestHttpClient.cs
@@ -47,6 +47,7 @@ namespace Examples.Console
}
System.Console.WriteLine("Press Enter key to exit.");
+ System.Console.ReadLine();
return null;
}
diff --git a/examples/Console/TestMetrics.cs b/examples/Console/TestMetrics.cs
new file mode 100644
index 000000000..e4ece9e12
--- /dev/null
+++ b/examples/Console/TestMetrics.cs
@@ -0,0 +1,203 @@
+//
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+
+namespace Examples.Console
+{
+ internal class TestMetrics
+ {
+ internal static object Run(MetricsOptions options)
+ {
+ var providerBuilder = Sdk.CreateMeterProviderBuilder()
+ .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("myservice"))
+ .AddSource("TestMeter"); // All instruments from this meter are enabled.
+
+ if (options.UseExporter.ToLower() == "otlp")
+ {
+ /*
+ * Prerequisite to run this example:
+ * Set up an OpenTelemetry Collector to run on local docker.
+ *
+ * Open a terminal window at the examples/Console/ directory and
+ * launch the OpenTelemetry Collector with an OTLP receiver, by running:
+ *
+ * - On Unix based systems use:
+ * docker run --rm -it -p 4317:4317 -v $(pwd):/cfg otel/opentelemetry-collector:0.28.0 --config=/cfg/otlp-collector-example/config.yaml
+ *
+ * - On Windows use:
+ * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.28.0 --config=/cfg/otlp-collector-example/config.yaml
+ *
+ * Open another terminal window at the examples/Console/ directory and
+ * launch the OTLP example by running:
+ *
+ * dotnet run metrics --useExporter otlp
+ *
+ * The OpenTelemetry Collector will output all received metrics to the stdout of its terminal.
+ *
+ */
+
+ // Adding the OtlpExporter creates a GrpcChannel.
+ // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service.
+ // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client
+ AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
+
+ providerBuilder
+ .AddOtlpExporter(o =>
+ {
+ o.MetricExportInterval = options.DefaultCollectionPeriodMilliseconds;
+ o.IsDelta = options.IsDelta;
+ });
+ }
+ else
+ {
+ providerBuilder
+ .AddConsoleExporter(o =>
+ {
+ o.MetricExportInterval = options.DefaultCollectionPeriodMilliseconds;
+ o.IsDelta = options.IsDelta;
+ });
+ }
+
+ using var provider = providerBuilder.Build();
+
+ using var meter = new Meter("TestMeter", "0.0.1");
+
+ Counter counter = null;
+ if (options.FlagCounter ?? true)
+ {
+ counter = meter.CreateCounter("counter", "things", "A count of things");
+ }
+
+ Histogram histogram = null;
+ if (options.FlagHistogram ?? false)
+ {
+ histogram = meter.CreateHistogram("histogram");
+ }
+
+ if (options.FlagGauge ?? false)
+ {
+ var observableCounter = meter.CreateObservableGauge("gauge", () =>
+ {
+ return new List>()
+ {
+ new Measurement(
+ (int)Process.GetCurrentProcess().PrivateMemorySize64,
+ new KeyValuePair("tag1", "value1")),
+ };
+ });
+ }
+
+ if (options.FlagUpDownCounter ?? true)
+ {
+ var observableCounter = meter.CreateObservableCounter("updown", () =>
+ {
+ return new List>()
+ {
+ new Measurement(
+ (int)Process.GetCurrentProcess().PrivateMemorySize64,
+ new KeyValuePair("tag1", "value1")),
+ };
+ });
+ }
+
+ var cts = new CancellationTokenSource();
+
+ var tasks = new List();
+
+ for (int i = 0; i < options.NumTasks; i++)
+ {
+ var taskno = i;
+
+ tasks.Add(Task.Run(() =>
+ {
+ System.Console.WriteLine($"Task started {taskno + 1}/{options.NumTasks}.");
+
+ var loops = 0;
+
+ while (!cts.IsCancellationRequested)
+ {
+ if (options.MaxLoops > 0 && loops >= options.MaxLoops)
+ {
+ break;
+ }
+
+ histogram?.Record(10);
+
+ histogram?.Record(
+ 100,
+ new KeyValuePair("tag1", "value1"));
+
+ histogram?.Record(
+ 200,
+ new KeyValuePair("tag1", "value2"),
+ new KeyValuePair("tag2", "value2"));
+
+ histogram?.Record(
+ 100,
+ new KeyValuePair("tag1", "value1"));
+
+ histogram?.Record(
+ 200,
+ new KeyValuePair("tag2", "value2"),
+ new KeyValuePair("tag1", "value2"));
+
+ counter?.Add(10);
+
+ counter?.Add(
+ 100,
+ new KeyValuePair("tag1", "value1"));
+
+ counter?.Add(
+ 200,
+ new KeyValuePair("tag1", "value2"),
+ new KeyValuePair("tag2", "value2"));
+
+ counter?.Add(
+ 100,
+ new KeyValuePair("tag1", "value1"));
+
+ counter?.Add(
+ 200,
+ new KeyValuePair("tag2", "value2"),
+ new KeyValuePair("tag1", "value2"));
+
+ loops++;
+ }
+ }));
+ }
+
+ cts.CancelAfter(options.RunTime);
+ System.Console.WriteLine($"Wait for {options.RunTime} milliseconds.");
+ while (!cts.IsCancellationRequested)
+ {
+ Task.Delay(1000).Wait();
+ }
+
+ Task.WaitAll(tasks.ToArray());
+
+ return null;
+ }
+ }
+}
diff --git a/examples/Console/TestOTelShimWithConsoleExporter.cs b/examples/Console/TestOTelShimWithConsoleExporter.cs
index cb439b8a0..984ac7ad3 100644
--- a/examples/Console/TestOTelShimWithConsoleExporter.cs
+++ b/examples/Console/TestOTelShimWithConsoleExporter.cs
@@ -51,6 +51,7 @@ namespace Examples.Console
}
System.Console.WriteLine("Press Enter key to exit.");
+ System.Console.ReadLine();
return null;
}
diff --git a/examples/Console/TestOpenTracingShim.cs b/examples/Console/TestOpenTracingShim.cs
index a0fbdf9c2..a5d75eac7 100644
--- a/examples/Console/TestOpenTracingShim.cs
+++ b/examples/Console/TestOpenTracingShim.cs
@@ -60,6 +60,7 @@ namespace Examples.Console
}
System.Console.WriteLine("Press Enter key to exit.");
+ System.Console.ReadLine();
return null;
}
diff --git a/examples/Console/TestPrometheusExporter.cs b/examples/Console/TestPrometheusExporter.cs
new file mode 100644
index 000000000..34ef6efcf
--- /dev/null
+++ b/examples/Console/TestPrometheusExporter.cs
@@ -0,0 +1,83 @@
+//
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Examples.Console
+{
+ internal class TestPrometheusExporter
+ {
+ private static readonly Meter MyMeter = new Meter("TestMeter", "0.0.1");
+ private static readonly Counter Counter = MyMeter.CreateCounter("counter");
+
+ internal static object Run(int port, int totalDurationInMins)
+ {
+ /*
+ Following is sample prometheus.yml config. Adjust port,interval as needed.
+
+ scrape_configs:
+ # The job name is added as a label `job=` to any timeseries scraped from this config.
+ - job_name: 'OpenTelemetryTest'
+
+ # metrics_path defaults to '/metrics'
+ # scheme defaults to 'http'.
+
+ static_configs:
+ - targets: ['localhost:9184']
+ */
+ using var meterProvider = Sdk.CreateMeterProviderBuilder()
+ .AddSource("TestMeter")
+ .AddPrometheusExporter(opt => opt.Url = $"http://localhost:{port}/metrics/")
+ .Build();
+
+ using var token = new CancellationTokenSource();
+ Task writeMetricTask = new Task(() =>
+ {
+ while (!token.IsCancellationRequested)
+ {
+ Counter.Add(
+ 10,
+ new KeyValuePair("tag1", "value1"),
+ new KeyValuePair("tag2", "value2"));
+
+ Counter.Add(
+ 100,
+ new KeyValuePair("tag1", "anothervalue"),
+ new KeyValuePair("tag2", "somethingelse"));
+ Task.Delay(10).Wait();
+ }
+ });
+ writeMetricTask.Start();
+
+ token.CancelAfter(totalDurationInMins * 60 * 1000);
+
+ System.Console.WriteLine($"OpenTelemetry Prometheus Exporter is making metrics available at http://localhost:{port}/metrics/");
+ System.Console.WriteLine($"Press Enter key to exit now or will exit automatically after {totalDurationInMins} minutes.");
+ System.Console.ReadLine();
+ token.Cancel();
+ System.Console.WriteLine("Exiting...");
+ return null;
+ }
+ }
+}
diff --git a/examples/Console/TestRedis.cs b/examples/Console/TestRedis.cs
index 745b07704..12981cc79 100644
--- a/examples/Console/TestRedis.cs
+++ b/examples/Console/TestRedis.cs
@@ -70,6 +70,9 @@ namespace Examples.Console
}
}
+ System.Console.Write("Press ENTER to stop.");
+ System.Console.ReadLine();
+
return null;
}
diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md
index c14b144e3..ed3d6108a 100644
--- a/src/OpenTelemetry.Api/CHANGELOG.md
+++ b/src/OpenTelemetry.Api/CHANGELOG.md
@@ -7,6 +7,9 @@ branch](https://github.com/open-telemetry/opentelemetry-dotnet/tree/metrics),
please check the latest changes
[here](https://github.com/open-telemetry/opentelemetry-dotnet/blob/metrics/src/OpenTelemetry.Api/CHANGELOG.md#experimental---metrics).
+* Removed existing Metrics code as the spec is completely being re-written.
+ ([#2030](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2030))
+
## Unreleased
* Removes .NET Framework 4.5.2 support. The minimum .NET Framework
@@ -81,9 +84,6 @@ Released 2021-Jan-29
* `Status.WithDescription` will now ignore the provided description if the
`Status.StatusCode` is anything other than `ERROR`.
([#1655](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1655))
-* Metrics removed as it is not part 1.0.0 release. See issue
- [#1501](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1655) for
- details on Metric release plans.
* Relax System.Diagnostics.DiagnosticSource version requirement to allow
versions >=5.0. Previously only versions up to 6.0 (excluding 6.0) was
allowed.
diff --git a/src/OpenTelemetry.Api/Metrics/MeterProvider.cs b/src/OpenTelemetry.Api/Metrics/MeterProvider.cs
new file mode 100644
index 000000000..80859e56c
--- /dev/null
+++ b/src/OpenTelemetry.Api/Metrics/MeterProvider.cs
@@ -0,0 +1,31 @@
+//
+// 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.
+//
+
+namespace OpenTelemetry.Metrics
+{
+ ///
+ /// MeterProvider base class.
+ ///
+ public class MeterProvider : BaseProvider
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected MeterProvider()
+ {
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs
new file mode 100644
index 000000000..f5bb6dc9f
--- /dev/null
+++ b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs
@@ -0,0 +1,49 @@
+//
+// 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;
+
+namespace OpenTelemetry.Metrics
+{
+ ///
+ /// TracerProviderBuilder base class.
+ ///
+ public abstract class MeterProviderBuilder
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected MeterProviderBuilder()
+ {
+ }
+
+ ///
+ /// Adds instrumentation to the provider.
+ ///
+ /// Type of instrumentation class.
+ /// Function that builds instrumentation.
+ /// Returns for chaining.
+ public abstract MeterProviderBuilder AddInstrumentation(
+ Func instrumentationFactory)
+ where TInstrumentation : class;
+
+ ///
+ /// Adds given Meter names to the list of subscribed sources.
+ ///
+ /// Meter source names.
+ /// Returns for chaining.
+ public abstract MeterProviderBuilder AddSource(params string[] names);
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricHelperExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricHelperExtensions.cs
new file mode 100644
index 000000000..2e8c4d56d
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricHelperExtensions.cs
@@ -0,0 +1,43 @@
+//
+// 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;
+using OpenTelemetry.Exporter;
+
+namespace OpenTelemetry.Metrics
+{
+ public static class ConsoleExporterMetricHelperExtensions
+ {
+ ///
+ /// Adds Console exporter to the TracerProvider.
+ ///
+ /// 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 AddConsoleExporter(this MeterProviderBuilder builder, Action configure = null)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ var options = new ConsoleExporterOptions();
+ configure?.Invoke(options);
+ return builder.AddMetricProcessor(new PushMetricProcessor(new ConsoleMetricExporter(options), options.MetricExportInterval, options.IsDelta));
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs
index a7183ea48..917b0fe41 100644
--- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs
+++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs
@@ -22,5 +22,16 @@ namespace OpenTelemetry.Exporter
/// Gets or sets the output targets for the console exporter.
///
public ConsoleExporterOutputTargets Targets { get; set; } = ConsoleExporterOutputTargets.Console;
+
+ ///
+ /// Gets or sets the metric export interval.
+ ///
+ public int MetricExportInterval { get; set; } = 1000;
+
+ ///
+ /// Gets or sets a value indicating whether to export Delta
+ /// values or not (Cumulative).
+ ///
+ public bool IsDelta { get; set; } = true;
}
}
diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs
new file mode 100644
index 000000000..a2904f886
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs
@@ -0,0 +1,125 @@
+//
+// 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;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+
+namespace OpenTelemetry.Exporter
+{
+ public class ConsoleMetricExporter : ConsoleExporter
+ {
+ private Resource resource;
+
+ public ConsoleMetricExporter(ConsoleExporterOptions options)
+ : base(options)
+ {
+ }
+
+ public override ExportResult Export(in Batch batch)
+ {
+ if (this.resource == null)
+ {
+ this.resource = this.ParentProvider.GetResource();
+ if (this.resource != Resource.Empty)
+ {
+ foreach (var resourceAttribute in this.resource.Attributes)
+ {
+ if (resourceAttribute.Key.Equals("service.name"))
+ {
+ Console.WriteLine("Service.Name" + resourceAttribute.Value);
+ }
+ }
+ }
+ }
+
+ foreach (var metricItem in batch)
+ {
+ foreach (var metric in metricItem.Metrics)
+ {
+ var tags = metric.Attributes.ToArray().Select(k => $"{k.Key}={k.Value?.ToString()}");
+
+ string valueDisplay = string.Empty;
+ if (metric is ISumMetric sumMetric)
+ {
+ if (sumMetric.Sum.Value is double doubleSum)
+ {
+ valueDisplay = ((double)doubleSum).ToString(CultureInfo.InvariantCulture);
+ }
+ else if (sumMetric.Sum.Value is long longSum)
+ {
+ valueDisplay = ((long)longSum).ToString();
+ }
+ }
+ else if (metric is IGaugeMetric gaugeMetric)
+ {
+ if (gaugeMetric.LastValue.Value is double doubleValue)
+ {
+ valueDisplay = ((double)doubleValue).ToString();
+ }
+ else if (gaugeMetric.LastValue.Value is long longValue)
+ {
+ valueDisplay = ((long)longValue).ToString();
+ }
+
+ // Qn: tags again ? gaugeMetric.LastValue.Tags
+ }
+ else if (metric is ISummaryMetric summaryMetric)
+ {
+ valueDisplay = string.Format("Sum: {0} Count: {1}", summaryMetric.PopulationSum, summaryMetric.PopulationCount);
+ }
+ else if (metric is IHistogramMetric histogramMetric)
+ {
+ valueDisplay = string.Format("Sum: {0} Count: {1}", histogramMetric.PopulationSum, histogramMetric.PopulationCount);
+ }
+
+ var kind = metric.GetType().Name;
+
+ string time = $"{metric.StartTimeExclusive.ToLocalTime().ToString("HH:mm:ss.fff")} {metric.EndTimeInclusive.ToLocalTime().ToString("HH:mm:ss.fff")}";
+
+ var msg = new StringBuilder($"Export {time} {metric.Name} [{string.Join(";", tags)}] {kind} Value: {valueDisplay}");
+
+ if (!string.IsNullOrEmpty(metric.Description))
+ {
+ msg.Append($", Description: {metric.Description}");
+ }
+
+ if (!string.IsNullOrEmpty(metric.Unit))
+ {
+ msg.Append($", Unit: {metric.Unit}");
+ }
+
+ if (!string.IsNullOrEmpty(metric.Meter.Name))
+ {
+ msg.Append($", Meter: {metric.Meter.Name}");
+
+ if (!string.IsNullOrEmpty(metric.Meter.Version))
+ {
+ msg.Append($"/{metric.Meter.Version}");
+ }
+ }
+
+ Console.WriteLine(msg);
+ }
+ }
+
+ return ExportResult.Success;
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs
new file mode 100644
index 000000000..f470e9a3c
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs
@@ -0,0 +1,325 @@
+//
+// 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;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.CompilerServices;
+using Google.Protobuf;
+using Google.Protobuf.Collections;
+using OpenTelemetry.Metrics;
+using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1;
+using OtlpCommon = Opentelemetry.Proto.Common.V1;
+using OtlpMetrics = Opentelemetry.Proto.Metrics.V1;
+using OtlpResource = Opentelemetry.Proto.Resource.V1;
+
+namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
+{
+ internal static class MetricItemExtensions
+ {
+ private static readonly ConcurrentBag MetricListPool = new ConcurrentBag();
+ private static readonly Action, int> RepeatedFieldOfMetricSetCountAction = CreateRepeatedFieldOfMetricSetCountAction();
+
+ internal static void AddBatch(
+ this OtlpCollector.ExportMetricsServiceRequest request,
+ OtlpResource.Resource processResource,
+ in Batch batch)
+ {
+ var metricsByLibrary = new Dictionary();
+ var resourceMetrics = new OtlpMetrics.ResourceMetrics
+ {
+ Resource = processResource,
+ };
+ request.ResourceMetrics.Add(resourceMetrics);
+
+ foreach (var metricItem in batch)
+ {
+ foreach (var metric in metricItem.Metrics)
+ {
+ var otlpMetric = metric.ToOtlpMetric();
+ if (otlpMetric == null)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateMetric(
+ nameof(MetricItemExtensions),
+ nameof(AddBatch));
+ continue;
+ }
+
+ var meterName = metric.Meter.Name;
+ if (!metricsByLibrary.TryGetValue(meterName, out var metrics))
+ {
+ metrics = GetMetricListFromPool(meterName, metric.Meter.Version);
+
+ metricsByLibrary.Add(meterName, metrics);
+ resourceMetrics.InstrumentationLibraryMetrics.Add(metrics);
+ }
+
+ metrics.Metrics.Add(otlpMetric);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static void Return(this OtlpCollector.ExportMetricsServiceRequest request)
+ {
+ var resourceMetrics = request.ResourceMetrics.FirstOrDefault();
+ if (resourceMetrics == null)
+ {
+ return;
+ }
+
+ foreach (var libraryMetrics in resourceMetrics.InstrumentationLibraryMetrics)
+ {
+ RepeatedFieldOfMetricSetCountAction(libraryMetrics.Metrics, 0);
+ MetricListPool.Add(libraryMetrics);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static OtlpMetrics.InstrumentationLibraryMetrics GetMetricListFromPool(string name, string version)
+ {
+ if (!MetricListPool.TryTake(out var metrics))
+ {
+ metrics = new OtlpMetrics.InstrumentationLibraryMetrics
+ {
+ InstrumentationLibrary = new OtlpCommon.InstrumentationLibrary
+ {
+ Name = name, // Name is enforced to not be null, but it can be empty.
+ Version = version ?? string.Empty, // NRE throw by proto
+ },
+ };
+ }
+ else
+ {
+ metrics.InstrumentationLibrary.Name = name;
+ metrics.InstrumentationLibrary.Version = version ?? string.Empty;
+ }
+
+ return metrics;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static OtlpMetrics.Metric ToOtlpMetric(this IMetric metric)
+ {
+ var otlpMetric = new OtlpMetrics.Metric
+ {
+ Name = metric.Name,
+ Description = metric.Description,
+ Unit = metric.Unit,
+ };
+
+ if (metric is ISumMetric sumMetric)
+ {
+ var sum = new OtlpMetrics.Sum
+ {
+ IsMonotonic = sumMetric.IsMonotonic,
+ AggregationTemporality = sumMetric.IsDeltaTemporality
+ ? OtlpMetrics.AggregationTemporality.Delta
+ : OtlpMetrics.AggregationTemporality.Cumulative,
+ };
+ var dataPoint = metric.ToNumberDataPoint(sumMetric.Sum, sumMetric.Exemplars);
+ sum.DataPoints.Add(dataPoint);
+ otlpMetric.Sum = sum;
+ }
+ else if (metric is IGaugeMetric gaugeMetric)
+ {
+ var gauge = new OtlpMetrics.Gauge();
+ var dataPoint = metric.ToNumberDataPoint(gaugeMetric.LastValue.Value, gaugeMetric.Exemplars);
+ gauge.DataPoints.Add(dataPoint);
+ otlpMetric.Gauge = gauge;
+ }
+ else if (metric is ISummaryMetric summaryMetric)
+ {
+ var summary = new OtlpMetrics.Summary();
+
+ var dataPoint = new OtlpMetrics.SummaryDataPoint
+ {
+ StartTimeUnixNano = (ulong)metric.StartTimeExclusive.ToUnixTimeNanoseconds(),
+ TimeUnixNano = (ulong)metric.EndTimeInclusive.ToUnixTimeNanoseconds(),
+ Count = (ulong)summaryMetric.PopulationCount,
+ Sum = summaryMetric.PopulationSum,
+ };
+
+ // TODO: Do TagEnumerationState thing.
+ foreach (var attribute in metric.Attributes)
+ {
+ dataPoint.Attributes.Add(attribute.ToOtlpAttribute());
+ }
+
+ foreach (var quantile in summaryMetric.Quantiles)
+ {
+ var quantileValue = new OtlpMetrics.SummaryDataPoint.Types.ValueAtQuantile
+ {
+ Quantile = quantile.Quantile,
+ Value = quantile.Value,
+ };
+ dataPoint.QuantileValues.Add(quantileValue);
+ }
+
+ otlpMetric.Summary = summary;
+ }
+ else if (metric is IHistogramMetric histogramMetric)
+ {
+ var histogram = new OtlpMetrics.Histogram
+ {
+ AggregationTemporality = histogramMetric.IsDeltaTemporality
+ ? OtlpMetrics.AggregationTemporality.Delta
+ : OtlpMetrics.AggregationTemporality.Cumulative,
+ };
+
+ var dataPoint = new OtlpMetrics.HistogramDataPoint
+ {
+ StartTimeUnixNano = (ulong)metric.StartTimeExclusive.ToUnixTimeNanoseconds(),
+ TimeUnixNano = (ulong)metric.EndTimeInclusive.ToUnixTimeNanoseconds(),
+ Count = (ulong)histogramMetric.PopulationCount,
+ Sum = histogramMetric.PopulationSum,
+ };
+
+ foreach (var bucket in histogramMetric.Buckets)
+ {
+ dataPoint.BucketCounts.Add((ulong)bucket.Count);
+
+ // TODO: Verify how to handle the bounds. We've modeled things with
+ // a LowBoundary and HighBoundary. OTLP data model has modeled this
+ // differently: https://github.com/open-telemetry/opentelemetry-proto/blob/bacfe08d84e21fb2a779e302d12e8dfeb67e7b86/opentelemetry/proto/metrics/v1/metrics.proto#L554-L568
+ dataPoint.ExplicitBounds.Add(bucket.HighBoundary);
+ }
+
+ // TODO: Do TagEnumerationState thing.
+ foreach (var attribute in metric.Attributes)
+ {
+ dataPoint.Attributes.Add(attribute.ToOtlpAttribute());
+ }
+
+ foreach (var exemplar in histogramMetric.Exemplars)
+ {
+ dataPoint.Exemplars.Add(exemplar.ToOtlpExemplar());
+ }
+
+ otlpMetric.Histogram = histogram;
+ }
+
+ return otlpMetric;
+ }
+
+ private static OtlpMetrics.NumberDataPoint ToNumberDataPoint(this IMetric metric, object value, IEnumerable exemplars)
+ {
+ var dataPoint = new OtlpMetrics.NumberDataPoint
+ {
+ StartTimeUnixNano = (ulong)metric.StartTimeExclusive.ToUnixTimeNanoseconds(),
+ TimeUnixNano = (ulong)metric.EndTimeInclusive.ToUnixTimeNanoseconds(),
+ };
+
+ if (value is double doubleValue)
+ {
+ dataPoint.AsDouble = doubleValue;
+ }
+ else if (value is long longValue)
+ {
+ dataPoint.AsInt = longValue;
+ }
+ else
+ {
+ // TODO: Determine how we want to handle exceptions here.
+ // Do we want to just skip this metric and move on?
+ throw new ArgumentException();
+ }
+
+ // TODO: Do TagEnumerationState thing.
+ foreach (var attribute in metric.Attributes)
+ {
+ dataPoint.Attributes.Add(attribute.ToOtlpAttribute());
+ }
+
+ foreach (var exemplar in exemplars)
+ {
+ dataPoint.Exemplars.Add(exemplar.ToOtlpExemplar());
+ }
+
+ return dataPoint;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar)
+ {
+ var otlpExemplar = new OtlpMetrics.Exemplar();
+
+ if (exemplar.Value is double doubleValue)
+ {
+ otlpExemplar.AsDouble = doubleValue;
+ }
+ else if (exemplar.Value is long longValue)
+ {
+ otlpExemplar.AsInt = longValue;
+ }
+ else
+ {
+ // TODO: Determine how we want to handle exceptions here.
+ // Do we want to just skip this exemplar and move on?
+ // Should we skip recording the whole metric?
+ throw new ArgumentException();
+ }
+
+ otlpExemplar.TimeUnixNano = (ulong)exemplar.Timestamp.ToUnixTimeNanoseconds();
+
+ // TODO: Do the TagEnumerationState thing.
+ foreach (var tag in exemplar.FilteredTags)
+ {
+ otlpExemplar.FilteredAttributes.Add(tag.ToOtlpAttribute());
+ }
+
+ if (exemplar.TraceId != default)
+ {
+ byte[] traceIdBytes = new byte[16];
+ exemplar.TraceId.CopyTo(traceIdBytes);
+ otlpExemplar.TraceId = UnsafeByteOperations.UnsafeWrap(traceIdBytes);
+ }
+
+ if (exemplar.SpanId != default)
+ {
+ byte[] spanIdBytes = new byte[8];
+ exemplar.SpanId.CopyTo(spanIdBytes);
+ otlpExemplar.SpanId = UnsafeByteOperations.UnsafeWrap(spanIdBytes);
+ }
+
+ return otlpExemplar;
+ }
+
+ private static Action, int> CreateRepeatedFieldOfMetricSetCountAction()
+ {
+ FieldInfo repeatedFieldOfMetricCountField = typeof(RepeatedField).GetField("count", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ DynamicMethod dynamicMethod = new DynamicMethod(
+ "CreateSetCountAction",
+ null,
+ new[] { typeof(RepeatedField), typeof(int) },
+ typeof(MetricItemExtensions).Module,
+ skipVisibility: true);
+
+ var generator = dynamicMethod.GetILGenerator();
+
+ generator.Emit(OpCodes.Ldarg_0);
+ generator.Emit(OpCodes.Ldarg_1);
+ generator.Emit(OpCodes.Stfld, repeatedFieldOfMetricCountField);
+ generator.Emit(OpCodes.Ret);
+
+ return (Action, int>)dynamicMethod.CreateDelegate(typeof(Action, int>));
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
index 826724621..27d4ec3de 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs
@@ -58,7 +58,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
this.WriteEvent(1, ex);
}
- [Event(2, Message = "Exporter failed send spans to collector. Data will not be sent. Exception: {0}", Level = EventLevel.Error)]
+ [Event(2, Message = "Exporter failed send data to collector. Data will not be sent. Exception: {0}", Level = EventLevel.Error)]
public void FailedToReachCollector(string ex)
{
this.WriteEvent(2, ex);
@@ -75,5 +75,11 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation
{
this.WriteEvent(4, ex);
}
+
+ [Event(5, Message = "Could not translate metric from class '{0}' and method '{1}', metric will not be recorded.", Level = EventLevel.Informational)]
+ public void CouldNotTranslateMetric(string className, string methodName)
+ {
+ this.WriteEvent(5, className, methodName);
+ }
}
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
index 267ee29f1..205b1929d 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj
@@ -32,10 +32,14 @@
-
+
+
+
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
index b6689e9fd..438400ac3 100644
--- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs
@@ -52,5 +52,16 @@ namespace OpenTelemetry.Exporter
/// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is Batch.
///
public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new BatchExportProcessorOptions();
+
+ ///
+ /// Gets or sets the metric export interval.
+ ///
+ public int MetricExportInterval { get; set; } = 1000;
+
+ ///
+ /// Gets or sets a value indicating whether to export Delta
+ /// values or not (Cumulative).
+ ///
+ public bool IsDelta { get; set; } = true;
}
}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterHelperExtensions.cs
new file mode 100644
index 000000000..b87d8ae5a
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterHelperExtensions.cs
@@ -0,0 +1,45 @@
+//
+// 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;
+using OpenTelemetry.Exporter;
+
+namespace OpenTelemetry.Metrics
+{
+ ///
+ /// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter.
+ ///
+ public static class OtlpMetricExporterHelperExtensions
+ {
+ ///
+ /// Adds OpenTelemetry Protocol (OTLP) exporter to the MeterProvider.
+ ///
+ /// builder to use.
+ /// Exporter configuration options.
+ /// The instance of to chain the calls.
+ public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder builder, Action configure = null)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ var options = new OtlpExporterOptions();
+ configure?.Invoke(options);
+ return builder.AddMetricProcessor(new PushMetricProcessor(new OtlpMetricsExporter(options), options.MetricExportInterval, options.IsDelta));
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs
new file mode 100644
index 000000000..b43cfdad4
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs
@@ -0,0 +1,206 @@
+//
+// 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;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Grpc.Core;
+#if NETSTANDARD2_1
+using Grpc.Net.Client;
+#endif
+using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1;
+using OtlpCommon = Opentelemetry.Proto.Common.V1;
+using OtlpResource = Opentelemetry.Proto.Resource.V1;
+
+namespace OpenTelemetry.Exporter
+{
+ ///
+ /// Exporter consuming and exporting the data using
+ /// the OpenTelemetry protocol (OTLP).
+ ///
+ public class OtlpMetricsExporter : BaseExporter
+ {
+ private readonly OtlpExporterOptions options;
+#if NETSTANDARD2_1
+ private readonly GrpcChannel channel;
+#else
+ private readonly Channel channel;
+#endif
+ private readonly OtlpCollector.MetricsService.MetricsServiceClient metricsClient;
+ private readonly Metadata headers;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Configuration options for the exporter.
+ public OtlpMetricsExporter(OtlpExporterOptions options)
+ : this(options, null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Configuration options for the exporter.
+ /// .
+ internal OtlpMetricsExporter(OtlpExporterOptions options, OtlpCollector.MetricsService.MetricsServiceClient metricsServiceClient = null)
+ {
+ this.options = options ?? throw new ArgumentNullException(nameof(options));
+ this.headers = GetMetadataFromHeaders(options.Headers);
+ if (this.options.TimeoutMilliseconds <= 0)
+ {
+ throw new ArgumentException("Timeout value provided is not a positive number.", nameof(this.options.TimeoutMilliseconds));
+ }
+
+ if (metricsServiceClient != null)
+ {
+ this.metricsClient = metricsServiceClient;
+ }
+ else
+ {
+ if (options.Endpoint.Scheme != Uri.UriSchemeHttp && options.Endpoint.Scheme != Uri.UriSchemeHttps)
+ {
+ throw new NotSupportedException($"Endpoint URI scheme ({options.Endpoint.Scheme}) is not supported. Currently only \"http\" and \"https\" are supported.");
+ }
+
+#if NETSTANDARD2_1
+ this.channel = GrpcChannel.ForAddress(options.Endpoint);
+#else
+ ChannelCredentials channelCredentials;
+ if (options.Endpoint.Scheme == Uri.UriSchemeHttps)
+ {
+ channelCredentials = new SslCredentials();
+ }
+ else
+ {
+ channelCredentials = ChannelCredentials.Insecure;
+ }
+
+ this.channel = new Channel(options.Endpoint.Authority, channelCredentials);
+#endif
+ this.metricsClient = new OtlpCollector.MetricsService.MetricsServiceClient(this.channel);
+ }
+ }
+
+ internal OtlpResource.Resource ProcessResource { get; private set; }
+
+ ///
+ public override ExportResult Export(in Batch batch)
+ {
+ if (this.ProcessResource == null)
+ {
+ this.SetResource(this.ParentProvider.GetResource());
+ }
+
+ // Prevents the exporter's gRPC and HTTP operations from being instrumented.
+ using var scope = SuppressInstrumentationScope.Begin();
+
+ var request = new OtlpCollector.ExportMetricsServiceRequest();
+
+ request.AddBatch(this.ProcessResource, batch);
+ var deadline = DateTime.UtcNow.AddMilliseconds(this.options.TimeoutMilliseconds);
+
+ try
+ {
+ this.metricsClient.Export(request, headers: this.headers, deadline: deadline);
+ }
+ catch (RpcException ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex);
+ return ExportResult.Failure;
+ }
+ catch (Exception ex)
+ {
+ OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex);
+ return ExportResult.Failure;
+ }
+ finally
+ {
+ request.Return();
+ }
+
+ return ExportResult.Success;
+ }
+
+ internal void SetResource(Resource resource)
+ {
+ OtlpResource.Resource processResource = new OtlpResource.Resource();
+
+ foreach (KeyValuePair attribute in resource.Attributes)
+ {
+ var oltpAttribute = attribute.ToOtlpAttribute();
+ if (oltpAttribute != null)
+ {
+ processResource.Attributes.Add(oltpAttribute);
+ }
+ }
+
+ if (!processResource.Attributes.Any(kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName))
+ {
+ var serviceName = (string)this.ParentProvider.GetDefaultResource().Attributes.Where(
+ kvp => kvp.Key == ResourceSemanticConventions.AttributeServiceName).FirstOrDefault().Value;
+ processResource.Attributes.Add(new OtlpCommon.KeyValue
+ {
+ Key = ResourceSemanticConventions.AttributeServiceName,
+ Value = new OtlpCommon.AnyValue { StringValue = serviceName },
+ });
+ }
+
+ this.ProcessResource = processResource;
+ }
+
+ ///
+ protected override bool OnShutdown(int timeoutMilliseconds)
+ {
+ if (this.channel == null)
+ {
+ return true;
+ }
+
+ return Task.WaitAny(new Task[] { this.channel.ShutdownAsync(), Task.Delay(timeoutMilliseconds) }) == 0;
+ }
+
+ private static Metadata GetMetadataFromHeaders(string headers)
+ {
+ var metadata = new Metadata();
+ if (!string.IsNullOrEmpty(headers))
+ {
+ Array.ForEach(
+ headers.Split(','),
+ (pair) =>
+ {
+ // Specify the maximum number of substrings to return to 2
+ // This treats everything that follows the first `=` in the string as the value to be added for the metadata key
+ var keyValueData = pair.Split(new char[] { '=' }, 2);
+ if (keyValueData.Length != 2)
+ {
+ throw new ArgumentException("Headers provided in an invalid format.");
+ }
+
+ var key = keyValueData[0].Trim();
+ var value = keyValueData[1].Trim();
+ metadata.Add(key, value);
+ });
+ }
+
+ return metadata;
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs
new file mode 100644
index 000000000..f53388400
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs
@@ -0,0 +1,76 @@
+//
+// 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;
+using System.Diagnostics.Tracing;
+using OpenTelemetry.Internal;
+
+namespace OpenTelemetry.Exporter.Prometheus.Implementation
+{
+ ///
+ /// EventSource events emitted from the project.
+ ///
+ [EventSource(Name = "OpenTelemetry-Exporter-Prometheus")]
+ internal class PrometheusExporterEventSource : EventSource
+ {
+ public static PrometheusExporterEventSource Log = new PrometheusExporterEventSource();
+
+ [NonEvent]
+ public void FailedExport(Exception ex)
+ {
+ if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1)))
+ {
+ this.FailedExport(ex.ToInvariantString());
+ }
+ }
+
+ [NonEvent]
+ public void FailedShutdown(Exception ex)
+ {
+ if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1)))
+ {
+ this.FailedShutdown(ex.ToInvariantString());
+ }
+ }
+
+ [NonEvent]
+ public void CanceledExport(Exception ex)
+ {
+ if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1)))
+ {
+ this.CanceledExport(ex.ToInvariantString());
+ }
+ }
+
+ [Event(1, Message = "Failed to export metrics: '{0}'", Level = EventLevel.Error)]
+ public void FailedExport(string exception)
+ {
+ this.WriteEvent(1, exception);
+ }
+
+ [Event(2, Message = "Canceled to export metrics: '{0}'", Level = EventLevel.Error)]
+ public void CanceledExport(string exception)
+ {
+ this.WriteEvent(2, exception);
+ }
+
+ [Event(3, Message = "Failed to shutdown Metrics server '{0}'", Level = EventLevel.Error)]
+ public void FailedShutdown(string exception)
+ {
+ this.WriteEvent(3, exception);
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs
new file mode 100644
index 000000000..a11c8e513
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs
@@ -0,0 +1,276 @@
+//
+// 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;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+#if NET452
+using OpenTelemetry.Internal;
+#endif
+
+namespace OpenTelemetry.Exporter.Prometheus.Implementation
+{
+ internal class PrometheusMetricBuilder
+ {
+ public const string ContentType = "text/plain; version = 0.0.4";
+
+ private static readonly char[] FirstCharacterNameCharset =
+ {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ '_', ':',
+ };
+
+ private static readonly char[] NameCharset =
+ {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ '_', ':',
+ };
+
+ private static readonly char[] FirstCharacterLabelCharset =
+ {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ '_',
+ };
+
+ private static readonly char[] LabelCharset =
+ {
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ '_',
+ };
+
+ private readonly ICollection values = new List();
+
+ private string name;
+ private string description;
+ private string type;
+
+ public PrometheusMetricBuilder WithName(string name)
+ {
+ this.name = name;
+ return this;
+ }
+
+ public PrometheusMetricBuilder WithDescription(string description)
+ {
+ this.description = description;
+ return this;
+ }
+
+ public PrometheusMetricBuilder WithType(string type)
+ {
+ this.type = type;
+ return this;
+ }
+
+ public PrometheusMetricValueBuilder AddValue()
+ {
+ var val = new PrometheusMetricValueBuilder();
+
+ this.values.Add(val);
+
+ return val;
+ }
+
+ public void Write(StreamWriter writer)
+ {
+ // https://prometheus.io/docs/instrumenting/exposition_formats/
+
+ if (string.IsNullOrEmpty(this.name))
+ {
+ throw new InvalidOperationException("Metric name should not be empty");
+ }
+
+ this.name = GetSafeMetricName(this.name);
+
+ if (!string.IsNullOrEmpty(this.description))
+ {
+ // Lines with a # as the first non-whitespace character are comments.
+ // They are ignored unless the first token after # is either HELP or TYPE.
+ // Those lines are treated as follows: If the token is HELP, at least one
+ // more token is expected, which is the metric name. All remaining tokens
+ // are considered the docstring for that metric name. HELP lines may contain
+ // any sequence of UTF-8 characters (after the metric name), but the backslash
+ // 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");
+ }
+
+ if (!string.IsNullOrEmpty(this.type))
+ {
+ // If the token is TYPE, exactly two more tokens are expected. The first is the
+ // metric name, and the second is either counter, gauge, histogram, summary, or
+ // untyped, defining the type for the metric of that name. Only one TYPE line
+ // may exist for a given metric name. The TYPE line for a metric name must appear
+ // 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");
+ }
+
+ // The remaining lines describe samples (one per line) using the following syntax (EBNF):
+ // metric_name [
+ // "{" label_name "=" `"` label_value `"` { "," label_name "=" `"` label_value `"` } [ "," ] "}"
+ // ] value [ timestamp ]
+ // In the sample syntax:
+
+ var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture);
+
+ 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);
+
+ // label_value can be any sequence of UTF-8 characters, but the backslash
+ // (\, double-quote ("}, and line feed (\n) characters have to be escaped
+ // as \\, \", and \n, respectively.
+
+ if (m.Labels.Count > 0)
+ {
+ writer.Write(@"{");
+ writer.Write(string.Join(",", m.Labels.Select(x => GetLabelAndValue(x.Item1, x.Item2))));
+ writer.Write(@"}");
+ }
+
+ // 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(" ");
+
+ // 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);
+
+ // 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");
+ }
+
+ static string GetLabelAndValue(string label, string value)
+ {
+ var safeKey = GetSafeLabelName(label);
+ var safeValue = GetSafeLabelValue(value);
+ return $"{safeKey}=\"{safeValue}\"";
+ }
+ }
+
+ private static string GetSafeName(string name, char[] firstCharNameCharset, char[] charNameCharset)
+ {
+ // https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
+ //
+ // Metric names and labels
+ // Every time series is uniquely identified by its metric name and a set of key-value pairs, also known as labels.
+ // The metric name specifies the general feature of a system that is measured (e.g. http_requests_total - the total number of HTTP requests received). It may contain ASCII letters and digits, as well as underscores and colons. It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*.
+ // Note: The colons are reserved for user defined recording rules. They should not be used by exporters or direct instrumentation.
+ // Labels enable Prometheus's dimensional data model: any given combination of labels for the same metric name identifies a particular dimensional instantiation of that metric (for example: all HTTP requests that used the method POST to the /api/tracks handler). The query language allows filtering and aggregation based on these dimensions. Changing any label value, including adding or removing a label, will create a new time series.
+ // Label names may contain ASCII letters, numbers, as well as underscores. They must match the regex [a-zA-Z_][a-zA-Z0-9_]*. Label names beginning with __ are reserved for internal use.
+ // Label values may contain any Unicode characters.
+
+ var sb = new StringBuilder();
+ var firstChar = name[0];
+
+ sb.Append(firstCharNameCharset.Contains(firstChar)
+ ? firstChar
+ : GetSafeChar(char.ToLowerInvariant(firstChar), firstCharNameCharset));
+
+ for (var i = 1; i < name.Length; ++i)
+ {
+ sb.Append(GetSafeChar(name[i], charNameCharset));
+ }
+
+ return sb.ToString();
+
+ static char GetSafeChar(char c, char[] charset) => charset.Contains(c) ? c : '_';
+ }
+
+ private static string GetSafeMetricName(string name) => GetSafeName(name, FirstCharacterNameCharset, NameCharset);
+
+ private static string GetSafeLabelName(string name) => GetSafeName(name, FirstCharacterLabelCharset, LabelCharset);
+
+ private static string GetSafeLabelValue(string value)
+ {
+ // label_value can be any sequence of UTF-8 characters, but the backslash
+ // (\), double-quote ("), and line feed (\n) characters have to be escaped
+ // as \\, \", and \n, respectively.
+
+ var result = value.Replace("\\", "\\\\");
+ result = result.Replace("\n", "\\n");
+ result = result.Replace("\"", "\\\"");
+
+ return result;
+ }
+
+ private static string GetSafeMetricDescription(string description)
+ {
+ // HELP lines may contain any sequence of UTF-8 characters(after the metric name), but the backslash
+ // and the line feed characters have to be escaped as \\ and \n, respectively.Only one HELP line may
+ // exist for any given metric name.
+ var result = description.Replace(@"\", @"\\");
+ result = result.Replace("\n", @"\n");
+
+ return result;
+ }
+
+ internal class PrometheusMetricValueBuilder
+ {
+ public readonly ICollection> Labels = new List>();
+ public double Value;
+ public string Name;
+
+ public PrometheusMetricValueBuilder WithLabel(string name, string value)
+ {
+ this.Labels.Add(new Tuple(name, value));
+ return this;
+ }
+
+ public PrometheusMetricValueBuilder WithValue(long metricValue)
+ {
+ this.Value = metricValue;
+ return this;
+ }
+
+ public PrometheusMetricValueBuilder WithValue(double metricValue)
+ {
+ this.Value = metricValue;
+ return this;
+ }
+
+ public PrometheusMetricValueBuilder WithName(string name)
+ {
+ this.Name = name;
+ return this;
+ }
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs
new file mode 100644
index 000000000..b7e307ada
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs
@@ -0,0 +1,49 @@
+//
+// 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;
+using OpenTelemetry.Exporter;
+
+namespace OpenTelemetry.Metrics
+{
+ public static class MeterProviderBuilderExtensions
+ {
+ ///
+ /// Adds Console exporter to the TracerProvider.
+ ///
+ /// 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)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ var options = new PrometheusExporterOptions();
+ configure?.Invoke(options);
+ var exporter = new PrometheusExporter(options);
+ var pullMetricProcessor = new PullMetricProcessor(exporter, false);
+ exporter.MakePullRequest = pullMetricProcessor.PullRequest;
+
+ var metricsHttpServer = new PrometheusExporterMetricsHttpServer(exporter);
+ metricsHttpServer.Start();
+ return builder.AddMetricProcessor(pullMetricProcessor);
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj b/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj
new file mode 100644
index 000000000..6ee2ac3fa
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj
@@ -0,0 +1,26 @@
+
+
+
+ netstandard2.0;net461
+ Console exporter for OpenTelemetry .NET
+ $(PackageTags);prometheus;metrics
+
+
+
+ $(NoWarn),1591
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs
new file mode 100644
index 000000000..ea51da4cf
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs
@@ -0,0 +1,50 @@
+//
+// 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;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenTelemetry.Metrics;
+
+namespace OpenTelemetry.Exporter
+{
+ ///
+ /// Exporter of OpenTelemetry metrics to Prometheus.
+ ///
+ public class PrometheusExporter : BaseExporter
+ {
+ internal readonly PrometheusExporterOptions Options;
+ internal Batch Batch;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Options for the exporter.
+ public PrometheusExporter(PrometheusExporterOptions options)
+ {
+ this.Options = options;
+ }
+
+ internal Action MakePullRequest { get; set; }
+
+ public override ExportResult Export(in Batch batch)
+ {
+ this.Batch = batch;
+ return ExportResult.Success;
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs
new file mode 100644
index 000000000..675acb6c5
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs
@@ -0,0 +1,145 @@
+//
+// 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.Collections.Generic;
+using System.IO;
+using System.Text;
+using OpenTelemetry.Exporter.Prometheus.Implementation;
+using OpenTelemetry.Metrics;
+
+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 PrometheusSummaryType = "summary";
+ private const string PrometheusSummarySumPostFix = "_sum";
+ private const string PrometheusSummaryCountPostFix = "_count";
+ private const string PrometheusSummaryQuantileLabelName = "quantile";
+ private const string PrometheusSummaryQuantileLabelValueForMin = "0";
+ private const string PrometheusSummaryQuantileLabelValueForMax = "1";
+
+ ///
+ /// Serialize to Prometheus Format.
+ ///
+ /// Prometheus Exporter.
+ /// StreamWriter to write to.
+ public static void WriteMetricsCollection(this PrometheusExporter exporter, StreamWriter writer)
+ {
+ foreach (var metricItem in exporter.Batch)
+ {
+ foreach (var metric in metricItem.Metrics)
+ {
+ var builder = new PrometheusMetricBuilder()
+ .WithName(metric.Name)
+ .WithDescription(metric.Name);
+
+ if (metric is ISumMetric sumMetric)
+ {
+ if (sumMetric.Sum.Value is double doubleSum)
+ {
+ WriteSum(writer, builder, metric.Attributes, doubleSum);
+ }
+ else if (sumMetric.Sum.Value is long longSum)
+ {
+ WriteSum(writer, builder, metric.Attributes, longSum);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// 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 WriteSum(StreamWriter writer, PrometheusMetricBuilder builder, IEnumerable> labels, double doubleValue)
+ {
+ builder = builder.WithType(PrometheusCounterType);
+
+ var metricValueBuilder = builder.AddValue();
+ metricValueBuilder = metricValueBuilder.WithValue(doubleValue);
+
+ foreach (var label in labels)
+ {
+ metricValueBuilder.WithLabel(label.Key, label.Value.ToString());
+ }
+
+ builder.Write(writer);
+ }
+
+ private static void WriteSummary(
+ StreamWriter writer,
+ PrometheusMetricBuilder builder,
+ IEnumerable> labels,
+ string metricName,
+ double sum,
+ long count,
+ double min,
+ double max)
+ {
+ builder = builder.WithType(PrometheusSummaryType);
+
+ foreach (var label in labels)
+ {
+ /* For Summary we emit one row for Sum, Count, Min, Max.
+ Min,Max exports as quantile 0 and 1.
+ In future, when OpenTelemetry implements more aggregation
+ algorithms, this section will need to be revisited.
+ Sample output:
+ MyMeasure_sum{dim1="value1"} 750 1587013352982
+ MyMeasure_count{dim1="value1"} 5 1587013352982
+ MyMeasure{dim1="value2",quantile="0"} 150 1587013352982
+ MyMeasure{dim1="value2",quantile="1"} 150 1587013352982
+ */
+ builder.AddValue()
+ .WithName(metricName + PrometheusSummarySumPostFix)
+ .WithLabel(label.Key, label.Value)
+ .WithValue(sum);
+ builder.AddValue()
+ .WithName(metricName + PrometheusSummaryCountPostFix)
+ .WithLabel(label.Key, label.Value)
+ .WithValue(count);
+ builder.AddValue()
+ .WithName(metricName)
+ .WithLabel(label.Key, label.Value)
+ .WithLabel(PrometheusSummaryQuantileLabelName, PrometheusSummaryQuantileLabelValueForMin)
+ .WithValue(min);
+ builder.AddValue()
+ .WithName(metricName)
+ .WithLabel(label.Key, label.Value)
+ .WithLabel(PrometheusSummaryQuantileLabelName, PrometheusSummaryQuantileLabelValueForMax)
+ .WithValue(max);
+ }
+
+ builder.Write(writer);
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs
new file mode 100644
index 000000000..73266aba2
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs
@@ -0,0 +1,153 @@
+//
+// 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;
+using System.IO;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using OpenTelemetry.Exporter.Prometheus.Implementation;
+
+namespace OpenTelemetry.Exporter
+{
+ ///
+ /// A HTTP listener used to expose Prometheus metrics.
+ ///
+ public class PrometheusExporterMetricsHttpServer : IDisposable
+ {
+ private readonly PrometheusExporter exporter;
+ private readonly HttpListener httpListener = new HttpListener();
+ private readonly object syncObject = new object();
+
+ private CancellationTokenSource tokenSource;
+ private Task workerThread;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance.
+ public PrometheusExporterMetricsHttpServer(PrometheusExporter exporter)
+ {
+ this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter));
+ this.httpListener.Prefixes.Add(exporter.Options.Url);
+ }
+
+ ///
+ /// Start exporter.
+ ///
+ /// An optional that can be used to stop the htto server.
+ public void Start(CancellationToken token = default)
+ {
+ lock (this.syncObject)
+ {
+ if (this.tokenSource != null)
+ {
+ return;
+ }
+
+ // link the passed in token if not null
+ this.tokenSource = token == default ?
+ new CancellationTokenSource() :
+ CancellationTokenSource.CreateLinkedTokenSource(token);
+
+ this.workerThread = Task.Factory.StartNew(this.WorkerThread, default, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+ }
+ }
+
+ ///
+ /// Stop exporter.
+ ///
+ public void Stop()
+ {
+ lock (this.syncObject)
+ {
+ if (this.tokenSource == null)
+ {
+ return;
+ }
+
+ this.tokenSource.Cancel();
+ this.workerThread.Wait();
+ this.tokenSource = null;
+ }
+ }
+
+ ///
+ 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)
+ {
+ this.Stop();
+ this.httpListener.Close();
+ }
+ }
+
+ private void WorkerThread()
+ {
+ this.httpListener.Start();
+
+ try
+ {
+ using var scope = SuppressInstrumentationScope.Begin();
+ while (!this.tokenSource.IsCancellationRequested)
+ {
+ var ctxTask = this.httpListener.GetContextAsync();
+ ctxTask.Wait(this.tokenSource.Token);
+
+ var ctx = ctxTask.Result;
+
+ ctx.Response.StatusCode = 200;
+ ctx.Response.ContentType = PrometheusMetricBuilder.ContentType;
+
+ using var output = ctx.Response.OutputStream;
+ using var writer = new StreamWriter(output);
+ this.exporter.MakePullRequest();
+ this.exporter.WriteMetricsCollection(writer);
+ }
+ }
+ catch (OperationCanceledException ex)
+ {
+ PrometheusExporterEventSource.Log.CanceledExport(ex);
+ }
+ catch (Exception ex)
+ {
+ PrometheusExporterEventSource.Log.FailedExport(ex);
+ }
+ finally
+ {
+ try
+ {
+ this.httpListener.Stop();
+ this.httpListener.Close();
+ }
+ catch (Exception exFromFinally)
+ {
+ PrometheusExporterEventSource.Log.FailedShutdown(exFromFinally);
+ }
+ }
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs
new file mode 100644
index 000000000..10cb608ee
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs
@@ -0,0 +1,60 @@
+//
+// 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;
+using System.Threading.Tasks;
+
+#if NETSTANDARD2_0
+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
new file mode 100644
index 000000000..ce4aae423
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs
@@ -0,0 +1,29 @@
+//
+// 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.
+//
+
+namespace OpenTelemetry.Exporter
+{
+ ///
+ /// Options to run prometheus exporter.
+ ///
+ public class PrometheusExporterOptions
+ {
+ ///
+ /// Gets or sets the port to listen to. Typically it ends with /metrics like http://localhost:9184/metrics/.
+ ///
+ public string Url { get; set; } = "http://localhost:9184/metrics/";
+ }
+}
diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs
new file mode 100644
index 000000000..0b38d4734
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs
@@ -0,0 +1,46 @@
+//
+// 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
new file mode 100644
index 000000000..ed79f8d2f
--- /dev/null
+++ b/src/OpenTelemetry.Exporter.Prometheus/README.md
@@ -0,0 +1,29 @@
+# Prometheus Exporter for OpenTelemetry .NET
+
+[](https://www.nuget.org/packages/OpenTelemetry.Exporter.Prometheus)
+[](https://www.nuget.org/packages/OpenTelemetry.Exporter.Prometheus)
+
+## Prerequisite
+
+* [Get Prometheus](https://prometheus.io/docs/introduction/first_steps/)
+
+## Installation
+
+```shell
+dotnet add package OpenTelemetry.Exporter.Prometheus
+```
+
+## Configuration
+
+You can configure the `PrometheusExporter` by following the directions below:
+
+* `Url`: The url to listen to. Typically it ends with `/metrics` like `http://localhost:9184/metrics/`.
+
+See
+[`TestPrometheusExporter.cs`](../../examples/Console/TestPrometheusExporter.cs)
+for example use.
+
+## References
+
+* [OpenTelemetry Project](https://opentelemetry.io/)
+* [Prometheus](https://prometheus.io)
diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs
new file mode 100644
index 000000000..7f4c22920
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs
@@ -0,0 +1,53 @@
+//
+// 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;
+using System.Diagnostics.Metrics;
+using System.Reflection;
+using OpenTelemetry.Instrumentation.AspNetCore.Implementation;
+
+namespace OpenTelemetry.Instrumentation.AspNetCore
+{
+ ///
+ /// Asp.Net Core Requests instrumentation.
+ ///
+ internal class AspNetCoreMetrics : IDisposable
+ {
+ internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName();
+ internal static readonly string InstrumentationName = AssemblyName.Name;
+ internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString();
+
+ private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber;
+ private readonly Meter meter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AspNetCoreMetrics()
+ {
+ this.meter = new Meter(InstrumentationName, InstrumentationVersion);
+ this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpInMetricsListener("Microsoft.AspNetCore", this.meter), null);
+ this.diagnosticSourceSubscriber.Subscribe();
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.diagnosticSourceSubscriber?.Dispose();
+ this.meter?.Dispose();
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs
new file mode 100644
index 000000000..3856299dd
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs
@@ -0,0 +1,78 @@
+//
+// 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.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using Microsoft.AspNetCore.Http;
+using OpenTelemetry.Trace;
+
+namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation
+{
+ internal class HttpInMetricsListener : ListenerHandler
+ {
+ private readonly PropertyFetcher stopContextFetcher = new PropertyFetcher("HttpContext");
+ private readonly Meter meter;
+
+ private Counter httpServerRequestCount;
+
+ public HttpInMetricsListener(string name, Meter meter)
+ : base(name)
+ {
+ this.meter = meter;
+
+ // TODO:
+ // In the future, this instrumentation should produce the http.server.duration metric which will likely be represented as a histogram.
+ // See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server
+ //
+ // Histograms are not yet supported by the SDK.
+ //
+ // For now we produce a count metric called http.server.request_count just for demonstration purposes.
+ // This metric is not defined by the in the semantic conventions.
+ this.httpServerRequestCount = meter.CreateCounter("http.server.request_count", null, "The number of HTTP requests processed.");
+ }
+
+ public override void OnStopActivity(Activity activity, object payload)
+ {
+ HttpContext context = this.stopContextFetcher.Fetch(payload);
+ if (context == null)
+ {
+ AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(this.OnStopActivity));
+ return;
+ }
+
+ // TODO: Prometheus pulls metrics by invoking the /metrics endpoint. Decide if it makes sense to suppress this.
+ // Below is just a temporary way of achieving this suppression for metrics (we should consider suppressing traces too).
+ // If we want to suppress activity from Prometheus then we should use SuppressInstrumentationScope.
+ if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("metrics"))
+ {
+ return;
+ }
+
+ // TODO: This is just a minimal set of attributes. See the spec for additional attributes:
+ // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server
+ var tags = new KeyValuePair[]
+ {
+ new KeyValuePair(SemanticConventions.AttributeHttpMethod, context.Request.Method),
+ new KeyValuePair(SemanticConventions.AttributeHttpScheme, context.Request.Scheme),
+ new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, context.Response.StatusCode),
+ new KeyValuePair(SemanticConventions.AttributeHttpFlavor, context.Request.Protocol),
+ };
+
+ this.httpServerRequestCount.Add(1, tags);
+ }
+ }
+}
diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs
new file mode 100644
index 000000000..8f32fea69
--- /dev/null
+++ b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs
@@ -0,0 +1,53 @@
+//
+// 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;
+using OpenTelemetry.Instrumentation.AspNetCore;
+
+namespace OpenTelemetry.Metrics
+{
+ ///
+ /// Extension methods to simplify registering of ASP.NET Core request instrumentation.
+ ///
+ public static class MeterProviderBuilderExtensions
+ {
+ ///
+ /// Enables the incoming requests automatic data collection for ASP.NET Core.
+ ///
+ /// being configured.
+ /// The instance of to chain the calls.
+ public static MeterProviderBuilder AddAspNetCoreInstrumentation(
+ this MeterProviderBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ // TODO: Implement an IDeferredMeterProviderBuilder
+
+ // TODO: Handle AspNetCoreInstrumentationOptions
+ // Filter - makes sense for metric instrumentation
+ // Enrich - do we want a similar kind of functionality for metrics?
+ // RecordException - probably doesn't make sense for metric instrumentation
+ // EnableGrpcAspNetCoreSupport - this instrumentation will also need to also handle gRPC requests
+
+ var instrumentation = new AspNetCoreMetrics();
+ builder.AddSource(AspNetCoreMetrics.InstrumentationName);
+ return builder.AddInstrumentation(() => instrumentation);
+ }
+ }
+}
diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs
index 8d6f5f821..d6dd47bc2 100644
--- a/src/OpenTelemetry/AssemblyInfo.cs
+++ b/src/OpenTelemetry/AssemblyInfo.cs
@@ -20,3 +20,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)]
[assembly: InternalsVisibleTo("Benchmarks" + AssemblyInfo.PublicKey)]
+
+// TODO: Much of the metrics SDK is currently internal. These should be removed once the public API surface area for metrics is defined.
+[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)]
+[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)]
diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md
index 3edf87d84..59560b93d 100644
--- a/src/OpenTelemetry/CHANGELOG.md
+++ b/src/OpenTelemetry/CHANGELOG.md
@@ -7,6 +7,9 @@ branch](https://github.com/open-telemetry/opentelemetry-dotnet/tree/metrics),
please check the latest changes
[here](https://github.com/open-telemetry/opentelemetry-dotnet/blob/metrics/src/OpenTelemetry/CHANGELOG.md#experimental---metrics).
+* Removed existing Metrics code as the spec is completely being re-written.
+ ([#2030](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2030))
+
## Unreleased
* Removes .NET Framework 4.5.2, .NET 4.6 support. The minimum .NET Framework
diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs
new file mode 100644
index 000000000..7e516e00f
--- /dev/null
+++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs
@@ -0,0 +1,183 @@
+//
+// 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;
+using System.Collections.Generic;
+using System.Diagnostics.Metrics;
+using System.Linq;
+
+namespace OpenTelemetry.Metrics
+{
+ internal class AggregatorStore
+ {
+ private static readonly string[] EmptySeqKey = new string[0];
+ private static readonly object[] EmptySeqValue = new object[0];
+ private readonly Instrument instrument;
+ private readonly object lockKeyValue2MetricAggs = new object();
+
+ // Two-Level lookup. TagKeys x [ TagValues x Metrics ]
+ private readonly Dictionary> keyValue2MetricAggs =
+ new Dictionary>(new StringArrayEqualityComparer());
+
+ private IAggregator[] tag0Metrics = null;
+
+ internal AggregatorStore(Instrument instrument)
+ {
+ this.instrument = instrument;
+ }
+
+ internal IAggregator[] MapToMetrics(string[] seqKey, object[] seqVal)
+ {
+ var aggregators = new List();
+
+ var name = $"{this.instrument.Meter.Name}:{this.instrument.Name}";
+
+ var tags = new KeyValuePair[seqKey.Length];
+ for (int i = 0; i < seqKey.Length; i++)
+ {
+ tags[i] = new KeyValuePair(seqKey[i], seqVal[i]);
+ }
+
+ var dt = DateTimeOffset.UtcNow;
+
+ // TODO: Need to map each instrument to metrics (based on View API)
+ if (this.instrument.GetType().Name.Contains("Counter"))
+ {
+ aggregators.Add(new SumMetricAggregator(name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags));
+ }
+ else if (this.instrument.GetType().Name.Contains("Gauge"))
+ {
+ aggregators.Add(new GaugeMetricAggregator(name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags));
+ }
+ else if (this.instrument.GetType().Name.Contains("Histogram"))
+ {
+ aggregators.Add(new HistogramMetricAggregator(name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags));
+ }
+ else
+ {
+ aggregators.Add(new SummaryMetricAggregator(name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags, false));
+ }
+
+ return aggregators.ToArray();
+ }
+
+ internal IAggregator[] FindMetricAggregators(ReadOnlySpan> tags)
+ {
+ int len = tags.Length;
+
+ if (len == 0)
+ {
+ if (this.tag0Metrics == null)
+ {
+ this.tag0Metrics = this.MapToMetrics(AggregatorStore.EmptySeqKey, AggregatorStore.EmptySeqValue);
+ }
+
+ return this.tag0Metrics;
+ }
+
+ var storage = ThreadStaticStorage.GetStorage();
+
+ storage.SplitToKeysAndValues(tags, out var tagKey, out var tagValue);
+
+ if (len > 1)
+ {
+ Array.Sort(tagKey, tagValue);
+ }
+
+ IAggregator[] metrics;
+
+ lock (this.lockKeyValue2MetricAggs)
+ {
+ string[] seqKey = null;
+
+ // GetOrAdd by TagKey at 1st Level of 2-level dictionary structure.
+ // Get back a Dictionary of [ Values x Metrics[] ].
+ if (!this.keyValue2MetricAggs.TryGetValue(tagKey, out var value2metrics))
+ {
+ // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage.
+
+ seqKey = new string[len];
+ tagKey.CopyTo(seqKey, 0);
+
+ value2metrics = new Dictionary