shill: Add preliminary UMA support

Add preliminary UMA framework and support for common service metrics and one
WiFi metric (Network.Shill.WiFi.Channel) so we can thrash out how the
metrics fit in the current code.  Other metrics will be added in the
future and are tracked by separate bugs.

BUG=chromium-os:22062
TEST=Unit tests

Change-Id: If3dc6da2e66e846e2178a1700b2576218b90abda
Reviewed-on: https://gerrit.chromium.org/gerrit/12474
Commit-Ready: Thieu Le <thieule@chromium.org>
Reviewed-by: Thieu Le <thieule@chromium.org>
Tested-by: Thieu Le <thieule@chromium.org>
diff --git a/Makefile b/Makefile
index 3b45ee5..163628a 100644
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,8 @@
 
 # libevent, gdk and gtk-2.0 are needed to leverage chrome's MessageLoop
 # TODO(cmasone): explore if newer versions of libbase let us avoid this.
-BASE_LIBS = -lbase -lchromeos -levent -lpthread -lrt -lcares -lmobile-provider
+BASE_LIBS = -lbase -lchromeos -levent -lpthread -lrt -lcares -lmobile-provider \
+            -lmetrics
 BASE_INCLUDE_DIRS = -I..
 BASE_LIB_DIRS =
 
@@ -111,6 +112,7 @@
 	key_value_store.o \
 	manager.o \
 	manager_dbus_adaptor.o \
+	metrics.o \
 	modem.o \
 	modem_cdma_proxy.o \
 	modem_gsm_card_proxy.o \
@@ -177,6 +179,7 @@
 	ipconfig_unittest.o \
 	key_file_store_unittest.o \
 	manager_unittest.o \
+	metrics_unittest.o \
 	mock_adaptors.o \
 	mock_ares.o \
 	mock_async_connection.o \
diff --git a/metrics.cc b/metrics.cc
new file mode 100644
index 0000000..1810164
--- /dev/null
+++ b/metrics.cc
@@ -0,0 +1,247 @@
+// Copyright (c) 2011 The Chromium OS 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 "shill/metrics.h"
+
+#include <base/lazy_instance.h>
+#include <base/logging.h>
+#include <base/string_util.h>
+#include <base/stringprintf.h>
+
+#include "shill/wifi_service.h"
+
+using std::string;
+using std::tr1::shared_ptr;
+
+namespace shill {
+
+static base::LazyInstance<Metrics> g_metrics(base::LINKER_INITIALIZED);
+
+// static
+const char Metrics::kMetricNetworkChannel[] = "Network.Shill.%s.Channel";
+const int Metrics::kMetricNetworkChannelMax = Metrics::kWiFiChannelMax;
+const char Metrics::kMetricNetworkServiceErrors[] =
+    "Network.Shill.ServiceErrors";
+const int Metrics::kMetricNetworkServiceErrorsMax = Service::kFailureMax;
+const char Metrics::kMetricTimeToConfigMilliseconds[] =
+    "Network.Shill.%s.TimeToConfig";
+const char Metrics::kMetricTimeToJoinMilliseconds[] =
+    "Network.Shill.%s.TimeToJoin";
+const char Metrics::kMetricTimeToOnlineMilliseconds[] =
+    "Network.Shill.%s.TimeToOnline";
+const char Metrics::kMetricTimeToPortalMilliseconds[] =
+    "Network.Shill.%s.TimeToPortal";
+const int Metrics::kTimerHistogramMaxMilliseconds = 45 * 1000;
+const int Metrics::kTimerHistogramMinMilliseconds = 1;
+const int Metrics::kTimerHistogramNumBuckets = 50;
+
+// static
+const uint16 Metrics::kWiFiBandwidth5MHz = 5;
+const uint16 Metrics::kWiFiBandwidth20MHz = 20;
+const uint16 Metrics::kWiFiFrequency2412 = 2412;
+const uint16 Metrics::kWiFiFrequency2472 = 2472;
+const uint16 Metrics::kWiFiFrequency2484 = 2484;
+const uint16 Metrics::kWiFiFrequency5170 = 5170;
+const uint16 Metrics::kWiFiFrequency5180 = 5180;
+const uint16 Metrics::kWiFiFrequency5230 = 5230;
+const uint16 Metrics::kWiFiFrequency5240 = 5240;
+const uint16 Metrics::kWiFiFrequency5320 = 5320;
+const uint16 Metrics::kWiFiFrequency5500 = 5500;
+const uint16 Metrics::kWiFiFrequency5700 = 5700;
+const uint16 Metrics::kWiFiFrequency5745 = 5745;
+const uint16 Metrics::kWiFiFrequency5825 = 5825;
+
+Metrics::Metrics() : library_(&metrics_library_) {
+  metrics_library_.Init();
+  chromeos_metrics::TimerReporter::set_metrics_lib(library_);
+}
+
+Metrics::~Metrics() {}
+
+// static
+Metrics *Metrics::GetInstance() {
+  return g_metrics.Pointer();
+}
+
+// static
+Metrics::WiFiChannel Metrics::WiFiFrequencyToChannel(uint16 frequency) {
+  WiFiChannel channel = kWiFiChannelUndef;
+  if (kWiFiFrequency2412 <= frequency && frequency <= kWiFiFrequency2472) {
+    if (((frequency - kWiFiFrequency2412) % kWiFiBandwidth5MHz) == 0)
+      channel = static_cast<WiFiChannel>(
+                    kWiFiChannel2412 +
+                    (frequency - kWiFiFrequency2412) / kWiFiBandwidth5MHz);
+  } else if (frequency == kWiFiFrequency2484) {
+    channel = kWiFiChannel2484;
+  } else if (kWiFiFrequency5170 <= frequency &&
+             frequency <= kWiFiFrequency5230) {
+    if ((frequency % kWiFiBandwidth20MHz) == 0)
+      channel = static_cast<WiFiChannel>(
+                    kWiFiChannel5180 +
+                    (frequency - kWiFiFrequency5180) / kWiFiBandwidth20MHz);
+    if ((frequency % kWiFiBandwidth20MHz) == 10)
+      channel = static_cast<WiFiChannel>(
+                    kWiFiChannel5170 +
+                    (frequency - kWiFiFrequency5170) / kWiFiBandwidth20MHz);
+  } else if (kWiFiFrequency5240 <= frequency &&
+             frequency <= kWiFiFrequency5320) {
+    if (((frequency - kWiFiFrequency5180) % kWiFiBandwidth20MHz) == 0)
+      channel = static_cast<WiFiChannel>(
+                    kWiFiChannel5180 +
+                    (frequency - kWiFiFrequency5180) / kWiFiBandwidth20MHz);
+  } else if (kWiFiFrequency5500 <= frequency &&
+             frequency <= kWiFiFrequency5700) {
+    if (((frequency - kWiFiFrequency5500) % kWiFiBandwidth20MHz) == 0)
+      channel = static_cast<WiFiChannel>(
+                    kWiFiChannel5500 +
+                    (frequency - kWiFiFrequency5500) / kWiFiBandwidth20MHz);
+  } else if (kWiFiFrequency5745 <= frequency &&
+             frequency <= kWiFiFrequency5825) {
+    if (((frequency - kWiFiFrequency5745) % kWiFiBandwidth20MHz) == 0)
+      channel = static_cast<WiFiChannel>(
+                    kWiFiChannel5745 +
+                    (frequency - kWiFiFrequency5745) / kWiFiBandwidth20MHz);
+  }
+  CHECK(kWiFiChannelUndef <= channel && channel < kWiFiChannelMax);
+
+  if (channel == kWiFiChannelUndef)
+    LOG(WARNING) << "no mapping for frequency " << frequency;
+  else
+    VLOG(3) << "map " << frequency << " to " << channel;
+
+  return channel;
+}
+
+void Metrics::RegisterService(const Service *service) {
+  shared_ptr<ServiceMetrics> service_metrics(new ServiceMetrics);
+  services_metrics_[service] = service_metrics;
+  service_metrics->service = service;
+  InitializeCommonServiceMetrics(service);
+  service->InitializeCustomMetrics();
+}
+
+void Metrics::DeregisterService(const Service *service) {
+  services_metrics_.erase(service);
+}
+
+void Metrics::AddServiceStateTransitionTimer(
+    const Service *service,
+    const string &histogram_name,
+    Service::ConnectState start_state,
+    Service::ConnectState stop_state) {
+  ServiceMetricsLookupMap::iterator it = services_metrics_.find(service);
+  if (it == services_metrics_.end()) {
+    VLOG(1) << "service not found";
+    DCHECK(false);
+    return;
+  }
+  ServiceMetrics *service_metrics = it->second.get();
+  CHECK(start_state < stop_state);
+  chromeos_metrics::TimerReporter *timer =
+      new chromeos_metrics::TimerReporter(histogram_name,
+                                          kTimerHistogramMinMilliseconds,
+                                          kTimerHistogramMaxMilliseconds,
+                                          kTimerHistogramNumBuckets);
+  service_metrics->timers.push_back(timer);  // passes ownership.
+  service_metrics->start_on_state[start_state].push_back(timer);
+  service_metrics->stop_on_state[stop_state].push_back(timer);
+}
+
+void Metrics::NotifyDefaultServiceChanged(const Service */*service*/) {
+  // TODO(thieule): Handle the case when the default service has changed.
+  // crosbug.com/24438
+}
+
+void Metrics::NotifyServiceStateChanged(const Service *service,
+                                        Service::ConnectState new_state) {
+  ServiceMetricsLookupMap::iterator it = services_metrics_.find(service);
+  if (it == services_metrics_.end()) {
+    VLOG(1) << "service not found";
+    DCHECK(false);
+    return;
+  }
+  ServiceMetrics *service_metrics = it->second.get();
+  UpdateServiceStateTransitionMetrics(service_metrics, new_state);
+
+  if (new_state == Service::kStateFailure)
+    SendServiceFailure(service);
+
+  if (new_state != Service::kStateReady)
+    return;
+
+  service->SendPostReadyStateMetrics();
+}
+
+string Metrics::GetFullMetricName(const char *metric_name,
+                                  Technology::Identifier technology_id) {
+  string technology = Technology::NameFromIdentifier(technology_id);
+  technology[0] = base::ToUpperASCII(technology[0]);
+  return base::StringPrintf(metric_name, technology.c_str());
+}
+
+void Metrics::NotifyServiceDisconnect(const Service */*service*/,
+                                      bool /*manual_disconnect*/) {
+  // TODO(thieule): Handle service disconnects.
+  // crosbug.com/23253
+}
+
+void Metrics::NotifyPower() {
+  // TODO(thieule): Handle suspend and resume.
+  // crosbug.com/24440
+}
+
+bool Metrics::SendEnumToUMA(const string &name, int sample, int max) {
+  return library_->SendEnumToUMA(name, sample, max);
+}
+
+void Metrics::InitializeCommonServiceMetrics(const Service *service) {
+  Technology::Identifier technology = service->technology();
+  string histogram = GetFullMetricName(kMetricTimeToConfigMilliseconds,
+                                       technology);
+  AddServiceStateTransitionTimer(
+      service,
+      histogram,
+      Service::kStateConfiguring,
+      Service::kStateReady);
+  histogram = GetFullMetricName(kMetricTimeToPortalMilliseconds, technology);
+  AddServiceStateTransitionTimer(
+      service,
+      histogram,
+      Service::kStateReady,
+      Service::kStatePortal);
+  histogram = GetFullMetricName(kMetricTimeToOnlineMilliseconds, technology);
+  AddServiceStateTransitionTimer(
+      service,
+      histogram,
+      Service::kStateReady,
+      Service::kStateOnline);
+}
+
+void Metrics::UpdateServiceStateTransitionMetrics(
+    ServiceMetrics *service_metrics,
+    Service::ConnectState new_state) {
+  TimerReportersList::iterator it;
+  TimerReportersList &start_timers = service_metrics->start_on_state[new_state];
+  for (it = start_timers.begin(); it != start_timers.end(); ++it)
+    (*it)->Start();
+
+  TimerReportersList &stop_timers = service_metrics->stop_on_state[new_state];
+  for (it = stop_timers.begin(); it != stop_timers.end(); ++it) {
+    (*it)->Stop();
+    (*it)->ReportMilliseconds();
+  }
+}
+
+void Metrics::SendServiceFailure(const Service *service) {
+  library_->SendEnumToUMA(kMetricNetworkServiceErrors,
+                          service->failure(),
+                          kMetricNetworkServiceErrorsMax);
+}
+
+void Metrics::set_library(MetricsLibraryInterface *library) {
+  chromeos_metrics::TimerReporter::set_metrics_lib(library);
+  library_ = library;
+}
+
+}  // namespace shill
diff --git a/metrics.h b/metrics.h
new file mode 100644
index 0000000..c33a7a5
--- /dev/null
+++ b/metrics.h
@@ -0,0 +1,202 @@
+// Copyright (c) 2011 The Chromium OS 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 SHILL_METRICS_
+#define SHILL_METRICS_
+
+#include <list>
+
+#include <base/lazy_instance.h>
+#include <base/memory/scoped_vector.h>
+#include <metrics/metrics_library.h>
+#include <metrics/timer.h>
+
+#include "shill/refptr_types.h"
+#include "shill/service.h"
+
+namespace shill {
+
+class WiFiService;
+
+class Metrics {
+ public:
+  enum WiFiChannel {
+    kWiFiChannelUndef = 0,
+    kWiFiChannel2412 = 1,
+    kWiFiChannel2417 = 2,
+    kWiFiChannel2422 = 3,
+    kWiFiChannel2427 = 4,
+    kWiFiChannel2432 = 5,
+    kWiFiChannel2437 = 6,
+    kWiFiChannel2442 = 7,
+    kWiFiChannel2447 = 8,
+    kWiFiChannel2452 = 9,
+    kWiFiChannel2457 = 10,
+    kWiFiChannel2462 = 11,
+    kWiFiChannel2467 = 12,
+    kWiFiChannel2472 = 13,
+    kWiFiChannel2484 = 14,
+
+    kWiFiChannel5180 = 15,
+    kWiFiChannel5200 = 16,
+    kWiFiChannel5220 = 17,
+    kWiFiChannel5240 = 18,
+    kWiFiChannel5260 = 19,
+    kWiFiChannel5280 = 20,
+    kWiFiChannel5300 = 21,
+    kWiFiChannel5320 = 22,
+
+    kWiFiChannel5500 = 23,
+    kWiFiChannel5520 = 24,
+    kWiFiChannel5540 = 25,
+    kWiFiChannel5560 = 26,
+    kWiFiChannel5580 = 27,
+    kWiFiChannel5600 = 28,
+    kWiFiChannel5620 = 29,
+    kWiFiChannel5640 = 30,
+    kWiFiChannel5660 = 31,
+    kWiFiChannel5680 = 32,
+    kWiFiChannel5700 = 33,
+
+    kWiFiChannel5745 = 34,
+    kWiFiChannel5765 = 35,
+    kWiFiChannel5785 = 36,
+    kWiFiChannel5805 = 37,
+    kWiFiChannel5825 = 38,
+
+    kWiFiChannel5170 = 39,
+    kWiFiChannel5190 = 40,
+    kWiFiChannel5210 = 41,
+    kWiFiChannel5230 = 42,
+
+    /* NB: ignore old 11b bands 2312..2372 and 2512..2532 */
+    /* NB: ignore regulated bands 4920..4980 and 5020..5160 */
+    kWiFiChannelMax
+  };
+
+  static const char kMetricNetworkChannel[];
+  static const int kMetricNetworkChannelMax;
+  static const char kMetricNetworkServiceErrors[];
+  static const int kMetricNetworkServiceErrorsMax;
+  static const char kMetricTimeToConfigMilliseconds[];
+  static const char kMetricTimeToJoinMilliseconds[];
+  static const char kMetricTimeToOnlineMilliseconds[];
+  static const char kMetricTimeToPortalMilliseconds[];
+  static const int kTimerHistogramMaxMilliseconds;
+  static const int kTimerHistogramMinMilliseconds;
+  static const int kTimerHistogramNumBuckets;
+
+  virtual ~Metrics();
+
+  // This is a singleton -- use Metrics::GetInstance()->Foo()
+  static Metrics *GetInstance();
+
+  // Converts the WiFi frequency into the associated UMA channel enumerator.
+  static WiFiChannel WiFiFrequencyToChannel(uint16 frequency);
+
+  // Registers a service with this object so it can use the timers to track
+  // state transition metrics.
+  void RegisterService(const Service *service);
+
+  // Deregisters the service from this class.  All state transition timers
+  // will be removed.
+  void DeregisterService(const Service *service);
+
+  // Tracks the time it takes |service| to go from |start_state| to
+  // |stop_state|.  When |stop_state| is reached, the time is sent to UMA.
+  void AddServiceStateTransitionTimer(const Service *service,
+                                      const std::string &histogram_name,
+                                      Service::ConnectState start_state,
+                                      Service::ConnectState stop_state);
+
+  // Specializes |metric_name| for the specified |technology_id|.
+  std::string GetFullMetricName(const char *metric_name,
+                                Technology::Identifier technology_id);
+
+  // Notifies this object that the default service has changed.
+  // |service| is the new default service.
+  void NotifyDefaultServiceChanged(const Service *service);
+
+  // Notifies this object that |service| state has changed.
+  virtual void NotifyServiceStateChanged(const Service *service,
+                                         Service::ConnectState new_state);
+
+  // Notifies this object that |service| has been disconnected and whether
+  // the disconnect was requested by the user or not.
+  void NotifyServiceDisconnect(const Service *service,
+                               bool manual_disconnect);
+
+  // Notifies this object of a power management event.
+  void NotifyPower();
+
+  // Sends linear histogram data to UMA.
+  bool SendEnumToUMA(const std::string &name, int sample, int max);
+
+ private:
+  friend struct base::DefaultLazyInstanceTraits<Metrics>;
+  friend class MetricsTest;
+  FRIEND_TEST(MetricsTest, TimeToConfig);
+  FRIEND_TEST(MetricsTest, TimeToPortal);
+  FRIEND_TEST(MetricsTest, TimeToOnline);
+  FRIEND_TEST(MetricsTest, ServiceFailure);
+  FRIEND_TEST(MetricsTest, WiFiServiceChannel);
+  FRIEND_TEST(MetricsTest, FrequencyToChannel);
+
+  typedef ScopedVector<chromeos_metrics::TimerReporter> TimerReporters;
+  typedef std::list<chromeos_metrics::TimerReporter *> TimerReportersList;
+  typedef std::map<Service::ConnectState, TimerReportersList>
+      TimerReportersByState;
+  struct ServiceMetrics {
+    ServiceMetrics() : service(NULL) {}
+    // The service is registered/deregistered in the Service
+    // constructor/destructor, therefore there is no need to keep a ref count.
+    const Service *service;
+    // All TimerReporter objects are stored in |timers| which owns the objects.
+    // |start_on_state| and |stop_on_state| contain pointers to the
+    // TimerReporter objects and control when to start and stop the timers.
+    TimerReporters timers;
+    TimerReportersByState start_on_state;
+    TimerReportersByState stop_on_state;
+  };
+  typedef std::map<const Service *, std::tr1::shared_ptr<ServiceMetrics> >
+      ServiceMetricsLookupMap;
+
+  static const uint16 kWiFiBandwidth5MHz;
+  static const uint16 kWiFiBandwidth20MHz;
+  static const uint16 kWiFiFrequency2412;
+  static const uint16 kWiFiFrequency2472;
+  static const uint16 kWiFiFrequency2484;
+  static const uint16 kWiFiFrequency5170;
+  static const uint16 kWiFiFrequency5180;
+  static const uint16 kWiFiFrequency5230;
+  static const uint16 kWiFiFrequency5240;
+  static const uint16 kWiFiFrequency5320;
+  static const uint16 kWiFiFrequency5500;
+  static const uint16 kWiFiFrequency5700;
+  static const uint16 kWiFiFrequency5745;
+  static const uint16 kWiFiFrequency5825;
+
+  Metrics();
+
+  void InitializeCommonServiceMetrics(const Service *service);
+  void UpdateServiceStateTransitionMetrics(ServiceMetrics *service_metrics,
+                                           Service::ConnectState new_state);
+  void SendServiceFailure(const Service *service);
+
+  // For unit test purposes.
+  void set_library(MetricsLibraryInterface *library);
+
+  // |library_| points to |metrics_library_| when shill runs normally.
+  // However, in order to allow for unit testing, we point |library_| to a
+  // MetricsLibraryMock object instead.
+  MetricsLibrary metrics_library_;
+  MetricsLibraryInterface *library_;
+  ServiceMetricsLookupMap services_metrics_;
+
+  DISALLOW_COPY_AND_ASSIGN(Metrics);
+};
+
+}  // namespace shill
+
+#endif  // SHILL_METRICS_
diff --git a/metrics_unittest.cc b/metrics_unittest.cc
new file mode 100644
index 0000000..1a5a159
--- /dev/null
+++ b/metrics_unittest.cc
@@ -0,0 +1,154 @@
+// Copyright (c) 2011 The Chromium OS 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 "shill/metrics.h"
+
+#include <chromeos/dbus/service_constants.h>
+#include <metrics/metrics_library_mock.h>
+#include <metrics/timer.h>
+
+#include "shill/mock_service.h"
+#include "shill/mock_wifi_service.h"
+#include "shill/property_store_unittest.h"
+
+using testing::_;
+using testing::Ge;
+using testing::Return;
+using testing::Test;
+
+namespace shill {
+
+class MetricsTest : public PropertyStoreTest {
+ public:
+  MetricsTest()
+      : service_(new MockService(control_interface(),
+                                 dispatcher(),
+                                 manager())),
+        wifi_(new WiFi(control_interface(),
+                       dispatcher(),
+                       manager(),
+                       "wlan0",
+                       "000102030405",
+                       0)),
+        wifi_service_(new MockWiFiService(control_interface(),
+                                          dispatcher(),
+                                          manager(),
+                                          wifi_,
+                                          ssid_,
+                                          flimflam::kModeManaged,
+                                          flimflam::kSecurityNone,
+                                          false)) {
+  }
+
+  virtual ~MetricsTest() {}
+
+  virtual void SetUp() {
+    metrics_.set_library(&library_);
+    service_->set_metrics(&metrics_);
+    wifi_service_->set_metrics(&metrics_);
+  }
+
+ protected:
+  Metrics metrics_;  // This must be destroyed after service_ and wifi_service_
+  MetricsLibraryMock library_;
+  scoped_refptr<MockService> service_;
+  WiFiRefPtr wifi_;
+  const std::vector<uint8_t> ssid_;
+  scoped_refptr<MockWiFiService> wifi_service_;
+};
+
+TEST_F(MetricsTest, TimeToConfig) {
+  EXPECT_CALL(library_, SendToUMA("Network.Shill.Unknown.TimeToConfig",
+                                  Ge(0),
+                                  Metrics::kTimerHistogramMinMilliseconds,
+                                  Metrics::kTimerHistogramMaxMilliseconds,
+                                  Metrics::kTimerHistogramNumBuckets));
+  metrics_.RegisterService(service_);
+  metrics_.NotifyServiceStateChanged(service_, Service::kStateConfiguring);
+  metrics_.NotifyServiceStateChanged(service_, Service::kStateReady);
+}
+
+TEST_F(MetricsTest, TimeToPortal) {
+  EXPECT_CALL(library_, SendToUMA("Network.Shill.Unknown.TimeToPortal",
+                                  Ge(0),
+                                  Metrics::kTimerHistogramMinMilliseconds,
+                                  Metrics::kTimerHistogramMaxMilliseconds,
+                                  Metrics::kTimerHistogramNumBuckets));
+  metrics_.RegisterService(service_);
+  metrics_.NotifyServiceStateChanged(service_, Service::kStateReady);
+  metrics_.NotifyServiceStateChanged(service_, Service::kStatePortal);
+}
+
+TEST_F(MetricsTest, TimeToOnline) {
+  EXPECT_CALL(library_, SendToUMA("Network.Shill.Unknown.TimeToOnline",
+                                  Ge(0),
+                                  Metrics::kTimerHistogramMinMilliseconds,
+                                  Metrics::kTimerHistogramMaxMilliseconds,
+                                  Metrics::kTimerHistogramNumBuckets));
+  metrics_.RegisterService(service_);
+  metrics_.NotifyServiceStateChanged(service_, Service::kStateReady);
+  metrics_.NotifyServiceStateChanged(service_, Service::kStateOnline);
+}
+
+TEST_F(MetricsTest, ServiceFailure) {
+  EXPECT_CALL(*service_.get(), failure())
+      .WillRepeatedly(Return(Service::kFailureBadCredentials));
+  EXPECT_CALL(library_, SendEnumToUMA(Metrics::kMetricNetworkServiceErrors,
+                                      Service::kFailureBadCredentials,
+                                      Metrics::kMetricNetworkServiceErrorsMax));
+  metrics_.RegisterService(service_);
+  metrics_.NotifyServiceStateChanged(service_, Service::kStateFailure);
+}
+
+TEST_F(MetricsTest, WiFiServiceTimeToJoin) {
+  EXPECT_CALL(library_, SendToUMA("Network.Shill.Wifi.TimeToJoin",
+                                  Ge(0),
+                                  Metrics::kTimerHistogramMinMilliseconds,
+                                  Metrics::kTimerHistogramMaxMilliseconds,
+                                  Metrics::kTimerHistogramNumBuckets));
+  metrics_.RegisterService(wifi_service_);
+  metrics_.NotifyServiceStateChanged(wifi_service_, Service::kStateAssociating);
+  metrics_.NotifyServiceStateChanged(wifi_service_, Service::kStateConfiguring);
+}
+
+TEST_F(MetricsTest, WiFiServiceChannel) {
+  EXPECT_CALL(library_, SendEnumToUMA("Network.Shill.Wifi.Channel",
+                                      Metrics::kWiFiChannel2412,
+                                      Metrics::kMetricNetworkChannelMax));
+  wifi_service_->frequency_ = 2412;
+  metrics_.RegisterService(wifi_service_);
+  metrics_.NotifyServiceStateChanged(wifi_service_, Service::kStateReady);
+}
+
+TEST_F(MetricsTest, FrequencyToChannel) {
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(2411));
+  EXPECT_EQ(Metrics::kWiFiChannel2412, metrics_.WiFiFrequencyToChannel(2412));
+  EXPECT_EQ(Metrics::kWiFiChannel2472, metrics_.WiFiFrequencyToChannel(2472));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(2473));
+  EXPECT_EQ(Metrics::kWiFiChannel2484, metrics_.WiFiFrequencyToChannel(2484));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5169));
+  EXPECT_EQ(Metrics::kWiFiChannel5170, metrics_.WiFiFrequencyToChannel(5170));
+  EXPECT_EQ(Metrics::kWiFiChannel5190, metrics_.WiFiFrequencyToChannel(5190));
+  EXPECT_EQ(Metrics::kWiFiChannel5180, metrics_.WiFiFrequencyToChannel(5180));
+  EXPECT_EQ(Metrics::kWiFiChannel5200, metrics_.WiFiFrequencyToChannel(5200));
+  EXPECT_EQ(Metrics::kWiFiChannel5230, metrics_.WiFiFrequencyToChannel(5230));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5231));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5239));
+  EXPECT_EQ(Metrics::kWiFiChannel5240, metrics_.WiFiFrequencyToChannel(5240));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5241));
+  EXPECT_EQ(Metrics::kWiFiChannel5320, metrics_.WiFiFrequencyToChannel(5320));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5321));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5499));
+  EXPECT_EQ(Metrics::kWiFiChannel5500, metrics_.WiFiFrequencyToChannel(5500));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5501));
+  EXPECT_EQ(Metrics::kWiFiChannel5700, metrics_.WiFiFrequencyToChannel(5700));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5701));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5744));
+  EXPECT_EQ(Metrics::kWiFiChannel5745, metrics_.WiFiFrequencyToChannel(5745));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5746));
+  EXPECT_EQ(Metrics::kWiFiChannel5825, metrics_.WiFiFrequencyToChannel(5825));
+  EXPECT_EQ(Metrics::kWiFiChannelUndef, metrics_.WiFiFrequencyToChannel(5826));
+}
+
+}  // namespace shill
diff --git a/mock_service.cc b/mock_service.cc
index 2af9656..7ad7911 100644
--- a/mock_service.cc
+++ b/mock_service.cc
@@ -34,6 +34,7 @@
   ON_CALL(*this, state()).WillByDefault(Return(kStateUnknown));
   ON_CALL(*this, failure()).WillByDefault(Return(kFailureUnknown));
   ON_CALL(*this, TechnologyIs(_)).WillByDefault(Return(false));
