diff --git a/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs index fd06ffcdfaa..619668af3b4 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using System.Runtime.CompilerServices; #if SIGNED diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusCollectionManager.cs index 6430143c779..81fd43f9401 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusCollectionManager.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusCollectionManager.cs @@ -203,7 +203,7 @@ private ExportResult OnCollect(Batch metrics) } } - this.previousDataView = new ArraySegment(this.buffer, 0, cursor); + this.previousDataView = new ArraySegment(this.buffer, 0, Math.Max(cursor - 1, 0)); return ExportResult.Success; } catch (Exception) diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializerExt.cs index 1e638501dc7..af9e5be45f7 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializerExt.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializerExt.cs @@ -25,27 +25,6 @@ internal static partial class PrometheusSerializer { private static readonly string[] MetricTypes = new string[] { "untyped", "counter", "gauge", "histogram", "summary" }; - public static int WriteMetrics(byte[] buffer, int cursor, Batch metrics) - { - var spacing = false; - - foreach (var metric in metrics) - { - if (spacing) - { - buffer[cursor++] = ASCII_LINEFEED; - } - else - { - spacing = true; - } - - cursor = WriteMetric(buffer, cursor, metric); - } - - return cursor; - } - public static int WriteMetric(byte[] buffer, int cursor, Metric metric) { if (!string.IsNullOrWhiteSpace(metric.Description)) @@ -199,6 +178,8 @@ public static int WriteMetric(byte[] buffer, int cursor, Metric metric) } } + buffer[cursor++] = ASCII_LINEFEED; + return cursor; } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/OpenTelemetry.Exporter.Prometheus.Tests.csproj b/test/OpenTelemetry.Exporter.Prometheus.Tests/OpenTelemetry.Exporter.Prometheus.Tests.csproj index 51d98723d3a..8a62f0d0e3b 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.Tests/OpenTelemetry.Exporter.Prometheus.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/OpenTelemetry.Exporter.Prometheus.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterHttpServerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterHttpServerTests.cs index 8c3a35d77c8..bdf875ea8bc 100644 --- a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterHttpServerTests.cs +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterHttpServerTests.cs @@ -89,8 +89,6 @@ public async Task PrometheusExporterHttpServerIntegration() new KeyValuePair("key2", "value2"), }; - var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - var counter = meter.CreateCounter("counter_double"); counter.Add(100.18D, tags); counter.Add(0.99D, tags); @@ -99,29 +97,11 @@ public async Task PrometheusExporterHttpServerIntegration() using var response = await client.GetAsync($"{address}metrics").ConfigureAwait(false); - var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - string[] lines = content.Split('\n'); - - Assert.Equal( - $"# TYPE counter_double counter", - lines[0]); - - Assert.Contains( - $"counter_double{{key1=\"value1\",key2=\"value2\"}} 101.17", - lines[1]); - - var index = content.LastIndexOf(' '); - - Assert.Equal('\n', content[content.Length - 1]); - - var timestamp = long.Parse(content.Substring(index, content.Length - index - 1)); - - Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp); + Assert.Matches( + "^# TYPE counter_double counter\ncounter_double{key1='value1',key2='value2'} 101.17 \\d+\n$".Replace('\'', '"'), + await response.Content.ReadAsStringAsync().ConfigureAwait(false)); } } } diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs new file mode 100644 index 00000000000..0698a0cacbf --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs @@ -0,0 +1,142 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Text; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests +{ + public sealed class PrometheusSerializerTests + { + [Fact] + public void ZeroDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + "^# TYPE test_gauge gauge\ntest_gauge 123 \\d+\n$", + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void ZeroDimensionWithDescription() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, description: "Hello, world!"); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + "^# HELP test_gauge Hello, world!\n# TYPE test_gauge gauge\ntest_gauge 123 \\d+\n$", + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void ZeroDimensionWithUnit() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, unit: "seconds"); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + "^# TYPE test_gauge_seconds gauge\ntest_gauge_seconds 123 \\d+\n$", + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void OneDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateCounter("test_counter"); + counter.Add(123, new KeyValuePair("tagKey", "tagValue")); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + "^# TYPE test_counter counter\ntest_counter{tagKey='tagValue'} 123 \\d+\n$".Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void DoubleInfinites() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateCounter("test_counter"); + counter.Add(1.0E308); + counter.Add(1.0E308); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + "^# TYPE test_counter counter\ntest_counter \\+Inf \\d+\n$", + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs index 053b18c10a9..b00b8165bfa 100644 --- a/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs @@ -27,28 +27,25 @@ public class InMemoryExporterTests [Fact(Skip = "To be run after https://github.com/open-telemetry/opentelemetry-dotnet/issues/2361 is fixed")] public void InMemoryExporterShouldDeepCopyMetricPoints() { - var meter = new Meter(Utils.GetCurrentMethodName()); - - var exportedItems = new List(); - using var inMemoryReader = new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) - { - PreferredAggregationTemporality = AggregationTemporality.Delta, - }; + var metrics = new List(); + using var meter = new Meter(Utils.GetCurrentMethodName()); using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddMeter(meter.Name) - .AddReader(inMemoryReader) - .Build(); + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(metrics)) + { + PreferredAggregationTemporality = AggregationTemporality.Delta, + }) + .Build(); var counter = meter.CreateCounter("meter"); // Emit 10 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") counter.Add(10, new KeyValuePair("tag1", "value1")); - // Pull metric data from AggregatorStore - inMemoryReader.Collect(); + meterProvider.ForceFlush(); - var metric = exportedItems[0]; // Only one Metric object is added to the collection at this point + var metric = metrics[0]; // Only one Metric object is added to the collection at this point var metricPointsEnumerator = metric.GetMetricPoints().GetEnumerator(); Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric ref var metricPointForFirstExport = ref metricPointsEnumerator.Current; @@ -57,10 +54,9 @@ public void InMemoryExporterShouldDeepCopyMetricPoints() // Emit 25 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") counter.Add(25, new KeyValuePair("tag1", "value1")); - // Pull metric data from AggregatorStore - inMemoryReader.Collect(); + meterProvider.ForceFlush(); - metric = exportedItems[1]; // Second Metric object is added to the collection at this point + metric = metrics[1]; // Second Metric object is added to the collection at this point metricPointsEnumerator = metric.GetMetricPoints().GetEnumerator(); Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric var metricPointForSecondExport = metricPointsEnumerator.Current; diff --git a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs index f28df1911b3..c70b73feff1 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs @@ -594,7 +594,7 @@ public void MultithreadedDoubleCounterTest() } [Theory] - [MemberData(nameof(MetricsTestData.InvalidInstrumentNames), MemberType = typeof(MetricsTestData))] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) { var exportedItems = new List(); @@ -616,7 +616,7 @@ public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) } [Theory] - [MemberData(nameof(MetricsTestData.ValidInstrumentNames), MemberType = typeof(MetricsTestData))] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] public void InstrumentWithValidNameIsExportedTest(string name) { var exportedItems = new List(); diff --git a/test/OpenTelemetry.Tests/Metrics/MetricsTestData.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs similarity index 94% rename from test/OpenTelemetry.Tests/Metrics/MetricsTestData.cs rename to test/OpenTelemetry.Tests/Metrics/MetricTestData.cs index 9ecbff1a9a2..aab52a57fff 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricsTestData.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,7 @@ namespace OpenTelemetry.Metrics.Tests { - public class MetricsTestData + public class MetricTestData { public static IEnumerable InvalidInstrumentNames => new List diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs index d8ed17008ee..a446f160fca 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -54,7 +54,7 @@ public void ViewToRenameMetric() } [Theory] - [MemberData(nameof(MetricsTestData.InvalidInstrumentNames), MemberType = typeof(MetricsTestData))] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] public void AddViewWithInvalidNameThrowsArgumentException(string viewNewName) { var exportedItems = new List(); @@ -93,7 +93,7 @@ public void AddViewWithNullMetricStreamConfigurationThrowsArgumentnullException( } [Theory] - [MemberData(nameof(MetricsTestData.InvalidHistogramBounds), MemberType = typeof(MetricsTestData))] + [MemberData(nameof(MetricTestData.InvalidHistogramBounds), MemberType = typeof(MetricTestData))] public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] bounds) { var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() @@ -103,7 +103,7 @@ public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] bo } [Theory] - [MemberData(nameof(MetricsTestData.ValidInstrumentNames), MemberType = typeof(MetricsTestData))] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] public void ViewWithValidNameExported(string viewNewName) { var exportedItems = new List(); @@ -168,7 +168,7 @@ public void ViewToRenameMetricConditionally() } [Theory] - [MemberData(nameof(MetricsTestData.InvalidInstrumentNames), MemberType = typeof(MetricsTestData))] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] public void ViewWithInvalidNameIgnoredConditionally(string viewNewName) { using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); @@ -205,7 +205,7 @@ public void ViewWithInvalidNameIgnoredConditionally(string viewNewName) } [Theory] - [MemberData(nameof(MetricsTestData.ValidInstrumentNames), MemberType = typeof(MetricsTestData))] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] public void ViewWithValidNameConditionally(string viewNewName) { using var meter1 = new Meter("ViewToRenameMetricConditionallyTest");