Add Installer.OSAgeDays metric

This adds a new metric conveying the age of the running OS instance,
where this is defined as the time-span between the current wall-clock
time and the time-stamp of the /etc/lsb-release file. This metric is
reported daily.

BUG=chromium:304950
TEST=New unit tests for daily metrics + unit tests pass. Manual test
    for Installer.OSAgeDays by inspecting chrome://histograms.

Change-Id: I6713bed6730641a95443372a3e3166c4e1dc64ee
Reviewed-on: https://chromium-review.googlesource.com/172162
Reviewed-by: Chris Sosa <sosa@chromium.org>
Commit-Queue: David Zeuthen <zeuthen@chromium.org>
Tested-by: David Zeuthen <zeuthen@chromium.org>
diff --git a/constants.cc b/constants.cc
index 767dc47..7f9fdf4 100644
--- a/constants.cc
+++ b/constants.cc
@@ -30,6 +30,8 @@
 const char kPrefsCurrentResponseSignature[] = "current-response-signature";
 const char kPrefsCurrentUrlFailureCount[] = "current-url-failure-count";
 const char kPrefsCurrentUrlIndex[] = "current-url-index";
+const char kPrefsDailyMetricsLastReportedAt[] =
+    "daily-metrics-last-reported-at";
 const char kPrefsDeltaUpdateFailures[] = "delta-update-failures";
 const char kPrefsFullPayloadAttemptNumber[] = "full-payload-attempt-number";
 const char kPrefsLastActivePingDay[] = "last-active-ping-day";
diff --git a/constants.h b/constants.h
index 1e69df8..5fbfa06 100644
--- a/constants.h
+++ b/constants.h
@@ -34,6 +34,7 @@
 extern const char kPrefsCurrentResponseSignature[];
 extern const char kPrefsCurrentUrlFailureCount[];
 extern const char kPrefsCurrentUrlIndex[];
+extern const char kPrefsDailyMetricsLastReportedAt[];
 extern const char kPrefsDeltaUpdateFailures[];
 extern const char kPrefsFullPayloadAttemptNumber[];
 extern const char kPrefsLastActivePingDay[];
diff --git a/update_attempter.cc b/update_attempter.cc
index ae9be74..7d2500b 100644
--- a/update_attempter.cc
+++ b/update_attempter.cc
@@ -16,6 +16,7 @@
 #include <policy/device_policy.h>
 
 #include "update_engine/certificate_checker.h"
+#include "update_engine/clock_interface.h"
 #include "update_engine/constants.h"
 #include "update_engine/dbus_service.h"
 #include "update_engine/download_action.h"
@@ -162,11 +163,83 @@
   CleanupCpuSharesManagement();
 }
 
