diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs index 25b345ac96e..54564cb823f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExperimentalOptions.cs @@ -17,6 +17,8 @@ internal sealed class ExperimentalOptions public const string OtlpDiskRetryDirectoryPathEnvVar = "OTEL_DOTNET_EXPERIMENTAL_OTLP_DISK_RETRY_DIRECTORY_PATH"; + public const string EmitNoRecordedValueNeededDataPointsEnvVar = "OTEL_DOTNET_EXPERIMENTAL_OTLP_METRICS_EMIT_NO_RECORDED_VALUE"; + public ExperimentalOptions() : this(new ConfigurationBuilder().AddEnvironmentVariables().Build()) { @@ -29,6 +31,11 @@ public ExperimentalOptions(IConfiguration configuration) this.EmitLogEventAttributes = emitLogEventAttributes; } + if (configuration.TryGetBoolValue(OpenTelemetryProtocolExporterEventSource.Log, EmitNoRecordedValueNeededDataPointsEnvVar, out var emitNoRecordedValueNeededDataPoints)) + { + this.EmitNoRecordedValueNeededDataPoints = emitNoRecordedValueNeededDataPoints; + } + if (configuration.TryGetStringValue(OtlpRetryEnvVar, out var retryPolicy) && retryPolicy != null) { if (retryPolicy.Equals("in_memory", StringComparison.OrdinalIgnoreCase)) @@ -78,4 +85,10 @@ public ExperimentalOptions(IConfiguration configuration) /// Gets the path on disk where the telemetry will be stored for retries at a later point. /// public string? DiskRetryDirectoryPath { get; } + + /// + /// Gets a value indicating whether the NoRecordedValue measurement should be sent when metrics are removed, + /// e.g when disposing a Meter. + /// + public bool EmitNoRecordedValueNeededDataPoints { get; } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricSerializer.cs index 5064e9cd122..ab44f7cbfa5 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricSerializer.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufOtlpMetricSerializer.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; @@ -19,7 +20,7 @@ internal static class ProtobufOtlpMetricSerializer private delegate int WriteExemplarFunc(byte[] buffer, int writePosition, in Exemplar exemplar); - internal static int WriteMetricsData(ref byte[] buffer, int writePosition, Resources.Resource? resource, in Batch batch) + internal static int WriteMetricsData(ref byte[] buffer, int writePosition, Resources.Resource? resource, in Batch batch, bool emitNoRecordedValueNeededDataPoints) { metricListPool ??= []; scopeMetricsList ??= []; @@ -36,13 +37,13 @@ internal static int WriteMetricsData(ref byte[] buffer, int writePosition, Resou metrics.Add(metric); } - writePosition = TryWriteResourceMetrics(ref buffer, writePosition, resource, scopeMetricsList); + writePosition = TryWriteResourceMetrics(ref buffer, writePosition, resource, scopeMetricsList, emitNoRecordedValueNeededDataPoints); ReturnMetricListToPool(); return writePosition; } - internal static int TryWriteResourceMetrics(ref byte[] buffer, int writePosition, Resources.Resource? resource, Dictionary> scopeMetrics) + internal static int TryWriteResourceMetrics(ref byte[] buffer, int writePosition, Resources.Resource? resource, Dictionary> scopeMetrics, bool emitNoRecordedValueNeededDataPoints) { int entryWritePosition = writePosition; @@ -52,7 +53,7 @@ internal static int TryWriteResourceMetrics(ref byte[] buffer, int writePosition int mericsDataLengthPosition = writePosition; writePosition += ReserveSizeForLength; - writePosition = WriteResourceMetrics(buffer, writePosition, resource, scopeMetrics); + writePosition = WriteResourceMetrics(buffer, writePosition, resource, scopeMetrics, emitNoRecordedValueNeededDataPoints); ProtobufSerializer.WriteReservedLength(buffer, mericsDataLengthPosition, writePosition - (mericsDataLengthPosition + ReserveSizeForLength)); } @@ -64,7 +65,7 @@ internal static int TryWriteResourceMetrics(ref byte[] buffer, int writePosition throw; } - return TryWriteResourceMetrics(ref buffer, writePosition, resource, scopeMetrics); + return TryWriteResourceMetrics(ref buffer, writePosition, resource, scopeMetrics, emitNoRecordedValueNeededDataPoints); } return writePosition; @@ -84,34 +85,45 @@ private static void ReturnMetricListToPool() } } - private static int WriteResourceMetrics(byte[] buffer, int writePosition, Resources.Resource? resource, Dictionary> scopeMetrics) + private static int WriteResourceMetrics( + byte[] buffer, + int writePosition, + Resource? resource, + Dictionary> scopeMetrics, + bool emitNoRecordedValueNeededDataPoints) { writePosition = ProtobufOtlpResourceSerializer.WriteResource(buffer, writePosition, resource); - writePosition = WriteScopeMetrics(buffer, writePosition, scopeMetrics); + writePosition = WriteScopeMetrics(buffer, writePosition, scopeMetrics, emitNoRecordedValueNeededDataPoints); return writePosition; } - private static int WriteScopeMetrics(byte[] buffer, int writePosition, Dictionary> scopeMetrics) + private static int WriteScopeMetrics( + byte[] buffer, + int writePosition, + Dictionary> scopeMetrics, + bool emitNoRecordedValueNeededDataPoints) { - if (scopeMetrics != null) + foreach (KeyValuePair> entry in scopeMetrics) { - foreach (KeyValuePair> entry in scopeMetrics) - { - writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ResourceMetrics_Scope_Metrics, ProtobufWireType.LEN); - int resourceMetricsScopeMetricsLengthPosition = writePosition; - writePosition += ReserveSizeForLength; + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ResourceMetrics_Scope_Metrics, ProtobufWireType.LEN); + int resourceMetricsScopeMetricsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; - writePosition = WriteScopeMetric(buffer, writePosition, entry.Key, entry.Value); + writePosition = WriteScopeMetric(buffer, writePosition, entry.Key, entry.Value, emitNoRecordedValueNeededDataPoints); - ProtobufSerializer.WriteReservedLength(buffer, resourceMetricsScopeMetricsLengthPosition, writePosition - (resourceMetricsScopeMetricsLengthPosition + ReserveSizeForLength)); - } + ProtobufSerializer.WriteReservedLength(buffer, resourceMetricsScopeMetricsLengthPosition, writePosition - (resourceMetricsScopeMetricsLengthPosition + ReserveSizeForLength)); } return writePosition; } - private static int WriteScopeMetric(byte[] buffer, int writePosition, string meterName, List metrics) + private static int WriteScopeMetric( + byte[] buffer, + int writePosition, + string meterName, + List metrics, + bool emitNoRecordedValueNeededDataPoints) { writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ScopeMetrics_Scope, ProtobufWireType.LEN); int instrumentationScopeLengthPosition = writePosition; @@ -149,13 +161,17 @@ private static int WriteScopeMetric(byte[] buffer, int writePosition, string met for (int i = 0; i < metrics.Count; i++) { - writePosition = WriteMetric(buffer, writePosition, metrics[i]); + writePosition = WriteMetric(buffer, writePosition, metrics[i], emitNoRecordedValueNeededDataPoints); } return writePosition; } - private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) + private static int WriteMetric( + byte[] buffer, + int writePosition, + Metric metric, + bool emitNoRecordedValueNeededDataPoints) { writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ScopeMetrics_Metrics, ProtobufWireType.LEN); int metricLengthPosition = writePosition; @@ -177,6 +193,8 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) ? ProtobufOtlpMetricFieldNumberConstants.Aggregation_Temporality_Cumulative : ProtobufOtlpMetricFieldNumberConstants.Aggregation_Temporality_Delta; + bool isNoRecordedValueNeeded = emitNoRecordedValueNeededDataPoints && metric.NoRecordedValueNeeded; + switch (metric.MetricType) { case MetricType.LongSum: @@ -193,6 +211,11 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) { var sum = metricPoint.GetSumLong(); writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Data_Points, in metricPoint, sum); + + if (isNoRecordedValueNeeded) + { + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Data_Points, in metricPoint, sum, isNoValueRecorded: true); + } } ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); @@ -213,6 +236,11 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) { var sum = metricPoint.GetSumDouble(); writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Data_Points, in metricPoint, sum); + + if (isNoRecordedValueNeeded) + { + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Sum_Data_Points, in metricPoint, sum, isNoValueRecorded: true); + } } ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); @@ -229,6 +257,11 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) { var lastValue = metricPoint.GetGaugeLastValueLong(); writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Gauge_Data_Points, in metricPoint, lastValue); + + if (isNoRecordedValueNeeded) + { + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Gauge_Data_Points, in metricPoint, lastValue, isNoValueRecorded: true); + } } ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); @@ -245,6 +278,11 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) { var lastValue = metricPoint.GetGaugeLastValueDouble(); writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Gauge_Data_Points, in metricPoint, lastValue); + + if (isNoRecordedValueNeeded) + { + writePosition = WriteNumberDataPoint(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Gauge_Data_Points, in metricPoint, lastValue, isNoValueRecorded: true); + } } ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); @@ -261,47 +299,12 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Histogram_Data_Points, ProtobufWireType.LEN); - int dataPointLengthPosition = writePosition; - writePosition += ReserveSizeForLength; + writePosition = WriteHistogramDataPoint(buffer, writePosition, in metricPoint); - var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Start_Time_Unix_Nano, startTime); - - var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Time_Unix_Nano, endTime); - - foreach (var tag in metricPoint.Tags) - { - writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Attributes); - } - - var count = (ulong)metricPoint.GetHistogramCount(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Count, count); - - var sum = metricPoint.GetHistogramSum(); - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Sum, sum); - - if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) - { - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Min, min); - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Max, max); - } - - foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + if (isNoRecordedValueNeeded) { - var bucketCount = (ulong)histogramMeasurement.BucketCount; - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Bucket_Counts, bucketCount); - - if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) - { - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Explicit_Bounds, histogramMeasurement.ExplicitBound); - } + writePosition = WriteHistogramDataPoint(buffer, writePosition, in metricPoint, isNoValueRecorded: true); } - - writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Exemplars, in metricPoint); - - ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); } ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); @@ -318,54 +321,12 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogram_Data_Points, ProtobufWireType.LEN); - int dataPointLengthPosition = writePosition; - writePosition += ReserveSizeForLength; - - var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Start_Time_Unix_Nano, startTime); - - var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Time_Unix_Nano, endTime); - - foreach (var tag in metricPoint.Tags) - { - writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Attributes); - } - - var sum = metricPoint.GetHistogramSum(); - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Sum, sum); - - var count = (ulong)metricPoint.GetHistogramCount(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Count, count); - - if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) - { - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Min, min); - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Max, max); - } - - var exponentialHistogramData = metricPoint.GetExponentialHistogramData(); + writePosition = WriteExponentialHistogramDataPoint(buffer, writePosition, in metricPoint); - writePosition = ProtobufSerializer.WriteSInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Scale, exponentialHistogramData.Scale); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Zero_Count, (ulong)exponentialHistogramData.ZeroCount); - - writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Positive, ProtobufWireType.LEN); - int positiveBucketsLengthPosition = writePosition; - writePosition += ReserveSizeForLength; - - writePosition = ProtobufSerializer.WriteSInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Buckets_Offset, exponentialHistogramData.PositiveBuckets.Offset); - - foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) + if (isNoRecordedValueNeeded) { - writePosition = ProtobufSerializer.WriteInt64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Buckets_Bucket_Counts, (ulong)bucketCount); + writePosition = WriteExponentialHistogramDataPoint(buffer, writePosition, in metricPoint, isNoValueRecorded: true); } - - ProtobufSerializer.WriteReservedLength(buffer, positiveBucketsLengthPosition, writePosition - (positiveBucketsLengthPosition + ReserveSizeForLength)); - - writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Exemplars, in metricPoint); - - ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); } ProtobufSerializer.WriteReservedLength(buffer, metricTypeLengthPosition, writePosition - (metricTypeLengthPosition + ReserveSizeForLength)); @@ -377,18 +338,22 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric) return writePosition; } - private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fieldNumber, in MetricPoint metricPoint, long value) + private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fieldNumber, in MetricPoint metricPoint, long value, bool isNoValueRecorded = false) { writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN); int dataPointLengthPosition = writePosition; writePosition += ReserveSizeForLength; - // Casting to ulong is ok here as the bit representation for long versus ulong will be the same - // The difference would in the way the bit representation is interpreted on decoding side (signed versus unsigned) - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Value_As_Int, (ulong)value); + if (!isNoValueRecorded) + { + // Casting to ulong is ok here as the bit representation for long versus ulong will be the same + // The difference would in the way the bit representation is interpreted on decoding side (signed versus unsigned) + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Value_As_Int, (ulong)value); - var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Start_Time_Unix_Nano, startTime); + // No value recorded data point have no aggregation period. They are single point in time markers. + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Start_Time_Unix_Nano, startTime); + } var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Time_Unix_Nano, endTime); @@ -398,7 +363,7 @@ private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fi writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Attributes); } - if (metricPoint.TryGetExemplars(out var exemplars)) + if (!isNoValueRecorded && metricPoint.TryGetExemplars(out var exemplars)) { foreach (ref readonly var exemplar in exemplars) { @@ -411,21 +376,29 @@ private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fi } } + if (isNoValueRecorded) + { + writePosition = ProtobufSerializer.WriteVarInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Flags, ProtobufOtlpMetricFieldNumberConstants.Data_Point_Flags_No_Recorded_Value_Mask); + } + ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); return writePosition; } - private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fieldNumber, in MetricPoint metricPoint, double value) + private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fieldNumber, in MetricPoint metricPoint, double value, bool isNoValueRecorded = false) { writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN); int dataPointLengthPosition = writePosition; writePosition += ReserveSizeForLength; - // Using a func here to avoid boxing/unboxing. - writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Value_As_Double, value); + if (!isNoValueRecorded) + { + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Value_As_Double, value); - var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); - writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Start_Time_Unix_Nano, startTime); + // No value recorded data point have no aggregation period. They are single point in time markers. + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Start_Time_Unix_Nano, startTime); + } var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Time_Unix_Nano, endTime); @@ -435,7 +408,135 @@ private static int WriteNumberDataPoint(byte[] buffer, int writePosition, int fi writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Attributes); } - writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Exemplars, in metricPoint); + if (!isNoValueRecorded) + { + writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Exemplars, in metricPoint); + } + else + { + writePosition = ProtobufSerializer.WriteVarInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.NumberDataPoint_Flags, ProtobufOtlpMetricFieldNumberConstants.Data_Point_Flags_No_Recorded_Value_Mask); + } + + ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); + return writePosition; + } + + private static int WriteHistogramDataPoint(byte[] buffer, int writePosition, in MetricPoint metricPoint, bool isNoValueRecorded = false) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Histogram_Data_Points, ProtobufWireType.LEN); + int dataPointLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + if (!isNoValueRecorded) + { + // No value recorded data point have no aggregation period. They are single point in time markers. + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Start_Time_Unix_Nano, startTime); + } + + var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Time_Unix_Nano, endTime); + + foreach (var tag in metricPoint.Tags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Attributes); + } + + if (!isNoValueRecorded) + { + var count = (ulong)metricPoint.GetHistogramCount(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Count, count); + + var sum = metricPoint.GetHistogramSum(); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Sum, sum); + + if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Min, min); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Max, max); + } + + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + var bucketCount = (ulong)histogramMeasurement.BucketCount; + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Bucket_Counts, bucketCount); + + if (!double.IsPositiveInfinity(histogramMeasurement.ExplicitBound)) + { + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Explicit_Bounds, histogramMeasurement.ExplicitBound); + } + } + + writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Exemplars, in metricPoint); + } + else + { + writePosition = ProtobufSerializer.WriteVarInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.HistogramDataPoint_Flags, ProtobufOtlpMetricFieldNumberConstants.Data_Point_Flags_No_Recorded_Value_Mask); + } + + ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); + return writePosition; + } + + private static int WriteExponentialHistogramDataPoint(byte[] buffer, int writePosition, in MetricPoint metricPoint, bool isNoValueRecorded = false) + { + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogram_Data_Points, ProtobufWireType.LEN); + int dataPointLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + if (!isNoValueRecorded) + { + // No value recorded data point have no aggregation period. They are single point in time markers. + var startTime = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Start_Time_Unix_Nano, startTime); + } + + var endTime = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Time_Unix_Nano, endTime); + + foreach (var tag in metricPoint.Tags) + { + writePosition = WriteTag(buffer, writePosition, tag, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Attributes); + } + + if (!isNoValueRecorded) + { + var sum = metricPoint.GetHistogramSum(); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Sum, sum); + + var count = (ulong)metricPoint.GetHistogramCount(); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Count, count); + + if (metricPoint.TryGetHistogramMinMaxValues(out double min, out double max)) + { + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Min, min); + writePosition = ProtobufSerializer.WriteDoubleWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Max, max); + } + + var exponentialHistogramData = metricPoint.GetExponentialHistogramData(); + + writePosition = ProtobufSerializer.WriteSInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Scale, exponentialHistogramData.Scale); + writePosition = ProtobufSerializer.WriteFixed64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Zero_Count, (ulong)exponentialHistogramData.ZeroCount); + + writePosition = ProtobufSerializer.WriteTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Positive, ProtobufWireType.LEN); + int positiveBucketsLengthPosition = writePosition; + writePosition += ReserveSizeForLength; + + writePosition = ProtobufSerializer.WriteSInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Buckets_Offset, exponentialHistogramData.PositiveBuckets.Offset); + + foreach (var bucketCount in exponentialHistogramData.PositiveBuckets) + { + writePosition = ProtobufSerializer.WriteInt64WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Buckets_Bucket_Counts, (ulong)bucketCount); + } + + ProtobufSerializer.WriteReservedLength(buffer, positiveBucketsLengthPosition, writePosition - (positiveBucketsLengthPosition + ReserveSizeForLength)); + + writePosition = WriteDoubleExemplars(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Exemplars, in metricPoint); + } + else + { + writePosition = ProtobufSerializer.WriteVarInt32WithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.ExponentialHistogramDataPoint_Flags, ProtobufOtlpMetricFieldNumberConstants.Data_Point_Flags_No_Recorded_Value_Mask); + } ProtobufSerializer.WriteReservedLength(buffer, dataPointLengthPosition, writePosition - (dataPointLengthPosition + ReserveSizeForLength)); return writePosition; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufSerializer.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufSerializer.cs index a74ec097299..56dadbe3f63 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufSerializer.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/Serializer/ProtobufSerializer.cs @@ -166,6 +166,15 @@ internal static int WriteSInt32WithTag(byte[] buffer, int writePosition, int fie return writePosition; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int WriteVarInt32WithTag(byte[] buffer, int writePosition, int fieldNumber, uint value) + { + writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.VARINT); + writePosition = WriteVarInt32(buffer, writePosition, value); + + return writePosition; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int WriteVarInt32(byte[] buffer, int writePosition, uint value) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs index 88bafa3007c..10fff05d26e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs @@ -21,6 +21,7 @@ public class OtlpMetricExporter : BaseExporter private readonly OtlpExporterTransmissionHandler transmissionHandler; private readonly int startWritePosition; + private readonly bool emitNoRecordedValueNeededDataPoints; private Resource? resource; // Initial buffer size set to ~732KB. @@ -53,6 +54,7 @@ internal OtlpMetricExporter( this.startWritePosition = exporterOptions!.Protocol == OtlpExportProtocol.Grpc ? GrpcStartWritePosition : 0; this.transmissionHandler = transmissionHandler ?? exporterOptions!.GetExportTransmissionHandler(experimentalOptions!, OtlpSignalType.Metrics); + this.emitNoRecordedValueNeededDataPoints = experimentalOptions!.EmitNoRecordedValueNeededDataPoints; } internal Resource Resource => this.resource ??= this.ParentProvider.GetResource(); @@ -65,7 +67,7 @@ public override ExportResult Export(in Batch metrics) try { - int writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref this.buffer, this.startWritePosition, this.Resource, metrics); + int writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref this.buffer, this.startWritePosition, this.Resource, metrics, this.emitNoRecordedValueNeededDataPoints); if (this.startWritePosition == GrpcStartWritePosition) { diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index 489b074663d..b93c3d1e9ba 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -6,6 +6,25 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Fixed storage exhaustion when disposing and recreating Meters. + **Previous Behavior:** Disposing a meter set the associated metric to null + in an array of default size 1000 allocated at creation time. Disposing and + recreating meters could exhaust the storage available in that list, leading + to an inability to collect the data points from newly created meters. + + **New Behavior:** Disposing a meter now marks the associated metrics for + deletion and they are cleaned up after the next collection cycle. + + **Limitation:** This means that quickly recreating meters within the same + collection cycle will still exhaust the storage limit. + +* Added an experimental flag + `OTEL_DOTNET_EXPERIMENTAL_OTLP_METRICS_EMIT_NO_RECORDED_VALUE`. When set to + `true`, after disposing a meter, a DataPoint with the flag NoRecordedValue + will be sent on the next collection cycle for all associated metrics as per + the + [OTLP data model](https://opentelemetry.io/docs/specs/otel/metrics/data-model/#no-recorded-value). + ## 1.11.0-rc.1 Released 2024-Dec-11 diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs index ac73955dda6..c4e6fe30fdd 100644 --- a/src/OpenTelemetry/Metrics/Metric.cs +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -243,6 +243,8 @@ internal Metric( internal bool Active { get; set; } = true; + internal bool NoRecordedValueNeeded { get; set; } + /// /// Get the metric points for the metric stream. /// diff --git a/src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs index 8c0ad5951fc..6f955639a30 100644 --- a/src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs +++ b/src/OpenTelemetry/Metrics/MetricPoint/MetricPoint.cs @@ -316,7 +316,7 @@ public readonly HistogramBuckets GetHistogramBuckets() /// /// . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ExponentialHistogramData GetExponentialHistogramData() + public readonly ExponentialHistogramData GetExponentialHistogramData() { if (this.aggType != AggregationType.Base2ExponentialHistogram && this.aggType != AggregationType.Base2ExponentialHistogramWithMinMax) diff --git a/src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs index 6074dd072f6..1d46646a550 100644 --- a/src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs +++ b/src/OpenTelemetry/Metrics/Reader/MetricReaderExt.cs @@ -197,8 +197,6 @@ private void CreateOrUpdateMetricStreamRegistration(in MetricStreamIdentity metr { if (!this.metricStreamNames.Add(metricStreamIdentity.MetricStreamName)) { - // TODO: If a metric is deactivated and then reactivated we log the - // same warning as if it was a duplicate. OpenTelemetrySdkEventSource.Log.DuplicateMetricInstrument( metricStreamIdentity.InstrumentName, metricStreamIdentity.MeterName, @@ -231,7 +229,35 @@ private Batch GetMetricsBatch() if (!metric.Active) { - this.RemoveMetric(ref metric); + // Inactive metrics are sent one last time so the remaining data points and + // NoRecordedValue data points can be sent. The Active property might be + // set to false between collection cycles, so a separate property must be + // used to avoid duplicate staleness markers. + metric.NoRecordedValueNeeded = true; + + lock (this.instrumentCreationLock) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentRemoved(metric.Name, metric.MeterName); + + // Note: This is using TryUpdate and NOT TryRemove because there is a + // race condition. If a metric is deactivated and then reactivated in + // the same collection cycle + // instrumentIdentityToMetric[metric.InstrumentIdentity] may already + // point to the new activated metric and not the old deactivated one. + this.instrumentIdentityToMetric.TryUpdate(metric.InstrumentIdentity, null, metric); + + this.metricStreamNames.Remove(metric.InstrumentIdentity.MetricStreamName); + + // Defragment metrics list so storage can be reused on future metrics. + for (int j = i + 1; j < target; j++) + { + this.metrics[j - 1] = this.metrics[j]; + } + + this.metrics[target - 1] = null; + this.metricIndex--; + i--; + } } } } @@ -244,29 +270,4 @@ private Batch GetMetricsBatch() return default; } } - - private void RemoveMetric(ref Metric? metric) - { - Debug.Assert(metric != null, "metric was null"); - - // TODO: This logic removes the metric. If the same - // metric is published again we will create a new metric - // for it. If this happens often we will run out of - // storage. Instead, should we keep the metric around - // and set a new start time + reset its data if it comes - // back? - - OpenTelemetrySdkEventSource.Log.MetricInstrumentRemoved(metric!.Name, metric.MeterName); - - // Note: This is using TryUpdate and NOT TryRemove because there is a - // race condition. If a metric is deactivated and then reactivated in - // the same collection cycle - // instrumentIdentityToMetric[metric.InstrumentIdentity] may already - // point to the new activated metric and not the old deactivated one. - this.instrumentIdentityToMetric.TryUpdate(metric.InstrumentIdentity, null, metric); - - // Note: metric is a reference to the array storage so - // this clears the metric out of the array. - metric = null; - } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 78c2d033383..d418dff7d6b 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -194,7 +194,7 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = CreateMetricExportRequest(batch, resourceBuilder.Build()); + var request = CreateMetricExportRequest(batch, resourceBuilder.Build(), false); Assert.Single(request.ResourceMetrics); var resourceMetric = request.ResourceMetrics.First(); @@ -227,7 +227,9 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) [InlineData("test_gauge", null, null, 123L, null, true)] [InlineData("test_gauge", null, null, null, 123.45, true)] [InlineData("test_gauge", "description", "unit", 123L, null)] - public void TestGaugeToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, bool enableExemplars = false) + [InlineData("test_gauge", "description", "unit", 123L, null, false, true)] + [InlineData("test_gauge", "description", "unit", 123L, null, false, true, true)] + public void TestGaugeToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, bool enableExemplars = false, bool disposeMeterEarly = false, bool experimentalEmitNoRecordedValue = false) { var metrics = new List(); @@ -247,11 +249,19 @@ public void TestGaugeToOtlpMetric(string name, string? description, string? unit meter.CreateObservableGauge(name, () => doubleValue!.Value, unit, description); } + if (disposeMeterEarly) + { + // For observable instruments, disposing the meter before the first collection leaves the metric with no data at all. + provider.ForceFlush(); + metrics.Clear(); + meter.Dispose(); + } + provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build(), experimentalEmitNoRecordedValue); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -269,25 +279,62 @@ public void TestGaugeToOtlpMetric(string name, string? description, string? unit Assert.Null(actual.ExponentialHistogram); Assert.Null(actual.Summary); - Assert.Single(actual.Gauge.DataPoints); - var dataPoint = actual.Gauge.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); - - if (longValue.HasValue) + if (!disposeMeterEarly || !experimentalEmitNoRecordedValue) { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); - Assert.Equal(longValue, dataPoint.AsInt); + Assert.Single(actual.Gauge.DataPoints); } else { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); - Assert.Equal(doubleValue, dataPoint.AsDouble); + Assert.Equal(2, actual.Gauge.DataPoints.Count); } - Assert.Empty(dataPoint.Attributes); + for (var index = 0; index < actual.Gauge.DataPoints.Count; index++) + { + var dataPoint = actual.Gauge.DataPoints[index]; + bool isNoRecordedValueDataPoint = index == 1; + + if (isNoRecordedValueDataPoint) + { + Assert.Equal(0UL, dataPoint.StartTimeUnixNano); + } + else + { + Assert.NotEqual(0UL, dataPoint.StartTimeUnixNano); + } + + Assert.True(dataPoint.TimeUnixNano > 0); + + if (!isNoRecordedValueDataPoint) + { + if (longValue.HasValue) + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); + } + else + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); + } + + Assert.Equal((uint)OtlpMetrics.DataPointFlags.DoNotUse, dataPoint.Flags); + } + else + { + Assert.Equal((uint)OtlpMetrics.DataPointFlags.NoRecordedValueMask, dataPoint.Flags); + } + + Assert.Empty(dataPoint.Attributes); - VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + if (!isNoRecordedValueDataPoint) + { + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } + else + { + Assert.Empty(dataPoint.Exemplars); + } + } } [Theory] @@ -301,7 +348,9 @@ public void TestGaugeToOtlpMetric(string name, string? description, string? unit [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] - public void TestCounterToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, false, true)] + [InlineData("test_counter", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, false, true, true)] + public void TestCounterToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false, bool disposeMeterEarly = false, bool experimentalEmitNoRecordedValue = false) { var metrics = new List(); @@ -327,10 +376,15 @@ public void TestCounterToOtlpMetric(string name, string? description, string? un counter.Add(doubleValue!.Value, attributes); } + if (disposeMeterEarly) + { + meter.Dispose(); + } + provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build(), experimentalEmitNoRecordedValue); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -355,32 +409,69 @@ public void TestCounterToOtlpMetric(string name, string? description, string? un : OtlpMetrics.AggregationTemporality.Delta; Assert.Equal(otlpAggregationTemporality, actual.Sum.AggregationTemporality); - Assert.Single(actual.Sum.DataPoints); - var dataPoint = actual.Sum.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); - - if (longValue.HasValue) + if (!disposeMeterEarly || !experimentalEmitNoRecordedValue) { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); - Assert.Equal(longValue, dataPoint.AsInt); + Assert.Single(actual.Sum.DataPoints); } else { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); - Assert.Equal(doubleValue, dataPoint.AsDouble); + Assert.Equal(2, actual.Sum.DataPoints.Count); } - if (attributes.Length > 0) + for (var index = 0; index < actual.Sum.DataPoints.Count; index++) { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); - } - else - { - Assert.Empty(dataPoint.Attributes); - } + var dataPoint = actual.Sum.DataPoints[index]; + bool isNoRecordedValueDataPoint = index == 1; + + if (isNoRecordedValueDataPoint) + { + Assert.Equal(0UL, dataPoint.StartTimeUnixNano); + } + else + { + Assert.NotEqual(0UL, dataPoint.StartTimeUnixNano); + } + + Assert.True(dataPoint.TimeUnixNano > 0); - VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + if (!isNoRecordedValueDataPoint) + { + if (longValue.HasValue) + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); + } + else + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); + } + + Assert.Equal((uint)OtlpMetrics.DataPointFlags.DoNotUse, dataPoint.Flags); + } + else + { + Assert.Equal((uint)OtlpMetrics.DataPointFlags.NoRecordedValueMask, dataPoint.Flags); + } + + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); + } + + if (!isNoRecordedValueDataPoint) + { + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } + else + { + Assert.Empty(dataPoint.Exemplars); + } + } } [Theory] @@ -396,7 +487,9 @@ public void TestCounterToOtlpMetric(string name, string? description, string? un [InlineData("test_counter", null, null, null, -123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_counter", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] - public void TestUpDownCounterToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, false, true)] + [InlineData("test_counter", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, false, true, true)] + public void TestUpDownCounterToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false, bool disposeMeterEarly = false, bool experimentalEmitNoRecordedValue = false) { var metrics = new List(); @@ -422,11 +515,16 @@ public void TestUpDownCounterToOtlpMetric(string name, string? description, stri counter.Add(doubleValue!.Value, attributes); } + if (disposeMeterEarly) + { + meter.Dispose(); + } + provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build(), experimentalEmitNoRecordedValue); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -451,32 +549,69 @@ public void TestUpDownCounterToOtlpMetric(string name, string? description, stri : OtlpMetrics.AggregationTemporality.Cumulative; Assert.Equal(otlpAggregationTemporality, actual.Sum.AggregationTemporality); - Assert.Single(actual.Sum.DataPoints); - var dataPoint = actual.Sum.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); - - if (longValue.HasValue) + if (!disposeMeterEarly || !experimentalEmitNoRecordedValue) { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); - Assert.Equal(longValue, dataPoint.AsInt); + Assert.Single(actual.Sum.DataPoints); } else { - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); - Assert.Equal(doubleValue, dataPoint.AsDouble); + Assert.Equal(2, actual.Sum.DataPoints.Count); } - if (attributes.Length > 0) - { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); - } - else + for (var index = 0; index < actual.Sum.DataPoints.Count; index++) { - Assert.Empty(dataPoint.Attributes); - } + var dataPoint = actual.Sum.DataPoints[index]; + bool isNoRecordedValueDataPoint = index == 1; + + if (isNoRecordedValueDataPoint) + { + Assert.Equal(0UL, dataPoint.StartTimeUnixNano); + } + else + { + Assert.NotEqual(0UL, dataPoint.StartTimeUnixNano); + } + + Assert.True(dataPoint.TimeUnixNano > 0); + + if (!isNoRecordedValueDataPoint) + { + if (longValue.HasValue) + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); + } + else + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); + } + + Assert.Equal((uint)OtlpMetrics.DataPointFlags.DoNotUse, dataPoint.Flags); + } + else + { + Assert.Equal((uint)OtlpMetrics.DataPointFlags.NoRecordedValueMask, dataPoint.Flags); + } - VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); + } + + if (!isNoRecordedValueDataPoint) + { + VerifyExemplars(longValue, doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } + else + { + Assert.Empty(dataPoint.Exemplars); + } + } } [Theory] @@ -492,7 +627,9 @@ public void TestUpDownCounterToOtlpMetric(string name, string? description, stri [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] - public void TestExponentialHistogramToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, false, true)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, false, true, true)] + public void TestExponentialHistogramToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false, bool disposeMeterEarly = false, bool experimentalEmitNoRecordedValue = false) { var metrics = new List(); @@ -524,10 +661,15 @@ public void TestExponentialHistogramToOtlpMetric(string name, string? descriptio histogram.Record(0, attributes); } + if (disposeMeterEarly) + { + meter.Dispose(); + } + provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build(), experimentalEmitNoRecordedValue); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -550,70 +692,108 @@ public void TestExponentialHistogramToOtlpMetric(string name, string? descriptio : OtlpMetrics.AggregationTemporality.Delta; Assert.Equal(otlpAggregationTemporality, actual.ExponentialHistogram.AggregationTemporality); - Assert.Single(actual.ExponentialHistogram.DataPoints); - var dataPoint = actual.ExponentialHistogram.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); - - Assert.Equal(20, dataPoint.Scale); - Assert.Equal(1UL, dataPoint.ZeroCount); - if (longValue > 0 || doubleValue > 0) + if (!disposeMeterEarly || !experimentalEmitNoRecordedValue) { - Assert.Equal(2UL, dataPoint.Count); + Assert.Single(actual.ExponentialHistogram.DataPoints); } else { - Assert.Equal(1UL, dataPoint.Count); + Assert.Equal(2, actual.ExponentialHistogram.DataPoints.Count); } - if (longValue.HasValue) + for (var index = 0; index < actual.ExponentialHistogram.DataPoints.Count; index++) { - if (longValue > 0) + var dataPoint = actual.ExponentialHistogram.DataPoints[index]; + bool isNoRecordedValueDataPoint = index == 1; + + if (isNoRecordedValueDataPoint) { - Assert.Equal((double)longValue, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset > 0); - Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); + Assert.Equal(0UL, dataPoint.StartTimeUnixNano); } else { - Assert.Equal(0, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.Equal(0, dataPoint.Positive.Offset); - Assert.Empty(dataPoint.Positive.BucketCounts); + Assert.NotEqual(0UL, dataPoint.StartTimeUnixNano); } - } - else - { - if (doubleValue > 0) + + Assert.True(dataPoint.TimeUnixNano > 0); + + if (!isNoRecordedValueDataPoint) { - Assert.Equal(doubleValue, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.True(dataPoint.Positive.Offset > 0); - Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); + Assert.Equal(20, dataPoint.Scale); + Assert.Equal(1UL, dataPoint.ZeroCount); + if (longValue > 0 || doubleValue > 0) + { + Assert.Equal(2UL, dataPoint.Count); + } + else + { + Assert.Equal(1UL, dataPoint.Count); + } + + if (longValue.HasValue) + { + if (longValue > 0) + { + Assert.Equal((double)longValue, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.True(dataPoint.Positive.Offset > 0); + Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); + } + else + { + Assert.Equal(0, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.Equal(0, dataPoint.Positive.Offset); + Assert.Empty(dataPoint.Positive.BucketCounts); + } + } + else + { + if (doubleValue > 0) + { + Assert.Equal(doubleValue, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.True(dataPoint.Positive.Offset > 0); + Assert.Equal(1UL, dataPoint.Positive.BucketCounts[0]); + } + else + { + Assert.Equal(0, dataPoint.Sum); + Assert.Null(dataPoint.Negative); + Assert.Equal(0, dataPoint.Positive.Offset); + Assert.Empty(dataPoint.Positive.BucketCounts); + } + } + + Assert.Equal((uint)OtlpMetrics.DataPointFlags.DoNotUse, dataPoint.Flags); } else { - Assert.Equal(0, dataPoint.Sum); - Assert.Null(dataPoint.Negative); - Assert.Equal(0, dataPoint.Positive.Offset); - Assert.Empty(dataPoint.Positive.BucketCounts); + Assert.Equal(0UL, dataPoint.Count); + Assert.Equal((uint)OtlpMetrics.DataPointFlags.NoRecordedValueMask, dataPoint.Flags); } - } - if (attributes.Length > 0) - { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); - } - else - { - Assert.Empty(dataPoint.Attributes); - } + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); + } - VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); - if (enableExemplars) - { - VerifyExemplars(null, 0, enableExemplars, d => d.Exemplars.Skip(1).FirstOrDefault(), dataPoint); + if (!isNoRecordedValueDataPoint) + { + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + if (enableExemplars) + { + VerifyExemplars(null, 0, enableExemplars, d => d.Exemplars.Skip(1).FirstOrDefault(), dataPoint); + } + } + else + { + Assert.Empty(dataPoint.Exemplars); + } } } @@ -630,7 +810,9 @@ public void TestExponentialHistogramToOtlpMetric(string name, string? descriptio [InlineData("test_histogram", null, null, null, 123.45, MetricReaderTemporalityPreference.Delta, false, true)] [InlineData("test_histogram", "description", "unit", 123L, null, MetricReaderTemporalityPreference.Cumulative)] [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, true)] - public void TestHistogramToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false) + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, false, true)] + [InlineData("test_histogram", null, null, 123L, null, MetricReaderTemporalityPreference.Delta, false, false, true, true)] + public void TestHistogramToOtlpMetric(string name, string? description, string? unit, long? longValue, double? doubleValue, MetricReaderTemporalityPreference aggregationTemporality, bool enableKeyValues = false, bool enableExemplars = false, bool disposeMeterEarly = false, bool experimentalEmitNoRecordedValue = false) { var metrics = new List(); @@ -656,10 +838,15 @@ public void TestHistogramToOtlpMetric(string name, string? description, string? histogram.Record(doubleValue!.Value, attributes); } + if (disposeMeterEarly) + { + meter.Dispose(); + } + provider.ForceFlush(); var batch = new Batch(metrics.ToArray(), metrics.Count); - var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build(), experimentalEmitNoRecordedValue); var resourceMetric = request.ResourceMetrics.Single(); var scopeMetrics = resourceMetric.ScopeMetrics.Single(); @@ -682,46 +869,84 @@ public void TestHistogramToOtlpMetric(string name, string? description, string? : OtlpMetrics.AggregationTemporality.Delta; Assert.Equal(otlpAggregationTemporality, actual.Histogram.AggregationTemporality); - Assert.Single(actual.Histogram.DataPoints); - var dataPoint = actual.Histogram.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); - - Assert.Equal(1UL, dataPoint.Count); - - // Known issue: Negative measurements affect the Sum. Per the spec, they should not. - if (longValue.HasValue) + if (!disposeMeterEarly || !experimentalEmitNoRecordedValue) { - Assert.Equal((double)longValue, dataPoint.Sum); + Assert.Single(actual.Histogram.DataPoints); } else { - Assert.Equal(doubleValue, dataPoint.Sum); + Assert.Equal(2, actual.Histogram.DataPoints.Count); } - int bucketIndex; - for (bucketIndex = 0; bucketIndex < dataPoint.ExplicitBounds.Count; ++bucketIndex) + for (var index = 0; index < actual.Histogram.DataPoints.Count; index++) { - if (dataPoint.Sum <= dataPoint.ExplicitBounds[bucketIndex]) + var dataPoint = actual.Histogram.DataPoints[index]; + bool isNoRecordedValueDataPoint = index == 1; + + if (isNoRecordedValueDataPoint) { - break; + Assert.Equal(0UL, dataPoint.StartTimeUnixNano); + } + else + { + Assert.NotEqual(0UL, dataPoint.StartTimeUnixNano); } - Assert.Equal(0UL, dataPoint.BucketCounts[bucketIndex]); - } + Assert.True(dataPoint.TimeUnixNano > 0); - Assert.Equal(1UL, dataPoint.BucketCounts[bucketIndex]); + if (!isNoRecordedValueDataPoint) + { + Assert.Equal(1UL, dataPoint.Count); - if (attributes.Length > 0) - { - OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); - } - else - { - Assert.Empty(dataPoint.Attributes); - } + // Known issue: Negative measurements affect the Sum. Per the spec, they should not. + if (longValue.HasValue) + { + Assert.Equal((double)longValue, dataPoint.Sum); + } + else + { + Assert.Equal(doubleValue, dataPoint.Sum); + } + + int bucketIndex; + for (bucketIndex = 0; bucketIndex < dataPoint.ExplicitBounds.Count; ++bucketIndex) + { + if (dataPoint.Sum <= dataPoint.ExplicitBounds[bucketIndex]) + { + break; + } + + Assert.Equal(0UL, dataPoint.BucketCounts[bucketIndex]); + } + + Assert.Equal(1UL, dataPoint.BucketCounts[bucketIndex]); - VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + Assert.Equal((uint)OtlpMetrics.DataPointFlags.DoNotUse, dataPoint.Flags); + } + else + { + Assert.Equal(0UL, dataPoint.Count); + Assert.Equal((uint)OtlpMetrics.DataPointFlags.NoRecordedValueMask, dataPoint.Flags); + } + + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); + } + + if (!isNoRecordedValueDataPoint) + { + VerifyExemplars(null, longValue ?? doubleValue, enableExemplars, d => d.Exemplars.FirstOrDefault(), dataPoint); + } + else + { + Assert.Empty(dataPoint.Exemplars); + } + } } [Theory] @@ -829,7 +1054,7 @@ public void ToOtlpExemplarTests(bool enableTagFiltering, bool enableTracing) meterProvider.ForceFlush(); var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); - var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build()); + var request = CreateMetricExportRequest(batch, ResourceBuilder.CreateEmpty().Build(), false); Assert.Single(request.ResourceMetrics); var resourceMetric = request.ResourceMetrics.First(); @@ -931,7 +1156,7 @@ public void MetricsSerialization_ExpandsBufferForMetricsAndSerializes() var batch = new Batch(metrics.ToArray(), metrics.Count); var buffer = new byte[50]; - var writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref buffer, 0, ResourceBuilder.CreateEmpty().Build(), in batch); + var writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref buffer, 0, ResourceBuilder.CreateEmpty().Build(), in batch, true); using var stream = new MemoryStream(buffer, 0, writePosition); var metricsData = OtlpMetrics.MetricsData.Parser.ParseFrom(stream); @@ -985,10 +1210,10 @@ private static void VerifyExemplars(long? longValue, double? doubleValue, boo } } - private static OtlpCollector.ExportMetricsServiceRequest CreateMetricExportRequest(in Batch batch, Resource resource) + private static OtlpCollector.ExportMetricsServiceRequest CreateMetricExportRequest(in Batch batch, Resource resource, bool experimentalEmitNoRecordedValue) { var buffer = new byte[4096]; - var writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref buffer, 0, resource, in batch); + var writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref buffer, 0, resource, in batch, experimentalEmitNoRecordedValue); using var stream = new MemoryStream(buffer, 0, writePosition); var metricsData = OtlpMetrics.MetricsData.Parser.ParseFrom(stream); diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs index d34dc060f1e..9b5dc35a786 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetryMetricsBuilderExtensionsTests.cs @@ -140,8 +140,7 @@ public void ReloadOfMetricsViaIConfigurationWithExportCleanupTest(bool useWithMe var duplicateMetricInstrumentEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 38); - // Note: We currently log a duplicate warning anytime a metric is reactivated. - Assert.Single(duplicateMetricInstrumentEvents); + Assert.Empty(duplicateMetricInstrumentEvents); var metricInstrumentDeactivatedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 52); @@ -211,7 +210,7 @@ public void ReloadOfMetricsViaIConfigurationWithoutExportCleanupTest(bool useWit var duplicateMetricInstrumentEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 38); - // Note: We currently log a duplicate warning anytime a metric is reactivated. + // Note: The old metric is only removed after a flush, so both duplicates lived for one collection cycle. Assert.Single(duplicateMetricInstrumentEvents); var metricInstrumentDeactivatedEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 52); diff --git a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs index 83d5d4644cf..f688f03548d 100644 --- a/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MeterProviderSdkTest.cs @@ -41,7 +41,7 @@ public void BuilderTypeDoesNotChangeTest() [InlineData(true, true)] [InlineData(false, false)] [InlineData(true, false)] - public void TransientMeterExhaustsMetricStorageTest(bool withView, bool forceFlushAfterEachTest) + public void TransientMeterBetweenCollectionsExhaustsMetricStorageTest(bool withView, bool forceFlushAfterEachTest) { using var inMemoryEventListener = new InMemoryEventListener(OpenTelemetrySdkEventSource.Log); @@ -74,7 +74,7 @@ public void TransientMeterExhaustsMetricStorageTest(bool withView, bool forceFlu if (forceFlushAfterEachTest) { - Assert.Empty(exportedItems); + Assert.Single(exportedItems); } else { @@ -85,7 +85,14 @@ public void TransientMeterExhaustsMetricStorageTest(bool withView, bool forceFlu var metricInstrumentIgnoredEvents = inMemoryEventListener.Events.Where((e) => e.EventId == 33 && (e.Payload?.Count ?? 0) >= 2 && e.Payload![1] as string == meterName); - Assert.Single(metricInstrumentIgnoredEvents); + if (forceFlushAfterEachTest) + { + Assert.Empty(metricInstrumentIgnoredEvents); + } + else + { + Assert.Single(metricInstrumentIgnoredEvents); + } void RunTest() {