aboutsummaryrefslogtreecommitdiffstats
path: root/library/cpp/monlib/dynamic_counters
diff options
context:
space:
mode:
authorDevtools Arcadia <arcadia-devtools@yandex-team.ru>2022-02-07 18:08:42 +0300
committerDevtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net>2022-02-07 18:08:42 +0300
commit1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch)
treee26c9fed0de5d9873cce7e00bc214573dc2195b7 /library/cpp/monlib/dynamic_counters
downloadydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'library/cpp/monlib/dynamic_counters')
-rw-r--r--library/cpp/monlib/dynamic_counters/contention_ut.cpp61
-rw-r--r--library/cpp/monlib/dynamic_counters/counters.cpp308
-rw-r--r--library/cpp/monlib/dynamic_counters/counters.h374
-rw-r--r--library/cpp/monlib/dynamic_counters/counters_ut.cpp342
-rw-r--r--library/cpp/monlib/dynamic_counters/encode.cpp131
-rw-r--r--library/cpp/monlib/dynamic_counters/encode.h23
-rw-r--r--library/cpp/monlib/dynamic_counters/encode_ut.cpp226
-rw-r--r--library/cpp/monlib/dynamic_counters/golovan_page.cpp79
-rw-r--r--library/cpp/monlib/dynamic_counters/golovan_page.h25
-rw-r--r--library/cpp/monlib/dynamic_counters/page.cpp141
-rw-r--r--library/cpp/monlib/dynamic_counters/page.h50
-rw-r--r--library/cpp/monlib/dynamic_counters/percentile/percentile.h59
-rw-r--r--library/cpp/monlib/dynamic_counters/percentile/percentile_base.h36
-rw-r--r--library/cpp/monlib/dynamic_counters/percentile/percentile_lg.h182
-rw-r--r--library/cpp/monlib/dynamic_counters/percentile/percentile_ut.cpp129
-rw-r--r--library/cpp/monlib/dynamic_counters/percentile/ut/ya.make9
-rw-r--r--library/cpp/monlib/dynamic_counters/percentile/ya.make15
-rw-r--r--library/cpp/monlib/dynamic_counters/ut/ya.make16
-rw-r--r--library/cpp/monlib/dynamic_counters/ya.make27
19 files changed, 2233 insertions, 0 deletions
diff --git a/library/cpp/monlib/dynamic_counters/contention_ut.cpp b/library/cpp/monlib/dynamic_counters/contention_ut.cpp
new file mode 100644
index 0000000000..8798044ee3
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/contention_ut.cpp
@@ -0,0 +1,61 @@
+#include "counters.h"
+#include <library/cpp/testing/unittest/registar.h>
+#include <util/system/event.h>
+#include <util/system/thread.h>
+
+using namespace NMonitoring;
+
+Y_UNIT_TEST_SUITE(TDynamicCountersContentionTest) {
+
+ Y_UNIT_TEST(EnsureNonlocking) {
+ TDynamicCounterPtr counters = MakeIntrusive<TDynamicCounters>();
+
+ class TConsumer : public ICountableConsumer {
+ TAutoEvent Ev;
+ TAutoEvent Response;
+ TDynamicCounterPtr Counters;
+ TThread Thread;
+
+ public:
+ TConsumer(TDynamicCounterPtr counters)
+ : Counters(counters)
+ , Thread(std::bind(&TConsumer::ThreadFunc, this))
+ {
+ Thread.Start();
+ }
+
+ ~TConsumer() override {
+ Thread.Join();
+ }
+
+ void OnCounter(const TString& /*labelName*/, const TString& /*labelValue*/, const TCounterForPtr* /*counter*/) override {
+ Ev.Signal();
+ Response.Wait();
+ }
+
+ void OnHistogram(const TString& /*labelName*/, const TString& /*labelValue*/, IHistogramSnapshotPtr /*snapshot*/, bool /*derivative*/) override {
+ }
+
+ void OnGroupBegin(const TString& /*labelName*/, const TString& /*labelValue*/, const TDynamicCounters* /*group*/) override {
+ }
+
+ void OnGroupEnd(const TString& /*labelName*/, const TString& /*labelValue*/, const TDynamicCounters* /*group*/) override {
+ }
+
+ private:
+ void ThreadFunc() {
+ // acts like a coroutine
+ Ev.Wait();
+ auto ctr = Counters->GetSubgroup("label", "value")->GetCounter("name");
+ Y_VERIFY(*ctr == 42);
+ Response.Signal();
+ }
+ };
+
+ auto ctr = counters->GetSubgroup("label", "value")->GetCounter("name");
+ *ctr = 42;
+ TConsumer consumer(counters);
+ counters->Accept({}, {}, consumer);
+ }
+
+}
diff --git a/library/cpp/monlib/dynamic_counters/counters.cpp b/library/cpp/monlib/dynamic_counters/counters.cpp
new file mode 100644
index 0000000000..3635d87d0d
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/counters.cpp
@@ -0,0 +1,308 @@
+#include "counters.h"
+
+#include <library/cpp/monlib/service/pages/templates.h>
+
+#include <util/generic/cast.h>
+
+using namespace NMonitoring;
+
+namespace {
+ TDynamicCounters* AsDynamicCounters(const TIntrusivePtr<TCountableBase>& ptr) {
+ return dynamic_cast<TDynamicCounters*>(ptr.Get());
+ }
+
+ TCounterForPtr* AsCounter(const TIntrusivePtr<TCountableBase>& ptr) {
+ return dynamic_cast<TCounterForPtr*>(ptr.Get());
+ }
+
+ TExpiringCounter* AsExpiringCounter(const TIntrusivePtr<TCountableBase>& ptr) {
+ return dynamic_cast<TExpiringCounter*>(ptr.Get());
+ }
+
+ TExpiringHistogramCounter* AsExpiringHistogramCounter(const TIntrusivePtr<TCountableBase>& ptr) {
+ return dynamic_cast<TExpiringHistogramCounter*>(ptr.Get());
+ }
+
+ THistogramCounter* AsHistogram(const TIntrusivePtr<TCountableBase>& ptr) {
+ return dynamic_cast<THistogramCounter*>(ptr.Get());
+ }
+
+ TIntrusivePtr<TCounterForPtr> AsCounterRef(const TIntrusivePtr<TCountableBase>& ptr) {
+ return VerifyDynamicCast<TCounterForPtr*>(ptr.Get());
+ }
+
+ TIntrusivePtr<TDynamicCounters> AsGroupRef(const TIntrusivePtr<TCountableBase>& ptr) {
+ return VerifyDynamicCast<TDynamicCounters*>(ptr.Get());
+ }
+
+ THistogramPtr AsHistogramRef(const TIntrusivePtr<TCountableBase>& ptr) {
+ return VerifyDynamicCast<THistogramCounter*>(ptr.Get());
+ }
+
+ bool IsExpiringCounter(const TIntrusivePtr<TCountableBase>& ptr) {
+ return AsExpiringCounter(ptr) != nullptr || AsExpiringHistogramCounter(ptr) != nullptr;
+ }
+}
+
+static constexpr TStringBuf INDENT = " ";
+
+TDynamicCounters::TDynamicCounters(EVisibility vis)
+{
+ Visibility_ = vis;
+}
+
+TDynamicCounters::~TDynamicCounters() {
+}
+
+TDynamicCounters::TCounterPtr TDynamicCounters::GetExpiringCounter(const TString& value, bool derivative, EVisibility vis) {
+ return GetExpiringNamedCounter("sensor", value, derivative, vis);
+}
+
+TDynamicCounters::TCounterPtr TDynamicCounters::GetExpiringNamedCounter(const TString& name, const TString& value, bool derivative, EVisibility vis) {
+ return AsCounterRef(GetNamedCounterImpl<true, TExpiringCounter>(name, value, derivative, vis));
+}
+
+TDynamicCounters::TCounterPtr TDynamicCounters::GetCounter(const TString& value, bool derivative, EVisibility vis) {
+ return GetNamedCounter("sensor", value, derivative, vis);
+}
+
+TDynamicCounters::TCounterPtr TDynamicCounters::GetNamedCounter(const TString& name, const TString& value, bool derivative, EVisibility vis) {
+ return AsCounterRef(GetNamedCounterImpl<false, TCounterForPtr>(name, value, derivative, vis));
+}
+
+THistogramPtr TDynamicCounters::GetHistogram(const TString& value, IHistogramCollectorPtr collector, bool derivative, EVisibility vis) {
+ return GetNamedHistogram("sensor", value, std::move(collector), derivative, vis);
+}
+
+THistogramPtr TDynamicCounters::GetNamedHistogram(const TString& name, const TString& value, IHistogramCollectorPtr collector, bool derivative, EVisibility vis) {
+ return AsHistogramRef(GetNamedCounterImpl<false, THistogramCounter>(name, value, std::move(collector), derivative, vis));
+}
+
+THistogramPtr TDynamicCounters::GetExpiringHistogram(const TString& value, IHistogramCollectorPtr collector, bool derivative, EVisibility vis) {
+ return GetExpiringNamedHistogram("sensor", value, std::move(collector), derivative, vis);
+}
+
+THistogramPtr TDynamicCounters::GetExpiringNamedHistogram(const TString& name, const TString& value, IHistogramCollectorPtr collector, bool derivative, EVisibility vis) {
+ return AsHistogramRef(GetNamedCounterImpl<true, TExpiringHistogramCounter>(name, value, std::move(collector), derivative, vis));
+}
+
+TDynamicCounters::TCounterPtr TDynamicCounters::FindCounter(const TString& value) const {
+ return FindNamedCounter("sensor", value);
+}
+
+TDynamicCounters::TCounterPtr TDynamicCounters::FindNamedCounter(const TString& name, const TString& value) const {
+ return AsCounterRef(FindNamedCounterImpl<TCounterForPtr>(name, value));
+}
+
+THistogramPtr TDynamicCounters::FindHistogram(const TString& value) const {
+ return FindNamedHistogram("sensor", value);
+}
+
+THistogramPtr TDynamicCounters::FindNamedHistogram(const TString& name,const TString& value) const {
+ return AsHistogramRef(FindNamedCounterImpl<THistogramCounter>(name, value));
+}
+
+void TDynamicCounters::RemoveCounter(const TString &value) {
+ RemoveNamedCounter("sensor", value);
+}
+
+void TDynamicCounters::RemoveNamedCounter(const TString& name, const TString &value) {
+ auto g = LockForUpdate("RemoveNamedCounter", name, value);
+ if (const auto it = Counters.find({name, value}); it != Counters.end() && AsCounter(it->second)) {
+ Counters.erase(it);
+ }
+}
+
+TIntrusivePtr<TDynamicCounters> TDynamicCounters::GetSubgroup(const TString& name, const TString& value) {
+ auto res = FindSubgroup(name, value);
+ if (!res) {
+ auto g = LockForUpdate("GetSubgroup", name, value);
+ const TChildId key(name, value);
+ if (const auto it = Counters.lower_bound(key); it != Counters.end() && it->first == key) {
+ res = AsGroupRef(it->second);
+ } else {
+ res = MakeIntrusive<TDynamicCounters>(this);
+ Counters.emplace_hint(it, key, res);
+ }
+ }
+ return res;
+}
+
+TIntrusivePtr<TDynamicCounters> TDynamicCounters::FindSubgroup(const TString& name, const TString& value) const {
+ TReadGuard g(Lock);
+ const auto it = Counters.find({name, value});
+ return it != Counters.end() ? AsDynamicCounters(it->second) : nullptr;
+}
+
+void TDynamicCounters::RemoveSubgroup(const TString& name, const TString& value) {
+ auto g = LockForUpdate("RemoveSubgroup", name, value);
+ if (const auto it = Counters.find({name, value}); it != Counters.end() && AsDynamicCounters(it->second)) {
+ Counters.erase(it);
+ }
+}
+
+void TDynamicCounters::ReplaceSubgroup(const TString& name, const TString& value, TIntrusivePtr<TDynamicCounters> subgroup) {
+ auto g = LockForUpdate("ReplaceSubgroup", name, value);
+ const auto it = Counters.find({name, value});
+ Y_VERIFY(it != Counters.end() && AsDynamicCounters(it->second));
+ it->second = std::move(subgroup);
+}
+
+void TDynamicCounters::MergeWithSubgroup(const TString& name, const TString& value) {
+ auto g = LockForUpdate("MergeWithSubgroup", name, value);
+ auto it = Counters.find({name, value});
+ Y_VERIFY(it != Counters.end());
+ TIntrusivePtr<TDynamicCounters> subgroup = AsDynamicCounters(it->second);
+ Y_VERIFY(subgroup);
+ Counters.erase(it);
+ Counters.merge(subgroup->Resign());
+ AtomicAdd(ExpiringCount, AtomicSwap(&subgroup->ExpiringCount, 0));
+}
+
+void TDynamicCounters::ResetCounters(bool derivOnly) {
+ TReadGuard g(Lock);
+ for (auto& [key, value] : Counters) {
+ if (auto counter = AsCounter(value)) {
+ if (!derivOnly || counter->ForDerivative()) {
+ *counter = 0;
+ }
+ } else if (auto subgroup = AsDynamicCounters(value)) {
+ subgroup->ResetCounters(derivOnly);
+ }
+ }
+}
+
+void TDynamicCounters::RegisterCountable(const TString& name, const TString& value, TCountablePtr countable) {
+ Y_VERIFY(countable);
+ auto g = LockForUpdate("RegisterCountable", name, value);
+ const bool inserted = Counters.emplace(TChildId(name, value), std::move(countable)).second;
+ Y_VERIFY(inserted);
+}
+
+void TDynamicCounters::RegisterSubgroup(const TString& name, const TString& value, TIntrusivePtr<TDynamicCounters> subgroup) {
+ RegisterCountable(name, value, subgroup);
+}
+
+void TDynamicCounters::OutputHtml(IOutputStream& os) const {
+ HTML(os) {
+ PRE() {
+ OutputPlainText(os);
+ }
+ }
+}
+
+void TDynamicCounters::EnumerateSubgroups(const std::function<void(const TString& name, const TString& value)>& output) const {
+ TReadGuard g(Lock);
+ for (const auto& [key, value] : Counters) {
+ if (AsDynamicCounters(value)) {
+ output(key.LabelName, key.LabelValue);
+ }
+ }
+}
+
+void TDynamicCounters::OutputPlainText(IOutputStream& os, const TString& indent) const {
+ auto snap = ReadSnapshot();
+ // mark private records in plain text output
+ auto outputVisibilityMarker = [] (EVisibility vis) {
+ return vis == EVisibility::Private ? "\t[PRIVATE]" : "";
+ };
+
+ for (const auto& [key, value] : snap) {
+ if (const auto counter = AsCounter(value)) {
+ os << indent
+ << key.LabelName << '=' << key.LabelValue
+ << ": " << counter->Val()
+ << outputVisibilityMarker(counter->Visibility())
+ << '\n';
+ } else if (const auto histogram = AsHistogram(value)) {
+ os << indent
+ << key.LabelName << '=' << key.LabelValue
+ << ":"
+ << outputVisibilityMarker(histogram->Visibility())
+ << "\n";
+
+ auto snapshot = histogram->Snapshot();
+ for (ui32 i = 0, count = snapshot->Count(); i < count; i++) {
+ os << indent << INDENT << TStringBuf("bin=");
+ TBucketBound bound = snapshot->UpperBound(i);
+ if (bound == Max<TBucketBound>()) {
+ os << TStringBuf("inf");
+ } else {
+ os << bound;
+ }
+ os << ": " << snapshot->Value(i) << '\n';
+ }
+ }
+ }
+
+ for (const auto& [key, value] : snap) {
+ if (const auto subgroup = AsDynamicCounters(value)) {
+ os << "\n";
+ os << indent << key.LabelName << "=" << key.LabelValue << ":\n";
+ subgroup->OutputPlainText(os, indent + INDENT);
+ }
+ }
+}
+
+void TDynamicCounters::Accept(const TString& labelName, const TString& labelValue, ICountableConsumer& consumer) const {
+ if (!IsVisible(Visibility(), consumer.Visibility())) {
+ return;
+ }
+
+ consumer.OnGroupBegin(labelName, labelValue, this);
+ for (auto& [key, value] : ReadSnapshot()) {
+ value->Accept(key.LabelName, key.LabelValue, consumer);
+ }
+ consumer.OnGroupEnd(labelName, labelValue, this);
+}
+
+void TDynamicCounters::RemoveExpired() const {
+ if (AtomicGet(ExpiringCount) == 0) {
+ return;
+ }
+
+ TWriteGuard g(Lock);
+ TAtomicBase count = 0;
+
+ for (auto it = Counters.begin(); it != Counters.end();) {
+ if (IsExpiringCounter(it->second) && it->second->RefCount() == 1) {
+ it = Counters.erase(it);
+ ++count;
+ } else {
+ ++it;
+ }
+ }
+
+ AtomicSub(ExpiringCount, count);
+}
+
+template <bool expiring, class TCounterType, class... TArgs>
+TDynamicCounters::TCountablePtr TDynamicCounters::GetNamedCounterImpl(const TString& name, const TString& value, TArgs&&... args) {
+ {
+ TReadGuard g(Lock);
+ auto it = Counters.find({name, value});
+ if (it != Counters.end()) {
+ return it->second;
+ }
+ }
+
+ auto g = LockForUpdate("GetNamedCounterImpl", name, value);
+ const TChildId key(name, value);
+ auto it = Counters.lower_bound(key);
+ if (it == Counters.end() || it->first != key) {
+ auto value = MakeIntrusive<TCounterType>(std::forward<TArgs>(args)...);
+ it = Counters.emplace_hint(it, key, value);
+ if constexpr (expiring) {
+ AtomicIncrement(ExpiringCount);
+ }
+ }
+ return it->second;
+}
+
+template <class TCounterType>
+TDynamicCounters::TCountablePtr TDynamicCounters::FindNamedCounterImpl(const TString& name, const TString& value) const {
+ TReadGuard g(Lock);
+ auto it = Counters.find({name, value});
+ return it != Counters.end() ? it->second : nullptr;
+}
+
diff --git a/library/cpp/monlib/dynamic_counters/counters.h b/library/cpp/monlib/dynamic_counters/counters.h
new file mode 100644
index 0000000000..dc178cfbe0
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/counters.h
@@ -0,0 +1,374 @@
+#pragma once
+
+#include <library/cpp/monlib/counters/counters.h>
+#include <library/cpp/monlib/metrics/histogram_collector.h>
+
+#include <library/cpp/threading/light_rw_lock/lightrwlock.h>
+#include <library/cpp/containers/stack_vector/stack_vec.h>
+
+#include <util/generic/cast.h>
+#include <util/generic/map.h>
+#include <util/generic/ptr.h>
+#include <util/string/cast.h>
+#include <util/system/rwlock.h>
+
+#include <functional>
+
+namespace NMonitoring {
+ struct TCounterForPtr;
+ struct TDynamicCounters;
+ struct ICountableConsumer;
+
+
+ struct TCountableBase: public TAtomicRefCount<TCountableBase> {
+ // Private means that the object must not be serialized unless the consumer
+ // has explicitly specified this by setting its Visibility to Private.
+ //
+ // Works only for the methods that accept ICountableConsumer
+ enum class EVisibility: ui8 {
+ Unspecified,
+ Public,
+ Private,
+ };
+
+ virtual ~TCountableBase() {
+ }
+
+ virtual void Accept(
+ const TString& labelName, const TString& labelValue,
+ ICountableConsumer& consumer) const = 0;
+
+ virtual EVisibility Visibility() const {
+ return Visibility_;
+ }
+
+ protected:
+ EVisibility Visibility_{EVisibility::Unspecified};
+ };
+
+ inline bool IsVisible(TCountableBase::EVisibility myLevel, TCountableBase::EVisibility consumerLevel) {
+ if (myLevel == TCountableBase::EVisibility::Private
+ && consumerLevel != TCountableBase::EVisibility::Private) {
+
+ return false;
+ }
+
+ return true;
+ }
+
+ struct ICountableConsumer {
+ virtual ~ICountableConsumer() {
+ }
+
+ virtual void OnCounter(
+ const TString& labelName, const TString& labelValue,
+ const TCounterForPtr* counter) = 0;
+
+ virtual void OnHistogram(
+ const TString& labelName, const TString& labelValue,
+ IHistogramSnapshotPtr snapshot, bool derivative) = 0;
+
+ virtual void OnGroupBegin(
+ const TString& labelName, const TString& labelValue,
+ const TDynamicCounters* group) = 0;
+
+ virtual void OnGroupEnd(
+ const TString& labelName, const TString& labelValue,
+ const TDynamicCounters* group) = 0;
+
+ virtual TCountableBase::EVisibility Visibility() const {
+ return TCountableBase::EVisibility::Unspecified;
+ }
+ };
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4522) // multiple assignment operators specified
+#endif // _MSC_VER
+
+ struct TCounterForPtr: public TDeprecatedCounter, public TCountableBase {
+ TCounterForPtr(bool derivative = false, EVisibility vis = EVisibility::Public)
+ : TDeprecatedCounter(0ULL, derivative)
+ {
+ Visibility_ = vis;
+ }
+
+ TCounterForPtr(const TCounterForPtr&) = delete;
+ TCounterForPtr& operator=(const TCounterForPtr& other) = delete;
+
+ void Accept(
+ const TString& labelName, const TString& labelValue,
+ ICountableConsumer& consumer) const override {
+ if (IsVisible(Visibility(), consumer.Visibility())) {
+ consumer.OnCounter(labelName, labelValue, this);
+ }
+ }
+
+ TCountableBase::EVisibility Visibility() const override {
+ return Visibility_;
+ }
+
+ using TDeprecatedCounter::operator++;
+ using TDeprecatedCounter::operator--;
+ using TDeprecatedCounter::operator+=;
+ using TDeprecatedCounter::operator-=;
+ using TDeprecatedCounter::operator=;
+ using TDeprecatedCounter::operator!;
+ };
+
+ struct TExpiringCounter: public TCounterForPtr {
+ explicit TExpiringCounter(bool derivative = false, EVisibility vis = EVisibility::Public)
+ : TCounterForPtr{derivative}
+ {
+ Visibility_ = vis;
+ }
+
+ void Reset() {
+ TDeprecatedCounter::operator=(0);
+ }
+ };
+
+ struct THistogramCounter: public TCountableBase {
+ explicit THistogramCounter(
+ IHistogramCollectorPtr collector, bool derivative = true, EVisibility vis = EVisibility::Public)
+ : Collector_(std::move(collector))
+ , Derivative_(derivative)
+ {
+ Visibility_ = vis;
+ }
+
+ void Collect(i64 value) {
+ Collector_->Collect(value);
+ }
+
+ void Collect(i64 value, ui32 count) {
+ Collector_->Collect(value, count);
+ }
+
+ void Collect(double value, ui32 count) {
+ Collector_->Collect(value, count);
+ }
+
+ void Collect(const IHistogramSnapshot& snapshot) {
+ Collector_->Collect(snapshot);
+ }
+
+ void Accept(
+ const TString& labelName, const TString& labelValue,
+ ICountableConsumer& consumer) const override
+ {
+ if (IsVisible(Visibility(), consumer.Visibility())) {
+ consumer.OnHistogram(labelName, labelValue, Collector_->Snapshot(), Derivative_);
+ }
+ }
+
+ void Reset() {
+ Collector_->Reset();
+ }
+
+ IHistogramSnapshotPtr Snapshot() const {
+ return Collector_->Snapshot();
+ }
+
+ private:
+ IHistogramCollectorPtr Collector_;
+ bool Derivative_;
+ };
+
+ struct TExpiringHistogramCounter: public THistogramCounter {
+ using THistogramCounter::THistogramCounter;
+ };
+
+ using THistogramPtr = TIntrusivePtr<THistogramCounter>;
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+ struct TDynamicCounters;
+
+ typedef TIntrusivePtr<TDynamicCounters> TDynamicCounterPtr;
+ struct TDynamicCounters: public TCountableBase {
+ public:
+ using TCounterPtr = TIntrusivePtr<TCounterForPtr>;
+ using TOnLookupPtr = void (*)(const char *methodName, const TString &name, const TString &value);
+
+ private:
+ TRWMutex Lock;
+ TCounterPtr LookupCounter; // Counts lookups by name
+ TOnLookupPtr OnLookup = nullptr; // Called on each lookup if not nullptr, intended for lightweight tracing.
+
+ typedef TIntrusivePtr<TCountableBase> TCountablePtr;
+
+ struct TChildId {
+ TString LabelName;
+ TString LabelValue;
+ TChildId() {
+ }
+ TChildId(const TString& labelName, const TString& labelValue)
+ : LabelName(labelName)
+ , LabelValue(labelValue)
+ {
+ }
+ auto AsTuple() const {
+ return std::make_tuple(std::cref(LabelName), std::cref(LabelValue));
+ }
+ friend bool operator <(const TChildId& x, const TChildId& y) {
+ return x.AsTuple() < y.AsTuple();
+ }
+ friend bool operator ==(const TChildId& x, const TChildId& y) {
+ return x.AsTuple() == y.AsTuple();
+ }
+ friend bool operator !=(const TChildId& x, const TChildId& y) {
+ return x.AsTuple() != y.AsTuple();
+ }
+ };
+
+ using TCounters = TMap<TChildId, TCountablePtr>;
+ using TLabels = TVector<TChildId>;
+
+ /// XXX: hack for deferred removal of expired counters. Remove once Output* functions are not used for serialization
+ mutable TCounters Counters;
+ mutable TAtomic ExpiringCount = 0;
+
+ public:
+ TDynamicCounters(TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ TDynamicCounters(const TDynamicCounters *origin)
+ : LookupCounter(origin->LookupCounter)
+ , OnLookup(origin->OnLookup)
+ {}
+
+ ~TDynamicCounters() override;
+
+ // This counter allows to track lookups by name within the whole subtree
+ void SetLookupCounter(TCounterPtr lookupCounter) {
+ TWriteGuard g(Lock);
+ LookupCounter = lookupCounter;
+ }
+
+ void SetOnLookup(TOnLookupPtr onLookup) {
+ TWriteGuard g(Lock);
+ OnLookup = onLookup;
+ }
+
+ TWriteGuard LockForUpdate(const char *method, const TString& name, const TString& value) {
+ auto res = TWriteGuard(Lock);
+ if (LookupCounter) {
+ ++*LookupCounter;
+ }
+ if (OnLookup) {
+ OnLookup(method, name, value);
+ }
+ return res;
+ }
+
+ TStackVec<TCounters::value_type, 256> ReadSnapshot() const {
+ RemoveExpired();
+ TReadGuard g(Lock);
+ TStackVec<TCounters::value_type, 256> items(Counters.begin(), Counters.end());
+ return items;
+ }
+
+ TCounterPtr GetCounter(
+ const TString& value,
+ bool derivative = false,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ TCounterPtr GetNamedCounter(
+ const TString& name,
+ const TString& value,
+ bool derivative = false,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ THistogramPtr GetHistogram(
+ const TString& value,
+ IHistogramCollectorPtr collector,
+ bool derivative = true,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ THistogramPtr GetNamedHistogram(
+ const TString& name,
+ const TString& value,
+ IHistogramCollectorPtr collector,
+ bool derivative = true,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ // These counters will be automatically removed from the registry
+ // when last reference to the counter expires.
+ TCounterPtr GetExpiringCounter(
+ const TString& value,
+ bool derivative = false,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ TCounterPtr GetExpiringNamedCounter(
+ const TString& name,
+ const TString& value,
+ bool derivative = false,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ THistogramPtr GetExpiringHistogram(
+ const TString& value,
+ IHistogramCollectorPtr collector,
+ bool derivative = true,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ THistogramPtr GetExpiringNamedHistogram(
+ const TString& name,
+ const TString& value,
+ IHistogramCollectorPtr collector,
+ bool derivative = true,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ TCounterPtr FindCounter(const TString& value) const;
+ TCounterPtr FindNamedCounter(const TString& name, const TString& value) const;
+
+ THistogramPtr FindHistogram(const TString& value) const;
+ THistogramPtr FindNamedHistogram(const TString& name,const TString& value) const;
+
+ void RemoveCounter(const TString &value);
+ void RemoveNamedCounter(const TString& name, const TString &value);
+
+ TIntrusivePtr<TDynamicCounters> GetSubgroup(const TString& name, const TString& value);
+ TIntrusivePtr<TDynamicCounters> FindSubgroup(const TString& name, const TString& value) const;
+ void RemoveSubgroup(const TString& name, const TString& value);
+ void ReplaceSubgroup(const TString& name, const TString& value, TIntrusivePtr<TDynamicCounters> subgroup);
+
+ // Move all counters from specified subgroup and remove the subgroup.
+ void MergeWithSubgroup(const TString& name, const TString& value);
+ // Recursively reset all/deriv counters to 0.
+ void ResetCounters(bool derivOnly = false);
+
+ void RegisterSubgroup(const TString& name,
+ const TString& value,
+ TIntrusivePtr<TDynamicCounters> subgroup);
+
+ void OutputHtml(IOutputStream& os) const;
+ void EnumerateSubgroups(const std::function<void(const TString& name, const TString& value)>& output) const;
+
+ // mostly for debugging purposes -- use accept with encoder instead
+ void OutputPlainText(IOutputStream& os, const TString& indent = "") const;
+
+ void Accept(
+ const TString& labelName, const TString& labelValue,
+ ICountableConsumer& consumer) const override;
+
+ private:
+ TCounters Resign() {
+ TCounters counters;
+ TWriteGuard g(Lock);
+ Counters.swap(counters);
+ return counters;
+ }
+
+ void RegisterCountable(const TString& name, const TString& value, TCountablePtr countable);
+ void RemoveExpired() const;
+
+ template <bool expiring, class TCounterType, class... TArgs>
+ TCountablePtr GetNamedCounterImpl(const TString& name, const TString& value, TArgs&&... args);
+
+ template <class TCounterType>
+ TCountablePtr FindNamedCounterImpl(const TString& name, const TString& value) const;
+ };
+
+}
diff --git a/library/cpp/monlib/dynamic_counters/counters_ut.cpp b/library/cpp/monlib/dynamic_counters/counters_ut.cpp
new file mode 100644
index 0000000000..3591037e0a
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/counters_ut.cpp
@@ -0,0 +1,342 @@
+#include "counters.h"
+
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NMonitoring;
+
+class TCountersPrinter: public ICountableConsumer {
+public:
+ TCountersPrinter(IOutputStream* out)
+ : Out_(out)
+ , Level_(0)
+ {
+ }
+
+private:
+ void OnCounter(
+ const TString& labelName, const TString& labelValue,
+ const TCounterForPtr* counter) override {
+ Indent(Out_, Level_)
+ << labelName << ':' << labelValue
+ << " = " << counter->Val() << '\n';
+ }
+
+ void OnHistogram(
+ const TString& labelName, const TString& labelValue,
+ IHistogramSnapshotPtr snapshot, bool /*derivative*/) override {
+ Indent(Out_, Level_)
+ << labelName << ':' << labelValue
+ << " = " << *snapshot << '\n';
+ }
+
+ void OnGroupBegin(
+ const TString& labelName, const TString& labelValue,
+ const TDynamicCounters*) override {
+ Indent(Out_, Level_++) << labelName << ':' << labelValue << " {\n";
+ }
+
+ void OnGroupEnd(
+ const TString&, const TString&,
+ const TDynamicCounters*) override {
+ Indent(Out_, --Level_) << "}\n";
+ }
+
+ static IOutputStream& Indent(IOutputStream* out, int level) {
+ for (int i = 0; i < level; i++) {
+ out->Write(" ");
+ }
+ return *out;
+ }
+
+private:
+ IOutputStream* Out_;
+ int Level_ = 0;
+};
+
+Y_UNIT_TEST_SUITE(TDynamicCountersTest) {
+ Y_UNIT_TEST(CountersConsumer) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+
+ auto usersCounter = rootGroup->GetNamedCounter("users", "count");
+ *usersCounter = 7;
+
+ auto hostGroup = rootGroup->GetSubgroup("counters", "resources");
+ auto cpuCounter = hostGroup->GetNamedCounter("resource", "cpu");
+ *cpuCounter = 30;
+
+ auto memGroup = hostGroup->GetSubgroup("resource", "mem");
+ auto usedCounter = memGroup->GetCounter("used");
+ auto freeCounter = memGroup->GetCounter("free");
+ *usedCounter = 100;
+ *freeCounter = 28;
+
+ auto netGroup = hostGroup->GetSubgroup("resource", "net");
+ auto rxCounter = netGroup->GetCounter("rx", true);
+ auto txCounter = netGroup->GetCounter("tx", true);
+ *rxCounter = 8;
+ *txCounter = 9;
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ " counters:resources {\n"
+ " resource:cpu = 30\n"
+ " resource:mem {\n"
+ " sensor:free = 28\n"
+ " sensor:used = 100\n"
+ " }\n"
+ " resource:net {\n"
+ " sensor:rx = 8\n"
+ " sensor:tx = 9\n"
+ " }\n"
+ " }\n"
+ " users:count = 7\n"
+ "}\n");
+ }
+
+ Y_UNIT_TEST(MergeSubgroup) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+
+ auto sensor1 = rootGroup->GetNamedCounter("sensor", "1");
+ *sensor1 = 1;
+
+ auto group1 = rootGroup->GetSubgroup("group", "1");
+ auto sensor2 = group1->GetNamedCounter("sensor", "2");
+ *sensor2 = 2;
+
+ auto group2 = group1->GetSubgroup("group", "2");
+ auto sensor3 = group2->GetNamedCounter("sensor", "3");
+ *sensor3 = 3;
+
+ rootGroup->MergeWithSubgroup("group", "1");
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ " group:2 {\n"
+ " sensor:3 = 3\n"
+ " }\n"
+ " sensor:1 = 1\n"
+ " sensor:2 = 2\n"
+ "}\n");
+ }
+
+ Y_UNIT_TEST(ResetCounters) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+
+ auto sensor1 = rootGroup->GetNamedCounter("sensor", "1");
+ *sensor1 = 1;
+
+ auto group1 = rootGroup->GetSubgroup("group", "1");
+ auto sensor2 = group1->GetNamedCounter("sensor", "2");
+ *sensor2 = 2;
+
+ auto group2 = group1->GetSubgroup("group", "2");
+ auto sensor3 = group2->GetNamedCounter("sensor", "3", true);
+ *sensor3 = 3;
+
+ rootGroup->ResetCounters(true);
+
+ TStringStream ss1;
+ TCountersPrinter printer1(&ss1);
+ rootGroup->Accept("root", "counters", printer1);
+
+ UNIT_ASSERT_STRINGS_EQUAL(ss1.Str(),
+ "root:counters {\n"
+ " group:1 {\n"
+ " group:2 {\n"
+ " sensor:3 = 0\n"
+ " }\n"
+ " sensor:2 = 2\n"
+ " }\n"
+ " sensor:1 = 1\n"
+ "}\n");
+
+ rootGroup->ResetCounters();
+
+ TStringStream ss2;
+ TCountersPrinter printer2(&ss2);
+ rootGroup->Accept("root", "counters", printer2);
+
+ UNIT_ASSERT_STRINGS_EQUAL(ss2.Str(),
+ "root:counters {\n"
+ " group:1 {\n"
+ " group:2 {\n"
+ " sensor:3 = 0\n"
+ " }\n"
+ " sensor:2 = 0\n"
+ " }\n"
+ " sensor:1 = 0\n"
+ "}\n");
+ }
+
+ Y_UNIT_TEST(RemoveCounter) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+
+ rootGroup->GetNamedCounter("label", "1");
+ rootGroup->GetCounter("2");
+ rootGroup->GetCounter("3");
+ rootGroup->GetSubgroup("group", "1");
+
+ rootGroup->RemoveNamedCounter("label", "1");
+ rootGroup->RemoveNamedCounter("label", "5");
+ rootGroup->RemoveNamedCounter("group", "1");
+ rootGroup->RemoveCounter("2");
+ rootGroup->RemoveCounter("5");
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ " group:1 {\n"
+ " }\n"
+ " sensor:3 = 0\n"
+ "}\n");
+ }
+
+ Y_UNIT_TEST(RemoveSubgroup) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+
+ rootGroup->GetSubgroup("group", "1");
+ rootGroup->GetSubgroup("group", "2");
+ rootGroup->GetCounter("2");
+
+ rootGroup->RemoveSubgroup("group", "1");
+ rootGroup->RemoveSubgroup("group", "3");
+ rootGroup->RemoveSubgroup("sensor", "2");
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ " group:2 {\n"
+ " }\n"
+ " sensor:2 = 0\n"
+ "}\n");
+ }
+
+ Y_UNIT_TEST(ExpiringCounters) {
+ TDynamicCounterPtr rootGroup{new TDynamicCounters()};
+
+ {
+ auto c = rootGroup->GetExpiringCounter("foo");
+ auto h = rootGroup->GetExpiringHistogram("bar", ExplicitHistogram({1, 42}));
+ h->Collect(15);
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ " sensor:bar = {1: 0, 42: 1, inf: 0}\n"
+ " sensor:foo = 0\n"
+ "}\n");
+ }
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ "}\n");
+ }
+
+ Y_UNIT_TEST(ExpiringCountersDiesAfterRegistry) {
+ TDynamicCounters::TCounterPtr ptr;
+
+ {
+ TDynamicCounterPtr rootGroup{new TDynamicCounters()};
+ ptr = rootGroup->GetExpiringCounter("foo");
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ " sensor:foo = 0\n"
+ "}\n");
+ }
+ }
+
+ Y_UNIT_TEST(HistogramCounter) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+
+ auto h = rootGroup->GetHistogram("timeMillis", ExponentialHistogram(4, 2));
+ for (i64 i = 1; i < 100; i++) {
+ h->Collect(i);
+ }
+
+ TStringStream ss;
+ TCountersPrinter printer(&ss);
+ rootGroup->Accept("root", "counters", printer);
+ UNIT_ASSERT_STRINGS_EQUAL(ss.Str(),
+ "root:counters {\n"
+ " sensor:timeMillis = {1: 1, 2: 1, 4: 2, inf: 95}\n"
+ "}\n");
+ }
+
+ Y_UNIT_TEST(CounterLookupCounter) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+ TDynamicCounters::TCounterPtr lookups = rootGroup->GetCounter("Lookups", true);
+ rootGroup->SetLookupCounter(lookups);
+
+ // Create subtree and check that counter is inherited
+ TDynamicCounterPtr serviceGroup = rootGroup->GetSubgroup("service", "MyService");
+ UNIT_ASSERT_VALUES_EQUAL(lookups->Val(), 1);
+
+ TDynamicCounterPtr subGroup = serviceGroup->GetSubgroup("component", "MyComponent");
+ UNIT_ASSERT_VALUES_EQUAL(lookups->Val(), 2);
+
+ auto counter = subGroup->GetNamedCounter("range", "20 msec", true);
+ UNIT_ASSERT_VALUES_EQUAL(lookups->Val(), 3);
+
+ auto hist = subGroup->GetHistogram("timeMsec", ExponentialHistogram(4, 2));
+ UNIT_ASSERT_VALUES_EQUAL(lookups->Val(), 4);
+
+ // Replace the counter for subGroup
+ auto subGroupLookups = rootGroup->GetCounter("LookupsInMyComponent", true);
+ UNIT_ASSERT_VALUES_EQUAL(lookups->Val(), 5);
+ subGroup->SetLookupCounter(subGroupLookups);
+ auto counter2 = subGroup->GetNamedCounter("range", "30 msec", true);
+ UNIT_ASSERT_VALUES_EQUAL(subGroupLookups->Val(), 1);
+ UNIT_ASSERT_VALUES_EQUAL(lookups->Val(), 5);
+ }
+
+ Y_UNIT_TEST(FindCounters) {
+ TDynamicCounterPtr rootGroup(new TDynamicCounters());
+
+ auto counter = rootGroup->FindCounter("counter1");
+ UNIT_ASSERT(!counter);
+ rootGroup->GetCounter("counter1");
+ counter = rootGroup->FindCounter("counter1");
+ UNIT_ASSERT(counter);
+
+ counter = rootGroup->FindNamedCounter("name", "counter2");
+ UNIT_ASSERT(!counter);
+ rootGroup->GetNamedCounter("name", "counter2");
+ counter = rootGroup->FindNamedCounter("name", "counter2");
+ UNIT_ASSERT(counter);
+
+ auto histogram = rootGroup->FindHistogram("histogram1");
+ UNIT_ASSERT(!histogram);
+ rootGroup->GetHistogram("histogram1", ExponentialHistogram(4, 2));
+ histogram = rootGroup->FindHistogram("histogram1");
+ UNIT_ASSERT(histogram);
+
+ histogram = rootGroup->FindNamedHistogram("name", "histogram2");
+ UNIT_ASSERT(!histogram);
+ rootGroup->GetNamedHistogram("name", "histogram2", ExponentialHistogram(4, 2));
+ histogram = rootGroup->FindNamedHistogram("name", "histogram2");
+ UNIT_ASSERT(histogram);
+ }
+}
diff --git a/library/cpp/monlib/dynamic_counters/encode.cpp b/library/cpp/monlib/dynamic_counters/encode.cpp
new file mode 100644
index 0000000000..ffa48d276e
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/encode.cpp
@@ -0,0 +1,131 @@
+#include "encode.h"
+
+#include <library/cpp/monlib/encode/encoder.h>
+#include <library/cpp/monlib/encode/json/json.h>
+#include <library/cpp/monlib/encode/spack/spack_v1.h>
+#include <library/cpp/monlib/encode/prometheus/prometheus.h>
+
+#include <util/stream/str.h>
+
+namespace NMonitoring {
+ namespace {
+ constexpr TInstant ZERO_TIME = TInstant::Zero();
+
+ class TConsumer final: public ICountableConsumer {
+ using TLabel = std::pair<TString, TString>; // name, value
+
+ public:
+ explicit TConsumer(NMonitoring::IMetricEncoderPtr encoderImpl, TCountableBase::EVisibility vis)
+ : EncoderImpl_(std::move(encoderImpl))
+ , Visibility_{vis}
+ {
+ }
+
+ void OnCounter(
+ const TString& labelName, const TString& labelValue,
+ const TCounterForPtr* counter) override {
+ NMonitoring::EMetricType metricType = counter->ForDerivative()
+ ? NMonitoring::EMetricType::RATE
+ : NMonitoring::EMetricType::GAUGE;
+ EncoderImpl_->OnMetricBegin(metricType);
+ EncodeLabels(labelName, labelValue);
+
+ if (metricType == NMonitoring::EMetricType::GAUGE) {
+ EncoderImpl_->OnDouble(ZERO_TIME, static_cast<double>(counter->Val()));
+ } else {
+ EncoderImpl_->OnUint64(ZERO_TIME, counter->Val());
+ }
+
+ EncoderImpl_->OnMetricEnd();
+ }
+
+ void OnHistogram(
+ const TString& labelName, const TString& labelValue,
+ IHistogramSnapshotPtr snapshot, bool derivative) override {
+ NMonitoring::EMetricType metricType = derivative ? EMetricType::HIST_RATE : EMetricType::HIST;
+
+ EncoderImpl_->OnMetricBegin(metricType);
+ EncodeLabels(labelName, labelValue);
+ EncoderImpl_->OnHistogram(ZERO_TIME, snapshot);
+ EncoderImpl_->OnMetricEnd();
+ }
+
+ void OnGroupBegin(
+ const TString& labelName, const TString& labelValue,
+ const TDynamicCounters*) override {
+ if (labelName.empty() && labelValue.empty()) {
+ // root group has empty label name and value
+ EncoderImpl_->OnStreamBegin();
+ } else {
+ ParentLabels_.emplace_back(labelName, labelValue);
+ }
+ }
+
+ void OnGroupEnd(
+ const TString& labelName, const TString& labelValue,
+ const TDynamicCounters*) override {
+ if (labelName.empty() && labelValue.empty()) {
+ // root group has empty label name and value
+ EncoderImpl_->OnStreamEnd();
+ EncoderImpl_->Close();
+ } else {
+ ParentLabels_.pop_back();
+ }
+ }
+
+ TCountableBase::EVisibility Visibility() const override {
+ return Visibility_;
+ }
+
+ private:
+ void EncodeLabels(const TString& labelName, const TString& labelValue) {
+ EncoderImpl_->OnLabelsBegin();
+ for (const auto& label : ParentLabels_) {
+ EncoderImpl_->OnLabel(label.first, label.second);
+ }
+ EncoderImpl_->OnLabel(labelName, labelValue);
+ EncoderImpl_->OnLabelsEnd();
+ }
+
+ private:
+ NMonitoring::IMetricEncoderPtr EncoderImpl_;
+ TVector<TLabel> ParentLabels_;
+ TCountableBase::EVisibility Visibility_;
+ };
+
+ }
+
+ THolder<ICountableConsumer> CreateEncoder(IOutputStream* out, EFormat format, TCountableBase::EVisibility vis) {
+ switch (format) {
+ case EFormat::JSON:
+ return MakeHolder<TConsumer>(NMonitoring::EncoderJson(out), vis);
+ case EFormat::SPACK:
+ return MakeHolder<TConsumer>(NMonitoring::EncoderSpackV1(
+ out,
+ NMonitoring::ETimePrecision::SECONDS,
+ NMonitoring::ECompression::ZSTD), vis);
+ case EFormat::PROMETHEUS:
+ return MakeHolder<TConsumer>(NMonitoring::EncoderPrometheus(
+ out), vis);
+ default:
+ ythrow yexception() << "unsupported metric encoding format: " << format;
+ break;
+ }
+ }
+
+ THolder<ICountableConsumer> AsCountableConsumer(IMetricEncoderPtr encoder, TCountableBase::EVisibility visibility) {
+ return MakeHolder<TConsumer>(std::move(encoder), visibility);
+ }
+
+ void ToJson(const TDynamicCounters& counters, IOutputStream* out) {
+ TConsumer consumer{EncoderJson(out), TCountableBase::EVisibility::Public};
+ counters.Accept(TString{}, TString{}, consumer);
+ }
+
+ TString ToJson(const TDynamicCounters& counters) {
+ TStringStream ss;
+ ToJson(counters, &ss);
+ return ss.Str();
+ }
+
+}
diff --git a/library/cpp/monlib/dynamic_counters/encode.h b/library/cpp/monlib/dynamic_counters/encode.h
new file mode 100644
index 0000000000..c79964d7cb
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/encode.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "counters.h"
+
+#include <library/cpp/monlib/encode/encoder.h>
+#include <library/cpp/monlib/encode/format.h>
+
+namespace NMonitoring {
+
+ THolder<ICountableConsumer> CreateEncoder(
+ IOutputStream* out,
+ EFormat format,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public
+ );
+
+ THolder<ICountableConsumer> AsCountableConsumer(
+ NMonitoring::IMetricEncoderPtr encoder,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public);
+
+ void ToJson(const TDynamicCounters& counters, IOutputStream* out);
+
+ TString ToJson(const TDynamicCounters& counters);
+}
diff --git a/library/cpp/monlib/dynamic_counters/encode_ut.cpp b/library/cpp/monlib/dynamic_counters/encode_ut.cpp
new file mode 100644
index 0000000000..52d77b6b41
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/encode_ut.cpp
@@ -0,0 +1,226 @@
+#include "encode.h"
+
+#include <library/cpp/monlib/encode/json/json.h>
+#include <library/cpp/monlib/encode/spack/spack_v1.h>
+#include <library/cpp/monlib/encode/protobuf/protobuf.h>
+
+#include <library/cpp/monlib/encode/protobuf/protos/samples.pb.h>
+#include <library/cpp/testing/unittest/registar.h>
+
+#include <util/generic/buffer.h>
+#include <util/stream/buffer.h>
+
+namespace NMonitoring {
+ struct TTestData: public TDynamicCounters {
+ TTestData() {
+ auto hostGroup = GetSubgroup("counters", "resources");
+ {
+ auto cpuCounter = hostGroup->GetNamedCounter("resource", "cpu");
+ *cpuCounter = 30;
+
+ auto memGroup = hostGroup->GetSubgroup("resource", "mem");
+ auto usedCounter = memGroup->GetCounter("used");
+ auto freeCounter = memGroup->GetCounter("free");
+ *usedCounter = 100;
+ *freeCounter = 28;
+
+ auto netGroup = hostGroup->GetSubgroup("resource", "net");
+ auto rxCounter = netGroup->GetCounter("rx", true);
+ auto txCounter = netGroup->GetCounter("tx", true);
+ *rxCounter = 8;
+ *txCounter = 9;
+ }
+
+ auto usersCounter = GetNamedCounter("users", "count");
+ *usersCounter = 7;
+
+ auto responseTimeMillis = GetHistogram("responseTimeMillis", ExplicitHistogram({1, 5, 10, 15, 20, 100, 200}));
+ for (i64 i = 0; i < 400; i++) {
+ responseTimeMillis->Collect(i);
+ }
+ }
+ };
+
+ void AssertLabelsEqual(const NProto::TLabel& l, TStringBuf name, TStringBuf value) {
+ UNIT_ASSERT_STRINGS_EQUAL(l.GetName(), name);
+ UNIT_ASSERT_STRINGS_EQUAL(l.GetValue(), value);
+ }
+
+ void AssertResult(const NProto::TSingleSamplesList& samples) {
+ UNIT_ASSERT_VALUES_EQUAL(samples.SamplesSize(), 7);
+
+ {
+ auto s = samples.GetSamples(0);
+ UNIT_ASSERT_VALUES_EQUAL(s.LabelsSize(), 2);
+ AssertLabelsEqual(s.GetLabels(0), "counters", "resources");
+ AssertLabelsEqual(s.GetLabels(1), "resource", "cpu");
+ UNIT_ASSERT_EQUAL(s.GetMetricType(), NProto::GAUGE);
+ UNIT_ASSERT_DOUBLES_EQUAL(s.GetFloat64(), 30.0, Min<double>());
+ }
+ {
+ auto s = samples.GetSamples(1);
+ UNIT_ASSERT_VALUES_EQUAL(s.LabelsSize(), 3);
+ AssertLabelsEqual(s.GetLabels(0), "counters", "resources");
+ AssertLabelsEqual(s.GetLabels(1), "resource", "mem");
+ AssertLabelsEqual(s.GetLabels(2), "sensor", "free");
+ UNIT_ASSERT_EQUAL(s.GetMetricType(), NProto::GAUGE);
+ UNIT_ASSERT_DOUBLES_EQUAL(s.GetFloat64(), 28.0, Min<double>());
+ }
+ {
+ auto s = samples.GetSamples(2);
+ UNIT_ASSERT_VALUES_EQUAL(s.LabelsSize(), 3);
+ AssertLabelsEqual(s.GetLabels(0), "counters", "resources");
+ AssertLabelsEqual(s.GetLabels(1), "resource", "mem");
+ AssertLabelsEqual(s.GetLabels(2), "sensor", "used");
+ UNIT_ASSERT_EQUAL(s.GetMetricType(), NProto::GAUGE);
+ UNIT_ASSERT_DOUBLES_EQUAL(s.GetFloat64(), 100.0, Min<double>());
+ }
+ {
+ auto s = samples.GetSamples(3);
+ UNIT_ASSERT_VALUES_EQUAL(s.LabelsSize(), 3);
+ AssertLabelsEqual(s.GetLabels(0), "counters", "resources");
+ AssertLabelsEqual(s.GetLabels(1), "resource", "net");
+ AssertLabelsEqual(s.GetLabels(2), "sensor", "rx");
+ UNIT_ASSERT_EQUAL(s.GetMetricType(), NProto::RATE);
+ UNIT_ASSERT_VALUES_EQUAL(s.GetUint64(), 8);
+ }
+ {
+ auto s = samples.GetSamples(4);
+ UNIT_ASSERT_VALUES_EQUAL(s.LabelsSize(), 3);
+ AssertLabelsEqual(s.GetLabels(0), "counters", "resources");
+ AssertLabelsEqual(s.GetLabels(1), "resource", "net");
+ AssertLabelsEqual(s.GetLabels(2), "sensor", "tx");
+ UNIT_ASSERT_EQUAL(s.GetMetricType(), NProto::RATE);
+ UNIT_ASSERT_VALUES_EQUAL(s.GetUint64(), 9);
+ }
+ {
+ auto s = samples.GetSamples(5);
+ UNIT_ASSERT_VALUES_EQUAL(s.LabelsSize(), 1);
+ AssertLabelsEqual(s.GetLabels(0), "sensor", "responseTimeMillis");
+ UNIT_ASSERT_EQUAL(s.GetMetricType(), NProto::HIST_RATE);
+
+ const NProto::THistogram& h = s.GetHistogram();
+
+ UNIT_ASSERT_EQUAL(h.BoundsSize(), 8);
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(0), 1, Min<double>());
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(1), 5, Min<double>());
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(2), 10, Min<double>());
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(3), 15, Min<double>());
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(4), 20, Min<double>());
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(5), 100, Min<double>());
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(6), 200, Min<double>());
+ UNIT_ASSERT_DOUBLES_EQUAL(h.GetBounds(7), Max<double>(), Min<double>());
+
+ UNIT_ASSERT_EQUAL(h.ValuesSize(), 8);
+ UNIT_ASSERT_EQUAL(h.GetValues(0), 2);
+ UNIT_ASSERT_EQUAL(h.GetValues(1), 4);
+ UNIT_ASSERT_EQUAL(h.GetValues(2), 5);
+ UNIT_ASSERT_EQUAL(h.GetValues(3), 5);
+ UNIT_ASSERT_EQUAL(h.GetValues(4), 5);
+ UNIT_ASSERT_EQUAL(h.GetValues(5), 80);
+ UNIT_ASSERT_EQUAL(h.GetValues(6), 100);
+ UNIT_ASSERT_EQUAL(h.GetValues(7), 199);
+ }
+ {
+ auto s = samples.GetSamples(6);
+ UNIT_ASSERT_VALUES_EQUAL(s.LabelsSize(), 1);
+ AssertLabelsEqual(s.GetLabels(0), "users", "count");
+ UNIT_ASSERT_EQUAL(s.GetMetricType(), NProto::GAUGE);
+ UNIT_ASSERT_DOUBLES_EQUAL(s.GetFloat64(), 7, Min<double>());
+ }
+ }
+
+ Y_UNIT_TEST_SUITE(TDynamicCountersEncodeTest) {
+ TTestData Data;
+
+ Y_UNIT_TEST(Json) {
+ TString result;
+ {
+ TStringOutput out(result);
+ auto encoder = CreateEncoder(&out, EFormat::JSON);
+ Data.Accept(TString(), TString(), *encoder);
+ }
+
+ NProto::TSingleSamplesList samples;
+ {
+ auto e = EncoderProtobuf(&samples);
+ DecodeJson(result, e.Get());
+ }
+
+ AssertResult(samples);
+ }
+
+ Y_UNIT_TEST(Spack) {
+ TBuffer result;
+ {
+ TBufferOutput out(result);
+ auto encoder = CreateEncoder(&out, EFormat::SPACK);
+ Data.Accept(TString(), TString(), *encoder);
+ }
+
+ NProto::TSingleSamplesList samples;
+ {
+ auto e = EncoderProtobuf(&samples);
+ TBufferInput in(result);
+ DecodeSpackV1(&in, e.Get());
+ }
+
+ AssertResult(samples);
+ }
+
+ Y_UNIT_TEST(PrivateSubgroupIsNotSerialized) {
+ TBuffer result;
+ auto subGroup = MakeIntrusive<TDynamicCounters>(TCountableBase::EVisibility::Private);
+ subGroup->GetCounter("hello");
+ Data.RegisterSubgroup("foo", "bar", subGroup);
+
+ {
+ TBufferOutput out(result);
+ auto encoder = CreateEncoder(&out, EFormat::SPACK);
+ Data.Accept(TString(), TString(), *encoder);
+ }
+
+ NProto::TSingleSamplesList samples;
+ {
+ auto e = EncoderProtobuf(&samples);
+ TBufferInput in(result);
+ DecodeSpackV1(&in, e.Get());
+ }
+
+ AssertResult(samples);
+ }
+
+ Y_UNIT_TEST(PrivateCounterIsNotSerialized) {
+ TBuffer result;
+ Data.GetCounter("foo", false, TCountableBase::EVisibility::Private);
+
+ {
+ TBufferOutput out(result);
+ auto encoder = CreateEncoder(&out, EFormat::SPACK);
+ Data.Accept(TString(), TString(), *encoder);
+ }
+
+ NProto::TSingleSamplesList samples;
+ {
+ auto e = EncoderProtobuf(&samples);
+ TBufferInput in(result);
+ DecodeSpackV1(&in, e.Get());
+ }
+
+ AssertResult(samples);
+ }
+
+ Y_UNIT_TEST(ToJson) {
+ TString result = ToJson(Data);
+
+ NProto::TSingleSamplesList samples;
+ {
+ auto e = EncoderProtobuf(&samples);
+ DecodeJson(result, e.Get());
+ }
+
+ AssertResult(samples);
+ }
+ }
+
+}
diff --git a/library/cpp/monlib/dynamic_counters/golovan_page.cpp b/library/cpp/monlib/dynamic_counters/golovan_page.cpp
new file mode 100644
index 0000000000..49cf2d39bb
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/golovan_page.cpp
@@ -0,0 +1,79 @@
+#include "golovan_page.h"
+
+#include <library/cpp/monlib/service/pages/templates.h>
+
+#include <util/string/split.h>
+#include <util/system/tls.h>
+
+using namespace NMonitoring;
+
+class TGolovanCountableConsumer: public ICountableConsumer {
+public:
+ using TOutputCallback = std::function<void()>;
+
+ TGolovanCountableConsumer(IOutputStream& out, TOutputCallback& OutputCallback)
+ : out(out)
+ {
+ if (OutputCallback) {
+ OutputCallback();
+ }
+
+ out << HTTPOKJSON << "[";
+ FirstCounter = true;
+ }
+
+ void OnCounter(const TString&, const TString& value, const TCounterForPtr* counter) override {
+ if (FirstCounter) {
+ FirstCounter = false;
+ } else {
+ out << ",";
+ }
+
+ out << "[\"" << prefix + value;
+ if (counter->ForDerivative()) {
+ out << "_dmmm";
+ } else {
+ out << "_ahhh";
+ }
+
+ out << "\"," << counter->Val() << "]";
+ }
+
+ void OnHistogram(const TString&, const TString&, IHistogramSnapshotPtr, bool) override {
+ }
+
+ void OnGroupBegin(const TString&, const TString& value, const TDynamicCounters*) override {
+ prefix += value;
+ if (!value.empty()) {
+ prefix += "_";
+ }
+ }
+
+ void OnGroupEnd(const TString&, const TString&, const TDynamicCounters*) override {
+ prefix = "";
+ }
+
+ void Flush() {
+ out << "]";
+ out.Flush();
+ }
+
+private:
+ IOutputStream& out;
+ bool FirstCounter;
+ TString prefix;
+};
+
+TGolovanCountersPage::TGolovanCountersPage(const TString& path, TIntrusivePtr<NMonitoring::TDynamicCounters> counters,
+ TOutputCallback outputCallback)
+ : IMonPage(path)
+ , Counters(counters)
+ , OutputCallback(outputCallback)
+{
+}
+
+void TGolovanCountersPage::Output(IMonHttpRequest& request) {
+ TGolovanCountableConsumer consumer(request.Output(), OutputCallback);
+ Counters->Accept("", "", consumer);
+ consumer.Flush();
+}
diff --git a/library/cpp/monlib/dynamic_counters/golovan_page.h b/library/cpp/monlib/dynamic_counters/golovan_page.h
new file mode 100644
index 0000000000..e1772c7734
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/golovan_page.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "counters.h"
+
+#include <library/cpp/monlib/service/pages/mon_page.h>
+
+#include <util/generic/ptr.h>
+
+#include <functional>
+
+// helper class to output json for Golovan.
+class TGolovanCountersPage: public NMonitoring::IMonPage {
+public:
+ using TOutputCallback = std::function<void()>;
+
+ const TIntrusivePtr<NMonitoring::TDynamicCounters> Counters;
+
+ TGolovanCountersPage(const TString& path, TIntrusivePtr<NMonitoring::TDynamicCounters> counters,
+ TOutputCallback outputCallback = nullptr);
+
+ void Output(NMonitoring::IMonHttpRequest& request) override;
+
+private:
+ TOutputCallback OutputCallback;
+};
diff --git a/library/cpp/monlib/dynamic_counters/page.cpp b/library/cpp/monlib/dynamic_counters/page.cpp
new file mode 100644
index 0000000000..5124a47bb3
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/page.cpp
@@ -0,0 +1,141 @@
+#include "page.h"
+#include "encode.h"
+
+#include <library/cpp/monlib/service/pages/templates.h>
+#include <library/cpp/string_utils/quote/quote.h>
+
+#include <util/string/split.h>
+#include <util/system/tls.h>
+
+using namespace NMonitoring;
+
+namespace {
+ Y_POD_STATIC_THREAD(TDynamicCounters*)
+ currentCounters(nullptr);
+}
+
+TMaybe<EFormat> ParseFormat(TStringBuf str) {
+ if (str == TStringBuf("json")) {
+ return EFormat::JSON;
+ } else if (str == TStringBuf("spack")) {
+ return EFormat::SPACK;
+ } else if (str == TStringBuf("prometheus")) {
+ return EFormat::PROMETHEUS;
+ } else {
+ return Nothing();
+ }
+}
+
+void TDynamicCountersPage::Output(NMonitoring::IMonHttpRequest& request) {
+ if (OutputCallback) {
+ OutputCallback();
+ }
+
+ TCountableBase::EVisibility visibility{
+ TCountableBase::EVisibility::Public
+ };
+
+ TVector<TStringBuf> parts;
+ StringSplitter(request.GetPathInfo())
+ .Split('/')
+ .SkipEmpty()
+ .Collect(&parts);
+
+ TMaybe<EFormat> format = !parts.empty() ? ParseFormat(parts.back()) : Nothing();
+ if (format) {
+ parts.pop_back();
+ }
+
+ if (!parts.empty() && parts.back() == TStringBuf("private")) {
+ visibility = TCountableBase::EVisibility::Private;
+ parts.pop_back();
+ }
+
+ auto counters = Counters;
+
+ for (const auto& escaped : parts) {
+ const auto part = CGIUnescapeRet(escaped);
+
+ TVector<TString> labels;
+ StringSplitter(part).Split('=').SkipEmpty().Collect(&labels);
+
+ if (labels.size() != 2U)
+ return NotFound(request);
+
+ if (const auto child = counters->FindSubgroup(
+ labels.front(),
+ labels.back())) {
+
+ counters = child;
+ } else {
+ return HandleAbsentSubgroup(request);
+ }
+ }
+
+ if (!format) {
+ currentCounters = counters.Get();
+ THtmlMonPage::Output(request);
+ currentCounters = nullptr;
+ return;
+ }
+
+ IOutputStream& out = request.Output();
+ if (*format == EFormat::JSON) {
+ out << HTTPOKJSON;
+ } else if (*format == EFormat::SPACK) {
+ out << HTTPOKSPACK;
+ } else if (*format == EFormat::PROMETHEUS) {
+ out << HTTPOKPROMETHEUS;
+ } else {
+ ythrow yexception() << "unsupported metric encoding format: " << *format;
+ }
+
+ auto encoder = CreateEncoder(&out, *format, visibility);
+ counters->Accept(TString(), TString(), *encoder);
+ out.Flush();
+}
+
+void TDynamicCountersPage::HandleAbsentSubgroup(IMonHttpRequest& request) {
+ if (UnknownGroupPolicy == EUnknownGroupPolicy::Error) {
+ NotFound(request);
+ } else if (UnknownGroupPolicy == EUnknownGroupPolicy::Ignore) {
+ NoContent(request);
+ } else {
+ Y_FAIL("Unsupported policy set");
+ }
+}
+
+void TDynamicCountersPage::BeforePre(IMonHttpRequest& request) {
+ IOutputStream& out = request.Output();
+ HTML(out) {
+ DIV() {
+ out << "<a href='" << request.GetPath() << "/json'>Counters as JSON</a>";
+ out << " for <a href='https://wiki.yandex-team.ru/solomon/'>Solomon</a>";
+ }
+
+ H5() {
+ out << "Counters subgroups";
+ }
+ UL() {
+ currentCounters->EnumerateSubgroups([&](const TString& name, const TString& value) {
+ LI() {
+ TString pathPart = name + "=" + value;
+ Quote(pathPart, "");
+ out << "\n<a href='" << request.GetPath() << "/" << pathPart << "'>" << name << " " << value << "</a>";
+ }
+ });
+ }
+
+ H4() {
+ out << "Counters as text";
+ }
+ }
+}
+
+void TDynamicCountersPage::OutputText(IOutputStream& out, IMonHttpRequest&) {
+ currentCounters->OutputPlainText(out);
+}
+
+void TDynamicCountersPage::SetUnknownGroupPolicy(EUnknownGroupPolicy value) {
+ UnknownGroupPolicy = value;
+}
diff --git a/library/cpp/monlib/dynamic_counters/page.h b/library/cpp/monlib/dynamic_counters/page.h
new file mode 100644
index 0000000000..1f0ef6a5ea
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/page.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "counters.h"
+
+#include <library/cpp/monlib/service/pages/pre_mon_page.h>
+
+#include <util/generic/ptr.h>
+
+#include <functional>
+
+namespace NMonitoring {
+ enum class EUnknownGroupPolicy {
+ Error, // send 404
+ Ignore, // send 204
+ };
+
+ struct TDynamicCountersPage: public TPreMonPage {
+ public:
+ using TOutputCallback = std::function<void()>;
+
+ private:
+ const TIntrusivePtr<TDynamicCounters> Counters;
+ TOutputCallback OutputCallback;
+ EUnknownGroupPolicy UnknownGroupPolicy {EUnknownGroupPolicy::Error};
+
+ private:
+ void HandleAbsentSubgroup(IMonHttpRequest& request);
+
+ public:
+ TDynamicCountersPage(const TString& path,
+ const TString& title,
+ TIntrusivePtr<TDynamicCounters> counters,
+ TOutputCallback outputCallback = nullptr)
+ : TPreMonPage(path, title)
+ , Counters(counters)
+ , OutputCallback(outputCallback)
+ {
+ }
+
+ void Output(NMonitoring::IMonHttpRequest& request) override;
+
+ void BeforePre(NMonitoring::IMonHttpRequest& request) override;
+
+ void OutputText(IOutputStream& out, NMonitoring::IMonHttpRequest&) override;
+
+ /// If set to Error, responds with 404 if the requested subgroup is not found. This is the default.
+ /// If set to Ignore, responds with 204 if the requested subgroup is not found
+ void SetUnknownGroupPolicy(EUnknownGroupPolicy value);
+ };
+}
diff --git a/library/cpp/monlib/dynamic_counters/percentile/percentile.h b/library/cpp/monlib/dynamic_counters/percentile/percentile.h
new file mode 100644
index 0000000000..73c482bce9
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/percentile/percentile.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "percentile_base.h"
+
+namespace NMonitoring {
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Percentile tracker for monitoring
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+template <size_t BUCKET_SIZE, size_t BUCKET_COUNT, size_t FRAME_COUNT>
+struct TPercentileTracker : public TPercentileBase {
+ TAtomic Items[BUCKET_COUNT];
+ TAtomicBase Frame[FRAME_COUNT][BUCKET_COUNT];
+ size_t CurrentFrame;
+
+ TPercentileTracker()
+ : CurrentFrame(0)
+ {
+ for (size_t i = 0; i < BUCKET_COUNT; ++i) {
+ AtomicSet(Items[i], 0);
+ }
+ for (size_t frame = 0; frame < FRAME_COUNT; ++frame) {
+ for (size_t bucket = 0; bucket < BUCKET_COUNT; ++bucket) {
+ Frame[frame][bucket] = 0;
+ }
+ }
+ }
+
+ void Increment(size_t value) {
+ AtomicIncrement(Items[Min((value + BUCKET_SIZE - 1) / BUCKET_SIZE, BUCKET_COUNT - 1)]);
+ }
+
+ // shift frame (call periodically)
+ void Update() {
+ TVector<TAtomicBase> totals(BUCKET_COUNT);
+ totals.resize(BUCKET_COUNT);
+ TAtomicBase total = 0;
+ for (size_t i = 0; i < BUCKET_COUNT; ++i) {
+ TAtomicBase item = AtomicGet(Items[i]);
+ TAtomicBase prevItem = Frame[CurrentFrame][i];
+ Frame[CurrentFrame][i] = item;
+ total += item - prevItem;
+ totals[i] = total;
+ }
+
+ for (size_t i = 0; i < Percentiles.size(); ++i) {
+ TPercentile &percentile = Percentiles[i];
+ auto threshold = (TAtomicBase)(percentile.first * (float)total);
+ threshold = Min(threshold, total);
+ auto it = LowerBound(totals.begin(), totals.end(), threshold);
+ size_t index = it - totals.begin();
+ (*percentile.second) = index * BUCKET_SIZE;
+ }
+ CurrentFrame = (CurrentFrame + 1) % FRAME_COUNT;
+ }
+};
+
+} // NMonitoring
diff --git a/library/cpp/monlib/dynamic_counters/percentile/percentile_base.h b/library/cpp/monlib/dynamic_counters/percentile/percentile_base.h
new file mode 100644
index 0000000000..d3c825c43d
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/percentile/percentile_base.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <library/cpp/monlib/dynamic_counters/counters.h>
+
+#include <util/string/printf.h>
+
+namespace NMonitoring {
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Percentile tracker for monitoring
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+struct TPercentileBase : public TThrRefBase {
+ using TPercentile = std::pair<float, NMonitoring::TDynamicCounters::TCounterPtr>;
+ using TPercentiles = TVector<TPercentile>;
+
+ TPercentiles Percentiles;
+
+ void Initialize(const TIntrusivePtr<NMonitoring::TDynamicCounters> &counters, const TVector<float> &thresholds,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public) {
+ Percentiles.reserve(thresholds.size());
+ for (size_t i = 0; i < thresholds.size(); ++i) {
+ Percentiles.emplace_back(thresholds[i],
+ counters->GetNamedCounter("percentile", Sprintf("%.1f", thresholds[i] * 100.f), false, visibility));
+ }
+ }
+
+ void Initialize(const TIntrusivePtr<NMonitoring::TDynamicCounters> &counters, TString group, TString subgroup,
+ TString name, const TVector<float> &thresholds,
+ TCountableBase::EVisibility visibility = TCountableBase::EVisibility::Public) {
+ auto subCounters = counters->GetSubgroup(group, subgroup)->GetSubgroup("sensor", name);
+ Initialize(subCounters, thresholds, visibility);
+ }
+};
+
+} // NMonitoring
diff --git a/library/cpp/monlib/dynamic_counters/percentile/percentile_lg.h b/library/cpp/monlib/dynamic_counters/percentile/percentile_lg.h
new file mode 100644
index 0000000000..0042cd9a6a
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/percentile/percentile_lg.h
@@ -0,0 +1,182 @@
+#pragma once
+
+#include <library/cpp/containers/stack_vector/stack_vec.h>
+
+#include <util/generic/bitops.h>
+
+#include <cmath>
+
+#include "percentile_base.h"
+
+namespace NMonitoring {
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Percentile tracker for monitoring
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+template <size_t BASE_BITS, size_t EXP_BITS, size_t FRAME_COUNT>
+struct TPercentileTrackerLg : public TPercentileBase {
+ static constexpr size_t BUCKET_COUNT = size_t(1) << EXP_BITS;
+ static constexpr size_t BUCKET_SIZE = size_t(1) << BASE_BITS;
+ static constexpr size_t ITEMS_COUNT = BUCKET_COUNT * BUCKET_SIZE;
+ static constexpr size_t TRACKER_LIMIT = BUCKET_SIZE * ((size_t(1) << BUCKET_COUNT) - 1)
+ - (size_t(1) << (BUCKET_COUNT - 1));
+ static constexpr size_t MAX_GRANULARITY = size_t(1) << (BUCKET_COUNT - 1);
+
+ size_t Borders[BUCKET_COUNT];
+ TAtomic Items[ITEMS_COUNT];
+ TAtomicBase Frame[FRAME_COUNT][ITEMS_COUNT];
+ size_t CurrentFrame;
+
+ TPercentileTrackerLg()
+ : CurrentFrame(0)
+ {
+ Borders[0] = 0;
+ for (size_t i = 1; i < BUCKET_COUNT; ++i) {
+ Borders[i] = Borders[i-1] + (BUCKET_SIZE << (i - 1));
+ }
+ for (size_t i = 0; i < ITEMS_COUNT; ++i) {
+ AtomicSet(Items[i], 0);
+ }
+ for (size_t frame = 0; frame < FRAME_COUNT; ++frame) {
+ for (size_t bucket = 0; bucket < ITEMS_COUNT; ++bucket) {
+ Frame[frame][bucket] = 0;
+ }
+ }
+ }
+
+ size_t inline BucketIdxIf(size_t value) {
+ static_assert(BASE_BITS == 5, "if-based bucket calculation cannot be used if BASE_BITS != 5");
+ size_t bucket_idx;
+ if (value < 8160) {
+ if (value < 480) {
+ if (value < 96) {
+ if (value < 32) {
+ bucket_idx = 0;
+ } else {
+ bucket_idx = 1;
+ }
+ } else {
+ if (value < 224) {
+ bucket_idx = 2;
+ } else {
+ bucket_idx = 3;
+ }
+ }
+ } else {
+ if (value < 2016) {
+ if (value < 992) {
+ bucket_idx = 4;
+ } else {
+ bucket_idx = 5;
+ }
+ } else {
+ if (value < 4064) {
+ bucket_idx = 6;
+ } else {
+ bucket_idx = 7;
+ }
+ }
+ }
+ } else {
+ if (value < 131040) {
+ if (value < 32736) {
+ if (value < 16352) {
+ bucket_idx = 8;
+ } else {
+ bucket_idx = 9;
+ }
+ } else {
+ if (value < 65504) {
+ bucket_idx = 10;
+ } else {
+ bucket_idx = 11;
+ }
+ }
+ } else {
+ if (value < 524256) {
+ if (value < 262112) {
+ bucket_idx = 12;
+ } else {
+ bucket_idx = 13;
+ }
+ } else {
+ if (value < 1048544) {
+ bucket_idx = 14;
+ } else {
+ bucket_idx = 15;
+ }
+ }
+ }
+ }
+ return Min(bucket_idx, BUCKET_COUNT - 1);
+ }
+
+ size_t inline BucketIdxBinarySearch(size_t value) {
+ size_t l = 0;
+ size_t r = BUCKET_COUNT;
+ while (l < r - 1) {
+ size_t mid = (l + r) / 2;
+ if (value < Borders[mid]) {
+ r = mid;
+ } else {
+ l = mid;
+ }
+ }
+ return l;
+ }
+
+ size_t inline BucketIdxMostSignificantBit(size_t value) {
+ size_t bucket_idx = MostSignificantBit(value + BUCKET_SIZE) - BASE_BITS;
+ return Min(bucket_idx, BUCKET_COUNT - 1);
+ }
+
+ void Increment(size_t value) {
+ size_t bucket_idx = BucketIdxMostSignificantBit(value);
+ size_t inside_bucket_idx = (value - Borders[bucket_idx] + (1 << bucket_idx) - 1) >> bucket_idx;
+ size_t idx = bucket_idx * BUCKET_SIZE + inside_bucket_idx;
+ AtomicIncrement(Items[Min(idx, ITEMS_COUNT - 1)]);
+ }
+
+ // Needed only for tests
+ size_t GetPercentile(float threshold) {
+ TStackVec<TAtomicBase, ITEMS_COUNT> totals(ITEMS_COUNT);
+ TAtomicBase total = 0;
+ for (size_t i = 0; i < ITEMS_COUNT; ++i) {
+ total += AtomicGet(Items[i]);
+ totals[i] = total;
+ }
+ TAtomicBase item_threshold = std::llround(threshold * (float)total);
+ item_threshold = Min(item_threshold, total);
+ auto it = LowerBound(totals.begin(), totals.end(), item_threshold);
+ size_t index = it - totals.begin();
+ size_t bucket_idx = index / BUCKET_SIZE;
+ return Borders[bucket_idx] + ((index % BUCKET_SIZE) << bucket_idx);
+ }
+
+ // shift frame (call periodically)
+ void Update() {
+ TStackVec<TAtomicBase, ITEMS_COUNT> totals(ITEMS_COUNT);
+ TAtomicBase total = 0;
+ for (size_t i = 0; i < ITEMS_COUNT; ++i) {
+ TAtomicBase item = AtomicGet(Items[i]);
+ TAtomicBase prevItem = Frame[CurrentFrame][i];
+ Frame[CurrentFrame][i] = item;
+ total += item - prevItem;
+ totals[i] = total;
+ }
+
+ for (size_t i = 0; i < Percentiles.size(); ++i) {
+ TPercentile &percentile = Percentiles[i];
+ TAtomicBase threshold = std::llround(percentile.first * (float)total);
+ threshold = Min(threshold, total);
+ auto it = LowerBound(totals.begin(), totals.end(), threshold);
+ size_t index = it - totals.begin();
+ size_t bucket_idx = index / BUCKET_SIZE;
+ (*percentile.second) = Borders[bucket_idx] + ((index % BUCKET_SIZE) << bucket_idx);
+ }
+ CurrentFrame = (CurrentFrame + 1) % FRAME_COUNT;
+ }
+};
+
+} // NMonitoring
diff --git a/library/cpp/monlib/dynamic_counters/percentile/percentile_ut.cpp b/library/cpp/monlib/dynamic_counters/percentile/percentile_ut.cpp
new file mode 100644
index 0000000000..6c8bb54ec9
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/percentile/percentile_ut.cpp
@@ -0,0 +1,129 @@
+#include "percentile_lg.h"
+#include <library/cpp/testing/unittest/registar.h>
+
+using namespace NMonitoring;
+
+Y_UNIT_TEST_SUITE(PercentileTest) {
+
+template<size_t A, size_t B, size_t B_BEGIN>
+void printSizeAndLimit() {
+ using TPerc = TPercentileTrackerLg<A, B, 15>;
+ Cout << "TPercentileTrackerLg<" << A << ", " << B << ", 15>"
+ << "; sizeof# " << LeftPad(HumanReadableSize(sizeof(TPerc), SF_BYTES), 7)
+ << "; max_granularity# " << LeftPad(HumanReadableSize(TPerc::MAX_GRANULARITY, SF_QUANTITY), 5)
+ << "; limit# " << LeftPad(HumanReadableSize(TPerc::TRACKER_LIMIT , SF_QUANTITY), 5) << Endl;
+ if constexpr (B > 1) {
+ printSizeAndLimit<A, B - 1, B_BEGIN>();
+ } else if constexpr (A > 1) {
+ Cout << Endl;
+ printSizeAndLimit<A - 1, B_BEGIN, B_BEGIN>();
+ }
+}
+
+ Y_UNIT_TEST(PrintTrackerLgSizeAndLimits) {
+ printSizeAndLimit<10, 5, 5>();
+ }
+
+ Y_UNIT_TEST(TrackerLimitTest) {
+ {
+ using TPerc = TPercentileTrackerLg<1, 0, 1>;
+ TPerc tracker;
+ tracker.Increment(Max<size_t>());
+ UNIT_ASSERT_EQUAL(TPerc::TRACKER_LIMIT, tracker.GetPercentile(1.0));
+ }
+ {
+ using TPerc = TPercentileTrackerLg<1, 1, 1>;
+ TPerc tracker;
+ tracker.Increment(Max<size_t>());
+ UNIT_ASSERT_EQUAL(TPerc::TRACKER_LIMIT, tracker.GetPercentile(1.0));
+ }
+ {
+ using TPerc = TPercentileTrackerLg<1, 5, 1>;
+ TPerc tracker;
+ tracker.Increment(Max<size_t>());
+ UNIT_ASSERT_EQUAL(TPerc::TRACKER_LIMIT, tracker.GetPercentile(1.0));
+ }
+ {
+ using TPerc = TPercentileTrackerLg<2, 1, 1>;
+ TPerc tracker;
+ tracker.Increment(Max<size_t>());
+ UNIT_ASSERT_EQUAL(TPerc::TRACKER_LIMIT, tracker.GetPercentile(1.0));
+ }
+ {
+ using TPerc = TPercentileTrackerLg<5, 4, 1>;
+ TPerc tracker;
+ tracker.Increment(Max<size_t>());
+ UNIT_ASSERT_EQUAL(TPerc::TRACKER_LIMIT, tracker.GetPercentile(1.0));
+ }
+ }
+
+ Y_UNIT_TEST(BucketIdxIfvsBucketIdxBinarySearch) {
+ for (size_t var = 0; var < 5; var++) {
+ if (var == 0) {
+ TPercentileTrackerLg<3, 2, 15> tracker;
+ for (size_t i = 0; i < 3000000; i += 1) {
+ size_t num1 = tracker.BucketIdxMostSignificantBit(i);
+ size_t num2 = tracker.BucketIdxBinarySearch(i);
+ UNIT_ASSERT_EQUAL(num1, num2);
+ }
+ } else if (var == 1) {
+ TPercentileTrackerLg<4, 4, 15> tracker;
+ for (size_t i = 0; i < 3000000; i += 1) {
+ size_t num1 = tracker.BucketIdxMostSignificantBit(i);
+ size_t num2 = tracker.BucketIdxBinarySearch(i);
+ UNIT_ASSERT_EQUAL(num1, num2);
+ }
+ } else if (var == 2) {
+ TPercentileTrackerLg<5, 3, 15> tracker;
+ for (size_t i = 0; i < 3000000; i += 1) {
+ size_t num1 = tracker.BucketIdxMostSignificantBit(i);
+ size_t num2 = tracker.BucketIdxBinarySearch(i);
+ size_t num3 = tracker.BucketIdxIf(i);
+ UNIT_ASSERT_EQUAL(num1, num2);
+ UNIT_ASSERT_EQUAL(num2, num3);
+ }
+ } else if (var == 3) {
+ TPercentileTrackerLg<5, 4, 15> tracker;
+ for (size_t i = 0; i < 3000000; i += 1) {
+ size_t num1 = tracker.BucketIdxMostSignificantBit(i);
+ size_t num2 = tracker.BucketIdxBinarySearch(i);
+ size_t num3 = tracker.BucketIdxIf(i);
+ UNIT_ASSERT_EQUAL(num1, num2);
+ UNIT_ASSERT_EQUAL(num2, num3);
+ }
+ } else if (var == 4) {
+ TPercentileTrackerLg<6, 5, 15> tracker;
+ for (size_t i = 0; i < 3000000; i += 1) {
+ size_t num1 = tracker.BucketIdxMostSignificantBit(i);
+ size_t num2 = tracker.BucketIdxBinarySearch(i);
+ UNIT_ASSERT_EQUAL(num1, num2);
+ }
+ for (size_t i = 0; i < 400000000000ul; i += 1303) {
+ size_t num1 = tracker.BucketIdxMostSignificantBit(i);
+ size_t num2 = tracker.BucketIdxBinarySearch(i);
+ UNIT_ASSERT_EQUAL(num1, num2);
+ }
+ }
+ }
+ }
+
+ Y_UNIT_TEST(DifferentPercentiles) {
+ TPercentileTrackerLg<5, 4, 15> tracker;
+ TVector<size_t> values({0, 115, 1216, 15, 3234567, 1234567, 216546, 263421, 751654, 96, 224, 223, 225});
+ TVector<size_t> percentiles50({0, 0, 116, 15, 116, 116, 1216, 1216, 217056, 1216, 1216, 224, 232});
+ TVector<size_t> percentiles75({0, 116, 116, 116, 1216, 1245152, 217056, 270304, 753632, 753632,
+ 270304, 270304, 270304});
+ TVector<size_t> percentiles90({ 0, 116, 1216, 1216, 2064352, 1245152, 1245152, 1245152, 1245152,
+ 1245152, 1245152, 1245152, 1245152});
+ TVector<size_t> percentiles100({ 0, 116, 1216, 1216, 2064352, 2064352, 2064352, 2064352, 2064352,
+ 2064352, 2064352, 2064352, 2064352 });
+
+ for (size_t i = 0; i < values.size(); ++i) {
+ tracker.Increment(values[i]);
+ UNIT_ASSERT_EQUAL(tracker.GetPercentile(0.5), percentiles50[i]);
+ UNIT_ASSERT_EQUAL(tracker.GetPercentile(0.75), percentiles75[i]);
+ UNIT_ASSERT_EQUAL(tracker.GetPercentile(0.90), percentiles90[i]);
+ UNIT_ASSERT_EQUAL(tracker.GetPercentile(1.0), percentiles100[i]);
+ }
+ }
+}
diff --git a/library/cpp/monlib/dynamic_counters/percentile/ut/ya.make b/library/cpp/monlib/dynamic_counters/percentile/ut/ya.make
new file mode 100644
index 0000000000..f9f3564101
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/percentile/ut/ya.make
@@ -0,0 +1,9 @@
+UNITTEST_FOR(library/cpp/monlib/dynamic_counters/percentile)
+
+ OWNER(alexvru g:kikimr g:solomon)
+
+ SRCS(
+ percentile_ut.cpp
+ )
+
+END()
diff --git a/library/cpp/monlib/dynamic_counters/percentile/ya.make b/library/cpp/monlib/dynamic_counters/percentile/ya.make
new file mode 100644
index 0000000000..cb52cdd9ad
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/percentile/ya.make
@@ -0,0 +1,15 @@
+LIBRARY()
+
+ OWNER(alexvru g:kikimr g:solomon)
+
+ SRCS(
+ percentile.h
+ percentile_lg.h
+ )
+
+ PEERDIR(
+ library/cpp/containers/stack_vector
+ library/cpp/monlib/dynamic_counters
+ )
+
+END()
diff --git a/library/cpp/monlib/dynamic_counters/ut/ya.make b/library/cpp/monlib/dynamic_counters/ut/ya.make
new file mode 100644
index 0000000000..8242f2fe30
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/ut/ya.make
@@ -0,0 +1,16 @@
+UNITTEST_FOR(library/cpp/monlib/dynamic_counters)
+
+OWNER(jamel)
+
+SRCS(
+ contention_ut.cpp
+ counters_ut.cpp
+ encode_ut.cpp
+)
+
+PEERDIR(
+ library/cpp/monlib/encode/protobuf
+ library/cpp/monlib/encode/json
+)
+
+END()
diff --git a/library/cpp/monlib/dynamic_counters/ya.make b/library/cpp/monlib/dynamic_counters/ya.make
new file mode 100644
index 0000000000..aafe1c34be
--- /dev/null
+++ b/library/cpp/monlib/dynamic_counters/ya.make
@@ -0,0 +1,27 @@
+LIBRARY()
+
+OWNER(
+ g:solomon
+ jamel
+)
+
+NO_WSHADOW()
+
+SRCS(
+ counters.cpp
+ encode.cpp
+ golovan_page.cpp
+ page.cpp
+)
+
+PEERDIR(
+ library/cpp/containers/stack_vector
+ library/cpp/monlib/encode/json
+ library/cpp/monlib/encode/spack
+ library/cpp/monlib/encode/prometheus
+ library/cpp/monlib/service/pages
+ library/cpp/string_utils/quote
+ library/cpp/threading/light_rw_lock
+)
+
+END()