Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for nested JSON format in json logging mode #12602

Merged
merged 7 commits into from
Aug 21, 2020
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...>;
zuercher marked this conversation as resolved.
Show resolved Hide 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.");
Pchelolo marked this conversation as resolved.
Show resolved Hide resolved
}
}
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_;
};
Pchelolo marked this conversation as resolved.
Show resolved Hide resolved

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