+  ON_CALL(*this, technology()).WillByDefault(Return(Technology::kUnknown));
 }
 
 MockService::~MockService() {}
diff --git a/service.cc b/service.cc
index 90f4d50..c571461 100644
--- a/service.cc
+++ b/service.cc
@@ -21,6 +21,7 @@
 #include "shill/error.h"
 #include "shill/http_proxy.h"
 #include "shill/manager.h"
+#include "shill/metrics.h"
 #include "shill/profile.h"
 #include "shill/property_accessor.h"
 #include "shill/refptr_types.h"
@@ -89,7 +90,8 @@
       configured_(false),
       configuration_(NULL),
       adaptor_(control_interface->CreateServiceAdaptor(this)),
-      manager_(manager) {
+      manager_(manager),
+      metrics_(Metrics::GetInstance()) {
 
   store_.RegisterBool(flimflam::kAutoConnectProperty, &auto_connect_);
 
@@ -162,10 +164,15 @@
   // flimflam::kWifiFrequency: Registered in WiFiService
   // flimflam::kWifiPhyMode: Registered in WiFiService
   // flimflam::kWifiHexSsid: Registered in WiFiService
+
+  metrics_->RegisterService(this);
+
   VLOG(2) << "Service initialized.";
 }
 
-Service::~Service() {}
+Service::~Service() {
+  metrics_->DeregisterService(this);
+}
 
 void Service::ActivateCellularModem(const string &/*carrier*/, Error *error) {
   const string kMessage = "Service doesn't support cellular modem activation.";
@@ -198,6 +205,7 @@
     failure_ = kFailureUnknown;
   }
   manager_->UpdateService(this);
+  metrics_->NotifyServiceStateChanged(this, state);
   Error error;
   if (state == kStateConnected) {
     // TODO(quiche): After we have portal detection in place, CalculateState
@@ -354,6 +362,8 @@
       return "OTASP Failure";
     case kFailureAAAFailure:
       return "AAA Failure";
+    case kFailureMax:
+      return "Max failure error code";
   }
   return "Invalid";
 }
