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
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,9 @@ removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`

new_features:
- area: formatter
change: |
Added ``QUERY_PARAMS`` support for substitution formatter to log all query params.
See :ref:`access log formatter <config_access_log_format>` for more details.

deprecated:
15 changes: 15 additions & 0 deletions docs/root/configuration/advanced/substitution_formatter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,21 @@ Current supported substitution commands include:
TCP/UDP
Not implemented. It will appear as ``"-"`` in the access logs.

``%QUERY_PARAMS(X):Z%``
HTTP
All of the query parameters. The parameter ``X`` is used to specify how the query parameters are presented
and is optional, with ``ORIG`` then being the default.

The ``X`` parameter can be:

* ``ORIG``: The output will be original query params string part of the path with no treatment.
* ``DECODED``: The query params will be URL decoded.

``Z`` is an optional parameter denoting string truncation up to ``Z`` characters long.

TCP/UDP
Not implemented. It will appear as ``"-"`` in the access logs.

``%PATH(X:Y):Z%``
HTTP
The value of the request path. The parameter ``X`` is used to specify whether the output contains
Expand Down
55 changes: 55 additions & 0 deletions source/common/formatter/http_specific_formatter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,56 @@ QueryParameterFormatter::formatValue(const Context& context,
return ValueUtil::optionalStringValue(format(context, stream_info));
}

absl::StatusOr<FormatterProviderPtr> QueryParametersFormatter::create(absl::string_view decoding,
absl::optional<size_t> max_length) {
bool decode_bool = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this logic into the constructor? It is better to parse the config at config time, than at runtime. E.g. save decode_bool_ as a bool field rather than recompute it from decoding_.

Copy link
Author

@frittentheke frittentheke Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will align this with how the PathFormatter does things (create method). I hope less variants are a good thing.

if (decoding.empty()) {
decode_bool = false;
} else if (decoding == "ORIG") {
decode_bool = false;
} if (decoding == "DECODED") {
decode_bool = true;
} else {
return absl::InvalidArgumentError(
fmt::format("Invalid QUERY_PARAMS option: '{}', only 'ORIG'/'DECODED' are allowed", decoding));
}

return std::make_unique<QueryParametersFormatter>(decode_bool, max_length);
}

// FormatterProvider
absl::optional<std::string> QueryParametersFormatter::format(const Context& context,
const StreamInfo::StreamInfo&) const {
const auto request_headers = context.requestHeaders();
if (!request_headers.has_value()) {
return absl::nullopt;
}

// Gather query parameters substring from path
absl::string_view path_view = request_headers->getPathValue();
auto query_offset = path_view.find('?');
absl::optional<string_view> query_params;
if (query_offset != absl::string_view::npos) {
query_params = path_view.substr(query_offset+1);
}

// Apply query param percent decoding on the query params if requested
if (query_params.has_value() && decode_bool) {
query_params = Http::Utility::PercentEncoding::urlDecodeQueryParameter(query_params);
}

if (query_params.has_value() && max_length_.has_value()) {
SubstitutionFormatUtils::truncate(query_params.value(), max_length_.value());
}
return query_params;
}

Protobuf::Value
QueryParametersFormatter::formatValue(const Context& context,
const StreamInfo::StreamInfo& stream_info) const {
return ValueUtil::optionalStringValue(format(context, stream_info));
}

absl::optional<std::string> PathFormatter::format(const Context& context,
const StreamInfo::StreamInfo&) const {

Expand Down Expand Up @@ -495,6 +545,11 @@ BuiltInHttpCommandParser::getKnownFormatters() {
[](absl::string_view format, absl::optional<size_t> max_length) {
return std::make_unique<QueryParameterFormatter>(std::string(format), max_length);
}}},
{"QUERY_PARAMS",
{CommandSyntaxChecker::PARAMS_OPTIONAL | CommandSyntaxChecker::LENGTH_ALLOWED,
[](absl::string_view decoding, absl::optional<size_t> max_length) {
return std::make_unique<QueryParametersFormatter>(std::string(decoding), max_length);
}}},
{"PATH",
{CommandSyntaxChecker::PARAMS_OPTIONAL | CommandSyntaxChecker::LENGTH_ALLOWED,
[](absl::string_view format, absl::optional<size_t> max_length) {
Expand Down
20 changes: 20 additions & 0 deletions source/common/formatter/http_specific_formatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,26 @@ class QueryParameterFormatter : public FormatterProvider {
absl::optional<size_t> max_length_;
};

class QueryParametersFormatter : public FormatterProvider {
public:

static absl::StatusOr<FormatterProviderPtr>
create(absl::string_view decoding, absl::optional<size_t> max_length);

// FormatterProvider
absl::optional<std::string> decoding(const Context& context,
const StreamInfo::StreamInfo& stream_info) const override;
Protobuf::Value formatValue(const Context& context,
const StreamInfo::StreamInfo& stream_info) const override;

QueryParametersFormatter(bool decode_bool, absl::optional<size_t> max_length)
: decode_bool_(decode_bool), max_length_(max_length) {}

private:
const bool decode_bool_{};
absl::optional<size_t> max_length_;
};

class PathFormatter : public FormatterProvider {
public:
enum PathFormatterOption {
Expand Down
59 changes: 58 additions & 1 deletion test/common/formatter/substitution_formatter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2737,7 +2737,7 @@ TEST(SubstitutionFormatterTest, requestHeaderFormatter) {
}
}

TEST(SubstitutionFormatterTest, QueryPraameterFormatter) {
TEST(SubstitutionFormatterTest, QueryParameterFormatter) {
StreamInfo::MockStreamInfo stream_info;
Http::TestRequestHeaderMapImpl request_header{{":method", "GET"}, {":path", "/path?x=xxxxxx"}};

Expand Down Expand Up @@ -2766,6 +2766,63 @@ TEST(SubstitutionFormatterTest, QueryPraameterFormatter) {
}
}

TEST(SubstitutionFormatterTest, QueryParametersFormatter) {
StreamInfo::MockStreamInfo stream_info;
Http::TestRequestHeaderMapImpl request_header{{":method", "GET"}, {":path",
"/path?x=xxxxxx&y=yyyyy&z=zzz&encoded=%26%"}};

Context formatter_context;
formatter_context.setRequestHeaders(request_header);

{
EXPECT_THROW_WITH_MESSAGE(SubstitutionFormatParser::parse("%QUERY_PARAMS(A)%").IgnoreError(),
EnvoyException,
"Invalid PATH option: 'A', only 'ORIG'/'DECODED' are allowed");
}

{
QueryParametersFormatter formatter("", absl::optional<size_t>());
EXPECT_EQ("x=xxxxxx&y=yyyyy&z=zzz&encoded=%23%", formatter.format(formatter_context, stream_info));
EXPECT_THAT(formatter.formatValue(formatter_context, stream_info),
ProtoEq(ValueUtil::stringValue("x=xxxxxx&y=yyyyy&z=zzz&encoded=%23%")));
}

{
QueryParametersFormatter formatter("ORIG", absl::optional<size_t>());
EXPECT_EQ("x=xxxxxx&y=yyyyy&z=zzz&encoded=%23%", formatter.format(formatter_context, stream_info));
EXPECT_THAT(formatter.formatValue(formatter_context, stream_info),
ProtoEq(ValueUtil::stringValue("x=xxxxxx&y=yyyyy&z=zzz&encoded=%23%")));
}

{
QueryParametersFormatter formatter("DECODED",absl::optional<size_t>());
EXPECT_EQ("x=xxxxxx&y=yyyyy&z=zzz&encoded=#", formatter.format(formatter_context, stream_info));
EXPECT_THAT(formatter.formatValue(formatter_context, stream_info),
ProtoEq(ValueUtil::stringValue("x=xxxxxx&y=yyyyy&z=zzz&encoded=#")));
}

{
QueryParametersFormatter formatter("", absl::optional<size_t>(4));
EXPECT_EQ("x=xx", formatter.format(formatter_context, stream_info));
EXPECT_THAT(formatter.formatValue(formatter_context, stream_info),
ProtoEq(ValueUtil::stringValue("x=xx")));
}

{
QueryParametersFormatter formatter("ORIG", absl::optional<size_t>(4));
EXPECT_EQ("x=xx", formatter.format(formatter_context, stream_info));
EXPECT_THAT(formatter.formatValue(formatter_context, stream_info),
ProtoEq(ValueUtil::stringValue("x=xx")));
}

{
QueryParametersFormatter formatter("DECODED", absl::optional<size_t>(4));
EXPECT_EQ("x=x", formatter.format(formatter_context, stream_info));
EXPECT_THAT(formatter.formatValue(formatter_context, stream_info),
ProtoEq(ValueUtil::stringValue("x=xx")));
}
}

TEST(SubstitutionFormatterTest, headersByteSizeFormatter) {
StreamInfo::MockStreamInfo stream_info;
Http::TestRequestHeaderMapImpl request_header{{":method", "GET"}, {":path", "/"}};
Expand Down
Loading