[cobalt][system_metrics] Log uptime once per hour

Adds a method which logs for the fuchsia_uptime metric
once per hour.

TEST= cobalt_system_metrics_unittests

Change-Id: Ic7888fd6d77af30f1a5e64f5974953732a9ee2fb
diff --git a/src/cobalt/bin/system-metrics/system_metrics_daemon.cc b/src/cobalt/bin/system-metrics/system_metrics_daemon.cc
index bcc0e8d..6ade346 100644
--- a/src/cobalt/bin/system-metrics/system_metrics_daemon.cc
+++ b/src/cobalt/bin/system-metrics/system_metrics_daemon.cc
@@ -43,8 +43,17 @@
 using fuchsia_system_metrics::
     FuchsiaMemoryExperimentalMetricDimensionMemoryBreakdown;
 using fuchsia_system_metrics::FuchsiaUpPingMetricDimensionUptime;
+using fuchsia_system_metrics::FuchsiaUptimeMetricDimensionUptimeRange;
 using std::chrono::steady_clock;
 
+namespace {
+// Given a number of seconds, return the number of seconds before the next
+// multiple of 1 hour.
+std::chrono::seconds SecondsBeforeNextHour(std::chrono::seconds uptime) {
+  return std::chrono::seconds(3600 - (uptime.count() % 3600));
+}
+}  // namespace
+
 SystemMetricsDaemon::SystemMetricsDaemon(async_dispatcher_t* dispatcher,
                                          sys::ComponentContext* context)
     : SystemMetricsDaemon(
@@ -74,19 +83,28 @@
 void SystemMetricsDaemon::StartLogging() {
   TRACE_DURATION("system_metrics", "SystemMetricsDaemon::StartLogging");
   // We keep gathering metrics until this process is terminated.
-  RepeatedlyLogUpTimeAndLifeTimeEvents();
+  RepeatedlyLogUpPingAndLifeTimeEvents();
+  RepeatedlyLogUptime();
   RepeatedlyLogCpuUsage();
   RepeatedlyLogMemoryUsage();
 }
 
-void SystemMetricsDaemon::RepeatedlyLogUpTimeAndLifeTimeEvents() {
-  std::chrono::seconds seconds_to_sleep = LogUpTimeAndLifeTimeEvents();
+void SystemMetricsDaemon::RepeatedlyLogUpPingAndLifeTimeEvents() {
+  std::chrono::seconds seconds_to_sleep = LogUpPingAndLifeTimeEvents();
   async::PostDelayedTask(
-      dispatcher_, [this]() { RepeatedlyLogUpTimeAndLifeTimeEvents(); },
+      dispatcher_, [this]() { RepeatedlyLogUpPingAndLifeTimeEvents(); },
       zx::sec(seconds_to_sleep.count() + 5));
   return;
 }
 
+void SystemMetricsDaemon::RepeatedlyLogUptime() {
+  std::chrono::seconds seconds_to_sleep = LogFuchsiaUptime();
+  async::PostDelayedTask(
+      dispatcher_, [this]() { RepeatedlyLogUptime(); },
+      zx::sec(seconds_to_sleep.count()));
+  return;
+}
+
 void SystemMetricsDaemon::RepeatedlyLogCpuUsage() {
   std::chrono::seconds seconds_to_sleep = LogCpuUsage();
   async::PostDelayedTask(
@@ -112,7 +130,7 @@
   return std::chrono::duration_cast<std::chrono::seconds>(now - start_time_);
 }
 
-std::chrono::seconds SystemMetricsDaemon::LogUpTimeAndLifeTimeEvents() {
+std::chrono::seconds SystemMetricsDaemon::LogUpPingAndLifeTimeEvents() {
   std::chrono::seconds uptime = GetUpTime();
   return std::min(LogFuchsiaUpPing(uptime), LogFuchsiaLifetimeEvents());
 }
@@ -247,6 +265,35 @@
   return std::chrono::hours(1);
 }
 
+std::chrono::seconds SystemMetricsDaemon::LogFuchsiaUptime() {
+  std::chrono::seconds uptime = GetUpTime();
+  if (!logger_) {
+    FX_LOGS(ERROR)
+        << "Cobalt SystemMetricsDaemon: No logger present. Reconnecting...";
+    InitializeLogger();
+    // Something went wrong. Pause for 5 minutes.
+    return std::chrono::minutes(5);
+  }
+  auto up_hours =
+      std::chrono::duration_cast<std::chrono::hours>(uptime).count();
+  uint32_t event_code =
+      (up_hours < 336)
+          ? FuchsiaUptimeMetricDimensionUptimeRange::LessThanTwoWeeks
+          : event_code =
+                FuchsiaUptimeMetricDimensionUptimeRange::TwoWeeksOrMore;
+
+  fuchsia::cobalt::Status status;
+  logger_->LogElapsedTime(fuchsia_system_metrics::kFuchsiaUptimeMetricId,
+                          event_code, "", up_hours, &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FX_LOGS(ERROR)
+        << "Cobalt SystemMetricsDaemon: LogCobaltEvent() returned status="
+        << StatusToString(status);
+  }
+  // Schedule a call of this function for the next multiple of an hour.
+  return SecondsBeforeNextHour(uptime);
+}
+
 std::chrono::seconds SystemMetricsDaemon::LogFuchsiaLifetimeEvents() {
   TRACE_DURATION("system_metrics",
                  "SystemMetricsDaemon::LogFuchsiaLifetimeEvents");
diff --git a/src/cobalt/bin/system-metrics/system_metrics_daemon.h b/src/cobalt/bin/system-metrics/system_metrics_daemon.h
index 5e9eca9..88eff9e 100644
--- a/src/cobalt/bin/system-metrics/system_metrics_daemon.h
+++ b/src/cobalt/bin/system-metrics/system_metrics_daemon.h
@@ -62,10 +62,14 @@
 
   void InitializeRootResourceHandle();
 
-  // Calls LogUpTimeAndLifeTimeEvents,
+  // Calls LogUpPingAndLifeTimeEvents,
   // and then uses the |dispatcher| passed to the constructor to
   // schedule the next round.
-  void RepeatedlyLogUpTimeAndLifeTimeEvents();
+  void RepeatedlyLogUpPingAndLifeTimeEvents();
+
+  // Calls LogFuchsiaUptime and then uses the |dispatcher| passed to the
+  // constructor to schedule the next round.
+  void RepeatedlyLogUptime();
 
   // Calls LogMemoryUsage,
   // then uses the |dispatcher| passed to the constructor to schedule
@@ -83,7 +87,7 @@
   // Calls LogFuchsiaUpPing and LogFuchsiaLifetimeEvents.
   //
   // Returns the amount of time before this method needs to be invoked again.
-  std::chrono::seconds LogUpTimeAndLifeTimeEvents();
+  std::chrono::seconds LogUpPingAndLifeTimeEvents();
 
   // Logs one or more UpPing events depending on how long the device has been
   // up.
@@ -108,6 +112,13 @@
   // Currently returns std::chrono::seconds::max().
   std::chrono::seconds LogFuchsiaLifetimeEvents();
 
+  // Once per hour, rounds the current uptime down to the nearest number of
+  // hours and logs an event for the fuchsia_uptime metric.
+  //
+  // Returns the amount of time before this method needs to be invoked again.
+  // This is the number of seconds until the uptime reaches the next full hour.
+  std::chrono::seconds LogFuchsiaUptime();
+
   // Logs several different measurements of system-wide memory usage.
   //
   // Returns the amount of time before this method needs to be invoked again.
diff --git a/src/cobalt/bin/system-metrics/system_metrics_daemon_test.cc b/src/cobalt/bin/system-metrics/system_metrics_daemon_test.cc
index e2a7e56..287312b 100644
--- a/src/cobalt/bin/system-metrics/system_metrics_daemon_test.cc
+++ b/src/cobalt/bin/system-metrics/system_metrics_daemon_test.cc
@@ -26,10 +26,18 @@
 using fuchsia_system_metrics::
     FuchsiaMemoryExperimental2MetricDimensionTimeSinceBoot;
 using fuchsia_system_metrics::FuchsiaUpPingMetricDimensionUptime;
+using fuchsia_system_metrics::FuchsiaUptimeMetricDimensionUptimeRange;
 using std::chrono::hours;
 using std::chrono::minutes;
 using std::chrono::seconds;
 
+namespace {
+typedef FuchsiaUptimeMetricDimensionUptimeRange UptimeRange;
+static constexpr int kHour = 3600;
+static constexpr int kDay = 24 * kHour;
+static constexpr int kWeek = 7 * kDay;
+}  // namespace
+
 class SystemMetricsDaemonTest : public gtest::TestLoopFixture {
  public:
   // Note that we first save an unprotected pointer in fake_clock_ and then
@@ -52,14 +60,18 @@
     return daemon_->LogFuchsiaLifetimeEvents();
   }
 
-  seconds LogUpTimeAndLifeTimeEvents() {
-    return daemon_->LogUpTimeAndLifeTimeEvents();
+  seconds LogUpPingAndLifeTimeEvents() {
+    return daemon_->LogUpPingAndLifeTimeEvents();
   }
 
-  void RepeatedlyLogUpTimeAndLifeTimeEvents() {
-    return daemon_->RepeatedlyLogUpTimeAndLifeTimeEvents();
+  seconds LogFuchsiaUptime() { return daemon_->LogFuchsiaUptime(); }
+
+  void RepeatedlyLogUpPingAndLifeTimeEvents() {
+    return daemon_->RepeatedlyLogUpPingAndLifeTimeEvents();
   }
 
+  void RepeatedlyLogUptime() { return daemon_->RepeatedlyLogUptime(); }
+
   seconds LogMemoryUsage() { return daemon_->LogMemoryUsage(); }
 
   seconds LogCpuUsage() {
@@ -86,6 +98,16 @@
     EXPECT_EQ(expected_event_count, fake_logger_.event_count());
   }
 
+  void CheckUptimeValues(size_t expected_call_count,
+                         uint32_t expected_last_event_code,
+                         int64_t expected_last_up_hours) {
+    EXPECT_EQ(expected_call_count, fake_logger_.call_count());
+    EXPECT_EQ(fuchsia_system_metrics::kFuchsiaUptimeMetricId,
+              fake_logger_.last_metric_id());
+    EXPECT_EQ(expected_last_event_code, fake_logger_.last_event_code());
+    EXPECT_EQ(expected_last_up_hours, fake_logger_.last_elapsed_time());
+  }
+
   void DoFuchsiaUpPingTest(seconds now_seconds, seconds expected_sleep_seconds,
                            size_t expected_call_count,
                            uint32_t expected_last_event_code) {
@@ -97,7 +119,7 @@
                 expected_last_event_code);
   }
 
-  void DoLogUpTimeAndLifeTimeEventsTest(seconds increment_seconds,
+  void DoLogUpPingAndLifeTimeEventsTest(seconds increment_seconds,
                                         seconds expected_sleep_seconds,
                                         size_t expected_call_count,
                                         uint32_t expected_last_metric_id,
@@ -105,16 +127,26 @@
     fake_logger_.reset();
     fake_clock_->Increment(increment_seconds);
     EXPECT_EQ(expected_sleep_seconds.count(),
-              LogUpTimeAndLifeTimeEvents().count());
+              LogUpPingAndLifeTimeEvents().count());
     CheckValues(cobalt::kLogEvent, expected_call_count, expected_last_metric_id,
                 expected_last_event_code);
   }
 
+  void DoFuchsiaUptimeTest(seconds now_seconds, seconds expected_sleep_seconds,
+                           uint32_t expected_event_code,
+                           int64_t expected_up_hours) {
+    fake_logger_.reset();
+    SetClockToDaemonStartTime();
+    fake_clock_->Increment(now_seconds);
+    EXPECT_EQ(expected_sleep_seconds.count(), LogFuchsiaUptime().count());
+    CheckUptimeValues(1u, expected_event_code, expected_up_hours);
+  }
+
   // This method is used by the test of the method
-  // RepeatedlyLogUpTimeAndLifeTimeEvents(). It advances our two fake clocks
+  // RepeatedlyLogUpPingAndLifeTimeEvents(). It advances our two fake clocks
   // (one used by the SystemMetricDaemon, one used by the MessageLoop) by the
   // specified amount, and then checks to make sure that
-  // RepeatedlyLogUpTimeAndLifeTimeEvents() was executed and did the expected
+  // RepeatedlyLogUpPingAndLifeTimeEvents() was executed and did the expected
   // thing.
   void AdvanceTimeAndCheck(seconds advance_time_seconds,
                            size_t expected_call_count,
@@ -131,12 +163,59 @@
     fake_logger_.reset();
   }
 
+  // This method is used by the test of the method RepeatedlyLogUptime(). It
+  // advances our two fake clocks by the specified amount, and then checks to
+  // make sure that RepeatedlyLogUptime() made the expected logging calls in the
+  // meantime.
+  void AdvanceAndCheckUptime(seconds advance_time_seconds,
+                             size_t expected_call_count,
+                             uint32_t expected_last_event_code,
+                             int64_t expected_last_up_hours) {
+    bool expected_activity = (expected_call_count != 0);
+    fake_clock_->Increment(advance_time_seconds);
+    EXPECT_EQ(expected_activity,
+              RunLoopFor(zx::sec(advance_time_seconds.count())));
+    if (expected_activity) {
+      CheckUptimeValues(expected_call_count, expected_last_event_code,
+                        expected_last_up_hours);
+    }
+    fake_logger_.reset();
+  }
+
+  // Rewinds the SystemMetricsDaemon's clock back to the daemon's startup time.
+  void SetClockToDaemonStartTime() {
+    fake_clock_->set_time(daemon_->start_time_);
+  }
+
  protected:
   FakeSteadyClock* fake_clock_;
   FakeLogger_Sync fake_logger_;
   std::unique_ptr<SystemMetricsDaemon> daemon_;
 };
 
+// Tests the method LogFuchsiaUptime(). Uses a local FakeLogger_Sync and
+// does not use FIDL. Does not use the message loop.
+TEST_F(SystemMetricsDaemonTest, LogFuchsiaUptime) {
+  DoFuchsiaUptimeTest(seconds(0), seconds(kHour), UptimeRange::LessThanTwoWeeks,
+                      0);
+  DoFuchsiaUptimeTest(seconds(kHour - 1), seconds(1),
+                      UptimeRange::LessThanTwoWeeks, 0);
+  DoFuchsiaUptimeTest(seconds(5), seconds(kHour - 5),
+                      UptimeRange::LessThanTwoWeeks, 0);
+  DoFuchsiaUptimeTest(seconds(kDay), seconds(kHour),
+                      UptimeRange::LessThanTwoWeeks, 24);
+  DoFuchsiaUptimeTest(seconds(kDay + 6 * kHour + 10), seconds(kHour - 10),
+                      UptimeRange::LessThanTwoWeeks, 30);
+  DoFuchsiaUptimeTest(seconds(kWeek), seconds(kHour),
+                      UptimeRange::LessThanTwoWeeks, 168);
+  DoFuchsiaUptimeTest(seconds(kWeek), seconds(kHour),
+                      UptimeRange::LessThanTwoWeeks, 168);
+  DoFuchsiaUptimeTest(seconds(2 * kWeek), seconds(kHour),
+                      UptimeRange::TwoWeeksOrMore, 336);
+  DoFuchsiaUptimeTest(seconds(2 * kWeek + 6 * kDay + 10), seconds(kHour - 10),
+                      UptimeRange::TwoWeeksOrMore, 480);
+}
+
 // Tests the method LogFuchsiaUpPing(). Uses a local FakeLogger_Sync and
 // does not use FIDL. Does not use the message loop.
 TEST_F(SystemMetricsDaemonTest, LogFuchsiaUpPing) {
@@ -161,7 +240,8 @@
                       FuchsiaUpPingMetricDimensionUptime::UpOneMinute);
 
   // If we've been up for 61 seconds, expect 2 log events, the second one
-  // being of type UpOneMinute, and a return value of 9 minutes minus 1 second.
+  // being of type UpOneMinute, and a return value of 9 minutes minus 1
+  // second.
   DoFuchsiaUpPingTest(seconds(61), minutes(9) - seconds(1), 2,
                       FuchsiaUpPingMetricDimensionUptime::UpOneMinute);
 
@@ -176,8 +256,8 @@
                       FuchsiaUpPingMetricDimensionUptime::UpTenMinutes);
 
   // If we've been up for 10 minutes plus 1 second, expect 3 log events, the
-  // last one being of type UpTenMinutes, and a return value of 50 minutes minus
-  // one second.
+  // last one being of type UpTenMinutes, and a return value of 50 minutes
+  // minus one second.
   DoFuchsiaUpPingTest(minutes(10) + seconds(1), minutes(50) - seconds(1), 3,
                       FuchsiaUpPingMetricDimensionUptime::UpTenMinutes);
 
@@ -237,46 +317,46 @@
                       FuchsiaUpPingMetricDimensionUptime::UpSixDays);
 }
 
-// Tests the method LogFuchsiaLifetimeEvents(). Uses a local FakeLogger_Sync and
-// does not use FIDL. Does not use the message loop.
+// Tests the method LogFuchsiaLifetimeEvents(). Uses a local FakeLogger_Sync
+// and does not use FIDL. Does not use the message loop.
 TEST_F(SystemMetricsDaemonTest, LogFuchsiaLifetimeEvents) {
   fake_logger_.reset();
-  // The first time LogFuchsiaLifetimeEvents() is invoked it should log 1 event
-  // of type "Boot" and return seconds::max().
+  // The first time LogFuchsiaLifetimeEvents() is invoked it should log 1
+  // event of type "Boot" and return seconds::max().
   EXPECT_EQ(seconds::max(), LogFuchsiaLifetimeEvents());
   CheckValues(cobalt::kLogEvent, 1,
               fuchsia_system_metrics::kFuchsiaLifetimeEventsMetricId,
               FuchsiaLifetimeEventsMetricDimensionEvents::Boot);
 
   fake_logger_.reset();
-  // The second time LogFuchsiaLifetimeEvents() is invoked it should do nothing
-  // and return seconds::max().
+  // The second time LogFuchsiaLifetimeEvents() is invoked it should do
+  // nothing and return seconds::max().
   EXPECT_EQ(seconds::max(), LogFuchsiaLifetimeEvents());
   CheckValues(cobalt::kOther, 0, -1, -1);
 }
 
-// Tests the method LogUpTimeAndLifeTimeEvents(). Uses a local FakeLogger_Sync
+// Tests the method LogUpPingAndLifeTimeEvents(). Uses a local FakeLogger_Sync
 // and does not use FIDL. Does not use the message loop.
-TEST_F(SystemMetricsDaemonTest, LogUpTimeAndLifeTimeEvents) {
+TEST_F(SystemMetricsDaemonTest, LogUpPingAndLifeTimeEvents) {
   // If we have been up for 1 second, expect 2 log events. First there is an
   // "Up" event and then there is "Boot" event. Expect a return value of
   // 59 seconds.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       seconds(1), seconds(59), 2,
       fuchsia_system_metrics::kFuchsiaLifetimeEventsMetricId,
       FuchsiaLifetimeEventsMetricDimensionEvents::Boot);
 
   // 59 Seconds later, expect 2 log events. First there is an "Up" event and
   // then there is an "UpOneMinute" event. Expect a return value of 9 minutes.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       seconds(59), minutes(9), 2,
       fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneMinute);
 
   // 9 Minutes minus 1 second later, expect 2 log events. First there is an
-  // "Up" event and then there is an "UpOneMinute" event. Expect a return value
-  // of 1 second.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  // "Up" event and then there is an "UpOneMinute" event. Expect a return
+  // value of 1 second.
+  DoLogUpPingAndLifeTimeEventsTest(
       minutes(9) - seconds(1), seconds(1), 2,
       fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneMinute);
@@ -284,7 +364,7 @@
   // 2 seconds later, expect 3 log events. First there is an
   // "Up" event and then there is an "UpOneMinute" event and then there is an
   // "UpTenMinutes" event. Expect a return value  of 50 minutes - 1 second.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       seconds(2), minutes(50) - seconds(1), 3,
       fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpTenMinutes);
