Skip to content

Commit c7cd338

Browse files
committed
h2m: add optional stats to header-to-metadata filter
Signed-off-by: Rohit Agrawal <[email protected]>
1 parent af61c6b commit c7cd338

File tree

8 files changed

+503
-26
lines changed

8 files changed

+503
-26
lines changed

api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,15 @@ message Config {
138138

139139
// The list of rules to apply to responses.
140140
repeated Rule response_rules = 2;
141+
142+
// Optional prefix to use when emitting filter statistics. When configured,
143+
// statistics are emitted with the prefix ``http_filter_name.<stat_prefix>``.
144+
//
145+
// This emits statistics such as:
146+
//
147+
// - ``http_filter_name.my_header_converter.rules_processed``
148+
// - ``http_filter_name.my_header_converter.metadata_added``
149+
//
150+
// If not configured, no statistics are emitted.
151+
string stat_prefix = 3;
141152
}

changelogs/current.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,11 @@ new_features:
6464
- area: dns_filter, redis_proxy and prefix_matcher_map
6565
change: |
6666
Switch to using Radix Tree instead of Trie for performance improvements.
67+
- area: header_to_metadata
68+
change: |
69+
Added optional statistics collection for the Header-To-Metadata filter. When the :ref:`stat_prefix
70+
<envoy_v3_api_field_extensions.filters.http.header_to_metadata.v3.Config.stat_prefix>` field is configured,
71+
the filter emits detailed counters for rule processing, metadata operations, etc. See
72+
:ref:`Header-To-Metadata filter statistics <config_http_filters_header_to_metadata>` for details.
6773
6874
deprecated:

docs/root/configuration/http/http_filters/header_to_metadata_filter.rst

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@ A typical use case for this filter is to dynamically match requests with load ba
2020
subsets. For this, a given header's value would be extracted and attached to the request
2121
as dynamic metadata which would then be used to match a subset of endpoints.
2222

23+
Statistics
24+
----------
25+
26+
The filter can optionally emit statistics when the :ref:`stat_prefix <envoy_v3_api_field_extensions.filters.http.header_to_metadata.v3.Config.stat_prefix>` field is configured.
27+
These statistics are rooted at *http_filter_name.<stat_prefix>* with the following counters:
28+
29+
.. csv-table::
30+
:header: Name, Type, Description
31+
:widths: 1, 1, 2
32+
33+
request_rules_processed, Counter, Total number of request rules processed
34+
response_rules_processed, Counter, Total number of response rules processed
35+
request_metadata_added, Counter, Total number of metadata entries successfully added from request headers
36+
response_metadata_added, Counter, Total number of metadata entries successfully added from response headers
37+
request_header_not_found, Counter, Total number of times expected request headers were missing
38+
response_header_not_found, Counter, Total number of times expected response headers were missing
39+
base64_decode_failed, Counter, Total number of times Base64 decoding failed
40+
header_value_too_long, Counter, Total number of times header values exceeded the maximum length
41+
regex_substitution_failed, Counter, Total number of times regex substitution resulted in empty values
42+
2343
Example
2444
-------
2545

@@ -79,7 +99,24 @@ Note that this filter also supports per route configuration:
7999
This can be used to either override the global configuration or if the global configuration
80100
is empty (no rules), it can be used to only enable the filter at a per route level.
81101

82-
Statistics
83-
----------
102+
Configuration with Statistics
103+
-----------------------------
104+
105+
To enable statistics collection, configure the ``stat_prefix`` field:
106+
107+
.. literalinclude:: _include/header-to-metadata-filter-with-stats.yaml
108+
:language: yaml
109+
:lines: 25-40
110+
:lineno-start: 25
111+
:linenos:
112+
:caption: :download:`header-to-metadata-filter-with-stats.yaml <_include/header-to-metadata-filter-with-stats.yaml>`
113+
114+
This configuration would emit statistics such as:
115+
116+
- ``http_filter_name.header_converter.request_rules_processed``
117+
- ``http_filter_name.header_converter.request_metadata_added``
118+
- ``http_filter_name.header_converter.response_rules_processed``
119+
- ``http_filter_name.header_converter.response_metadata_added``
120+
- ``http_filter_name.header_converter.request_header_not_found``
84121

