Skip to content

Commit 416d9c9

Browse files
ctillercopybara-github
authored andcommitted
[telemetry] Instrument library
Add a new library to manage metrics: this is expected to subsume both the StatsPlugin architecture, and the original core stats stack. The primary artifacts here are InstrumentDomain and its inner Storage type, and MetricsQuery. InstrumentDomain manages a set of metrics that have a common suite of labels. Its Storage type then maintains values of those metrics for each distinct set of labels. MetricsQuery allows querying against all domains for metrics. Importantly it also has a 'CollapseLabel' mechanism, which allows omitting a label from the output wherever it exists, and summing across all instances of Storage that contained the remaining labels. This is the mechanism we'll use to regain `optional` labels from our APIs. Histograms are omitted from the implementation initially. PiperOrigin-RevId: 793728616
1 parent 65cdfba commit 416d9c9

File tree

9 files changed

+1103
-0
lines changed

9 files changed

+1103
-0
lines changed

CMakeLists.txt

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build_autogenerated.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/BUILD

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10187,6 +10187,38 @@ grpc_cc_library(
1018710187
],
1018810188
)
1018910189

10190+
grpc_cc_library(
10191+
name = "instrument",
10192+
srcs = [
10193+
"telemetry/instrument.cc",
10194+
],
10195+
hdrs = [
10196+
"telemetry/instrument.h",
10197+
],
10198+
external_deps = [
10199+
"absl/base:core_headers",
10200+
"absl/functional:any_invocable",
10201+
"absl/functional:function_ref",
10202+
"absl/log:check",
10203+
"absl/log",
10204+
"absl/strings",
10205+
"absl/types:span",
10206+
"absl/container:flat_hash_set",
10207+
"absl/container:flat_hash_map",
10208+
"absl/container:node_hash_map",
10209+
"absl/hash",
10210+
],
10211+
deps = [
10212+
"avl",
10213+
"match",
10214+
"per_cpu",
10215+
"ref_counted",
10216+
"sync",
10217+
"//:gpr",
10218+
"//:ref_counted_ptr",
10219+
],
10220+
)
10221+
1019010222
grpc_cc_library(
1019110223
name = "wait_for_single_owner",
1019210224
srcs = ["util/wait_for_single_owner.cc"],

src/core/telemetry/instrument.cc

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
// Copyright 2025 gRPC authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "src/core/telemetry/instrument.h"
16+
17+
#include <cstddef>
18+
#include <cstdint>
19+
#include <string>
20+
#include <tuple>
21+
#include <utility>
22+
#include <vector>
23+
24+
#include "absl/container/flat_hash_map.h"
25+
#include "absl/log/check.h"
26+
#include "absl/log/log.h"
27+
#include "absl/strings/string_view.h"
28+
#include "absl/types/span.h"
29+
30+
namespace grpc_core {
31+
32+
MetricsQuery& MetricsQuery::WithLabelEq(absl::string_view label,
33+
std::string value) {
34+
label_eqs_.emplace(label, std::move(value));
35+
return *this;
36+
}
37+
38+
MetricsQuery& MetricsQuery::CollapseLabels(
39+
absl::Span<const std::string> labels) {
40+
for (const auto& label : labels) {
41+
collapsed_labels_.insert(label);
42+
}
43+
return *this;
44+
}
45+
46+
MetricsQuery& MetricsQuery::OnlyMetrics(absl::Span<const std::string> metrics) {
47+
only_metrics_.emplace(metrics.begin(), metrics.end());
48+
return *this;
49+
}
50+
51+
void MetricsQuery::Run(MetricsSink& sink) const {
52+
QueryableDomain::ExportMetrics(*this, sink);
53+
}
54+
55+
const InstrumentIndex::Description* InstrumentIndex::Register(
56+
QueryableDomain* domain, uint64_t offset, absl::string_view name,
57+
absl::string_view description, absl::string_view unit, Shape shape) {
58+
auto it = metrics_.emplace(
59+
name, Description{domain, offset, name, description, unit, shape});
60+
if (!it.second) {
61+
LOG(FATAL) << "Metric with name '" << name << "' already registered.";
62+
}
63+
return &it.first->second;
64+
}
65+
66+
const InstrumentIndex::Description* InstrumentIndex::Find(
67+
absl::string_view name) const {
68+
auto it = metrics_.find(name);
69+
if (it == metrics_.end()) {
70+
return nullptr;
71+
}
72+
return &it->second;
73+
}
74+
75+
template <typename Fn>
76+
void MetricsQuery::Apply(absl::Span<const std::string> label_names, Fn fn,
77+
MetricsSink& sink) const {
78+
if (collapsed_labels_.empty()) {
79+
ApplyLabelChecks(label_names, std::move(fn), sink);
80+
return;
81+
}
82+
std::vector<size_t> include_labels;
83+
for (size_t i = 0; i < label_names.size(); ++i) {
84+
if (!collapsed_labels_.contains(label_names[i])) {
85+
include_labels.push_back(i);
86+
}
87+
}
88+
if (include_labels.size() == label_names.size()) {
89+
ApplyLabelChecks(label_names, std::move(fn), sink);
90+
return;
91+
}
92+
class Filter final : public MetricsSink {
93+
public:
94+
explicit Filter(absl::Span<const size_t> include_labels)
95+
: include_labels_(include_labels) {}
96+
97+
void Counter(absl::Span<const std::string> label, absl::string_view name,
98+
uint64_t value) override {
99+
uint64_counters_[ConstructKey(label, name)] += value;
100+
}
101+
102+
void Publish(MetricsSink& sink) const {
103+
for (const auto& [key, value] : uint64_counters_) {
104+
sink.Counter(std::get<0>(key), std::get<1>(key), value);
105+
}
106+
}
107+
108+
private:
109+
std::tuple<std::vector<std::string>, absl::string_view> ConstructKey(
110+
absl::Span<const std::string> label, absl::string_view name) const {
111+
std::vector<std::string> key;
112+
key.reserve(include_labels_.size());
113+
for (auto i : include_labels_) {
114+
key.push_back(label[i]);
115+
}
116+
return std::tuple(std::move(key), name);
117+
}
118+
119+
absl::Span<const size_t> include_labels_;
120+
absl::flat_hash_map<std::tuple<std::vector<std::string>, absl::string_view>,
121+
uint64_t>
122+
uint64_counters_;
123+
};
124+
Filter filter(include_labels);
125+
ApplyLabelChecks(label_names, std::move(fn), filter);
126+
filter.Publish(sink);
127+
}
128+
129+
template <typename Fn, typename Sink>
130+
void MetricsQuery::ApplyLabelChecks(absl::Span<const std::string> label_names,
131+
Fn fn, Sink& sink) const {
132+
if (label_eqs_.empty()) {
133+
fn(sink);
134+
return;
135+
}
136+
struct LabelEq {
137+
size_t offset;
138+
absl::string_view value;
139+
};
140+
std::vector<LabelEq> label_eqs;
141+
for (size_t i = 0; i < label_names.size(); ++i) {
142+
const auto& label = label_names[i];
143+
auto it = label_eqs_.find(label);
144+
if (it != label_eqs_.end()) label_eqs.push_back({i, it->second});
145+
}
146+
// If there are labels to match, but this domain doesn't have all the labels
147+
// requested, skip it - it can never match all!
148+
if (label_eqs.size() < label_eqs_.size()) return;
149+
class Filter final : public MetricsSink {
150+
public:
151+
explicit Filter(Sink& sink, absl::Span<const LabelEq> inclusion_checks)
152+
: inclusion_checks_(inclusion_checks), sink_(sink) {}
153+
154+
void Counter(absl::Span<const std::string> label, absl::string_view name,
155+
uint64_t value) override {
156+
if (!Matches(label)) return;
157+
sink_.Counter(label, name, value);
158+
}
159+
160+
private:
161+
bool Matches(absl::Span<const std::string> label) const {
162+
for (const auto& check : inclusion_checks_) {
163+
if (label[check.offset] != check.value) return false;
164+
}
165+
return true;
166+
}
167+
168+
absl::Span<const LabelEq> inclusion_checks_;
169+
Sink& sink_;
170+
};
171+
Filter filter(sink, label_eqs);
172+
fn(filter);
173+
}
174+
175+
void QueryableDomain::Constructed() {
176+
CHECK_EQ(prev_, nullptr);
177+
prev_ = last_;
178+
last_ = this;
179+
}
180+
181+
void QueryableDomain::ExportMetrics(const MetricsQuery& query,
182+
MetricsSink& sink) {
183+
auto selected_metrics = query.selected_metrics();
184+
if (selected_metrics.has_value()) {
185+
absl::flat_hash_map<QueryableDomain*,
186+
std::vector<const InstrumentIndex::Description*>>
187+
what;
188+
for (const auto& metric : *selected_metrics) {
189+
const auto* desc = InstrumentIndex::Get().Find(metric);
190+
if (desc == nullptr) continue;
191+
what[desc->domain].push_back(desc);
192+
}
193+
for (auto it = what.begin(); it != what.end(); ++it) {
194+
// TODO(ctiller): switch to structured bindings when we have C++20.
195+
// Avoids "error: captured structured bindings are a C++20 extension
196+
// [-Werror,-Wc++20-extensions]"
197+
auto* domain = it->first;
198+
auto& metrics = it->second;
199+
query.Apply(
200+
domain->label_names(),
201+
[&](MetricsSink& sink) { domain->ExportMetrics(sink, metrics); },
202+
sink);
203+
}
204+
} else {
205+
for (auto* domain = last_; domain != nullptr; domain = domain->prev_) {
206+
query.Apply(
207+
domain->label_names(),
208+
[&](MetricsSink& sink) {
209+
domain->ExportMetrics(sink, domain->all_metrics());
210+
},
211+
sink);
212+
}
213+
}
214+
}
215+
216+
uint64_t QueryableDomain::AllocateCounter(absl::string_view name,
217+
absl::string_view description,
218+
absl::string_view unit) {
219+
const size_t offset = Allocate(1);
220+
metrics_.push_back(InstrumentIndex::Get().Register(
221+
this, offset, name, description, unit, InstrumentIndex::Counter{}));
222+
return offset;
223+
}
224+
225+
} // namespace grpc_core

0 commit comments

Comments
 (0)