@@ -292,7 +372,7 @@
   // 50 minutes - 1 second later, the device has been up for one hour.
   // Expect 4 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour".
   // Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       minutes(50) - seconds(1), hours(1), 4,
       fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneHour);
@@ -300,85 +380,128 @@
   // One hour later, the device has been up for two hours.
   // Expect 4 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour".
   // Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 4, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneHour);
 
   // One hour later, the device has been up for three hours.
   // Expect 4 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour".
   // Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 4, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneHour);
 
   // One hour later, the device has been up for four hours.
   // Expect 4 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour".
   // Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 4, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneHour);
 
   // One hour later, the device has been up for five hours.
   // Expect 4 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour".
   // Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 4, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneHour);
 
   // One hour later, the device has been up for six hours.
   // Expect 4 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour".
   // Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 4, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneHour);
 
   // Six hours later, the device has been up for twelve hours.
   // Expect 5 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour",
   // "UpTwelveHours". Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(6), hours(1), 5, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpTwelveHours);
 
   // One hour later, the device has been up for 13 hours.
   // Expect 5 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour",
   // "UpTwelveHours". Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 5, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpTwelveHours);
 
   // One hour later, the device has been up for 14 hours.
   // Expect 5 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour",
   // "UpTwelveHours". Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 5, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpTwelveHours);
 
   // Ten hours later, the device has been up for 24 hours.
   // Expect 6 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour",
   // "UpTwelveHours", "UpOneDay". Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(10), hours(1), 6, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneDay);
 
   // One  later, the device has been up for 25 hours.
   // Expect 6 log events: "Up", "UpOneMinute", "UpTenMinutes", "UpOneHour",
   // "UpTwelveHours", "UpOneDay". Expect a return value of one hour.
