Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions envoy/stats/stats_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,38 @@ static inline std::string statPrefixJoin(absl::string_view prefix, absl::string_
#define POOL_HISTOGRAM(POOL) POOL_HISTOGRAM_PREFIX(POOL, "")
#define POOL_TEXT_READOUT(POOL) POOL_TEXT_READOUT_PREFIX(POOL, "")

// Tagged variants of the POOL_*_PREFIX macros: create each stat directly on POOL with pre-encoded
// tags. BASE_PREFIX and PREFIX are Stats::StatNames (the tag-extracted and flat prefixes,
// pre-encoded once by the caller) and TAGS is a Stats::StatNameTagSpan. Callers must include
// "source/common/stats/utility.h". See Stats::Utility::counterFromTaggedPrefix.
#define POOL_COUNTER_TAGGED_PREFIX(POOL, BASE_PREFIX, TAGS, PREFIX) \
Envoy::Stats::Utility::counterFromTaggedPrefix((POOL), (BASE_PREFIX), (TAGS), (PREFIX), \
(FINISH_STAT_DECL_
#define POOL_GAUGE_TAGGED_PREFIX(POOL, BASE_PREFIX, TAGS, PREFIX) \
Envoy::Stats::Utility::gaugeFromTaggedPrefix((POOL), (BASE_PREFIX), (TAGS), (PREFIX), \
(FINISH_STAT_DECL_MODE_
#define POOL_HISTOGRAM_TAGGED_PREFIX(POOL, BASE_PREFIX, TAGS, PREFIX) \
Envoy::Stats::Utility::histogramFromTaggedPrefix((POOL), (BASE_PREFIX), (TAGS), (PREFIX), \
(FINISH_STAT_DECL_UNIT_
#define POOL_TEXT_READOUT_TAGGED_PREFIX(POOL, BASE_PREFIX, TAGS, PREFIX) \
Envoy::Stats::Utility::textReadoutFromTaggedPrefix((POOL), (BASE_PREFIX), (TAGS), (PREFIX), \
(FINISH_STAT_DECL_

// Convenience wrappers taking a Stats::TaggedStatName (see utility.h), which pre-encodes the
// tag-extracted prefix, the flat prefix and the tags.
#define POOL_COUNTER_TAGGED(POOL, TAGGED_NAME) \
POOL_COUNTER_TAGGED_PREFIX(POOL, (TAGGED_NAME).baseName(), (TAGGED_NAME).tags(), \
(TAGGED_NAME).name())
#define POOL_GAUGE_TAGGED(POOL, TAGGED_NAME) \
POOL_GAUGE_TAGGED_PREFIX(POOL, (TAGGED_NAME).baseName(), (TAGGED_NAME).tags(), \
(TAGGED_NAME).name())
#define POOL_HISTOGRAM_TAGGED(POOL, TAGGED_NAME) \
POOL_HISTOGRAM_TAGGED_PREFIX(POOL, (TAGGED_NAME).baseName(), (TAGGED_NAME).tags(), \
(TAGGED_NAME).name())
#define POOL_TEXT_READOUT_TAGGED(POOL, TAGGED_NAME) \
POOL_TEXT_READOUT_TAGGED_PREFIX(POOL, (TAGGED_NAME).baseName(), (TAGGED_NAME).tags(), \
(TAGGED_NAME).name())

#define NULL_STAT_DECL_(X) std::string(#X)),
#define NULL_STAT_DECL_IGNORE_MODE_(X, MODE) std::string(#X)),

Expand Down
13 changes: 13 additions & 0 deletions source/common/stats/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,16 @@ envoy_cc_library(
"//envoy/stats:stats_interface",
],
)

envoy_cc_library(
name = "prefix_utility_lib",
srcs = ["prefix_utility.cc"],
hdrs = ["prefix_utility.h"],
deps = [
":utility_lib",
"//envoy/stats:stats_interface",
"//envoy/stats:tag_interface",
"//source/common/common:assert_lib",
"//source/common/config:well_known_names",
],
)
82 changes: 82 additions & 0 deletions source/common/stats/prefix_utility.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include "source/common/stats/prefix_utility.h"

#include <optional>
#include <string>

#include "source/common/common/assert.h"
#include "source/common/config/well_known_names.h"

#include "absl/container/inlined_vector.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"

namespace Envoy {
namespace Stats {

namespace {

// Extracts the parent prefix's single tag: "http.<x>." -> {HTTP_CONN_MANAGER_PREFIX, x},
// "cluster.<x>." -> {CLUSTER_NAME, x}, anything else -> none. The trailing dot is stripped first.
// `prefix` must outlive the returned value.
std::optional<TagStringView> extractParentTag(absl::string_view prefix) {
if (!absl::EndsWith(prefix, ".")) {
return std::nullopt;
}
prefix.remove_suffix(1);

if (absl::StartsWith(prefix, "http.")) {
return TagStringView{Envoy::Config::TagNames::get().HTTP_CONN_MANAGER_PREFIX, prefix.substr(5)};
}
if (absl::StartsWith(prefix, "cluster.")) {
return TagStringView{Envoy::Config::TagNames::get().CLUSTER_NAME, prefix.substr(8)};
}
return std::nullopt;
}

} // namespace

TaggedStatName mergeStatPrefix(SymbolTable& symbol_table, absl::string_view prefix,
absl::string_view base_name, TagStringViewSpan tags,
absl::string_view name) {
// With no own tags the own prefix has no variable segment, so its tagged and tag-extracted forms
// are identical; callers need only supply base_name.
if (tags.empty()) {
name = base_name;
} else {
ASSERT(!name.empty(), "When tags are supplied, the caller must supply the tagged name with the "
"tag values interleaved.");
}

absl::InlinedVector<TagStringView, 2> merged_tags;

// The parent (HCM/cluster) prefix contributes at most one tag; the tag-extracted base then uses
// just its root ("http"/"cluster"). An unrecognized parent is kept verbatim and untagged.
absl::string_view base_prefix = prefix;
const std::optional<TagStringView> prefix_tag = extractParentTag(prefix);
if (prefix_tag.has_value()) {
merged_tags.push_back(*prefix_tag);
ASSERT(prefix.find('.') != absl::string_view::npos);
// Keep the trailing dot of the base part of the parent prefix, so that the final base is
// "<base_prefix><base_name>".
base_prefix = prefix.substr(0, prefix.find('.') + 1);
}
merged_tags.insert(merged_tags.end(), tags.begin(), tags.end());

// This helper function assumes the caller has already handled the dot correctly and then we can
// concatenate the two prefixes directly. The TaggedStatName then sanitizes the name, base, and
// tags to remove any leading or trailing dot.
const std::string tagged = absl::StrCat(prefix, name);

// When no tag is contributed (neither the parent prefix nor the input tags), base_prefix is the
// full parent prefix and name equals base_name, so the base and tagged forms are identical; reuse
// the tagged name instead of building an identical base string.
if (merged_tags.empty()) {
ASSERT(tagged == absl::StrCat(base_prefix, base_name));
return {symbol_table, tagged, merged_tags, tagged};
}
const std::string base = absl::StrCat(base_prefix, base_name);
return {symbol_table, base, merged_tags, tagged};
}

} // namespace Stats
} // namespace Envoy
42 changes: 42 additions & 0 deletions source/common/stats/prefix_utility.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#pragma once

#include "envoy/stats/stats.h"
#include "envoy/stats/tag.h"

#include "source/common/stats/utility.h"

#include "absl/strings/string_view.h"

namespace Envoy {
namespace Stats {

/**
* Merges a parent prefix with an owner's own name and tags to produce a TaggedStatName. This helper
* extracts the well-known tag from the parent prefix and merges it with the input tags to produce
* the final TaggedStatName.
*
* The helper assumes the caller has already handled the dots correctly, so it can concatenate the
* parent prefix and the own name directly.
*
* The merged tagged name is `<prefix><name>` and the merged base name is
* `<parent_root><base_name>`, where `<parent_root>` is `<prefix>` with its well-known tag value
* removed (e.g. "http.ingress." -> "http."), or the full `<prefix>` when it carries no well-known
* tag. When neither the parent prefix nor `tags` contributes a tag, the base and tagged names are
* identical.
*
* @param symbol_table the symbol table used to pre-encode the names and tags.
* @param prefix the parent prefix, whose well-known variable segment is extracted as a tag when
* the prefix ends with a trailing '.' (e.g. "http.<hcm>." / "cluster.<name>."); may be
* empty.
* @param base_name the tag-extracted own name (every own tag value removed).
* @param tags one {tag_name, value} per variable segment of the own name. When empty,
* `name` is ignored and `base_name` is used for both forms.
* @param name the tagged own name (own tag values interleaved). Combined with `prefix` this must
* equal the legacy flat prefix. Ignored when `tags` is empty.
*/
TaggedStatName mergeStatPrefix(SymbolTable& symbol_table, absl::string_view prefix,
absl::string_view base_name, TagStringViewSpan tags = {},
absl::string_view name = {});

} // namespace Stats
} // namespace Envoy
7 changes: 7 additions & 0 deletions source/common/stats/symbol_table.h
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,13 @@ class StatNamePool {
*/
void clear();

/**
* Pre-allocates capacity in the underlying storage for at least `count` StatNames. This avoids
* incremental re-allocations when the number of names to be added is known up-front.
* @param count the number of StatNames the pool is expected to hold.
*/
void reserve(size_t count) { storage_vector_.reserve(count); }

/**
* @param name the name to add the container.
* @return the StatName held in the container for this name.
Expand Down
140 changes: 140 additions & 0 deletions source/common/stats/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <optional>
#include <string>

#include "source/common/stats/symbol_table.h"

#include "absl/strings/match.h"
#include "absl/strings/str_replace.h"

Expand All @@ -26,6 +28,44 @@ std::string Utility::sanitizeStatsName(absl::string_view name) {
});
}

absl::string_view Utility::sanitizeStatsName(absl::string_view name, std::string& buffer) {
name = absl::StripPrefix(absl::StripSuffix(name, "."), ".");

// Check if the name needs sanitization.
if (name.find(':') != absl::string_view::npos || name.find('\0') != absl::string_view::npos) {
buffer = absl::StrReplaceAll(name, {
{"://", "_"},
{":/", "_"},
{":", "_"},
{absl::string_view("\0", 1), "_"},
});
return buffer;
}
return name;
}

TaggedStatName::TaggedStatName(SymbolTable& symbol_table, absl::string_view base_name,
TagStringViewSpan tags, absl::string_view name)
: tag_pool_(symbol_table) {
tag_pool_.reserve(tags.size() * 2 + 2);

std::string buffer;
base_name_ = tag_pool_.add(Utility::sanitizeStatsName(base_name, buffer));
if (tags.empty()) {
name_ = base_name_;
} else {
ASSERT(!name.empty(), "When tags are supplied, the caller must supply the tagged name with the "
"tag values interleaved.");
name_ = tag_pool_.add(Utility::sanitizeStatsName(name, buffer));
}

tags_.reserve(tags.size());
for (const TagStringView& tag : tags) {
tags_.push_back({tag_pool_.add(Utility::sanitizeStatsName(tag.first, buffer)),
tag_pool_.add(Utility::sanitizeStatsName(tag.second, buffer))});
}
}

std::optional<StatName> Utility::findTag(const Metric& metric, StatName find_tag_name) {
std::optional<StatName> value;
metric.iterateTagStatNames(
Expand Down Expand Up @@ -125,6 +165,106 @@ TextReadout& textReadoutFromStatNames(Scope& scope, const StatNameVec& elements,
return scope.textReadoutFromStatNameWithTags(StatName(joined.get()), tags);
}

Counter& counterFromTaggedPrefix(Scope& scope, StatName base_prefix, StatNameTagSpan prefix_tags,
StatName prefix, absl::string_view name) {
StatNameManagedStorage name_storage(name, scope.symbolTable());
return counterFromTaggedPrefix(scope, base_prefix, prefix_tags, prefix, name_storage.statName());
}

Gauge& gaugeFromTaggedPrefix(Scope& scope, StatName base_prefix, StatNameTagSpan prefix_tags,
StatName prefix, absl::string_view name,
Gauge::ImportMode import_mode) {
StatNameManagedStorage name_storage(name, scope.symbolTable());
return gaugeFromTaggedPrefix(scope, base_prefix, prefix_tags, prefix, name_storage.statName(),
import_mode);
}

Histogram& histogramFromTaggedPrefix(Scope& scope, StatName base_prefix,
StatNameTagSpan prefix_tags, StatName prefix,
absl::string_view name, Histogram::Unit unit) {
StatNameManagedStorage name_storage(name, scope.symbolTable());
return histogramFromTaggedPrefix(scope, base_prefix, prefix_tags, prefix, name_storage.statName(),
unit);
}

TextReadout& textReadoutFromTaggedPrefix(Scope& scope, StatName base_prefix,
StatNameTagSpan prefix_tags, StatName prefix,
absl::string_view name) {
StatNameManagedStorage name_storage(name, scope.symbolTable());
return textReadoutFromTaggedPrefix(scope, base_prefix, prefix_tags, prefix,
name_storage.statName());
}

Counter& counterFromTaggedPrefix(Scope& scope, StatName base_prefix, StatNameTagSpan prefix_tags,
StatName prefix, StatName name) {
SymbolTable& symbol_table = scope.symbolTable();
if (prefix_tags.empty()) {
prefix = base_prefix;
} else {
ASSERT(!prefix.empty(),
"When tags are supplied, the caller must supply the tagged prefix with the "
"tag values interleaved.");
}

SymbolTable::StoragePtr full_name = symbol_table.join({prefix, name});
SymbolTable::StoragePtr full_name_base = symbol_table.join({base_prefix, name});
return scope.counterFromTaggedName(StatName(full_name_base.get()), prefix_tags,
StatName(full_name.get()));
}

Gauge& gaugeFromTaggedPrefix(Scope& scope, StatName base_prefix, StatNameTagSpan prefix_tags,
StatName prefix, StatName name, Gauge::ImportMode import_mode) {
SymbolTable& symbol_table = scope.symbolTable();
if (prefix_tags.empty()) {
prefix = base_prefix;
} else {
ASSERT(!prefix.empty(),
"When tags are supplied, the caller must supply the tagged prefix with the "
"tag values interleaved.");
}

SymbolTable::StoragePtr full_name = symbol_table.join({prefix, name});
SymbolTable::StoragePtr full_name_base = symbol_table.join({base_prefix, name});
return scope.gaugeFromTaggedName(StatName(full_name_base.get()), prefix_tags,
StatName(full_name.get()), import_mode);
}

Histogram& histogramFromTaggedPrefix(Scope& scope, StatName base_prefix,
StatNameTagSpan prefix_tags, StatName prefix, StatName name,
Histogram::Unit unit) {
SymbolTable& symbol_table = scope.symbolTable();
if (prefix_tags.empty()) {
prefix = base_prefix;
} else {
ASSERT(!prefix.empty(),
"When tags are supplied, the caller must supply the tagged prefix with the "
"tag values interleaved.");
}

SymbolTable::StoragePtr full_name = symbol_table.join({prefix, name});
SymbolTable::StoragePtr full_name_base = symbol_table.join({base_prefix, name});
return scope.histogramFromTaggedName(StatName(full_name_base.get()), prefix_tags,
StatName(full_name.get()), unit);
}

TextReadout& textReadoutFromTaggedPrefix(Scope& scope, StatName base_prefix,
StatNameTagSpan prefix_tags, StatName prefix,
StatName name) {
SymbolTable& symbol_table = scope.symbolTable();
if (prefix_tags.empty()) {
prefix = base_prefix;
} else {
ASSERT(!prefix.empty(),
"When tags are supplied, the caller must supply the tagged prefix with the "
"tag values interleaved.");
}

SymbolTable::StoragePtr full_name = symbol_table.join({prefix, name});
SymbolTable::StoragePtr full_name_base = symbol_table.join({base_prefix, name});
return scope.textReadoutFromTaggedName(StatName(full_name_base.get()), prefix_tags,
StatName(full_name.get()));
}

} // namespace Utility
} // namespace Stats
} // namespace Envoy
Loading
Loading