Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion docs/root/configuration/observability/access_log/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ would be rendered as the number ``123``.
Format dictionaries have the following restrictions:

* The dictionary must map strings to strings (specifically, strings to command operators). Nesting
is not currently supported.
is supported.
* When using the ``typed_json_format`` command operators will only produce typed output if the
command operator is the only string that appears in the dictionary value. For example,
``"%DURATION%"`` will log a numeric duration value, but ``"%DURATION%.0"`` will log a string
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ New Features
------------
* access log: added a :ref:`dynamic metadata filter<envoy_v3_api_msg_config.accesslog.v3.MetadataFilter>` for access logs, which filters whether to log based on matching dynamic metadata.
* access log: added support for :ref:`%DOWNSTREAM_PEER_FINGERPRINT_1% <config_access_log_format_response_flags>` as a response flag.
* access log: added support for nested objects in :ref:`JSON logging mode <config_access_log_format_dictionaries>`.
* build: enable building envoy :ref:`arm64 images <arm_binaries>` by buildx tool in x86 CI platform.
* dynamic_forward_proxy: added :ref:`use_tcp_for_dns_lookups<envoy_v3_api_field_extensions.common.dynamic_forward_proxy.v3.DnsCacheConfig.use_tcp_for_dns_lookups>` option to use TCP for DNS lookups in order to match the DNS options for :ref:`Clusters<envoy_v3_api_msg_config.cluster.v3.Cluster>`.
* ext_authz filter: added support for emitting dynamic metadata for both :ref:`HTTP <config_http_filters_ext_authz_dynamic_metadata>` and :ref:`network <config_network_filters_ext_authz_dynamic_metadata>` filters.
Expand Down
18 changes: 1 addition & 17 deletions source/common/formatter/substitution_format_string.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,11 @@