-  DoLogUpTimeAndLifeTimeEventsTest(
+  DoLogUpPingAndLifeTimeEventsTest(
       hours(1), hours(1), 6, fuchsia_system_metrics::kFuchsiaUpPingMetricId,
       FuchsiaUpPingMetricDimensionUptime::UpOneDay);
 }
 
-// Tests the method RepeatedlyLogUpTimeAndLifeTimeEvents(). This test differs
+// Tests the method RepeatedlyLogUptime(). This test uses the message loop to
+// schedule future runs of work. Uses a local FakeLogger_Sync and does not use
+// FIDL.
+TEST_F(SystemMetricsDaemonTest, RepeatedlyLogUptime) {
+  RunLoopUntilIdle();
+
+  // Invoke the method under test. This should cause the uptime to be logged
+  // once, and schedules the next run for approximately 1 hour in the future.
+  // (More precisely, the next run should occur in 1 hour minus the amount of
+  // time after the daemon's start time which this method is invoked.)
+  RepeatedlyLogUptime();
+
+  // The first event should have been logged, with an uptime of 0 hours.
+  CheckUptimeValues(1u, UptimeRange::LessThanTwoWeeks, 0);
+  fake_logger_.reset();
+
+  // Advance the clock by 30 seconds. Nothing should have happened.
+  AdvanceAndCheckUptime(seconds(30), 0, -1, -1);
+
+  // Advance the clock to the next hour. The system metrics daemon has been up
+  // for 1 hour by now, so the second event should have been logged.
+  AdvanceAndCheckUptime(seconds(kHour - 30), 1, UptimeRange::LessThanTwoWeeks,
+                        1);
+
+  // Advance the clock by 1 day. At this point, the daemon has been up for 25
+  // hours. Since the last time we checked |fake_logger_|, the daemon should
+  // have logged the uptime 24 times, with the most recent value equal to 25.
+  AdvanceAndCheckUptime(seconds(kDay), 24, UptimeRange::LessThanTwoWeeks, 25);
+
+  // Advance the clock by 1 week. At this point, the daemon has been up for 8
+  // days + 1 hour. Since the last time we checked |fake_logger_|, the daemon
+  // should have logged the uptime 168 times, with the most recent value equal
+  // to 193.
+  AdvanceAndCheckUptime(seconds(kWeek), 168, UptimeRange::LessThanTwoWeeks,
+                        193);
+
+  // Advance the clock 1 more week. At this point, the daemon has been up for
+  // 15 days + 1 hour. Since the last time we checked |fake_logger_|, the daemon
+  // should have logged the uptime 168 times, with the most recent value equal
+  // to 361.
+  AdvanceAndCheckUptime(seconds(kWeek), 168, UptimeRange::TwoWeeksOrMore, 361);
+}
+
+// Tests the method RepeatedlyLogUpPingAndLifeTimeEvents(). This test differs
 // from the previous ones because it makes use of the message loop in order to
 // schedule future runs of work. Uses a local FakeLogger_Sync and does not use
 // FIDL.
