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

access log: add JSON logging mode #4693

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
bfe7744
Initial commit
aa-stripe Oct 9, 2018
031377f
Minor fixes, remove a unique_ptr
aa-stripe Oct 9, 2018
1c754a5
Run clang-format on source/common/access_log
aa-stripe Oct 9, 2018
f604562
Run clang-format on source/extensions/access_loggers/file
aa-stripe Oct 9, 2018
cbdc6dd
Run clang-format on test/common/access_log
aa-stripe Oct 9, 2018
8db57a7
Run clang-format on test/extensions/access_loggers/file and switch fr…
aa-stripe Oct 9, 2018
6af83d5
Run clang-format on include/envoy/access_log
aa-stripe Oct 10, 2018
bded5f2
Update exception message in config load
aa-stripe Oct 10, 2018
ffd3358
Switch to using ProtobufWkt
aa-stripe Oct 10, 2018
7c777d3
Fix some format and ProtobufWkt issues
aa-stripe Oct 11, 2018
ed1d0d8
Adopt RequestInfo -> StreamInfo in the JSON logging code
aa-stripe Oct 11, 2018
4c7e000
More format fixes
aa-stripe Oct 11, 2018
216814f
More format fixes
aa-stripe Oct 11, 2018
777bd96
More format fixes
aa-stripe Oct 11, 2018
9064213
More format fixes - header reordering
aa-stripe Oct 11, 2018
1d23323
Fix formatting issues
aa-stripe Oct 11, 2018
cc0090e
Add docs for JSON logging
aa-stripe Oct 11, 2018
ccaab7c
Format fixes in file.proto
aa-stripe Oct 11, 2018
234a683
Update comments in access_log.h
aa-stripe Oct 12, 2018
9f78381
Switch to unordered_map
aa-stripe Oct 12, 2018
f2b1173
Case change convertJsonFormatToMap
aa-stripe Oct 12, 2018
633365a
Describe some test cases
aa-stripe Oct 12, 2018
d700a64
Remove new usage from config_test
aa-stripe Oct 12, 2018
b8a19d8
Use protobuf to serialize json output, instead of rapidjson
aa-stripe Oct 12, 2018
6482235
Check error value after serializing to JSON
aa-stripe Oct 12, 2018
53e066b
Use ProtobufWkt
aa-stripe Oct 12, 2018
195c1be
Formatting fix
aa-stripe Oct 12, 2018
7dfd316
Rename to_map to toMap
aa-stripe Oct 12, 2018
9d0ab16
Formatting fixes
aa-stripe Oct 12, 2018
6dc6869
Formatting fixes
aa-stripe Oct 12, 2018
258a03d
Formatting fixes
aa-stripe Oct 12, 2018
76bc028
Formatting fixes
aa-stripe Oct 12, 2018
7f10f1a
Kick CI
aa-stripe Oct 12, 2018
a7d514c
Merge remote-tracking branch 'upstream/master' into aa-stripe/2692-js…
aa-stripe Oct 15, 2018
034dffc
Add doxygen comments and rename a param
aa-stripe Oct 15, 2018
ee3f1dc
Fix spelling
aa-stripe Oct 15, 2018
bc79d68
Merge remote-tracking branch 'upstream/master' into aa-stripe/2692-js…
aa-stripe Oct 15, 2018
6273f4b
Fix some wording
aa-stripe Oct 16, 2018
588a28f
Make toMap private and compare parsed JSON in unit tests
aa-stripe Oct 16, 2018
85e05f9
Log an error instead of throwing an exception on proto -> json conver…
aa-stripe Oct 16, 2018
a3e4410
Switch to unordered_map for config.cc
aa-stripe Oct 17, 2018
91b1b80
Remove single token restriction
aa-stripe Oct 17, 2018
0bf86f8
Remove wording about one token limitation
aa-stripe Oct 17, 2018
5aa43e2
Formatting
aa-stripe Oct 17, 2018
d8a51e9
Formatting
aa-stripe Oct 17, 2018
b87f2a4
Formatting
aa-stripe Oct 17, 2018
6ef6456
use const auto
aa-stripe Oct 17, 2018
1ffc475
Formatting
aa-stripe Oct 17, 2018
e92a886
Break up tests and remove this->
aa-stripe Oct 18, 2018
252df75
Break up config tests into separate test cases
aa-stripe Oct 18, 2018
a060f50
Remove std::string conversion
aa-stripe Oct 18, 2018
e7ca4a7
Refactor json map assertions into a helper
aa-stripe Oct 18, 2018
d0221c8
Formatting fixes
aa-stripe Oct 18, 2018
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
9 changes: 8 additions & 1 deletion api/envoy/config/accesslog/v2/file.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package envoy.config.accesslog.v2;
option go_package = "v2";