namespace Envoy {
namespace Formatter {
namespace {

absl::flat_hash_map<std::string, std::string>
convertJsonFormatToMap(const ProtobufWkt::Struct& json_format) {
absl::flat_hash_map<std::string, std::string> output;
for (const auto& pair : json_format.fields()) {
if (pair.second.kind_case() != ProtobufWkt::Value::kStringValue) {
throw EnvoyException("Only string values are supported in the JSON access log format.");
}
output.emplace(pair.first, pair.second.string_value());
}
return output;
}

} // namespace

FormatterPtr
SubstitutionFormatStringUtils::createJsonFormatter(const ProtobufWkt::Struct& struct_format,
bool preserve_types) {
auto json_format_map = convertJsonFormatToMap(struct_format);
return std::make_unique<JsonFormatterImpl>(json_format_map, preserve_types);
return std::make_unique<JsonFormatterImpl>(struct_format, preserve_types);
}

FormatterPtr SubstitutionFormatStringUtils::fromProtoConfig(
Expand Down
86 changes: 52 additions & 34 deletions source/common/formatter/substitution_formatter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ const std::regex& getStartTimeNewlinePattern() {
}
const std::regex& getNewlinePattern() { CONSTRUCT_ON_FIRST_USE(std::regex, "\n"); }

template <class... Ts> struct JsonFormatMapVisitor : Ts... { using Ts::operator()...; };
template <class... Ts> JsonFormatMapVisitor(Ts...) -> JsonFormatMapVisitor<Ts...>;
Comment thread
zuercher marked this conversation as resolved.

} // namespace

const std::string SubstitutionFormatUtils::DEFAULT_FORMAT =
Expand Down Expand Up @@ -109,14 +112,6 @@ std::string FormatterImpl::format(const Http::RequestHeaderMap& request_headers,
return log_line;
}

JsonFormatterImpl::JsonFormatterImpl(
const absl::flat_hash_map<std::string, std::string>& format_mapping, bool preserve_types)
: preserve_types_(preserve_types) {
for (const auto& pair : format_mapping) {
json_output_format_.emplace(pair.first, SubstitutionFormatParser::parse(pair.second));
}
}

std::string JsonFormatterImpl::format(const Http::RequestHeaderMap& request_headers,
const Http::ResponseHeaderMap& response_headers,
const Http::ResponseTrailerMap& response_trailers,
Expand All @@ -129,37 +124,60 @@ std::string JsonFormatterImpl::format(const Http::RequestHeaderMap& request_head
return absl::StrCat(log_line, "\n");
}

JsonFormatMap JsonFormatterImpl::toFormatMap(const ProtobufWkt::Struct& json_format) const {
auto output = std::make_unique<std::map<std::string, JsonFormatMapValue>>();
for (const auto& pair : json_format.fields()) {
switch (pair.second.kind_case()) {
case ProtobufWkt::Value::kStringValue:
output->emplace(pair.first, SubstitutionFormatParser::parse(pair.second.string_value()));
break;
case ProtobufWkt::Value::kStructValue:
output->emplace(pair.first, toFormatMap(pair.second.struct_value()));
break;
default:
throw EnvoyException(
"Only string/object values are supported in the JSON access log format.");
Comment thread
Pchelolo marked this conversation as resolved.
Outdated
}
}
return {std::move(output)};
};

ProtobufWkt::Struct JsonFormatterImpl::toStruct(const Http::RequestHeaderMap& request_headers,
const Http::ResponseHeaderMap& response_headers,
const Http::ResponseTrailerMap& response_trailers,
const StreamInfo::StreamInfo& stream_info,
absl::string_view local_reply_body) const {
ProtobufWkt::Struct output;
auto* fields = output.mutable_fields();
for (const auto& pair : json_output_format_) {
const auto& providers = pair.second;
ASSERT(!providers.empty());

if (providers.size() == 1) {
const auto& provider = providers.front();
const auto val =
preserve_types_ ? provider->formatValue(request_headers, response_headers,
response_trailers, stream_info, local_reply_body)
: ValueUtil::stringValue(
provider->format(request_headers, response_headers,
response_trailers, stream_info, local_reply_body));
(*fields)[pair.first] = val;
} else {
// Multiple providers forces string output.
std::string str;
for (const auto& provider : providers) {
str += provider->format(request_headers, response_headers, response_trailers, stream_info,
local_reply_body);
}
(*fields)[pair.first] = ValueUtil::stringValue(str);
}
}
return output;
const std::function<ProtobufWkt::Value(const std::vector<FormatterProviderPtr>&)>
providers_callback = [&](const std::vector<FormatterProviderPtr>& providers) {
ASSERT(!providers.empty());
if (providers.size() == 1) {
const auto& provider = providers.front();
if (preserve_types_) {
return provider->formatValue(request_headers, response_headers, response_trailers,
stream_info, local_reply_body);
}
return ValueUtil::stringValue(provider->format(
request_headers, response_headers, response_trailers, stream_info, local_reply_body));
}
// Multiple providers forces string output.
std::string str;
for (const auto& provider : providers) {
str += provider->format(request_headers, response_headers, response_trailers, stream_info,
local_reply_body);
}
return ValueUtil::stringValue(str);
};
const std::function<ProtobufWkt::Value(const JsonFormatMap&)> json_format_map_callback =
[&](const JsonFormatMap& format) {
ProtobufWkt::Struct output;
auto* fields = output.mutable_fields();
for (const auto& pair : *format.value_) {
(*fields)[pair.first] = std::visit(
JsonFormatMapVisitor{json_format_map_callback, providers_callback}, pair.second);
}
return ValueUtil::structValue(output);
};
return json_format_map_callback(json_output_format_).struct_value();
}

void SubstitutionFormatParser::parseCommandHeader(const std::string& token, const size_t start,
Expand Down
14 changes: 11 additions & 3 deletions source/common/formatter/substitution_formatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,17 @@ class FormatterImpl : public Formatter {
std::vector<FormatterProviderPtr> providers_;
};

struct JsonFormatMap;
using JsonFormatMapValue =
absl::variant<const std::vector<FormatterProviderPtr>, const JsonFormatMap>;
struct JsonFormatMap {
std::unique_ptr<std::map<std::string, JsonFormatMapValue>> value_;
};
Comment thread
Pchelolo marked this conversation as resolved.
Outdated

class JsonFormatterImpl : public Formatter {
public:
JsonFormatterImpl(const absl::flat_hash_map<std::string, std::string>& format_mapping,
bool preserve_types);
JsonFormatterImpl(const ProtobufWkt::Struct& format_mapping, bool preserve_types)
: preserve_types_(preserve_types), json_output_format_(toFormatMap(format_mapping)) {}

// Formatter::format
std::string format(const Http::RequestHeaderMap& request_headers,
Expand All @@ -114,13 +121,14 @@ class JsonFormatterImpl : public Formatter {

private:
const bool preserve_types_;
std::map<const std::string, const std::vector<FormatterProviderPtr>> json_output_format_;
const JsonFormatMap json_output_format_;

ProtobufWkt::Struct toStruct(const Http::RequestHeaderMap& request_headers,
const Http::ResponseHeaderMap& response_headers,
const Http::ResponseTrailerMap& response_trailers,
const StreamInfo::StreamInfo& stream_info,
absl::string_view local_reply_body) const;
JsonFormatMap toFormatMap(const ProtobufWkt::Struct& json_format) const;
};

/**
Expand Down
21 changes: 11 additions & 10 deletions test/common/formatter/substitution_format_string_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class SubstitutionFormatStringUtilsTest : public ::testing::Test {
EXPECT_CALL(stream_info_, responseCode()).WillRepeatedly(Return(response_code));
}

Http::TestRequestHeaderMapImpl request_headers_{{":method", "GET"}, {":path", "/bar/foo"}};
Http::TestRequestHeaderMapImpl request_headers_{
{":method", "GET"}, {":path", "/bar/foo"}, {"content-type", "application/json"}};
Http::TestResponseHeaderMapImpl response_headers_;
Http::TestResponseTrailerMapImpl response_trailers_;
StreamInfo::MockStreamInfo stream_info_;
Expand Down Expand Up @@ -54,6 +55,8 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigJson) {
text: "plain text"
path: "%REQ(:path)%"
code: "%RESPONSE_CODE%"
headers:
content-type: "%REQ(CONTENT-TYPE)%"
)EOF";
TestUtility::loadFromYaml(yaml, config_);

Expand All @@ -64,7 +67,10 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigJson) {
const std::string expected = R"EOF({
"text": "plain text",
"path": "/bar/foo",
"code": 200
"code": 200,
"headers": {
"content-type": "application/json"
}
})EOF";
EXPECT_TRUE(TestUtility::jsonStringEqual(out_json, expected));
}
Expand All @@ -78,18 +84,13 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestInvalidConfigs) {
R"(
json_format:
field: 200
)",
R"(
json_format:
field:
nest_field: "value"
)",
};
for (const auto& yaml : invalid_configs) {
TestUtility::loadFromYaml(yaml, config_);
EXPECT_THROW_WITH_MESSAGE(SubstitutionFormatStringUtils::fromProtoConfig(config_),
EnvoyException,
"Only string values are supported in the JSON access log format.");
EXPECT_THROW_WITH_MESSAGE(
SubstitutionFormatStringUtils::fromProtoConfig(config_), EnvoyException,
"Only string/object values are supported in the JSON access log format.");
}
}

Expand Down
26 changes: 14 additions & 12 deletions test/common/formatter/substitution_formatter_speed_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ namespace Envoy {
namespace {

std::unique_ptr<Envoy::Formatter::JsonFormatterImpl> makeJsonFormatter(bool typed) {
absl::flat_hash_map<std::string, std::string> JsonLogFormat = {
{"remote_address", "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"},
{"start_time", "%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)%"},
{"method", "%REQ(:METHOD)%"},
{"url", "%REQ(X-FORWARDED-PROTO)%://%REQ(:AUTHORITY)%%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"},
{"protocol", "%PROTOCOL%"},
{"respoinse_code", "%RESPONSE_CODE%"},
{"bytes_sent", "%BYTES_SENT%"},
{"duration", "%DURATION%"},
{"referer", "%REQ(REFERER)%"},
{"user-agent", "%REQ(USER-AGENT)%"}};

ProtobufWkt::Struct JsonLogFormat;
const std::string format_yaml = R"EOF(
remote_address: '%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%'
start_time: '%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)%'
method: '%REQ(:METHOD)%'
url: '%REQ(X-FORWARDED-PROTO)%://%REQ(:AUTHORITY)%%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%'
protocol: '%PROTOCOL%'
respoinse_code: '%RESPONSE_CODE%'
bytes_sent: '%BYTES_SENT%'
duration: '%DURATION%'
referer: '%REQ(REFERER)%'
user-agent: '%REQ(USER-AGENT)%'
)EOF";
TestUtility::loadFromYaml(format_yaml, JsonLogFormat);
return std::make_unique<Envoy::Formatter::JsonFormatterImpl>(JsonLogFormat, typed);
}

Expand Down
Loading