-TEST_F(SystemMetricsDaemonTest, RepeatedlyLogUpTimeAndLifeTimeEvents) {
+TEST_F(SystemMetricsDaemonTest, RepeatedlyLogUpPingAndLifeTimeEvents) {
   // Make sure the loop has no initial pending work.
   RunLoopUntilIdle();
 
   // Invoke the method under test. This kicks of the first run and schedules
   // the second run for 1 minute plus 5 seconds in the future.
-  RepeatedlyLogUpTimeAndLifeTimeEvents();
+  RepeatedlyLogUpPingAndLifeTimeEvents();
 
   // The initial two events should have been logged, the second of which is
   // |Boot|.
@@ -390,7 +513,7 @@
   // Advance the clock by 30 seconds. Nothing should have happened.
   AdvanceTimeAndCheck(seconds(30), 0, -1, -1);
   // Advance the clock by 30 seconds again. Nothing should have happened
-  // because the first run of RepeatedlyLogUpTimeAndLifeTimeEvents() added a 5
+  // because the first run of RepeatedlyLogUpPingAndLifeTimeEvents() added a 5
   // second buffer to the next scheduled run time.
   AdvanceTimeAndCheck(seconds(30), 0, -1, -1);
 
@@ -419,7 +542,8 @@
   AdvanceTimeAndCheck(minutes(60) - (minutes(10) + seconds(5)), 0, -1, -1);
 
   // Advance the clock 5 s to t=1h + 5s. Now expect the fourth batch of
-  // work to occur. This consists of 4 events the last of which is |UpOneHour|.
+  // work to occur. This consists of 4 events the last of which is
+  // |UpOneHour|.
   AdvanceTimeAndCheck(seconds(5), 4,
                       fuchsia_system_metrics::kFuchsiaUpPingMetricId,
                       FuchsiaUpPingMetricDimensionUptime::UpOneHour);
diff --git a/src/cobalt/bin/testing/fake_clock.h b/src/cobalt/bin/testing/fake_clock.h
index 2192571..d437a28 100644
--- a/src/cobalt/bin/testing/fake_clock.h
+++ b/src/cobalt/bin/testing/fake_clock.h
@@ -22,6 +22,8 @@
     now_ += increment_seconds;
   }
 
+  void set_time(std::chrono::steady_clock::time_point t) { now_ = t; }
+
  private:
   std::chrono::steady_clock::time_point now_ = std::chrono::steady_clock::now();
 };