@@ -373,6 +383,10 @@
       return "Connected";
     case kStateDisconnected:
       return "Disconnected";
+    case kStateReady:
+      return "Ready";
+    case kStatePortal:
+      return "Portal";
     case kStateFailure:
       return "Failure";
     case kStateOnline:
diff --git a/service.h b/service.h
index 643452d..096ed8a 100644
--- a/service.h
+++ b/service.h
@@ -29,6 +29,7 @@
 class HTTPProxy;
 class KeyValueStore;
 class Manager;
+class Metrics;
 class ServiceAdaptorInterface;
 class StoreInterface;
 
@@ -54,7 +55,8 @@
     kFailureNeedEVDO,
     kFailureNeedHomeNetwork,
     kFailureOTASPFailure,
-    kFailureAAAFailure
+    kFailureAAAFailure,
+    kFailureMax
   };
   enum ConnectState {
     kStateUnknown,
@@ -63,6 +65,8 @@
     kStateConfiguring,
     kStateConnected,
     kStateDisconnected,
+    kStateReady,
+    kStatePortal,
     kStateFailure,
     kStateOnline
   };
@@ -159,6 +163,13 @@
   // connection to serve requests.
   virtual void SetConnection(ConnectionRefPtr connection);
 
+  // The inherited class should register any custom metrics in this method.
+  virtual void InitializeCustomMetrics() const {}
+
+  // The inherited class that needs to send metrics after the service has
+  // transitioned to the ready state.
+  virtual void SendPostReadyStateMetrics() const {}
+
   bool auto_connect() const { return auto_connect_; }
   void set_auto_connect(bool connect) { auto_connect_ = connect; }
 
