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()
{