From 9394c6d1ee2e2cd66571efd138ac7f6b6bf3a3a3 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Mon, 27 Sep 2021 17:38:34 +0200 Subject: [PATCH 01/27] add metrics mapper, replace serializer. --- ...race.OpenTelemetry.Exporter.Metrics.csproj | 1 + .../DynatraceMetricsExporter.cs | 7 +- .../DynatraceMetricsMapper.cs | 86 +++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj index a8b71ba..97f868a 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index d8bd479..7fc8aa4 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -38,6 +38,7 @@ public class DynatraceMetricsExporter : MetricExporter private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly DynatraceMetricSerializer _serializer; + private readonly Dynatrace.MetricUtils.MetricsSerializer _metricsSerializer; public DynatraceMetricsExporter(DynatraceExporterOptions options = null, ILogger logger = null) : this(options, logger, new HttpClient()) { } @@ -54,6 +55,7 @@ internal DynatraceMetricsExporter(DynatraceExporterOptions options, ILogger ExportAsync(IEnumerable metrics, CancellationToken cancellationToken) @@ -73,7 +75,10 @@ public override async Task ExportAsync(IEnumerable metrics foreach (var metric in chunk) { - _serializer.SerializeMetric(sb, metric); + foreach(var dynatraceMetric in DynatraceMetricsMapper.ToDynatraceMetric(metric)) + { + sb.AppendLine(_metricsSerializer.SerializeMetric(dynatraceMetric)); + } } var metricLines = sb.ToString(); diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs new file mode 100644 index 0000000..3daeaed --- /dev/null +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -0,0 +1,86 @@ + + +using System.Collections.Generic; +using OpenTelemetry.Metrics.Export; +using DynatraceMetric = Dynatrace.MetricUtils.Metric; +using DynatraceMetricFactory = Dynatrace.MetricUtils.MetricsFactory; +using System.Text; + +public class DynatraceMetricsMapper +{ + + /// + /// Combines metric namespace and key into a single key for use in . + /// + /// A metric key in the form {MetricNamespace}.{MetricKey} + private static string CreateMetricKey(Metric metric) + { + var keyBuilder = new StringBuilder(); + if (!string.IsNullOrEmpty(metric.MetricNamespace)) + { + keyBuilder.Append($"{metric.MetricNamespace}."); + } + keyBuilder.Append(metric.MetricName); + return keyBuilder.ToString(); + } + + public static IEnumerable ToDynatraceMetric(Metric metric) + { + var metricName = CreateMetricKey(metric); + foreach (var metricData in metric.Data) + { + var timestamp = metricData.Timestamp; + var dimensions = metricData.Labels; + + switch (metric.AggregationType) + { + case AggregationType.DoubleSum: + { + var sum = metricData as DoubleSumData; + var dynatraceMetric = DynatraceMetricFactory.CreateDoubleCounterDelta(metricName: metricName, + value: sum.Sum, + dimensions: dimensions, + timestamp: timestamp); + yield return dynatraceMetric; + break; + } + case AggregationType.LongSum: + { + var sum = metricData as Int64SumData; + var dynatraceMetric = DynatraceMetricFactory.CreateLongCounterDelta(metricName: metricName, + value: sum.Sum, + dimensions: dimensions, + timestamp: timestamp); + yield return dynatraceMetric; + break; + } + case AggregationType.DoubleSummary: + { + var summary = metricData as DoubleSummaryData; + var dynatraceMetric = DynatraceMetricFactory.CreateDoubleSummary(metricName: metricName, + min: summary.Min, + max: summary.Max, + sum: summary.Sum, + count: summary.Count, + dimensions: dimensions, + timestamp: timestamp); + yield return dynatraceMetric; + break; + } + case AggregationType.Int64Summary: + { + var summary = metricData as Int64SummaryData; + var dynatraceMetric = DynatraceMetricFactory.CreateLongSummary(metricName: metricName, + min: summary.Min, + max: summary.Max, + sum: summary.Sum, + count: summary.Count, + dimensions: dimensions, + timestamp: timestamp); + yield return dynatraceMetric; + break; + } + } + } + } +} \ No newline at end of file From 4e358a4f4fbca5530e8b8cc4ce19f1d7e32dade8 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Tue, 28 Sep 2021 11:43:12 +0200 Subject: [PATCH 02/27] remove unused code. --- .../DynatraceMetricSerializerTests.cs | 217 --------------- .../OneAgentMetadataEnricherTests.cs | 257 ------------------ .../Utils/NormalizeTests.cs | 227 ---------------- .../DynatraceMetadataEnricher.cs | 90 ------ .../DynatraceMetricSerializer.cs | 231 ---------------- .../DynatraceMetricsExporter.cs | 8 +- .../DynatraceMetricsMapper.cs | 16 +- .../FileReader.cs | 45 --- .../Utils/Normalize.cs | 226 --------------- 9 files changed, 18 insertions(+), 1299 deletions(-) delete mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricSerializerTests.cs delete mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/OneAgentMetadataEnricherTests.cs delete mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/Utils/NormalizeTests.cs delete mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetadataEnricher.cs delete mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricSerializer.cs delete mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics/FileReader.cs delete mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics/Utils/Normalize.cs diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricSerializerTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricSerializerTests.cs deleted file mode 100644 index 29a6112..0000000 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricSerializerTests.cs +++ /dev/null @@ -1,217 +0,0 @@ -// -// Copyright 2020 Dynatrace LLC -// -// 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 Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using OpenTelemetry.Metrics.Export; -using Xunit; - -namespace Dynatrace.OpenTelemetry.Exporter.Metrics.Tests -{ - public class DynatraceMetricSerializerTests - { - private static readonly ILogger _logger = NullLogger.Instance; - - [Fact] - public void SerializeLongSum() - { - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; - - var labels = new List> { - new KeyValuePair("label1", "value1"), - new KeyValuePair("label2", "value2") - }; - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 100, - Timestamp = timestamp - }); - - string serialized = new DynatraceMetricSerializer(_logger).SerializeMetric(metric); - string expected = "namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - - [Fact] - public void TestDimensionValuesNormalized() - { - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; - - var labels = new List> { - new KeyValuePair("label1", "\\=\" =="), - }; - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 100, - Timestamp = timestamp - }); - - string serialized = new DynatraceMetricSerializer(_logger).SerializeMetric(metric); - string expected = "namespace1.metric1,label1=\\\\\\=\\\"\\ \\=\\=,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeWithoutLabels() - { - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; - - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = new List>(), - Sum = 100, - Timestamp = timestamp - }); - - string serialized = new DynatraceMetricSerializer(_logger).SerializeMetric(metric); - string expected = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - - [Fact] - public void PrefixOption() - { - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; - - var labels = new List>(); - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 100, - Timestamp = timestamp - }); - - string serialized = new DynatraceMetricSerializer(_logger, prefix: "prefix1").SerializeMetric(metric); - string expected = "prefix1.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - - [Fact] - public void PrefixOptionWithTrailingDot() - { - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; - - var labels = new List>(); - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 100, - Timestamp = timestamp - }); - - string serialized = new DynatraceMetricSerializer(_logger, prefix: "prefix.").SerializeMetric(metric); - string expected = "prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - - [Fact] - public void DimensionsOption() - { - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; - - var labels = new List> { - new KeyValuePair("label1", "value1"), - new KeyValuePair("label2", "value2") - }; - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 100, - Timestamp = timestamp - }); - - string serialized = new DynatraceMetricSerializer(_logger, - defaultDimensions: new List> { - new KeyValuePair("default1", "value1") , - new KeyValuePair("default2", "value2") , - new KeyValuePair("default3", "value3") , - }).SerializeMetric(metric); - string expected = "namespace1.metric1,default1=value1,default2=value2,default3=value3,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeLongSumBatch() - { - var labels = new List>{ - new KeyValuePair("label1", "value1"), - new KeyValuePair("label2", "value2") - }; - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 100, - Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime - }); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 130, - Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628882).UtcDateTime - }); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 150, - Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628883).UtcDateTime - }); - - string serialized = new DynatraceMetricSerializer(_logger).SerializeMetric(metric); - string expected = - "namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + - "namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=130 1604660628882" + Environment.NewLine + - "namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=150 1604660628883" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - - [Fact] - public void DimensionPrecedence() - { - var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; - - var defaultDimensions = new Dictionary { { "dimension1", "default" }, { "dimension2", "default" }, { "dimension3", "default" } }; - var labels = new List> { - new KeyValuePair("dimension2", "label"), - new KeyValuePair("dimension3", "label") - }; - var staticDimensions = new Dictionary { { "dimension3", "static" } }; - - var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); - metric.Data.Add(new Int64SumData - { - Labels = labels, - Sum = 100, - Timestamp = timestamp - }); - - var serialized = new DynatraceMetricSerializer(_logger, null, defaultDimensions, staticDimensions).SerializeMetric(metric); - - string expected = "namespace1.metric1,dimension1=default,dimension2=label,dimension3=static,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine; - Assert.Equal(expected, serialized); - } - } -} diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/OneAgentMetadataEnricherTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/OneAgentMetadataEnricherTests.cs deleted file mode 100644 index 35e17c4..0000000 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/OneAgentMetadataEnricherTests.cs +++ /dev/null @@ -1,257 +0,0 @@ -// -// Copyright 2020 Dynatrace LLC -// -// 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.IO; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; - -namespace Dynatrace.OpenTelemetry.Exporter.Metrics.Tests -{ - public class DynatraceMetadataEnricherTests - { - [Fact] - public void ValidMultiline() - { - var enricher = new DynatraceMetadataEnricher(NullLogger.Instance); - var metadata = enricher.ProcessMetadata(new string[] { - "a=123", - "b=456", - }); - Assert.Collection(metadata, - elem1 => - { - Assert.Equal("a", elem1.Key); - Assert.Equal("123", elem1.Value); - }, - elem2 => - { - Assert.Equal("b", elem2.Key); - Assert.Equal("456", elem2.Value); - }); - } - - [Fact] - public void WrongSyntax() - { - var enricher = new DynatraceMetadataEnricher(NullLogger.Instance); - Assert.Empty(enricher.ProcessMetadata(new string[] { "=0x5c14d9a68d569861" })); - Assert.Empty(enricher.ProcessMetadata(new string[] { "otherKey=" })); - Assert.Empty(enricher.ProcessMetadata(new string[] { "" })); - Assert.Empty(enricher.ProcessMetadata(new string[] { "=" })); - Assert.Empty(enricher.ProcessMetadata(new string[] { "===" })); - Assert.Empty(enricher.ProcessMetadata(new string[] { })); - } - - [Fact] - public void IndirectionFileMissing() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Throws(); - - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - // contains only the element that was in the list before. - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(mock => mock.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - } - - [Fact] - public void IndirectionFileNotAccessible() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Throws(); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - } - - [Fact] - public void IndirectionFileAnyOtherException() - { - var fileReader = Mock.Of(); - // there is a whole host of exceptions that can be thrown by ReadAllText: https://docs.microsoft.com/en-us/dotnet/api/system.io.file.readalltext?view=net-5.0 - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Throws(); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - } - - [Fact] - public void IndirectionFileEmpty() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns(""); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - // if the metadata file is empty, there should be no attempt at reading the contents. - Mock.Get(fileReader).Verify(f => f.ReadAllLines(""), Times.Never()); - } - - [Fact] - public void IndirectionFileContainsAdditionalText() - { - var fileReader = Mock.Of(); - var indirectionFileContent = - @"indirection_file_name.properties - some other text - and some more text"; - - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns(indirectionFileContent); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - // if the metadata file is empty, there should be no attempt at reading the contents. - Mock.Get(fileReader).Verify(f => f.ReadAllLines(indirectionFileContent), Times.Once()); - } - - [Fact] - public void IndirectionTargetMissing() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns("indirection_file_name.properties"); - Mock.Get(fileReader).Setup(f => f.ReadAllLines(It.IsAny())).Throws(); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - Mock.Get(fileReader).Verify(f => f.ReadAllLines("indirection_file_name.properties"), Times.Once()); - } - - [Fact] - public void IndirectionTargetInvalidAccess() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns("indirection_file_name.properties"); - Mock.Get(fileReader).Setup(f => f.ReadAllLines(It.IsAny())).Throws(); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - Mock.Get(fileReader).Verify(f => f.ReadAllLines("indirection_file_name.properties"), Times.Once()); - } - - [Fact] - public void IndirectionTargetThrowsAnyOtherException() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns("indirection_file_name.properties"); - Mock.Get(fileReader).Setup(f => f.ReadAllLines(It.IsAny())).Throws(); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - Mock.Get(fileReader).Verify(f => f.ReadAllLines("indirection_file_name.properties"), Times.Once()); - } - - [Fact] - public void IndirectionTargetEmpty() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns("indirection_file_name.properties"); - Mock.Get(fileReader).Setup(f => f.ReadAllLines(It.IsAny())).Returns(Array.Empty()); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, item => Assert.Equal(item, kv)); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - Mock.Get(fileReader).Verify(f => f.ReadAllLines("indirection_file_name.properties"), Times.Once()); - } - - [Fact] - public void IndirectionTargetValid() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns("indirection_file_name.properties"); - Mock.Get(fileReader).Setup(f => f.ReadAllLines(It.IsAny())).Returns(new[] { "key1=value1", "key2=value2" }); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, - item => Assert.Equal(kv, item), - item => Assert.Equal(KeyValuePair.Create("key1", "value1"), item), - item => Assert.Equal(KeyValuePair.Create("key2", "value2"), item) - ); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - Mock.Get(fileReader).Verify(f => f.ReadAllLines("indirection_file_name.properties"), Times.Once()); - } - - [Fact] - public void IndirectionTargetValidWithInvalidLines() - { - var fileReader = Mock.Of(); - Mock.Get(fileReader).Setup(f => f.ReadAllText(It.IsAny())).Returns("indirection_file_name.properties"); - Mock.Get(fileReader).Setup(f => f.ReadAllLines(It.IsAny())).Returns(new[] { "key1=value1", "key2=", "=value2", "===" }); - var kv = KeyValuePair.Create("initialDimensionKey", "initialDimensionValue"); - var targetList = new List>() { kv }; - - var unitUnderTest = new DynatraceMetadataEnricher(NullLogger.Instance, fileReader); - unitUnderTest.EnrichWithDynatraceMetadata(targetList); - - Assert.Collection(targetList, - item => Assert.Equal(kv, item), - item => Assert.Equal(KeyValuePair.Create("key1", "value1"), item) - ); - Mock.Get(fileReader).Verify(f => f.ReadAllText("dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"), Times.Once()); - Mock.Get(fileReader).Verify(f => f.ReadAllLines("indirection_file_name.properties"), Times.Once()); - } - } -} diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/Utils/NormalizeTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/Utils/NormalizeTests.cs deleted file mode 100644 index 1e285b8..0000000 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/Utils/NormalizeTests.cs +++ /dev/null @@ -1,227 +0,0 @@ -// -// Copyright 2021 Dynatrace LLC -// -// 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.Linq; -using Xunit; - -// This warning appears since the parametrized unit tests have a name (for human readability) -// which is not used, since it is currently not supported by xUnit. This #pragma disables the warning that the name is not used. -#pragma warning disable xUnit1026 - -namespace Dynatrace.OpenTelemetry.Exporter.Metrics.Utils.Tests -{ - - public class NormalizeTests - { - [Theory] - [InlineData("valid base case", "basecase", "basecase")] - [InlineData("valid base case", "just.a.normal.key", "just.a.normal.key")] - [InlineData("valid leading underscore", "_case", "_case")] - [InlineData("valid underscore", "case_case", "case_case")] - [InlineData("valid number", "case1", "case1")] - [InlineData("invalid leading number", "1case", "_case")] - [InlineData("invalid multiple leading", "!@#case", "_case")] - [InlineData("invalid multiple trailing", "case!@#", "case_")] - [InlineData("valid leading uppercase", "Case", "Case")] - [InlineData("valid all uppercase", "CASE", "CASE")] - [InlineData("valid intermittent uppercase", "someCase", "someCase")] - [InlineData("valid multiple sections", "prefix.case", "prefix.case")] - [InlineData("valid multiple sections upper", "This.Is.Valid", "This.Is.Valid")] - [InlineData("invalid multiple sections leading number", "0a.b", "_a.b")] - [InlineData("valid multiple section leading underscore", "_a.b", "_a.b")] - [InlineData("valid leading number second section", "a.0", "a.0")] - [InlineData("valid leading number second section 2", "a.0.c", "a.0.c")] - [InlineData("valid leading number second section 3", "a.0b.c", "a.0b.c")] - [InlineData("invalid leading hyphen", "-dim", "_dim")] - [InlineData("valid trailing hyphen", "dim-", "dim-")] - [InlineData("valid trailing hyphens", "dim---", "dim---")] - [InlineData("invalid empty", "", null)] - [InlineData("invalid only number", "000", "_")] - [InlineData("invalid key first section only number", "0.section", "_.section")] - [InlineData("invalid leading character", "~key", "_key")] - [InlineData("invalid leading characters", "~0#key", "_key")] - [InlineData("invalid intermittent character", "some~key", "some_key")] - [InlineData("invalid intermittent characters", "some#~äkey", "some_key")] - [InlineData("invalid two consecutive dots", "a..b", "a.b")] - [InlineData("invalid five consecutive dots", "a.....b", "a.b")] - [InlineData("invalid just a dot", ".", null)] - [InlineData("invalid three dots", "...", null)] - [InlineData("invalid leading dot", ".a", null)] - [InlineData("invalid trailing dot", "a.", "a")] - [InlineData("invalid enclosing dots", ".a.", null)] - [InlineData("valid consecutive leading underscores", "___a", "___a")] - [InlineData("valid consecutive trailing underscores", "a___", "a___")] - [InlineData("invalid trailing invalid chars groups", "a.b$%@.c#@", "a.b_.c_")] - [InlineData("valid consecutive enclosed underscores", "a___b", "a___b")] - [InlineData("invalid mixture dots underscores", "._._._a_._._.", null)] - [InlineData("valid mixture dots underscores 2", "_._._.a_._", "_._._.a_._")] - [InlineData("invalid empty section", "an..empty.section", "an.empty.section")] - [InlineData("invalid characters", "a,,,b c=d\\e\\ =,f", "a_b_c_d_e_f")] - [InlineData("invalid characters long", "a!b\"c#d$e%f&g'h(i)j*k+l,m-n.o/p:q;ru?v@w[x]y\\z^0 1_2;3{4|5}6~7", "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")] - [InlineData("invalid trailing characters", "a.b.+", "a.b._")] - [InlineData("valid combined test", "metric.key-number-1.001", "metric.key-number-1.001")] - [InlineData("valid example 1", "MyMetric", "MyMetric")] - [InlineData("invalid example 1", "0MyMetric", "_MyMetric")] - [InlineData("invalid example 2", "mÄtric", "m_tric")] - [InlineData("invalid example 3", "metriÄ", "metri_")] - [InlineData("invalid example 4", "Ätric", "_tric")] - [InlineData("invalid example 5", "meträääääÖÖÖc", "metr_c")] - public void MetricKeyNormalizedCorrectly(string name, string input, string expected) - { - Assert.Equal(expected, Normalize.MetricKey(input)); - } - - [Fact] - public void MetricKeyTruncatedCorrectly() - { - Assert.Equal(new string('a', 250), Normalize.MetricKey(new string('a', 270))); - } - - [Theory] - [InlineData("valid case", "dim", "dim")] - [InlineData("valid number", "dim1", "dim1")] - [InlineData("valid leading underscore", "_dim", "_dim")] - [InlineData("invalid leading uppercase", "Dim", "dim")] - [InlineData("invalid internal uppercase", "dIm", "dim")] - [InlineData("invalid trailing uppercase", "diM", "dim")] - [InlineData("invalid leading umlaut and uppercase", "äABC", "_abc")] - [InlineData("invalid multiple leading", "!@#case", "_case")] - [InlineData("invalid multiple trailing", "case!@#", "case_")] - [InlineData("invalid all uppercase", "DIM", "dim")] - [InlineData("valid dimension colon", "dim:dim", "dim:dim")] - [InlineData("valid dimension underscore", "dim_dim", "dim_dim")] - [InlineData("valid dimension hyphen", "dim-dim", "dim-dim")] - [InlineData("invalid leading hyphen", "-dim", "_dim")] - [InlineData("valid trailing hyphen", "dim-", "dim-")] - [InlineData("valid trailing hyphens", "dim---", "dim---")] - [InlineData("invalid leading multiple hyphens", "---dim", "_dim")] - [InlineData("invalid leading colon", ":dim", "_dim")] - [InlineData("invalid chars", "~@#ä", "_")] - [InlineData("invalid trailing chars", "aaa~@#ä", "aaa_")] - [InlineData("valid trailing underscores", "aaa___", "aaa___")] - [InlineData("invalid only numbers", "000", "_")] - [InlineData("valid compound key", "dim1.value1", "dim1.value1")] - [InlineData("invalid compound leading number", "dim.0dim", "dim._dim")] - [InlineData("invalid compound only number", "dim.000", "dim._")] - [InlineData("invalid compound leading invalid char", "dim.~val", "dim._val")] - [InlineData("invalid compound trailing invalid char", "dim.val~~", "dim.val_")] - [InlineData("invalid compound only invalid char", "dim.~~~", "dim._")] - [InlineData("valid compound leading underscore", "dim._val", "dim._val")] - [InlineData("valid compound only underscore", "dim.___", "dim.___")] - [InlineData("valid compound long", "dim.dim.dim.dim", "dim.dim.dim.dim")] - [InlineData("invalid two dots", "a..b", "a.b")] - [InlineData("invalid five dots", "a.....b", "a.b")] - [InlineData("invalid leading dot", ".a", "a")] - [InlineData("valid colon in compound", "a.b:c.d", "a.b:c.d")] - [InlineData("invalid trailing dot", "a.", "a")] - [InlineData("invalid just a dot", ".", "")] - [InlineData("invalid trailing dots", "a...", "a")] - [InlineData("invalid enclosing dots", ".a.", "a")] - [InlineData("invalid leading whitespace", " a", "_a")] - [InlineData("invalid trailing whitespace", "a ", "a_")] - [InlineData("invalid internal whitespace", "a b", "a_b")] - [InlineData("invalid internal whitespace", "a b", "a_b")] - [InlineData("invalid empty", "", "")] - [InlineData("valid combined key", "dim.val:count.val001", "dim.val:count.val001")] - [InlineData("invalid characters", "a,,,b c=d\\e\\ =,f", "a_b_c_d_e_f")] - [InlineData("invalid characters long", "a!b\"c#d$e%f&g'h(i)j*k+l,m-n.o/p:q;ru?v@w[x]y\\z^0 1_2;3{4|5}6~7", "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")] - [InlineData("invalid example 1", "Tag", "tag")] - [InlineData("invalid example 2", "0Tag", "_tag")] - [InlineData("invalid example 3", "tÄg", "t_g")] - [InlineData("invalid example 4", "mytäääg", "myt_g")] - [InlineData("invalid example 5", "ääätag", "_tag")] - [InlineData("invalid example 6", "ä_ätag", "___tag")] - [InlineData("invalid example 7", "Bla___", "bla___")] - public void DimensionKeyNormalizedCorrectly(string name, string input, string expected) - { - Assert.Equal(expected, Normalize.DimensionKey(input)); - } - - [Fact] - public void DimensionKeyTruncatedCorrectly() - { - Assert.Equal(new string('a', 100), Normalize.DimensionKey(new string('a', 110))); - } - - [Theory] - [InlineData("valid value", "value", "value")] - [InlineData("valid empty", "", "")] - [InlineData("pass null", null, "")] - [InlineData("valid uppercase", "VALUE", "VALUE")] - [InlineData("valid colon", "a:3", "a:3")] - [InlineData("valid value 2", "~@#ä", "~@#ä")] - [InlineData("valid spaces", "a b", "a b")] - [InlineData("valid comma", "a,b", "a,b")] - [InlineData("valid equals", "a=b", "a=b")] - [InlineData("valid backslash", "a\\b", "a\\b")] - [InlineData("valid multiple special chars", " ,=\\", " ,=\\")] - [InlineData("valid key-value pair", "key=\"value\"", "key=\"value\"")] - // \u0000 NUL character, \u0007 bell character - [InlineData("invalid unicode", "\u0000a\u0007", "_a_")] - [InlineData("invalid unicode space", "a\u0001b", "a_b")] - // 'Ab' in unicode: - [InlineData("valid unicode", "\u0034\u0066", "\u0034\u0066")] - // A umlaut, a with ring, O umlaut, U umlaut, all valid. - [InlineData("valid unicode", "\u0132_\u0133_\u0150_\u0156", "\u0132_\u0133_\u0150_\u0156")] - [InlineData("invalid leading unicode NUL", "\u0000a", "_a")] - [InlineData("invalid only unicode", "\u0000\u0000", "_")] - [InlineData("invalid consecutive leading unicode", "\u0000\u0000\u0000a", "_a")] - [InlineData("invalid consecutive trailing unicode", "a\u0000\u0000\u0000", "a_")] - [InlineData("invalid trailing unicode NUL", "a\u0000", "a_")] - [InlineData("invalid enclosed unicode NUL", "a\u0000b", "a_b")] - [InlineData("invalid consecutive enclosed unicode NUL", "a\u0000\u0007\u0000b", "a_b")] - public void DimensionValueNormalizedCorrectly(string name, string input, string expected) - { - Assert.Equal(expected, Normalize.DimensionValue(input)); - } - - [Fact] - public void DimensionValueTruncatedCorrectly() - { - Assert.Equal(new string('a', 250), Normalize.DimensionValue(new string('a', 270))); - } - - [Theory] - [InlineData("escape spaces", "a b", "a\\ b")] - [InlineData("escape comma", "a,b", "a\\,b")] - [InlineData("escape equals", "a=b", "a\\=b")] - [InlineData("escape backslash", "a\\b", "a\\\\b")] - [InlineData("escape double quotes", "a\"b\"\"c", "a\\\"b\\\"\\\"c")] - [InlineData("escape multiple special chars", " ,=\\", "\\ \\,\\=\\\\")] - [InlineData("escape consecutive special chars", " ,,==\\\\", "\\ \\ \\,\\,\\=\\=\\\\\\\\")] - [InlineData("escape key-value pair", "key=\"value\"", "key\\=\\\"value\\\"")] - public void DimensionValueEscapedCorrectly(string name, string input, string expected) - { - Assert.Equal(expected, Normalize.EscapeDimensionValue(input)); - } - - [Fact] - public void DimensionValueTruncatedCorrectlyAfterEscaping() - { - // escape too long string - Assert.Equal(string.Concat(Enumerable.Repeat("\\=", 125)), Normalize.EscapeDimensionValue(new string('=', 250))); - // escape sequence not broken apart 1 - Assert.Equal(new string('a', 249), Normalize.EscapeDimensionValue(new string('a', 249) + '=')); - // escape sequence not broken apart 2 - Assert.Equal(new string('a', 248) + "\\=", Normalize.EscapeDimensionValue(new string('a', 248) + "==")); - // escape sequence not broken apart 3: - // 3 trailing backslashes before escaping, 1 escaped trailing backslash - Assert.Equal(new string('a', 247) + "\\\\", Normalize.EscapeDimensionValue(new string('a', 247) + "\\\\\\")); - // dimension value of only backslashes - Assert.Equal(string.Concat(Enumerable.Repeat("\\\\", 125)), Normalize.EscapeDimensionValue(new string('\\', 260))); - } - } -} diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetadataEnricher.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetadataEnricher.cs deleted file mode 100644 index ddd92bf..0000000 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetadataEnricher.cs +++ /dev/null @@ -1,90 +0,0 @@ -// -// Copyright 2020 Dynatrace LLC -// -// 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 Microsoft.Extensions.Logging; - -namespace Dynatrace.OpenTelemetry.Exporter.Metrics -{ - /// - /// Queries Dynatrace metadata, and provides it as key-value pairs. - /// - class DynatraceMetadataEnricher - { - private readonly ILogger _logger; - private readonly IFileReader _fileReader; - - private const string OneAgentIndirectionFileName = "dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties"; - - public DynatraceMetadataEnricher(ILogger logger) : this(logger, new DefaultFileReader()) { } - - // Allows mocking of File.ReadAllText and File.ReadAllLines methods. When using the public constructor, - // the used FileReader passes the calls through to the System.IO methods. - internal DynatraceMetadataEnricher(ILogger logger, IFileReader fileReader) - { - _logger = logger; - _fileReader = fileReader; - } - - public void EnrichWithDynatraceMetadata(ICollection> labels) - { - var metadata = ProcessMetadata(GetMetadataFileContent()); - foreach (var md in metadata) - { - labels.Add(new KeyValuePair(md.Key, md.Value)); - } - } - - internal IEnumerable> ProcessMetadata(string[] lines) - { - foreach (var line in lines) - { - _logger.LogDebug("Parsing metadata line: {Line}", line); - var split = line.Split('='); - if (split.Length != 2) - { - _logger.LogWarning("Failed to parse line from metadata file: {Line}", line); - continue; - } - var key = split[0]; - var value = split[1]; - if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) - { - _logger.LogWarning("Failed to parse line from metadata file: {Line}", line); - continue; - } - yield return new KeyValuePair(split[0], split[1]); - } - } - - private string[] GetMetadataFileContent() - { - try - { - var metadataFilePath = _fileReader.ReadAllText(OneAgentIndirectionFileName); - if (string.IsNullOrEmpty(metadataFilePath)) - return Array.Empty(); - return _fileReader.ReadAllLines(metadataFilePath); - } - catch (Exception e) - { - _logger.LogWarning("Could not read metadata file. This is normal if OneAgent is not installed.", e.Message); - return Array.Empty(); - } - } - } -} diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricSerializer.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricSerializer.cs deleted file mode 100644 index d11b114..0000000 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricSerializer.cs +++ /dev/null @@ -1,231 +0,0 @@ -// -// Copyright 2020 Dynatrace LLC -// -// 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; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Text; -using Dynatrace.OpenTelemetry.Exporter.Metrics.Utils; -using Microsoft.Extensions.Logging; -using OpenTelemetry.Metrics.Export; - -namespace Dynatrace.OpenTelemetry.Exporter.Metrics -{ - class DynatraceMetricSerializer - { - private readonly ILogger _logger; - private readonly string _prefix; - private readonly IEnumerable> _defaultDimensions; - private readonly IEnumerable> _staticDimensions; - - // public constructor. - public DynatraceMetricSerializer(ILogger logger, string prefix = null, IEnumerable> defaultDimensions = null, bool enrichWithDynatraceMetadata = true) - : this(logger, prefix, defaultDimensions, PrepareMetadataDimensions(logger, enrichWithDynatraceMetadata)) { } - - // this is required to read the Dynatrace metadata dimensions and still use constructor chaining - private static IEnumerable> PrepareMetadataDimensions(ILogger logger, bool enrichWithDynatraceMetadata = true) - { - var metadataDimensions = new List>(); - - if (enrichWithDynatraceMetadata) - { - var enricher = new DynatraceMetadataEnricher(logger); - enricher.EnrichWithDynatraceMetadata(metadataDimensions); - } - return metadataDimensions; - } - - // internal constructor offers an interface for testing and is used by the public constructor - internal DynatraceMetricSerializer(ILogger logger, string prefix, IEnumerable> defaultDimensions, IEnumerable> metadataDimensions) - { - _logger = logger; - _prefix = prefix; - _defaultDimensions = Normalize.DimensionList(defaultDimensions); - - var staticDimensions = new List> { new KeyValuePair("dt.metrics.source", "opentelemetry") }; - staticDimensions.AddRange(metadataDimensions); - _staticDimensions = Normalize.DimensionList(staticDimensions); - } - - public string SerializeMetric(Metric metric) - { - var sb = new StringBuilder(); - SerializeMetric(sb, metric); - return sb.ToString(); - } - - public void SerializeMetric(StringBuilder sb, Metric metric) - { - foreach (var metricData in metric.Data) - { - var metricKey = CreateMetricKey(metric); - // skip lines with invalid metric keys. - if (string.IsNullOrEmpty(metricKey)) - { - _logger.LogWarning("metric key was empty after normalization, skipping metric (original name '{}')", metric.MetricName); - continue; - } - sb.Append(metricKey); - - // default dimensions and static dimensions are normalized once upon serializer creation. - // the labels from opentelemetry are normalized here, then all dimensions are merged. - var normalizedDimensions = MergeDimensions(_defaultDimensions, Normalize.DimensionList(metricData.Labels), _staticDimensions); - - // merged dimensions are normalized and escaped since we called Normalize.DimensionList on each of the sublists. - WriteDimensions(sb, normalizedDimensions); - - switch (metric.AggregationType) - { - case AggregationType.DoubleSum: - { - var sum = metricData as DoubleSumData; - var sumValue = sum.Sum; - WriteSum(sb, sumValue); - break; - } - - case AggregationType.LongSum: - { - var sum = metricData as Int64SumData; - var sumValue = sum.Sum; - WriteSum(sb, sumValue); - break; - } - - case AggregationType.DoubleSummary: - { - var summary = metricData as DoubleSummaryData; - var count = summary.Count; - var sum = summary.Sum; - var min = summary.Min; - var max = summary.Max; - WriteSummary(sb, sum, count, min, max); - break; - } - - case AggregationType.Int64Summary: - { - var summary = metricData as Int64SummaryData; - var count = summary.Count; - var sum = summary.Sum; - var min = summary.Min; - var max = summary.Max; - WriteSummary(sb, sum, count, min, max); - break; - } - } - - WriteTimestamp(sb, metricData.Timestamp); - sb.AppendLine(); - } - } - - private void WriteSummary(StringBuilder sb, double sum, long count, double min, double max) - { - sb.Append($" gauge,min={FormatDouble(min)},max={FormatDouble(max)},sum={FormatDouble(sum)},count={count}"); - } - - private string FormatDouble(double min) - { - return min.ToString("0.############"); - } - - private void WriteTimestamp(StringBuilder sb, DateTime timestamp) - { - sb.Append($" {new DateTimeOffset(timestamp.ToLocalTime()).ToUnixTimeMilliseconds()}"); - } - - private void WriteSum(StringBuilder sb, double sumValue) - { - sb.Append($" count,delta={sumValue}"); - } - - /// - /// Transforms OpenTelemetry metric names to metric keys valid in Dynatrace - /// - /// a valid metric key or null, if the input could not be normalized - private string CreateMetricKey(Metric metric) - { - var keyBuilder = new StringBuilder(); - if (!string.IsNullOrEmpty(_prefix)) - { - keyBuilder.Append($"{_prefix}."); - } - if (!string.IsNullOrEmpty(metric.MetricNamespace)) - { - keyBuilder.Append($"{metric.MetricNamespace}."); - } - keyBuilder.Append(metric.MetricName); - return Normalize.MetricKey(keyBuilder.ToString()); - } - - // Items from Enumerables passed further right overwrite items from Enumerables passed further left. - // Pass only normalized dimension lists to this function. - // The order of the items passed will be preserved. - private static List> MergeDimensions(params IEnumerable>[] dimensionLists) - { - var dictionary = new OrderedDictionary(); - - if (dimensionLists == null) - { - return new List>(); - } - - foreach (var dimensionList in dimensionLists) - { - if (dimensionList != null) - { - foreach (var dimension in dimensionList) - { - if (!dictionary.Contains(dimension.Key)) - { - dictionary.Add(dimension.Key, dimension.Value); - } - else - { - dictionary[dimension.Key] = dimension.Value; - } - } - } - } - - var outList = new List>(dictionary.Count); - - foreach (DictionaryEntry entry in dictionary) - { - outList.Add(new KeyValuePair(entry.Key.ToString(), entry.Value.ToString())); - } - - return outList; - } - - // pass only normalized lists to this function. - private void WriteDimensions(StringBuilder sb, List> dimensions) - { - // should be negative if there are fewer dimensions than the maximum - var diffToMaxDimensions = DynatraceMetricApiConstants.MaxDimensions - dimensions.Count; - var toSkip = diffToMaxDimensions < 0 ? Math.Abs(diffToMaxDimensions) : 0; - - // if there are more dimensions, skip the first n dimensions so that 50 dimensions remain - foreach (var dimension in dimensions.Skip(toSkip)) - { - sb.Append($",{dimension.Key}={dimension.Value}"); - } - } - } -} diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index 7fc8aa4..4bf9492 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -37,8 +37,7 @@ public class DynatraceMetricsExporter : MetricExporter private readonly DynatraceExporterOptions _options; private readonly ILogger _logger; private readonly HttpClient _httpClient; - private readonly DynatraceMetricSerializer _serializer; - private readonly Dynatrace.MetricUtils.MetricsSerializer _metricsSerializer; + private readonly Dynatrace.MetricUtils.MetricsSerializer _serializer; public DynatraceMetricsExporter(DynatraceExporterOptions options = null, ILogger logger = null) : this(options, logger, new HttpClient()) { } @@ -54,8 +53,7 @@ internal DynatraceMetricsExporter(DynatraceExporterOptions options, ILogger ExportAsync(IEnumerable metrics, CancellationToken cancellationToken) @@ -77,7 +75,7 @@ public override async Task ExportAsync(IEnumerable metrics { foreach(var dynatraceMetric in DynatraceMetricsMapper.ToDynatraceMetric(metric)) { - sb.AppendLine(_metricsSerializer.SerializeMetric(dynatraceMetric)); + sb.AppendLine(_serializer.SerializeMetric(dynatraceMetric)); } } diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 3daeaed..7c5b5d2 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -1,4 +1,18 @@ - +// +// Copyright 2020 Dynatrace LLC +// +// 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 OpenTelemetry.Metrics.Export; diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/FileReader.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/FileReader.cs deleted file mode 100644 index 31a83bc..0000000 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/FileReader.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright 2021 Dynatrace LLC -// -// 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.IO; - -namespace Dynatrace.OpenTelemetry.Exporter.Metrics -{ - /// - /// Provides specific file reading operations, which are passed through to the - /// respective methods on System.IO.File - /// - internal interface IFileReader - { - string ReadAllText(string filename); - string[] ReadAllLines(string filename); - } - - internal class DefaultFileReader : IFileReader - { - /// Returns the result of File.ReadAllLines(filename). - public string[] ReadAllLines(string filename) - { - return File.ReadAllLines(filename); - } - - /// Returns the result of File.ReadAllText(filename). - public string ReadAllText(string filename) - { - return File.ReadAllText(filename); - } - } -} diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Utils/Normalize.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Utils/Normalize.cs deleted file mode 100644 index 4332a92..0000000 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Utils/Normalize.cs +++ /dev/null @@ -1,226 +0,0 @@ -// -// Copyright 2021 Dynatrace LLC -// -// 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.Text; -using System.Text.RegularExpressions; - -namespace Dynatrace.OpenTelemetry.Exporter.Metrics.Utils -{ - class Normalize - { - private const int MaxLengthMetricKey = 250; - private const int MaxLengthDimensionKey = 100; - private const int MaxLengthDimensionValue = 250; - - // Metric keys (mk) - // characters not valid as leading characters in the first identifier key section - private static readonly Regex ReMkFirstIdentifierSectionStart = new Regex("^[^a-zA-Z_]+", RegexOptions.Compiled); - // characters not valid as leading characters in subsequent subsections. - private static readonly Regex ReMkSubsequentIdentifierSectionStart = new Regex("^[^a-zA-Z0-9_]+", RegexOptions.Compiled); - // invalid characters for the rest of the key. - private static readonly Regex ReMkInvalidCharacters = new Regex("[^a-zA-Z0-9_\\-]+", RegexOptions.Compiled); - - // Dimension keys (dk) - // Dimension keys start with a lowercase letter or an underscore. - private static readonly Regex ReDkSectionStart = new Regex("^[^a-z_]+", RegexOptions.Compiled); - // invalid characters in the rest of the dimension key - private static readonly Regex ReDkInvalidCharacters = new Regex("[^a-z0-9_\\-:]+", RegexOptions.Compiled); - - // Dimension values (dv) - // Characters that need to be escaped in dimension values - private static readonly Regex ReDvCharactersToEscape = new Regex("([= ,\\\\\"])", RegexOptions.Compiled); - private static readonly Regex ReDvControlCharacters = new Regex("[\\p{C}]+", RegexOptions.Compiled); - - // This regex checks if there is an odd number of trailing backslashes in the string. It can be - // read as: {not a slash}{any number of 2-slash pairs}{one slash}{end line}. - private static readonly Regex ReDvHasOddNumberOfTrailingBackslashes = new Regex("[^\\\\](?:\\\\\\\\)*\\\\$", RegexOptions.Compiled); - - private Normalize() { } - - - /// - /// Transforms OpenTelemetry metric names into Dynatrace-compatible metric keys - /// - /// A valid Dynatrace metric key or null, if the input could not be normalized - internal static string MetricKey(string key) - { - if (string.IsNullOrWhiteSpace(key)) - { - return null; - } - - if (key.Length > MaxLengthMetricKey) - { - key = key.Substring(0, MaxLengthMetricKey); - } - - var sections = key.Split('.'); - if (sections.Length == 0) - { - return null; - } - bool firstSection = true; - StringBuilder normalizedKeyBuilder = new StringBuilder(); - - foreach (string section in sections) - { - // check only if it is empty, and ignore a null check. - if (section.Length == 0) - { - if (firstSection) - { - return null; - } - else - { - // skip empty sections - continue; - } - } - - string normalizedSection; - // first key section cannot start with a number while subsequent sections can. - if (firstSection) - { - normalizedSection = ReMkFirstIdentifierSectionStart.Replace(section, "_"); - } - else - { - normalizedSection = ReMkSubsequentIdentifierSectionStart.Replace(section, "_"); - } - - // replace invalid chars with an underscore - normalizedSection = ReMkInvalidCharacters.Replace(normalizedSection, "_"); - - // re-concatenate the split sections separated with dots. - if (!firstSection) - { - normalizedKeyBuilder.Append("."); - } - else - { - firstSection = false; - } - - normalizedKeyBuilder.Append(normalizedSection); - } - return normalizedKeyBuilder.ToString(); - } - - internal static string DimensionKey(string key) - { - if (string.IsNullOrEmpty(key)) - { - return ""; - } - if (key.Length > MaxLengthDimensionKey) - { - key = key.Substring(0, MaxLengthDimensionKey); - } - - var sections = key.Split('.'); - var normalizedKeyBuilder = new StringBuilder(); - bool firstSection = true; - - foreach (string section in sections) - { - if (section.Length > 0) - { - // move to lowercase - string normalizedSection = section.ToLower(); - // replace consecutive leading chars with an underscore. - normalizedSection = ReDkSectionStart.Replace(normalizedSection, "_"); - // replace consecutive invalid characters within the section with one underscore: - normalizedSection = ReDkInvalidCharacters.Replace(normalizedSection, "_"); - - // re-concatenate the split sections separated with dots. - if (!firstSection) - { - normalizedKeyBuilder.Append("."); - } - else - { - firstSection = false; - } - - normalizedKeyBuilder.Append(normalizedSection); - } - } - return normalizedKeyBuilder.ToString(); - } - - internal static string DimensionValue(string value) - { - if (string.IsNullOrEmpty(value)) - { - return ""; - } - if (value.Length > MaxLengthDimensionValue) - { - value = value.Substring(0, MaxLengthDimensionValue); - } - - // collapse invalid characters to an underscore. - value = ReDvControlCharacters.Replace(value, "_"); - - return value; - } - - internal static string EscapeDimensionValue(string value) - { - // escape characters matched by regex with backslash. $1 inserts the matched character. - var escaped = ReDvCharactersToEscape.Replace(value, "\\$1"); - if (escaped.Length > MaxLengthDimensionValue) - { - escaped = escaped.Substring(0, MaxLengthDimensionValue); - if (ReDvHasOddNumberOfTrailingBackslashes.IsMatch(escaped)) - { - // string has trailing backslashes. Since every backslash must be escaped, there must be an - // even number of backslashes, otherwise the substring operation cut an escaped character - // in half: e.g.: "some_long_string," -> escaped: "some_long_string\," -> cut with substring - // results in "some_long_string\" since the two slashes were on either side of the char - // at which the string was cut using substring. If this is the case, trim the last - // backslash character, resulting in a properly escaped string. - escaped = escaped.Substring(0, MaxLengthDimensionValue - 1); - } - } - - return escaped; - } - - internal static IEnumerable> DimensionList(IEnumerable> dimensions) - { - var targetList = new List>(); - if (dimensions == null) - { - return targetList; - } - - foreach (var dimension in dimensions) - { - - var normalizedKey = DimensionKey(dimension.Key); - if (!string.IsNullOrEmpty(normalizedKey)) - { - targetList.Add(new KeyValuePair(normalizedKey, EscapeDimensionValue(DimensionValue(dimension.Value)))); - } - } - - return targetList; - } - } -} From 9972c834fe3758346874aa088f439f474f1ba47f Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Tue, 28 Sep 2021 15:32:43 +0200 Subject: [PATCH 03/27] make DynatraceMetricsMapper internal and static. --- .../DynatraceMetricsMapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 7c5b5d2..5a341cc 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -20,7 +20,7 @@ using DynatraceMetricFactory = Dynatrace.MetricUtils.MetricsFactory; using System.Text; -public class DynatraceMetricsMapper +internal static class DynatraceMetricsMapper { /// @@ -38,7 +38,7 @@ private static string CreateMetricKey(Metric metric) return keyBuilder.ToString(); } - public static IEnumerable ToDynatraceMetric(Metric metric) + internal static IEnumerable ToDynatraceMetric(Metric metric) { var metricName = CreateMetricKey(metric); foreach (var metricData in metric.Data) From f42bd15e6e142e26177c30dcb2a66e27372b08ff Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Tue, 28 Sep 2021 18:02:20 +0200 Subject: [PATCH 04/27] switch order of PackageReferences. --- .../Dynatrace.OpenTelemetry.Exporter.Metrics.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj index 97f868a..b2090bd 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj @@ -18,8 +18,8 @@ - + From a0843075b56de94f3dd94aa0790125d79a73375f Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Wed, 29 Sep 2021 13:23:07 +0200 Subject: [PATCH 05/27] catch MetricException on Mapping and Serialization --- .../DynatraceMetricsExporterTests.cs | 126 +++++++++++++++++- .../DynatraceMetricsExporter.cs | 41 +++++- 2 files changed, 160 insertions(+), 7 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 363b522..b7f2251 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -22,6 +22,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Moq; using Moq.Protected; using OpenTelemetry.Metrics.Export; @@ -138,6 +139,129 @@ public async void TestExportMultipleWithPrefix() Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); } + [Fact] + public async void TestExportMultiDataMetric() + { + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; + + var metric = new Metric("namespace1", "metric1", "Description", AggregationType.LongSum); + metric.Data.Add(new Int64SumData + { + Sum = 100, + Timestamp = timestamp + }); + metric.Data.Add(new Int64SumData + { + Sum = 101, + Timestamp = timestamp + }); + + var mockMessageHandler = new Mock(); + HttpRequestMessage req = null; + + mockMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("test") + }) + .Callback((HttpRequestMessage r, CancellationToken _) => req = r); + + var exporter = new DynatraceMetricsExporter(null, null, new HttpClient(mockMessageHandler.Object)); + + await exporter.ExportAsync(new List { metric }, CancellationToken.None); + var expectedString = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + + "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=101 1604660628881" + Environment.NewLine; + + mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); + Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); + } + + [Fact] + public async void TestExportNullNameMetric() + { + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; + + var metric = new Metric(null, null, null, AggregationType.LongSum); + metric.Data.Add(new Int64SumData + { + Sum = 100, + Timestamp = timestamp + }); + + var mockMessageHandler = new Mock(); + HttpRequestMessage req = null; + + mockMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("test") + }) + .Callback((HttpRequestMessage r, CancellationToken _) => req = r); + + var mockLogger = new Mock>(); + + var exporter = new DynatraceMetricsExporter(null, mockLogger.Object, new HttpClient(mockMessageHandler.Object)); + + await exporter.ExportAsync(new List { metric }, CancellationToken.None); + var expectedString = String.Empty; + + mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); + Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); + mockLogger.Verify(x => x.Log(It.Is(level => level == LogLevel.Warning), + It.IsAny(), + It.Is((value, type) => value.ToString().Contains("Mapping")), + It.IsAny(), + It.IsAny>()), Times.Exactly(1)); + } + + [Fact] + public async void TestExportLargeMetric() + { + var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; + + var dimensions = new List>(); + // 20 dimensions of ~ 100 characters should result in lines with more than 2000 characters + for (var i = 0; i < 20; i++) + { + // creates a dimension that takes up a little more than 100 characters + dimensions.Add(new KeyValuePair(new string('a', 50) + i, new string('b', 50) + i)); + } + + var metric = CreateMetric(); + metric.Data[0].Labels = dimensions; + + var mockMessageHandler = new Mock(); + HttpRequestMessage req = null; + + mockMessageHandler.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("test") + }) + .Callback((HttpRequestMessage r, CancellationToken _) => req = r); + + var mockLogger = new Mock>(); + + var exporter = new DynatraceMetricsExporter(null, mockLogger.Object, new HttpClient(mockMessageHandler.Object)); + + await exporter.ExportAsync(new List { metric }, CancellationToken.None); + var expectedString = String.Empty; + + mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); + Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); + mockLogger.Verify(x => x.Log(It.Is(level => level == LogLevel.Warning), + It.IsAny(), + It.Is((value, type) => value.ToString().Contains("Serialization")), + It.IsAny(), + It.IsAny>()), Times.Exactly(1)); + } + private List CreateMetrics() { var metrics = new List(); @@ -162,7 +286,6 @@ private List CreateMetrics() metrics.Add(metric2); return metrics; - } private Metric CreateMetric() @@ -183,6 +306,5 @@ private Metric CreateMetric() return metric; } - } } diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index 4bf9492..035877d 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -25,6 +25,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using OpenTelemetry.Metrics.Export; +using DynatraceMetric = Dynatrace.MetricUtils.Metric; +using DynatraceMetricSerializer = Dynatrace.MetricUtils.MetricsSerializer; +using DynatraceMetricException = Dynatrace.MetricUtils.MetricException; namespace Dynatrace.OpenTelemetry.Exporter.Metrics { @@ -53,7 +56,7 @@ internal DynatraceMetricsExporter(DynatraceExporterOptions options, ILogger ExportAsync(IEnumerable metrics, CancellationToken cancellationToken) @@ -73,10 +76,7 @@ public override async Task ExportAsync(IEnumerable metrics foreach (var metric in chunk) { - foreach(var dynatraceMetric in DynatraceMetricsMapper.ToDynatraceMetric(metric)) - { - sb.AppendLine(_serializer.SerializeMetric(dynatraceMetric)); - } + SerializeMetric(sb, metric); } var metricLines = sb.ToString(); @@ -107,5 +107,36 @@ public override async Task ExportAsync(IEnumerable metrics // if all chunks were exported successfully, return success, otherwise failed. return exportResults.All(x => x == ExportResult.Success) ? ExportResult.Success : ExportResult.FailedNotRetryable; } + + private void SerializeMetric(StringBuilder sb, Metric metric) + { + IEnumerable dynatraceMetrics = DynatraceMetricsMapper.ToDynatraceMetric(metric); + using (var metricsEnumerator = dynatraceMetrics.GetEnumerator()) + { + var moveNext = false; + do + { + try + { + moveNext = metricsEnumerator.MoveNext(); + } + catch (DynatraceMetricException e) + { + _logger.LogWarning("Skipping metric with the original name '{}'. Mapping failed with message: {}", metric.MetricName, e.Message); + } + if (moveNext) + { + try + { + sb.AppendLine(_serializer.SerializeMetric(metricsEnumerator.Current)); + } + catch (DynatraceMetricException e) + { + _logger.LogWarning("Skipping metric with the original name '{}'. Serialization failed with message: {}", metric.MetricName, e.Message); + } + } + } while (moveNext); + } + } } } From 733426cf71f93aff00485d64501aec23c76fca0c Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Wed, 29 Sep 2021 13:30:06 +0200 Subject: [PATCH 06/27] Use DynatraceMetricsSerializer instead of Dynatrace.MetricUtils.MetricsSerializer for consistency. --- .../DynatraceMetricsExporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index 035877d..4f959a7 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -40,7 +40,7 @@ public class DynatraceMetricsExporter : MetricExporter private readonly DynatraceExporterOptions _options; private readonly ILogger _logger; private readonly HttpClient _httpClient; - private readonly Dynatrace.MetricUtils.MetricsSerializer _serializer; + private readonly DynatraceMetricSerializer _serializer; public DynatraceMetricsExporter(DynatraceExporterOptions options = null, ILogger logger = null) : this(options, logger, new HttpClient()) { } From 0207066822a50c606987c540773b5bfdf4c36f84 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Wed, 29 Sep 2021 13:45:37 +0200 Subject: [PATCH 07/27] change do-while to while loop. --- .../DynatraceMetricsExporterTests.cs | 2 +- .../DynatraceMetricsExporter.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index b7f2251..22fd9c3 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -261,7 +261,7 @@ public async void TestExportLargeMetric() It.IsAny(), It.IsAny>()), Times.Exactly(1)); } - + private List CreateMetrics() { var metrics = new List(); diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index 4f959a7..1e49879 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -113,8 +113,8 @@ private void SerializeMetric(StringBuilder sb, Metric metric) IEnumerable dynatraceMetrics = DynatraceMetricsMapper.ToDynatraceMetric(metric); using (var metricsEnumerator = dynatraceMetrics.GetEnumerator()) { - var moveNext = false; - do + var moveNext = true; + while(moveNext) { try { @@ -123,6 +123,7 @@ private void SerializeMetric(StringBuilder sb, Metric metric) catch (DynatraceMetricException e) { _logger.LogWarning("Skipping metric with the original name '{}'. Mapping failed with message: {}", metric.MetricName, e.Message); + continue; } if (moveNext) { @@ -135,7 +136,7 @@ private void SerializeMetric(StringBuilder sb, Metric metric) _logger.LogWarning("Skipping metric with the original name '{}'. Serialization failed with message: {}", metric.MetricName, e.Message); } } - } while (moveNext); + } } } } From 789f8452ca5aaf91e1d1c83f4581fb632a7fe70c Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Wed, 29 Sep 2021 13:47:51 +0200 Subject: [PATCH 08/27] update copyright year in DynatraceMetricsMapper. --- .../DynatraceMetricsMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 5a341cc..733cd4b 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -1,5 +1,5 @@ // -// Copyright 2020 Dynatrace LLC +// Copyright 2021 Dynatrace LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From 15b395303fe4b4b8328c97d56f91b546e10a9f3a Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Wed, 29 Sep 2021 13:52:52 +0200 Subject: [PATCH 09/27] add newline to end of DynatraceMetricsMapper.cs --- .../DynatraceMetricsMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 733cd4b..4a3015b 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -97,4 +97,4 @@ internal static IEnumerable ToDynatraceMetric(Metric metric) } } } -} \ No newline at end of file +} From 844fd4e7b3d76bd088e669481ab057509e5b4ad0 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Wed, 29 Sep 2021 16:59:01 +0200 Subject: [PATCH 10/27] do not send requests if there are no valid metrics to send. fix indentation. --- .../DynatraceMetricsExporterTests.cs | 10 ++-- .../DynatraceMetricsExporter.cs | 7 +++ .../DynatraceMetricsMapper.cs | 48 ++++++++++--------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 22fd9c3..0072d87 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -207,10 +207,8 @@ public async void TestExportNullNameMetric() var exporter = new DynatraceMetricsExporter(null, mockLogger.Object, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { metric }, CancellationToken.None); - var expectedString = String.Empty; - mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); + mockMessageHandler.Protected().Verify("SendAsync", Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); mockLogger.Verify(x => x.Log(It.Is(level => level == LogLevel.Warning), It.IsAny(), It.Is((value, type) => value.ToString().Contains("Mapping")), @@ -251,17 +249,15 @@ public async void TestExportLargeMetric() var exporter = new DynatraceMetricsExporter(null, mockLogger.Object, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { metric }, CancellationToken.None); - var expectedString = String.Empty; - mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); + mockMessageHandler.Protected().Verify("SendAsync", Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); mockLogger.Verify(x => x.Log(It.Is(level => level == LogLevel.Warning), It.IsAny(), It.Is((value, type) => value.ToString().Contains("Serialization")), It.IsAny(), It.IsAny>()), Times.Exactly(1)); } - + private List CreateMetrics() { var metrics = new List(); diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index 1e49879..76bc204 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -81,6 +81,13 @@ public override async Task ExportAsync(IEnumerable metrics var metricLines = sb.ToString(); _logger.LogDebug(metricLines); + + // If there are no metric lines, we do not need to send a request. + if(metricLines.Length == 0) + { + return ExportResult.Success; + } + httpRequest.Content = new StringContent(metricLines); try { diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 4a3015b..40cdc13 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -51,46 +51,50 @@ internal static IEnumerable ToDynatraceMetric(Metric metric) case AggregationType.DoubleSum: { var sum = metricData as DoubleSumData; - var dynatraceMetric = DynatraceMetricFactory.CreateDoubleCounterDelta(metricName: metricName, - value: sum.Sum, - dimensions: dimensions, - timestamp: timestamp); + var dynatraceMetric = DynatraceMetricFactory.CreateDoubleCounterDelta( + metricName: metricName, + value: sum.Sum, + dimensions: dimensions, + timestamp: timestamp); yield return dynatraceMetric; break; } case AggregationType.LongSum: { var sum = metricData as Int64SumData; - var dynatraceMetric = DynatraceMetricFactory.CreateLongCounterDelta(metricName: metricName, - value: sum.Sum, - dimensions: dimensions, - timestamp: timestamp); + var dynatraceMetric = DynatraceMetricFactory.CreateLongCounterDelta( + metricName: metricName, + value: sum.Sum, + dimensions: dimensions, + timestamp: timestamp); yield return dynatraceMetric; break; } case AggregationType.DoubleSummary: { var summary = metricData as DoubleSummaryData; - var dynatraceMetric = DynatraceMetricFactory.CreateDoubleSummary(metricName: metricName, - min: summary.Min, - max: summary.Max, - sum: summary.Sum, - count: summary.Count, - dimensions: dimensions, - timestamp: timestamp); + var dynatraceMetric = DynatraceMetricFactory.CreateDoubleSummary( + metricName: metricName, + min: summary.Min, + max: summary.Max, + sum: summary.Sum, + count: summary.Count, + dimensions: dimensions, + timestamp: timestamp); yield return dynatraceMetric; break; } case AggregationType.Int64Summary: { var summary = metricData as Int64SummaryData; - var dynatraceMetric = DynatraceMetricFactory.CreateLongSummary(metricName: metricName, - min: summary.Min, - max: summary.Max, - sum: summary.Sum, - count: summary.Count, - dimensions: dimensions, - timestamp: timestamp); + var dynatraceMetric = DynatraceMetricFactory.CreateLongSummary( + metricName: metricName, + min: summary.Min, + max: summary.Max, + sum: summary.Sum, + count: summary.Count, + dimensions: dimensions, + timestamp: timestamp); yield return dynatraceMetric; break; } From ee755002d90d4287405817b2b62085dce960fba4 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 10:47:17 +0200 Subject: [PATCH 11/27] Convert manual iteration to foreach. Actually fix indentation. --- .../DynatraceMetricsExporterTests.cs | 22 ++-- .../DynatraceMetricsExporter.cs | 39 ++---- .../DynatraceMetricsMapper.cs | 114 ++++++++++-------- 3 files changed, 87 insertions(+), 88 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 0072d87..0783b1f 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -209,11 +209,12 @@ public async void TestExportNullNameMetric() await exporter.ExportAsync(new List { metric }, CancellationToken.None); mockMessageHandler.Protected().Verify("SendAsync", Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - mockLogger.Verify(x => x.Log(It.Is(level => level == LogLevel.Warning), - It.IsAny(), - It.Is((value, type) => value.ToString().Contains("Mapping")), - It.IsAny(), - It.IsAny>()), Times.Exactly(1)); + mockLogger.Verify(x => x.Log( + It.Is(level => level == LogLevel.Warning), + It.IsAny(), + It.Is((value, type) => value.ToString().Contains("Mapping")), + It.IsAny(), + It.IsAny>()), Times.Exactly(1)); } [Fact] @@ -251,11 +252,12 @@ public async void TestExportLargeMetric() await exporter.ExportAsync(new List { metric }, CancellationToken.None); mockMessageHandler.Protected().Verify("SendAsync", Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - mockLogger.Verify(x => x.Log(It.Is(level => level == LogLevel.Warning), - It.IsAny(), - It.Is((value, type) => value.ToString().Contains("Serialization")), - It.IsAny(), - It.IsAny>()), Times.Exactly(1)); + mockLogger.Verify(x => x.Log( + It.Is(level => level == LogLevel.Warning), + It.IsAny(), + It.Is((value, type) => value.ToString().Contains("Serialization")), + It.IsAny(), + It.IsAny>()), Times.Exactly(1)); } private List CreateMetrics() diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index 76bc204..35ea51e 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -25,7 +25,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using OpenTelemetry.Metrics.Export; -using DynatraceMetric = Dynatrace.MetricUtils.Metric; using DynatraceMetricSerializer = Dynatrace.MetricUtils.MetricsSerializer; using DynatraceMetricException = Dynatrace.MetricUtils.MetricException; @@ -56,7 +55,12 @@ internal DynatraceMetricsExporter(DynatraceExporterOptions options, ILogger ExportAsync(IEnumerable metrics, CancellationToken cancellationToken) @@ -117,32 +121,15 @@ public override async Task ExportAsync(IEnumerable metrics private void SerializeMetric(StringBuilder sb, Metric metric) { - IEnumerable dynatraceMetrics = DynatraceMetricsMapper.ToDynatraceMetric(metric); - using (var metricsEnumerator = dynatraceMetrics.GetEnumerator()) + foreach(var dynatraceMetric in DynatraceMetricsMapper.ToDynatraceMetric(metric, _logger)) { - var moveNext = true; - while(moveNext) + try { - try - { - moveNext = metricsEnumerator.MoveNext(); - } - catch (DynatraceMetricException e) - { - _logger.LogWarning("Skipping metric with the original name '{}'. Mapping failed with message: {}", metric.MetricName, e.Message); - continue; - } - if (moveNext) - { - try - { - sb.AppendLine(_serializer.SerializeMetric(metricsEnumerator.Current)); - } - catch (DynatraceMetricException e) - { - _logger.LogWarning("Skipping metric with the original name '{}'. Serialization failed with message: {}", metric.MetricName, e.Message); - } - } + sb.AppendLine(_serializer.SerializeMetric(dynatraceMetric)); + } + catch (DynatraceMetricException e) + { + _logger.LogWarning("Skipping metric with the original name '{}'. Serialization failed with message: {}", metric.MetricName, e.Message); } } } diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 40cdc13..18d90cf 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -18,7 +18,9 @@ using OpenTelemetry.Metrics.Export; using DynatraceMetric = Dynatrace.MetricUtils.Metric; using DynatraceMetricFactory = Dynatrace.MetricUtils.MetricsFactory; +using DynatraceMetricException = Dynatrace.MetricUtils.MetricException; using System.Text; +using Microsoft.Extensions.Logging; internal static class DynatraceMetricsMapper { @@ -38,66 +40,74 @@ private static string CreateMetricKey(Metric metric) return keyBuilder.ToString(); } - internal static IEnumerable ToDynatraceMetric(Metric metric) + internal static IEnumerable ToDynatraceMetric(Metric metric, ILogger logger) { var metricName = CreateMetricKey(metric); foreach (var metricData in metric.Data) { + DynatraceMetric dynatraceMetric = null; var timestamp = metricData.Timestamp; var dimensions = metricData.Labels; + try + { + switch (metric.AggregationType) + { + case AggregationType.DoubleSum: + { + var sum = metricData as DoubleSumData; + dynatraceMetric = DynatraceMetricFactory.CreateDoubleCounterDelta( + metricName: metricName, + value: sum.Sum, + dimensions: dimensions, + timestamp: timestamp); + break; + } + case AggregationType.LongSum: + { + var sum = metricData as Int64SumData; + dynatraceMetric = DynatraceMetricFactory.CreateLongCounterDelta( + metricName: metricName, + value: sum.Sum, + dimensions: dimensions, + timestamp: timestamp); + break; + } + case AggregationType.DoubleSummary: + { + var summary = metricData as DoubleSummaryData; + dynatraceMetric = DynatraceMetricFactory.CreateDoubleSummary( + metricName: metricName, + min: summary.Min, + max: summary.Max, + sum: summary.Sum, + count: summary.Count, + dimensions: dimensions, + timestamp: timestamp); + break; + } + case AggregationType.Int64Summary: + { + var summary = metricData as Int64SummaryData; + dynatraceMetric = DynatraceMetricFactory.CreateLongSummary( + metricName: metricName, + min: summary.Min, + max: summary.Max, + sum: summary.Sum, + count: summary.Count, + dimensions: dimensions, + timestamp: timestamp); + break; + } + } + } + catch (DynatraceMetricException e) + { + logger.LogWarning("Skipping metric with the original name '{}'. Mapping failed with message: {}", metric.MetricName, e.Message); + } - switch (metric.AggregationType) + if(dynatraceMetric != null) { - case AggregationType.DoubleSum: - { - var sum = metricData as DoubleSumData; - var dynatraceMetric = DynatraceMetricFactory.CreateDoubleCounterDelta( - metricName: metricName, - value: sum.Sum, - dimensions: dimensions, - timestamp: timestamp); - yield return dynatraceMetric; - break; - } - case AggregationType.LongSum: - { - var sum = metricData as Int64SumData; - var dynatraceMetric = DynatraceMetricFactory.CreateLongCounterDelta( - metricName: metricName, - value: sum.Sum, - dimensions: dimensions, - timestamp: timestamp); - yield return dynatraceMetric; - break; - } - case AggregationType.DoubleSummary: - { - var summary = metricData as DoubleSummaryData; - var dynatraceMetric = DynatraceMetricFactory.CreateDoubleSummary( - metricName: metricName, - min: summary.Min, - max: summary.Max, - sum: summary.Sum, - count: summary.Count, - dimensions: dimensions, - timestamp: timestamp); - yield return dynatraceMetric; - break; - } - case AggregationType.Int64Summary: - { - var summary = metricData as Int64SummaryData; - var dynatraceMetric = DynatraceMetricFactory.CreateLongSummary( - metricName: metricName, - min: summary.Min, - max: summary.Max, - sum: summary.Sum, - count: summary.Count, - dimensions: dimensions, - timestamp: timestamp); - yield return dynatraceMetric; - break; - } + yield return dynatraceMetric; } } } From 865ec7702b5927bafbac1a70801480ed06bbbd30 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 15:24:49 +0200 Subject: [PATCH 12/27] use pattern matching in switch, add default case. update formatting. --- .../DynatraceMetricsMapper.cs | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 18d90cf..812aaab 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -24,7 +24,6 @@ internal static class DynatraceMetricsMapper { - /// /// Combines metric namespace and key into a single key for use in . /// @@ -46,66 +45,65 @@ internal static IEnumerable ToDynatraceMetric(Metric metric, IL foreach (var metricData in metric.Data) { DynatraceMetric dynatraceMetric = null; - var timestamp = metricData.Timestamp; - var dimensions = metricData.Labels; try { - switch (metric.AggregationType) + switch (metricData) { - case AggregationType.DoubleSum: + case DoubleSumData sum: { - var sum = metricData as DoubleSumData; dynatraceMetric = DynatraceMetricFactory.CreateDoubleCounterDelta( metricName: metricName, value: sum.Sum, - dimensions: dimensions, - timestamp: timestamp); + dimensions: metricData.Labels, + timestamp: metricData.Timestamp); break; } - case AggregationType.LongSum: + case Int64SumData sum: { - var sum = metricData as Int64SumData; dynatraceMetric = DynatraceMetricFactory.CreateLongCounterDelta( metricName: metricName, value: sum.Sum, - dimensions: dimensions, - timestamp: timestamp); + dimensions: metricData.Labels, + timestamp: metricData.Timestamp); break; } - case AggregationType.DoubleSummary: + case DoubleSummaryData summary: { - var summary = metricData as DoubleSummaryData; dynatraceMetric = DynatraceMetricFactory.CreateDoubleSummary( metricName: metricName, min: summary.Min, max: summary.Max, sum: summary.Sum, count: summary.Count, - dimensions: dimensions, - timestamp: timestamp); + dimensions: metricData.Labels, + timestamp: metricData.Timestamp); break; } - case AggregationType.Int64Summary: + case Int64SummaryData summary: { - var summary = metricData as Int64SummaryData; dynatraceMetric = DynatraceMetricFactory.CreateLongSummary( metricName: metricName, min: summary.Min, max: summary.Max, sum: summary.Sum, count: summary.Count, - dimensions: dimensions, - timestamp: timestamp); + dimensions: metricData.Labels, + timestamp: metricData.Timestamp); + break; + } + default: + { + logger.LogWarning("Skipping metric with the original name '{MetricName}'. Mapping of {MetricDataType} is not yet supported.", metric.MetricName, metricData.GetType().FullName); break; } } } catch (DynatraceMetricException e) { - logger.LogWarning("Skipping metric with the original name '{}'. Mapping failed with message: {}", metric.MetricName, e.Message); + logger.LogWarning("Skipping metric with the original name '{MetricName}'. Mapping failed with message: {Message}", metric.MetricName, e.Message); } - if(dynatraceMetric != null) + if (dynatraceMetric != null) { yield return dynatraceMetric; } From 838153e14f69caa55c8663d31c81735ea5ea7d10 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 16:14:38 +0200 Subject: [PATCH 13/27] use await instead of .Result, mark test helpers static. --- .../DynatraceMetricsExporterTests.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 0783b1f..cff8a01 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -59,7 +59,8 @@ public async void TestDefaultOptions() Assert.True(actualRequestMessage.Headers.Contains("User-Agent")); Assert.Single(actualRequestMessage.Headers.GetValues("User-Agent")); Assert.Equal("opentelemetry-metric-dotnet", actualRequestMessage.Headers.GetValues("User-Agent").First()); - Assert.Equal("namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine, actualRequestMessage.Content.ReadAsStringAsync().Result); + var actualMetricString = await actualRequestMessage.Content.ReadAsStringAsync(); + Assert.Equal("namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine, actualMetricString); } [Fact] @@ -85,7 +86,8 @@ public async void TestUriAndToken() Assert.True(req.Headers.Contains("Authorization")); Assert.Single(req.Headers.GetValues("Authorization")); Assert.Equal("Api-Token test-token", req.Headers.GetValues("Authorization").First()); - Assert.Equal("namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine, req.Content.ReadAsStringAsync().Result); + var actualMetricString = await req.Content.ReadAsStringAsync(); + Assert.Equal("namespace1.metric1,label1=value1,label2=value2,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine, actualMetricString); } [Fact] @@ -131,12 +133,13 @@ public async void TestExportMultipleWithPrefix() var exporter = new DynatraceMetricsExporter(new DynatraceExporterOptions { Prefix = "my.prefix" }, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(CreateMetrics(), CancellationToken.None); - var expectedString = "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "my.prefix.namespace2.metric2,dt.metrics.source=opentelemetry count,delta=200 1604660628881" + Environment.NewLine; + var expectedMetricString = "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "my.prefix.namespace2.metric2,dt.metrics.source=opentelemetry count,delta=200 1604660628881" + Environment.NewLine; mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); Assert.Equal(DynatraceMetricApiConstants.DefaultOneAgentEndpoint, req.RequestUri.AbsoluteUri); Assert.False(req.Headers.Contains("Api-Token")); - Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); + var actualMetricString = await req.Content.ReadAsStringAsync(); + Assert.Equal(expectedMetricString, actualMetricString); } [Fact] @@ -171,11 +174,12 @@ public async void TestExportMultiDataMetric() var exporter = new DynatraceMetricsExporter(null, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { metric }, CancellationToken.None); - var expectedString = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + + var expectedMetricString = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=101 1604660628881" + Environment.NewLine; mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - Assert.Equal(expectedString, req.Content.ReadAsStringAsync().Result); + var actualMetricString = await req.Content.ReadAsStringAsync(); + Assert.Equal(expectedMetricString, actualMetricString); } [Fact] @@ -260,7 +264,7 @@ public async void TestExportLargeMetric() It.IsAny>()), Times.Exactly(1)); } - private List CreateMetrics() + private static List CreateMetrics() { var metrics = new List(); @@ -286,7 +290,7 @@ private List CreateMetrics() return metrics; } - private Metric CreateMetric() + private static Metric CreateMetric() { var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; From 0a488a8f45c2c13f73ce90f58a8cab606c8efe5d Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 16:19:57 +0200 Subject: [PATCH 14/27] update formatting. --- .../DynatraceMetricsExporterTests.cs | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index cff8a01..9aa9c55 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -51,7 +51,11 @@ public async void TestDefaultOptions() var exporter = new DynatraceMetricsExporter(null, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { CreateMetric() }, CancellationToken.None); - mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.IsAny(), ItExpr.IsAny()); + mockMessageHandler.Protected().Verify( + "SendAsync", + Times.Exactly(1), + ItExpr.IsAny(), + ItExpr.IsAny()); Assert.Equal(HttpMethod.Post, actualRequestMessage.Method); Assert.Equal(DynatraceMetricApiConstants.DefaultOneAgentEndpoint, actualRequestMessage.RequestUri.AbsoluteUri); @@ -81,7 +85,12 @@ public async void TestUriAndToken() var exporter = new DynatraceMetricsExporter(new DynatraceExporterOptions { Url = "http://my.url", ApiToken = "test-token" }, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { CreateMetric() }, CancellationToken.None); - mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.IsAny(), ItExpr.IsAny()); + mockMessageHandler.Protected().Verify( + "SendAsync", + Times.Exactly(1), + ItExpr.IsAny(), + ItExpr.IsAny()); + Assert.Equal("http://my.url/", req.RequestUri.AbsoluteUri); Assert.True(req.Headers.Contains("Authorization")); Assert.Single(req.Headers.GetValues("Authorization")); @@ -112,7 +121,11 @@ public async void TestSendInBatches() await exporter.ExportAsync(metrics, CancellationToken.None); // for more than 1000 lines, SendAsync is called twice. - mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(2), ItExpr.IsAny(), ItExpr.IsAny()); + mockMessageHandler.Protected().Verify( + "SendAsync", + Times.Exactly(2), + ItExpr.IsAny(), + ItExpr.IsAny()); } [Fact] @@ -135,7 +148,11 @@ public async void TestExportMultipleWithPrefix() await exporter.ExportAsync(CreateMetrics(), CancellationToken.None); var expectedMetricString = "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "my.prefix.namespace2.metric2,dt.metrics.source=opentelemetry count,delta=200 1604660628881" + Environment.NewLine; - mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); + mockMessageHandler.Protected().Verify( + "SendAsync", + Times.Exactly(1), + ItExpr.Is(req => req.Method == HttpMethod.Post), + ItExpr.IsAny()); Assert.Equal(DynatraceMetricApiConstants.DefaultOneAgentEndpoint, req.RequestUri.AbsoluteUri); Assert.False(req.Headers.Contains("Api-Token")); var actualMetricString = await req.Content.ReadAsStringAsync(); @@ -177,7 +194,12 @@ public async void TestExportMultiDataMetric() var expectedMetricString = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=101 1604660628881" + Environment.NewLine; - mockMessageHandler.Protected().Verify("SendAsync", Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); + mockMessageHandler.Protected().Verify( + "SendAsync", + Times.Exactly(1), + ItExpr.Is(req => req.Method == HttpMethod.Post), + ItExpr.IsAny()); + var actualMetricString = await req.Content.ReadAsStringAsync(); Assert.Equal(expectedMetricString, actualMetricString); } @@ -212,7 +234,12 @@ public async void TestExportNullNameMetric() await exporter.ExportAsync(new List { metric }, CancellationToken.None); - mockMessageHandler.Protected().Verify("SendAsync", Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); + mockMessageHandler.Protected().Verify( + "SendAsync", + Times.Never(), + ItExpr.Is(req => req.Method == HttpMethod.Post), + ItExpr.IsAny()); + mockLogger.Verify(x => x.Log( It.Is(level => level == LogLevel.Warning), It.IsAny(), @@ -250,12 +277,16 @@ public async void TestExportLargeMetric() .Callback((HttpRequestMessage r, CancellationToken _) => req = r); var mockLogger = new Mock>(); - var exporter = new DynatraceMetricsExporter(null, mockLogger.Object, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { metric }, CancellationToken.None); - mockMessageHandler.Protected().Verify("SendAsync", Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); + mockMessageHandler.Protected().Verify( + "SendAsync", + Times.Never(), + ItExpr.Is(req => req.Method == HttpMethod.Post), + ItExpr.IsAny()); + mockLogger.Verify(x => x.Log( It.Is(level => level == LogLevel.Warning), It.IsAny(), From 73bdf0e7006f5b711461722068605a8fa65c1546 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 16:23:33 +0200 Subject: [PATCH 15/27] update formatting. --- .../DynatraceMetricsMapper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index 812aaab..d7814ee 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -31,10 +31,12 @@ internal static class DynatraceMetricsMapper private static string CreateMetricKey(Metric metric) { var keyBuilder = new StringBuilder(); + if (!string.IsNullOrEmpty(metric.MetricNamespace)) { keyBuilder.Append($"{metric.MetricNamespace}."); } + keyBuilder.Append(metric.MetricName); return keyBuilder.ToString(); } From b164498b0d01e611de68d6e87fdf5e3617452725 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 16:29:18 +0200 Subject: [PATCH 16/27] rename DynatraceMetricsMapper into DynatraceMetricsExtensions and introduce extension on Metric. --- .../DynatraceMetricsExporter.cs | 2 +- .../DynatraceMetricsMapper.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index 35ea51e..f351ee9 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -121,7 +121,7 @@ public override async Task ExportAsync(IEnumerable metrics private void SerializeMetric(StringBuilder sb, Metric metric) { - foreach(var dynatraceMetric in DynatraceMetricsMapper.ToDynatraceMetric(metric, _logger)) + foreach(var dynatraceMetric in metric.ToDynatraceMetrics(_logger)) { try { diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs index d7814ee..680e226 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs @@ -22,7 +22,7 @@ using System.Text; using Microsoft.Extensions.Logging; -internal static class DynatraceMetricsMapper +internal static class DynatraceMetricsExtensions { /// /// Combines metric namespace and key into a single key for use in . @@ -36,12 +36,12 @@ private static string CreateMetricKey(Metric metric) { keyBuilder.Append($"{metric.MetricNamespace}."); } - + keyBuilder.Append(metric.MetricName); return keyBuilder.ToString(); } - internal static IEnumerable ToDynatraceMetric(Metric metric, ILogger logger) + internal static IEnumerable ToDynatraceMetrics(this Metric metric, ILogger logger) { var metricName = CreateMetricKey(metric); foreach (var metricData in metric.Data) From 9ab17bfe8390d3c1a13be346d4680671519f3b8c Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 16:33:56 +0200 Subject: [PATCH 17/27] rename file DynatraceMetricsMapper.cs to DynatraceMetricExtensions.cs --- .../{DynatraceMetricsMapper.cs => DynatraceMetricsExtensions.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Dynatrace.OpenTelemetry.Exporter.Metrics/{DynatraceMetricsMapper.cs => DynatraceMetricsExtensions.cs} (100%) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs similarity index 100% rename from src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsMapper.cs rename to src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs From 835ff5335dc09e3c9ac832818f9561d676157669 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Thu, 30 Sep 2021 16:44:46 +0200 Subject: [PATCH 18/27] do not use async void --- .../DynatraceMetricsExporterTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 9aa9c55..7672d83 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -33,7 +33,7 @@ namespace Dynatrace.OpenTelemetry.Exporter.Metrics.Tests public class DynatraceMetricsExporterTests { [Fact] - public async void TestDefaultOptions() + public async Task TestDefaultOptions() { var mockMessageHandler = new Mock(); // this var will hold the actual passed in params @@ -68,7 +68,7 @@ public async void TestDefaultOptions() } [Fact] - public async void TestUriAndToken() + public async Task TestUriAndToken() { var mockMessageHandler = new Mock(); HttpRequestMessage req = null; @@ -100,7 +100,7 @@ public async void TestUriAndToken() } [Fact] - public async void TestSendInBatches() + public async Task TestSendInBatches() { var mockMessageHandler = new Mock(); mockMessageHandler.Protected() @@ -129,7 +129,7 @@ public async void TestSendInBatches() } [Fact] - public async void TestExportMultipleWithPrefix() + public async Task TestExportMultipleWithPrefix() { var mockMessageHandler = new Mock(); HttpRequestMessage req = null; @@ -160,7 +160,7 @@ public async void TestExportMultipleWithPrefix() } [Fact] - public async void TestExportMultiDataMetric() + public async Task TestExportMultiDataMetric() { var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; @@ -205,7 +205,7 @@ public async void TestExportMultiDataMetric() } [Fact] - public async void TestExportNullNameMetric() + public async Task TestExportNullNameMetric() { var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; @@ -249,7 +249,7 @@ public async void TestExportNullNameMetric() } [Fact] - public async void TestExportLargeMetric() + public async Task TestExportLargeMetric() { var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; From c00e01c9403642861aada4f15d01fce48fe22262 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 09:10:50 +0200 Subject: [PATCH 19/27] change test names. --- .../DynatraceMetricsExporterTests.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 7672d83..ac09d2d 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -146,7 +146,9 @@ public async Task TestExportMultipleWithPrefix() var exporter = new DynatraceMetricsExporter(new DynatraceExporterOptions { Prefix = "my.prefix" }, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(CreateMetrics(), CancellationToken.None); - var expectedMetricString = "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "my.prefix.namespace2.metric2,dt.metrics.source=opentelemetry count,delta=200 1604660628881" + Environment.NewLine; + var expectedMetricString = + "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + + "my.prefix.namespace2.metric2,dt.metrics.source=opentelemetry count,delta=200 1604660628881" + Environment.NewLine; mockMessageHandler.Protected().Verify( "SendAsync", @@ -160,7 +162,7 @@ public async Task TestExportMultipleWithPrefix() } [Fact] - public async Task TestExportMultiDataMetric() + public async Task ExportAsync_WithMultiDataMetric_ShouldSendRequestWithMultipleLines() { var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; @@ -191,8 +193,9 @@ public async Task TestExportMultiDataMetric() var exporter = new DynatraceMetricsExporter(null, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { metric }, CancellationToken.None); - var expectedMetricString = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + - "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=101 1604660628881" + Environment.NewLine; + var expectedMetricString = + "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + + "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=101 1604660628881" + Environment.NewLine; mockMessageHandler.Protected().Verify( "SendAsync", @@ -205,7 +208,7 @@ public async Task TestExportMultiDataMetric() } [Fact] - public async Task TestExportNullNameMetric() + public async Task ExportAsync_WithNullNameMetric_ShouldNotSendRequestAndLogError() { var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; @@ -249,7 +252,7 @@ public async Task TestExportNullNameMetric() } [Fact] - public async Task TestExportLargeMetric() + public async Task ExportAsync_WithTooLargeMetric_ShouldNotSendRequest() { var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(1604660628881).UtcDateTime; From 965699d80a29112b4392851727e59b88a90a31ae Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 09:48:37 +0200 Subject: [PATCH 20/27] rename tests. --- .../DynatraceMetricsExporterTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index ac09d2d..54693ca 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -33,7 +33,7 @@ namespace Dynatrace.OpenTelemetry.Exporter.Metrics.Tests public class DynatraceMetricsExporterTests { [Fact] - public async Task TestDefaultOptions() + public async Task ExportAsync_WithDefaultOptions_ShouldSendRequestToOneAgent() { var mockMessageHandler = new Mock(); // this var will hold the actual passed in params @@ -68,7 +68,7 @@ public async Task TestDefaultOptions() } [Fact] - public async Task TestUriAndToken() + public async Task ExportAsync_WithUriAndTokenOptions_ShouldSendReqeustToUrlWithToken() { var mockMessageHandler = new Mock(); HttpRequestMessage req = null; @@ -100,7 +100,7 @@ public async Task TestUriAndToken() } [Fact] - public async Task TestSendInBatches() + public async Task ExportAsync_WithMoreThan1000Metrics_ShouldSendMoreThanOneRequest() { var mockMessageHandler = new Mock(); mockMessageHandler.Protected() @@ -129,7 +129,7 @@ public async Task TestSendInBatches() } [Fact] - public async Task TestExportMultipleWithPrefix() + public async Task ExportAsync_WithPrefixOptions_ShouldAppendPrefixToMetrics() { var mockMessageHandler = new Mock(); HttpRequestMessage req = null; From 9cc8abdb6764789ae00b1a6887ef5cc4975ff6ac Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 10:11:06 +0200 Subject: [PATCH 21/27] use 'using static System.Threading.CancellationToken;' in tests --- .../DynatraceMetricsExporterTests.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 54693ca..95cf1f3 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -27,6 +27,7 @@ using Moq.Protected; using OpenTelemetry.Metrics.Export; using Xunit; +using static System.Threading.CancellationToken; namespace Dynatrace.OpenTelemetry.Exporter.Metrics.Tests { @@ -50,7 +51,7 @@ public async Task ExportAsync_WithDefaultOptions_ShouldSendRequestToOneAgent() var exporter = new DynatraceMetricsExporter(null, null, new HttpClient(mockMessageHandler.Object)); - await exporter.ExportAsync(new List { CreateMetric() }, CancellationToken.None); + await exporter.ExportAsync(new List { CreateMetric() }, None); mockMessageHandler.Protected().Verify( "SendAsync", Times.Exactly(1), @@ -84,7 +85,7 @@ public async Task ExportAsync_WithUriAndTokenOptions_ShouldSendReqeustToUrlWithT var exporter = new DynatraceMetricsExporter(new DynatraceExporterOptions { Url = "http://my.url", ApiToken = "test-token" }, null, new HttpClient(mockMessageHandler.Object)); - await exporter.ExportAsync(new List { CreateMetric() }, CancellationToken.None); + await exporter.ExportAsync(new List { CreateMetric() }, None); mockMessageHandler.Protected().Verify( "SendAsync", Times.Exactly(1), @@ -118,7 +119,7 @@ public async Task ExportAsync_WithMoreThan1000Metrics_ShouldSendMoreThanOneReque metrics.Add(CreateMetric()); } - await exporter.ExportAsync(metrics, CancellationToken.None); + await exporter.ExportAsync(metrics, None); // for more than 1000 lines, SendAsync is called twice. mockMessageHandler.Protected().Verify( @@ -145,7 +146,7 @@ public async Task ExportAsync_WithPrefixOptions_ShouldAppendPrefixToMetrics() var exporter = new DynatraceMetricsExporter(new DynatraceExporterOptions { Prefix = "my.prefix" }, null, new HttpClient(mockMessageHandler.Object)); - await exporter.ExportAsync(CreateMetrics(), CancellationToken.None); + await exporter.ExportAsync(CreateMetrics(), None); var expectedMetricString = "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "my.prefix.namespace2.metric2,dt.metrics.source=opentelemetry count,delta=200 1604660628881" + Environment.NewLine; @@ -192,7 +193,7 @@ public async Task ExportAsync_WithMultiDataMetric_ShouldSendRequestWithMultipleL var exporter = new DynatraceMetricsExporter(null, null, new HttpClient(mockMessageHandler.Object)); - await exporter.ExportAsync(new List { metric }, CancellationToken.None); + await exporter.ExportAsync(new List { metric }, None); var expectedMetricString = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=101 1604660628881" + Environment.NewLine; @@ -235,7 +236,7 @@ public async Task ExportAsync_WithNullNameMetric_ShouldNotSendRequestAndLogError var exporter = new DynatraceMetricsExporter(null, mockLogger.Object, new HttpClient(mockMessageHandler.Object)); - await exporter.ExportAsync(new List { metric }, CancellationToken.None); + await exporter.ExportAsync(new List { metric }, None); mockMessageHandler.Protected().Verify( "SendAsync", @@ -282,7 +283,7 @@ public async Task ExportAsync_WithTooLargeMetric_ShouldNotSendRequest() var mockLogger = new Mock>(); var exporter = new DynatraceMetricsExporter(null, mockLogger.Object, new HttpClient(mockMessageHandler.Object)); - await exporter.ExportAsync(new List { metric }, CancellationToken.None); + await exporter.ExportAsync(new List { metric }, None); mockMessageHandler.Protected().Verify( "SendAsync", From 4d0a02b36c6f3e6bdbe479b131f9d4952f654b30 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 10:12:43 +0200 Subject: [PATCH 22/27] change method order in DynatraceMetricsExtensions. --- .../DynatraceMetricsExtensions.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs index 680e226..0120ae7 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs @@ -24,23 +24,6 @@ internal static class DynatraceMetricsExtensions { - /// - /// Combines metric namespace and key into a single key for use in . - /// - /// A metric key in the form {MetricNamespace}.{MetricKey} - private static string CreateMetricKey(Metric metric) - { - var keyBuilder = new StringBuilder(); - - if (!string.IsNullOrEmpty(metric.MetricNamespace)) - { - keyBuilder.Append($"{metric.MetricNamespace}."); - } - - keyBuilder.Append(metric.MetricName); - return keyBuilder.ToString(); - } - internal static IEnumerable ToDynatraceMetrics(this Metric metric, ILogger logger) { var metricName = CreateMetricKey(metric); @@ -111,4 +94,21 @@ internal static IEnumerable ToDynatraceMetrics(this Metric metr } } } + + /// + /// Combines metric namespace and key into a single key for use in . + /// + /// A metric key in the form {MetricNamespace}.{MetricKey} + private static string CreateMetricKey(Metric metric) + { + var keyBuilder = new StringBuilder(); + + if (!string.IsNullOrEmpty(metric.MetricNamespace)) + { + keyBuilder.Append($"{metric.MetricNamespace}."); + } + + keyBuilder.Append(metric.MetricName); + return keyBuilder.ToString(); + } } From d6fec0b2cc05de0118d3d540221b209133d2a272 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 10:21:19 +0200 Subject: [PATCH 23/27] fix formatting. --- .../DynatraceMetricsExporter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs index f351ee9..9704e1c 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExporter.cs @@ -87,7 +87,7 @@ public override async Task ExportAsync(IEnumerable metrics _logger.LogDebug(metricLines); // If there are no metric lines, we do not need to send a request. - if(metricLines.Length == 0) + if (metricLines.Length == 0) { return ExportResult.Success; } @@ -121,7 +121,7 @@ public override async Task ExportAsync(IEnumerable metrics private void SerializeMetric(StringBuilder sb, Metric metric) { - foreach(var dynatraceMetric in metric.ToDynatraceMetrics(_logger)) + foreach (var dynatraceMetric in metric.ToDynatraceMetrics(_logger)) { try { From 468aabbf79394a1da7577383359ed35869f849bd Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 10:49:01 +0200 Subject: [PATCH 24/27] fix whitespace. --- .../DynatraceMetricsExporterTests.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs index 95cf1f3..4934272 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/DynatraceMetricsExporterTests.cs @@ -1,4 +1,3 @@ - // // Copyright 2021 Dynatrace LLC // @@ -87,11 +86,11 @@ public async Task ExportAsync_WithUriAndTokenOptions_ShouldSendReqeustToUrlWithT await exporter.ExportAsync(new List { CreateMetric() }, None); mockMessageHandler.Protected().Verify( - "SendAsync", + "SendAsync", Times.Exactly(1), ItExpr.IsAny(), ItExpr.IsAny()); - + Assert.Equal("http://my.url/", req.RequestUri.AbsoluteUri); Assert.True(req.Headers.Contains("Authorization")); Assert.Single(req.Headers.GetValues("Authorization")); @@ -147,8 +146,8 @@ public async Task ExportAsync_WithPrefixOptions_ShouldAppendPrefixToMetrics() var exporter = new DynatraceMetricsExporter(new DynatraceExporterOptions { Prefix = "my.prefix" }, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(CreateMetrics(), None); - var expectedMetricString = - "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + + var expectedMetricString = + "my.prefix.namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "my.prefix.namespace2.metric2,dt.metrics.source=opentelemetry count,delta=200 1604660628881" + Environment.NewLine; mockMessageHandler.Protected().Verify( @@ -194,7 +193,7 @@ public async Task ExportAsync_WithMultiDataMetric_ShouldSendRequestWithMultipleL var exporter = new DynatraceMetricsExporter(null, null, new HttpClient(mockMessageHandler.Object)); await exporter.ExportAsync(new List { metric }, None); - var expectedMetricString = + var expectedMetricString = "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=100 1604660628881" + Environment.NewLine + "namespace1.metric1,dt.metrics.source=opentelemetry count,delta=101 1604660628881" + Environment.NewLine; @@ -203,7 +202,7 @@ public async Task ExportAsync_WithMultiDataMetric_ShouldSendRequestWithMultipleL Times.Exactly(1), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - + var actualMetricString = await req.Content.ReadAsStringAsync(); Assert.Equal(expectedMetricString, actualMetricString); } @@ -243,7 +242,7 @@ public async Task ExportAsync_WithNullNameMetric_ShouldNotSendRequestAndLogError Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - + mockLogger.Verify(x => x.Log( It.Is(level => level == LogLevel.Warning), It.IsAny(), @@ -290,7 +289,7 @@ public async Task ExportAsync_WithTooLargeMetric_ShouldNotSendRequest() Times.Never(), ItExpr.Is(req => req.Method == HttpMethod.Post), ItExpr.IsAny()); - + mockLogger.Verify(x => x.Log( It.Is(level => level == LogLevel.Warning), It.IsAny(), From 3f2573702dcdf3f6a2d1abd50e251e527cc65c3d Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 11:02:49 +0200 Subject: [PATCH 25/27] add GlobalSuppressions.cs --- .../GlobalSuppressions.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/GlobalSuppressions.cs diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/GlobalSuppressions.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..2b5e995 --- /dev/null +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics.Tests/GlobalSuppressions.cs @@ -0,0 +1,24 @@ +// +// Copyright 2021 Dynatrace LLC +// +// 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. +// + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Test project")] From 48ab008f7990f3b7e41f3b9d5324c00c0bceed88 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 11:11:40 +0200 Subject: [PATCH 26/27] fix whitespace. --- .../DynatraceMetricsExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs index 0120ae7..b02c139 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/DynatraceMetricsExtensions.cs @@ -94,7 +94,7 @@ internal static IEnumerable ToDynatraceMetrics(this Metric metr } } } - + /// /// Combines metric namespace and key into a single key for use in . /// From ce39559b55bd2fe42281178452f28bcb75cad113 Mon Sep 17 00:00:00 2001 From: "marc.pichler" Date: Fri, 1 Oct 2021 17:14:36 +0200 Subject: [PATCH 27/27] bump patch version due to dependency change. --- .../Dynatrace.OpenTelemetry.Exporter.Metrics.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj index b2090bd..5799526 100644 --- a/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj +++ b/src/Dynatrace.OpenTelemetry.Exporter.Metrics/Dynatrace.OpenTelemetry.Exporter.Metrics.csproj @@ -5,7 +5,7 @@ Dynatrace Dynatrace Dynatrace OpenTelemetry Metrics Exporter for .NET - 0.2.0-beta + 0.2.1-beta See https://github.com/dynatrace-oss/opentelemetry-metric-dotnet to learn more. Apache-2.0 Copyright 2020 Dynatrace LLC; Licensed under the Apache License, Version 2.0