@@ -245,9 +256,11 @@
   EventDispatcher *dispatcher() const { return dispatcher_; }
   const std::string &GetEAPKeyManagement() const;
   void SetEAPKeyManagement(const std::string &key_management);
+  Metrics *metrics() const { return metrics_; }
 
  private:
   friend class ServiceAdaptorInterface;
+  friend class MetricsTest;
   FRIEND_TEST(DeviceTest, SelectedService);
   FRIEND_TEST(ManagerTest, SortServicesWithConnection);
   FRIEND_TEST(ServiceTest, Constructor);
@@ -295,6 +308,9 @@
   // are, "decision" is populated with the boolean value of "a > b".
   static bool DecideBetween(int a, int b, bool *decision);
 
+  // For unit testing.
+  void set_metrics(Metrics *metrics) { metrics_ = metrics; }
+
   ConnectState state_;
   ConnectFailure failure_;
   bool auto_connect_;
@@ -326,6 +342,7 @@
   ConnectionRefPtr connection_;
   Manager *manager_;
   Sockets sockets_;
+  Metrics *metrics_;
 
   DISALLOW_COPY_AND_ASSIGN(Service);
 };
diff --git a/wifi_service.cc b/wifi_service.cc
index 1f2d978..995394f 100644
--- a/wifi_service.cc
+++ b/wifi_service.cc
@@ -20,6 +20,7 @@
 #include "shill/error.h"
 #include "shill/event_dispatcher.h"
 #include "shill/ieee80211.h"