+bool UpdateAttempter::CheckAndReportDailyMetrics() {
+  int64_t stored_value;
+  base::Time now = system_state_->clock()->GetWallclockTime();
+  if (system_state_->prefs()->Exists(kPrefsDailyMetricsLastReportedAt) &&
+      system_state_->prefs()->GetInt64(kPrefsDailyMetricsLastReportedAt,
+                                       &stored_value)) {
+    base::Time last_reported_at = base::Time::FromInternalValue(stored_value);
+    base::TimeDelta time_reported_since = now - last_reported_at;
+    if (time_reported_since.InSeconds() < 0) {
+      LOG(WARNING) << "Last reported daily metrics "
+                   << utils::FormatTimeDelta(time_reported_since) << " ago "
+                   << "which is negative. Either the system clock is wrong or "
+                   << "the kPrefsDailyMetricsLastReportedAt state variable "
+                   << "is wrong.";
+      // In this case, report daily metrics to reset.
+    } else {
+      if (time_reported_since.InSeconds() < 24*60*60) {
+        LOG(INFO) << "Last reported daily metrics "
+                  << utils::FormatTimeDelta(time_reported_since) << " ago.";
+        return false;
+      }
+      LOG(INFO) << "Last reported daily metrics "
+                << utils::FormatTimeDelta(time_reported_since) << " ago, "
+                << "which is more than 24 hours ago.";
+    }
+  }
+
+  LOG(INFO) << "Reporting daily metrics.";
+  system_state_->prefs()->SetInt64(kPrefsDailyMetricsLastReportedAt,
+                                   now.ToInternalValue());
+
+  ReportOSAge();
+
+  return true;
+}
+
+void UpdateAttempter::ReportOSAge() {
+  struct stat sb;
+
+  if (system_state_ == NULL)
+    return;
+
+  if (stat("/etc/lsb-release", &sb) != 0) {
+    PLOG(ERROR) << "Error getting file status for /etc/lsb-release";
+    return;
+  }
+
+  base::Time lsb_release_timestamp = utils::TimeFromStructTimespec(&sb.st_ctim);
+  base::Time now = system_state_->clock()->GetWallclockTime();
+  base::TimeDelta age = now - lsb_release_timestamp;
+  if (age.InSeconds() < 0) {
+    LOG(ERROR) << "The OS age (" << utils::FormatTimeDelta(age)
+               << ") is negative. Maybe the clock is wrong?";
+    return;
+  }
+
+  std::string metric = "Installer.OSAgeDays";
+  LOG(INFO) << "Uploading " << utils::FormatTimeDelta(age)
+            << " for metric " <<  metric;
+  system_state_->metrics_lib()->SendToUMA(
+       metric,
+       static_cast<int>(age.InDays()),
+       0,             // min: 0 days
+       6*30,          // max: 6 months (approx)
+       kNumDefaultUmaBuckets);
+}
+
 void UpdateAttempter::Update(const string& app_version,
                              const string& omaha_url,
                              bool obey_proxies,
                              bool interactive,
                              bool is_test_mode) {
+  // This is called at least every 4 hours (see the constant
+  // UpdateCheckScheduler::kTimeoutMaxBackoffInterval) so it's
+  // appropriate to use as a hook for reporting daily metrics.
+  CheckAndReportDailyMetrics();
+
   chrome_proxy_resolver_.Init();
   fake_update_success_ = false;
   if (status_ == UPDATE_STATUS_UPDATED_NEED_REBOOT) {
diff --git a/update_attempter.h b/update_attempter.h
index b2ee3b0..74b73f6 100644
--- a/update_attempter.h
+++ b/update_attempter.h
@@ -202,11 +202,21 @@
   FRIEND_TEST(UpdateAttempterTest, ScheduleErrorEventActionNoEventTest);
   FRIEND_TEST(UpdateAttempterTest, ScheduleErrorEventActionTest);
   FRIEND_TEST(UpdateAttempterTest, UpdateTest);
+  FRIEND_TEST(UpdateAttempterTest, ReportDailyMetrics);
 
   // Ctor helper method.
   void Init(SystemState* system_state,
             const std::string& update_completed_marker);
 
+  // Checks if it's more than 24 hours since daily metrics were last
+  // reported and, if so, reports daily metrics. Returns |true| if
+  // metrics were reported, |false| otherwise.
+  bool CheckAndReportDailyMetrics();
+
+  // Calculates and reports the age of the currently running OS. This
+  // is defined as the age of the /etc/lsb-release file.
+  void ReportOSAge();
+
   // Sets the status to the given status and notifies a status update over dbus.
   // Also accepts a supplement notice, which is delegated to the scheduler and
   // used for making better informed scheduling decisions (e.g. retry timeout).
diff --git a/update_attempter_unittest.cc b/update_attempter_unittest.cc
index 7036c83..11464d4 100644
--- a/update_attempter_unittest.cc
+++ b/update_attempter_unittest.cc
@@ -9,6 +9,7 @@
 
 #include "update_engine/action_mock.h"
 #include "update_engine/action_processor_mock.h"
+#include "update_engine/fake_clock.h"
 #include "update_engine/filesystem_copier_action.h"
 #include "update_engine/install_plan.h"
 #include "update_engine/mock_dbus_interface.h"
@@ -24,6 +25,8 @@
 #include "update_engine/update_check_scheduler.h"
 #include "update_engine/utils.h"
 
+using base::Time;
+using base::TimeDelta;
 using std::string;
 using testing::_;
 using testing::DoAll;
@@ -1041,4 +1044,75 @@
   g_idle_add(&StaticQuitMainLoop, this);
 }
 
+// Checks that we only report daily metrics at most every 24 hours.
+TEST_F(UpdateAttempterTest, ReportDailyMetrics) {
+  FakeClock fake_clock;
+  Prefs prefs;
+  string temp_dir;
+
+  // We need persistent preferences for this test
+  EXPECT_TRUE(utils::MakeTempDirectory("/tmp/UpdateCheckScheduler.XXXXXX",
+                                       &temp_dir));
+  prefs.Init(FilePath(temp_dir));
+  mock_system_state_.set_clock(&fake_clock);
+  mock_system_state_.set_prefs(&prefs);
+
+  Time epoch = Time::FromInternalValue(0);
+  fake_clock.SetWallclockTime(epoch);
+
+  // If there is no kPrefsDailyMetricsLastReportedAt state variable,
+  // we should report.
+  EXPECT_TRUE(attempter_.CheckAndReportDailyMetrics());
+  // We should not report again if no time has passed.
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  // We should not report if only 10 hours has passed.
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(10));
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  // We should not report if only 24 hours - 1 sec has passed.
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(24) -
+                              TimeDelta::FromSeconds(1));
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  // We should report if 24 hours has passed.
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(24));
+  EXPECT_TRUE(attempter_.CheckAndReportDailyMetrics());
+
+  // But then we should not report again..
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  // .. until another 24 hours has passed
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(47));
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(48));
+  EXPECT_TRUE(attempter_.CheckAndReportDailyMetrics());
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  // .. and another 24 hours
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(71));
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(72));
+  EXPECT_TRUE(attempter_.CheckAndReportDailyMetrics());
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  // If the span between time of reporting and present time is
+  // negative, we report. This is in order to reset the timestamp and
+  // avoid an edge condition whereby a distant point in the future is
+  // in the state variable resulting in us never ever reporting again.
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(71));
+  EXPECT_TRUE(attempter_.CheckAndReportDailyMetrics());
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  // In this case we should not update until the clock reads 71 + 24 = 95.
+  // Check that.
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(94));
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+  fake_clock.SetWallclockTime(epoch + TimeDelta::FromHours(95));
+  EXPECT_TRUE(attempter_.CheckAndReportDailyMetrics());
+  EXPECT_FALSE(attempter_.CheckAndReportDailyMetrics());
+
+  EXPECT_TRUE(utils::RecursiveUnlinkDir(temp_dir));
+}
+
 }  // namespace chromeos_update_engine