|
13 | 13 | #include "common.h" |
14 | 14 |
|
15 | 15 | #include "opentelemetry/common/key_value_iterable_view.h" |
| 16 | +#include "opentelemetry/common/timestamp.h" |
16 | 17 | #include "opentelemetry/context/context.h" |
17 | 18 | #include "opentelemetry/nostd/function_ref.h" |
18 | 19 | #include "opentelemetry/nostd/span.h" |
@@ -379,10 +380,121 @@ TEST(SyncMetricStorageTest, DeltaCounterStartTimestampTracksEmptyCycles) |
379 | 380 | // Check that the fast path correctly preserved the timestamp from the empty |
380 | 381 | // collection cycle (cycle 2) |
381 | 382 | EXPECT_EQ(metric_cycle3.start_ts, collection_ts2); |
382 | | - EXPECT_EQ(metric_cycle1.start_ts, sdk_start_ts); |
| 383 | + // Per OTel spec (#4062), the first delta interval's start_ts is the |
| 384 | + // instrument's creation time (captured at storage construction above), not |
| 385 | + // the sdk_start_ts that the MeterProvider would otherwise pass in. Storage |
| 386 | + // was constructed before sdk_start_ts was sampled, so the start_ts must be |
| 387 | + // less-or-equal to sdk_start_ts. SystemTimestamp does not define ordering |
| 388 | + // operators directly, so compare via time_since_epoch(). |
| 389 | + EXPECT_LE(metric_cycle1.start_ts.time_since_epoch(), sdk_start_ts.time_since_epoch()); |
383 | 390 | EXPECT_EQ(metric_cycle1.end_ts, collection_ts1); |
384 | 391 | EXPECT_EQ(metric_cycle3.end_ts, collection_ts3); |
385 | 392 | } |
| 393 | + |
| 394 | +TEST(SyncMetricStorageTest, DeltaCounterFirstIntervalUsesInstrumentCreationTime) |
| 395 | +{ |
| 396 | + // Spec-compliance test for issue #4062: the start_ts of the first delta |
| 397 | + // collection interval MUST be the per-instrument creation time, NOT the |
| 398 | + // MeterProvider/SDK start time. Simulate a MeterProvider that was created |
| 399 | + // long before the instrument by passing an artificial sdk_start_ts that is |
| 400 | + // strictly earlier than the storage's construction time. |
| 401 | + InstrumentDescriptor instr_desc = {"name", "desc", "1unit", InstrumentType::kCounter, |
| 402 | + InstrumentValueType::kLong}; |
| 403 | + std::shared_ptr<DefaultAttributesProcessor> default_attributes_processor{ |
| 404 | + new DefaultAttributesProcessor{}}; |
| 405 | + |
| 406 | + auto before_creation = std::chrono::system_clock::now(); |
| 407 | + opentelemetry::sdk::metrics::SyncMetricStorage storage( |
| 408 | + instr_desc, AggregationType::kSum, default_attributes_processor, |
| 409 | +#ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW |
| 410 | + ExemplarFilterType::kAlwaysOff, ExemplarReservoir::GetNoExemplarReservoir(), |
| 411 | +#endif |
| 412 | + nullptr); |
| 413 | + auto after_creation = std::chrono::system_clock::now(); |
| 414 | + |
| 415 | + // sdk_start_ts is intentionally well before storage construction. |
| 416 | + auto sdk_start_ts = before_creation - std::chrono::seconds(60); |
| 417 | + auto collection_ts = after_creation + std::chrono::seconds(1); |
| 418 | + |
| 419 | + std::map<std::string, std::string> attributes = {{"RequestType", "GET"}}; |
| 420 | + std::shared_ptr<CollectorHandle> collector( |
| 421 | + new MockCollectorHandle(AggregationTemporality::kDelta)); |
| 422 | + std::vector<std::shared_ptr<CollectorHandle>> collectors; |
| 423 | + collectors.push_back(collector); |
| 424 | + |
| 425 | + storage.RecordLong(10, KeyValueIterableView<std::map<std::string, std::string>>(attributes), |
| 426 | + opentelemetry::context::Context{}); |
| 427 | + |
| 428 | + MetricData metric_data; |
| 429 | + bool collected = false; |
| 430 | + storage.Collect(collector.get(), collectors, sdk_start_ts, collection_ts, |
| 431 | + [&](const MetricData &md) { |
| 432 | + metric_data = md; |
| 433 | + collected = true; |
| 434 | + return true; |
| 435 | + }); |
| 436 | + ASSERT_TRUE(collected); |
| 437 | + |
| 438 | + // start_ts must be the instrument creation time (between the two clock |
| 439 | + // samples taken around the storage constructor), and strictly greater than |
| 440 | + // the simulated MeterProvider start time. SystemTimestamp does not define |
| 441 | + // ordering operators directly; compare via time_since_epoch() durations. |
| 442 | + EXPECT_GE(metric_data.start_ts.time_since_epoch(), before_creation.time_since_epoch()); |
| 443 | + EXPECT_LE(metric_data.start_ts.time_since_epoch(), after_creation.time_since_epoch()); |
| 444 | + EXPECT_GT(metric_data.start_ts.time_since_epoch(), sdk_start_ts.time_since_epoch()); |
| 445 | + EXPECT_EQ(metric_data.end_ts, collection_ts); |
| 446 | +} |
| 447 | + |
| 448 | +TEST(SyncMetricStorageTest, DeltaCounterMultiCollectorFirstIntervalUsesInstrumentCreationTime) |
| 449 | +{ |
| 450 | + // Slow-path counterpart of DeltaCounterFirstIntervalUsesInstrumentCreationTime. |
| 451 | + // With more than one collector, buildMetrics takes the slow path; that path |
| 452 | + // must also use instrument_creation_ts_ as the start_ts for the first delta |
| 453 | + // collection interval (issue #4062 explicitly mentions both paths). |
| 454 | + InstrumentDescriptor instr_desc = {"name", "desc", "1unit", InstrumentType::kCounter, |
| 455 | + InstrumentValueType::kLong}; |
| 456 | + std::shared_ptr<DefaultAttributesProcessor> default_attributes_processor{ |
| 457 | + new DefaultAttributesProcessor{}}; |
| 458 | + |
| 459 | + auto before_creation = std::chrono::system_clock::now(); |
| 460 | + opentelemetry::sdk::metrics::SyncMetricStorage storage( |
| 461 | + instr_desc, AggregationType::kSum, default_attributes_processor, |
| 462 | +#ifdef ENABLE_METRICS_EXEMPLAR_PREVIEW |
| 463 | + ExemplarFilterType::kAlwaysOff, ExemplarReservoir::GetNoExemplarReservoir(), |
| 464 | +#endif |
| 465 | + nullptr); |
| 466 | + auto after_creation = std::chrono::system_clock::now(); |
| 467 | + |
| 468 | + auto sdk_start_ts = before_creation - std::chrono::seconds(60); |
| 469 | + auto collection_ts = after_creation + std::chrono::seconds(1); |
| 470 | + |
| 471 | + std::map<std::string, std::string> attributes = {{"RequestType", "GET"}}; |
| 472 | + // Two collectors force collectors.size() > 1, which bypasses the fast path. |
| 473 | + std::shared_ptr<CollectorHandle> collector_a( |
| 474 | + new MockCollectorHandle(AggregationTemporality::kDelta)); |
| 475 | + std::shared_ptr<CollectorHandle> collector_b( |
| 476 | + new MockCollectorHandle(AggregationTemporality::kDelta)); |
| 477 | + std::vector<std::shared_ptr<CollectorHandle>> collectors{collector_a, collector_b}; |
| 478 | + |
| 479 | + storage.RecordLong(10, KeyValueIterableView<std::map<std::string, std::string>>(attributes), |
| 480 | + opentelemetry::context::Context{}); |
| 481 | + |
| 482 | + MetricData metric_data; |
| 483 | + bool collected = false; |
| 484 | + storage.Collect(collector_a.get(), collectors, sdk_start_ts, collection_ts, |
| 485 | + [&](const MetricData &md) { |
| 486 | + metric_data = md; |
| 487 | + collected = true; |
| 488 | + return true; |
| 489 | + }); |
| 490 | + ASSERT_TRUE(collected); |
| 491 | + |
| 492 | + // Same assertion shape as the fast-path test. |
| 493 | + EXPECT_GE(metric_data.start_ts.time_since_epoch(), before_creation.time_since_epoch()); |
| 494 | + EXPECT_LE(metric_data.start_ts.time_since_epoch(), after_creation.time_since_epoch()); |
| 495 | + EXPECT_GT(metric_data.start_ts.time_since_epoch(), sdk_start_ts.time_since_epoch()); |
| 496 | + EXPECT_EQ(metric_data.end_ts, collection_ts); |
| 497 | +} |
386 | 498 | INSTANTIATE_TEST_SUITE_P(WritableMetricStorageTestDouble, |
387 | 499 | WritableMetricStorageTestFixture, |
388 | 500 | ::testing::Values(AggregationTemporality::kCumulative, |
|
0 commit comments