Component
OpenTelemetry.Exporter.Geneva (metrics)
Is your feature request related to a problem?
The TLV and OTLP protobuf metric encodings disagree on how a dimension whose value is
null is exported, and the difference is silent.
TlvMetricExporter converts every tag value through
Convert.ToString(value, CultureInfo.InvariantCulture), which turns null into an empty
string, so the dimension name is always emitted.
OtlpProtobufSerializer.SerializeTags skips the tag entirely:
internal static void SerializeTags(byte[] buffer, ref int cursor, ReadOnlyTagCollection tags, int fieldNumber)
{
foreach (var tag in tags)
{
if (tag.Value != null)
{
SerializeTag(buffer, ref cursor, tag.Key, tag.Value, fieldNumber);
}
}
}
so the dimension name disappears from the data point.
This matters when switching an existing metric from the TLV encoding to OTLP protobuf
encoding. A metric with optional dimensions — dimensions declared as string? that are
legitimately null on some code paths — will still flow, and metrics that aggregate only
over always-present dimensions keep working. But any Geneva Metrics (MDM) pre-aggregate
whose dimension set includes one of the optional dimensions silently stops matching the
emitted time series and returns no data.
There is no exception, no log and no failed export. The metric keeps being emitted, so
nothing looks broken; only the subset of pre-aggregates that reference an omitted
dimension goes empty. That makes it quite hard to attribute back to the encoding change.
Minimal illustration — a counter emitted with two dimensions where one is null:
var tags = new TagList
{
{ "presentKey", "presentValue" },
{ "nullKey", null },
};
counter.Add(1, tags);
- TLV encoding: both
presentKey and nullKey are present, nullKey having an empty value.
- OTLP protobuf encoding: only
presentKey is present.
Describe the solution you'd like
An opt-in option on GenevaMetricExporterOptions selecting the behavior, so that users
migrating from TLV can keep the emitted dimension set stable:
public enum NullDimensionExportMode
{
/// Dimension is omitted from the data point. Current behavior.
Drop,
/// Dimension is exported with an empty string value, matching the TLV encoding.
ExportAsEmptyString,
}
with Drop as the default and the zero value, so nothing changes unless the option is set
explicitly.
Scoped to metric point tags — the dimensions — at the number, histogram and exponential
histogram data point call sites. Prepopulated dimensions, instrumentation scope attributes,
resource attributes and exemplar filtered tags would be left alone.
(PrepopulatedMetricDimensions already rejects null values in its setter, so it cannot
be affected.)
Naming would follow the existing ExceptionStackExportMode / EventNameExportMode
convention on GenevaExporterOptions, which model the same kind of "how is this value
represented" choice, rather than a connection string switch, which in this exporter carries
transport and destination concerns.
Describe alternatives you've considered
- Changing the default so OTLP matches TLV. Rejected: it would alter the emitted
payload, and therefore the time series identity, for every existing user of the OTLP
protobuf encoding.
- Normalizing
null to string.Empty at every call site in the instrumented
application. This works but has to be repeated in every service and is easy to
regress, since the failure mode is silent.
- A connection string flag. Possible, but the connection string in this exporter is
used for transport and destination concerns plus flags that select a transport or
encoding, whereas this controls how a single value is represented inside the payload.
Additional context
I put together a candidate implementation with tests in #5018 (opened, then closed after
discussion on our side — happy for it to be used as a starting point, or to rework it if
maintainers prefer a different shape). It added the enum, threaded the option through
GenevaMetricExporter → OtlpProtobufMetricExporter → OtlpProtobufSerializer, and added
theory-based tests over both modes plus the option-omitted case, for counters and
histograms. The full OtlpProtobufMetricExporterTests suite passed on net8.0 with the
change.
Worth noting for anyone verifying the behavior: with ExportAsEmptyString the dimension is
emitted as a KeyValue whose AnyValue.string_value has length 0, which is what makes MDM
record it as __Empty, matching TLV byte-for-byte in effect.
Before raising a PR again I'd like maintainer input on whether an option is the shape you
want here, and whether GenevaMetricExporterOptions is the right home for it.
Disclosure: this issue was drafted with AI assistance. I have reviewed and verified the
technical content.
Component
OpenTelemetry.Exporter.Geneva(metrics)Is your feature request related to a problem?
The TLV and OTLP protobuf metric encodings disagree on how a dimension whose value is
nullis exported, and the difference is silent.TlvMetricExporterconverts every tag value throughConvert.ToString(value, CultureInfo.InvariantCulture), which turnsnullinto an emptystring, so the dimension name is always emitted.
OtlpProtobufSerializer.SerializeTagsskips the tag entirely:so the dimension name disappears from the data point.
This matters when switching an existing metric from the TLV encoding to OTLP protobuf
encoding. A metric with optional dimensions — dimensions declared as
string?that arelegitimately
nullon some code paths — will still flow, and metrics that aggregate onlyover always-present dimensions keep working. But any Geneva Metrics (MDM) pre-aggregate
whose dimension set includes one of the optional dimensions silently stops matching the
emitted time series and returns no data.
There is no exception, no log and no failed export. The metric keeps being emitted, so
nothing looks broken; only the subset of pre-aggregates that reference an omitted
dimension goes empty. That makes it quite hard to attribute back to the encoding change.
Minimal illustration — a counter emitted with two dimensions where one is
null:presentKeyandnullKeyare present,nullKeyhaving an empty value.presentKeyis present.Describe the solution you'd like
An opt-in option on
GenevaMetricExporterOptionsselecting the behavior, so that usersmigrating from TLV can keep the emitted dimension set stable:
with
Dropas the default and the zero value, so nothing changes unless the option is setexplicitly.
Scoped to metric point tags — the dimensions — at the number, histogram and exponential
histogram data point call sites. Prepopulated dimensions, instrumentation scope attributes,
resource attributes and exemplar filtered tags would be left alone.
(
PrepopulatedMetricDimensionsalready rejectsnullvalues in its setter, so it cannotbe affected.)
Naming would follow the existing
ExceptionStackExportMode/EventNameExportModeconvention on
GenevaExporterOptions, which model the same kind of "how is this valuerepresented" choice, rather than a connection string switch, which in this exporter carries
transport and destination concerns.
Describe alternatives you've considered
payload, and therefore the time series identity, for every existing user of the OTLP
protobuf encoding.
nulltostring.Emptyat every call site in the instrumentedapplication. This works but has to be repeated in every service and is easy to
regress, since the failure mode is silent.
used for transport and destination concerns plus flags that select a transport or
encoding, whereas this controls how a single value is represented inside the payload.
Additional context
I put together a candidate implementation with tests in #5018 (opened, then closed after
discussion on our side — happy for it to be used as a starting point, or to rework it if
maintainers prefer a different shape). It added the enum, threaded the option through
GenevaMetricExporter→OtlpProtobufMetricExporter→OtlpProtobufSerializer, and addedtheory-based tests over both modes plus the option-omitted case, for counters and
histograms. The full
OtlpProtobufMetricExporterTestssuite passed on net8.0 with thechange.
Worth noting for anyone verifying the behavior: with
ExportAsEmptyStringthe dimension isemitted as a
KeyValuewhoseAnyValue.string_valuehas length 0, which is what makes MDMrecord it as
__Empty, matching TLV byte-for-byte in effect.Before raising a PR again I'd like maintainer input on whether an option is the shape you
want here, and whether
GenevaMetricExporterOptionsis the right home for it.Disclosure: this issue was drafted with AI assistance. I have reviewed and verified the
technical content.