import "validate/validate.proto";
import "google/protobuf/struct.proto";

// [#protodoc-title: File access log]

Expand All @@ -17,5 +18,11 @@ message FileAccessLog {
// Access log format. Envoy supports :ref:`custom access log formats
// <config_access_log_format>` as well as a :ref:`default format
// <config_access_log_default_format>`.
string format = 2;
oneof access_log_format {
// Access log :ref:`format string<config_access_log_format_strings>`
string format = 2;

// Access log :ref:`format dictionary<config_access_log_format_dictionaries>`
google.protobuf.Struct json_format = 3;
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a comment describing how this is interpreted? E.g. can we use the same custom access log formats in leaf strings, limitations on depth of nesting, etc.

Copy link
Contributor Author

@aa-stripe aa-stripe Oct 11, 2018

Choose a reason for hiding this comment

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

Yup, this will be coming soon with the docs updates.

  • protobuf comment

}
}
104 changes: 79 additions & 25 deletions docs/root/configuration/access_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,91 @@ Access logs are configured as part of the :ref:`HTTP connection manager config
Format rules
------------

The access log format string contains either command operators or other characters interpreted as a
plain string. The access log formatter does not make any assumptions about a new line separator, so one
Access log formats contain command operators that extract the relevant data and insert it.
They support two formats: :ref:`"format strings" <config_access_log_format_strings>` and
:ref:`"format dictionaries" <config_access_log_format_dictionaries>`. In both cases, the command operators
are used to extract the relevant data, which is then inserted into the specified log format.
Only one access log format may be specified at a time.

.. _config_access_log_format_strings:

Format Strings
--------------

Format strings are plain strings, specified using the ``format`` key. They may contain
either command operators or other characters interpreted as a plain string.
The access log formatter does not make any assumptions about a new line separator, so one
has to specified as part of the format string.
See the :ref:`default format <config_access_log_default_format>` for an example.
Note that the access log line will contain a '-' character for every not set/empty value.

The same format strings are used by different types of access logs (such as HTTP and TCP). Some
.. _config_access_log_default_format:

Default Format String
---------------------

If custom format string is not specified, Envoy uses the following default format:

.. code-block:: none

[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"
%RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION%
%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%"
"%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n

Example of the default Envoy access log format:

.. code-block:: none

[2016-04-15T20:17:00.310Z] "POST /api/v1/locations HTTP/2" 204 - 154 0 226 100 "10.0.35.28"
"nsq2http" "cc21d9b0-cf5c-432b-8c7e-98aeb7988cd2" "locations" "tcp://10.0.2.1:80"

.. _config_access_log_format_dictionaries:

Format Dictionaries
-------------------

Format dictionaries are dictionaries that specify a structured access log output format,
specified using the ``json_format`` key. This allows logs to be output in a structured format
such as JSON.
Similar to format strings, command operators are evaluated and their values inserted into the format
dictionary to construct the log output.

For example, with the following format provided in the configuration:

.. code-block:: json

{
"config": {
"json_format": {
"protocol": "%PROTOCOL%",
"duration": "%DURATION%",
"my_custom_header": "%REQ(MY_CUSTOM_HEADER)%"
}
}
}

The following JSON object would be written to the log file:

.. code-block:: json

{"protocol": "HTTP/1.1", "duration": "123", "my_custom_header": "value_of_MY_CUSTOM_HEADER"}

This allows you to specify a custom key for each command operator.

Format dictionaries have the following restrictions:

* The dictionary must map strings to strings (specifically, strings to command operators). Nesting is not currently supported.

Command Operators
Copy link
Member

Choose a reason for hiding this comment

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

FWIW, I think "command operator" is not the right terminology, but I see it was existing before this PR. I probably would have used "format expressions" or the like.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll see if it's pretty straightforward to change this. I personally prefer "format specifiers" since it lines up with the C terminology

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left this as is for now

Copy link
Member

Choose a reason for hiding this comment

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

Sure, this was more just a non-actionable observation.

-----------------

Command operators are used to extract values that will be inserted into the access logs.
The same operators are used by different types of access logs (such as HTTP and TCP). Some
fields may have slightly different meanings, depending on what type of log it is. Differences
are noted.

Note that if a value is not set/empty, the logs will contain a '-' character.

The following command operators are supported:

.. _config_access_log_format_start_time:
Expand Down Expand Up @@ -232,24 +307,3 @@ The following command operators are supported:
String value set on ssl connection socket for Server Name Indication (SNI)
TCP
String value set on ssl connection socket for Server Name Indication (SNI)

.. _config_access_log_default_format:

Default format
--------------

If custom format is not specified, Envoy uses the following default format:

.. code-block:: none

[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%"
%RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION%
%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%"
"%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%"\n

Example of the default Envoy access log format:

.. code-block:: none

[2016-04-15T20:17:00.310Z] "POST /api/v1/locations HTTP/2" 204 - 154 0 226 100 "10.0.35.28"
"nsq2http" "cc21d9b0-cf5c-432b-8c7e-98aeb7988cd2" "locations" "tcp://10.0.2.1:80"
33 changes: 33 additions & 0 deletions include/envoy/access_log/access_log.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,20 @@ typedef std::shared_ptr<Instance> InstanceSharedPtr;

/**
* Interface for access log formatter.
* Formatters provide a complete access log output line for the given headers/trailers/stream.
*/
class Formatter {
public:
virtual ~Formatter() {}

/**
* Return a formatted access log line.
* @param request_headers supplies the request headers.
* @param response_headers supplies the response headers.
* @param response_trailers supplies the response trailers.
* @param stream_info supplies the stream info.
* @return std::string string containing the complete formatted access log line.
*/
virtual std::string format(const Http::HeaderMap& request_headers,
const Http::HeaderMap& response_headers,
const Http::HeaderMap& response_trailers,
Expand All @@ -84,5 +93,29 @@ class Formatter {

typedef std::unique_ptr<Formatter> FormatterPtr;

/**
* Interface for access log provider.
* FormatterProviders extract information from the given headers/trailers/stream.
*/
class FormatterProvider {
public:
virtual ~FormatterProvider() {}

/**
* Extract a value from the provided headers/trailers/stream.
* @param request_headers supplies the request headers.
* @param response_headers supplies the response headers.
* @param response_trailers supplies the response trailers.
* @param stream_info supplies the stream info.
* @return std::string containing a single value extracted from the given headers/trailers/stream.
*/
virtual std::string format(const Http::HeaderMap& request_headers,
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a Doxygen comment here with args/return explanation?

Copy link
Contributor Author

@aa-stripe aa-stripe Oct 11, 2018

Choose a reason for hiding this comment

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

Agreed

  • doxygen comment on FormatterProvider

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Felt like it was redundant to call out the args, so just added an explanation and then an @return

Copy link
Member

Choose a reason for hiding this comment

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

Envoy include/envoy APIs do have a verbose/redundant format, the best way to think of them is as official documentation that could ship without even the code (even though nobody uses them that way today, this is the style..).

const Http::HeaderMap& response_headers,
const Http::HeaderMap& response_trailers,
const StreamInfo::StreamInfo& stream_info) const PURE;
};

typedef std::unique_ptr<FormatterProvider> FormatterProviderPtr;

} // namespace AccessLog
} // namespace Envoy
75 changes: 58 additions & 17 deletions source/common/access_log/access_log_formatter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ AccessLogFormatUtils::protocolToString(const absl::optional<Http::Protocol>& pro
}

FormatterImpl::FormatterImpl(const std::string& format) {
formatters_ = AccessLogFormatParser::parse(format);
providers_ = AccessLogFormatParser::parse(format);
}

std::string FormatterImpl::format(const Http::HeaderMap& request_headers,
Expand All @@ -64,14 +64,54 @@ std::string FormatterImpl::format(const Http::HeaderMap& request_headers,
std::string log_line;
log_line.reserve(256);

for (const FormatterPtr& formatter : formatters_) {
log_line +=
formatter->format(request_headers, response_headers, response_trailers, stream_info);
for (const FormatterProviderPtr& provider : providers_) {
log_line += provider->format(request_headers, response_headers, response_trailers, stream_info);
}

return log_line;
}

JsonFormatterImpl::JsonFormatterImpl(std::unordered_map<std::string, std::string>& format_mapping) {
for (const auto& pair : format_mapping) {
auto providers = AccessLogFormatParser::parse(pair.second);
json_output_format_.emplace(pair.first, FormatterPtr{new FormatterImpl(pair.second)});
}
}

std::string JsonFormatterImpl::format(const Http::HeaderMap& request_headers,
const Http::HeaderMap& response_headers,
const Http::HeaderMap& response_trailers,
const StreamInfo::StreamInfo& stream_info) const {
const auto output_map = toMap(request_headers, response_headers, response_trailers, stream_info);

ProtobufWkt::Struct output_struct;
for (const auto& pair : output_map) {
ProtobufWkt::Value string_value;
string_value.set_string_value(pair.second);
(*output_struct.mutable_fields())[pair.first] = string_value;
}

std::string log_line;
const auto conversion_status = ProtobufUtil::MessageToJsonString(output_struct, &log_line);
if (!conversion_status.ok()) {
log_line =
fmt::format("Error serializing access log to JSON: {}", conversion_status.ToString());
}

return log_line;
}

std::unordered_map<std::string, std::string> JsonFormatterImpl::toMap(
const Http::HeaderMap& request_headers, const Http::HeaderMap& response_headers,
const Http::HeaderMap& response_trailers, const StreamInfo::StreamInfo& stream_info) const {
std::unordered_map<std::string, std::string> output;
for (const auto& pair : json_output_format_) {
output.emplace(pair.first, pair.second->format(request_headers, response_headers,
response_trailers, stream_info));
}
return output;
}

void AccessLogFormatParser::parseCommandHeader(const std::string& token, const size_t start,
std::string& main_header,
std::string& alternative_header,
Expand Down Expand Up @@ -131,16 +171,16 @@ void AccessLogFormatParser::parseCommand(const std::string& token, const size_t
}

// TODO(derekargueta): #2967 - Rewrite AccessLogformatter with parser library & formal grammar
std::vector<FormatterPtr> AccessLogFormatParser::parse(const std::string& format) {
std::vector<FormatterProviderPtr> AccessLogFormatParser::parse(const std::string& format) {
std::string current_token;
std::vector<FormatterPtr> formatters;
std::vector<FormatterProviderPtr> formatters;
const std::string DYNAMIC_META_TOKEN = "DYNAMIC_METADATA(";
const std::regex command_w_args_regex(R"EOF(%([A-Z]|_)+(\([^\)]*\))?(:[0-9]+)?(%))EOF");

for (size_t pos = 0; pos < format.length(); ++pos) {
if (format[pos] == '%') {
if (!current_token.empty()) {
formatters.emplace_back(new PlainStringFormatter(current_token));
formatters.emplace_back(FormatterProviderPtr{new PlainStringFormatter(current_token)});
current_token = "";
}

Expand All @@ -163,42 +203,43 @@ std::vector<FormatterPtr> AccessLogFormatParser::parse(const std::string& format

parseCommandHeader(token, ReqParamStart, main_header, alternative_header, max_length);

formatters.emplace_back(
new RequestHeaderFormatter(main_header, alternative_header, max_length));
formatters.emplace_back(FormatterProviderPtr{
new RequestHeaderFormatter(main_header, alternative_header, max_length)});
} else if (token.find("RESP(") == 0) {
std::string main_header, alternative_header;
absl::optional<size_t> max_length;

parseCommandHeader(token, RespParamStart, main_header, alternative_header, max_length);

formatters.emplace_back(
new ResponseHeaderFormatter(main_header, alternative_header, max_length));
formatters.emplace_back(FormatterProviderPtr{
new ResponseHeaderFormatter(main_header, alternative_header, max_length)});
} else if (token.find("TRAILER(") == 0) {
std::string main_header, alternative_header;
absl::optional<size_t> max_length;

parseCommandHeader(token, TrailParamStart, main_header, alternative_header, max_length);

formatters.emplace_back(
new ResponseTrailerFormatter(main_header, alternative_header, max_length));
formatters.emplace_back(FormatterProviderPtr{
new ResponseTrailerFormatter(main_header, alternative_header, max_length)});
} else if (token.find(DYNAMIC_META_TOKEN) == 0) {
std::string filter_namespace;
absl::optional<size_t> max_length;
std::vector<std::string> path;
const size_t start = DYNAMIC_META_TOKEN.size();

parseCommand(token, start, ":", filter_namespace, path, max_length);
formatters.emplace_back(new DynamicMetadataFormatter(filter_namespace, path, max_length));
formatters.emplace_back(
FormatterProviderPtr{new DynamicMetadataFormatter(filter_namespace, path, max_length)});
} else if (token.find("START_TIME") == 0) {
const size_t parameters_length = pos + StartTimeParamStart + 1;
const size_t parameters_end = command_end_position - parameters_length;

const std::string args = token[StartTimeParamStart - 1] == '('
? token.substr(StartTimeParamStart, parameters_end)
: "";
formatters.emplace_back(new StartTimeFormatter(args));
formatters.emplace_back(FormatterProviderPtr{new StartTimeFormatter(args)});
} else {
formatters.emplace_back(new StreamInfoFormatter(token));
formatters.emplace_back(FormatterProviderPtr{new StreamInfoFormatter(token)});
}
pos = command_end_position;
} else {
Expand All @@ -207,7 +248,7 @@ std::vector<FormatterPtr> AccessLogFormatParser::parse(const std::string& format
}

if (!current_token.empty()) {
formatters.emplace_back(new PlainStringFormatter(current_token));
formatters.emplace_back(FormatterProviderPtr{new PlainStringFormatter(current_token)});
}

return formatters;
Expand Down
Loading