Skip to content

[Geneva] Optional metric dimensions are silently dropped by the OTLP protobuf encoding but kept by TLV #5020

Description

@vsvandelik

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
GenevaMetricExporterOtlpProtobufMetricExporterOtlpProtobufSerializer, 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    comp:exporter.genevaThings related to OpenTelemetry.Exporter.Geneva

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions