1 // Copyright 2014 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/metrics/unsent_log_store.h"
6
7 #include <cmath>
8 #include <memory>
9 #include <string>
10 #include <utility>
11
12 #include "base/base64.h"
13 #include "base/hash/sha1.h"
14 #include "base/metrics/histogram_macros.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "base/strings/string_util.h"
17 #include "base/timer/elapsed_timer.h"
18 #include "components/metrics/unsent_log_store_metrics.h"
19 #include "components/prefs/pref_service.h"
20 #include "components/prefs/scoped_user_pref_update.h"
21 #include "crypto/hmac.h"
22 #include "third_party/zlib/google/compression_utils.h"
23
24 namespace metrics {
25
26 namespace {
27
28 const char kLogHashKey[] = "hash";
29 const char kLogSignatureKey[] = "signature";
30 const char kLogTimestampKey[] = "timestamp";
31 const char kLogDataKey[] = "data";
32 const char kLogUnsentCountKey[] = "unsent_samples_count";
33 const char kLogSentCountKey[] = "sent_samples_count";
34 const char kLogPersistedSizeInKbKey[] = "unsent_persisted_size_in_kb";
35 const char kLogUserIdKey[] = "user_id";
36 const char kLogSourceType[] = "type";
37
EncodeToBase64(const std::string & to_convert)38 std::string EncodeToBase64(const std::string& to_convert) {
39 DCHECK(to_convert.data());
40 std::string base64_result;
41 base::Base64Encode(to_convert, &base64_result);
42 return base64_result;
43 }
44
DecodeFromBase64(const std::string & to_convert)45 std::string DecodeFromBase64(const std::string& to_convert) {
46 std::string result;
47 base::Base64Decode(to_convert, &result);
48 return result;
49 }
50
51 // Used to write unsent logs to prefs.
52 class LogsPrefWriter {
53 public:
54 // Create a writer that will write unsent logs to |list_value|. |list_value|
55 // should be a base::Value::List representing a pref. Clears the contents of
56 // |list_value|.
LogsPrefWriter(base::Value::List * list_value)57 explicit LogsPrefWriter(base::Value::List* list_value)
58 : list_value_(list_value) {
59 DCHECK(list_value);
60 list_value->clear();
61 }
62
63 LogsPrefWriter(const LogsPrefWriter&) = delete;
64 LogsPrefWriter& operator=(const LogsPrefWriter&) = delete;
65
~LogsPrefWriter()66 ~LogsPrefWriter() { DCHECK(finished_); }
67
68 // Persists |log| by appending it to |list_value_|.
WriteLogEntry(UnsentLogStore::LogInfo * log)69 void WriteLogEntry(UnsentLogStore::LogInfo* log) {
70 DCHECK(!finished_);
71
72 base::Value::Dict dict_value;
73 dict_value.Set(kLogHashKey, EncodeToBase64(log->hash));
74 dict_value.Set(kLogSignatureKey, EncodeToBase64(log->signature));
75 dict_value.Set(kLogDataKey, EncodeToBase64(log->compressed_log_data));
76 dict_value.Set(kLogTimestampKey, log->timestamp);
77 if (log->log_metadata.log_source_type.has_value()) {
78 dict_value.Set(
79 kLogSourceType,
80 static_cast<int>(log->log_metadata.log_source_type.value()));
81 }
82 auto user_id = log->log_metadata.user_id;
83 if (user_id.has_value()) {
84 dict_value.Set(kLogUserIdKey,
85 EncodeToBase64(base::NumberToString(user_id.value())));
86 }
87 list_value_->Append(std::move(dict_value));
88
89 auto samples_count = log->log_metadata.samples_count;
90 if (samples_count.has_value()) {
91 unsent_samples_count_ += samples_count.value();
92 }
93 unsent_persisted_size_ += log->compressed_log_data.length();
94 ++unsent_logs_count_;
95 }
96
97 // Indicates to this writer that it is finished, and that it should not write
98 // any more logs. This also reverses |list_value_| in order to maintain the
99 // original order of the logs that were written.
Finish()100 void Finish() {
101 DCHECK(!finished_);
102 finished_ = true;
103 std::reverse(list_value_->begin(), list_value_->end());
104 }
105
unsent_samples_count() const106 base::HistogramBase::Count unsent_samples_count() const {
107 return unsent_samples_count_;
108 }
109
unsent_persisted_size() const110 size_t unsent_persisted_size() const { return unsent_persisted_size_; }
111
unsent_logs_count() const112 size_t unsent_logs_count() const { return unsent_logs_count_; }
113
114 private:
115 // The list where the logs will be written to. This should represent a pref.
116 raw_ptr<base::Value::List> list_value_;
117
118 // Whether or not this writer has finished writing to pref.
119 bool finished_ = false;
120
121 // The total number of histogram samples written so far.
122 base::HistogramBase::Count unsent_samples_count_ = 0;
123
124 // The total size of logs written so far.
125 size_t unsent_persisted_size_ = 0;
126
127 // The total number of logs written so far.
128 size_t unsent_logs_count_ = 0;
129 };
130
GetString(const base::Value::Dict & dict,base::StringPiece key,std::string & out)131 bool GetString(const base::Value::Dict& dict,
132 base::StringPiece key,
133 std::string& out) {
134 const std::string* value = dict.FindString(key);
135 if (!value)
136 return false;
137 out = *value;
138 return true;
139 }
140
141 } // namespace
142
143 UnsentLogStore::LogInfo::LogInfo() = default;
144 UnsentLogStore::LogInfo::~LogInfo() = default;
145
Init(const std::string & log_data,const std::string & log_timestamp,const std::string & signing_key,const LogMetadata & optional_log_metadata)146 void UnsentLogStore::LogInfo::Init(const std::string& log_data,
147 const std::string& log_timestamp,
148 const std::string& signing_key,
149 const LogMetadata& optional_log_metadata) {
150 DCHECK(!log_data.empty());
151
152 if (!compression::GzipCompress(log_data, &compressed_log_data)) {
153 NOTREACHED();
154 return;
155 }
156
157 hash = base::SHA1HashString(log_data);
158
159 if (!ComputeHMACForLog(log_data, signing_key, &signature)) {
160 NOTREACHED() << "HMAC signing failed";
161 }
162
163 timestamp = log_timestamp;
164 this->log_metadata = optional_log_metadata;
165 }
166
Init(const std::string & log_data,const std::string & signing_key,const LogMetadata & optional_log_metadata)167 void UnsentLogStore::LogInfo::Init(const std::string& log_data,
168 const std::string& signing_key,
169 const LogMetadata& optional_log_metadata) {
170 Init(log_data, base::NumberToString(base::Time::Now().ToTimeT()), signing_key,
171 optional_log_metadata);
172 }
173
UnsentLogStore(std::unique_ptr<UnsentLogStoreMetrics> metrics,PrefService * local_state,const char * log_data_pref_name,const char * metadata_pref_name,UnsentLogStoreLimits log_store_limits,const std::string & signing_key,MetricsLogsEventManager * logs_event_manager)174 UnsentLogStore::UnsentLogStore(std::unique_ptr<UnsentLogStoreMetrics> metrics,
175 PrefService* local_state,
176 const char* log_data_pref_name,
177 const char* metadata_pref_name,
178 UnsentLogStoreLimits log_store_limits,
179 const std::string& signing_key,
180 MetricsLogsEventManager* logs_event_manager)
181 : metrics_(std::move(metrics)),
182 local_state_(local_state),
183 log_data_pref_name_(log_data_pref_name),
184 metadata_pref_name_(metadata_pref_name),
185 log_store_limits_(log_store_limits),
186 signing_key_(signing_key),
187 logs_event_manager_(logs_event_manager),
188 staged_log_index_(-1) {
189 DCHECK(local_state_);
190 // One of the limit arguments must be non-zero.
191 DCHECK(log_store_limits_.min_log_count > 0 ||
192 log_store_limits_.min_queue_size_bytes > 0);
193 }
194
195 UnsentLogStore::~UnsentLogStore() = default;
196
has_unsent_logs() const197 bool UnsentLogStore::has_unsent_logs() const {
198 return !!size();
199 }
200
201 // True if a log has been staged.
has_staged_log() const202 bool UnsentLogStore::has_staged_log() const {
203 return staged_log_index_ != -1;
204 }
205
206 // Returns the compressed data of the element in the front of the list.
staged_log() const207 const std::string& UnsentLogStore::staged_log() const {
208 DCHECK(has_staged_log());
209 return list_[staged_log_index_]->compressed_log_data;
210 }
211
212 // Returns the hash of element in the front of the list.
staged_log_hash() const213 const std::string& UnsentLogStore::staged_log_hash() const {
214 DCHECK(has_staged_log());
215 return list_[staged_log_index_]->hash;
216 }
217
218 // Returns the signature of element in the front of the list.
staged_log_signature() const219 const std::string& UnsentLogStore::staged_log_signature() const {
220 DCHECK(has_staged_log());
221 return list_[staged_log_index_]->signature;
222 }
223
224 // Returns the timestamp of the element in the front of the list.
staged_log_timestamp() const225 const std::string& UnsentLogStore::staged_log_timestamp() const {
226 DCHECK(has_staged_log());
227 return list_[staged_log_index_]->timestamp;
228 }
229
230 // Returns the user id of the current staged log.
staged_log_user_id() const231 absl::optional<uint64_t> UnsentLogStore::staged_log_user_id() const {
232 DCHECK(has_staged_log());
233 return list_[staged_log_index_]->log_metadata.user_id;
234 }
235
staged_log_metadata() const236 const LogMetadata UnsentLogStore::staged_log_metadata() const {
237 DCHECK(has_staged_log());
238 return std::move(list_[staged_log_index_]->log_metadata);
239 }
240
241 // static
ComputeHMACForLog(const std::string & log_data,const std::string & signing_key,std::string * signature)242 bool UnsentLogStore::ComputeHMACForLog(const std::string& log_data,
243 const std::string& signing_key,
244 std::string* signature) {
245 crypto::HMAC hmac(crypto::HMAC::SHA256);
246 const size_t digest_length = hmac.DigestLength();
247 unsigned char* hmac_data = reinterpret_cast<unsigned char*>(
248 base::WriteInto(signature, digest_length + 1));
249 return hmac.Init(signing_key) &&
250 hmac.Sign(log_data, hmac_data, digest_length);
251 }
252
StageNextLog()253 void UnsentLogStore::StageNextLog() {
254 // CHECK, rather than DCHECK, because swap()ing with an empty list causes
255 // hard-to-identify crashes much later.
256 CHECK(!list_.empty());
257 DCHECK(!has_staged_log());
258 staged_log_index_ = list_.size() - 1;
259 NotifyLogEvent(MetricsLogsEventManager::LogEvent::kLogStaged,
260 list_[staged_log_index_]->hash);
261 DCHECK(has_staged_log());
262 }
263
DiscardStagedLog(base::StringPiece reason)264 void UnsentLogStore::DiscardStagedLog(base::StringPiece reason) {
265 DCHECK(has_staged_log());
266 DCHECK_LT(static_cast<size_t>(staged_log_index_), list_.size());
267 NotifyLogEvent(MetricsLogsEventManager::LogEvent::kLogDiscarded,
268 list_[staged_log_index_]->hash, reason);
269 list_.erase(list_.begin() + staged_log_index_);
270 staged_log_index_ = -1;
271 }
272
MarkStagedLogAsSent()273 void UnsentLogStore::MarkStagedLogAsSent() {
274 DCHECK(has_staged_log());
275 DCHECK_LT(static_cast<size_t>(staged_log_index_), list_.size());
276 auto samples_count = list_[staged_log_index_]->log_metadata.samples_count;
277 if (samples_count.has_value())
278 total_samples_sent_ += samples_count.value();
279 NotifyLogEvent(MetricsLogsEventManager::LogEvent::kLogUploaded,
280 list_[staged_log_index_]->hash);
281 }
282
TrimAndPersistUnsentLogs(bool overwrite_in_memory_store)283 void UnsentLogStore::TrimAndPersistUnsentLogs(bool overwrite_in_memory_store) {
284 ScopedListPrefUpdate update(local_state_, log_data_pref_name_);
285 LogsPrefWriter writer(&update.Get());
286
287 std::vector<std::unique_ptr<LogInfo>> trimmed_list;
288 size_t bytes_used = 0;
289
290 // The distance of the staged log from the end of the list of logs, which is
291 // usually 0 (end of list). This is used in case there is currently a staged
292 // log, which may or may not get trimmed. We want to keep track of the new
293 // position of the staged log after trimming so that we can update
294 // |staged_log_index_|.
295 absl::optional<size_t> staged_index_distance;
296
297 // Reverse order, so newest ones are prioritized.
298 for (int i = list_.size() - 1; i >= 0; --i) {
299 size_t log_size = list_[i]->compressed_log_data.length();
300 // Hit the caps, we can stop moving the logs.
301 if (bytes_used >= log_store_limits_.min_queue_size_bytes &&
302 writer.unsent_logs_count() >= log_store_limits_.min_log_count) {
303 // The rest of the logs (including the current one) are trimmed.
304 if (overwrite_in_memory_store) {
305 NotifyLogsEvent(base::span<std::unique_ptr<LogInfo>>(
306 list_.begin(), list_.begin() + i + 1),
307 MetricsLogsEventManager::LogEvent::kLogTrimmed);
308 }
309 break;
310 }
311 // Omit overly large individual logs if the value is non-zero.
312 if (log_store_limits_.max_log_size_bytes != 0 &&
313 log_size > log_store_limits_.max_log_size_bytes) {
314 metrics_->RecordDroppedLogSize(log_size);
315 if (overwrite_in_memory_store) {
316 NotifyLogEvent(MetricsLogsEventManager::LogEvent::kLogTrimmed,
317 list_[i]->hash, "Log size too large.");
318 }
319 continue;
320 }
321
322 bytes_used += log_size;
323
324 if (staged_log_index_ == i) {
325 staged_index_distance = writer.unsent_logs_count();
326 }
327
328 // Append log to prefs.
329 writer.WriteLogEntry(list_[i].get());
330 if (overwrite_in_memory_store)
331 trimmed_list.emplace_back(std::move(list_[i]));
332 }
333
334 writer.Finish();
335
336 if (overwrite_in_memory_store) {
337 // We went in reverse order, but appended entries. So reverse list to
338 // correct.
339 std::reverse(trimmed_list.begin(), trimmed_list.end());
340
341 size_t dropped_logs_count = list_.size() - trimmed_list.size();
342 if (dropped_logs_count > 0)
343 metrics_->RecordDroppedLogsNum(dropped_logs_count);
344
345 // Put the trimmed list in the correct place.
346 list_.swap(trimmed_list);
347
348 // We may need to adjust the staged index since the number of logs may be
349 // reduced.
350 if (staged_index_distance.has_value()) {
351 staged_log_index_ = list_.size() - 1 - staged_index_distance.value();
352 } else {
353 // Set |staged_log_index_| to -1. It might already be -1. E.g., at the
354 // time we are trimming logs, there was no staged log. However, it is also
355 // possible that we trimmed away the staged log, so we need to update the
356 // index to -1.
357 staged_log_index_ = -1;
358 }
359 }
360
361 WriteToMetricsPref(writer.unsent_samples_count(), total_samples_sent_,
362 writer.unsent_persisted_size());
363 }
364
LoadPersistedUnsentLogs()365 void UnsentLogStore::LoadPersistedUnsentLogs() {
366 ReadLogsFromPrefList(local_state_->GetList(log_data_pref_name_));
367 RecordMetaDataMetrics();
368 }
369
StoreLog(const std::string & log_data,const LogMetadata & log_metadata,MetricsLogsEventManager::CreateReason reason)370 void UnsentLogStore::StoreLog(const std::string& log_data,
371 const LogMetadata& log_metadata,
372 MetricsLogsEventManager::CreateReason reason) {
373 std::unique_ptr<LogInfo> info = std::make_unique<LogInfo>();
374 info->Init(log_data, signing_key_, log_metadata);
375 StoreLogInfo(std::move(info), log_data.size(), reason);
376 }
377
StoreLogInfo(std::unique_ptr<LogInfo> log_info,size_t uncompressed_log_size,MetricsLogsEventManager::CreateReason reason)378 void UnsentLogStore::StoreLogInfo(
379 std::unique_ptr<LogInfo> log_info,
380 size_t uncompressed_log_size,
381 MetricsLogsEventManager::CreateReason reason) {
382 DCHECK(log_info);
383 metrics_->RecordCompressionRatio(log_info->compressed_log_data.size(),
384 uncompressed_log_size);
385 NotifyLogCreated(*log_info, reason);
386 list_.emplace_back(std::move(log_info));
387 }
388
GetLogAtIndex(size_t index)389 const std::string& UnsentLogStore::GetLogAtIndex(size_t index) {
390 DCHECK_GE(index, 0U);
391 DCHECK_LT(index, list_.size());
392 return list_[index]->compressed_log_data;
393 }
394
ReplaceLogAtIndex(size_t index,const std::string & new_log_data,const LogMetadata & log_metadata)395 std::string UnsentLogStore::ReplaceLogAtIndex(size_t index,
396 const std::string& new_log_data,
397 const LogMetadata& log_metadata) {
398 DCHECK_GE(index, 0U);
399 DCHECK_LT(index, list_.size());
400
401 // Avoid copying of long strings.
402 std::string old_log_data;
403 old_log_data.swap(list_[index]->compressed_log_data);
404 std::string old_timestamp;
405 old_timestamp.swap(list_[index]->timestamp);
406 std::string old_hash;
407 old_hash.swap(list_[index]->hash);
408
409 std::unique_ptr<LogInfo> info = std::make_unique<LogInfo>();
410 info->Init(new_log_data, old_timestamp, signing_key_, log_metadata);
411 // Note that both the compression ratio of the new log and the log that is
412 // being replaced are recorded.
413 metrics_->RecordCompressionRatio(info->compressed_log_data.size(),
414 new_log_data.size());
415
416 // TODO(crbug/1363747): Pass a message to make it clear that the new log is
417 // replacing the old log.
418 NotifyLogEvent(MetricsLogsEventManager::LogEvent::kLogDiscarded, old_hash);
419 NotifyLogCreated(*info, MetricsLogsEventManager::CreateReason::kUnknown);
420 list_[index] = std::move(info);
421 return old_log_data;
422 }
423
Purge()424 void UnsentLogStore::Purge() {
425 NotifyLogsEvent(list_, MetricsLogsEventManager::LogEvent::kLogDiscarded,
426 "Purged.");
427
428 if (has_staged_log()) {
429 DiscardStagedLog();
430 }
431 list_.clear();
432 local_state_->ClearPref(log_data_pref_name_);
433 // The |total_samples_sent_| isn't cleared intentionally because it is still
434 // meaningful.
435 if (metadata_pref_name_)
436 local_state_->ClearPref(metadata_pref_name_);
437 }
438
SetLogsEventManager(MetricsLogsEventManager * logs_event_manager)439 void UnsentLogStore::SetLogsEventManager(
440 MetricsLogsEventManager* logs_event_manager) {
441 logs_event_manager_ = logs_event_manager;
442 }
443
ReadLogsFromPrefList(const base::Value::List & list_value)444 void UnsentLogStore::ReadLogsFromPrefList(const base::Value::List& list_value) {
445 // The below DCHECK ensures that a log from prefs is not loaded multiple
446 // times, which is important for the semantics of the NotifyLogsCreated() call
447 // below.
448 DCHECK(list_.empty());
449
450 if (list_value.empty()) {
451 metrics_->RecordLogReadStatus(UnsentLogStoreMetrics::LIST_EMPTY);
452 return;
453 }
454
455 const size_t log_count = list_value.size();
456
457 list_.resize(log_count);
458
459 for (size_t i = 0; i < log_count; ++i) {
460 const base::Value::Dict* dict = list_value[i].GetIfDict();
461 std::unique_ptr<LogInfo> info = std::make_unique<LogInfo>();
462 if (!dict || !GetString(*dict, kLogDataKey, info->compressed_log_data) ||
463 !GetString(*dict, kLogHashKey, info->hash) ||
464 !GetString(*dict, kLogTimestampKey, info->timestamp) ||
465 !GetString(*dict, kLogSignatureKey, info->signature)) {
466 // Something is wrong, so we don't try to get any persisted logs.
467 list_.clear();
468 metrics_->RecordLogReadStatus(
469 UnsentLogStoreMetrics::LOG_STRING_CORRUPTION);
470 return;
471 }
472
473 info->compressed_log_data = DecodeFromBase64(info->compressed_log_data);
474 info->hash = DecodeFromBase64(info->hash);
475 info->signature = DecodeFromBase64(info->signature);
476 // timestamp doesn't need to be decoded.
477
478 absl::optional<int> log_source_type = dict->FindInt(kLogSourceType);
479 if (log_source_type.has_value()) {
480 info->log_metadata.log_source_type =
481 static_cast<UkmLogSourceType>(log_source_type.value());
482 }
483
484 // Extract user id of the log if it exists.
485 const std::string* user_id_str = dict->FindString(kLogUserIdKey);
486 if (user_id_str) {
487 uint64_t user_id;
488
489 // Only initialize the metadata if conversion was successful.
490 if (base::StringToUint64(DecodeFromBase64(*user_id_str), &user_id))
491 info->log_metadata.user_id = user_id;
492 }
493
494 list_[i] = std::move(info);
495 }
496
497 // Only notify log observers after loading all logs from pref instead of
498 // notifying as logs are loaded. This is because we may return early and end
499 // up not loading any logs.
500 NotifyLogsCreated(
501 list_, MetricsLogsEventManager::CreateReason::kLoadFromPreviousSession);
502
503 metrics_->RecordLogReadStatus(UnsentLogStoreMetrics::RECALL_SUCCESS);
504 }
505
WriteToMetricsPref(base::HistogramBase::Count unsent_samples_count,base::HistogramBase::Count sent_samples_count,size_t unsent_persisted_size) const506 void UnsentLogStore::WriteToMetricsPref(
507 base::HistogramBase::Count unsent_samples_count,
508 base::HistogramBase::Count sent_samples_count,
509 size_t unsent_persisted_size) const {
510 if (metadata_pref_name_ == nullptr)
511 return;
512
513 ScopedDictPrefUpdate update(local_state_, metadata_pref_name_);
514 base::Value::Dict& pref_data = update.Get();
515 pref_data.Set(kLogUnsentCountKey, unsent_samples_count);
516 pref_data.Set(kLogSentCountKey, sent_samples_count);
517 // Round up to kb.
518 pref_data.Set(kLogPersistedSizeInKbKey,
519 static_cast<int>(std::ceil(unsent_persisted_size / 1024.0)));
520 }
521
RecordMetaDataMetrics()522 void UnsentLogStore::RecordMetaDataMetrics() {
523 if (metadata_pref_name_ == nullptr)
524 return;
525
526 const base::Value::Dict& value = local_state_->GetDict(metadata_pref_name_);
527
528 auto unsent_samples_count = value.FindInt(kLogUnsentCountKey);
529 auto sent_samples_count = value.FindInt(kLogSentCountKey);
530 auto unsent_persisted_size_in_kb = value.FindInt(kLogPersistedSizeInKbKey);
531
532 if (unsent_samples_count && sent_samples_count &&
533 unsent_persisted_size_in_kb) {
534 metrics_->RecordLastUnsentLogMetadataMetrics(
535 unsent_samples_count.value(), sent_samples_count.value(),
536 unsent_persisted_size_in_kb.value());
537 }
538 }
539
NotifyLogCreated(const LogInfo & info,MetricsLogsEventManager::CreateReason reason)540 void UnsentLogStore::NotifyLogCreated(
541 const LogInfo& info,
542 MetricsLogsEventManager::CreateReason reason) {
543 if (!logs_event_manager_)
544 return;
545 logs_event_manager_->NotifyLogCreated(info.hash, info.compressed_log_data,
546 info.timestamp, reason);
547 }
548
NotifyLogsCreated(base::span<std::unique_ptr<LogInfo>> logs,MetricsLogsEventManager::CreateReason reason)549 void UnsentLogStore::NotifyLogsCreated(
550 base::span<std::unique_ptr<LogInfo>> logs,
551 MetricsLogsEventManager::CreateReason reason) {
552 if (!logs_event_manager_)
553 return;
554 for (const std::unique_ptr<LogInfo>& info : logs) {
555 logs_event_manager_->NotifyLogCreated(info->hash, info->compressed_log_data,
556 info->timestamp, reason);
557 }
558 }
559
NotifyLogEvent(MetricsLogsEventManager::LogEvent event,base::StringPiece log_hash,base::StringPiece message)560 void UnsentLogStore::NotifyLogEvent(MetricsLogsEventManager::LogEvent event,
561 base::StringPiece log_hash,
562 base::StringPiece message) {
563 if (!logs_event_manager_)
564 return;
565 logs_event_manager_->NotifyLogEvent(event, log_hash, message);
566 }
567
NotifyLogsEvent(base::span<std::unique_ptr<LogInfo>> logs,MetricsLogsEventManager::LogEvent event,base::StringPiece message)568 void UnsentLogStore::NotifyLogsEvent(base::span<std::unique_ptr<LogInfo>> logs,
569 MetricsLogsEventManager::LogEvent event,
570 base::StringPiece message) {
571 if (!logs_event_manager_)
572 return;
573 for (const std::unique_ptr<LogInfo>& info : logs) {
574 logs_event_manager_->NotifyLogEvent(event, info->hash, message);
575 }
576 }
577
578 } // namespace metrics
579