| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef SECAGENTD_METRICS_SENDER_H_ |
| #define SECAGENTD_METRICS_SENDER_H_ |
| |
| #include <memory> |
| #include <string> |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/functional/callback.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/strcat.h" |
| #include "base/task/thread_pool.h" |
| #include "base/timer/timer.h" |
| #include "metrics/metrics_library.h" |
| |
| namespace secagentd { |
| namespace testing { |
| class MetricsSenderTestFixture; |
| } // namespace testing |
| namespace metrics { |
| |
| using MetricsMap = std::unordered_map<std::string, int>; |
| |
| static constexpr auto kMetricNamePrefix = "ChromeOS.Secagentd."; |
| static constexpr int kMaxMapValue = 256; |
| // Matches the time at which the metrics file is flushed (30s). |
| static constexpr int kBatchTimer = 30; |
| |
| struct CountMetric { |
| const char* name; |
| int max; |
| int min; |
| unsigned int nbuckets; // prefer to be <50, must be <100. See |
| // src/platform2/metrics/metrics_library.h description |
| // for SendToUMA. |
| }; |
| |
| bool operator==(const CountMetric& m1, const CountMetric& m2); |
| } // namespace metrics |
| } // namespace secagentd |
| |
| // make CountMetric hashable for use within unordered_map. |
| namespace std { |
| template <> |
| struct hash<secagentd::metrics::CountMetric> { |
| std::size_t operator()(const secagentd::metrics::CountMetric& m) const { |
| return std::hash<std::string>{}(std::string(m.name)); |
| } |
| }; |
| } // namespace std |
| |
| namespace secagentd { |
| namespace metrics { |
| |
| using MetricsCountSubMap = std::unordered_map<int, unsigned int>; |
| using MetricsCountMap = std::unordered_map<CountMetric, MetricsCountSubMap>; |
| |
| static constexpr CountMetric kCommandLineSize = { |
| .name = "CommandLineLength", .max = 4096, .min = 64, .nbuckets = 32}; |
| |
| template <class E> |
| struct EnumMetric { |
| const char* name; |
| using Enum = E; |
| }; |
| |
| enum class Policy { |
| kChecked, |
| kEnabled, |
| kMaxValue = kEnabled, |
| }; |
| |
| static constexpr EnumMetric<Policy> kPolicy = {.name = "Policy"}; |
| |
| enum class BpfAttachResult { |
| kSuccess, |
| kErrorOpen, |
| kErrorLoad, |
| kErrorAttach, |
| kErrorRingBuffer, |
| kMaxValue = kErrorRingBuffer, |
| }; |
| |
| static constexpr EnumMetric<BpfAttachResult> kNetworkBpfAttach = { |
| .name = "Bpf.Network.AttachResult"}; |
| |
| static constexpr EnumMetric<BpfAttachResult> kProcessBpfAttach = { |
| .name = "Bpf.Process.AttachResult"}; |
| |
| // This should always follow the missive status code. |
| // https://chromium.googlesource.com/chromiumos/platform2/+/6142bdcb70dc0987f9234c2294660f798d5df05a/missive/util/status.h#26 |
| enum class SendMessage { |
| kSuccess, |
| kCancelled, |
| kUnknown, |
| kInvalidArgument, |
| kDeadlineExceeded, |
| kNotFound, |
| kAlreadyExists, |
| kPermissionDenied, |
| kResourceExhausted, |
| kFailedPrecondition, |
| kAborted, |
| kOutOfRange, |
| kUnimplemetned, |
| kInternal, |
| kUnavailable, |
| kDataLoss, |
| kUnauthenticated, |
| // The value should always be kept last. |
| kMaxValue = kUnauthenticated, |
| }; |
| |
| static constexpr EnumMetric<SendMessage> kSendMessage = { |
| .name = "SendMessageResult"}; |
| |
| enum class CrosBootmode { |
| kSuccess, |
| kValueNotSet, |
| kUnavailable, |
| kFailedRetrieval, |
| kMaxValue = kFailedRetrieval, |
| }; |
| |
| static constexpr EnumMetric<CrosBootmode> kCrosBootmode = {.name = |
| "Bootmode.Cros"}; |
| |
| enum class UefiBootmode { |
| kSuccess, |
| kFileNotFound, |
| kFailedToReadBootParams, |
| kBootParamInvalidSize, |
| kMaxValue = kBootParamInvalidSize, |
| }; |
| |
| static constexpr EnumMetric<UefiBootmode> kUefiBootmode = {.name = |
| "Bootmode.Uefi"}; |
| |
| enum class Tpm { |
| kSuccess, |
| kValueNotSet, |
| kUnavailable, |
| kFailedRetrieval, |
| kMaxValue = kFailedRetrieval, |
| }; |
| |
| static constexpr EnumMetric<Tpm> kTpm = {.name = "Tpm"}; |
| |
| enum class Cache { |
| kCacheHit, |
| kCacheMiss, |
| kProcfsFilled, |
| kMaxValue = kProcfsFilled, |
| }; |
| |
| static constexpr EnumMetric<Cache> kCache = {.name = "Cache"}; |
| |
| constexpr char kCacheFullness[] = "CacheFullness"; |
| |
| enum class ProcessEvent { |
| kFullEvent, |
| kSpawnPidNotInCache, |
| kProcessPidNotInCache, |
| kParentPidNotInCache, |
| kParentStillAlive, |
| kMaxValue = kParentStillAlive, |
| }; |
| |
| static constexpr EnumMetric<ProcessEvent> kExecEvent = { |
| .name = "Process.ExecEvent"}; |
| static constexpr EnumMetric<ProcessEvent> kTerminateEvent = { |
| .name = "Process.TerminateEvent"}; |
| |
| constexpr char kRedaction[] = "Redaction"; |
| static constexpr int kRedactionBucketCount = 6; |
| |
| enum class AuthFactor { |
| kUnknown, |
| kPassword, |
| kPin, |
| kOnlineRecovery, |
| kKiosk, |
| kSmartCard, |
| kFingerprint, |
| kNewUser, |
| kMaxValue = kNewUser |
| }; |
| |
| static constexpr EnumMetric<AuthFactor> kLogin = {.name = "AuthFactor.Login"}; |
| static constexpr EnumMetric<AuthFactor> kUnlock = {.name = "AuthFactor.Unlock"}; |
| static constexpr EnumMetric<AuthFactor> kFailure = {.name = |
| "AuthFactor.Failure"}; |
| |
| } // namespace metrics |
| |
| // Class for sending UMA metrics. Expected to be accessed as a Singleton via |
| // MetricsSender::GetInstance(). |
| class MetricsSender { |
| public: |
| static MetricsSender& GetInstance(); |
| |
| // Starts job to set up timer. |
| void InitBatchedMetrics(); |
| |
| // Send a metrics::EnumMetric sample to UMA. Synchronously calls into |
| // MetricsLibrary. |
| // Warning: Not safe for use in hot paths. Limit usage to infrequent events |
| // (such as during daemon initialization). |
| template <typename M> |
| bool SendEnumMetricToUMA(M metric, typename M::Enum sample) { |
| return metrics_library_->SendEnumToUMA( |
| base::StrCat({metrics::kMetricNamePrefix, metric.name}), sample); |
| } |
| |
| // Same as SendEnumMetricToUMA except sends percentage instead. |
| bool SendPercentageMetricToUMA(std::string_view name, int sample) { |
| return metrics_library_->SendPercentageToUMA( |
| base::StrCat({metrics::kMetricNamePrefix, name}), sample); |
| } |
| |
| // Same as SendEnumMetricToUMA except sends linear data instead. |
| bool SendLinearMetricToUMA(std::string_view name, int sample, int max) { |
| return metrics_library_->SendLinearToUMA( |
| base::StrCat({metrics::kMetricNamePrefix, name}), sample, max); |
| } |
| |
| // Increment the batched count histogram for sample. |
| // The contents of the batch are periodically flushed. |
| void IncrementCountMetric(metrics::CountMetric m, int sample); |
| |
| // Creates a key with the given metric sample pair and increments the map |
| // value by one. |
| template <typename M> |
| void IncrementBatchedMetric(M metric, typename M::Enum sample) { |
| int sample_val = static_cast<int>(sample); |
| // The key is name and sample separated by a colon. |
| std::string key = |
| base::StrCat({metric.name, ":", std::to_string(sample_val)}); |
| if (exclusive_max_map_.find(metric.name) == exclusive_max_map_.end()) { |
| LOG(ERROR) << "Key not found in exclusive_max_map. Key = " << metric.name; |
| return; |
| } |
| auto success_value = success_value_map_.find(metric.name); |
| if (success_value == success_value_map_.end()) { |
| LOG(ERROR) << "Key not found in success_value_map. Key = " << metric.name; |
| return; |
| } |
| batch_enum_map_[key]++; |
| |
| // Trigger a flush if count is high and not success value. |
| if (batch_enum_map_[key] >= metrics::kMaxMapValue && |
| sample_val != success_value->second) { |
| Flush(); |
| } |
| } |
| |
| void SetMetricsLibraryForTesting( |
| std::unique_ptr<MetricsLibraryInterface> metrics_library) { |
| metrics_library_ = std::move(metrics_library); |
| } |
| |
| // Registers the given callback that will be sent every |
| // 30 seconds by the flush timer. |
| void RegisterMetricOnFlushCallback(base::RepeatingCallback<void()> cb); |
| |
| private: |
| friend class base::NoDestructor<MetricsSender>; |
| friend class testing::MetricsSenderTestFixture; |
| |
| // Sends all metrics contained in the metrics map. |
| void SendBatchedMetricsToUMA(metrics::MetricsMap map_copy, |
| metrics::MetricsCountMap count_map_copy); |
| |
| // Starts job for SendMetricsToUMA and clears metrics_map_. |
| void Flush(); |
| |
| // Starts the batch timer using task_runner_. |
| void StartBatchTimer(); |
| |
| // Allow calling the private test-only constructor without befriending |
| // unique_ptr. |
| template <typename... Args> |
| static std::unique_ptr<MetricsSender> CreateForTesting(Args&&... args) { |
| return base::WrapUnique(new MetricsSender(std::forward<Args>(args)...)); |
| } |
| |
| MetricsSender(); |
| explicit MetricsSender( |
| std::unique_ptr<MetricsLibraryInterface> metrics_library); |
| |
| base::WeakPtrFactory<MetricsSender> weak_ptr_factory_; |
| base::RepeatingTimer flush_batched_metrics_timer_; |
| scoped_refptr<base::SequencedTaskRunner> task_runner_ = |
| base::ThreadPool::CreateSequencedTaskRunner({}); |
| std::unique_ptr<MetricsLibraryInterface> metrics_library_; |
| std::vector<base::RepeatingCallback<void()>> metric_callbacks_; |
| metrics::MetricsMap batch_enum_map_; |
| metrics::MetricsCountMap batch_count_map_; |
| const metrics::MetricsMap exclusive_max_map_ = { |
| {metrics::kSendMessage.name, |
| static_cast<int>(metrics::SendMessage::kMaxValue) + 1}, |
| {metrics::kCache.name, static_cast<int>(metrics::Cache::kMaxValue) + 1}, |
| {metrics::kExecEvent.name, |
| static_cast<int>(metrics::ProcessEvent::kMaxValue) + 1}, |
| {metrics::kTerminateEvent.name, |
| static_cast<int>(metrics::ProcessEvent::kMaxValue) + 1}}; |
| const metrics::MetricsMap success_value_map_ = { |
| {metrics::kSendMessage.name, 0}, |
| {metrics::kCache.name, 0}, |
| {metrics::kExecEvent.name, 0}, |
| {metrics::kTerminateEvent.name, 0}}; |
| }; |
| } // namespace secagentd |
| |
| #endif // SECAGENTD_METRICS_SENDER_H_ |