Skip to content

Delta temporality uses MeterProvider start time instead of per-instrument creation time for first collection interval #4062

@pixerit

Description

@pixerit

Bug:
The OpenTelemetry spec states:

For delta aggregations, the start timestamp MUST equal the previous collection interval's timestamp, or the creation time of the instrument if this is the first collection interval for the instrument.

The C++ SDK uses MeterContext::GetSDKStartTime() (the MeterProvider creation time) as start_ts for the first delta collection interval of all instruments, rather than the creation time of each individual instrument. This affects both the fast path and slow path in TemporalMetricStorage::buildMetrics.

Call chain:
MeterContext constructor:

sdk_start_ts_{std::chrono::system_clock::now()}   // captured once

Meter::Collect():
  metric_storage.second->Collect(collector, ctx->GetCollectors(), ctx->GetSDKStartTime(), ...)

SyncMetricStorage::Collect():
  temporal_metric_storage_.buildMetrics(collector, collectors, sdk_start_ts, ...)

TemporalMetricStorage::buildMetrics():
  // slow path, first interval:
  opentelemetry::common::SystemTimestamp last_collection_ts = sdk_start_ts;  // should be instrument creation time

All instruments receive the same sdk_start_ts regardless of when they were created.

Expected behavior:
If an instrument is created at T=30s and the MeterProvider was created at T=0, the first delta data point for that instrument should have start_time_unix_nano = T=30s.

Actual behavior:
The first delta data point has start_time_unix_nano = T=0 (MeterProvider creation time) regardless of when the instrument was created.

Impact:
Backends that use start_time_unix_nano for data point placement (e.g., New Relic maps it to the timestamp field) will show the first data point at the wrong time in charts. For instruments created well after SDK initialization, the first delta interval spans an artificially long window.

Reference implementation:
The Java SDK handles this correctly in DeltaSynchronousMetricStorage:

// Constructor stores per-instrument creation time
this.instrumentCreationEpochNanos = clock.now();

// Collection uses it as the fallback for the first interval
long startEpochNanos =
    registeredReader.getLastCollectEpochNanosOrDefault(instrumentCreationEpochNanos);

Source: DefaultSynchronousMetricStorage.java

Suggested fix:
Store a creation timestamp in TemporalMetricStorage (or SyncMetricStorage) initialized to std::chrono::system_clock::now() at construction time. Use this instead of the passed-in sdk_start_ts for the first collection interval's start_ts.

Related: #4038 (fast path always using sdk_start_ts, even on subsequent exports)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedGood for taking. Extra help will be provided by maintainersspec-complianceNot compliant to OpenTelemetry specs

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions