hot restart: propagate programmatic stat tags across restart#45674
hot restart: propagate programmatic stat tags across restart#45674bpalermo wants to merge 6 commits into
Conversation
|
Hi @bpalermo, welcome and thank you for your contribution. We will try to review your Pull Request as quickly as possible. In the meantime, please take a look at the contribution guidelines if you have not done so already. |
Stats created via *FromStatNameWithTags inline their tag values into the
stat name. The hot restart stat merge re-created such stats from the name
alone with no tags, so after a restart the tag values collapsed into the
metric name and the labels went empty (e.g. as exported to Prometheus).
This resolves the long-standing "TODO(snowp): Propagate tag values during
hot restarts".
The parent now transmits the tag-extracted name and tag values for stats
that carry programmatic tags; the child re-creates them with identical
labels via new Scope::{counter,gauge}FromMergedStatName methods (default
implementations fall back to the prior name-derived behavior, so other
Scope implementations are unaffected). Guarded by
envoy.reloadable_features.hot_restart_propagate_stat_tags.
Signed-off-by: Bruno Palermo <b@palermo.dev>
0bdc37d to
481e3ea
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request addresses a bug where stats created with programmatic tags lost their labels across hot restarts. It introduces mechanisms to transmit tag-extracted names and tag values from the parent process to the child, allowing the child to correctly re-create the stats with identical labels. The review feedback focuses on minor performance and readability optimizations, including reserving space in maps and protobuf repeated fields to prevent unnecessary reallocations, and simplifying absl::Span construction by utilizing implicit conversions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
please fix format and address ai-gen comments. |
|
/wait |
Resolves conflicts with the new tag-aware scope API (envoyproxy#45146): counterFromMergedStatName/gaugeFromMergedStatName now default to the tag-aware counterFromTaggedName/gaugeFromTaggedName (which retain tag metadata on tag-aware scopes and isolated stores), and the legacy ThreadLocalStore ScopeImpl overrides them via the tag-aware TagStatNameJoiner, so the getOrCreate*Base refactor is no longer needed. Signed-off-by: Bruno Palermo <b@palermo.dev>
…add tests - Export tag metadata from the hot restart parent only for stats whose tags the child cannot re-derive from the name via tag extraction (regex-extracted tags, including the default tag regexes, are reproduced by the child during the merge), keeping the stats transfer payload unchanged for stats without programmatic tags. Adds a nullable Store::tagProducer() accessor to support the comparison. - Reserve map/repeated-field capacity in the tag conversion paths (review feedback). - Fix spelling flagged by CI (inlines -> embeds). - New tests: parent-side gating, end-to-end tag propagation through export/merge, runtime-flag-off legacy behavior, gauge merge path, and direct coverage of the merged-stat creation methods on legacy and tag-aware scopes. Signed-off-by: Bruno Palermo <b@palermo.dev>
|
Updated — changes since the review:
cc @paul-r-gall |
|
@paul-r-gall please review |
|
@wbpcode any concerns with relation to your recent stat tags changes? |
wbpcode
left a comment
There was a problem hiding this comment.
Thanks for calling this out. This is a great fix. I guess this bug is there for very very very long time because we have added the *FromWithTags for very very very long time.
Because the 1.39 release is approaching, let's merge this to 1.40. And I added some comments. Thanks again.
| virtual Counter& counterFromMergedStatName(StatName full_name, StatName tag_extracted_name, | ||
| std::optional<StatNameTagSpan> tags) { | ||
| if (!tags.has_value() || tags->empty()) { | ||
| // Without tags the flat name is the only meaningful component; tags may be re-derived from | ||
| // it by extraction as usual. | ||
| return counterFromTaggedName(full_name, std::nullopt, StatName()); | ||
| } | ||
| return counterFromTaggedName(tag_extracted_name, tags, full_name); | ||
| } |
There was a problem hiding this comment.
I guess we may needn't this new method but could use the counterFromTaggedName() directly?
There was a problem hiding this comment.
Calling counterFromTaggedName() directly would reintroduce the bug: the merge scope is the legacy ScopeImpl, whose counterFromTaggedName drops the tags when a pre-joined tagged_name is supplied (it can't compose tag metadata with its prefix in general — that's the contract from #45146). The merged stat would then be created with no tags and win the central-cache slot. counterFromMergedStatName exists so the legacy scope can honor the fully-resolved components from the parent (safe on the merge path specifically, because the scope prefix is empty), while TagScopeImpl maps it back onto counterFromTaggedName, which it does honor. Happy to restructure if you'd rather change counterFromTaggedName's contract on the legacy scope instead, but that would affect all existing callers of the tag-aware API.
There was a problem hiding this comment.
Sorry. But I didn't get you here. Did you mean a stat with specific tags may cannot be re-constructed because if we specific the full name, the tags will be ignored (for legacy scope), if we not specific the full name, we may lost the correct full name?
if I understood correctly, how this API fix it? Could you share a specific example? Like for example, a stat that created with *FromStatNameWithTags("xxxxx", {{"tag", "tagv"}}), how your API hope? seems the legacy scope still will ignore the input tags because there is a full_name?
If I understood wrong, could you illustrate a little more?
Thanks.
BTW, we needn't take the the case where the old and new scope implementation be used at same time when doing the hot restart. It's bring chaos. We can document it clear to this limitation.
There was a problem hiding this comment.
Good catch — that trade-off is exactly right for counterFromTaggedName, but counterFromMergedStatName is a different method that avoids it. Concrete example:
Parent creates counterFromStatNameWithTags("custom.requests", {{"source","svc-a"}}) → flat name custom.requests.source.svc-a, tag-extracted name custom.requests, tags [source=svc-a]. It transmits all three over the wire.
On the child, the merge scope is the legacy ScopeImpl (the merge uses store.createScope("") and use_tag_scope defaults to false). If StatMerger called counterFromTaggedName("custom.requests", [source=svc-a], /*tagged_name=*/"custom.requests.source.svc-a"), you're right — the legacy scope drops the tags because tagged_name is set, and we'd get the mangled no-tags counter. That's the bug.
counterFromMergedStatName instead uses the same 7-arg tag-aware joiner that TagScopeImpl::counterFromTaggedName uses, so it keeps the exact flat name as the cache key and the tag metadata:
counterFromMergedStatName("custom.requests.source.svc-a", "custom.requests", [source=svc-a])
→ name = custom.requests.source.svc-a (matches parent; == child's own WithTags cache key)
tagExtractedName = custom.requests
tags = [source=svc-a]
We can't just re-derive the flat name from tag_extracted_name + tags, because it has to match the parent's exact name (dynamic components, tag order) and the key the child's own *FromStatNameWithTags will later look up.
Agree on documenting that parent and child must use the same scope implementation — I'll add that.
| if (tag_producer != nullptr) { | ||
| Stats::TagVector rederived_tags; | ||
| const std::string tag_extracted_name = tag_producer->produceTags(name, rederived_tags); | ||
| if (tag_extracted_name == metric.tagExtractedName() && rederived_tags == tags) { | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
Rather than to extract the tags and compare them, could we add a flag to the metrics directly so we can know the stats has programmatic tags? Like our current used() and so on. We could add two method: markAsNoTagExtracion() and bool NoTagExtraction() to tell whether a stat has tags that generated by the extraction rules/tag producer.
There was a problem hiding this comment.
That's a nicer design — it also removes the assumption that parent and child share the same tag-extraction config (the re-derivability check runs with the parent's tag producer, but it's the child that re-derives). I'll add a flag bit (there's room in Metric::Flags), set it on the programmatic-tag creation paths — including merged-stat re-creation so a second hot restart still ships the metadata — and drop the TagProducer plumbing from HotRestartingParent.
There was a problem hiding this comment.
Done in 0af527a — added Metric::noTagExtraction() / markAsNoTagExtraction() backed by a new Flags::NoTagExtraction bit. The stores mark stats at creation when tags are supplied by the caller (including stats re-created by the hot restart merge, so a second hot restart still exports their metadata), and recordTags is now just the flag check — the TagProducer plumbing and Store::tagProducer() accessor are gone. One deviation from the sketch: the two methods have default implementations on Metric (false / no-op) rather than being pure, so metric types without allocator flags (null stats, histograms) and mocks don't need boilerplate — happy to make them pure if you'd rather.
Nope. This is old bug and the fix also be independent with my migration. |
…vation Per review feedback, replace the parent-side re-derivation check (produceTags + compare) with a NoTagExtraction metric flag set at stat creation when tags are supplied by the caller rather than derived from the name. This drops the TagProducer plumbing from HotRestartingParent and the Store::tagProducer() accessor, and no longer assumes the parent and child run the same tag extraction configuration. Stats re-created by the hot restart merge are marked as well, so a subsequent hot restart exports their tag metadata again. Signed-off-by: Bruno Palermo <b@palermo.dev>
Per review discussion, document that counterFromMergedStatName / gaugeFromMergedStatName take the parent's flat name verbatim as the child's cache key, so parent and child must use the same scope implementation across a hot restart. Signed-off-by: Bruno Palermo <b@palermo.dev>
|
flaky perhaps? 🤔 |
|
/retest |
|
@wbpcode gentle ping 🙏 ? |
…-stat-tags # Conflicts: # source/common/stats/thread_local_store.h # test/common/stats/thread_local_store_test.cc Signed-off-by: Bruno Palermo <b@palermo.dev>
|
/retest |
Commit Message: hot restart: propagate programmatic stat tags across restart
Additional Description:
Stats created with programmatic tags (via
Scope::*FromStatNameWithTags/ the tag-aware*FromTaggedNameAPI) embed their tag values into the flat stat name while keeping a cleantagExtractedNameand a set of tags. This works on a fresh process, but breaks across a hot restart:HotRestartingParent::Internal::exportStatsToChild);tags()/tagExtractedName()are dropped.StatMerger::mergeCountersre-creates the stat from the name alone, which runs only regex tag extraction. With no matchingstats_tagsregex, the merged stat gets empty tags andtagExtractedName == the full mangled name.This is the gap behind the long-standing
// TODO(snowp): Propagate tag values during hot restartsinstat_merger.cc. Regex-extracted tags survive only because extraction re-derives them from the name; programmatic tags have nothing to re-derive from.This change actually propagates the tags:
hot_restart.proto: newTaggedMetric(tag-extracted name + repeatedTag) andcounter_tags/gauge_tagsmaps onReply.Stats.TagProduceron the flat name and skips the stat when extraction reproduces the stored tags (the common case — including all default tag regexes). The stats transfer payload is therefore unchanged except for stats that actually carry programmatic tags. A nullableStore::tagProducer()accessor (defaultnullptr; onlyThreadLocalStoreImploverrides) supports the comparison.StatMergerre-creates the stat with the original tags via newScope::counterFromMergedStatName/gaugeFromMergedStatName. Following stats: new tags friendly scope based API #45146 these have non-virtual-breaking defaults that delegate to the tag-awarecounterFromTaggedName/gaugeFromTaggedName(which retain the metadata on tag-aware scopes andIsolatedStoreImpl); the legacyThreadLocalStoreScopeImpl— which intentionally drops tag metadata on the*FromTaggedNamepath because it cannot compose tag-extracted names with its prefix in general — overrides them to honor the components, which arrive fully resolved. The full name is still reconstructed viaDynamicContext(dynamic spans preserved), so the cache key matches the stat the child independently creates — the fix removes the first-write-wins poisoning rather than working around it.Risk Level: Medium — changes stats labels after a hot restart for tag-bearing stats. Runtime guarded.
Testing: New tests:
stat_merger_test(ProgrammaticTagsSurviveMerge,ProgrammaticGaugeTagsSurviveMerge,ProgrammaticTagsLostWithoutMetadatapinning the pre-fix symptom),hot_restarting_parent_test(ExportsTagMetadataOnlyWhenNotRederivable,TagMetadataAppliedToMergedStatsend-to-end,TagMetadataIgnoredWhenRuntimeFlagDisabled), andthread_local_store_test(MergedStatNameHonorsSuppliedTagson both the legacy and tag-aware scopes). Built/run via RBE.Docs Changes: N/A
Release Notes: Added (
changelogs/current/bug_fixes/stats__hot-restart-propagate-programmatic-stat-tags.rst).Runtime guard:
envoy.reloadable_features.hot_restart_propagate_stat_tags(revert to legacy name-derived behavior).