Introduce MetricsLoggerAndExporter

Introduce main API for new test performance metrics logging system.

Bug: b/246095034
Change-Id: I9b33740bfe69158c2d7f3f73e18442d1683aa8d4
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/274901
Reviewed-by: Tomas Gunnarsson <tommi@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Commit-Queue: Artem Titov <titovartem@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38116}
diff --git a/api/numerics/samples_stats_counter.h b/api/numerics/samples_stats_counter.h
index 16d5d2a..5e22041 100644
--- a/api/numerics/samples_stats_counter.h
+++ b/api/numerics/samples_stats_counter.h
@@ -11,6 +11,8 @@
 #ifndef API_NUMERICS_SAMPLES_STATS_COUNTER_H_
 #define API_NUMERICS_SAMPLES_STATS_COUNTER_H_
 
+#include <map>
+#include <string>
 #include <vector>
 
 #include "api/array_view.h"
@@ -27,6 +29,8 @@
   struct StatsSample {
     double value;
     Timestamp time;
+    // Sample's specific metadata.
+    std::map<std::string, std::string> metadata;
   };
 
   SamplesStatsCounter();
diff --git a/api/test/metrics/BUILD.gn b/api/test/metrics/BUILD.gn
index 099ee8a..544808f 100644
--- a/api/test/metrics/BUILD.gn
+++ b/api/test/metrics/BUILD.gn
@@ -12,6 +12,7 @@
   deps = [
     ":metric",
     ":metrics_exporter",
+    ":metrics_logger_and_exporter",
     ":stdout_metrics_exporter",
   ]
 }
@@ -20,7 +21,10 @@
   group("metrics_unittests") {
     testonly = true
 
-    deps = [ ":stdout_metrics_exporter_test" ]
+    deps = [
+      ":metrics_logger_and_exporter_test",
+      ":stdout_metrics_exporter_test",
+    ]
   }
 }
 
@@ -39,7 +43,7 @@
   sources = [ "metrics_exporter.h" ]
   deps = [
     ":metric",
-    "../../../api:array_view",
+    "../..:array_view",
   ]
 }
 
@@ -52,12 +56,34 @@
   deps = [
     ":metric",
     ":metrics_exporter",
-    "../../../api:array_view",
+    "../..:array_view",
     "../../../rtc_base:stringutils",
   ]
   absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
 }
 
+rtc_library("metrics_logger_and_exporter") {
+  visibility = [ "*" ]
+  sources = [
+    "metrics_logger_and_exporter.cc",
+    "metrics_logger_and_exporter.h",
+  ]
+  deps = [
+    ":metric",
+    ":metrics_exporter",
+    "../../../rtc_base:checks",
+    "../../../rtc_base:logging",
+    "../../../rtc_base/synchronization:mutex",
+    "../../../system_wrappers",
+    "../../numerics",
+  ]
+
+  absl_deps = [
+    "//third_party/abseil-cpp/absl/strings",
+    "//third_party/abseil-cpp/absl/types:optional",
+  ]
+}
+
 if (rtc_include_tests) {
   rtc_library("stdout_metrics_exporter_test") {
     testonly = true
@@ -65,8 +91,22 @@
     deps = [
       ":metric",
       ":stdout_metrics_exporter",
-      "../../../api/units:timestamp",
       "../../../test:test_support",
+      "../../units:timestamp",
     ]
   }
+
+  rtc_library("metrics_logger_and_exporter_test") {
+    testonly = true
+    sources = [ "metrics_logger_and_exporter_test.cc" ]
+    deps = [
+      ":metric",
+      ":metrics_exporter",
+      ":metrics_logger_and_exporter",
+      "../../../system_wrappers",
+      "../../../test:test_support",
+      "../../numerics",
+    ]
+    absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
+  }
 }
