From a12fddd54bab11bdf6e1e04e6f1a92adfe85d656 Mon Sep 17 00:00:00 2001 From: Dawid Szmigielski Date: Wed, 28 Jul 2021 18:35:05 +0200 Subject: [PATCH 1/5] Otlp exporter documentation update (#2199) --- .../README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 2f4d0639e4d..5f39d810b80 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -18,7 +18,11 @@ dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol ## Configuration -You can configure the `OtlpExporter` through `OtlpExporterOptions` properties: +You can configure the `OtlpExporter` through `OtlpExporterOptions` +properties and environment variables. The `OtlpExporterOptions` +setters take precedence over the environment variables. + +## Options Properties * `Endpoint`: Target to which the exporter is going to send traces or metrics. The endpoint must be a valid Uri with scheme (http or https) and host, and MAY @@ -35,6 +39,18 @@ You can configure the `OtlpExporter` through `OtlpExporterOptions` properties: See the [`TestOtlpExporter.cs`](../../examples/Console/TestOtlpExporter.cs) for an example of how to use the exporter. +## Environment Variables + +The following environment variables can be used to override the default +values of the `OtlpExporterOptions` +(following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md)). + +| Environment variable | `OtlpExporterOptions` property | +| ------------------------------| -------------------------------| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | `Endpoint` | +| `OTEL_EXPORTER_OTLP_HEADERS` | `Headers` | +| `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` | + ## Special case when using insecure channel If your application is [.NET Standard From f03cdddbe8c09bc0e248701f3040f0d8762c2803 Mon Sep 17 00:00:00 2001 From: Vishwesh Bankwar Date: Wed, 28 Jul 2021 10:01:24 -0700 Subject: [PATCH 2/5] Enable zipkin endpoint configuration via environment variable (#1924) --- .../CHANGELOG.md | 4 ++ .../ZipkinExporterEventSource.cs | 15 ++++++ src/OpenTelemetry.Exporter.Zipkin/README.md | 18 +++++-- .../ZipkinExporterOptions.cs | 22 +++++++- .../ZipkinExporterTests.cs | 54 +++++++++++++++++++ 5 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md index 6b692170103..1e1dac0d3ff 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Enabling endpoint configuration in ZipkinExporterOptions via + `OTEL_EXPORTER_ZIPKIN_ENDPOINT` environment variable. + ([#1453](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1453)) + ## 1.2.0-alpha1 Released 2021-Jul-23 diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs index f2d1bd94f40..eb8a6af0b45 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs @@ -37,10 +37,25 @@ public void FailedExport(Exception ex) } } + [NonEvent] + public void FailedEndpointInitialization(Exception ex) + { + if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1))) + { + this.FailedEndpointInitialization(ex.ToInvariantString()); + } + } + [Event(1, Message = "Failed to export activities: '{0}'", Level = EventLevel.Error)] public void FailedExport(string exception) { this.WriteEvent(1, exception); } + + [Event(2, Message = "Error initializing Zipkin endpoint, falling back to default value: '{0}'", Level = EventLevel.Error)] + public void FailedEndpointInitialization(string exception) + { + this.WriteEvent(2, exception); + } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/README.md b/src/OpenTelemetry.Exporter.Zipkin/README.md index b73fa16c26d..779d464be55 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/README.md +++ b/src/OpenTelemetry.Exporter.Zipkin/README.md @@ -20,8 +20,11 @@ method on `TracerProviderBuilder`. ## Configuration -You can configure the `ZipkinExporter` through -`ZipkinExporterOptions` properties: +You can configure the `ZipkinExporter` through `ZipkinExporterOptions` +and environment variables. The `ZipkinExporterOptions` setters +take precedence over the environment variables. + +### Configuration using Properties * `ServiceName`: Name of the service reporting telemetry. If the `Resource` associated with the telemetry has "service.name" defined, then it'll be @@ -41,7 +44,7 @@ See [`TestZipkinExporter.cs`](../../examples/Console/TestZipkinExporter.cs) for example use. -## Configuration using Dependency Injection +### Configuration using Dependency Injection This exporter allows easy configuration of `ZipkinExporterOptions` from dependency injection container, when used in conjunction with @@ -50,6 +53,15 @@ dependency injection container, when used in conjunction with See the [Startup](../../examples/AspNetCore/Startup.cs) class of the ASP.NET Core application for example use. +### Configuration using Environment Variables + +The following environment variables can be used to override the default +values of the `ZipkinExporterOptions`. + +| Environment variable | `ZipkinExporterOptions` property | +| --------------------------------| -------------------------------- | +| `OTEL_EXPORTER_ZIPKIN_ENDPOINT` | `Endpoint` | + ## References * [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs index ac1682017ae..21df9181fd7 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs @@ -16,6 +16,7 @@ using System; using System.Diagnostics; +using OpenTelemetry.Exporter.Zipkin.Implementation; namespace OpenTelemetry.Exporter { @@ -25,12 +26,31 @@ namespace OpenTelemetry.Exporter public sealed class ZipkinExporterOptions { internal const int DefaultMaxPayloadSizeInBytes = 4096; + internal const string ZipkinEndpointEnvVar = "OTEL_EXPORTER_ZIPKIN_ENDPOINT"; + internal const string DefaultZipkinEndpoint = "http://localhost:9411/api/v2/spans"; + + /// + /// Initializes a new instance of the class. + /// Initializes zipkin endpoint. + /// + public ZipkinExporterOptions() + { + try + { + this.Endpoint = new Uri(Environment.GetEnvironmentVariable(ZipkinEndpointEnvVar) ?? DefaultZipkinEndpoint); + } + catch (Exception ex) + { + this.Endpoint = new Uri(DefaultZipkinEndpoint); + ZipkinExporterEventSource.Log.FailedEndpointInitialization(ex); + } + } /// /// Gets or sets Zipkin endpoint address. See https://zipkin.io/zipkin-api/#/default/post_spans. /// Typically https://zipkin-server-name:9411/api/v2/spans. /// - public Uri Endpoint { get; set; } = new Uri("http://localhost:9411/api/v2/spans"); + public Uri Endpoint { get; set; } /// /// Gets or sets a value indicating whether short trace id should be used. diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs index 47829b712b1..8b4a6482b65 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs @@ -131,6 +131,60 @@ public void SuppresssesInstrumentation() Assert.Equal(1, endCalledCount); } + [Fact] + public void EndpointConfigurationUsingEnvironmentVariable() + { + try + { + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "http://urifromenvironmentvariable"); + + var exporterOptions = new ZipkinExporterOptions(); + + Assert.Equal(new Uri(Environment.GetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar)).AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); + } + finally + { + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); + } + } + + [Fact] + public void IncodeEndpointConfigTakesPrecedenceOverEnvironmentVariable() + { + try + { + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "http://urifromenvironmentvariable"); + + var exporterOptions = new ZipkinExporterOptions + { + Endpoint = new Uri("http://urifromcode"), + }; + + Assert.Equal(new Uri("http://urifromcode").AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); + } + finally + { + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); + } + } + + [Fact] + public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() + { + try + { + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "InvalidUri"); + + var exporterOptions = new ZipkinExporterOptions(); + + Assert.Equal(new Uri(ZipkinExporterOptions.DefaultZipkinEndpoint).AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); + } + finally + { + Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, null); + } + } + [Theory] [InlineData(true, false, false)] [InlineData(false, false, false)] From 7db5ec8dcb2567c1bbadf71d5c81bf2ae6d29120 Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Wed, 28 Jul 2021 10:52:20 -0700 Subject: [PATCH 3/5] Modify benchmarks to not use static tags (#2201) --- test/Benchmarks/Metrics/MetricsBenchmarks.cs | 83 ++++++++------------ 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/test/Benchmarks/Metrics/MetricsBenchmarks.cs b/test/Benchmarks/Metrics/MetricsBenchmarks.cs index 0e20f42b281..64ba7d5294d 100644 --- a/test/Benchmarks/Metrics/MetricsBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricsBenchmarks.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using System; using System.Collections.Generic; using System.Diagnostics.Metrics; using BenchmarkDotNet.Attributes; @@ -22,40 +23,22 @@ /* BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 -Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores -.NET Core SDK=5.0.202 - [Host] : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT - DefaultJob : .NET Core 3.1.13 (CoreCLR 4.700.21.11102, CoreFX 4.700.21.11602), X64 RyuJIT - - -| Method | WithSDK | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |-------- |-----------:|-----------:|----------:|-------:|------:|------:|----------:| -| CounterHotPath | False | 15.126 ns | 0.3228 ns | 0.3965 ns | - | - | - | - | -| CounterWith1LabelsHotPath | False | 9.766 ns | 0.2268 ns | 0.3530 ns | - | - | - | - | -| CounterWith3LabelsHotPath | False | 25.240 ns | 0.2876 ns | 0.2690 ns | - | - | - | - | -| CounterWith5LabelsHotPath | False | 37.929 ns | 0.7512 ns | 0.5865 ns | 0.0249 | - | - | 104 B | -| CounterHotPath | True | 44.790 ns | 0.9101 ns | 1.3621 ns | - | - | - | - | -| CounterWith1LabelsHotPath | True | 115.023 ns | 2.1001 ns | 1.9644 ns | - | - | - | - | -| CounterWith3LabelsHotPath | True | 436.527 ns | 6.5121 ns | 5.7728 ns | - | - | - | - | -| CounterWith5LabelsHotPath | True | 586.498 ns | 11.4783 ns | 9.5849 ns | 0.0553 | - | - | 232 B | - - -BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 -Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores - [Host] : .NET Framework 4.8 (4.8.4360.0), X64 RyuJIT - DefaultJob : .NET Framework 4.8 (4.8.4360.0), X64 RyuJIT - - -| Method | WithSDK | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |-------- |------------:|----------:|----------:|-------:|------:|------:|----------:| -| CounterHotPath | False | 23.53 ns | 0.480 ns | 0.401 ns | - | - | - | - | -| CounterWith1LabelsHotPath | False | 28.70 ns | 0.592 ns | 0.770 ns | - | - | - | - | -| CounterWith3LabelsHotPath | False | 46.27 ns | 0.942 ns | 1.157 ns | - | - | - | - | -| CounterWith5LabelsHotPath | False | 51.66 ns | 1.060 ns | 1.857 ns | 0.0249 | - | - | 104 B | -| CounterHotPath | True | 70.44 ns | 1.029 ns | 0.912 ns | - | - | - | - | -| CounterWith1LabelsHotPath | True | 151.92 ns | 3.067 ns | 3.651 ns | - | - | - | - | -| CounterWith3LabelsHotPath | True | 876.20 ns | 15.920 ns | 14.892 ns | - | - | - | - | -| CounterWith5LabelsHotPath | True | 1,973.64 ns | 38.393 ns | 45.705 ns | 0.0534 | - | - | 233 B | +Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +.NET Core SDK=5.0.302 + [Host] : .NET Core 3.1.17 (CoreCLR 4.700.21.31506, CoreFX 4.700.21.31502), X64 RyuJIT + DefaultJob : .NET Core 3.1.17 (CoreCLR 4.700.21.31506, CoreFX 4.700.21.31502), X64 RyuJIT + + +| Method | WithSDK | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------------------------- |-------- |----------:|----------:|-----------:|----------:|-------:|------:|------:|----------:| +| CounterHotPath | False | 18.48 ns | 0.366 ns | 0.570 ns | 18.52 ns | - | - | - | - | +| CounterWith1LabelsHotPath | False | 30.25 ns | 1.274 ns | 3.530 ns | 29.14 ns | - | - | - | - | +| CounterWith3LabelsHotPath | False | 82.93 ns | 2.586 ns | 7.124 ns | 81.79 ns | - | - | - | - | +| CounterWith5LabelsHotPath | False | 134.94 ns | 4.756 ns | 13.491 ns | 132.45 ns | 0.0248 | - | - | 104 B | +| CounterHotPath | True | 68.58 ns | 1.417 ns | 3.228 ns | 68.40 ns | - | - | - | - | +| CounterWith1LabelsHotPath | True | 192.19 ns | 8.114 ns | 23.151 ns | 184.06 ns | - | - | - | - | +| CounterWith3LabelsHotPath | True | 799.33 ns | 47.442 ns | 136.882 ns | 757.73 ns | - | - | - | - | +| CounterWith5LabelsHotPath | True | 972.16 ns | 45.809 ns | 133.626 ns | 939.95 ns | 0.0553 | - | - | 232 B | */ namespace Benchmarks.Metrics @@ -63,15 +46,11 @@ namespace Benchmarks.Metrics [MemoryDiagnoser] public class MetricsBenchmarks { - private readonly KeyValuePair tag1 = new KeyValuePair("attrib1", "value1"); - private readonly KeyValuePair tag2 = new KeyValuePair("attrib2", "value2"); - private readonly KeyValuePair tag3 = new KeyValuePair("attrib3", "value3"); - private readonly KeyValuePair tag4 = new KeyValuePair("attrib4", "value4"); - private readonly KeyValuePair tag5 = new KeyValuePair("attrib5", "value5"); - - private Counter counter; + private Counter counter; private MeterProvider provider; private Meter meter; + private Random random = new Random(); + private string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; [Params(false, true)] public bool WithSDK { get; set; } @@ -87,7 +66,7 @@ public void Setup() } this.meter = new Meter("TestMeter"); - this.counter = this.meter.CreateCounter("counter"); + this.counter = this.meter.CreateCounter("counter"); } [GlobalCleanup] @@ -106,24 +85,28 @@ public void CounterHotPath() [Benchmark] public void CounterWith1LabelsHotPath() { - this.counter?.Add(100, this.tag1); + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); + this.counter?.Add(100, tag1); } [Benchmark] - public void CounterWith1LabelLocal() - { - this.counter?.Add(100, new KeyValuePair("key", "value")); - } - public void CounterWith3LabelsHotPath() { - this.counter?.Add(100, this.tag1, this.tag2, this.tag3); + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); + this.counter?.Add(100, tag1, tag2, tag3); } [Benchmark] public void CounterWith5LabelsHotPath() { - this.counter?.Add(100, this.tag1, this.tag2, this.tag3, this.tag4, this.tag5); + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 2)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 2)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 5)]); + var tag4 = new KeyValuePair("DimName4", this.dimensionValues[this.random.Next(0, 5)]); + var tag5 = new KeyValuePair("DimName4", this.dimensionValues[this.random.Next(0, 10)]); + this.counter?.Add(100, tag1, tag2, tag3, tag4, tag5); } } } From 175154e1950441ab73544dfa5c2cc59cf338b38f Mon Sep 17 00:00:00 2001 From: Cijo Thomas Date: Wed, 28 Jul 2021 17:35:33 -0700 Subject: [PATCH 4/5] Add a basic metric test for multi-thread update (#2205) * Add a basic metric test for multi-thread update * nulklable --- src/OpenTelemetry/Metrics/AggregatorStore.cs | 12 ++ .../Metrics/MetricAPITest.cs | 109 +++++++++++++++++- 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index c13c01d1961..20aa4c8760e 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -157,6 +157,18 @@ internal List Collect(bool isDelta, DateTimeOffset dt) { var collectedMetrics = new List(); + if (this.tag0Metrics != null) + { + foreach (var aggregator in this.tag0Metrics) + { + var m = aggregator.Collect(dt, isDelta); + if (m != null) + { + collectedMetrics.Add(m); + } + } + } + // Lock to prevent new time series from being added // until collect is done. lock (this.lockKeyValue2MetricAggs) diff --git a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs index 2a4efce8fc1..1f0d8424d6a 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs @@ -14,20 +14,123 @@ // limitations under the License. // +using System; using System.Collections.Generic; using System.Diagnostics.Metrics; -using System.Threading.Tasks; +using System.Threading; +using OpenTelemetry.Tests; using Xunit; -#nullable enable - namespace OpenTelemetry.Metrics.Tests { public class MetricApiTest { + private static int numberOfThreads = 10; + private static long deltaValueUpdatedByEachCall = 10; + private static int numberOfMetricUpdateByEachThread = 1000000; + [Fact] public void SimpleTest() { + var metricItems = new List(); + var metricExporter = new TestExporter(ProcessExport); + void ProcessExport(Batch batch) + { + foreach (var metricItem in batch) + { + metricItems.Add(metricItem); + } + } + + var meter = new Meter("TestMeter"); + var counterLong = meter.CreateCounter("mycounter"); + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddSource("TestMeter") + .AddMetricProcessor(new PushMetricProcessor(metricExporter, 100, isDelta: true)) + .Build(); + + // setup args to threads. + var mreToBlockUpdateThreads = new ManualResetEvent(false); + var mreToEnsureAllThreadsStarted = new ManualResetEvent(false); + + var argToThread = new UpdateThreadArguments(); + argToThread.Counter = counterLong; + argToThread.ThreadsStartedCount = 0; + argToThread.MreToBlockUpdateThread = mreToBlockUpdateThreads; + argToThread.MreToEnsureAllThreadsStart = mreToEnsureAllThreadsStarted; + + Thread[] t = new Thread[numberOfThreads]; + for (int i = 0; i < numberOfThreads; i++) + { + t[i] = new Thread(CounterUpdateThread); + t[i].Start(argToThread); + } + + // Block until all threads started. + mreToEnsureAllThreadsStarted.WaitOne(); + + // unblock all the threads. + // (i.e let them start counter.Add) + mreToBlockUpdateThreads.Set(); + + for (int i = 0; i < numberOfThreads; i++) + { + // wait for all threads to complete + t[i].Join(); + } + + meterProvider.Dispose(); + + // TODO: Once Dispose does flush, we may not need this + // unknown sleep below. + Thread.Sleep(1000); + + long sumReceived = 0; + foreach (var metricItem in metricItems) + { + var metrics = metricItem.Metrics; + foreach (var metric in metrics) + { + sumReceived += (long)(metric as ISumMetric).Sum.Value; + } + } + + var expectedSum = deltaValueUpdatedByEachCall * numberOfMetricUpdateByEachThread * numberOfThreads; + Assert.Equal(expectedSum, sumReceived); + } + + private static void CounterUpdateThread(object obj) + { + var arguments = obj as UpdateThreadArguments; + if (arguments == null) + { + throw new Exception("Invalid args"); + } + + var mre = arguments.MreToBlockUpdateThread; + var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; + var counter = arguments.Counter; + + if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == numberOfThreads) + { + mreToEnsureAllThreadsStart.Set(); + } + + // Wait until signalled to start calling update on aggregator + mre.WaitOne(); + + for (int i = 0; i < numberOfMetricUpdateByEachThread; i++) + { + counter.Add(deltaValueUpdatedByEachCall, new KeyValuePair("verb", "GET")); + } + } + + private class UpdateThreadArguments + { + public ManualResetEvent MreToBlockUpdateThread; + public ManualResetEvent MreToEnsureAllThreadsStart; + public int ThreadsStartedCount; + public Counter Counter; } } } From 138035bc22791adbb0ae4d76e263db6bbfd5372f Mon Sep 17 00:00:00 2001 From: Alan West <3676547+alanwest@users.noreply.github.com> Date: Wed, 28 Jul 2021 18:24:32 -0700 Subject: [PATCH 5/5] OtlpMetricsExporter: Add test transforming metrics to OTLP metrics (#2206) --- .../Implementation/MetricItemExtensions.cs | 16 +- .../Implementation/MetricsService.cs | 50 ++++++ .../OtlpMetricsExporter.cs | 6 +- .../OtlpMetricsExporterTests.cs | 150 ++++++++++++++++++ .../OtlpTestHelpers.cs | 120 ++++++++++++++ ...orterTest.cs => OtlpTraceExporterTests.cs} | 104 +----------- 6 files changed, 341 insertions(+), 105 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricsService.cs create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs rename test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/{OtlpExporterTest.cs => OtlpTraceExporterTests.cs} (80%) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index f470e9a3c7a..3fa9c2a76f5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -120,10 +120,18 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this IMetric metric) var otlpMetric = new OtlpMetrics.Metric { Name = metric.Name, - Description = metric.Description, - Unit = metric.Unit, }; + if (metric.Description != null) + { + otlpMetric.Description = metric.Description; + } + + if (metric.Unit != null) + { + otlpMetric.Unit = metric.Unit; + } + if (metric is ISumMetric sumMetric) { var sum = new OtlpMetrics.Sum @@ -133,7 +141,7 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this IMetric metric) ? OtlpMetrics.AggregationTemporality.Delta : OtlpMetrics.AggregationTemporality.Cumulative, }; - var dataPoint = metric.ToNumberDataPoint(sumMetric.Sum, sumMetric.Exemplars); + var dataPoint = metric.ToNumberDataPoint(sumMetric.Sum.Value, sumMetric.Exemplars); sum.DataPoints.Add(dataPoint); otlpMetric.Sum = sum; } @@ -238,7 +246,7 @@ private static OtlpMetrics.NumberDataPoint ToNumberDataPoint(this IMetric metric { // TODO: Determine how we want to handle exceptions here. // Do we want to just skip this metric and move on? - throw new ArgumentException(); + throw new ArgumentException($"Value must be a long or a double.", nameof(value)); } // TODO: Do TagEnumerationState thing. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricsService.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricsService.cs new file mode 100644 index 00000000000..051c70d4c18 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricsService.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.Threading; +using Grpc.Core; + +namespace Opentelemetry.Proto.Collector.Metrics.V1 +{ + /// + /// MetricsService extensions. + /// + internal static partial class MetricsService + { + /// Interface for MetricService. + public interface IMetricsServiceClient + { + /// + /// For performance reasons, it is recommended to keep this RPC + /// alive for the entire life of the application. + /// + /// The request to send to the server. + /// The initial metadata to send with the call. This parameter is optional. + /// An optional deadline for the call. The call will be cancelled if deadline is hit. + /// An optional token for canceling the call. + /// The response received from the server. + ExportMetricsServiceResponse Export(ExportMetricsServiceRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default); + } + + /// + /// MetricServiceClient extensions. + /// + public partial class MetricsServiceClient : IMetricsServiceClient + { + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs index b43cfdad41a..37aa8afcf71 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs @@ -43,7 +43,7 @@ public class OtlpMetricsExporter : BaseExporter #else private readonly Channel channel; #endif - private readonly OtlpCollector.MetricsService.MetricsServiceClient metricsClient; + private readonly OtlpCollector.MetricsService.IMetricsServiceClient metricsClient; private readonly Metadata headers; /// @@ -59,8 +59,8 @@ public OtlpMetricsExporter(OtlpExporterOptions options) /// Initializes a new instance of the class. /// /// Configuration options for the exporter. - /// . - internal OtlpMetricsExporter(OtlpExporterOptions options, OtlpCollector.MetricsService.MetricsServiceClient metricsServiceClient = null) + /// . + internal OtlpMetricsExporter(OtlpExporterOptions options, OtlpCollector.MetricsService.IMetricsServiceClient metricsServiceClient = null) { this.options = options ?? throw new ArgumentNullException(nameof(options)); this.headers = GetMetadataFromHeaders(options.Headers); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs new file mode 100644 index 00000000000..2c2eda9f717 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -0,0 +1,150 @@ +// +// 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; +using System.Threading; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Tests; +using OpenTelemetry.Trace; +using Xunit; +using GrpcCore = Grpc.Core; +using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1; +using OtlpMetrics = Opentelemetry.Proto.Metrics.V1; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +{ + public class OtlpMetricsExporterTests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ToOtlpResourceMetricsTest(bool addResource) + { + using var exporter = new OtlpMetricsExporter( + new OtlpExporterOptions(), + new NoopMetricsServiceClient()); + + if (addResource) + { + exporter.SetResource( + ResourceBuilder.CreateEmpty().AddAttributes( + new List> + { + new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "service-name"), + new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "ns1"), + }).Build()); + } + else + { + exporter.SetResource(Resource.Empty); + } + + var tags = new KeyValuePair[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + }; + + var processor = new PullMetricProcessor(new TestExporter(RunTest), true); + + using var provider = Sdk.CreateMeterProviderBuilder() + .AddSource("TestMeter") + .AddMetricProcessor(processor) + .Build(); + + using var meter = new Meter("TestMeter", "0.0.1"); + + var counter = meter.CreateCounter("counter"); + + counter.Add(100, tags); + + var testCompleted = false; + + // Invokes the TestExporter which will invoke RunTest + processor.PullRequest(); + + Assert.True(testCompleted); + + void RunTest(Batch metricItem) + { + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddBatch(exporter.ProcessResource, metricItem); + + Assert.Single(request.ResourceMetrics); + var resourceMetric = request.ResourceMetrics.First(); + var oltpResource = resourceMetric.Resource; + + if (addResource) + { + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); + } + else + { + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + } + + Assert.Single(resourceMetric.InstrumentationLibraryMetrics); + var instrumentationLibraryMetrics = resourceMetric.InstrumentationLibraryMetrics.First(); + Assert.Equal(string.Empty, instrumentationLibraryMetrics.SchemaUrl); + Assert.Equal("TestMeter", instrumentationLibraryMetrics.InstrumentationLibrary.Name); + Assert.Equal("0.0.1", instrumentationLibraryMetrics.InstrumentationLibrary.Version); + + Assert.Single(instrumentationLibraryMetrics.Metrics); + + foreach (var metric in instrumentationLibraryMetrics.Metrics) + { + Assert.Equal(string.Empty, metric.Description); + Assert.Equal(string.Empty, metric.Unit); + Assert.Equal("counter", metric.Name); + + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Sum, metric.DataCase); + Assert.True(metric.Sum.IsMonotonic); + Assert.Equal(OtlpMetrics.AggregationTemporality.Delta, metric.Sum.AggregationTemporality); + + Assert.Single(metric.Sum.DataPoints); + var dataPoint = metric.Sum.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(100, dataPoint.AsInt); + +#pragma warning disable CS0612 // Type or member is obsolete + Assert.Empty(dataPoint.Labels); +#pragma warning restore CS0612 // Type or member is obsolete + OtlpTestHelpers.AssertOtlpAttributes(tags.ToList(), dataPoint.Attributes); + + Assert.Empty(dataPoint.Exemplars); + } + + testCompleted = true; + } + } + + private class NoopMetricsServiceClient : OtlpCollector.MetricsService.IMetricsServiceClient + { + public OtlpCollector.ExportMetricsServiceResponse Export(OtlpCollector.ExportMetricsServiceRequest request, GrpcCore.Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default) + { + return null; + } + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs new file mode 100644 index 00000000000..7b0da3aeee2 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTestHelpers.cs @@ -0,0 +1,120 @@ +// +// 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.Linq; +using Google.Protobuf.Collections; +using Xunit; +using OtlpCommon = Opentelemetry.Proto.Common.V1; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +{ + internal static class OtlpTestHelpers + { + public static void AssertOtlpAttributes( + IEnumerable> expected, + RepeatedField actual) + { + var expectedAttributes = expected.ToList(); + int expectedSize = 0; + for (int i = 0; i < expectedAttributes.Count; i++) + { + var current = expectedAttributes[i].Value; + + if (current.GetType().IsArray) + { + if (current is bool[] boolArray) + { + int index = 0; + foreach (var item in boolArray) + { + Assert.Equal(expectedAttributes[i].Key, actual[i + index].Key); + AssertOtlpAttributeValue(item, actual[i + index]); + index++; + expectedSize++; + } + } + else if (current is int[] intArray) + { + int index = 1; + foreach (var item in intArray) + { + Assert.Equal(expectedAttributes[i].Key, actual[i + index].Key); + AssertOtlpAttributeValue(item, actual[i + index]); + index++; + expectedSize++; + } + } + else if (current is double[] doubleArray) + { + int index = 2; + foreach (var item in doubleArray) + { + Assert.Equal(expectedAttributes[i].Key, actual[i + index].Key); + AssertOtlpAttributeValue(item, actual[i + index]); + index++; + expectedSize++; + } + } + else if (current is string[] stringArray) + { + int index = 3; + foreach (var item in stringArray) + { + Assert.Equal(expectedAttributes[i].Key, actual[i + index].Key); + AssertOtlpAttributeValue(item, actual[i + index]); + index++; + expectedSize++; + } + } + } + else + { + Assert.Equal(expectedAttributes[i].Key, actual[i].Key); + AssertOtlpAttributeValue(current, actual[i]); + expectedSize++; + } + } + + Assert.Equal(expectedSize, actual.Count); + } + + private static void AssertOtlpAttributeValue(object expected, OtlpCommon.KeyValue actual) + { + switch (expected) + { + case string s: + Assert.Equal(s, actual.Value.StringValue); + break; + case bool b: + Assert.Equal(b, actual.Value.BoolValue); + break; + case long l: + Assert.Equal(l, actual.Value.IntValue); + break; + case double d: + Assert.Equal(d, actual.Value.DoubleValue); + break; + case int i: + Assert.Equal(i, actual.Value.IntValue); + break; + default: + Assert.Equal(expected.ToString(), actual.Value.StringValue); + break; + } + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterTest.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs similarity index 80% rename from test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterTest.cs rename to test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index f8a2499f2d0..832d163090a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterTest.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,9 +36,9 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests { - public class OtlpExporterTest + public class OtlpTraceExporterTests { - static OtlpExporterTest() + static OtlpTraceExporterTests() { Activity.DefaultIdFormat = ActivityIdFormat.W3C; Activity.ForceDefaultIdFormat = true; @@ -213,7 +213,7 @@ public void ToOtlpSpanTest() Assert.Null(otlpSpan.Status); Assert.Empty(otlpSpan.Events); Assert.Empty(otlpSpan.Links); - AssertOtlpAttributes(attributes, otlpSpan.Attributes); + OtlpTestHelpers.AssertOtlpAttributes(attributes, otlpSpan.Attributes); var expectedStartTimeUnixNano = 100 * expectedUnixTimeTicks; Assert.Equal(expectedStartTimeUnixNano, otlpSpan.StartTimeUnixNano); @@ -254,14 +254,14 @@ public void ToOtlpSpanTest() for (var i = 0; i < childEvents.Count; i++) { Assert.Equal(childEvents[i].Name, otlpSpan.Events[i].Name); - AssertOtlpAttributes(childEvents[i].Tags.ToList(), otlpSpan.Events[i].Attributes); + OtlpTestHelpers.AssertOtlpAttributes(childEvents[i].Tags.ToList(), otlpSpan.Events[i].Attributes); } childLinks.Reverse(); Assert.Equal(childLinks.Count, otlpSpan.Links.Count); for (var i = 0; i < childLinks.Count; i++) { - AssertOtlpAttributes(childLinks[i].Tags.ToList(), otlpSpan.Links[i].Attributes); + OtlpTestHelpers.AssertOtlpAttributes(childLinks[i].Tags.ToList(), otlpSpan.Links[i].Attributes); } } @@ -389,98 +389,6 @@ public void GetMetadataFromHeadersThrowsExceptionOnOnvalidFormat(string headers) throw new XunitException("GetMetadataFromHeaders did not throw an exception for invalid input headers"); } - private static void AssertOtlpAttributes( - List> expectedAttributes, - RepeatedField otlpAttributes) - { - int expectedSize = 0; - for (int i = 0; i < expectedAttributes.Count; i++) - { - var current = expectedAttributes[i].Value; - - if (current.GetType().IsArray) - { - if (current is bool[] boolArray) - { - int index = 0; - foreach (var item in boolArray) - { - Assert.Equal(expectedAttributes[i].Key, otlpAttributes[i + index].Key); - AssertOtlpAttributeValue(item, otlpAttributes[i + index]); - index++; - expectedSize++; - } - } - else if (current is int[] intArray) - { - int index = 1; - foreach (var item in intArray) - { - Assert.Equal(expectedAttributes[i].Key, otlpAttributes[i + index].Key); - AssertOtlpAttributeValue(item, otlpAttributes[i + index]); - index++; - expectedSize++; - } - } - else if (current is double[] doubleArray) - { - int index = 2; - foreach (var item in doubleArray) - { - Assert.Equal(expectedAttributes[i].Key, otlpAttributes[i + index].Key); - AssertOtlpAttributeValue(item, otlpAttributes[i + index]); - index++; - expectedSize++; - } - } - else if (current is string[] stringArray) - { - int index = 3; - foreach (var item in stringArray) - { - Assert.Equal(expectedAttributes[i].Key, otlpAttributes[i + index].Key); - AssertOtlpAttributeValue(item, otlpAttributes[i + index]); - index++; - expectedSize++; - } - } - } - else - { - Assert.Equal(expectedAttributes[i].Key, otlpAttributes[i].Key); - AssertOtlpAttributeValue(current, otlpAttributes[i]); - expectedSize++; - } - } - - Assert.Equal(expectedSize, otlpAttributes.Count); - } - - private static void AssertOtlpAttributeValue(object originalValue, OtlpCommon.KeyValue akv) - { - switch (originalValue) - { - case string s: - Assert.Equal(s, akv.Value.StringValue); - break; - case bool b: - Assert.Equal(b, akv.Value.BoolValue); - break; - case long l: - Assert.Equal(l, akv.Value.IntValue); - break; - case double d: - Assert.Equal(d, akv.Value.DoubleValue); - break; - case int i: - Assert.Equal(i, akv.Value.IntValue); - break; - default: - Assert.Equal(originalValue.ToString(), akv.Value.StringValue); - break; - } - } - private class NoopTraceServiceClient : OtlpCollector.TraceService.ITraceServiceClient { public OtlpCollector.ExportTraceServiceResponse Export(OtlpCollector.ExportTraceServiceRequest request, GrpcCore.Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default)