[cobalt] SystemMetricsDaemon: New version.

We have written a new version of the SystemMetricsDaemon with the
following changes:

(1) We use new Cobalt 1.0 locally aggregated metrics.
(2) We reorganized the code into multiple files.
(3) We reorganized the code to make it more testable.
(4) We added extensive testing.

In support of this we also introduce a new cobalt/testing library
containing the following:

(1) FakeSteadyClock
(2) FakeLogger_Sync

TEST=unit test and manual test

Change-Id: I3cb7038ab4ed0a02a32d18e6a24b360b86f24944
diff --git a/garnet/bin/cobalt/BUILD.gn b/garnet/bin/cobalt/BUILD.gn
index 6be7cac2..79d12e3 100644
--- a/garnet/bin/cobalt/BUILD.gn
+++ b/garnet/bin/cobalt/BUILD.gn
@@ -20,6 +20,7 @@
     ":cobalt_encoder_unittests",
     "app:app",
     "app:cobalt_app_unittests",
+    "system-metrics:cobalt_system_metrics_unittests",
     "testapp:cobalt_testapp",
     "testapp:generate_legacy_testapp_metrics",
     "testapp:generate_testapp_metrics",
@@ -53,6 +54,10 @@
     },
 
     {
+      name = "cobalt_system_metrics_unittests"
+    },
+
+    {
       name = "cobalt_utils_unittests"
 
       disabled = true
diff --git a/garnet/bin/cobalt/system-metrics/BUILD.gn b/garnet/bin/cobalt/system-metrics/BUILD.gn
index 9ae7969..017a663 100644
--- a/garnet/bin/cobalt/system-metrics/BUILD.gn
+++ b/garnet/bin/cobalt/system-metrics/BUILD.gn
@@ -6,6 +6,29 @@
 import("//third_party/cobalt_config/metrics_registry.gni")
 import("//third_party/protobuf/proto_library.gni")
 
+metrics_registry("metrics_registry") {
+  project_name = "fuchsia_system_metrics"
+  namespace = "fuchsia_system_metrics"
+  generate_cc = true
+  generate_binarypb = false
+}
+
+source_set("system_metrics_daemon_lib") {
+  sources = [
+    "system_metrics_daemon.cc",
+    "system_metrics_daemon.h",
+  ]
+
+  public_deps = [
+    ":metrics_registry_cc",
+    "//garnet/bin/cobalt/utils:clock",
+    "//garnet/public/lib/component/cpp",
+    "//garnet/public/lib/fsl",
+    "//zircon/public/fidl/fuchsia-cobalt",
+    "//zircon/public/lib/async-loop-cpp",
+  ]
+}
+
 executable("system-metrics") {
   output_name = "cobalt_system_metrics_bin"
 
@@ -14,21 +37,13 @@
   ]
 
   deps = [
+    ":system_metrics_daemon_lib",
     "//garnet/public/lib/component/cpp",
-    "//garnet/public/lib/fsl",
-    "//zircon/public/fidl/fuchsia-cobalt",
-    "//zircon/public/fidl/fuchsia-sysinfo:fuchsia-sysinfo_c",
-    "//zircon/public/lib/async-loop-cpp",
   ]
 }
 
-metrics_registry("cobalt_system_metrics_registry") {
-  project_id = 102
-}
-
 package("cobalt_system_metrics") {
   deps = [
-    ":cobalt_system_metrics_registry",
     ":system-metrics",
   ]
 
@@ -39,14 +54,25 @@
     },
   ]
 
-  resources = [
-    {
-      path = rebase_path(get_label_info(":cobalt_system_metrics_registry",
-                                        "target_gen_dir") +
-                         "/cobalt_system_metrics_registry.pb")
-      dest = "cobalt_system_metrics_registry.pb"
-    },
+  binary = "cobalt_system_metrics_bin"
+}
+
+executable("cobalt_system_metrics_unittests") {
+  testonly = true
+
+  sources = [
+    "system_metrics_daemon_test.cc",
   ]
 
-  binary = "cobalt_system_metrics_bin"
+  deps = [
+    ":system_metrics_daemon_lib",
+    "//garnet/bin/cobalt/testing:fake_clock_lib",
+    "//garnet/bin/cobalt/testing:fake_logger_lib",
+    "//garnet/bin/cobalt/utils:clock",
+    "//garnet/public/lib/component/cpp/testing",
+    "//garnet/public/lib/fsl",
+    "//garnet/public/lib/fxl/test:gtest_main",
+    "//garnet/public/lib/gtest",
+    "//zircon/public/fidl/fuchsia-cobalt",
+  ]
 }