diff --git a/src/cobalt/bin/testing/fake_logger.cc b/src/cobalt/bin/testing/fake_logger.cc
index 9f188e9..9e6b1a0 100644
--- a/src/cobalt/bin/testing/fake_logger.cc
+++ b/src/cobalt/bin/testing/fake_logger.cc
@@ -43,6 +43,7 @@
   last_log_method_invoked_ = kLogElapsedTime;
   last_metric_id_ = metric_id;
   last_event_code_ = event_code;
+  last_elapsed_time_ = elapsed_micros;
   *out_status = Status::OK;
   return ZX_OK;
 }
diff --git a/src/cobalt/bin/testing/fake_logger.h b/src/cobalt/bin/testing/fake_logger.h
index 3ebe3c3..07f01e6 100644
--- a/src/cobalt/bin/testing/fake_logger.h
+++ b/src/cobalt/bin/testing/fake_logger.h
@@ -69,6 +69,10 @@
 
   void reset_last_event_code() { last_event_code_ = -1; }
 
+  int64_t last_elapsed_time() { return last_elapsed_time_; }
+
+  void reset_last_elapsed_time() { last_elapsed_time_ = -1; }
+
   LogMethod last_log_method_invoked() { return last_log_method_invoked_; }
 
   void reset_last_log_method_invoked() { last_log_method_invoked_ = kOther; }
@@ -84,6 +88,7 @@
   void reset() {
     reset_last_metric_id();
     reset_last_event_code();
+    reset_last_elapsed_time();
     reset_last_log_method_invoked();
     reset_call_count();
     reset_event_count();
@@ -92,6 +97,7 @@
  private:
   uint32_t last_metric_id_ = -1;
   uint32_t last_event_code_ = -1;
+  int64_t last_elapsed_time_ = -1;
   LogMethod last_log_method_invoked_ = kOther;
   size_t call_count_ = 0;
   size_t event_count_ = 0;  // for LogCobaltEvents only