85-
Currently, this filter generates no statistics.
122+
When ``stat_prefix`` is not configured, no statistics are emitted.

source/extensions/filters/http/header_to_metadata/config.cc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ namespace HeaderToMetadataFilter {
1717
absl::StatusOr<Http::FilterFactoryCb> HeaderToMetadataConfig::createFilterFactoryFromProtoTyped(
1818
const envoy::extensions::filters::http::header_to_metadata::v3::Config& proto_config,
1919
const std::string&, Server::Configuration::FactoryContext& context) {
20-
absl::StatusOr<ConfigSharedPtr> filter_config_or =
21-
Config::create(proto_config, context.serverFactoryContext().regexEngine(), false);
20+
absl::StatusOr<ConfigSharedPtr> filter_config_or = Config::create(
21+
proto_config, context.serverFactoryContext().regexEngine(), context.scope(), false);
2222
RETURN_IF_ERROR(filter_config_or.status());
2323

2424
return [filter_config = std::move(filter_config_or.value())](
@@ -32,7 +32,8 @@ absl::StatusOr<Router::RouteSpecificFilterConfigConstSharedPtr>
3232
HeaderToMetadataConfig::createRouteSpecificFilterConfigTyped(
3333
const envoy::extensions::filters::http::header_to_metadata::v3::Config& config,
3434
Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) {
35-
absl::StatusOr<ConfigSharedPtr> config_or = Config::create(config, context.regexEngine(), true);
35+
absl::StatusOr<ConfigSharedPtr> config_or =
36+
Config::create(config, context.regexEngine(), context.scope(), true);
3637
RETURN_IF_ERROR(config_or.status());
3738
return std::move(config_or.value());
3839
}

source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,16 @@ Rule::Rule(const ProtoRule& rule, Regex::Engine& regex_engine, absl::Status& cre
111111

112112
absl::StatusOr<ConfigSharedPtr>
113113
Config::create(const envoy::extensions::filters::http::header_to_metadata::v3::Config& config,
114-
Regex::Engine& regex_engine, bool per_route) {
114+
Regex::Engine& regex_engine, Stats::Scope& scope, bool per_route) {
115115
absl::Status creation_status = absl::OkStatus();
116-
auto cfg = ConfigSharedPtr(new Config(config, regex_engine, per_route, creation_status));
116+
auto cfg = ConfigSharedPtr(new Config(config, regex_engine, scope, per_route, creation_status));
117117
RETURN_IF_NOT_OK_REF(creation_status);
118118
return cfg;
119119
}
120120

121121
Config::Config(const envoy::extensions::filters::http::header_to_metadata::v3::Config config,
122-
Regex::Engine& regex_engine, const bool per_route, absl::Status& creation_status) {
122+
Regex::Engine& regex_engine, Stats::Scope& scope, const bool per_route,
123+
absl::Status& creation_status) {
123124
absl::StatusOr<bool> request_set_or =
124125
Config::configToVector(config.request_rules(), request_rules_, regex_engine);
125126
SET_AND_RETURN_IF_NOT_OK(request_set_or.status(), creation_status);
@@ -130,6 +131,11 @@ Config::Config(const envoy::extensions::filters::http::header_to_metadata::v3::C
130131
SET_AND_RETURN_IF_NOT_OK(response_set_or.status(), creation_status);
131132
response_set_ = response_set_or.value();
132133

134+
// Generate stats only if stat_prefix is configured (opt-in behavior).
135+
if (!config.stat_prefix().empty()) {
136+
stats_.emplace(generateStats(config.stat_prefix(), scope));
137+
}
138+
133139
// Note: empty configs are fine for the global config, which would be the case for enabling
134140
// the filter globally without rules and then applying them at the virtual host or
135141
// route level. At the virtual or route level, it makes no sense to have an empty
@@ -158,6 +164,12 @@ absl::StatusOr<bool> Config::configToVector(const ProtobufRepeatedRule& proto_ru
158164
return true;
159165
}
160166

167+
HeaderToMetadataFilterStats Config::generateStats(const std::string& stat_prefix,
168+
Stats::Scope& scope) {
169+
const std::string final_prefix = fmt::format("http_filter_name.{}", stat_prefix);
170+
return {ALL_HEADER_TO_METADATA_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))};
171+
}
172+
161173
HeaderToMetadataFilter::HeaderToMetadataFilter(const ConfigSharedPtr config) : config_(config) {}
162174

163175
HeaderToMetadataFilter::~HeaderToMetadataFilter() = default;
@@ -166,7 +178,8 @@ Http::FilterHeadersStatus HeaderToMetadataFilter::decodeHeaders(Http::RequestHea
166178
bool) {
167179
const auto* config = getConfig();
168180
if (config->doRequest()) {
169-
writeHeaderToMetadata(headers, config->requestRules(), *decoder_callbacks_);
181+
writeHeaderToMetadata(headers, config->requestRules(), *decoder_callbacks_,
182+
HeaderDirection::Request);
170183
}
171184

172185
return Http::FilterHeadersStatus::Continue;
@@ -181,7 +194,8 @@ Http::FilterHeadersStatus HeaderToMetadataFilter::encodeHeaders(Http::ResponseHe
181194
bool) {
182195
const auto* config = getConfig();
183196
if (config->doResponse()) {
184-
writeHeaderToMetadata(headers, config->responseRules(), *encoder_callbacks_);
197+
writeHeaderToMetadata(headers, config->responseRules(), *encoder_callbacks_,
198+
HeaderDirection::Response);
185199
}
186200
return Http::FilterHeadersStatus::Continue;
187201
}
@@ -193,21 +207,28 @@ void HeaderToMetadataFilter::setEncoderFilterCallbacks(
193207

194208
bool HeaderToMetadataFilter::addMetadata(StructMap& struct_map, const std::string& meta_namespace,
195209
const std::string& key, std::string value, ValueType type,
196-
ValueEncode encode) const {
210+
ValueEncode encode, HeaderDirection direction) const {
197211
ProtobufWkt::Value val;
212+
const auto* config = getConfig();
198213

199214
ASSERT(!value.empty());
200215

201216
if (value.size() >= MAX_HEADER_VALUE_LEN) {
202217
// Too long, go away.
203218
ENVOY_LOG(debug, "metadata value is too long");
219+
if (config->stats().has_value()) {
220+
config->stats().value().header_value_too_long_.inc();
221+
}
204222
return false;
205223
}
206224

207225
if (encode == envoy::extensions::filters::http::header_to_metadata::v3::Config::BASE64) {
208226
value = Base64::decodeWithoutPadding(value);
209227
if (value.empty()) {
210228
ENVOY_LOG(debug, "Base64 decode failed");
229+
if (config->stats().has_value()) {
230+
config->stats().value().base64_decode_failed_.inc();
231+
}
211232
return false;
212233
}
213234
}
@@ -240,6 +261,15 @@ bool HeaderToMetadataFilter::addMetadata(StructMap& struct_map, const std::strin
240261
auto& keyval = struct_map[meta_namespace];
241262
(*keyval.mutable_fields())[key] = std::move(val);
242263

264+
// Increment metadata_added stat if stats are enabled.
265+
if (config->stats().has_value()) {
266+
if (direction == HeaderDirection::Request) {
267+
config->stats().value().request_metadata_added_.inc();
268+
} else {
269+
config->stats().value().response_metadata_added_.inc();
270+
}
271+
}
272+
243273
return true;
244274
}
245275

@@ -249,38 +279,68 @@ const std::string& HeaderToMetadataFilter::decideNamespace(const std::string& ns
249279

250280
// add metadata['key']= value depending on header present or missing case
251281
void HeaderToMetadataFilter::applyKeyValue(std::string&& value, const Rule& rule,
252-
const KeyValuePair& keyval, StructMap& np) {
282+
const KeyValuePair& keyval, StructMap& np,
283+
HeaderDirection direction) {
284+
const auto* config = getConfig();
285+
253286
if (!keyval.value().empty()) {
254287
value = keyval.value();
255288
} else {
256289
const auto& matcher = rule.regexRewrite();
257290
if (matcher != nullptr) {
291+
std::string original_value = value;
258292
value = matcher->replaceAll(value, rule.regexSubstitution());
293+
// If we had a non-empty input but got an empty result from regex, it could indicate a
294+
// failure.
295+
if (!original_value.empty() && value.empty()) {
296+
if (config->stats().has_value()) {
297+
config->stats().value().regex_substitution_failed_.inc();
298+
}
299+
}
259300
}
260301
}
261302
if (!value.empty()) {
262303
const auto& nspace = decideNamespace(keyval.metadata_namespace());
263-
addMetadata(np, nspace, keyval.key(), value, keyval.type(), keyval.encode());
304+
addMetadata(np, nspace, keyval.key(), value, keyval.type(), keyval.encode(), direction);
264305
} else {
265306
ENVOY_LOG(debug, "value is empty, not adding metadata");
266307
}
267308
}
268309

269310
void HeaderToMetadataFilter::writeHeaderToMetadata(Http::HeaderMap& headers,
270311
const HeaderToMetadataRules& rules,
271-
Http::StreamFilterCallbacks& callbacks) {
312+
Http::StreamFilterCallbacks& callbacks,
313+
HeaderDirection direction) {
272314
StructMap structs_by_namespace;
315+
const auto* config = getConfig();
273316

274317
for (const auto& rule : rules) {
275318
const auto& proto_rule = rule.rule();
276319
absl::optional<std::string> value = rule.selector_->extract(headers);
277320

321+
// Increment rules_processed stat if stats are enabled.
322+
if (config->stats().has_value()) {
323+
if (direction == HeaderDirection::Request) {
324+
config->stats().value().request_rules_processed_.inc();
325+
} else {
326+
config->stats().value().response_rules_processed_.inc();
327+
}
328+
}
329+
278330
if (value && proto_rule.has_on_header_present()) {
279331
applyKeyValue(std::move(value).value_or(""), rule, proto_rule.on_header_present(),
280-
structs_by_namespace);
332+
structs_by_namespace, direction);
281333
} else if (!value && proto_rule.has_on_header_missing()) {
334+
// Increment header_not_found stat if stats are enabled.
335+
if (config->stats().has_value()) {
336+
if (direction == HeaderDirection::Request) {
337+
config->stats().value().request_header_not_found_.inc();
338+
} else {
339+
config->stats().value().response_header_not_found_.inc();
340+
}
341+
}
282342
applyKeyValue(std::move(value).value_or(""), rule, proto_rule.on_header_missing(),
283-
structs_by_namespace);
343+
structs_by_namespace, direction);
284344
}
285345
}
286346
// Any matching rules?

source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,37 @@ namespace Extensions {
1717
namespace HttpFilters {
1818
namespace HeaderToMetadataFilter {
1919

20+
/**
21+
* All stats for the Header-To-Metadata filter. @see stats_macros.h
22+
*/
23+
#define ALL_HEADER_TO_METADATA_FILTER_STATS(COUNTER) \
24+
COUNTER(request_rules_processed) \
25+
COUNTER(response_rules_processed) \
26+
COUNTER(request_metadata_added) \
27+
COUNTER(response_metadata_added) \
28+
COUNTER(request_header_not_found) \
29+
COUNTER(response_header_not_found) \
30+
COUNTER(base64_decode_failed) \
31+
COUNTER(header_value_too_long) \
32+
COUNTER(regex_substitution_failed)
33+
34+
/**
35+
* Wrapper struct for header-to-metadata filter stats. @see stats_macros.h
36+
*/
37+
struct HeaderToMetadataFilterStats {
38+
ALL_HEADER_TO_METADATA_FILTER_STATS(GENERATE_COUNTER_STRUCT)
39+
};
40+
2041
using ProtoRule = envoy::extensions::filters::http::header_to_metadata::v3::Config::Rule;
2142
using ValueType = envoy::extensions::filters::http::header_to_metadata::v3::Config::ValueType;
2243
using ValueEncode = envoy::extensions::filters::http::header_to_metadata::v3::Config::ValueEncode;
2344
using KeyValuePair = envoy::extensions::filters::http::header_to_metadata::v3::Config::KeyValuePair;
2445

46+
/**
47+
* Enum to distinguish between request and response processing for stats collection.
48+
*/
49+
enum class HeaderDirection { Request, Response };
50+
2551
// Interface for getting values from a cookie or a header.
2652
class ValueSelector {
2753
public:
@@ -98,18 +124,20 @@ class Config : public ::Envoy::Router::RouteSpecificFilterConfig,
98124
public:
99125
static absl::StatusOr<std::shared_ptr<Config>>
100126
create(const envoy::extensions::filters::http::header_to_metadata::v3::Config& config,
101-
Regex::Engine& regex_engine, bool per_route = false);
127+
Regex::Engine& regex_engine, Stats::Scope& scope, bool per_route = false);
102128

103129
const HeaderToMetadataRules& requestRules() const { return request_rules_; }
104130
const HeaderToMetadataRules& responseRules() const { return response_rules_; }
105131
bool doResponse() const { return response_set_; }
106132
bool doRequest() const { return request_set_; }
133+
const absl::optional<HeaderToMetadataFilterStats>& stats() const { return stats_; }
107134

108135
private:
109136
using ProtobufRepeatedRule = Protobuf::RepeatedPtrField<ProtoRule>;
110137

111138
Config(const envoy::extensions::filters::http::header_to_metadata::v3::Config config,
112-
Regex::Engine& regex_engine, bool per_route, absl::Status& creation_status);
139+
Regex::Engine& regex_engine, Stats::Scope& scope, bool per_route,
140+
absl::Status& creation_status);
113141

114142
/**
115143
* configToVector is a helper function for converting from configuration (protobuf types) into
@@ -125,12 +153,22 @@ class Config : public ::Envoy::Router::RouteSpecificFilterConfig,
125153
static absl::StatusOr<bool> configToVector(const ProtobufRepeatedRule&, HeaderToMetadataRules&,
126154
Regex::Engine&);
127155

156+
/**
157+
* Generate stats for the header-to-metadata filter.
158+
* @param stat_prefix the prefix to use for stats.
159+
* @param scope the stats scope.
160+
* @return HeaderToMetadataFilterStats the generated stats.
161+
*/
162+
static HeaderToMetadataFilterStats generateStats(const std::string& stat_prefix,
163+
Stats::Scope& scope);
164+
128165
const std::string& decideNamespace(const std::string& nspace) const;
129166

130167
HeaderToMetadataRules request_rules_;
131168
HeaderToMetadataRules response_rules_;
132169
bool response_set_;
133170
bool request_set_;
171+
absl::optional<HeaderToMetadataFilterStats> stats_;
134172
};
135173

136174
using ConfigSharedPtr = std::shared_ptr<Config>;
@@ -193,12 +231,14 @@ class HeaderToMetadataFilter : public Http::StreamFilter,
193231
* @param rules the header-to-metadata mapping set in configuration.
194232
* @param callbacks the callback used to fetch the StreamInfo (which is then used to get
195233
* metadata). Callable with both encoder_callbacks_ and decoder_callbacks_.
234+
* @param direction whether processing request or response headers for stats collection.
196235
*/
197236
void writeHeaderToMetadata(Http::HeaderMap& headers, const HeaderToMetadataRules& rules,
198-
Http::StreamFilterCallbacks& callbacks);
237+
Http::StreamFilterCallbacks& callbacks, HeaderDirection direction);
199238
bool addMetadata(StructMap&, const std::string&, const std::string&, std::string, ValueType,
200-
ValueEncode) const;
201-
void applyKeyValue(std::string&&, const Rule&, const KeyValuePair&, StructMap&);
239+
ValueEncode, HeaderDirection direction) const;
240+
void applyKeyValue(std::string&&, const Rule&, const KeyValuePair&, StructMap&,
241+
HeaderDirection direction);
202242
const std::string& decideNamespace(const std::string& nspace) const;
203243
const Config* getConfig() const;
204244
};

0 commit comments

Comments
 (0)