-
Notifications
You must be signed in to change notification settings - Fork 5.3k
StatsAccessLogger: fixes connection gauge underflow crashes when decrementing metrics after Scope evictions. #43812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 50 commits
b8fd92e
4dd73b0
4f243b3
f44561b
32cc03e
b9deaef
8ae0f97
4e530a7
d5f87d2
e5259b5
7e9176a
5628f8f
77dac2d
8ad7e93
2995c40
0ed297f
464577d
cbd6fba
ee2a3ad
e6a43a0
b28e622
fe1706c
a836a3a
f6f870c
7c910c9
09e2737
163fd59
5b3a9af
f54bb9f
b927087
80ce33d
154c3b7
71c8b8c
8395f2b
db26105
ff3a7e0
ab3f86e
2a8f97d
1b1e6e0
a9cdc38
81cca1c
0c6103d
b8d6c70
cabad6d
627aab0
222c0fa
f097aad
713686f
d15e5ed
d8af134
eabbbfd
9b6ef12
808a9c6
d2d97fa
98ef2b0
8757699
617b60b
b4bdd87
e447bea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,64 +19,77 @@ using Extensions::Matching::Actions::TransformStat::ActionContext; | |
|
|
||
| class AccessLogState : public StreamInfo::FilterState::Object { | ||
| public: | ||
| AccessLogState(Stats::ScopeSharedPtr scope) : scope_(std::move(scope)) {} | ||
| AccessLogState(std::shared_ptr<const StatsAccessLog> parent) : parent_(std::move(parent)) {} | ||
|
|
||
| // When the request is destroyed, we need to subtract the value from the gauge. | ||
| // We need to look up the gauge again in the scope because it might have been evicted. | ||
| // The gauge object itself is kept alive by the shared_ptr in the state, so we can access its | ||
| // name and tags to re-lookup/re-create it in the scope. | ||
| ~AccessLogState() override { | ||
TAOXUY marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| for (const auto& [gauge_ptr, state] : inflight_gauges_) { | ||
| // TODO(taoxuy): make this as an accessor of the | ||
| // Stat class. | ||
| Stats::StatNameTagVector tag_names; | ||
| state.gauge_->iterateTagStatNames( | ||
| [&tag_names](Stats::StatName name, Stats::StatName value) -> bool { | ||
| tag_names.emplace_back(name, value); | ||
| return true; | ||
| }); | ||
|
|
||
| // Using state.gauge_->statName() directly would be incorrect because it returns the fully | ||
| // qualified name (including tags). Passing this full name to scope_->gaugeFromStatName(...) | ||
| // would cause the scope to attempt tag extraction on the full name. Since the tags in | ||
| // AccessLogState are often dynamic and not configured in the global tag extractors, this | ||
| // extraction would likely fail to identify the tags correctly, resulting in a gauge with a | ||
| // different identity (the full name as the stat name and no tags). | ||
| auto& gauge = scope_->gaugeFromStatNameWithTags( | ||
| state.gauge_->tagExtractedStatName(), tag_names, Stats::Gauge::ImportMode::Accumulate); | ||
| gauge.sub(state.value_); | ||
| for (const std::pair<const GaugeKey, InflightGauge>& p : inflight_gauges_) { | ||
| Stats::Gauge& gauge_stat = parent_->scope().gaugeFromStatNameWithTags( | ||
| p.first.statName(), p.first.tags(), p.first.importMode()); | ||
| gauge_stat.sub(p.second.value_); | ||
| } | ||
| } | ||
|
|
||
| void addInflightGauge(Stats::Gauge* gauge, uint64_t value) { | ||
| inflight_gauges_.try_emplace(gauge, Stats::GaugeSharedPtr(gauge), value); | ||
| } | ||
| // Adds an incremental value to an existing gauge, or creates it if that gauge doesn't exist. | ||
| // Zero values are ignored. | ||
| void addInflightGauge(Stats::StatName stat_name, Stats::StatNameTagVectorOptConstRef tags, | ||
| Stats::Gauge::ImportMode import_mode, uint64_t value, | ||
| std::vector<Stats::StatNameDynamicStorage> tags_storage) { | ||
| if (value == 0) { | ||
TAOXUY marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return; | ||
| } | ||
|
|
||
| absl::optional<uint64_t> removeInflightGauge(Stats::Gauge* gauge) { | ||
| auto it = inflight_gauges_.find(gauge); | ||
| GaugeKey key{stat_name, import_mode, tags}; | ||
|
|
||
| auto it = inflight_gauges_.find(key); | ||
| if (it == inflight_gauges_.end()) { | ||
| return absl::nullopt; | ||
| key.makeOwned(); | ||
| auto [new_it, inserted] = | ||
| inflight_gauges_.emplace(std::move(key), InflightGauge{std::move(tags_storage), 0}); | ||
| it = new_it; | ||
| } | ||
| it->second.value_ += value; | ||
| parent_->scope().gaugeFromStatNameWithTags(stat_name, tags, import_mode).add(value); | ||
| } | ||
|
|
||
| // Removes an amount from an existing gauge, allowing the gauge to be evicted if the value reaches | ||
| // 0. | ||
| void removeInflightGauge(Stats::StatName stat_name, Stats::StatNameTagVectorOptConstRef tags, | ||
| Stats::Gauge::ImportMode import_mode, uint64_t value) { | ||
| if (value == 0) { | ||
TAOXUY marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return; | ||
| } | ||
|
|
||
| GaugeKey key{stat_name, import_mode, tags}; | ||
|
|
||
| // Create the gauge so it gets registered in the stat store (expected by some tests and stats | ||
| // logic) | ||
| Stats::Gauge& gauge_stat = | ||
| parent_->scope().gaugeFromStatNameWithTags(stat_name, tags, import_mode); | ||
|
|
||
| auto it = inflight_gauges_.find(key); | ||
| if (it != inflight_gauges_.end()) { | ||
| ENVOY_BUG(it->second.value_ >= value, "Connection gauge underflow in removeInflightGauge"); | ||
jmarantz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| it->second.value_ -= value; | ||
| gauge_stat.sub(value); | ||
| if (it->second.value_ == 0) { | ||
TAOXUY marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| inflight_gauges_.erase(it); | ||
| } | ||
| } | ||
| uint64_t value = it->second.value_; | ||
| inflight_gauges_.erase(it); | ||
| return value; | ||
| } | ||
|
|
||
| static constexpr absl::string_view key() { return "envoy.access_loggers.stats.access_log_state"; } | ||
|
|
||
| private: | ||
| struct State { | ||
| State(Stats::GaugeSharedPtr gauge, uint64_t value) : gauge_(std::move(gauge)), value_(value) {} | ||
| // Hold a shared_ptr to the parent to ensure the parent and its members exist for the lifetime of | ||
| // AccessLogState. | ||
| std::shared_ptr<const StatsAccessLog> parent_; | ||
|
|
||
| Stats::GaugeSharedPtr gauge_; | ||
| struct InflightGauge { | ||
| std::vector<Stats::StatNameDynamicStorage> tags_storage_; | ||
| uint64_t value_; | ||
| }; | ||
|
|
||
| Stats::ScopeSharedPtr scope_; | ||
|
|
||
| // The map key holds a raw pointer to the gauge. The value holds a ref-counted pointer to ensure | ||
| // the gauge is not destroyed if it is evicted from the stats scope. | ||
| absl::flat_hash_map<Stats::Gauge*, State> inflight_gauges_; | ||
| absl::flat_hash_map<GaugeKey, InflightGauge> inflight_gauges_; | ||
| }; | ||
|
|
||
| Formatter::FormatterProviderPtr | ||
|
|
@@ -138,6 +151,38 @@ class TagActionValidationVisitor | |
|
|
||
| } // namespace | ||
|
|
||
| GaugeKey::GaugeKey(Stats::StatName stat_name, Stats::Gauge::ImportMode import_mode, | ||
| Stats::StatNameTagVectorOptConstRef borrowed_tags) | ||
| : stat_name_(stat_name), import_mode_(import_mode), borrowed_tags_(borrowed_tags) {} | ||
|
|
||
| void GaugeKey::makeOwned() { | ||
| ASSERT(!(borrowed_tags_.has_value() && owned_tags_.has_value()), | ||
| "Both borrowed and owned tags are present in GaugeKey::makeOwned"); | ||
| if (borrowed_tags_.has_value() && !owned_tags_.has_value()) { | ||
TAOXUY marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| owned_tags_ = borrowed_tags_.value().get(); | ||
| borrowed_tags_ = absl::nullopt; | ||
| } | ||
| } | ||
|
|
||
| Stats::StatNameTagVectorOptConstRef GaugeKey::tags() const { | ||
| if (owned_tags_.has_value()) { | ||
| return std::cref(owned_tags_.value()); | ||
| } | ||
| return borrowed_tags_; | ||
| } | ||
|
|
||
| bool GaugeKey::operator==(const GaugeKey& rhs) const { | ||
| if (stat_name_ != rhs.stat_name_ || import_mode_ != rhs.import_mode_) { | ||
| return false; | ||
| } | ||
| Stats::StatNameTagVectorOptConstRef lhs_tags = tags(); | ||
| Stats::StatNameTagVectorOptConstRef rhs_tags = rhs.tags(); | ||
| if (lhs_tags.has_value() != rhs_tags.has_value()) { | ||
| return false; | ||
| } | ||
| return !lhs_tags.has_value() || lhs_tags.value().get() == rhs_tags.value().get(); | ||
| } | ||
|
|
||
| StatsAccessLog::StatsAccessLog(const envoy::extensions::access_loggers::stats::v3::Config& config, | ||
| Server::Configuration::GenericFactoryContext& context, | ||
| AccessLog::FilterPtr&& filter, | ||
|
|
@@ -417,31 +462,26 @@ void StatsAccessLog::emitLogForGauge(const Gauge& gauge, const Formatter::Contex | |
| Stats::Gauge::ImportMode import_mode = op == Gauge::OperationType::SET | ||
| ? Stats::Gauge::ImportMode::NeverImport | ||
| : Stats::Gauge::ImportMode::Accumulate; | ||
| auto& gauge_stat = scope_->gaugeFromStatNameWithTags(gauge.stat_.name_, tags, import_mode); | ||
|
|
||
| if (op == Gauge::OperationType::PAIRED_ADD || op == Gauge::OperationType::PAIRED_SUBTRACT) { | ||
| if (op == Gauge::OperationType::SET) { | ||
| Stats::Gauge& gauge_stat = | ||
| scope_->gaugeFromStatNameWithTags(gauge.stat_.name_, tags, import_mode); | ||
| gauge_stat.set(value); | ||
| } else if (op == Gauge::OperationType::PAIRED_ADD || | ||
| op == Gauge::OperationType::PAIRED_SUBTRACT) { | ||
| auto& filter_state = const_cast<StreamInfo::FilterState&>(stream_info.filterState()); | ||
| if (!filter_state.hasData<AccessLogState>(AccessLogState::key())) { | ||
| filter_state.setData(AccessLogState::key(), std::make_shared<AccessLogState>(scope_), | ||
| StreamInfo::FilterState::StateType::Mutable, | ||
| StreamInfo::FilterState::LifeSpan::Request); | ||
| filter_state.setData( | ||
| AccessLogState::key(), std::make_shared<AccessLogState>(shared_from_this()), | ||
| StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::Request); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if this is a connection-level access log, like a listener access log? I'm not sure what a lifespan of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! Updated the comment that it should be good for TCP connection and I have add an integration test for TCP. |
||
| } | ||
| auto* state = filter_state.getDataMutable<AccessLogState>(AccessLogState::key()); | ||
|
|
||
| if (op == Gauge::OperationType::PAIRED_ADD) { | ||
| state->addInflightGauge(&gauge_stat, value); | ||
| gauge_stat.add(value); | ||
| state->addInflightGauge(gauge.stat_.name_, tags, import_mode, value, std::move(storage)); | ||
| } else { | ||
| absl::optional<uint64_t> added_value = state->removeInflightGauge(&gauge_stat); | ||
| if (added_value.has_value()) { | ||
| gauge_stat.sub(added_value.value()); | ||
| } | ||
| state->removeInflightGauge(gauge.stat_.name_, tags, import_mode, value); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (op == Gauge::OperationType::SET) { | ||
| gauge_stat.set(value); | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.