diff --git a/src/telemetry/CMakeLists.txt b/src/telemetry/CMakeLists.txt index 004ab29b42..14b59d3c36 100644 --- a/src/telemetry/CMakeLists.txt +++ b/src/telemetry/CMakeLists.txt @@ -8,6 +8,7 @@ include_directories(BEFORE set(telemetry_SRCS Counter.cc Gauge.cc + Histogram.cc Manager.cc MetricFamily.cc ) diff --git a/src/telemetry/Counter.cc b/src/telemetry/Counter.cc index e8f8841ce2..6b4f76adc3 100644 --- a/src/telemetry/Counter.cc +++ b/src/telemetry/Counter.cc @@ -12,44 +12,6 @@ namespace ct = caf::telemetry; namespace zeek::telemetry { -// -- bindings to private utility functions ------------------------------------ - -template <> -struct PimplTrait - { - using Native = ct::int_counter; - using Oqaque = IntCounter::Impl; - using NativeFamily = ct::metric_family_impl; - }; - -template <> -struct PimplTrait : PimplTrait { }; - -template <> -struct PimplTrait - { - using Native = typename PimplTrait::NativeFamily; - using Oqaque = IntCounterFamily::Impl; - }; - -template <> -struct PimplTrait - { - using Native = ct::dbl_counter; - using Oqaque = DblCounter::Impl; - using NativeFamily = ct::metric_family_impl; - }; - -template <> -struct PimplTrait : PimplTrait { }; - -template <> -struct PimplTrait - { - using Native = typename PimplTrait::NativeFamily; - using Oqaque = DblCounterFamily::Impl; - }; - // -- IntCounter --------------------------------------------------------------- void IntCounter::Inc() noexcept diff --git a/src/telemetry/Detail.h b/src/telemetry/Detail.h index 15a3d867f3..0a235df868 100644 --- a/src/telemetry/Detail.h +++ b/src/telemetry/Detail.h @@ -10,6 +10,10 @@ #include #include "zeek/Span.h" +#include "zeek/telemetry/Counter.h" +#include "zeek/telemetry/Gauge.h" +#include "zeek/telemetry/Histogram.h" +#include "zeek/telemetry/Manager.h" #include "zeek/telemetry/MetricFamily.h" #include "caf/telemetry/label_view.hpp" @@ -17,6 +21,8 @@ namespace zeek::telemetry { +// -- traits for converting between opaque handles and native pointers --------- + /** * This trait must provide the member types @c Native for referring to the CAF * type, @c Opaque for referring to the @c Impl type. For instance types such as @@ -25,6 +31,155 @@ namespace zeek::telemetry { template struct PimplTrait; +template <> +struct PimplTrait + { + using Native = caf::telemetry::int_counter; + using Oqaque = IntCounter::Impl; + using NativeFamily = caf::telemetry::metric_family_impl; + }; + +template <> +struct PimplTrait +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = typename PimplTrait::NativeFamily; + using Oqaque = IntCounterFamily::Impl; + }; + +template <> +struct PimplTrait::NativeFamily> +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = caf::telemetry::dbl_counter; + using Oqaque = DblCounter::Impl; + using NativeFamily = caf::telemetry::metric_family_impl; + }; + +template <> +struct PimplTrait +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = typename PimplTrait::NativeFamily; + using Oqaque = DblCounterFamily::Impl; + }; + +template <> +struct PimplTrait::NativeFamily> +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = caf::telemetry::int_gauge; + using Oqaque = IntGauge::Impl; + using NativeFamily = caf::telemetry::metric_family_impl; + }; + +template <> +struct PimplTrait : PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = typename PimplTrait::NativeFamily; + using Oqaque = IntGaugeFamily::Impl; + }; + +template <> +struct PimplTrait::NativeFamily> +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = caf::telemetry::dbl_gauge; + using Oqaque = DblGauge::Impl; + using NativeFamily = caf::telemetry::metric_family_impl; + }; + +template <> +struct PimplTrait : PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = typename PimplTrait::NativeFamily; + using Oqaque = DblGaugeFamily::Impl; + }; + +template <> +struct PimplTrait::NativeFamily> +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = caf::telemetry::int_histogram; + using Oqaque = IntHistogram::Impl; + using NativeFamily = caf::telemetry::metric_family_impl; + }; + +template <> +struct PimplTrait +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = typename PimplTrait::NativeFamily; + using Oqaque = IntHistogramFamily::Impl; + }; + +template <> +struct PimplTrait::NativeFamily> +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = caf::telemetry::dbl_histogram; + using Oqaque = DblHistogram::Impl; + using NativeFamily = caf::telemetry::metric_family_impl; + }; + +template <> +struct PimplTrait +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = typename PimplTrait::NativeFamily; + using Oqaque = DblHistogramFamily::Impl; + }; + +template <> +struct PimplTrait::NativeFamily> +: PimplTrait { }; + +template <> +struct PimplTrait + { + using Native = caf::telemetry::metric_registry; + using Oqaque = Manager::Impl; + }; + +template <> +struct PimplTrait::Native> +: PimplTrait { }; + +// -- free functions ----------------------------------------------------------- + template ::Native> auto& deref(T* ptr) { @@ -87,4 +242,23 @@ auto with_native_labels(Span xs, F continuation) } } +template +auto with_native_labels(Span xs, F continuation) + { + if ( xs.size() <= 10 ) + { + caf::string_view buf[10]; + for ( size_t index = 0; index < xs.size(); ++index ) + buf[index] = xs[index]; + return continuation(Span{buf, xs.size()}); + } + else + { + std::vector buf; + for ( auto x : xs ) + buf.emplace_back(x); + return continuation(Span{buf}); + } + } + } // namespace zeek::telemetry diff --git a/src/telemetry/Gauge.cc b/src/telemetry/Gauge.cc index 0cd4a1a91e..2db5311cdb 100644 --- a/src/telemetry/Gauge.cc +++ b/src/telemetry/Gauge.cc @@ -8,48 +8,8 @@ #include "zeek/telemetry/Detail.h" -namespace ct = caf::telemetry; - namespace zeek::telemetry { -// -- bindings to private utility functions ------------------------------------ - -template <> -struct PimplTrait - { - using Native = ct::int_gauge; - using Oqaque = IntGauge::Impl; - using NativeFamily = ct::metric_family_impl; - }; - -template <> -struct PimplTrait : PimplTrait { }; - -template <> -struct PimplTrait - { - using Native = typename PimplTrait::NativeFamily; - using Oqaque = IntGaugeFamily::Impl; - }; - -template <> -struct PimplTrait - { - using Native = ct::dbl_gauge; - using Oqaque = DblGauge::Impl; - using NativeFamily = ct::metric_family_impl; - }; - -template <> -struct PimplTrait : PimplTrait { }; - -template <> -struct PimplTrait - { - using Native = typename PimplTrait::NativeFamily; - using Oqaque = DblGaugeFamily::Impl; - }; - // -- IntGauge --------------------------------------------------------------- void IntGauge::Inc() noexcept diff --git a/src/telemetry/Histogram.cc b/src/telemetry/Histogram.cc new file mode 100644 index 0000000000..b02fe88bd1 --- /dev/null +++ b/src/telemetry/Histogram.cc @@ -0,0 +1,103 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#include "zeek/telemetry/Histogram.h" + +#include "caf/telemetry/histogram.hpp" +#include "caf/telemetry/metric_family.hpp" +#include "caf/telemetry/metric_family_impl.hpp" + +#include "zeek/telemetry/Detail.h" + +#include + +namespace zeek::telemetry { + +// -- IntHistogram --------------------------------------------------------------- + +void IntHistogram::Observe(int64_t value) noexcept + { + deref(pimpl).observe(value); + } + +int64_t IntHistogram::Sum() const noexcept + { + return deref(pimpl).sum(); + } + +size_t IntHistogram::NumBuckets() const noexcept + { + return deref(pimpl).buckets().size(); + } + +int64_t IntHistogram::CountAt(size_t index) const noexcept + { + auto xs = deref(pimpl).buckets(); + assert(index < xs.size()); + return xs[index].count.value(); + } + +int64_t IntHistogram::UpperBoundAt(size_t index) const noexcept + { + auto xs = deref(pimpl).buckets(); + assert(index < xs.size()); + return xs[index].upper_bound; + } + +IntHistogramFamily::IntHistogramFamily(Impl* ptr) : MetricFamily(upcast(ptr)) + { + } + +IntHistogram IntHistogramFamily::GetOrAdd(Span labels) + { + return with_native_labels(labels, [this](auto nativeLabels) + { + auto hdl = opaque(deref(this, pimpl).get_or_add(nativeLabels)); + return IntHistogram{hdl}; + }); + } + +// -- DblHistogram --------------------------------------------------------------- + +void DblHistogram::Observe(double amount) noexcept + { + deref(pimpl).observe(amount); + } + +double DblHistogram::Sum() const noexcept + { + return deref(pimpl).sum(); + } + +size_t DblHistogram::NumBuckets() const noexcept + { + return deref(pimpl).buckets().size(); + } + +int64_t DblHistogram::CountAt(size_t index) const noexcept + { + auto xs = deref(pimpl).buckets(); + assert(index < xs.size()); + return xs[index].count.value(); + } + +double DblHistogram::UpperBoundAt(size_t index) const noexcept + { + auto xs = deref(pimpl).buckets(); + assert(index < xs.size()); + return xs[index].upper_bound; + } + +DblHistogramFamily::DblHistogramFamily(Impl* ptr) : MetricFamily(upcast(ptr)) + { + } + +DblHistogram DblHistogramFamily::GetOrAdd(Span labels) + { + return with_native_labels(labels, [this](auto nativeLabels) + { + auto hdl = opaque(deref(this, pimpl).get_or_add(nativeLabels)); + return DblHistogram{hdl}; + }); + } + +} // namespace zeek::telemetry diff --git a/src/telemetry/Histogram.h b/src/telemetry/Histogram.h new file mode 100644 index 0000000000..93a0189e18 --- /dev/null +++ b/src/telemetry/Histogram.h @@ -0,0 +1,234 @@ +// See the file "COPYING" in the main distribution directory for copyright. + +#pragma once + +#include +#include +#include + +#include "zeek/Span.h" +#include "zeek/telemetry/MetricFamily.h" + +namespace zeek::telemetry { + +class DblHistogramFamily; +class IntHistogramFamily; +class Manager; + +/** + * A handle to a metric that represents an aggregatable distribution of observed + * measurements with integer precision. Sorts individual measurements into + * configurable buckets. + */ +class IntHistogram { +public: + friend class IntHistogramFamily; + + struct Impl; + + IntHistogram() = delete; + IntHistogram(const IntHistogram&) noexcept = default; + IntHistogram& operator=(const IntHistogram&) noexcept = default; + + /** + * Increments all buckets with an upper bound less than or equal to @p value + * by one and adds @p value to the total sum of all observed values. + */ + void Observe(int64_t value) noexcept; + + /// @return The sum of all observed values. + int64_t Sum() const noexcept; + + /// @return The number of buckets, including the implicit "infinite" bucket. + size_t NumBuckets() const noexcept; + + /// @return The number of observations in the bucket at @p index. + /// @pre index < NumBuckets() + int64_t CountAt(size_t index) const noexcept; + + /// @return The upper bound of the bucket at @p index. + /// @pre index < NumBuckets() + int64_t UpperBoundAt(size_t index) const noexcept; + + /** + * @return Whether @c this and @p other refer to the same histogram. + */ + constexpr bool IsSameAs(IntHistogram other) const noexcept + { + return pimpl == other.pimpl; + } + +private: + explicit IntHistogram(Impl* ptr) noexcept : pimpl(ptr) + { + } + + Impl* pimpl; +}; + +/** + * Checks whether two @ref IntHistogram handles are identical. + * @return Whether @p lhs and @p rhs refer to the same object. + */ +constexpr bool operator==(IntHistogram lhs, IntHistogram rhs) noexcept + { + return lhs.IsSameAs(rhs); + } + +/// @relates IntHistogram +constexpr bool operator!=(IntHistogram lhs, IntHistogram rhs) noexcept + { + return !(lhs == rhs); + } + +/** + * Manages a collection of IntHistogram metrics. + */ +class IntHistogramFamily : public MetricFamily { +public: + friend class Manager; + + class Impl; + + using InstanceType = IntHistogram; + + IntHistogramFamily(const IntHistogramFamily&) noexcept = default; + IntHistogramFamily& operator=(const IntHistogramFamily&) noexcept = default; + + /** + * Returns the metrics handle for given labels, creating a new instance + * lazily if necessary. + */ + IntHistogram GetOrAdd(Span labels); + + /** + * @copydoc GetOrAdd + */ + IntHistogram GetOrAdd(std::initializer_list labels) + { + return GetOrAdd(Span{labels.begin(), labels.size()}); + } + +private: + explicit IntHistogramFamily(Impl* ptr); +}; + +/** + * A handle to a metric that represents an aggregatable distribution of observed + * measurements with floating point precision. Sorts individual measurements + * into configurable buckets. + */ +class DblHistogram { +public: + friend class DblHistogramFamily; + + struct Impl; + + DblHistogram() = delete; + DblHistogram(const DblHistogram&) noexcept = default; + DblHistogram& operator=(const DblHistogram&) noexcept = default; + + /** + * Increments all buckets with an upper bound less than or equal to @p value + * by one and adds @p value to the total sum of all observed values. + */ + void Observe(double value) noexcept; + + /// @return The sum of all observed values. + double Sum() const noexcept; + + /// @return The number of buckets, including the implicit "infinite" bucket. + size_t NumBuckets() const noexcept; + + /// @return The number of observations in the bucket at @p index. + /// @pre index < NumBuckets() + int64_t CountAt(size_t index) const noexcept; + + /// @return The upper bound of the bucket at @p index. + /// @pre index < NumBuckets() + double UpperBoundAt(size_t index) const noexcept; + + /** + * @return Whether @c this and @p other refer to the same histogram. + */ + constexpr bool IsSameAs(DblHistogram other) const noexcept + { + return pimpl == other.pimpl; + } + +private: + explicit DblHistogram(Impl* ptr) noexcept : pimpl(ptr) + { + } + + Impl* pimpl; +}; + +/** + * Checks whether two @ref DblHistogram handles are identical. + * @return Whether @p lhs and @p rhs refer to the same object. + */ +constexpr bool operator==(DblHistogram lhs, DblHistogram rhs) noexcept + { + return lhs.IsSameAs(rhs); + } + +/// @relates DblHistogram +constexpr bool operator!=(DblHistogram lhs, DblHistogram rhs) noexcept + { + return !(lhs == rhs); + } + +/** + * Manages a collection of DblHistogram metrics. + */ +class DblHistogramFamily : public MetricFamily { +public: + friend class Manager; + + class Impl; + + using InstanceType = DblHistogram; + + DblHistogramFamily(const DblHistogramFamily&) noexcept = default; + DblHistogramFamily& operator=(const DblHistogramFamily&) noexcept = default; + + /** + * Returns the metrics handle for given labels, creating a new instance + * lazily if necessary. + */ + DblHistogram GetOrAdd(Span labels); + + /** + * @copydoc GetOrAdd + */ + DblHistogram GetOrAdd(std::initializer_list labels) + { + return GetOrAdd(Span{labels.begin(), labels.size()}); + } + +private: + explicit DblHistogramFamily(Impl* ptr); +}; + +namespace detail { + +template +struct HistogramOracle { + static_assert(std::is_same::value, + "Histogram only supports int64_t and double"); + + using type = IntHistogram; +}; + +template <> +struct HistogramOracle { + using type = DblHistogram; +}; + +} // namespace detail + +template +using Histogram = typename detail::HistogramOracle::type; + +} // namespace zeek::telemetry diff --git a/src/telemetry/Manager.cc b/src/telemetry/Manager.cc index 2c006d1def..3670cd9cdf 100644 --- a/src/telemetry/Manager.cc +++ b/src/telemetry/Manager.cc @@ -6,81 +6,10 @@ #include "zeek/3rdparty/doctest.h" +#include "zeek/telemetry/Detail.h" + namespace zeek::telemetry { -namespace { - -namespace ct = caf::telemetry; - -using NativeManager = ct::metric_registry; - -using NativeIntCounter = ct::int_counter; - -using NativeIntCounterFamily = ct::metric_family_impl; - -using NativeDblCounter = ct::dbl_counter; - -using NativeDblCounterFamily = ct::metric_family_impl; - -using NativeIntGauge = ct::int_gauge; - -using NativeIntGaugeFamily = ct::metric_family_impl; - -using NativeDblGauge = ct::dbl_gauge; - -using NativeDblGaugeFamily = ct::metric_family_impl; - -auto& deref(Manager::Impl* ptr) - { - return *reinterpret_cast(ptr); - } - -auto opaque(NativeManager* ptr) - { - return reinterpret_cast(ptr); - } - -auto opaque(NativeIntCounterFamily* ptr) - { - return reinterpret_cast(ptr); - } - -auto opaque(NativeDblCounterFamily* ptr) - { - return reinterpret_cast(ptr); - } - -auto opaque(NativeIntGaugeFamily* ptr) - { - return reinterpret_cast(ptr); - } - -auto opaque(NativeDblGaugeFamily* ptr) - { - return reinterpret_cast(ptr); - } - -template -auto with_native(Span xs, F continuation) - { - if ( xs.size() <= 10 ) - { - caf::string_view buf[10]; - for ( size_t index = 0; index < xs.size(); ++index ) - buf[index] = xs[index]; - return continuation(Span{buf, xs.size()}); - } - else - { - std::vector buf; - for ( auto x : xs ) - buf.emplace_back(x); - return continuation(Span{buf}); - } - } - -} // namespace - Manager::~Manager() { } @@ -91,7 +20,7 @@ IntCounterFamily Manager::IntCounterFam(std::string_view prefix, std::string_view helptext, std::string_view unit, bool is_sum) { - return with_native(labels, [&, this](auto xs) + return with_native_labels(labels, [&, this](auto xs) { auto ptr = deref(pimpl).counter_family(prefix, name, xs, helptext, unit, is_sum); @@ -105,7 +34,7 @@ DblCounterFamily Manager::DblCounterFam(std::string_view prefix, std::string_view helptext, std::string_view unit, bool is_sum) { - return with_native(labels, [&, this](auto xs) + return with_native_labels(labels, [&, this](auto xs) { auto ptr = deref(pimpl).counter_family(prefix, name, xs, helptext, unit, is_sum); @@ -119,7 +48,7 @@ IntGaugeFamily Manager::IntGaugeFam(std::string_view prefix, std::string_view helptext, std::string_view unit, bool is_sum) { - return with_native(labels, [&, this](auto xs) + return with_native_labels(labels, [&, this](auto xs) { auto ptr = deref(pimpl).gauge_family(prefix, name, xs, helptext, unit, is_sum); @@ -133,7 +62,7 @@ DblGaugeFamily Manager::DblGaugeFam(std::string_view prefix, std::string_view helptext, std::string_view unit, bool is_sum) { - return with_native(labels, [&, this](auto xs) + return with_native_labels(labels, [&, this](auto xs) { auto ptr = deref(pimpl).gauge_family(prefix, name, xs, helptext, unit, is_sum); @@ -141,6 +70,39 @@ DblGaugeFamily Manager::DblGaugeFam(std::string_view prefix, }); } +IntHistogramFamily Manager::IntHistoFam(std::string_view prefix, + std::string_view name, + Span labels, + Span ubounds, + std::string_view helptext, + std::string_view unit, bool is_sum) + { + return with_native_labels(labels, [&, this](auto xs) + { + auto bounds = caf::span{ubounds.data(), ubounds.size()}; + auto ptr = deref(pimpl).histogram_family(prefix, name, xs, bounds, + helptext, unit, is_sum); + return IntHistogramFamily{opaque(ptr)}; + }); + } + +DblHistogramFamily Manager::DblHistoFam(std::string_view prefix, + std::string_view name, + Span labels, + Span ubounds, + std::string_view helptext, + std::string_view unit, bool is_sum) + { + return with_native_labels(labels, [&, this](auto xs) + { + auto bounds = caf::span{ubounds.data(), ubounds.size()}; + auto ptr = deref(pimpl).histogram_family(prefix, name, xs, + bounds, helptext, + unit, is_sum); + return DblHistogramFamily{opaque(ptr)}; + }); + } + } // namespace zeek::telemetry // -- unit tests --------------------------------------------------------------- @@ -148,6 +110,8 @@ DblGaugeFamily Manager::DblGaugeFam(std::string_view prefix, using namespace std::literals; using namespace zeek::telemetry; +using NativeManager = caf::telemetry::metric_registry; + namespace { template @@ -241,13 +205,13 @@ SCENARIO("telemetry managers provide access to counter families") CHECK_EQ(family.Unit(), "1"sv); CHECK_EQ(family.IsSum(), true); } - AND_THEN("getOrAdd returns the same metric for the same labels") + AND_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") + AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family.GetOrAdd({{"method", "get"}}); auto second = family.GetOrAdd({{"method", "put"}}); @@ -266,13 +230,13 @@ SCENARIO("telemetry managers provide access to counter families") CHECK_EQ(family.Unit(), "seconds"sv); CHECK_EQ(family.IsSum(), true); } - AND_THEN("getOrAdd returns the same metric for the same labels") + AND_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") + AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family.GetOrAdd({{"query", "foo"}}); auto second = family.GetOrAdd({{"query", "bar"}}); @@ -371,13 +335,13 @@ SCENARIO("telemetry managers provide access to gauge families") CHECK_EQ(family.Unit(), "1"sv); CHECK_EQ(family.IsSum(), false); } - AND_THEN("getOrAdd returns the same metric for the same labels") + AND_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") + AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family.GetOrAdd({{"protocol", "tcp"}}); auto second = family.GetOrAdd({{"protocol", "quic"}}); @@ -396,13 +360,13 @@ SCENARIO("telemetry managers provide access to gauge families") CHECK_EQ(family.Unit(), "meters"sv); CHECK_EQ(family.IsSum(), false); } - AND_THEN("getOrAdd returns the same metric for the same labels") + AND_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") + AND_THEN("GetOrAdd returns different metric for the disjoint labels") { auto first = family.GetOrAdd({{"query", "Sacramento"}}); auto second = family.GetOrAdd({{"query", "San Joaquin"}}); @@ -411,3 +375,151 @@ SCENARIO("telemetry managers provide access to gauge families") } } } + +SCENARIO("telemetry managers provide access to histogram singletons") + { + GIVEN("a telemetry manager") + { + NativeManager native_mgr; + Manager mgr{opaque(&native_mgr)}; + WHEN("retrieving an IntHistogram singleton") + { + const auto max_int = std::numeric_limits::max(); + int64_t buckets [] = { 10, 20 }; + auto first = mgr.HistogramSingleton("zeek", "int-hist", buckets, "test"); + THEN("it initially has no observations") + { + REQUIRE_EQ(first.NumBuckets(), 3u); + CHECK_EQ(first.Sum(), 0); + CHECK_EQ(first.CountAt(0), 0); + CHECK_EQ(first.CountAt(1), 0); + CHECK_EQ(first.CountAt(2), 0); + CHECK_EQ(first.UpperBoundAt(0), 10); + CHECK_EQ(first.UpperBoundAt(1), 20); + CHECK_EQ(first.UpperBoundAt(2), max_int); + } + AND_THEN("calling Observe() increments bucket counters") + { + first.Observe(1); + first.Observe(9); + first.Observe(10); + first.Observe(11); + first.Observe(19); + first.Observe(20); + first.Observe(21); + CHECK_EQ(first.Sum(), 91); + CHECK_EQ(first.CountAt(0), 3); + CHECK_EQ(first.CountAt(1), 3); + CHECK_EQ(first.CountAt(2), 1); + } + AND_THEN("calling HistogramSingleton again for the same name returns the same handle") + { + auto second = mgr.HistogramSingleton("zeek", "int-hist", buckets, "test"); + CHECK_EQ(first, second); + } + AND_THEN("calling HistogramSingleton for a different name returns another handle") + { + auto third = mgr.HistogramSingleton("zeek", "int-hist-2", buckets, "test"); + CHECK_NE(first, third); + } + } + WHEN("retrieving a DblHistogram singleton") + { + double buckets [] = { 10.0, 20.0 }; + auto first = mgr.HistogramSingleton("zeek", "dbl-count", buckets, "test"); + THEN("it initially has no observations") + { + REQUIRE_EQ(first.NumBuckets(), 3u); + CHECK_EQ(first.Sum(), 0.0); + CHECK_EQ(first.CountAt(0), 0); + CHECK_EQ(first.CountAt(1), 0); + CHECK_EQ(first.CountAt(2), 0); + CHECK_EQ(first.UpperBoundAt(0), 10.0); + CHECK_EQ(first.UpperBoundAt(1), 20.0); + } + AND_THEN("calling Observe() increments bucket counters") + { + first.Observe(2.0); + first.Observe(4.0); + first.Observe(8.0); + first.Observe(16.0); + first.Observe(32.0); + CHECK_EQ(first.Sum(), 62.0); + CHECK_EQ(first.CountAt(0), 3); + CHECK_EQ(first.CountAt(1), 1); + CHECK_EQ(first.CountAt(2), 1); + } + AND_THEN("calling histogramSingleton again for the same name returns the same handle") + { + auto second = mgr.HistogramSingleton("zeek", "dbl-count", buckets, "test"); + CHECK_EQ(first, second); + } + AND_THEN("calling histogramSingleton for a different name returns another handle") + { + auto third = mgr.HistogramSingleton("zeek", "dbl-count-2", buckets, "test"); + CHECK_NE(first, third); + } + } + } + } + +SCENARIO("telemetry managers provide access to histogram families") + { + GIVEN("a telemetry manager") + { + NativeManager native_mgr; + Manager mgr{opaque(&native_mgr)}; + WHEN("retrieving an IntHistogram family") + { + int64_t buckets [] = { 10, 20 }; + auto family = mgr.HistogramFamily("zeek", "payload-size", {"protocol"}, buckets, "test", "bytes"); + THEN("the family object stores the parameters") + { + CHECK_EQ(family.Prefix(), "zeek"sv); + CHECK_EQ(family.Name(), "payload-size"sv); + CHECK_EQ(toVector(family.LabelNames()), std::vector{"protocol"s}); + CHECK_EQ(family.Helptext(), "test"sv); + CHECK_EQ(family.Unit(), "bytes"sv); + CHECK_EQ(family.IsSum(), false); + } + AND_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("the family object stores the parameters") + { + CHECK_EQ(family.Prefix(), "zeek"sv); + CHECK_EQ(family.Name(), "parse-time"sv); + CHECK_EQ(toVector(family.LabelNames()), std::vector{"protocol"s}); + CHECK_EQ(family.Helptext(), "test"sv); + CHECK_EQ(family.Unit(), "seconds"sv); + CHECK_EQ(family.IsSum(), false); + } + AND_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); + } + } + } + } diff --git a/src/telemetry/Manager.h b/src/telemetry/Manager.h index a6844885db..9c8aa08427 100644 --- a/src/telemetry/Manager.h +++ b/src/telemetry/Manager.h @@ -10,6 +10,7 @@ #include "zeek/Span.h" #include "zeek/telemetry/Counter.h" #include "zeek/telemetry/Gauge.h" +#include "zeek/telemetry/Histogram.h" namespace zeek::telemetry { @@ -143,7 +144,8 @@ public: auto GaugeFamily(std::string_view prefix, std::string_view name, Span labels, std::string_view helptext, - std::string_view unit = "1", bool is_sum = false) { + std::string_view unit = "1", bool is_sum = false) + { if constexpr (std::is_same::value) { return IntGaugeFam(prefix, name, labels, helptext, unit, is_sum); @@ -154,7 +156,7 @@ public: "metrics only support int64_t and double values"); return DblGaugeFam(prefix, name, labels, helptext, unit, is_sum); } - } + } /// @copydoc GaugeFamily template @@ -162,11 +164,11 @@ public: std::initializer_list labels, std::string_view helptext, std::string_view unit = "1", bool is_sum = false) - { - auto lbl_span = Span{labels.begin(), labels.size()}; - return GaugeFamily(prefix, name, lbl_span, helptext, - unit, is_sum); - } + { + auto lbl_span = Span{labels.begin(), labels.size()}; + return GaugeFamily(prefix, name, lbl_span, helptext, + unit, is_sum); + } /** * Accesses a gauge instance. Creates the hosting metric family as well @@ -228,6 +230,160 @@ public: return fam.GetOrAdd({}); } + // Forces the compiler to use the type `Span` instead of trying to + // match paremeters to a `span`. + template + struct ConstSpanOracle + { + using Type = Span; + }; + + // Convenience alias to safe some typing. + template + using ConstSpan = typename ConstSpanOracle::Type; + + /** + * Returns a histogram metric family. Creates the family lazily if + * necessary. + * @param prefix The prefix (namespace) this family belongs to. Usually the + * application or protocol name, e.g., `http`. The prefix `caf` + * as well as prefixes starting with an underscore are + * reserved. + * @param name The human-readable name of the metric, e.g., `requests`. + * @param labels Names for all label dimensions of the metric. + * @param default_upper_bounds Upper bounds for the metric buckets. + * @param helptext Short explanation of the metric. + * @param unit Unit of measurement. Please use base units such as `bytes` or + * `seconds` (prefer lowercase). The pseudo-unit `1` identifies + * dimensionless counts. + * @param is_sum Setting this to `true` indicates that this metric adds + * something up to a total, where only the total value is of + * interest. For example, the total number of HTTP requests. + * @note The first call wins when calling this function multiple times with + * different bucket settings. Users may also override + * @p default_upper_bounds via run-time configuration. + */ + template + auto HistogramFamily(std::string_view prefix, std::string_view name, + Span labels, + ConstSpan default_upper_bounds, + std::string_view helptext, std::string_view unit = "1", + bool is_sum = false) + { + if constexpr (std::is_same::value) + { + return IntHistoFam(prefix, name, labels, default_upper_bounds, + helptext, unit, is_sum); + } + else + { + static_assert(std::is_same::value, + "metrics only support int64_t and double values"); + return DblHistoFam(prefix, name, labels, default_upper_bounds, + helptext, unit, is_sum); + } + } + + /// @copydoc HistogramFamily + template + auto HistogramFamily(std::string_view prefix, std::string_view name, + std::initializer_list labels, + ConstSpan default_upper_bounds, + std::string_view helptext, std::string_view unit = "1", + bool is_sum = false) + { + auto lbl_span = Span{labels.begin(), labels.size()}; + return HistogramFamily(prefix, name, lbl_span, + default_upper_bounds, helptext, + unit, is_sum); + } + + /** + * Returns a histogram. Creates the family lazily if necessary. + * @param prefix The prefix (namespace) this family belongs to. Usually the + * application or protocol name, e.g., `http`. The prefix `caf` + * as well as prefixes starting with an underscore are + * reserved. + * @param name The human-readable name of the metric, e.g., `requests`. + * @param labels Names for all label dimensions of the metric. + * @param default_upper_bounds Upper bounds for the metric buckets. + * @param helptext Short explanation of the metric. + * @param unit Unit of measurement. Please use base units such as `bytes` or + * `seconds` (prefer lowercase). The pseudo-unit `1` identifies + * dimensionless counts. + * @param is_sum Setting this to `true` indicates that this metric adds + * something up to a total, where only the total value is of + * interest. For example, the total number of HTTP requests. + * @note The first call wins when calling this function multiple times with + * different bucket settings. Users may also override + * @p default_upper_bounds via run-time configuration. + */ + template + Histogram + HistogramInstance(std::string_view prefix, std::string_view name, + Span labels, + ConstSpan default_upper_bounds, + std::string_view helptext, std::string_view unit = "1", + bool is_sum = false) + { + return WithLabelNames(labels, [&, this](auto labelNames) + { + auto family = HistogramFamily(prefix, name, labelNames, + default_upper_bounds, + helptext, unit, is_sum); + return family.getOrAdd(labels); + }); + } + + /// @copdoc HistogramInstance + template + Histogram + HistogramInstance(std::string_view prefix, std::string_view name, + std::initializer_list labels, + ConstSpan default_upper_bounds, + std::string_view helptext, std::string_view unit = "1", + bool is_sum = false) + { + auto lbls = Span{labels.begin(), labels.size()}; + return HistogramInstance(prefix, name, lbls, default_upper_bounds, + helptext, unit, is_sum); + } + + /** + * Returns a histogram metric singleton, i.e., the single instance of a + * family without label dimensions. Creates all objects lazily if necessary, + * but fails if the full name already belongs to a different family. + * @param prefix The prefix (namespace) this family belongs to. Usually the + * application or protocol name, e.g., `http`. The prefix `caf` + * as well as prefixes starting with an underscore are + * reserved. + * @param name The human-readable name of the metric, e.g., `requests`. + * @param default_upper_bounds Upper bounds for the metric buckets. + * @param helptext Short explanation of the metric. + * @param unit Unit of measurement. Please use base units such as `bytes` or + * `seconds` (prefer lowercase). The pseudo-unit `1` identifies + * dimensionless counts. + * @param is_sum Setting this to `true` indicates that this metric adds + * something up to a total, where only the total value is of + * interest. For example, the total number of HTTP requests. + * @note The first call wins when calling this function multiple times with + * different bucket settings. Users may also override + * @p default_upper_bounds via run-time configuration. + */ + template + Histogram + HistogramSingleton(std::string_view prefix, std::string_view name, + ConstSpan default_upper_bounds, + std::string_view helptext, std::string_view unit = "1", + bool is_sum = false) + { + auto lbls = Span{}; + auto fam = HistogramFamily(prefix, name, lbls, + default_upper_bounds, helptext, + unit, is_sum); + return fam.GetOrAdd({}); + } + private: IntCounterFamily IntCounterFam(std::string_view prefix, std::string_view name, @@ -253,6 +409,18 @@ private: std::string_view helptext, std::string_view unit, bool is_sum); + IntHistogramFamily + IntHistoFam(std::string_view prefix, std::string_view name, + Span labels, + Span ubounds, std::string_view helptext, + std::string_view unit, bool is_sum); + + DblHistogramFamily + DblHistoFam(std::string_view prefix, std::string_view name, + Span labels, + Span ubounds, std::string_view helptext, + std::string_view unit, bool is_sum); + template static void WithLabelNames(Span xs, F continuation) { @@ -276,8 +444,6 @@ private: namespace zeek { -// @note for technically reasons (CAF dependency), this variable gets -// initialized in broker/Manager.cc. extern telemetry::Manager* telemetry_mgr; } // namespace zeek