diff --git a/api/test/metrics/DEPS b/api/test/metrics/DEPS
new file mode 100644
index 0000000..d0def60
--- /dev/null
+++ b/api/test/metrics/DEPS
@@ -0,0 +1,6 @@
+specific_include_rules = {
+  "metrics_logger_and_exporter\.h": [
+    "+rtc_base/synchronization/mutex.h",
+    "+system_wrappers/include/clock.h",
+  ],
+}
diff --git a/api/test/metrics/metrics_logger_and_exporter.cc b/api/test/metrics/metrics_logger_and_exporter.cc
new file mode 100644
index 0000000..d76003b
--- /dev/null
+++ b/api/test/metrics/metrics_logger_and_exporter.cc
@@ -0,0 +1,120 @@
+/*
+ *  Copyright (c) 2022 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "api/test/metrics/metrics_logger_and_exporter.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "api/numerics/samples_stats_counter.h"
+#include "api/test/metrics/metric.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/synchronization/mutex.h"
+
+namespace webrtc {
+namespace test {
+
+MetricsLoggerAndExporter::~MetricsLoggerAndExporter() {
+  bool export_result = Export();
+  if (crash_on_export_failure_) {
+    RTC_CHECK(export_result);
+  } else {
+    RTC_LOG(LS_ERROR) << "One of exporters failed to export collected metrics";
+  }
+}
+
+void MetricsLoggerAndExporter::LogSingleValueMetric(
+    absl::string_view name,
+    absl::string_view test_case_name,
+    double value,
+    Unit unit,
+    ImprovementDirection improvement_direction,
+    std::map<std::string, std::string> metadata) {
+  MutexLock lock(&mutex_);
+  metrics_.push_back(Metric{
+      .name = std::string(name),
+      .unit = unit,
+      .improvement_direction = improvement_direction,
+      .test_case = std::string(test_case_name),
+      .metric_metadata = std::move(metadata),
+      .time_series =
+          Metric::TimeSeries{.samples = std::vector{Metric::TimeSeries::Sample{
+                                 .timestamp = Now(), .value = value}}},
+      .stats = Metric::Stats{
+          .mean = value, .stddev = absl::nullopt, .min = value, .max = value}});
+}
+
+void MetricsLoggerAndExporter::LogMetric(
+    absl::string_view name,
+    absl::string_view test_case_name,
+    const SamplesStatsCounter& values,
+    Unit unit,
+    ImprovementDirection improvement_direction,
+    std::map<std::string, std::string> metadata) {
+  MutexLock lock(&mutex_);
+  Metric::TimeSeries time_series;
+  for (const SamplesStatsCounter::StatsSample& sample :
+       values.GetTimedSamples()) {
+    time_series.samples.push_back(
+        Metric::TimeSeries::Sample{.timestamp = sample.time,
+                                   .value = sample.value,
+                                   .sample_metadata = sample.metadata});
+  }
+
+  metrics_.push_back(
+      Metric{.name = std::string(name),
+             .unit = unit,
+             .improvement_direction = improvement_direction,
+             .test_case = std::string(test_case_name),
+             .metric_metadata = std::move(metadata),
+             .time_series = std::move(time_series),
+             .stats = Metric::Stats{.mean = values.GetAverage(),
+                                    .stddev = values.GetStandardDeviation(),
+                                    .min = values.GetMin(),
+                                    .max = values.GetMax()}});
+}
+
+void MetricsLoggerAndExporter::LogMetric(
+    absl::string_view name,
+    absl::string_view test_case_name,
+    const Metric::Stats& metric_stats,
+    Unit unit,
+    ImprovementDirection improvement_direction,
+    std::map<std::string, std::string> metadata) {
+  MutexLock lock(&mutex_);
+  metrics_.push_back(Metric{.name = std::string(name),
+                            .unit = unit,
+                            .improvement_direction = improvement_direction,
+                            .test_case = std::string(test_case_name),
+                            .metric_metadata = std::move(metadata),
+                            .time_series = Metric::TimeSeries{.samples = {}},
+                            .stats = std::move(metric_stats)});
+}
+
+Timestamp MetricsLoggerAndExporter::Now() {
+  return clock_->CurrentTime();
+}
+
+bool MetricsLoggerAndExporter::Export() {
+  MutexLock lock(&mutex_);
+  bool success = true;
+  for (auto& exporter : exporters_) {
+    bool export_result = exporter->Export(metrics_);
+    success = success && export_result;
+  }
+  return success;
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/api/test/metrics/metrics_logger_and_exporter.h b/api/test/metrics/metrics_logger_and_exporter.h
new file mode 100644
index 0000000..a761f74
--- /dev/null
+++ b/api/test/metrics/metrics_logger_and_exporter.h
@@ -0,0 +1,94 @@
+/*
+ *  Copyright (c) 2022 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef API_TEST_METRICS_METRICS_LOGGER_AND_EXPORTER_H_
+#define API_TEST_METRICS_METRICS_LOGGER_AND_EXPORTER_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "api/numerics/samples_stats_counter.h"
+#include "api/test/metrics/metric.h"
+#include "api/test/metrics/metrics_exporter.h"
+#include "rtc_base/synchronization/mutex.h"
+#include "system_wrappers/include/clock.h"
+
+namespace webrtc {
+namespace test {
+
+// Combines metrics logging and exporting to provide simple API to automatically
+// export metrics at the end of the scope.
+class MetricsLoggerAndExporter {
+ public:
+  // `crash_on_export_failure` - makes MetricsLoggerAndExporter to crash if
+  // any of exporters failed to export data.
+  MetricsLoggerAndExporter(
+      webrtc::Clock* clock,
+      std::vector<std::unique_ptr<MetricsExporter>> exporters,
+      bool crash_on_export_failure = true)
+      : clock_(clock),
+        crash_on_export_failure_(crash_on_export_failure),
+        exporters_(std::move(exporters)) {}
+  ~MetricsLoggerAndExporter();
+
+  // Adds a metric with a single value.
+  // `metadata` - metric's level metadata to add.
+  void LogSingleValueMetric(absl::string_view name,
+                            absl::string_view test_case_name,
+                            double value,
+                            Unit unit,
+                            ImprovementDirection improvement_direction,
+                            std::map<std::string, std::string> metadata);
+
+  // Adds metrics with a time series created based on the provided `values`.
+  // `metadata` - metric's level metadata to add.
+  void LogMetric(absl::string_view name,
+                 absl::string_view test_case_name,
+                 const SamplesStatsCounter& values,
+                 Unit unit,
+                 ImprovementDirection improvement_direction,
+                 std::map<std::string, std::string> metadata);
+
+  // Adds metric with a time series with only stats object and without actual
+  // collected values.
+  // `metadata` - metric's level metadata to add.
+  void LogMetric(absl::string_view name,
+                 absl::string_view test_case_name,
+                 const Metric::Stats& metric_stats,
+                 Unit unit,
+                 ImprovementDirection improvement_direction,
+                 std::map<std::string, std::string> metadata);
+
+  // Returns all metrics collected by this logger.
+  std::vector<Metric> GetCollectedMetrics() const {
+    MutexLock lock(&mutex_);
+    return metrics_;
+  }
+
+ private:
+  webrtc::Timestamp Now();
+  bool Export();
+
+  webrtc::Clock* const clock_;
+  const bool crash_on_export_failure_;
+
+  mutable Mutex mutex_;
+  std::vector<Metric> metrics_ RTC_GUARDED_BY(mutex_);
+  std::vector<std::unique_ptr<MetricsExporter>> exporters_;
+};
+
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // API_TEST_METRICS_METRICS_LOGGER_AND_EXPORTER_H_
diff --git a/api/test/metrics/metrics_logger_and_exporter_test.cc b/api/test/metrics/metrics_logger_and_exporter_test.cc
new file mode 100644
index 0000000..f984a12
--- /dev/null
+++ b/api/test/metrics/metrics_logger_and_exporter_test.cc
@@ -0,0 +1,333 @@
+/*
+ *  Copyright (c) 2022 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "api/test/metrics/metrics_logger_and_exporter.h"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/numerics/samples_stats_counter.h"
+#include "api/test/metrics/metric.h"
+#include "api/test/metrics/metrics_exporter.h"
+#include "system_wrappers/include/clock.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+
+using ::testing::Eq;
+using ::testing::IsEmpty;
+
+std::map<std::string, std::string> DefaultMetadata() {
+  return std::map<std::string, std::string>{{"key", "value"}};
+}
+
+struct TestMetricsExporterFactory {
+ public:
+  std::unique_ptr<MetricsExporter> CreateExporter() {
+    return std::make_unique<TestMetricsExporter>(this, /*export_result=*/true);
+  }
+
+  std::unique_ptr<MetricsExporter> CreateFailureExporter() {
+    return std::make_unique<TestMetricsExporter>(this, /*export_result=*/false);
+  }
+
+  std::vector<Metric> exported_metrics;
+
+ private:
+  class TestMetricsExporter : public MetricsExporter {
+   public:
+    TestMetricsExporter(TestMetricsExporterFactory* factory, bool export_result)
+        : factory_(factory), export_result_(export_result) {}
+    ~TestMetricsExporter() override = default;
+
+    bool Export(rtc::ArrayView<const Metric> metrics) override {
+      factory_->exported_metrics =
+          std::vector<Metric>(metrics.begin(), metrics.end());
+      return export_result_;
+    }
+
+    TestMetricsExporterFactory* factory_;
+    bool export_result_;
+  };
+};
+
+TEST(MetricsLoggerAndExporterTest, LogSingleValueMetricRecordsMetric) {
+  TestMetricsExporterFactory exporter_factory;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters));
+    logger.LogSingleValueMetric(
+        "metric_name", "test_case_name",
+        /*value=*/10, Unit::kTimeMs, ImprovementDirection::kBiggerIsBetter,
+        std::map<std::string, std::string>{{"key", "value"}});
+  }
+
+  std::vector<Metric> metrics = exporter_factory.exported_metrics;
+  ASSERT_THAT(metrics.size(), Eq(1lu));
+  const Metric& metric = metrics[0];
+  EXPECT_THAT(metric.name, Eq("metric_name"));
+  EXPECT_THAT(metric.test_case, Eq("test_case_name"));
+  EXPECT_THAT(metric.unit, Eq(Unit::kTimeMs));
+  EXPECT_THAT(metric.improvement_direction,
+              Eq(ImprovementDirection::kBiggerIsBetter));
+  EXPECT_THAT(metric.metric_metadata,
+              Eq(std::map<std::string, std::string>{{"key", "value"}}));
+  ASSERT_THAT(metric.time_series.samples.size(), Eq(1lu));
+  EXPECT_THAT(metric.time_series.samples[0].value, Eq(10.0));
+  EXPECT_THAT(metric.time_series.samples[0].sample_metadata,
+              Eq(std::map<std::string, std::string>{}));
+  ASSERT_THAT(metric.stats.mean, absl::optional<double>(10.0));
+  ASSERT_THAT(metric.stats.stddev, absl::nullopt);
+  ASSERT_THAT(metric.stats.min, absl::optional<double>(10.0));
+  ASSERT_THAT(metric.stats.max, absl::optional<double>(10.0));
+}
+
+TEST(MetricsLoggerAndExporterTest,
+     LogMetricWithSamplesStatsCounterRecordsMetric) {
+  TestMetricsExporterFactory exporter_factory;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters));
+
+    SamplesStatsCounter values;
+    values.AddSample(SamplesStatsCounter::StatsSample{
+        .value = 10,
+        .time = Clock::GetRealTimeClock()->CurrentTime(),
+        .metadata =
+            std::map<std::string, std::string>{{"point_key1", "value1"}}});
+    values.AddSample(SamplesStatsCounter::StatsSample{
+        .value = 20,
+        .time = Clock::GetRealTimeClock()->CurrentTime(),
+        .metadata =
+            std::map<std::string, std::string>{{"point_key2", "value2"}}});
+    logger.LogMetric("metric_name", "test_case_name", values, Unit::kTimeMs,
+                     ImprovementDirection::kBiggerIsBetter,
+                     std::map<std::string, std::string>{{"key", "value"}});
+  }
+
+  std::vector<Metric> metrics = exporter_factory.exported_metrics;
+  ASSERT_THAT(metrics.size(), Eq(1lu));
+  const Metric& metric = metrics[0];
+  EXPECT_THAT(metric.name, Eq("metric_name"));
+  EXPECT_THAT(metric.test_case, Eq("test_case_name"));
+  EXPECT_THAT(metric.unit, Eq(Unit::kTimeMs));
+  EXPECT_THAT(metric.improvement_direction,
+              Eq(ImprovementDirection::kBiggerIsBetter));
+  EXPECT_THAT(metric.metric_metadata,
+              Eq(std::map<std::string, std::string>{{"key", "value"}}));
+  ASSERT_THAT(metric.time_series.samples.size(), Eq(2lu));
+  EXPECT_THAT(metric.time_series.samples[0].value, Eq(10.0));
+  EXPECT_THAT(metric.time_series.samples[0].sample_metadata,
+              Eq(std::map<std::string, std::string>{{"point_key1", "value1"}}));
+  EXPECT_THAT(metric.time_series.samples[1].value, Eq(20.0));
+  EXPECT_THAT(metric.time_series.samples[1].sample_metadata,
+              Eq(std::map<std::string, std::string>{{"point_key2", "value2"}}));
+  ASSERT_THAT(metric.stats.mean, absl::optional<double>(15.0));
+  ASSERT_THAT(metric.stats.stddev, absl::optional<double>(5.0));
+  ASSERT_THAT(metric.stats.min, absl::optional<double>(10.0));
+  ASSERT_THAT(metric.stats.max, absl::optional<double>(20.0));
+}
+
+TEST(MetricsLoggerAndExporterTest, LogMetricWithStatsRecordsMetric) {
+  TestMetricsExporterFactory exporter_factory;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters));
+    Metric::Stats metric_stats{.mean = 15, .stddev = 5, .min = 10, .max = 20};
+    logger.LogMetric("metric_name", "test_case_name", metric_stats,
+                     Unit::kTimeMs, ImprovementDirection::kBiggerIsBetter,
+                     std::map<std::string, std::string>{{"key", "value"}});
+  }
+
+  std::vector<Metric> metrics = exporter_factory.exported_metrics;
+  ASSERT_THAT(metrics.size(), Eq(1lu));
+  const Metric& metric = metrics[0];
+  EXPECT_THAT(metric.name, Eq("metric_name"));
+  EXPECT_THAT(metric.test_case, Eq("test_case_name"));
+  EXPECT_THAT(metric.unit, Eq(Unit::kTimeMs));
+  EXPECT_THAT(metric.improvement_direction,
+              Eq(ImprovementDirection::kBiggerIsBetter));
+  EXPECT_THAT(metric.metric_metadata,
+              Eq(std::map<std::string, std::string>{{"key", "value"}}));
+  ASSERT_THAT(metric.time_series.samples.size(), Eq(0lu));
+  ASSERT_THAT(metric.stats.mean, absl::optional<double>(15.0));
+  ASSERT_THAT(metric.stats.stddev, absl::optional<double>(5.0));
+  ASSERT_THAT(metric.stats.min, absl::optional<double>(10.0));
+  ASSERT_THAT(metric.stats.max, absl::optional<double>(20.0));
+}
+
+TEST(MetricsLoggerAndExporterTest, LogSingleValueMetricRecordsMultipleMetrics) {
+  TestMetricsExporterFactory exporter_factory;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters));
+
+    logger.LogSingleValueMetric("metric_name1", "test_case_name1",
+                                /*value=*/10, Unit::kTimeMs,
+                                ImprovementDirection::kBiggerIsBetter,
+                                DefaultMetadata());
+    logger.LogSingleValueMetric("metric_name2", "test_case_name2",
+                                /*value=*/10, Unit::kTimeMs,
+                                ImprovementDirection::kBiggerIsBetter,
+                                DefaultMetadata());
+  }
+
+  std::vector<Metric> metrics = exporter_factory.exported_metrics;
+  ASSERT_THAT(metrics.size(), Eq(2lu));
+  EXPECT_THAT(metrics[0].name, Eq("metric_name1"));
+  EXPECT_THAT(metrics[0].test_case, Eq("test_case_name1"));
+  EXPECT_THAT(metrics[1].name, Eq("metric_name2"));
+  EXPECT_THAT(metrics[1].test_case, Eq("test_case_name2"));
+}
+
+TEST(MetricsLoggerAndExporterTest,
+     LogMetricWithSamplesStatsCounterRecordsMultipleMetrics) {
+  TestMetricsExporterFactory exporter_factory;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters));
+    SamplesStatsCounter values;
+    values.AddSample(SamplesStatsCounter::StatsSample{
+        .value = 10,
+        .time = Clock::GetRealTimeClock()->CurrentTime(),
+        .metadata = DefaultMetadata()});
+    values.AddSample(SamplesStatsCounter::StatsSample{
+        .value = 20,
+        .time = Clock::GetRealTimeClock()->CurrentTime(),
+        .metadata = DefaultMetadata()});
+
+    logger.LogMetric("metric_name1", "test_case_name1", values, Unit::kTimeMs,
+                     ImprovementDirection::kBiggerIsBetter, DefaultMetadata());
+    logger.LogMetric("metric_name2", "test_case_name2", values, Unit::kTimeMs,
+                     ImprovementDirection::kBiggerIsBetter, DefaultMetadata());
+  }
+
+  std::vector<Metric> metrics = exporter_factory.exported_metrics;
+  ASSERT_THAT(metrics.size(), Eq(2lu));
+  EXPECT_THAT(metrics[0].name, Eq("metric_name1"));
+  EXPECT_THAT(metrics[0].test_case, Eq("test_case_name1"));
+  EXPECT_THAT(metrics[1].name, Eq("metric_name2"));
+  EXPECT_THAT(metrics[1].test_case, Eq("test_case_name2"));
+}
+
+TEST(MetricsLoggerAndExporterTest, LogMetricWithStatsRecordsMultipleMetrics) {
+  TestMetricsExporterFactory exporter_factory;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters));
+    Metric::Stats metric_stats{.mean = 15, .stddev = 5, .min = 10, .max = 20};
+
+    logger.LogMetric("metric_name1", "test_case_name1", metric_stats,
+                     Unit::kTimeMs, ImprovementDirection::kBiggerIsBetter,
+                     DefaultMetadata());
+    logger.LogMetric("metric_name2", "test_case_name2", metric_stats,
+                     Unit::kTimeMs, ImprovementDirection::kBiggerIsBetter,
+                     DefaultMetadata());
+  }
+
+  std::vector<Metric> metrics = exporter_factory.exported_metrics;
+  ASSERT_THAT(metrics.size(), Eq(2lu));
+  EXPECT_THAT(metrics[0].name, Eq("metric_name1"));
+  EXPECT_THAT(metrics[0].test_case, Eq("test_case_name1"));
+  EXPECT_THAT(metrics[1].name, Eq("metric_name2"));
+  EXPECT_THAT(metrics[1].test_case, Eq("test_case_name2"));
+}
+
+TEST(MetricsLoggerAndExporterTest,
+     LogMetricThroughtAllMethodsAccumulateAllMetrics) {
+  TestMetricsExporterFactory exporter_factory;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters));
+    SamplesStatsCounter values;
+    values.AddSample(SamplesStatsCounter::StatsSample{
+        .value = 10,
+        .time = Clock::GetRealTimeClock()->CurrentTime(),
+        .metadata = DefaultMetadata()});
+    values.AddSample(SamplesStatsCounter::StatsSample{
+        .value = 20,
+        .time = Clock::GetRealTimeClock()->CurrentTime(),
+        .metadata = DefaultMetadata()});
+    Metric::Stats metric_stats{.mean = 15, .stddev = 5, .min = 10, .max = 20};
+
+    logger.LogSingleValueMetric("metric_name1", "test_case_name1",
+                                /*value=*/10, Unit::kTimeMs,
+                                ImprovementDirection::kBiggerIsBetter,
+                                DefaultMetadata());
+    logger.LogMetric("metric_name2", "test_case_name2", values, Unit::kTimeMs,
+                     ImprovementDirection::kBiggerIsBetter, DefaultMetadata());
+    logger.LogMetric("metric_name3", "test_case_name3", metric_stats,
+                     Unit::kTimeMs, ImprovementDirection::kBiggerIsBetter,
+                     DefaultMetadata());
+  }
+
+  std::vector<Metric> metrics = exporter_factory.exported_metrics;
+  ASSERT_THAT(metrics.size(), Eq(3lu));
+  EXPECT_THAT(metrics[0].name, Eq("metric_name1"));
+  EXPECT_THAT(metrics[0].test_case, Eq("test_case_name1"));
+  EXPECT_THAT(metrics[1].name, Eq("metric_name2"));
+  EXPECT_THAT(metrics[1].test_case, Eq("test_case_name2"));
+  EXPECT_THAT(metrics[2].name, Eq("metric_name3"));
+  EXPECT_THAT(metrics[2].test_case, Eq("test_case_name3"));
+}
+
+TEST(MetricsLoggerAndExporterTest,
+     OneFailedExporterDoesNotPreventExportToOthers) {
+  TestMetricsExporterFactory exporter_factory1;
+  TestMetricsExporterFactory exporter_factory2;
+  TestMetricsExporterFactory exporter_factory3;
+  {
+    std::vector<std::unique_ptr<MetricsExporter>> exporters;
+    exporters.push_back(exporter_factory1.CreateExporter());
+    exporters.push_back(exporter_factory2.CreateFailureExporter());
+    exporters.push_back(exporter_factory3.CreateExporter());
+    MetricsLoggerAndExporter logger(Clock::GetRealTimeClock(),
+                                    std::move(exporters),
+                                    /*crash_on_export_failure=*/false);
+
+    logger.LogSingleValueMetric("metric_name", "test_case_name",
+                                /*value=*/10, Unit::kTimeMs,
+                                ImprovementDirection::kBiggerIsBetter,
+                                DefaultMetadata());
+  }
+
+  std::vector<Metric> metrics1 = exporter_factory1.exported_metrics;
+  std::vector<Metric> metrics2 = exporter_factory2.exported_metrics;
+  std::vector<Metric> metrics3 = exporter_factory3.exported_metrics;
+  ASSERT_THAT(metrics1.size(), Eq(1lu));
+  EXPECT_THAT(metrics1[0].name, Eq("metric_name"));
+  ASSERT_THAT(metrics2.size(), Eq(1lu));
+  EXPECT_THAT(metrics2[0].name, Eq("metric_name"));
+  ASSERT_THAT(metrics3.size(), Eq(1lu));
+  EXPECT_THAT(metrics3[0].name, Eq("metric_name"));
+}
+
+}  // namespace
+}  // namespace test
+}  // namespace webrtc