diff --git a/garnet/bin/cobalt/system-metrics/system_metrics_daemon.cc b/garnet/bin/cobalt/system-metrics/system_metrics_daemon.cc
new file mode 100644
index 0000000..64ecc56
--- /dev/null
+++ b/garnet/bin/cobalt/system-metrics/system_metrics_daemon.cc
@@ -0,0 +1,228 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// The cobalt system metrics collection daemon uses cobalt to log system metrics
+// on a regular basis.
+#include "garnet/bin/cobalt/system-metrics/system_metrics_daemon.h"
+
+#include <fcntl.h>
+#include <chrono>
+#include <memory>
+#include <thread>
+
+#include <fuchsia/cobalt/cpp/fidl.h>
+#include <fuchsia/sysinfo/c/fidl.h>
+#include <lib/fdio/util.h>
+#include <lib/fxl/logging.h>
+#include <lib/zx/resource.h>
+#include <zircon/device/device.h>
+
+#include "garnet/bin/cobalt/system-metrics/metrics_registry.cb.h"
+#include "garnet/bin/cobalt/utils/clock.h"
+#include "garnet/bin/cobalt/utils/status_utils.h"
+#include "lib/fxl/logging.h"
+
+using cobalt::StatusToString;
+using fuchsia::cobalt::Logger_Sync;
+using fuchsia_system_metrics::FuchsiaLifetimeEventsEventCode;
+using fuchsia_system_metrics::FuchsiaUpPingEventCode;
+using std::chrono::steady_clock;
+
+SystemMetricsDaemon::SystemMetricsDaemon(async_dispatcher_t* dispatcher,
+                                         component::StartupContext* context)
+    : SystemMetricsDaemon(
+          dispatcher, context, nullptr,
+          std::unique_ptr<cobalt::SteadyClock>(new cobalt::RealSteadyClock())) {
+  InitializeLogger();
+}
+
+SystemMetricsDaemon::SystemMetricsDaemon(
+    async_dispatcher_t* dispatcher, component::StartupContext* context,
+    fuchsia::cobalt::Logger_Sync* logger,
+    std::unique_ptr<cobalt::SteadyClock> clock)
+    : dispatcher_(dispatcher),
+      context_(context),
+      logger_(logger),
+      start_time_(clock->Now()),
+      clock_(std::move(clock)) {}
+
+void SystemMetricsDaemon::Work() {
+  // We keep gathering metrics until this process is terminated.
+  std::chrono::seconds seconds_to_sleep = LogMetrics();
+  async::PostDelayedTask(
+      dispatcher_, [this]() { Work(); }, zx::sec(seconds_to_sleep.count() + 5));
+}
+
+std::chrono::seconds SystemMetricsDaemon::LogMetrics() {
+  auto now = clock_->Now();
+  // Note(rudominer) We are using the startime of the SystemMetricsDaemon
+  // as a proxy for the system start time. This is fine as long as we don't
+  //start seeing systematic restarts of the SystemMetricsDaemon. If that
+  // starts happening we should look into how to capture actual boot time.
+  auto uptime =
+      std::chrono::duration_cast<std::chrono::seconds>(now - start_time_);
+
+  std::chrono::seconds seconds_to_sleep = LogFuchsiaUpPing(uptime);
+  seconds_to_sleep = std::min(seconds_to_sleep, LogFuchsiaLifetimeEvents());
+  return seconds_to_sleep;
+}
+
+std::chrono::seconds SystemMetricsDaemon::LogFuchsiaUpPing(
+    std::chrono::seconds uptime) {
+  // We always log that we are |Up|.
+  // If |uptime| is at least one minute we log that we are |UpOneMinute|.
+  // If |uptime| is at least ten minutes we log that we are |UpTenMinutes|.
+  // If |uptime| is at least one hour we log that we are |UpOneHour|.
+  // If |uptime| is at least 12 hours we log that we are |UpTwelveHours|.
+  // If |uptime| is at least 24 hours we log that we are |UpOneDay|.
+  //
+  // To understand the logic of this function it is important to note that
+  // the events we are logging are intended to take advantage of Cobalt's
+  // local aggregation feature. Thus, for example, although we log the
+  // |Up| event many times throughout a calendar day, only a single
+  // Observation per day will be sent from the device to the Cobalt backend
+  // indicating that this device was "Up" during the day.
+
+  if (!logger_) {
+    FXL_LOG(ERROR)
+        << "Cobalt SystemMetricsDaemon: No logger present. Reconnecting...";
+    InitializeLogger();
+    // Something went wrong. Pause for 5 minutes.
+    return std::chrono::minutes(5);
+  }
+
+  fuchsia::cobalt::Status status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  // Always log that we are "Up".
+  logger_->LogEvent(fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                    FuchsiaUpPingEventCode::Up, &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: LogEvent() returned status="
+                   << StatusToString(status);
+  }
+  if (uptime < std::chrono::minutes(1)) {
+    // If we have been up for less than a minute, come back here after it
+    // has been a minute.
+    return std::chrono::minutes(1) - uptime;
+  }
+  // Log UpOneMinute
+  status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  logger_->LogEvent(fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                    FuchsiaUpPingEventCode::UpOneMinute, &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: LogEvent() returned status="
+                   << StatusToString(status);
+  }
+  if (uptime < std::chrono::minutes(10)) {
+    // If we have been up for less than 10 minutes, come back here after it
+    // has been 10 minutes.
+    return std::chrono::minutes(10) - uptime;
+  }
+  // Log UpTenMinutes
+  status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  logger_->LogEvent(fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                    FuchsiaUpPingEventCode::UpTenMinutes, &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: LogEvent() returned status="
+                   << StatusToString(status);
+  }
+  if (uptime < std::chrono::hours(1)) {
+    // If we have been up for less than an hour, come back here after it has
+    // has been an hour.
+    return std::chrono::hours(1) - uptime;
+  }
+  // Log UpOneHour
+  status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  logger_->LogEvent(fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                    FuchsiaUpPingEventCode::UpOneHour, &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: LogEvent() returned status="
+                   << StatusToString(status);
+  }
+  if (uptime < std::chrono::hours(12)) {
+    // If we have been up for less than 12 hours, come back here after *one*
+    // hour. Notice this time we don't wait 12 hours to come back. The reason
+    // is that it may be close to the end of the day. When the new day starts
+    // we want to come back in a reasonable amount of time (we consider
+    // one hour to be reasonable) so that we can log the earlier events
+    // in the new day.
+    return std::chrono::hours(1);
+  }
+  // Log UpTwelveHours.
+  status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  logger_->LogEvent(fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                    FuchsiaUpPingEventCode::UpTwelveHours, &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: LogEvent() returned status="
+                   << StatusToString(status);
+  }
+  if (uptime < std::chrono::hours(24)) {
+    // As above, come back in one hour.
+    return std::chrono::hours(1);
+  }
+  // Log UpOneDay.
+  status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  logger_->LogEvent(fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                    FuchsiaUpPingEventCode::UpOneDay, &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: LogEvent() returned status="
+                   << StatusToString(status);
+  }
+  // As above, come back in one hour.
+  return std::chrono::hours(1);
+}
+
+std::chrono::seconds SystemMetricsDaemon::LogFuchsiaLifetimeEvents() {
+  if (!logger_) {
+    FXL_LOG(ERROR)
+        << "Cobalt SystemMetricsDaemon: No logger present. Reconnecting...";
+    InitializeLogger();
+    // Something went wrong. Pause for 5 minutes.
+    return std::chrono::minutes(5);
+  }
+
+  fuchsia::cobalt::Status status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  if (!boot_reported_) {
+    logger_->LogEvent(fuchsia_system_metrics::kFuchsiaLifetimeEventsMetricId,
+                      FuchsiaLifetimeEventsEventCode::Boot, &status);
+    if (status != fuchsia::cobalt::Status::OK) {
+      FXL_LOG(ERROR)
+          << "Cobalt SystemMetricsDaemon: LogEvent() returned status="
+          << StatusToString(status);
+    } else {
+      boot_reported_ = true;
+    }
+  }
+  return std::chrono::seconds::max();
+}
+
+void SystemMetricsDaemon::InitializeLogger() {
+  fuchsia::cobalt::Status status = fuchsia::cobalt::Status::INTERNAL_ERROR;
+  // Create a Cobalt Logger. The project name is the one we specified in the
+  // Cobalt metrics registry. We specify that our release stage is DOGFOOD.
+  // This means we are not allowed to use any metrics declared as DEBUG
+  // or FISHFOOD.
+  static const char kProjectName[] = "fuchsia_system_metrics";
+  // Connect to the cobalt fidl service provided by the environment.
+  context_->ConnectToEnvironmentService(factory_.NewRequest());
+  if (!factory_) {
+    FXL_LOG(ERROR)
+        << "Cobalt SystemMetricsDaemon: Unable to get LoggerFactory.";
+    return;
+  }
+
+  factory_->CreateLoggerFromProjectName(
+      kProjectName, fuchsia::cobalt::ReleaseStage::DOGFOOD,
+      logger_fidl_proxy_.NewRequest(), &status);
+  if (status != fuchsia::cobalt::Status::OK) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: Unable to get Logger from "
+                      "factory. Status="
+                   << StatusToString(status);
+    return;
+  }
+  logger_ = logger_fidl_proxy_.get();
+  if (!logger_) {
+    FXL_LOG(ERROR) << "Cobalt SystemMetricsDaemon: Unable to get Logger from "
+                      "factory.";
+  }
+}
diff --git a/garnet/bin/cobalt/system-metrics/system_metrics_daemon.h b/garnet/bin/cobalt/system-metrics/system_metrics_daemon.h
new file mode 100644
index 0000000..ed3eaf3
--- /dev/null
+++ b/garnet/bin/cobalt/system-metrics/system_metrics_daemon.h
@@ -0,0 +1,98 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// The cobalt system metrics collection daemon uses cobalt to log system metrics
+// on a regular basis.
+
+#ifndef GARNET_BIN_COBALT_SYSTEM_METRICS_SYSTEM_METRICS_DAEMON_H_
+#define GARNET_BIN_COBALT_SYSTEM_METRICS_SYSTEM_METRICS_DAEMON_H_
+
+#include <chrono>
+#include <memory>
+#include <thread>
+
+#include <fuchsia/cobalt/cpp/fidl.h>
+#include <lib/async/dispatcher.h>
+#include <lib/component/cpp/startup_context.h>
+
+#include "garnet/bin/cobalt/utils/clock.h"
+
+// A daemon to send system metrics to Cobalt.
+//
+// Usage:
+//
+// async::Loop loop(&kAsyncLoopConfigAttachToThread);
+// std::unique_ptr<component::StartupContext> context(
+//     component::StartupContext::CreateFromStartupInfo());
+// SystemMetricsDaemon daemon(loop.dispatcher(), context.get());
+// daemon.Work();
+// loop.Run();
+class SystemMetricsDaemon {
+ public:
+  // Constructor
+  //
+  // |dispatcher|. This is used to schedule future work.
+  //
+  // |context|. The Cobalt LoggerFactory interface is fetched from this context.
+  SystemMetricsDaemon(async_dispatcher_t* dispatcher,
+                      component::StartupContext* context);
+
+  // Performs one round of work, depending on the current time relative to when
+  // this class was constructed, and then uses the |dispatcher| passed to the
+  // constructor to schedule the next round of work.
+  void Work();
+
+ private:
+  friend class SystemMetricsDaemonTest;
+
+  // This private constructor is intended for use in tests. |context| may
+  // be null because InitializeLogger() will not be invoked. Instead,
+  // pass a non-null |logger| which may be a local mock that does not use FIDL.
+  SystemMetricsDaemon(async_dispatcher_t* dispatcher,
+                      component::StartupContext* context,
+                      fuchsia::cobalt::Logger_Sync* logger,
+                      std::unique_ptr<cobalt::SteadyClock> clock);
+
+  void InitializeLogger();
+
+  // Logs one or more events depending on how long the device has been
+  // up.
+  //
+  // Returns the amount of time before this method needs to be invoked again.
+  std::chrono::seconds LogMetrics();
+
+  // Logs one or more UpPing events depending on how long the device has been
+  // up.
+  //
+  // |uptime| An estimate of how long since device boot time.
+  //
+  // First the "Up" event is logged indicating only that the device is up.
+  //
+  // If the device has been up for at least a minute then "UpOneMinute" is also
+  // logged.
+  //
+  // If the device has been up for at least 10 minutes, then "UpTenMinutes" is
+  // also logged. Etc.
+  //
+  // Returns the amount of time before this method needs to be invoked again.
+  std::chrono::seconds LogFuchsiaUpPing(std::chrono::seconds uptime);
+
+  // Logs one FuchsiaLifetimeEvent event of type "Boot" the first time it
+  // is invoked and does nothing on subsequent invocations.
+  //
+  // Returns the amount of time before this method needs to be invoked again.
+  // Currently returns std::chrono::seconds::max().
+  std::chrono::seconds LogFuchsiaLifetimeEvents();
+
+  bool boot_reported_ = false;
+  async_dispatcher_t* const dispatcher_;
+  component::StartupContext* context_;
+  fuchsia::cobalt::LoggerFactorySyncPtr factory_;
+  fuchsia::cobalt::LoggerSyncPtr logger_fidl_proxy_;
+  fuchsia::cobalt::Logger_Sync* logger_;
+  std::chrono::steady_clock::time_point start_time_;
+  std::unique_ptr<cobalt::SteadyClock> clock_;
+};
+
+#endif  // GARNET_BIN_COBALT_SYSTEM_METRICS_SYSTEM_METRICS_DAEMON_H_
\ No newline at end of file
diff --git a/garnet/bin/cobalt/system-metrics/system_metrics_daemon_test.cc b/garnet/bin/cobalt/system-metrics/system_metrics_daemon_test.cc
new file mode 100644
index 0000000..e680747
--- /dev/null
+++ b/garnet/bin/cobalt/system-metrics/system_metrics_daemon_test.cc
@@ -0,0 +1,377 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "garnet/bin/cobalt/system-metrics/system_metrics_daemon.h"
+
+#include <future>
+
+#include <fuchsia/cobalt/cpp/fidl.h>
+#include <lib/component/cpp/testing/test_with_context.h>
+#include <lib/gtest/test_loop_fixture.h>
+
+#include "garnet/bin/cobalt/system-metrics/metrics_registry.cb.h"
+#include "garnet/bin/cobalt/testing/fake_clock.h"
+#include "garnet/bin/cobalt/testing/fake_logger.h"
+#include "garnet/bin/cobalt/utils/clock.h"
+#include "gtest/gtest.h"
+
+using cobalt::FakeLogger_Sync;
+using cobalt::FakeSteadyClock;
+using cobalt::LogMethod;
+using fuchsia_system_metrics::FuchsiaLifetimeEventsEventCode;
+using fuchsia_system_metrics::FuchsiaUpPingEventCode;
+using std::chrono::hours;
+using std::chrono::minutes;
+using std::chrono::seconds;
+
+class SystemMetricsDaemonTest : public component::testing::TestWithContext {
+ public:
+  // Note that we first save an unprotected pointer in fake_clock_ and then
+  // give ownership of the pointer to daemon_.
+  SystemMetricsDaemonTest()
+      : fake_clock_(new FakeSteadyClock()),
+        daemon_(new SystemMetricsDaemon(
+            dispatcher(), nullptr, &fake_logger_,
+            std::unique_ptr<cobalt::SteadyClock>(fake_clock_))) {}
+
+  seconds LogFuchsiaUpPing(seconds uptime) {
+    return daemon_->LogFuchsiaUpPing(uptime);
+  }
+
+  seconds LogFuchsiaLifetimeEvents() {
+    return daemon_->LogFuchsiaLifetimeEvents();
+  }
+
+  seconds LogMetrics() { return daemon_->LogMetrics(); }
+
+  void CheckValues(LogMethod expected_log_method_invoked,
+                   size_t expected_call_count, uint32_t expected_metric_id,
+                   uint32_t expected_last_event_code) {
+    EXPECT_EQ(expected_log_method_invoked,
+              fake_logger_.last_log_method_invoked());
+    EXPECT_EQ(expected_call_count, fake_logger_.call_count());
+    EXPECT_EQ(expected_metric_id, fake_logger_.last_metric_id());
+    EXPECT_EQ(expected_last_event_code, fake_logger_.last_event_code());
+  }
+
+  void DoFuchsiaUpPingTest(seconds now_seconds, seconds expected_sleep_seconds,
+                           size_t expected_call_count,
+                           uint32_t expected_last_event_code) {
+    fake_logger_.reset();
+    EXPECT_EQ(expected_sleep_seconds.count(),
+              LogFuchsiaUpPing(now_seconds).count());
+    CheckValues(cobalt::kLogEvent, expected_call_count,
+                fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                expected_last_event_code);
+  }
+
+  void DoLogMetricsTest(seconds increment_seconds,
+                        seconds expected_sleep_seconds,
+                        size_t expected_call_count,
+                        uint32_t expected_last_metric_id,
+                        uint32_t expected_last_event_code) {
+    fake_logger_.reset();
+    fake_clock_->Increment(increment_seconds);
+    EXPECT_EQ(expected_sleep_seconds.count(), LogMetrics().count());
+    CheckValues(cobalt::kLogEvent, expected_call_count, expected_last_metric_id,
+                expected_last_event_code);
+  }
+
+  // This method is used by the test of the method Work(). 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 Work() was executed and did the expected thing.
+  void AdvanceTimeAndCheck(seconds advance_time_seconds,
+                           size_t expected_call_count,
+                           uint32_t expected_metric_id,
+                           uint32_t expected_last_event_code) {
+    bool expected_activity = (expected_call_count != 0);
+    fake_clock_->Increment(advance_time_seconds);
+    EXPECT_EQ(expected_activity,
+              RunLoopFor(zx::sec(advance_time_seconds.count())));
+    LogMethod expected_log_method_invoked =
+        (expected_call_count == 0 ? cobalt::kOther : cobalt::kLogEvent);
+    CheckValues(expected_log_method_invoked, expected_call_count,
+                expected_metric_id, expected_last_event_code);
+    fake_logger_.reset();
+  }
+
+ protected:
+  FakeSteadyClock* fake_clock_;
+  FakeLogger_Sync fake_logger_;
+  std::unique_ptr<SystemMetricsDaemon> daemon_;
+};
+
+// Tests the method LogFuchsiaUpPing(). Uses a local FakeLogger_Sync and
+// does not use FIDL. Does not use the message loop.
+TEST_F(SystemMetricsDaemonTest, LogFuchsiaUpPing) {
+  // If we were just booted, expect 1 log event of type "Up" and a return
+  // value of 60 seconds.
+  DoFuchsiaUpPingTest(seconds(0), seconds(60), 1, FuchsiaUpPingEventCode::Up);
+
+  // If we've been up for 10 seconds, expect 1 log event of type "Up" and a
+  // return value of 50 seconds.
+  DoFuchsiaUpPingTest(seconds(10), seconds(50), 1, FuchsiaUpPingEventCode::Up);
+
+  // If we've been up for 59 seconds, expect 1 log event of type "Up" and a
+  // return value of 1 second.
+  DoFuchsiaUpPingTest(seconds(59), seconds(1), 1, FuchsiaUpPingEventCode::Up);
+
+  // If we've been up for 60 seconds, expect 2 log events, the second one
+  // being of type UpOneMinute, and a return value of 9 minutes.
+  DoFuchsiaUpPingTest(seconds(60), minutes(9), 2,
+                      FuchsiaUpPingEventCode::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.
+  DoFuchsiaUpPingTest(seconds(61), minutes(9) - seconds(1), 2,
+                      FuchsiaUpPingEventCode::UpOneMinute);
+
+  // If we've been up for 10 minutes minus 1 second, expect 2 log events, the
+  // second one being of type UpOneMinute, and a return value of 1 second.
+  DoFuchsiaUpPingTest(minutes(10) - seconds(1), seconds(1), 2,
+                      FuchsiaUpPingEventCode::UpOneMinute);
+
+  // If we've been up for 10 minutes, expect 3 log events, the
+  // last one being of type UpTenMinutes, and a return value of 50 minutes.
+  DoFuchsiaUpPingTest(minutes(10), minutes(50), 3,
+                      FuchsiaUpPingEventCode::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.
+  DoFuchsiaUpPingTest(minutes(10) + seconds(1), minutes(50) - seconds(1), 3,
+                      FuchsiaUpPingEventCode::UpTenMinutes);
+
+  // If we've been up for 59 minutes, expect 3 log events, the last one being
+  // of type UpTenMinutes, and a return value of 1 minute
+  DoFuchsiaUpPingTest(minutes(59), minutes(1), 3,
+                      FuchsiaUpPingEventCode::UpTenMinutes);
+
+  // If we've been up for 60 minutes, expect 4 log events, the last one being
+  // of type UpOneHour, and a return value of 1 hour
+  DoFuchsiaUpPingTest(minutes(60), hours(1), 4,
+                      FuchsiaUpPingEventCode::UpOneHour);
+
+  // If we've been up for 61 minutes, expect 4 log events, the last one being
+  // of type UpOneHour, and a return value of 1 hour
+  DoFuchsiaUpPingTest(minutes(61), hours(1), 4,
+                      FuchsiaUpPingEventCode::UpOneHour);
+
+  // If we've been up for 11 hours, expect 4 log events, the last one being
+  // of type UpOneHour, and a return value of 1 hour
+  DoFuchsiaUpPingTest(hours(11), hours(1), 4,
+                      FuchsiaUpPingEventCode::UpOneHour);
+
+  // If we've been up for 12 hours, expect 5 log events, the last one being
+  // of type UpTwelveHours, and a return value of 1 hour
+  DoFuchsiaUpPingTest(hours(12), hours(1), 5,
+                      FuchsiaUpPingEventCode::UpTwelveHours);
+
+  // If we've been up for 13 hours, expect 5 log events, the last one being
+  // of type UpTwelveHours, and a return value of 1 hour
+  DoFuchsiaUpPingTest(hours(13), hours(1), 5,
+                      FuchsiaUpPingEventCode::UpTwelveHours);
+
+  // If we've been up for 23 hours, expect 5 log events, the last one being
+  // of type UpTwelveHours, and a return value of 1 hour
+  DoFuchsiaUpPingTest(hours(23), hours(1), 5,
+                      FuchsiaUpPingEventCode::UpTwelveHours);
+
+  // If we've been up for 24 hours, expect 6 log events, the last one being
+  // of type UpOneDay, and a return value of 1 hour
+  DoFuchsiaUpPingTest(hours(24), hours(1), 6, FuchsiaUpPingEventCode::UpOneDay);
+
+  // If we've been up for 25 hours, expect 6 log events, the last one being
+  // of type UpOneDay, and a return value of 1 hour
+  DoFuchsiaUpPingTest(hours(25), hours(1), 6, FuchsiaUpPingEventCode::UpOneDay);
+
+  // If we've been up for 250 hours, expect 6 log events, the last one being
+  // of type UpOneDay, and a return value of 1 hour
+  DoFuchsiaUpPingTest(hours(250), hours(1), 6,
+                      FuchsiaUpPingEventCode::UpOneDay);
+}
+
+// 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().
+  EXPECT_EQ(seconds::max(), LogFuchsiaLifetimeEvents());
+  CheckValues(cobalt::kLogEvent, 1,
+              fuchsia_system_metrics::kFuchsiaLifetimeEventsMetricId,
+              FuchsiaLifetimeEventsEventCode::Boot);
+
+  fake_logger_.reset();
+  // 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 LogMetrics(). Uses a local FakeLogger_Sync and
+// does not use FIDL. Does not use the message loop.
+TEST_F(SystemMetricsDaemonTest, LogMetrics) {
+  // 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.
+  DoLogMetricsTest(seconds(1), seconds(59), 2,
+                   fuchsia_system_metrics::kFuchsiaLifetimeEventsMetricId,
+                   FuchsiaLifetimeEventsEventCode::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.
+  DoLogMetricsTest(seconds(59), minutes(9), 2,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(minutes(9) - seconds(1), seconds(1), 2,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::UpOneMinute);
+
+  // 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.
+  DoLogMetricsTest(seconds(2), minutes(50) - seconds(1), 3,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::UpTenMinutes);
+
+  // 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.
+  DoLogMetricsTest(minutes(50) - seconds(1), hours(1), 4,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::UpOneHour);
+
+  // 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.
+  DoLogMetricsTest(hours(1), hours(1), 4,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(1), hours(1), 4,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(1), hours(1), 4,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(1), hours(1), 4,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(1), hours(1), 4,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(6), hours(1), 5,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(1), hours(1), 5,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(1), hours(1), 5,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(10), hours(1), 6,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::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.
+  DoLogMetricsTest(hours(1), hours(1), 6,
+                   fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                   FuchsiaUpPingEventCode::UpOneDay);
+}
+
+// Tests the method Work(). 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, Work) {
+  // 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.
+  daemon_->Work();
+
+  // The initial two events should have been logged, the second of which is
+  // |Boot|.
+  CheckValues(cobalt::kLogEvent, 2,
+              fuchsia_system_metrics::kFuchsiaLifetimeEventsMetricId,
+              FuchsiaLifetimeEventsEventCode::Boot);
+  fake_logger_.reset();
+
+  // 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 Work() added a 5 second buffer to the next
+  // scheduled run time.
+  AdvanceTimeAndCheck(seconds(30), 0, -1, -1);
+
+  // Advance the clock by 5 seconds to t=65s. Now expect the second batch
+  // of work to occur. This consists of two events the second of which is
+  // |UpOneMinute|. The third batch of work should be schedule for
+  // t = 10m + 5s.
+  AdvanceTimeAndCheck(seconds(5), 2,
+                      fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                      FuchsiaUpPingEventCode::UpOneMinute);
+
+  // Advance the clock to t=10m. Nothing should have happened because the
+  // previous round added a 5s buffer.
+  AdvanceTimeAndCheck(minutes(10) - seconds(65), 0, -1, -1);
+
+  // Advance the clock 5 s to t=10m + 5s. Now expect the third batch of
+  // work to occur. This consists of three events the second of which is
+  // |UpTenMinutes|. The fourth batch of work should be scheduled for
+  // t = 1 hour + 5s.
+  AdvanceTimeAndCheck(seconds(5), 3,
+                      fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                      FuchsiaUpPingEventCode::UpTenMinutes);
+
+  // Advance the clock to t=1h. Nothing should have happened because the
+  // previous round added a 5s buffer.
+  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|.
+  AdvanceTimeAndCheck(seconds(5), 4,
+                      fuchsia_system_metrics::kFuchsiaUpPingMetricId,
+                      FuchsiaUpPingEventCode::UpOneHour);
+}
diff --git a/garnet/bin/cobalt/system-metrics/system_metrics_main.cc b/garnet/bin/cobalt/system-metrics/system_metrics_main.cc
index 214880e..21452c6 100644
--- a/garnet/bin/cobalt/system-metrics/system_metrics_main.cc
+++ b/garnet/bin/cobalt/system-metrics/system_metrics_main.cc
@@ -5,216 +5,21 @@
 // The cobalt system metrics collection daemon uses cobalt to log system metrics
 // on a regular basis.
 
-#include <fcntl.h>
-#include <chrono>
 #include <memory>
-#include <thread>
 
-#include <fuchsia/cobalt/cpp/fidl.h>
-#include <fuchsia/sysinfo/c/fidl.h>
 #include <lib/async-loop/cpp/loop.h>
-#include <lib/fdio/util.h>
-#include <lib/zx/resource.h>
-#include <zircon/device/device.h>
+#include <lib/component/cpp/startup_context.h>
 
-#include "lib/component/cpp/startup_context.h"
-#include "lib/fsl/vmo/file.h"
-#include "lib/fxl/logging.h"
-
-constexpr char kConfigBinProtoPath[] =
-    "/pkg/data/cobalt_system_metrics_registry.pb";
-const uint32_t kUptimeMetricId = 1;
-const uint32_t kMemoryUsageMetricId = 3;
-const unsigned int kIntervalMinutes = 1;
-
-// Gets the root resource which is needed in order to access a variety of system
-// metrics, including memory usage data.
-zx_status_t get_root_resource(zx::resource* resource_out) {
-  static constexpr char kResourcePath[] = "/dev/misc/sysinfo";
-  int fd = open(kResourcePath, O_RDWR);
-  if (fd < 0) {
-    FXL_LOG(ERROR) << "Failed to open " << kResourcePath << " with "
-                   << strerror(errno);
-    return ZX_ERR_IO;
-  }
-
-  zx::channel channel;
-  zx_status_t status =
-      fdio_get_service_handle(fd, channel.reset_and_get_address());
-  if (status != ZX_OK) {
-    return status;
-  }
-
-  zx_handle_t raw_resource;
-  zx_status_t fidl_status = fuchsia_sysinfo_DeviceGetRootResource(
-      channel.get(), &status, &raw_resource);
-  if (fidl_status != ZX_OK) {
-    FXL_LOG(ERROR) << "Failed to get root resource: " << fidl_status;
-    return fidl_status;
-  } else if (status != ZX_OK) {
-    FXL_LOG(ERROR) << "Failed to get root resource: " << status;
-    return status;
-  }
-  resource_out->reset(raw_resource);
-  return ZX_OK;
-}
-
-std::string StatusToString(fuchsia::cobalt::Status status) {
-  switch (status) {
-    case fuchsia::cobalt::Status::OK:
-      return "OK";
-    case fuchsia::cobalt::Status::INVALID_ARGUMENTS:
-      return "INVALID_ARGUMENTS";
-    case fuchsia::cobalt::Status::EVENT_TOO_BIG:
-      return "EVENT_TOO_BIG";
-    case fuchsia::cobalt::Status::BUFFER_FULL:
-      return "BUFFER_FULL";
-    case fuchsia::cobalt::Status::INTERNAL_ERROR:
-      return "INTERNAL_ERROR";
-  }
-};
-
-// Loads the CobaltConfig proto for this project and writes it to a VMO.
-fuchsia::cobalt::ProjectProfile LoadCobaltConfig() {
-  fsl::SizedVmo config_vmo;
-  bool success = fsl::VmoFromFilename(kConfigBinProtoPath, &config_vmo);
-  FXL_CHECK(success) << "Could not read Cobalt config file into VMO";
-
-  fuchsia::cobalt::ProjectProfile profile;
-  profile.config = std::move(config_vmo).ToTransport();
-  return profile;
-}
-
-class SystemMetricsApp {
- public:
-  // tick_interval_minutes is the number of minutes to sleep in between calls to
-  // the GatherMetrics method.
-  SystemMetricsApp(unsigned int tick_interval_minutes)
-      : context_(component::StartupContext::CreateFromStartupInfo()),
-        start_time_(std::chrono::steady_clock::now()),
-        tick_interval_(tick_interval_minutes) {}
-
-  // Main is invoked to initialize the app and start the metric gathering loop.
-  void Main(async::Loop* loop);
-
- private:
-  void ConnectToEnvironmentService();
-
-  void GatherMetrics();
-
-  // LogUptime returns the status returned by its call to Add*Observation.
-  fuchsia::cobalt::Status LogUptime(std::chrono::minutes uptime_minutes);
-
-  // LogMemoryUsage returns the status OK if everything went fine, or the
-  // logging was skipped due to scheduling, INTERNAL_ERROR if it was somehow
-  // unable to get the memory usage information and whatever was returned by
-  // Add*Observation otherwise.
-  fuchsia::cobalt::Status LogMemoryUsage(std::chrono::minutes uptime_minutes);
-
- private:
-  std::unique_ptr<component::StartupContext> context_;
-  fuchsia::cobalt::LoggerSyncPtr logger_;
-  std::chrono::steady_clock::time_point start_time_;
-  std::chrono::minutes tick_interval_;
-  // We don't log every minute of uptime. We log in exponentially-growing
-  // increments. This keeps track of which minute should be logged.
-  int next_uptime_bucket_ = 0;
-
-  // We log memory usage no more than once every 5 minutes.
-  int next_log_memory_usage_ = 0;
-};
-
-void SystemMetricsApp::GatherMetrics() {
-  auto now = std::chrono::steady_clock::now();
-  auto uptime = now - start_time_;
-  auto uptime_minutes =
-      std::chrono::duration_cast<std::chrono::minutes>(uptime);
-
-  LogUptime(uptime_minutes);
-  LogMemoryUsage(uptime_minutes);
-}
-
-fuchsia::cobalt::Status SystemMetricsApp::LogUptime(
-    std::chrono::minutes uptime_minutes) {
-  while (next_uptime_bucket_ <= uptime_minutes.count()) {
-    fuchsia::cobalt::Status status = fuchsia::cobalt::Status::INTERNAL_ERROR;
-
-    logger_->LogElapsedTime(kUptimeMetricId, 0, "", next_uptime_bucket_,
-                            &status);
-    // If we failed to send an observation, we stop gathering metrics for up to
-    // one minute.
-    if (status != fuchsia::cobalt::Status::OK) {
-      FXL_LOG(ERROR) << "LogElapsedTime() => " << StatusToString(status);
-      return status;
-    }
-
-    if (next_uptime_bucket_ == 0) {
-      next_uptime_bucket_ = 1;
-    } else {
-      next_uptime_bucket_ *= 2;
-    }
-  }
-
-  return fuchsia::cobalt::Status::OK;
-}
-
-fuchsia::cobalt::Status SystemMetricsApp::LogMemoryUsage(
-    std::chrono::minutes uptime_minutes) {
-  if (uptime_minutes.count() < next_log_memory_usage_) {
-    return fuchsia::cobalt::Status::OK;
-  }
-
-  zx::resource root_resource;
-  zx_status_t status = get_root_resource(&root_resource);
-  if (status != ZX_OK) {
-    FXL_LOG(ERROR) << "get_root_resource failed!!!";
-    return fuchsia::cobalt::Status::INTERNAL_ERROR;
-  }
-
-  zx_info_kmem_stats_t stats;
-  status = zx_object_get_info(root_resource.get(), ZX_INFO_KMEM_STATS, &stats,
-                              sizeof(stats), NULL, NULL);
-  if (status != ZX_OK) {
-    FXL_LOG(ERROR) << "zx_object_get_info failed with " << status << ".";
-    return fuchsia::cobalt::Status::INTERNAL_ERROR;
-  }
-
-  auto cobalt_status = fuchsia::cobalt::Status::INTERNAL_ERROR;
-  logger_->LogMemoryUsage(kMemoryUsageMetricId, 0, "",
-                          stats.total_bytes - stats.free_bytes, &cobalt_status);
-  if (cobalt_status != fuchsia::cobalt::Status::OK) {
-    FXL_LOG(ERROR) << "LogMemoryUsage() => " << StatusToString(cobalt_status);
-    return cobalt_status;
-  }
-
-  // The next time to log is in 5 minutes.
-  next_log_memory_usage_ = uptime_minutes.count() + 5;
-  return fuchsia::cobalt::Status::OK;
-}
-
-void SystemMetricsApp::Main(async::Loop* loop) {
-  ConnectToEnvironmentService();
-  // We keep gathering metrics until this process is terminated.
-  for (;;) {
-    GatherMetrics();
-    loop->Run(zx::clock::get_monotonic() + zx::min(tick_interval_.count()));
-  }
-}
-
-void SystemMetricsApp::ConnectToEnvironmentService() {
-  // connect to the cobalt fidl service provided by the environment.
-  fuchsia::cobalt::LoggerFactorySyncPtr factory;
-  context_->ConnectToEnvironmentService(factory.NewRequest());
-
-  fuchsia::cobalt::Status status = fuchsia::cobalt::Status::INTERNAL_ERROR;
-  factory->CreateLogger(LoadCobaltConfig(), logger_.NewRequest(), &status);
-  FXL_CHECK(status == fuchsia::cobalt::Status::OK)
-      << "CreateLogger() => " << StatusToString(status);
-}
+#include "garnet/bin/cobalt/system-metrics/system_metrics_daemon.h"
 
 int main(int argc, const char** argv) {
   async::Loop loop(&kAsyncLoopConfigAttachToThread);
-  SystemMetricsApp app(kIntervalMinutes);
-  app.Main(&loop);
+  std::unique_ptr<component::StartupContext> context(
+      component::StartupContext::CreateFromStartupInfo());
+
+  // Create the SystemMetricsDaemon and start it.
+  SystemMetricsDaemon daemon(loop.dispatcher(), context.get());
+  daemon.Work();
+  loop.Run();
   return 0;
 }
diff --git a/garnet/bin/cobalt/testing/BUILD.gn b/garnet/bin/cobalt/testing/BUILD.gn
new file mode 100644
index 0000000..4b5aff6
--- /dev/null
+++ b/garnet/bin/cobalt/testing/BUILD.gn
@@ -0,0 +1,28 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/package.gni")
+
+source_set("fake_logger_lib") {
+  testonly = true
+  sources = [
+    "fake_logger.cc",
+    "fake_logger.h",
+  ]
+
+  public_deps = [
+    "//zircon/public/fidl/fuchsia-cobalt",
+  ]
+}
+
+source_set("fake_clock_lib") {
+  testonly = true
+  sources = [
+    "fake_clock.h",
+  ]
+
+  public_deps = [
+    "//garnet/bin/cobalt/utils:clock",
+  ]
+}
diff --git a/garnet/bin/cobalt/testing/fake_clock.h b/garnet/bin/cobalt/testing/fake_clock.h
new file mode 100644
index 0000000..ba41a4f
--- /dev/null
+++ b/garnet/bin/cobalt/testing/fake_clock.h
@@ -0,0 +1,31 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_COBALT_TESTING_FAKE_CLOCK_H_
+#define GARNET_BIN_COBALT_TESTING_FAKE_CLOCK_H_
+
+#include <chrono>
+
+#include "garnet/bin/cobalt/utils/clock.h"
+
+namespace cobalt {
+
+// An implementation of SteadyClock that returns a time that does not
+// increase with real time but only when Increment() is invoked. For use in
+// tests.
+class FakeSteadyClock : public SteadyClock {
+ public:
+  std::chrono::steady_clock::time_point Now() override { return now_; }
+
+  void Increment(std::chrono::seconds increment_seconds) {
+    now_ += increment_seconds;
+  }
+
+ private:
+  std::chrono::steady_clock::time_point now_ = std::chrono::steady_clock::now();
+};
+
+}  // namespace cobalt
+
+#endif  // GARNET_BIN_COBALT_TESTING_FAKE_CLOCK_H_
\ No newline at end of file
diff --git a/garnet/bin/cobalt/testing/fake_logger.cc b/garnet/bin/cobalt/testing/fake_logger.cc
new file mode 100644
index 0000000..6a9ee6d2
--- /dev/null
+++ b/garnet/bin/cobalt/testing/fake_logger.cc
@@ -0,0 +1,122 @@
+// Copyright 2019  The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "garnet/bin/cobalt/testing/fake_logger.h"
+
+using fuchsia::cobalt::CobaltEvent;
+using fuchsia::cobalt::CustomEventValue;
+using fuchsia::cobalt::HistogramBucket;
+using fuchsia::cobalt::Status;
+
+namespace cobalt {
+
+zx_status_t FakeLogger_Sync::LogEvent(uint32_t metric_id, uint32_t event_code,
+                                      Status* out_status) {
+  call_count_++;
+  last_log_method_invoked_ = kLogEvent;
+  last_metric_id_ = metric_id;
+  last_event_code_ = event_code;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogEventCount(uint32_t metric_id,
+                                           uint32_t event_code,
+                                           ::std::string component,
+                                           int64_t period_duration_micros,
+                                           int64_t count, Status* out_status) {
+  call_count_++;
+  last_log_method_invoked_ = kLogEventCount;
+  last_metric_id_ = metric_id;
+  last_event_code_ = event_code;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogElapsedTime(uint32_t metric_id,
+                                            uint32_t event_code,
+                                            ::std::string component,
+                                            int64_t elapsed_micros,
+                                            Status* out_status) {
+  call_count_++;
+  last_log_method_invoked_ = kLogElapsedTime;
+  last_metric_id_ = metric_id;
+  last_event_code_ = event_code;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogFrameRate(uint32_t metric_id,
+                                          uint32_t event_code,
+                                          ::std::string component, float fps,
+                                          Status* out_status) {
+  call_count_++;
+  last_log_method_invoked_ = kLogFrameRate;
+  last_metric_id_ = metric_id;
+  last_event_code_ = event_code;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogMemoryUsage(uint32_t metric_id,
+                                            uint32_t event_code,
+                                            ::std::string component,
+                                            int64_t bytes, Status* out_status) {
+  call_count_++;
+  last_log_method_invoked_ = kLogMemoryUsage;
+  last_metric_id_ = metric_id;
+  last_event_code_ = event_code;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogString(uint32_t metric_id, ::std::string s,
+                                       Status* out_status) {
+  call_count_++;
+  last_log_method_invoked_ = kLogString;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::StartTimer(uint32_t metric_id, uint32_t event_code,
+                                        ::std::string component,
+                                        ::std::string timer_id,
+                                        uint64_t timestamp, uint32_t timeout_s,
+                                        Status* out_status) {
+  call_count_++;
+  last_metric_id_ = metric_id;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::EndTimer(::std::string timer_id,
+                                      uint64_t timestamp, uint32_t timeout_s,
+                                      Status* out_status) {
+  call_count_++;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogIntHistogram(
+    uint32_t metric_id, uint32_t event_code, ::std::string component,
+    ::std::vector<HistogramBucket> histogram, Status* out_status) {
+  call_count_++;
+  last_metric_id_ = metric_id;
+  last_event_code_ = event_code;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogCustomEvent(
+    uint32_t metric_id, ::std::vector<CustomEventValue> event_values,
+    Status* out_status) {
+  last_metric_id_ = metric_id;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogCobaltEvent(CobaltEvent event,
+                                            Status* out_status) {
+  call_count_++;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+zx_status_t FakeLogger_Sync::LogCobaltEvents(::std::vector<CobaltEvent> events,
+                                             Status* out_status) {
+  call_count_++;
+  *out_status = Status::OK;
+  return ZX_OK;
+}
+
+}  // namespace cobalt
\ No newline at end of file
diff --git a/garnet/bin/cobalt/testing/fake_logger.h b/garnet/bin/cobalt/testing/fake_logger.h
new file mode 100644
index 0000000..dfc5e6d
--- /dev/null
+++ b/garnet/bin/cobalt/testing/fake_logger.h
@@ -0,0 +1,95 @@
+// Copyright 2019  The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_COBALT_TESTING_FAKE_LOGGER_H_
+#define GARNET_BIN_COBALT_TESTING_FAKE_LOGGER_H_
+
+#include <fuchsia/cobalt/cpp/fidl.h>
+
+namespace cobalt {
+
+enum LogMethod {
+  kOther = 0,
+  kLogEvent = 1,
+  kLogEventCount = 2,
+  kLogElapsedTime = 3,
+  kLogFrameRate = 4,
+  kLogMemoryUsage = 5,
+  kLogString = 6,
+  kLogCustomEvent = 7,
+};
+
+class FakeLogger_Sync : public fuchsia::cobalt::Logger_Sync {
+ public:
+  zx_status_t LogEvent(uint32_t metric_id, uint32_t event_code,
+                       fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogEventCount(uint32_t metric_id, uint32_t event_code,
+                            ::std::string component,
+                            int64_t period_duration_micros, int64_t count,
+                            fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogElapsedTime(uint32_t metric_id, uint32_t event_code,
+                             ::std::string component, int64_t elapsed_micros,
+                             fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogFrameRate(uint32_t metric_id, uint32_t event_code,
+                           ::std::string component, float fps,
+                           fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogMemoryUsage(uint32_t metric_id, uint32_t event_code,
+                             ::std::string component, int64_t bytes,
+                             fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogString(uint32_t metric_id, ::std::string s,
+                        fuchsia::cobalt::Status* out_status) override;
+  zx_status_t StartTimer(uint32_t metric_id, uint32_t event_code,
+                         ::std::string component, ::std::string timer_id,
+                         uint64_t timestamp, uint32_t timeout_s,
+                         fuchsia::cobalt::Status* out_status) override;
+  zx_status_t EndTimer(::std::string timer_id, uint64_t timestamp,
+                       uint32_t timeout_s,
+                       fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogIntHistogram(
+      uint32_t metric_id, uint32_t event_code, ::std::string component,
+      ::std::vector<fuchsia::cobalt::HistogramBucket> histogram,
+      fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogCustomEvent(
+      uint32_t metric_id,
+      ::std::vector<fuchsia::cobalt::CustomEventValue> event_values,
+      fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogCobaltEvent(fuchsia::cobalt::CobaltEvent event,
+                             fuchsia::cobalt::Status* out_status) override;
+  zx_status_t LogCobaltEvents(
+      ::std::vector<fuchsia::cobalt::CobaltEvent> events,
+      fuchsia::cobalt::Status* out_status) override;
+
+  uint32_t last_metric_id() { return last_metric_id_; }
+
+  void reset_last_metric_id() { last_metric_id_ = -1; }
+
+  uint32_t last_event_code() { return last_event_code_; }
+
+  void reset_last_event_code() { last_event_code_ = -1; }
+
+  LogMethod last_log_method_invoked() { return last_log_method_invoked_; }
+
+  void reset_last_log_method_invoked() { last_log_method_invoked_ = kOther; }
+
+  size_t call_count() { return call_count_; }
+
+  void reset_call_count() { call_count_ = 0; }
+
+  void reset() {
+    reset_last_metric_id();
+    reset_last_event_code();
+    reset_last_log_method_invoked();
+    reset_call_count();
+  }
+
+ private:
+  uint32_t last_metric_id_ = -1;
+  uint32_t last_event_code_ = -1;
+  LogMethod last_log_method_invoked_ = kOther;
+  size_t call_count_ = 0;
+};
+
+}  // namespace cobalt
+
+#endif  // GARNET_BIN_COBALT_TESTING_FAKE_LOGGER_H_
\ No newline at end of file
diff --git a/garnet/bin/cobalt/utils/BUILD.gn b/garnet/bin/cobalt/utils/BUILD.gn
index fb76fc1..183305e 100644
--- a/garnet/bin/cobalt/utils/BUILD.gn
+++ b/garnet/bin/cobalt/utils/BUILD.gn
@@ -19,6 +19,12 @@
   ]
 }
 
+source_set("clock") {
+  sources = [
+    "clock.h",
+  ]
+}
+
 executable("cobalt_utils_unittests") {
   testonly = true
 
diff --git a/garnet/bin/cobalt/utils/clock.h b/garnet/bin/cobalt/utils/clock.h
new file mode 100644
index 0000000..902bebe
--- /dev/null
+++ b/garnet/bin/cobalt/utils/clock.h
@@ -0,0 +1,30 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_COBALT_UTILS_CLOCK_H_
+#define GARNET_BIN_COBALT_UTILS_CLOCK_H_
+
+#include <chrono>
+
+namespace cobalt {
+
+// An abstract interface to a SteadyClock that may be faked in tests.
+class SteadyClock {
+ public:
+  virtual ~SteadyClock() = default;
+
+  virtual std::chrono::steady_clock::time_point Now() = 0;
+};
+
+// An implementation of SteadyClock that uses a real clock.
+class RealSteadyClock : public SteadyClock {
+ public:
+  std::chrono::steady_clock::time_point Now() override {
+    return std::chrono::steady_clock::now();
+  }
+};
+
+}  // namespace cobalt
+
+#endif  // GARNET_BIN_COBALT_UTILS_CLOCK_H_
\ No newline at end of file
diff --git a/garnet/bin/cobalt/utils/status_utils.h b/garnet/bin/cobalt/utils/status_utils.h
new file mode 100644
index 0000000..1404f81
--- /dev/null
+++ b/garnet/bin/cobalt/utils/status_utils.h
@@ -0,0 +1,28 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef GARNET_BIN_COBALT_UTILS_STATUS_UTILS_H_
+#define GARNET_BIN_COBALT_UTILS_STATUS_UTILS_H_
+
+
+namespace cobalt {
+
+std::string StatusToString(fuchsia::cobalt::Status status) {
+  switch (status) {
+    case fuchsia::cobalt::Status::OK:
+      return "OK";
+    case fuchsia::cobalt::Status::INVALID_ARGUMENTS:
+      return "INVALID_ARGUMENTS";
+    case fuchsia::cobalt::Status::EVENT_TOO_BIG:
+      return "EVENT_TOO_BIG";
+    case fuchsia::cobalt::Status::BUFFER_FULL:
+      return "BUFFER_FULL";
+    case fuchsia::cobalt::Status::INTERNAL_ERROR:
+      return "INTERNAL_ERROR";
+  }
+};
+
+}  // namespace cobalt
+
+#endif  // GARNET_BIN_COBALT_UTILS_STATUS_UTILS_H_
\ No newline at end of file