diff --git a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md index 84dfece69b..e52f423ed0 100644 --- a/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md @@ -2,10 +2,24 @@ ## Unreleased +* Fix a bug where metrics without exemplars were not getting exported. + ([#1099](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1099)) + * Relaxed table name mapping validation rules to restore the previous behavior from version 1.3.0. ([#1109](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/issues/1109)) +* Add support for exporting metrics to more than a single account/namespace + combination using a single GenevaMetricExporter instance. Users can now export + individual metric streams to: + * An account of their choice by adding the dimension + `_microsoft_metrics_account` and providing a `string` value for it as the + account name. + * A metric namespace of their choice by adding the dimension + `_microsoft_metrics_namespace` and providing a `string` value for it as the + namespace name. + ([#1111](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1111)) + ## 1.5.0-alpha.1 Released 2023-Mar-13 diff --git a/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporter.cs b/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporter.cs index 72819a4f56..a9cafcfbb5 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporter.cs @@ -35,13 +35,13 @@ public class GenevaMetricExporter : BaseExporter internal const int MaxDimensionValueSize = 1024; - private readonly ushort prepopulatedDimensionsCount; + internal const string DimensionKeyForCustomMonitoringAccount = "_microsoft_metrics_account"; - private readonly int fixedPayloadStartIndex; + internal const string DimensionKeyForCustomMetricsNamespace = "_microsoft_metrics_namespace"; - private readonly string monitoringAccount; + private readonly ushort prepopulatedDimensionsCount; - private readonly string metricNamespace; + private readonly int fixedPayloadStartIndex; private readonly IMetricDataTransport metricDataTransport; @@ -59,6 +59,10 @@ public class GenevaMetricExporter : BaseExporter private readonly int bufferIndexForHistogramMetrics; + private readonly string defaultMonitoringAccount; + + private readonly string defaultMetricNamespace; + private bool isDisposed; public GenevaMetricExporter(GenevaMetricExporterOptions options) @@ -67,8 +71,8 @@ public GenevaMetricExporter(GenevaMetricExporterOptions options) Guard.ThrowIfNullOrWhitespace(options.ConnectionString); var connectionStringBuilder = new ConnectionStringBuilder(options.ConnectionString); - this.monitoringAccount = connectionStringBuilder.Account; - this.metricNamespace = connectionStringBuilder.Namespace; + this.defaultMonitoringAccount = connectionStringBuilder.Account; + this.defaultMetricNamespace = connectionStringBuilder.Namespace; if (options.PrepopulatedMetricDimensions != null) { @@ -119,6 +123,9 @@ public GenevaMetricExporter(GenevaMetricExporterOptions options) public override ExportResult Export(in Batch batch) { + string monitoringAccount = this.defaultMonitoringAccount; + string metricNamespace = this.defaultMetricNamespace; + var result = ExportResult.Success; foreach (var metric in batch) { @@ -140,7 +147,9 @@ public override ExportResult Export(in Batch batch) metricPoint.EndTime.ToFileTime(), // Using the endTime here as the timestamp as Geneva Metrics only allows for one field for timestamp metricPoint.Tags, metricData, - exemplars); + exemplars, + out monitoringAccount, + out metricNamespace); this.metricDataTransport.Send(MetricEventType.TLV, this.buffer, bodyLength); break; } @@ -158,7 +167,9 @@ public override ExportResult Export(in Batch batch) metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out monitoringAccount, + out metricNamespace); this.metricDataTransport.Send(MetricEventType.TLV, this.buffer, bodyLength); break; } @@ -176,7 +187,9 @@ public override ExportResult Export(in Batch batch) metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out monitoringAccount, + out metricNamespace); this.metricDataTransport.Send(MetricEventType.TLV, this.buffer, bodyLength); break; } @@ -192,7 +205,9 @@ public override ExportResult Export(in Batch batch) metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out monitoringAccount, + out metricNamespace); this.metricDataTransport.Send(MetricEventType.TLV, this.buffer, bodyLength); break; } @@ -207,7 +222,9 @@ public override ExportResult Export(in Batch batch) metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out monitoringAccount, + out metricNamespace); this.metricDataTransport.Send(MetricEventType.TLV, this.buffer, bodyLength); break; } @@ -231,7 +248,9 @@ public override ExportResult Export(in Batch batch) count, min, max, - exemplars); + exemplars, + out monitoringAccount, + out metricNamespace); this.metricDataTransport.Send(MetricEventType.TLV, this.buffer, bodyLength); break; } @@ -239,7 +258,7 @@ public override ExportResult Export(in Batch batch) } catch (Exception ex) { - ExporterEventSource.Log.FailedToSendMetricData(this.monitoringAccount, this.metricNamespace, metric.Name, ex); // TODO: preallocate exception or no exception + ExporterEventSource.Log.FailedToSendMetricData(monitoringAccount, metricNamespace, metric.Name, ex); // TODO: preallocate exception or no exception result = ExportResult.Failure; } } @@ -496,7 +515,9 @@ internal unsafe ushort SerializeMetricWithTLV( long timestamp, in ReadOnlyTagCollection tags, MetricData value, - Exemplar[] exemplars) + Exemplar[] exemplars, + out string monitoringAccount, + out string metricNamespace) { ushort bodyLength; try @@ -512,13 +533,20 @@ internal unsafe ushort SerializeMetricWithTLV( SerializeNonHistogramMetricData(eventType, value, timestamp, this.buffer, ref bufferIndex); - SerializeMetricDimensions(tags, this.prepopulatedDimensionsCount, this.serializedPrepopulatedDimensionsKeys, this.serializedPrepopulatedDimensionsValues, this.buffer, ref bufferIndex); + // Serializes metric dimensions and also gets the custom account name and metric namespace + // if specified by adding custom tags: _microsoft_metrics_namespace and _microsoft_metrics_namespace + this.SerializeDimensionsAndGetCustomAccountNamespace( + tags, + this.buffer, + ref bufferIndex, + out monitoringAccount, + out metricNamespace); SerializeExemplars(exemplars, this.buffer, ref bufferIndex); - SerializeMonitoringAccount(this.monitoringAccount, this.buffer, ref bufferIndex); + SerializeMonitoringAccount(monitoringAccount, this.buffer, ref bufferIndex); - SerializeMetricNamespace(this.metricNamespace, this.buffer, ref bufferIndex); + SerializeMetricNamespace(metricNamespace, this.buffer, ref bufferIndex); // Write the final size of the payload bodyLength = (ushort)(bufferIndex - this.fixedPayloadStartIndex); @@ -547,7 +575,9 @@ internal unsafe ushort SerializeHistogramMetricWithTLV( uint count, double min, double max, - Exemplar[] exemplars) + Exemplar[] exemplars, + out string monitoringAccount, + out string metricNamespace) { ushort bodyLength; try @@ -563,13 +593,20 @@ internal unsafe ushort SerializeHistogramMetricWithTLV( SerializeHistogramMetricData(buckets, sum, count, min, max, timestamp, this.buffer, ref bufferIndex); - SerializeMetricDimensions(tags, this.prepopulatedDimensionsCount, this.serializedPrepopulatedDimensionsKeys, this.serializedPrepopulatedDimensionsValues, this.buffer, ref bufferIndex); + // Serializes metric dimensions and also gets the custom account name and metric namespace + // if specified by adding custom tags: _microsoft_metrics_namespace and _microsoft_metrics_namespace + this.SerializeDimensionsAndGetCustomAccountNamespace( + tags, + this.buffer, + ref bufferIndex, + out monitoringAccount, + out metricNamespace); SerializeExemplars(exemplars, this.buffer, ref bufferIndex); - SerializeMonitoringAccount(this.monitoringAccount, this.buffer, ref bufferIndex); + SerializeMonitoringAccount(monitoringAccount, this.buffer, ref bufferIndex); - SerializeMetricNamespace(this.metricNamespace, this.buffer, ref bufferIndex); + SerializeMetricNamespace(metricNamespace, this.buffer, ref bufferIndex); // Write the final size of the payload bodyLength = (ushort)(bufferIndex - this.fixedPayloadStartIndex); @@ -610,70 +647,6 @@ private static void SerializeMonitoringAccount(string monitoringAccount, byte[] MetricSerializer.SerializeEncodedString(buffer, ref bufferIndex, Encoding.UTF8.GetBytes(monitoringAccount)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SerializeMetricDimensions(in ReadOnlyTagCollection tags, ushort prepopulatedDimensionsCount, List serializedPrepopulatedDimensionsKeys, List serializedPrepopulatedDimensionsValues, byte[] buffer, ref int bufferIndex) - { - MetricSerializer.SerializeByte(buffer, ref bufferIndex, (byte)PayloadType.Dimensions); - - // Get a placeholder to add the payloadType length - var payloadTypeStartIndex = bufferIndex; - bufferIndex += 2; - - // Get a placeholder to add dimensions count later - var bufferIndexForDimensionsCount = bufferIndex; - bufferIndex += 2; - - ushort dimensionsWritten = 0; - - // Serialize PrepopulatedDimensions keys - for (ushort i = 0; i < prepopulatedDimensionsCount; i++) - { - MetricSerializer.SerializeEncodedString(buffer, ref bufferIndex, serializedPrepopulatedDimensionsKeys[i]); - } - - if (prepopulatedDimensionsCount > 0) - { - dimensionsWritten += prepopulatedDimensionsCount; - } - - // Serialize MetricPoint Dimension keys - foreach (var tag in tags) - { - if (tag.Key.Length > MaxDimensionNameSize) - { - // TODO: Data Validation - } - - MetricSerializer.SerializeString(buffer, ref bufferIndex, tag.Key); - } - - dimensionsWritten += (ushort)tags.Count; - - // Serialize PrepopulatedDimensions values - for (ushort i = 0; i < prepopulatedDimensionsCount; i++) - { - MetricSerializer.SerializeEncodedString(buffer, ref bufferIndex, serializedPrepopulatedDimensionsValues[i]); - } - - // Serialize MetricPoint Dimension values - foreach (var tag in tags) - { - var dimensionValue = Convert.ToString(tag.Value, CultureInfo.InvariantCulture); - if (dimensionValue.Length > MaxDimensionValueSize) - { - // TODO: Data Validation - } - - MetricSerializer.SerializeString(buffer, ref bufferIndex, dimensionValue); - } - - // Backfill the number of dimensions written - MetricSerializer.SerializeUInt16(buffer, ref bufferIndexForDimensionsCount, dimensionsWritten); - - var payloadTypeLength = (ushort)(bufferIndex - payloadTypeStartIndex - 2); - MetricSerializer.SerializeUInt16(buffer, ref payloadTypeStartIndex, payloadTypeLength); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void SerializeExemplars(Exemplar[] exemplars, byte[] buffer, ref int bufferIndex) { @@ -885,6 +858,102 @@ private static void SerializeHistogramBucketWithTLV(in HistogramBucket bucket, b MetricSerializer.SerializeUInt32(buffer, ref bufferIndex, Convert.ToUInt32(bucket.BucketCount)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SerializeDimensionsAndGetCustomAccountNamespace(in ReadOnlyTagCollection tags, byte[] buffer, ref int bufferIndex, out string monitoringAccount, out string metricNamespace) + { + monitoringAccount = this.defaultMonitoringAccount; + metricNamespace = this.defaultMetricNamespace; + + MetricSerializer.SerializeByte(buffer, ref bufferIndex, (byte)PayloadType.Dimensions); + + // Get a placeholder to add the payloadType length + var payloadTypeStartIndex = bufferIndex; + bufferIndex += 2; + + // Get a placeholder to add dimensions count later + var bufferIndexForDimensionsCount = bufferIndex; + bufferIndex += 2; + + ushort dimensionsWritten = 0; + + // Serialize PrepopulatedDimensions keys + for (ushort i = 0; i < this.prepopulatedDimensionsCount; i++) + { + MetricSerializer.SerializeEncodedString(buffer, ref bufferIndex, this.serializedPrepopulatedDimensionsKeys[i]); + } + + if (this.prepopulatedDimensionsCount > 0) + { + dimensionsWritten += this.prepopulatedDimensionsCount; + } + + int reservedTags = 0; + + // Serialize MetricPoint Dimension keys + foreach (var tag in tags) + { + if (tag.Key.Length > MaxDimensionNameSize) + { + // TODO: Data Validation + } + + if (tag.Key.Equals(DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) || + tag.Key.Equals(DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase)) + { + reservedTags++; + continue; + } + + MetricSerializer.SerializeString(buffer, ref bufferIndex, tag.Key); + } + + dimensionsWritten += (ushort)(tags.Count - reservedTags); + + // Serialize PrepopulatedDimensions values + for (ushort i = 0; i < this.prepopulatedDimensionsCount; i++) + { + MetricSerializer.SerializeEncodedString(buffer, ref bufferIndex, this.serializedPrepopulatedDimensionsValues[i]); + } + + // Serialize MetricPoint Dimension values + foreach (var tag in tags) + { + if (tag.Key.Equals(DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsAccount) + { + if (!string.IsNullOrWhiteSpace(metricsAccount)) + { + monitoringAccount = metricsAccount; + } + + continue; + } + + if (tag.Key.Equals(DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsNamespace) + { + if (!string.IsNullOrWhiteSpace(metricsNamespace)) + { + metricNamespace = metricsNamespace; + } + + continue; + } + + var dimensionValue = Convert.ToString(tag.Value, CultureInfo.InvariantCulture); + if (dimensionValue.Length > MaxDimensionValueSize) + { + // TODO: Data Validation + } + + MetricSerializer.SerializeString(buffer, ref bufferIndex, dimensionValue); + } + + // Backfill the number of dimensions written + MetricSerializer.SerializeUInt16(buffer, ref bufferIndexForDimensionsCount, dimensionsWritten); + + var payloadTypeLength = (ushort)(bufferIndex - payloadTypeStartIndex - 2); + MetricSerializer.SerializeUInt16(buffer, ref payloadTypeStartIndex, payloadTypeLength); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SerializeHistogramBucket(in HistogramBucket bucket, ref int bufferIndex, double lastExplicitBound) { @@ -934,8 +1003,8 @@ private unsafe int InitializeBufferForNonHistogramMetrics() // Leave enough space for the header and fixed payload var bufferIndex = sizeof(BinaryHeader) + sizeof(MetricPayload); - MetricSerializer.SerializeString(this.bufferForNonHistogramMetrics, ref bufferIndex, this.monitoringAccount); - MetricSerializer.SerializeString(this.bufferForNonHistogramMetrics, ref bufferIndex, this.metricNamespace); + MetricSerializer.SerializeString(this.bufferForNonHistogramMetrics, ref bufferIndex, this.defaultMonitoringAccount); + MetricSerializer.SerializeString(this.bufferForNonHistogramMetrics, ref bufferIndex, this.defaultMetricNamespace); return bufferIndex; } @@ -950,8 +1019,8 @@ private unsafe int InitializeBufferForHistogramMetrics() // Leave enough space for the header and fixed payload var bufferIndex = sizeof(BinaryHeader) + sizeof(ExternalPayload); - MetricSerializer.SerializeString(this.bufferForHistogramMetrics, ref bufferIndex, this.monitoringAccount); - MetricSerializer.SerializeString(this.bufferForHistogramMetrics, ref bufferIndex, this.metricNamespace); + MetricSerializer.SerializeString(this.bufferForHistogramMetrics, ref bufferIndex, this.defaultMonitoringAccount); + MetricSerializer.SerializeString(this.bufferForHistogramMetrics, ref bufferIndex, this.defaultMetricNamespace); return bufferIndex; } diff --git a/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs b/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs index 6488e894f8..2132e5ef95 100644 --- a/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Geneva/Metrics/GenevaMetricExporterOptions.cs @@ -68,6 +68,12 @@ public IReadOnlyDictionary PrepopulatedMetricDimensions foreach (var entry in value) { + if (entry.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMonitoringAccount, StringComparison.OrdinalIgnoreCase) || + entry.Key.Equals(GenevaMetricExporter.DimensionKeyForCustomMetricsNamespace, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"The dimension: {entry.Key} is reserved and cannot be used as a prepopulated dimension."); + } + if (entry.Key.Length > GenevaMetricExporter.MaxDimensionNameSize) { throw new ArgumentException($"The dimension: {entry.Key} exceeds the maximum allowed limit of {GenevaMetricExporter.MaxDimensionNameSize} characters for a dimension name."); diff --git a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaMetricExporterTests.cs b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaMetricExporterTests.cs index 56bb3892eb..1bc4d76708 100644 --- a/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaMetricExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Geneva.Tests/GenevaMetricExporterTests.cs @@ -77,8 +77,8 @@ public void ParseConnectionStringCorrectly() } using var exporter = new GenevaMetricExporter(exporterOptions); - var monitoringAccount = typeof(GenevaMetricExporter).GetField("monitoringAccount", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as string; - var metricNamespace = typeof(GenevaMetricExporter).GetField("metricNamespace", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as string; + var monitoringAccount = typeof(GenevaMetricExporter).GetField("defaultMonitoringAccount", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as string; + var metricNamespace = typeof(GenevaMetricExporter).GetField("defaultMetricNamespace", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as string; Assert.Equal("OTelMonitoringAccount", monitoringAccount); Assert.Equal("OTelMetricNamespace", metricNamespace); } @@ -95,6 +95,33 @@ public void ParseConnectionStringCorrectly() } } + [Fact] + public void CannotUseReservedDimensionsInPrepopulatedFields() + { + var exporterOptions = new GenevaMetricExporterOptions(); + var prepopulatedMetricDimensions = new Dictionary + { + ["_microsoft_metrics_account"] = "MetricsAccount", + }; + + Assert.Throws(() => { exporterOptions.PrepopulatedMetricDimensions = prepopulatedMetricDimensions; }); + + prepopulatedMetricDimensions = new Dictionary + { + ["_microsoft_metrics_namespace"] = "MetricsNamespace", + }; + + Assert.Throws(() => { exporterOptions.PrepopulatedMetricDimensions = prepopulatedMetricDimensions; }); + + prepopulatedMetricDimensions = new Dictionary + { + ["_microsoft_metrics_account"] = "MetricsAccount", + ["_microsoft_metrics_namespace"] = "MetricsNamespace", + }; + + Assert.Throws(() => { exporterOptions.PrepopulatedMetricDimensions = prepopulatedMetricDimensions; }); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -578,7 +605,9 @@ public void SuccessfulExportOnLinux() metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out _, + out _); // Wait a little more than the ExportInterval for the exporter to export the data. Task.Delay(5500).Wait(); @@ -1130,6 +1159,124 @@ public void SuccessfulSerializationWithTLVWithViews() } } + [Fact] + public void SuccessfulSerializationWithCustomAccountAndNamespace() + { + using var meter = new Meter("SuccessfulSerializationWithCustomAccountAndNamespace", "0.0.1"); + var longCounter = meter.CreateCounter("longCounter"); + var doubleCounter = meter.CreateCounter("doubleCounter"); + var histogram = meter.CreateHistogram("histogram"); + var exportedItems = new List(); + using var inMemoryReader = new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + TemporalityPreference = MetricReaderTemporalityPreference.Delta, + }; + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("SuccessfulSerializationWithCustomAccountAndNamespace") + .AddReader(inMemoryReader) + .Build(); + + long longValue = 123; + double doubleValue = 123.45; + + longCounter.Add( + longValue, new("tag1", "value1"), new("tag2", "value2"), new("_microsoft_metrics_account", "AccountForLongCounter")); + + doubleCounter.Add( + doubleValue, new("tag1", "value1"), new("tag2", "value2"), new("_microsoft_metrics_namespace", "NamespaceForDoubleCounter")); + + // Record the following values from Histogram: + // (-inf - 0] : 1 + // (0 - 5] : 0 + // (5 - 10] : 0 + // (10 - 25] : 0 + // (25 - 50] : 0 + // (50 - 75] : 0 + // (75 - 100] : 0 + // (100 - 250] : 2 + // (250 - 500] : 0 + // (500 - 1000] : 1 + // (1000 - +inf) : 1 + // + // The corresponding value-count pairs to be sent for the given distribution: + // 0: 1 + // 250: 2 + // 1000: 1 + // 1001: 1 (We use one greater than the last bound provided (1000 + 1) as the value for the overflow bucket) + + histogram.Record(0, new("tag1", "value1"), new("tag2", "value2"), new("_microsoft_metrics_account", "AccountForHistogram"), new("_microsoft_metrics_namespace", "NamespaceForHistogram")); + histogram.Record(150, new("tag1", "value1"), new("tag2", "value2"), new("_microsoft_metrics_account", "AccountForHistogram"), new("_microsoft_metrics_namespace", "NamespaceForHistogram")); + histogram.Record(150, new("tag1", "value1"), new("tag2", "value2"), new("_microsoft_metrics_account", "AccountForHistogram"), new("_microsoft_metrics_namespace", "NamespaceForHistogram")); + histogram.Record(750, new("tag1", "value1"), new("tag2", "value2"), new("_microsoft_metrics_account", "AccountForHistogram"), new("_microsoft_metrics_namespace", "NamespaceForHistogram")); + histogram.Record(2500, new("tag1", "value1"), new("tag2", "value2"), new("_microsoft_metrics_account", "AccountForHistogram"), new("_microsoft_metrics_namespace", "NamespaceForHistogram")); + + string path = string.Empty; + Socket server = null; + try + { + var exporterOptions = new GenevaMetricExporterOptions(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + exporterOptions.ConnectionString = "Account=OTelMonitoringAccount;Namespace=OTelMetricNamespace"; + } + else + { + path = GenerateTempFilePath(); + exporterOptions.ConnectionString = $"Endpoint=unix:{path};Account=OTelMonitoringAccount;Namespace=OTelMetricNamespace"; + var endpoint = new UnixDomainSocketEndPoint(path); + server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); + server.Bind(endpoint); + server.Listen(1); + } + + exporterOptions.PrepopulatedMetricDimensions = new Dictionary + { + ["cloud.role"] = "BusyWorker", + ["cloud.roleInstance"] = "CY1SCH030021417", + ["cloud.roleVer"] = "9.0.15289.2", + }; + + using var exporter = new GenevaMetricExporter(exporterOptions); + + inMemoryReader.Collect(); + + Assert.Equal(3, exportedItems.Count); + + // check serialization for longCounter + CheckSerializationWithTLVForSingleMetricPoint(exportedItems[0], exporter, exporterOptions); + var data = GetSerializedData(exportedItems[0], exporter); + var fields = data.Fields; + Assert.Contains(fields, field => field.Type == PayloadTypes.AccountName && (field.Value as WrappedString).Value == "AccountForLongCounter"); + Assert.Contains(fields, field => field.Type == PayloadTypes.NamespaceName && (field.Value as WrappedString).Value == "OTelMetricNamespace"); + + // check serialization for doubleCounter + CheckSerializationWithTLVForSingleMetricPoint(exportedItems[1], exporter, exporterOptions); + data = GetSerializedData(exportedItems[1], exporter); + fields = data.Fields; + Assert.Contains(fields, field => field.Type == PayloadTypes.AccountName && (field.Value as WrappedString).Value == "OTelMonitoringAccount"); + Assert.Contains(fields, field => field.Type == PayloadTypes.NamespaceName && (field.Value as WrappedString).Value == "NamespaceForDoubleCounter"); + + // check serialization for histogram + CheckSerializationWithTLVForSingleMetricPoint(exportedItems[2], exporter, exporterOptions); + data = GetSerializedData(exportedItems[2], exporter); + fields = data.Fields; + Assert.Contains(fields, field => field.Type == PayloadTypes.AccountName && (field.Value as WrappedString).Value == "AccountForHistogram"); + Assert.Contains(fields, field => field.Type == PayloadTypes.NamespaceName && (field.Value as WrappedString).Value == "NamespaceForHistogram"); + } + finally + { + server?.Dispose(); + try + { + File.Delete(path); + } + catch + { + } + } + } + private static string GenerateTempFilePath() { while (true) @@ -1371,7 +1518,9 @@ private static void CheckSerializationWithTLVForSingleMetricPoint(Metric metric, metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out _, + out _); var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; var stream = new KaitaiStream(buffer); @@ -1395,7 +1544,9 @@ private static void CheckSerializationWithTLVForSingleMetricPoint(Metric metric, metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out _, + out _); var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; var stream = new KaitaiStream(buffer); @@ -1421,7 +1572,9 @@ private static void CheckSerializationWithTLVForSingleMetricPoint(Metric metric, metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out _, + out _); var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; var stream = new KaitaiStream(buffer); @@ -1447,7 +1600,9 @@ private static void CheckSerializationWithTLVForSingleMetricPoint(Metric metric, metricPoint.EndTime.ToFileTime(), metricPoint.Tags, metricData, - exemplars); + exemplars, + out _, + out _); var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; var stream = new KaitaiStream(buffer); @@ -1480,7 +1635,9 @@ private static void CheckSerializationWithTLVForSingleMetricPoint(Metric metric, count, min, max, - exemplars); + exemplars, + out _, + out _); var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; var stream = new KaitaiStream(buffer); @@ -1541,9 +1698,31 @@ private static void CheckSerializationWithTLVForSingleMetricPoint(Metric metric, // Check metric name, account, and namespace var connectionStringBuilder = new ConnectionStringBuilder(exporterOptions.ConnectionString); + + string monitoringAccount = connectionStringBuilder.Account; + string metricNamespace = connectionStringBuilder.Namespace; + + foreach (var tag in metricPoint.Tags) + { + if (tag.Key.Equals("_microsoft_metrics_account", StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsAccount) + { + if (!string.IsNullOrWhiteSpace(metricsAccount)) + { + monitoringAccount = metricsAccount; + } + } + else if (tag.Key.Equals("_microsoft_metrics_namespace", StringComparison.OrdinalIgnoreCase) && tag.Value is string metricsNamespace) + { + if (!string.IsNullOrWhiteSpace(metricsNamespace)) + { + metricNamespace = metricsNamespace; + } + } + } + Assert.Contains(fields, field => field.Type == PayloadTypes.MetricName && (field.Value as WrappedString).Value == metric.Name); - Assert.Contains(fields, field => field.Type == PayloadTypes.AccountName && (field.Value as WrappedString).Value == connectionStringBuilder.Account); - Assert.Contains(fields, field => field.Type == PayloadTypes.NamespaceName && (field.Value as WrappedString).Value == connectionStringBuilder.Namespace); + Assert.Contains(fields, field => field.Type == PayloadTypes.AccountName && (field.Value as WrappedString).Value == monitoringAccount); + Assert.Contains(fields, field => field.Type == PayloadTypes.NamespaceName && (field.Value as WrappedString).Value == metricNamespace); // Check dimensions var dimensions = fields.FirstOrDefault(field => field.Type == PayloadTypes.Dimensions).Value as Dimensions; @@ -1568,14 +1747,22 @@ private static void CheckSerializationWithTLVForSingleMetricPoint(Metric metric, index++; } + int reservedTags = 0; foreach (var tag in metricPoint.Tags) { + if (tag.Key.Equals("_microsoft_metrics_account", StringComparison.OrdinalIgnoreCase) || + tag.Key.Equals("_microsoft_metrics_namespace", StringComparison.OrdinalIgnoreCase)) + { + reservedTags++; + continue; + } + Assert.Equal(tag.Key, dimensions.DimensionsNames[index].Value); Assert.Equal(tag.Value, dimensions.DimensionsValues[index].Value); index++; } - dimensionsCount += metricPoint.Tags.Count; + dimensionsCount += (ushort)(metricPoint.Tags.Count - reservedTags); Assert.Equal(dimensionsCount, dimensions.NumDimensions); } @@ -1629,4 +1816,125 @@ private static void AssertExemplarFilteredTagSerialization(Exemplar expectedExem } } } + + private static UserdataV2 GetSerializedData(Metric metric, GenevaMetricExporter exporter) + { + var metricType = metric.MetricType; + var metricPointsEnumerator = metric.GetMetricPoints().GetEnumerator(); + metricPointsEnumerator.MoveNext(); + var metricPoint = metricPointsEnumerator.Current; + var exemplars = metricPoint.GetExemplars(); + UserdataV2 result = null; + + if (metricType == MetricType.LongSum) + { + var metricDataValue = Convert.ToUInt64(metricPoint.GetSumLong()); + var metricData = new MetricData { UInt64Value = metricDataValue }; + _ = exporter.SerializeMetricWithTLV( + MetricEventType.ULongMetric, + metric.Name, + metricPoint.EndTime.ToFileTime(), + metricPoint.Tags, + metricData, + exemplars, + out _, + out _); + + var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; + var stream = new KaitaiStream(buffer); + var data = new MetricsContract(stream); + result = data.Body as UserdataV2; + } + else if (metricType == MetricType.LongGauge) + { + var metricDataValue = Convert.ToDouble(metricPoint.GetGaugeLastValueLong()); + var metricData = new MetricData { DoubleValue = metricDataValue }; + _ = exporter.SerializeMetricWithTLV( + MetricEventType.DoubleMetric, + metric.Name, + metricPoint.EndTime.ToFileTime(), + metricPoint.Tags, + metricData, + exemplars, + out _, + out _); + + var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; + var stream = new KaitaiStream(buffer); + var data = new MetricsContract(stream); + result = data.Body as UserdataV2; + } + else if (metricType == MetricType.DoubleSum || metricType == MetricType.DoubleGauge) + { + var metricDataValue = metricType == MetricType.DoubleSum ? + metricPoint.GetSumDouble() : + metricPoint.GetGaugeLastValueDouble(); + var metricData = new MetricData { DoubleValue = metricDataValue }; + _ = exporter.SerializeMetricWithTLV( + MetricEventType.DoubleMetric, + metric.Name, + metricPoint.EndTime.ToFileTime(), + metricPoint.Tags, + metricData, + exemplars, + out _, + out _); + + var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; + var stream = new KaitaiStream(buffer); + var data = new MetricsContract(stream); + result = data.Body as UserdataV2; + } + else if (metricType == MetricType.LongSumNonMonotonic || metricType == MetricType.DoubleSumNonMonotonic) + { + var metricDataValue = metricType == MetricType.LongSumNonMonotonic ? + Convert.ToDouble(metricPoint.GetSumLong()) : + Convert.ToDouble(metricPoint.GetSumDouble()); + var metricData = new MetricData { DoubleValue = metricDataValue }; + _ = exporter.SerializeMetricWithTLV( + MetricEventType.DoubleMetric, + metric.Name, + metricPoint.EndTime.ToFileTime(), + metricPoint.Tags, + metricData, + exemplars, + out _, + out _); + + var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; + var stream = new KaitaiStream(buffer); + var data = new MetricsContract(stream); + result = data.Body as UserdataV2; + } + else if (metricType == MetricType.Histogram) + { + var sum = Convert.ToUInt64(metricPoint.GetHistogramSum()); + var count = Convert.ToUInt32(metricPoint.GetHistogramCount()); + if (!metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + min = 0; + max = 0; + } + + _ = exporter.SerializeHistogramMetricWithTLV( + metric.Name, + metricPoint.EndTime.ToFileTime(), + metricPoint.Tags, + metricPoint.GetHistogramBuckets(), + sum, + count, + min, + max, + exemplars, + out _, + out _); + + var buffer = typeof(GenevaMetricExporter).GetField("buffer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(exporter) as byte[]; + var stream = new KaitaiStream(buffer); + var data = new MetricsContract(stream); + result = data.Body as UserdataV2; + } + + return result; + } }