+#include "shill/metrics.h"
 #include "shill/property_accessor.h"
 #include "shill/store_interface.h"
 #include "shill/wifi.h"
@@ -158,17 +159,6 @@
 string WiFiService::GetStorageIdentifier() const {
   return storage_identifier_;
 }
-const string &WiFiService::mode() const {
-  return mode_;
-}
-
-const string &WiFiService::key_management() const {
-  return GetEAPKeyManagement();
-}
-
-const vector<uint8_t> &WiFiService::ssid() const {
-  return ssid_;
-}
 
 void WiFiService::SetPassphrase(const string &passphrase, Error *error) {
   if (security_ == flimflam::kSecurityWep) {
@@ -265,6 +255,26 @@
   return GetSecurityClass(security) == GetSecurityClass(security_);
 }
 
+void WiFiService::InitializeCustomMetrics() const {
+  string histogram = metrics()->GetFullMetricName(
+                         Metrics::kMetricTimeToJoinMilliseconds,
+                         technology());
+  metrics()->AddServiceStateTransitionTimer(this,
+                                            histogram,
+                                            Service::kStateAssociating,
+                                            Service::kStateConfiguring);
+}
+
+void WiFiService::SendPostReadyStateMetrics() const {
+  // TODO(thieule): Send physical mode and security metrics.
+  // crosbug.com/24441
+  metrics()->SendEnumToUMA(
+      metrics()->GetFullMetricName(Metrics::kMetricNetworkChannel,
+                                   technology()),
+      Metrics::WiFiFrequencyToChannel(frequency_),
+      Metrics::kMetricNetworkChannelMax);
+}
+
 // private methods
 void WiFiService::HelpRegisterDerivedString(
     PropertyStore *store,
diff --git a/wifi_service.h b/wifi_service.h
index 5573589..13b0a9d 100644
--- a/wifi_service.h
+++ b/wifi_service.h
@@ -51,9 +51,9 @@
                                      std::string *mode,
                                      std::string *security);
 
-  const std::string &mode() const;
-  const std::string &key_management() const;
-  const std::vector<uint8_t> &ssid() const;
+  const std::string &mode() const { return mode_; }
+  const std::string &key_management() const { return GetEAPKeyManagement(); }
+  const std::vector<uint8_t> &ssid() const { return ssid_; }
 
   void SetPassphrase(const std::string &passphrase, Error *error);
 
@@ -67,8 +67,12 @@
   bool IsSecurityMatch(const std::string &security) const;
   bool hidden_ssid() const { return hidden_ssid_; }
 
+  virtual void InitializeCustomMetrics() const;
+  virtual void SendPostReadyStateMetrics() const;
+
  private:
   friend class WiFiServiceSecurityTest;
+  FRIEND_TEST(MetricsTest, WiFiServiceChannel);
   FRIEND_TEST(WiFiServiceTest, ConnectTaskRSN);
   FRIEND_TEST(WiFiServiceTest, ConnectTaskWPA);
   FRIEND_TEST(WiFiServiceTest, ConnectTaskPSK);