// See the file "COPYING" in the main distribution directory for copyright. #include "zeek/telemetry/Manager.h" #define RAPIDJSON_HAS_STDSTRING 1 // CivetServer is from the civetweb submodule in prometheus-cpp #include #include #include #include #include #include #include #include #include "zeek/3rdparty/doctest.h" #include "zeek/ID.h" #include "zeek/ZeekString.h" #include "zeek/broker/Manager.h" #include "zeek/telemetry/ProcessStats.h" #include "zeek/telemetry/Timer.h" #include "zeek/telemetry/telemetry.bif.h" #include "zeek/threading/formatters/detail/json.h" namespace zeek::telemetry { Manager::Manager() { prometheus_registry = std::make_shared(); } // This can't be defined as =default because of the use of unique_ptr with a forward-declared type // in Manager.h Manager::~Manager() {} void Manager::InitPostScript() { // Metrics port setting is used to calculate a URL for prometheus scraping std::string prometheus_url; auto metrics_port = id::find_val("Telemetry::metrics_port")->AsPortVal(); auto metrics_address = id::find_val("Telemetry::metrics_address")->AsStringVal()->ToStdString(); if ( metrics_port->Port() != 0 ) prometheus_url = util::fmt("%s:%u", metrics_address.data(), metrics_port->Port()); if ( ! prometheus_url.empty() ) { CivetCallbacks* callbacks = nullptr; auto local_node_name = id::find_val("Cluster::node")->AsStringVal(); if ( local_node_name->Len() > 0 ) { auto cluster_nodes = id::find_val("Cluster::nodes")->AsTableVal(); auto local_node = cluster_nodes->Find(IntrusivePtr{NewRef{}, local_node_name}); auto local_node_type = local_node->AsRecordVal()->GetField("node_type")->Get(); static auto node_type_type = id::find_type("Cluster::NodeType")->AsEnumType(); static auto manager_type = node_type_type->Lookup("Cluster", "MANAGER"); if ( local_node_type == manager_type ) { BuildClusterJson(); callbacks = new CivetCallbacks(); callbacks->begin_request = [](struct mg_connection* conn) -> int { // Handle the services.json request ourselves by building up a response based on // the cluster configuration. auto req_info = mg_get_request_info(conn); if ( strcmp(req_info->request_uri, "/services.json") == 0 ) { // send a request to a topic for data from workers auto json = telemetry_mgr->GetClusterJson(); mg_send_http_ok(conn, "application/json", static_cast(json.size())); mg_write(conn, json.data(), json.size()); return 1; } return 0; }; } } if ( ! getenv("ZEEKCTL_CHECK_CONFIG") ) { try { prometheus_exposer = std::make_unique(prometheus_url, 2, callbacks); // CivetWeb stores a copy of the callbacks, so we're safe to delete the pointer here delete callbacks; } catch ( const CivetException& exc ) { reporter->FatalError("Failed to setup Prometheus endpoint: %s. Attempted to bind to %s.", exc.what(), prometheus_url.c_str()); } prometheus_exposer->RegisterCollectable(prometheus_registry); } } #ifdef HAVE_PROCESS_STAT_METRICS static auto get_stats = [this]() -> const detail::process_stats* { double now = util::current_time(); if ( this->process_stats_last_updated < now - 0.01 ) { this->current_process_stats = detail::get_process_stats(); this->process_stats_last_updated = now; } return &this->current_process_stats; }; rss_gauge = GaugeInstance("process", "resident_memory", {}, "Resident memory size", "bytes", []() -> prometheus::ClientMetric { auto* s = get_stats(); prometheus::ClientMetric metric; metric.gauge.value = static_cast(s->rss); return metric; }); vms_gauge = GaugeInstance("process", "virtual_memory", {}, "Virtual memory size", "bytes", []() -> prometheus::ClientMetric { auto* s = get_stats(); prometheus::ClientMetric metric; metric.gauge.value = static_cast(s->vms); return metric; }); cpu_gauge = GaugeInstance("process", "cpu", {}, "Total user and system CPU time spent", "seconds", []() -> prometheus::ClientMetric { auto* s = get_stats(); prometheus::ClientMetric metric; metric.gauge.value = s->cpu; return metric; }); fds_gauge = GaugeInstance("process", "open_fds", {}, "Number of open file descriptors", "", []() -> prometheus::ClientMetric { auto* s = get_stats(); prometheus::ClientMetric metric; metric.gauge.value = static_cast(s->fds); return metric; }); #endif } // -- collect metric stuff ----------------------------------------------------- RecordValPtr Manager::GetMetricOptsRecord(const prometheus::MetricFamily& metric_family) { // Avoid recreating this repeatedly. // TODO: this may cause problems if new metrics are added or removed by external users, // since the validation of label names needs to happen. if ( auto it = opts_records.find(metric_family.name); it != opts_records.end() ) return it->second; // Get the opt record static auto string_vec_type = zeek::id::find_type("string_vec"); static auto metric_opts_type = zeek::id::find_type("Telemetry::MetricOpts"); static auto prefix_idx = metric_opts_type->FieldOffset("prefix"); static auto name_idx = metric_opts_type->FieldOffset("name"); static auto help_text_idx = metric_opts_type->FieldOffset("help_text"); static auto unit_idx = metric_opts_type->FieldOffset("unit"); static auto is_total_idx = metric_opts_type->FieldOffset("is_total"); static auto metric_type_idx = metric_opts_type->FieldOffset("metric_type"); auto record_val = make_intrusive(metric_opts_type); record_val->Assign(name_idx, make_intrusive(metric_family.name)); record_val->Assign(help_text_idx, make_intrusive(metric_family.help)); // prometheus-cpp doesn't store the prefix information separately. we pull the word // before the first underscore as the prefix instead. The Prometheus docs state // that the prefix "should exist" not "must exist" so it's possible this could result // in incorrect data, but it should be correct for all of our uses. std::string prefix; auto first_underscore = metric_family.name.find('_'); if ( first_underscore != std::string::npos ) prefix = metric_family.name.substr(0, first_underscore); record_val->Assign(prefix_idx, make_intrusive(prefix)); // Assume that a metric ending with _total is always a summed metric so we can set that. record_val->Assign(is_total_idx, val_mgr->Bool(util::ends_with(metric_family.name, "_total"))); if ( metric_family.type == prometheus::MetricType::Counter ) record_val->Assign(metric_type_idx, zeek::BifType::Enum::Telemetry::MetricType->GetEnumVal( BifEnum::Telemetry::MetricType::COUNTER)); if ( metric_family.type == prometheus::MetricType::Gauge ) record_val->Assign(metric_type_idx, zeek::BifType::Enum::Telemetry::MetricType->GetEnumVal( BifEnum::Telemetry::MetricType::GAUGE)); if ( metric_family.type == prometheus::MetricType::Histogram ) record_val->Assign(metric_type_idx, zeek::BifType::Enum::Telemetry::MetricType->GetEnumVal( BifEnum::Telemetry::MetricType::HISTOGRAM)); opts_records.insert({metric_family.name, record_val}); return record_val; } static bool compare_string_vectors(const VectorValPtr& a, const VectorValPtr& b) { if ( a->Size() < b->Size() ) return true; if ( a->Size() > b->Size() ) return false; auto a_v = a->RawVec(); auto b_v = b->RawVec(); auto b_it = b_v.begin(); for ( auto a_it = a_v.begin(); a_it != a_v.end(); ++a_it, ++b_it ) { if ( ! a_it->has_value() ) return false; if ( ! b_it->has_value() ) return true; if ( (*a_it)->AsString()->ToStdStringView() < (*b_it)->AsString()->ToStdStringView() ) return true; } return false; } static bool comparer(const std::optional& a, const std::optional& b, const RecordTypePtr& type) { if ( ! a ) return false; if ( ! b ) return true; auto a_r = a->ToVal(type)->AsRecordVal(); auto b_r = b->ToVal(type)->AsRecordVal(); auto a_labels = a_r->GetField("label_values"); auto b_labels = b_r->GetField("label_values"); return compare_string_vectors(a_labels, b_labels); } static bool compare_metrics(const std::optional& a, const std::optional& b) { static auto metric_record_type = zeek::id::find_type("Telemetry::Metric"); return comparer(a, b, metric_record_type); } static bool compare_histograms(const std::optional& a, const std::optional& b) { static auto metric_record_type = zeek::id::find_type("Telemetry::HistogramMetric"); return comparer(a, b, metric_record_type); } ValPtr Manager::CollectMetrics(std::string_view prefix_pattern, std::string_view name_pattern) { static auto metrics_vector_type = zeek::id::find_type("Telemetry::MetricVector"); static auto string_vec_type = zeek::id::find_type("string_vec"); static auto metric_record_type = zeek::id::find_type("Telemetry::Metric"); static auto opts_idx = metric_record_type->FieldOffset("opts"); static auto value_idx = metric_record_type->FieldOffset("value"); static auto label_names_idx = metric_record_type->FieldOffset("label_names"); static auto label_values_idx = metric_record_type->FieldOffset("label_values"); static auto metric_opts_type = zeek::id::find_type("Telemetry::MetricOpts"); static auto metric_type_idx = metric_opts_type->FieldOffset("metric_type"); VectorValPtr ret_val = make_intrusive(metrics_vector_type); // Due to the name containing the full information about a metric including a potential unit add an // asterisk to the end of the full pattern so matches work correctly. std::string full_pattern = util::fmt("%s_%s", prefix_pattern.data(), name_pattern.data()); if ( full_pattern[full_pattern.size() - 1] != '*' ) full_pattern.append("*"); auto collected = prometheus_registry->Collect(); for ( const auto& fam : collected ) { if ( fam.type == prometheus::MetricType::Histogram ) continue; if ( fnmatch(full_pattern.c_str(), fam.name.c_str(), 0) == FNM_NOMATCH ) continue; RecordValPtr opts_record = GetMetricOptsRecord(fam); for ( const auto& inst : fam.metric ) { auto r = make_intrusive(metric_record_type); r->Assign(opts_idx, opts_record); if ( fam.type == prometheus::MetricType::Counter ) r->Assign(value_idx, zeek::make_intrusive(inst.counter.value)); else if ( fam.type == prometheus::MetricType::Gauge ) r->Assign(value_idx, zeek::make_intrusive(inst.gauge.value)); auto label_names_vec = make_intrusive(string_vec_type); auto label_values_vec = make_intrusive(string_vec_type); for ( const auto& lbl : inst.label ) { label_names_vec->Append(make_intrusive(lbl.name)); label_values_vec->Append(make_intrusive(lbl.value)); } r->Assign(label_names_idx, std::move(label_names_vec)); r->Assign(label_values_idx, std::move(label_values_vec)); ret_val->Append(std::move(r)); } } // If running under test, there are issues with the non-deterministic // ordering of the metrics coming out of prometheus-cpp, which uses // std::hash on the label values to sort them. Check for that case and sort // the results to some fixed order so that the tests have consistent // results. if ( ret_val->Size() > 0 ) { static auto running_under_test = id::find_val("running_under_test")->AsBool(); if ( running_under_test ) { auto& vec = ret_val->RawVec(); std::sort(vec.begin(), vec.end(), compare_histograms); } } return std::move(ret_val); } ValPtr Manager::CollectHistogramMetrics(std::string_view prefix_pattern, std::string_view name_pattern) { static auto metrics_vector_type = zeek::id::find_type("Telemetry::HistogramMetricVector"); static auto string_vec_type = zeek::id::find_type("string_vec"); static auto double_vec_type = zeek::id::find_type("double_vec"); static auto histogram_metric_type = zeek::id::find_type("Telemetry::HistogramMetric"); static auto values_idx = histogram_metric_type->FieldOffset("values"); static auto label_names_idx = histogram_metric_type->FieldOffset("label_names"); static auto label_values_idx = histogram_metric_type->FieldOffset("label_values"); static auto observations_idx = histogram_metric_type->FieldOffset("observations"); static auto sum_idx = histogram_metric_type->FieldOffset("sum"); static auto opts_idx = histogram_metric_type->FieldOffset("opts"); static auto opts_rt = zeek::id::find_type("Telemetry::MetricOpts"); static auto bounds_idx = opts_rt->FieldOffset("bounds"); static auto metric_opts_type = zeek::id::find_type("Telemetry::MetricOpts"); static auto metric_type_idx = metric_opts_type->FieldOffset("metric_type"); VectorValPtr ret_val = make_intrusive(metrics_vector_type); // Due to the name containing the full information about a metric including a potential unit add an // asterisk to the end of the full pattern so matches work correctly. std::string full_pattern = util::fmt("%s_%s", prefix_pattern.data(), name_pattern.data()); if ( full_pattern[full_pattern.size() - 1] != '*' ) full_pattern.append("*"); auto collected = prometheus_registry->Collect(); for ( const auto& fam : collected ) { if ( fam.type != prometheus::MetricType::Histogram ) continue; if ( fnmatch(full_pattern.c_str(), fam.name.c_str(), 0) == FNM_NOMATCH ) continue; RecordValPtr opts_record = GetMetricOptsRecord(fam); for ( const auto& inst : fam.metric ) { auto r = make_intrusive(histogram_metric_type); r->Assign(opts_idx, opts_record); auto label_names_vec = make_intrusive(string_vec_type); auto label_values_vec = make_intrusive(string_vec_type); for ( const auto& lbl : inst.label ) { label_names_vec->Append(make_intrusive(lbl.name)); label_values_vec->Append(make_intrusive(lbl.value)); } r->Assign(label_names_idx, std::move(label_names_vec)); r->Assign(label_values_idx, std::move(label_values_vec)); auto double_values_vec = make_intrusive(double_vec_type); std::vector boundaries; uint64_t last = 0.0; for ( const auto& b : inst.histogram.bucket ) { double_values_vec->Append( zeek::make_intrusive(static_cast(b.cumulative_count - last))); last = b.cumulative_count; boundaries.push_back(b.upper_bound); } // TODO: these could be stored somehow to avoid recreating them repeatedly auto bounds_vec = make_intrusive(double_vec_type); for ( auto b : boundaries ) bounds_vec->Append(zeek::make_intrusive(b)); r->Assign(values_idx, double_values_vec); r->Assign(observations_idx, zeek::make_intrusive(static_cast(inst.histogram.sample_count))); r->Assign(sum_idx, zeek::make_intrusive(inst.histogram.sample_sum)); RecordValPtr local_opts_record = r->GetField(opts_idx); local_opts_record->Assign(bounds_idx, std::move(bounds_vec)); ret_val->Append(std::move(r)); } } // If running under btest, there are issues with the non-deterministic // ordering of the metrics coming out of prometheus-cpp, which uses // std::hash on the label values to sort them. Check for that case and sort // the results to some fixed order so that the tests have consistent // results. if ( ret_val->Size() > 0 ) { static auto running_under_test = id::find_val("running_under_test")->AsBool(); if ( running_under_test ) { auto& vec = ret_val->RawVec(); std::sort(vec.begin(), vec.end(), compare_histograms); } } return std::move(ret_val); } void Manager::BuildClusterJson() { rapidjson::StringBuffer buffer; json::detail::NullDoubleWriter writer(buffer); writer.StartArray(); writer.StartObject(); writer.Key("targets"); writer.StartArray(); auto& node_val = id::find_val("Cluster::nodes"); auto node_map = node_val->AsTableVal()->ToMap(); for ( const auto& [idx, value] : node_map ) { auto node = value->AsRecordVal(); auto ip = node->GetField("ip"); auto port = node->GetField("metrics_port"); if ( ip && port && port->Port() != 0 ) writer.String(util::fmt("%s:%d", ip->Get().AsString().c_str(), port->Port())); } writer.EndArray(); writer.Key("labels"); writer.StartObject(); writer.EndObject(); writer.EndObject(); writer.EndArray(); cluster_json = buffer.GetString(); } CounterFamilyPtr Manager::CounterFamily(std::string_view prefix, std::string_view name, Span labels, std::string_view helptext, std::string_view unit) { auto full_name = detail::BuildFullPrometheusName(prefix, name, unit, true); auto& prom_fam = prometheus::BuildCounter().Name(full_name).Help(std::string{helptext}).Register(*prometheus_registry); if ( auto it = families.find(prom_fam.GetName()); it != families.end() ) return std::static_pointer_cast(it->second); auto fam = std::make_shared(&prom_fam, labels); families.insert({prom_fam.GetName(), fam}); return fam; } CounterFamilyPtr Manager::CounterFamily(std::string_view prefix, std::string_view name, std::initializer_list labels, std::string_view helptext, std::string_view unit) { auto lbl_span = Span{labels.begin(), labels.size()}; return CounterFamily(prefix, name, lbl_span, helptext, unit); } CounterPtr Manager::CounterInstance(std::string_view prefix, std::string_view name, Span labels, std::string_view helptext, std::string_view unit, prometheus::CollectCallbackPtr callback) { return WithLabelNames(labels, [&, this](auto labelNames) { auto family = CounterFamily(prefix, name, labelNames, helptext, unit); return family->GetOrAdd(labels, callback); }); } CounterPtr Manager::CounterInstance(std::string_view prefix, std::string_view name, std::initializer_list labels, std::string_view helptext, std::string_view unit, prometheus::CollectCallbackPtr callback) { auto lbl_span = Span{labels.begin(), labels.size()}; return CounterInstance(prefix, name, lbl_span, helptext, unit, std::move(callback)); } std::shared_ptr Manager::GaugeFamily(std::string_view prefix, std::string_view name, Span labels, std::string_view helptext, std::string_view unit) { auto full_name = detail::BuildFullPrometheusName(prefix, name, unit, false); auto& prom_fam = prometheus::BuildGauge().Name(full_name).Help(std::string{helptext}).Register(*prometheus_registry); if ( auto it = families.find(prom_fam.GetName()); it != families.end() ) return std::static_pointer_cast(it->second); auto fam = std::make_shared(&prom_fam, labels); families.insert({prom_fam.GetName(), fam}); return fam; } GaugeFamilyPtr Manager::GaugeFamily(std::string_view prefix, std::string_view name, std::initializer_list labels, std::string_view helptext, std::string_view unit) { auto lbl_span = Span{labels.begin(), labels.size()}; return GaugeFamily(prefix, name, lbl_span, helptext, unit); } GaugePtr Manager::GaugeInstance(std::string_view prefix, std::string_view name, Span labels, std::string_view helptext, std::string_view unit, prometheus::CollectCallbackPtr callback) { return WithLabelNames(labels, [&, this](auto labelNames) { auto family = GaugeFamily(prefix, name, labelNames, helptext, unit); return family->GetOrAdd(labels, callback); }); } GaugePtr Manager::GaugeInstance(std::string_view prefix, std::string_view name, std::initializer_list labels, std::string_view helptext, std::string_view unit, prometheus::CollectCallbackPtr callback) { auto lbl_span = Span{labels.begin(), labels.size()}; return GaugeInstance(prefix, name, lbl_span, helptext, unit, std::move(callback)); } HistogramFamilyPtr Manager::HistogramFamily(std::string_view prefix, std::string_view name, Span labels, ConstSpan bounds, std::string_view helptext, std::string_view unit) { auto full_name = detail::BuildFullPrometheusName(prefix, name, unit); auto& prom_fam = prometheus::BuildHistogram().Name(full_name).Help(std::string{helptext}).Register(*prometheus_registry); if ( auto it = families.find(prom_fam.GetName()); it != families.end() ) return std::static_pointer_cast(it->second); auto fam = std::make_shared(&prom_fam, bounds, labels); families.insert({prom_fam.GetName(), fam}); return fam; } HistogramFamilyPtr Manager::HistogramFamily(std::string_view prefix, std::string_view name, std::initializer_list labels, ConstSpan bounds, std::string_view helptext, std::string_view unit) { auto lbl_span = Span{labels.begin(), labels.size()}; return HistogramFamily(prefix, name, lbl_span, bounds, helptext, unit); } HistogramPtr Manager::HistogramInstance(std::string_view prefix, std::string_view name, Span labels, ConstSpan bounds, std::string_view helptext, std::string_view unit) { return WithLabelNames(labels, [&, this](auto labelNames) { auto family = HistogramFamily(prefix, name, labelNames, bounds, helptext, unit); return family->GetOrAdd(labels); }); } HistogramPtr Manager::HistogramInstance(std::string_view prefix, std::string_view name, std::initializer_list labels, std::initializer_list bounds, std::string_view helptext, std::string_view unit) { auto lbls = Span{labels.begin(), labels.size()}; auto bounds_span = Span{bounds.begin(), bounds.size()}; return HistogramInstance(prefix, name, lbls, bounds_span, helptext, unit); } } // namespace zeek::telemetry // -- unit tests --------------------------------------------------------------- using namespace std::literals; using namespace zeek::telemetry; namespace { template auto toVector(zeek::Span xs) { std::vector> result; for ( auto&& x : xs ) result.emplace_back(x); return result; } } // namespace SCENARIO("telemetry managers provide access to counter families") { GIVEN("a telemetry manager") { Manager mgr; WHEN("retrieving an IntCounter family") { auto family = mgr.CounterFamily("zeek", "requests", {"method"}, "test"); THEN("GetOrAdd returns the same metric for the same labels") { auto first = family->GetOrAdd({{"method", "get"}}); auto second = family->GetOrAdd({{"method", "get"}}); CHECK_EQ(first, second); } AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family->GetOrAdd({{"method", "get"}}); auto second = family->GetOrAdd({{"method", "put"}}); CHECK_NE(first, second); } } WHEN("retrieving a DblCounter family") { auto family = mgr.CounterFamily("zeek", "runtime", {"query"}, "test", "seconds"); THEN("GetOrAdd returns the same metric for the same labels") { auto first = family->GetOrAdd({{"query", "foo"}}); auto second = family->GetOrAdd({{"query", "foo"}}); CHECK_EQ(first, second); } AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family->GetOrAdd({{"query", "foo"}}); auto second = family->GetOrAdd({{"query", "bar"}}); CHECK_NE(first, second); } } } } SCENARIO("telemetry managers provide access to gauge families") { GIVEN("a telemetry manager") { Manager mgr; WHEN("retrieving an IntGauge family") { auto family = mgr.GaugeFamily("zeek", "open-connections", {"protocol"}, "test"); THEN("GetOrAdd returns the same metric for the same labels") { auto first = family->GetOrAdd({{"protocol", "tcp"}}); auto second = family->GetOrAdd({{"protocol", "tcp"}}); CHECK_EQ(first, second); } AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family->GetOrAdd({{"protocol", "tcp"}}); auto second = family->GetOrAdd({{"protocol", "quic"}}); CHECK_NE(first, second); } } WHEN("retrieving a DblGauge family") { auto family = mgr.GaugeFamily("zeek", "water-level", {"river"}, "test", "meters"); THEN("GetOrAdd returns the same metric for the same labels") { auto first = family->GetOrAdd({{"river", "Sacramento"}}); auto second = family->GetOrAdd({{"river", "Sacramento"}}); CHECK_EQ(first, second); } AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family->GetOrAdd({{"query", "Sacramento"}}); auto second = family->GetOrAdd({{"query", "San Joaquin"}}); CHECK_NE(first, second); } } } } SCENARIO("telemetry managers provide access to histogram families") { GIVEN("a telemetry manager") { Manager mgr; WHEN("retrieving an IntHistogram family") { double buckets[] = {10, 20}; auto family = mgr.HistogramFamily("zeek", "payload-size", {"protocol"}, buckets, "test", "bytes"); THEN("GetOrAdd returns the same metric for the same labels") { auto first = family->GetOrAdd({{"protocol", "tcp"}}); auto second = family->GetOrAdd({{"protocol", "tcp"}}); CHECK_EQ(first, second); } AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family->GetOrAdd({{"protocol", "tcp"}}); auto second = family->GetOrAdd({{"protocol", "udp"}}); CHECK_NE(first, second); } } WHEN("retrieving a DblHistogram family") { double buckets[] = {10.0, 20.0}; auto family = mgr.HistogramFamily("zeek", "parse-time", {"protocol"}, buckets, "test", "seconds"); THEN("GetOrAdd returns the same metric for the same labels") { auto first = family->GetOrAdd({{"protocol", "tcp"}}); auto second = family->GetOrAdd({{"protocol", "tcp"}}); CHECK_EQ(first, second); } AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family->GetOrAdd({{"protocol", "tcp"}}); auto second = family->GetOrAdd({{"protocol", "udp"}}); CHECK_NE(first, second); } AND_THEN("Timers add observations to histograms") { auto hg = family->GetOrAdd({{"protocol", "tst"}}); CHECK_EQ(hg->Sum(), 0.0); { Timer observer{hg}; std::this_thread::sleep_for(1ms); } CHECK_NE(hg->Sum(), 0.0); } } } }