From b9cd95290cefb78e50ca26d2e68c4cd2dc84cb7f Mon Sep 17 00:00:00 2001 From: Yaroslav Skopets Date: Fri, 21 Feb 2020 01:22:19 +0100 Subject: [PATCH] WIP: add support for canonical representation --- pkg/envoy/accesslog/commands.go | 6 +- pkg/envoy/accesslog/composite_formatter.go | 15 +- .../accesslog/dynamic_metadata_formatter.go | 25 ++- .../dynamic_metadata_formatter_test.go | 81 ++++++++ pkg/envoy/accesslog/field_formatter.go | 7 +- pkg/envoy/accesslog/field_formatter_test.go | 38 +++- pkg/envoy/accesslog/filter_state_formatter.go | 19 +- .../accesslog/filter_state_formatter_test.go | 45 +++++ pkg/envoy/accesslog/format_parser.go | 8 +- pkg/envoy/accesslog/format_parser_test.go | 188 ++++++++++++++---- pkg/envoy/accesslog/header_formatter.go | 25 +++ pkg/envoy/accesslog/interfaces.go | 9 + .../accesslog/request_header_formatter.go | 7 + .../request_header_formatter_test.go | 59 ++++++ .../accesslog/response_header_formatter.go | 7 + .../response_header_formatter_test.go | 59 ++++++ .../accesslog/response_trailer_formatter.go | 7 + .../response_trailer_formatter_test.go | 59 ++++++ pkg/envoy/accesslog/start_time_formatter.go | 10 + .../accesslog/start_time_formatter_test.go | 32 +++ pkg/envoy/accesslog/text_literal_formatter.go | 5 + .../accesslog/text_literal_formatter_test.go | 78 ++++++++ 22 files changed, 735 insertions(+), 54 deletions(-) create mode 100644 pkg/envoy/accesslog/dynamic_metadata_formatter_test.go create mode 100644 pkg/envoy/accesslog/filter_state_formatter_test.go create mode 100644 pkg/envoy/accesslog/text_literal_formatter_test.go diff --git a/pkg/envoy/accesslog/commands.go b/pkg/envoy/accesslog/commands.go index 15526dca8895..bf8c1eb1ec14 100644 --- a/pkg/envoy/accesslog/commands.go +++ b/pkg/envoy/accesslog/commands.go @@ -63,14 +63,14 @@ const ( CMD_HOSTNAME = "HOSTNAME" ) -// CommandOperatorName represents a reference name of an Envoy access log command operator. +// CommandOperatorDescriptor represents a descriptor of an Envoy access log command operator. // // See https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log#command-operators -type CommandOperatorName string +type CommandOperatorDescriptor string // String returns the reference name of an Envoy access log command operator // as it appears on https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log#command-operators -func (o CommandOperatorName) String() string { +func (o CommandOperatorDescriptor) String() string { switch string(o) { case CMD_REQ: return "%REQ(X?Y):Z%" diff --git a/pkg/envoy/accesslog/composite_formatter.go b/pkg/envoy/accesslog/composite_formatter.go index bb5a8f5b85a6..602106a6953d 100644 --- a/pkg/envoy/accesslog/composite_formatter.go +++ b/pkg/envoy/accesslog/composite_formatter.go @@ -8,7 +8,7 @@ import ( ) const ( - unspecifiedValue = "-" // to replicate Envoy behaviour + unspecifiedValue = "-" // to replicate Envoy's behaviour ) // CompositeLogConfigureFormatter represents the entire access log format string. @@ -46,7 +46,7 @@ func (c *CompositeLogConfigureFormatter) FormatHttpLogEntry(entry *accesslog_dat return "", err } if value == "" { - value = unspecifiedValue // to replicate Envoy behaviour + value = unspecifiedValue // to replicate Envoy's behaviour } values[i] = value } @@ -61,9 +61,18 @@ func (c *CompositeLogConfigureFormatter) FormatTcpLogEntry(entry *accesslog_data return "", err } if value == "" { - value = unspecifiedValue // to replicate Envoy behaviour + value = unspecifiedValue // to replicate Envoy's behaviour } values[i] = value } return strings.Join(values, ""), nil } + +// String returns the canonical representation of this format string. +func (f *CompositeLogConfigureFormatter) String() string { + fragments := make([]string, len(f.Formatters)) + for i, formatter := range f.Formatters { + fragments[i] = formatter.String() + } + return strings.Join(fragments, "") +} diff --git a/pkg/envoy/accesslog/dynamic_metadata_formatter.go b/pkg/envoy/accesslog/dynamic_metadata_formatter.go index 64a29d312b7e..36e1d33bff4f 100644 --- a/pkg/envoy/accesslog/dynamic_metadata_formatter.go +++ b/pkg/envoy/accesslog/dynamic_metadata_formatter.go @@ -1,6 +1,9 @@ package accesslog import ( + "strconv" + "strings" + accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" ) @@ -21,5 +24,25 @@ func (f *DynamicMetadataFormatter) FormatTcpLogEntry(entry *accesslog_data.TCPAc func (f *DynamicMetadataFormatter) format(entry *accesslog_data.AccessLogCommon) (string, error) { // TODO(yskopets): implement - return "UNSUPPORTED(DYNAMIC_METADATA)", nil + return "UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)", nil +} + +// String returns the canonical representation of this command operator. +func (f *DynamicMetadataFormatter) String() string { + var builder []string + builder = append(builder, "%DYNAMIC_METADATA(") + builder = append(builder, f.FilterNamespace) + if len(f.Path) > 0 { + for _, segment := range f.Path { + builder = append(builder, ":") + builder = append(builder, segment) + } + } + builder = append(builder, ")") + if f.MaxLength != 0 { + builder = append(builder, ":") + builder = append(builder, strconv.FormatInt(int64(f.MaxLength), 10)) + } + builder = append(builder, "%") + return strings.Join(builder, "") } diff --git a/pkg/envoy/accesslog/dynamic_metadata_formatter_test.go b/pkg/envoy/accesslog/dynamic_metadata_formatter_test.go new file mode 100644 index 000000000000..4fff662f9e2d --- /dev/null +++ b/pkg/envoy/accesslog/dynamic_metadata_formatter_test.go @@ -0,0 +1,81 @@ +package accesslog_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + . "github.com/Kong/kuma/pkg/envoy/accesslog" +) + +var _ = Describe("DynamicMetadataFormatter", func() { + + Describe("String()", func() { + type testCase struct { + filterNamespace string + path []string + maxLength int + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := &DynamicMetadataFormatter{FilterNamespace: given.filterNamespace, Path: given.path, MaxLength: given.maxLength} + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("%DYNAMIC_METADATA()%", testCase{ + expected: `%DYNAMIC_METADATA()%`, + }), + Entry("%DYNAMIC_METADATA():10%", testCase{ + maxLength: 10, + expected: `%DYNAMIC_METADATA():10%`, + }), + Entry("%DYNAMIC_METADATA(com.test.my_filter)%", testCase{ + filterNamespace: "com.test.my_filter", + expected: `%DYNAMIC_METADATA(com.test.my_filter)%`, + }), + Entry("%DYNAMIC_METADATA(com.test.my_filter):10%", testCase{ + filterNamespace: "com.test.my_filter", + maxLength: 10, + expected: `%DYNAMIC_METADATA(com.test.my_filter):10%`, + }), + Entry("%DYNAMIC_METADATA(com.test.my_filter:test_object)%", testCase{ + filterNamespace: "com.test.my_filter", + path: []string{"test_object"}, + expected: `%DYNAMIC_METADATA(com.test.my_filter:test_object)%`, + }), + Entry("%DYNAMIC_METADATA(com.test.my_filter:test_object):10%", testCase{ + filterNamespace: "com.test.my_filter", + path: []string{"test_object"}, + maxLength: 10, + expected: `%DYNAMIC_METADATA(com.test.my_filter:test_object):10%`, + }), + Entry("%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key)%", testCase{ + filterNamespace: "com.test.my_filter", + path: []string{"test_object", "inner_key"}, + expected: `%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key)%`, + }), + Entry("%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key):10%", testCase{ + filterNamespace: "com.test.my_filter", + path: []string{"test_object", "inner_key"}, + maxLength: 10, + expected: `%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key):10%`, + }), + Entry("%DYNAMIC_METADATA(:test_object:inner_key)%", testCase{ + path: []string{"test_object", "inner_key"}, + expected: `%DYNAMIC_METADATA(:test_object:inner_key)%`, + }), + Entry("%DYNAMIC_METADATA(:test_object:inner_key):10%", testCase{ + path: []string{"test_object", "inner_key"}, + maxLength: 10, + expected: `%DYNAMIC_METADATA(:test_object:inner_key):10%`, + }), + ) + }) +}) diff --git a/pkg/envoy/accesslog/field_formatter.go b/pkg/envoy/accesslog/field_formatter.go index f5a58dcec92c..bf5d6c54c096 100644 --- a/pkg/envoy/accesslog/field_formatter.go +++ b/pkg/envoy/accesslog/field_formatter.go @@ -25,6 +25,11 @@ const ( // such as `%BYTES_RECEIVED%` or `%PROTOCOL%`. type FieldFormatter string +// String returns the canonical representation of this command operator. +func (f FieldFormatter) String() string { + return CommandOperatorDescriptor(f).String() +} + func (f FieldFormatter) FormatHttpLogEntry(entry *accesslog_data.HTTPAccessLogEntry) (string, error) { switch f { case CMD_BYTES_RECEIVED: @@ -125,7 +130,7 @@ func (f FieldFormatter) formatAccessLogCommon(entry *accesslog_data.AccessLogCom fallthrough // these fields have no equivalent data in GrpcAccessLog default: // make it clear to the user what is happening - return fmt.Sprintf("UNSUPPORTED_FIELD(%s)", f), nil + return fmt.Sprintf("UNSUPPORTED_COMMAND(%s)", f), nil } } diff --git a/pkg/envoy/accesslog/field_formatter_test.go b/pkg/envoy/accesslog/field_formatter_test.go index 76ed2d6c6898..70c465ae77d0 100644 --- a/pkg/envoy/accesslog/field_formatter_test.go +++ b/pkg/envoy/accesslog/field_formatter_test.go @@ -856,31 +856,55 @@ var _ = Describe("FieldFormatter", func() { }), Entry("DOWNSTREAM_PEER_FINGERPRINT_256", testCase{ field: "DOWNSTREAM_PEER_FINGERPRINT_256", - expected: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_FINGERPRINT_256)`, + expected: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_FINGERPRINT_256%)`, }), Entry("DOWNSTREAM_PEER_SERIAL", testCase{ field: "DOWNSTREAM_PEER_SERIAL", - expected: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_SERIAL)`, + expected: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_SERIAL%)`, }), Entry("DOWNSTREAM_PEER_ISSUER", testCase{ field: "DOWNSTREAM_PEER_ISSUER", - expected: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_ISSUER)`, + expected: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_ISSUER%)`, }), Entry("DOWNSTREAM_PEER_CERT", testCase{ field: "DOWNSTREAM_PEER_CERT", - expected: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT)`, + expected: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT%)`, }), Entry("DOWNSTREAM_PEER_CERT_V_START", testCase{ field: "DOWNSTREAM_PEER_CERT_V_START", - expected: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT_V_START)`, + expected: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT_V_START%)`, }), Entry("DOWNSTREAM_PEER_CERT_V_END", testCase{ field: "DOWNSTREAM_PEER_CERT_V_END", - expected: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT_V_END)`, + expected: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT_V_END%)`, }), Entry("HOSTNAME", testCase{ field: "HOSTNAME", - expected: `UNSUPPORTED_FIELD(HOSTNAME)`, + expected: `UNSUPPORTED_COMMAND(%HOSTNAME%)`, + }), + ) + }) + + Describe("String()", func() { + type testCase struct { + field string + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := FieldFormatter(given.field) + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("%BYTES_RECEIVED%", testCase{ + field: "BYTES_RECEIVED", + expected: `%BYTES_RECEIVED%`, }), ) }) diff --git a/pkg/envoy/accesslog/filter_state_formatter.go b/pkg/envoy/accesslog/filter_state_formatter.go index 6032cf2ee460..c19363d69170 100644 --- a/pkg/envoy/accesslog/filter_state_formatter.go +++ b/pkg/envoy/accesslog/filter_state_formatter.go @@ -1,6 +1,9 @@ package accesslog import ( + "strconv" + "strings" + accesslog_config "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v2" accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" ) @@ -21,7 +24,7 @@ func (f *FilterStateFormatter) FormatTcpLogEntry(entry *accesslog_data.TCPAccess func (f *FilterStateFormatter) format(entry *accesslog_data.AccessLogCommon) (string, error) { // TODO(yskopets): implement - return "UNSUPPORTED(FILTER_STATE)", nil + return "UNSUPPORTED_COMMAND(%FILTER_STATE(KEY):Z%)", nil } func (f *FilterStateFormatter) ConfigureHttpLog(config *accesslog_config.HttpGrpcAccessLogConfig) error { @@ -50,3 +53,17 @@ func (f *FilterStateFormatter) appendTo(values []string) []string { } return values } + +// String returns the canonical representation of this command operator. +func (f *FilterStateFormatter) String() string { + var builder []string + builder = append(builder, "%FILTER_STATE(") + builder = append(builder, f.Key) + builder = append(builder, ")") + if f.MaxLength != 0 { + builder = append(builder, ":") + builder = append(builder, strconv.FormatInt(int64(f.MaxLength), 10)) + } + builder = append(builder, "%") + return strings.Join(builder, "") +} diff --git a/pkg/envoy/accesslog/filter_state_formatter_test.go b/pkg/envoy/accesslog/filter_state_formatter_test.go new file mode 100644 index 000000000000..1ced8f2aa77c --- /dev/null +++ b/pkg/envoy/accesslog/filter_state_formatter_test.go @@ -0,0 +1,45 @@ +package accesslog_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + . "github.com/Kong/kuma/pkg/envoy/accesslog" +) + +var _ = Describe("FilterStateFormatter", func() { + + Describe("String()", func() { + type testCase struct { + key string + maxLength int + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := &FilterStateFormatter{Key: given.key, MaxLength: given.maxLength} + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("%FILTER_STATE()%", testCase{ + expected: `%FILTER_STATE()%`, + }), + Entry("%FILTER_STATE(filter.state.key)%", testCase{ + key: "filter.state.key", + expected: `%FILTER_STATE(filter.state.key)%`, + }), + Entry("%FILTER_STATE(filter.state.key):10%", testCase{ + key: "filter.state.key", + maxLength: 10, + expected: `%FILTER_STATE(filter.state.key):10%`, + }), + ) + }) +}) diff --git a/pkg/envoy/accesslog/format_parser.go b/pkg/envoy/accesslog/format_parser.go index adac883f01de..79afafaa5868 100644 --- a/pkg/envoy/accesslog/format_parser.go +++ b/pkg/envoy/accesslog/format_parser.go @@ -133,7 +133,7 @@ func (p formatParser) parseCommandOperator(token, command, args, limit string) ( func (p formatParser) parseHeaderOperator(token, command, args, limit string) (header string, altHeader string, maxLen int, err error) { if p.hasNoArguments(token, command, args, limit) { - return "", "", 0, errors.Errorf(`command %q requires a header and optional alternative header names as its arguments, instead got %q`, CommandOperatorName(command), token) + return "", "", 0, errors.Errorf(`command %q requires a header and optional alternative header names as its arguments, instead got %q`, CommandOperatorDescriptor(command), token) } header, altHeaders, maxLen, err := p.parseOperator(token, args, limit, "?") if err != nil { @@ -159,7 +159,7 @@ func (p formatParser) parseDynamicMetadataOperator(token, command, args, limit s return "", nil, 0, err } if p.hasNoArguments(token, command, args, limit) { - return "", nil, 0, errors.Errorf(`command %q requires a filter namespace and optional path as its arguments, instead got %q`, CommandOperatorName(command), token) + return "", nil, 0, errors.Errorf(`command %q requires a filter namespace and optional path as its arguments, instead got %q`, CommandOperatorDescriptor(command), token) } return namespace, path, maxLen, err } @@ -170,7 +170,7 @@ func (p formatParser) parseFilterStateOperator(token, command, args, limit strin return "", 0, err } if p.hasNoArguments(token, command, args, limit) || key == "" { - return "", 0, errors.Errorf(`command %q requires a key as its argument, instead got %q`, CommandOperatorName(command), token) + return "", 0, errors.Errorf(`command %q requires a key as its argument, instead got %q`, CommandOperatorDescriptor(command), token) } return key, maxLen, nil } @@ -186,7 +186,7 @@ func (p formatParser) parseStartTimeOperator(token, command, args, limit string) func (p formatParser) parseFieldOperator(token, command, args, limit string) (field string, err error) { if token[1:len(token)-1] != command { - return "", errors.Errorf(`command %q doesn't support arguments or max length constraint, instead got %q`, CommandOperatorName(command), token) + return "", errors.Errorf(`command %q doesn't support arguments or max length constraint, instead got %q`, CommandOperatorDescriptor(command), token) } return command, nil } diff --git a/pkg/envoy/accesslog/format_parser_test.go b/pkg/envoy/accesslog/format_parser_test.go index 84e8230d4ce5..adaac56efccd 100644 --- a/pkg/envoy/accesslog/format_parser_test.go +++ b/pkg/envoy/accesslog/format_parser_test.go @@ -367,53 +367,53 @@ var _ = Describe("ParseFormat()", func() { }), Entry("%DYNAMIC_METADATA()%", testCase{ // apparently, Envoy allows both `FilterNamespace` and `Path` to be empty format: `%DYNAMIC_METADATA()%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%DYNAMIC_METADATA():10%", testCase{ // apparently, Envoy allows both `FilterNamespace` and `Path` to be empty format: `%DYNAMIC_METADATA():10%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%DYNAMIC_METADATA(com.test.my_filter)%", testCase{ format: `%DYNAMIC_METADATA(com.test.my_filter)%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%DYNAMIC_METADATA(com.test.my_filter):10%", testCase{ format: `%DYNAMIC_METADATA(com.test.my_filter):10%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%DYNAMIC_METADATA(com.test.my_filter:test_key)%", testCase{ format: `%DYNAMIC_METADATA(com.test.my_filter:test_key)%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%DYNAMIC_METADATA(com.test.my_filter:test_key):10%", testCase{ format: `%DYNAMIC_METADATA(com.test.my_filter:test_key):10%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key)%", testCase{ format: `%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key)%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key):10%", testCase{ format: `%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key):10%`, - expectedHTTP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet - expectedTCP: `UNSUPPORTED(DYNAMIC_METADATA)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%DYNAMIC_METADATA(NAMESPACE:KEY*):Z%)`, // not supported yet }), Entry("%FILTER_STATE(key)%", testCase{ format: `%FILTER_STATE(key)%`, - expectedHTTP: `UNSUPPORTED(FILTER_STATE)`, // not supported yet - expectedTCP: `UNSUPPORTED(FILTER_STATE)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%FILTER_STATE(KEY):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%FILTER_STATE(KEY):Z%)`, // not supported yet }), Entry("%FILTER_STATE(key):10%", testCase{ format: `%FILTER_STATE(key):10%`, - expectedHTTP: `UNSUPPORTED(FILTER_STATE)`, // not supported yet - expectedTCP: `UNSUPPORTED(FILTER_STATE)`, // not supported yet + expectedHTTP: `UNSUPPORTED_COMMAND(%FILTER_STATE(KEY):Z%)`, // not supported yet + expectedTCP: `UNSUPPORTED_COMMAND(%FILTER_STATE(KEY):Z%)`, // not supported yet }), Entry("%UPSTREAM_HOST%", testCase{ format: `%UPSTREAM_HOST%`, @@ -507,38 +507,38 @@ var _ = Describe("ParseFormat()", func() { }), Entry("%DOWNSTREAM_PEER_FINGERPRINT_256%", testCase{ format: `%DOWNSTREAM_PEER_FINGERPRINT_256%`, - expectedHTTP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_FINGERPRINT_256)`, - expectedTCP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_FINGERPRINT_256)`, + expectedHTTP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_FINGERPRINT_256%)`, + expectedTCP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_FINGERPRINT_256%)`, }), Entry("%DOWNSTREAM_PEER_SERIAL%", testCase{ format: `%DOWNSTREAM_PEER_SERIAL%`, - expectedHTTP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_SERIAL)`, - expectedTCP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_SERIAL)`, + expectedHTTP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_SERIAL%)`, + expectedTCP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_SERIAL%)`, }), Entry("%DOWNSTREAM_PEER_ISSUER%", testCase{ format: `%DOWNSTREAM_PEER_ISSUER%`, - expectedHTTP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_ISSUER)`, - expectedTCP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_ISSUER)`, + expectedHTTP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_ISSUER%)`, + expectedTCP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_ISSUER%)`, }), Entry("%DOWNSTREAM_PEER_CERT%", testCase{ format: `%DOWNSTREAM_PEER_CERT%`, - expectedHTTP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT)`, - expectedTCP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT)`, + expectedHTTP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT%)`, + expectedTCP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT%)`, }), Entry("%DOWNSTREAM_PEER_CERT_V_START%", testCase{ format: `%DOWNSTREAM_PEER_CERT_V_START%`, - expectedHTTP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT_V_START)`, - expectedTCP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT_V_START)`, + expectedHTTP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT_V_START%)`, + expectedTCP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT_V_START%)`, }), Entry("%DOWNSTREAM_PEER_CERT_V_END%", testCase{ format: `%DOWNSTREAM_PEER_CERT_V_END%`, - expectedHTTP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT_V_END)`, - expectedTCP: `UNSUPPORTED_FIELD(DOWNSTREAM_PEER_CERT_V_END)`, + expectedHTTP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT_V_END%)`, + expectedTCP: `UNSUPPORTED_COMMAND(%DOWNSTREAM_PEER_CERT_V_END%)`, }), Entry("%HOSTNAME%", testCase{ format: `%HOSTNAME%`, - expectedHTTP: `UNSUPPORTED_FIELD(HOSTNAME)`, - expectedTCP: `UNSUPPORTED_FIELD(HOSTNAME)`, + expectedHTTP: `UNSUPPORTED_COMMAND(%HOSTNAME%)`, + expectedTCP: `UNSUPPORTED_COMMAND(%HOSTNAME%)`, }), Entry("composite", testCase{ format: `[%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)%"`, @@ -811,4 +811,124 @@ UF,URX }), ) }) + + Describe("support String()", func() { + type testCase struct { + format string + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // when + formatter, err := ParseFormat(given.format) + // then + Expect(err).ToNot(HaveOccurred()) + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("composite", testCase{ + format: `[%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)%"`, + expected: `[%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)%"`, + }), + Entry("multi-line", testCase{ + format: ` +%START_TIME% +%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)% +%REQ(:METHOD)% +%RESP(content-type?SERVER):10% +%TRAILER(PROTOCOL)% +%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key):10% +%FILTER_STATE(filter.state.key):10% +%BYTES_RECEIVED% +%BYTES_RECEIVED% +%BYTES_SENT% +%PROTOCOL% +%RESPONSE_CODE% +%RESPONSE_CODE_DETAILS% +%REQUEST_DURATION% +%RESPONSE_DURATION% +%RESPONSE_TX_DURATION% +%DURATION% +%RESPONSE_FLAGS% +%UPSTREAM_HOST% +%UPSTREAM_CLUSTER% +%UPSTREAM_LOCAL_ADDRESS% +%DOWNSTREAM_LOCAL_ADDRESS% +%DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT% +%DOWNSTREAM_REMOTE_ADDRESS% +%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT% +%DOWNSTREAM_DIRECT_REMOTE_ADDRESS% +%DOWNSTREAM_DIRECT_REMOTE_ADDRESS_WITHOUT_PORT% +%REQUESTED_SERVER_NAME% +%ROUTE_NAME% +%DOWNSTREAM_PEER_URI_SAN% +%DOWNSTREAM_LOCAL_URI_SAN% +%DOWNSTREAM_PEER_SUBJECT% +%DOWNSTREAM_LOCAL_SUBJECT% +%DOWNSTREAM_TLS_SESSION_ID% +%DOWNSTREAM_TLS_CIPHER% +%DOWNSTREAM_TLS_VERSION% +%UPSTREAM_TRANSPORT_FAILURE_REASON% +%DOWNSTREAM_PEER_FINGERPRINT_256% +%DOWNSTREAM_PEER_SERIAL% +%DOWNSTREAM_PEER_ISSUER% +%DOWNSTREAM_PEER_CERT% +%DOWNSTREAM_PEER_CERT_V_START% +%DOWNSTREAM_PEER_CERT_V_END% +%HOSTNAME% +`, + expected: ` +%START_TIME% +%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)% +%REQ(:method)% +%RESP(content-type?server):10% +%TRAILER(protocol)% +%DYNAMIC_METADATA(com.test.my_filter:test_object:inner_key):10% +%FILTER_STATE(filter.state.key):10% +%BYTES_RECEIVED% +%BYTES_RECEIVED% +%BYTES_SENT% +%PROTOCOL% +%RESPONSE_CODE% +%RESPONSE_CODE_DETAILS% +%REQUEST_DURATION% +%RESPONSE_DURATION% +%RESPONSE_TX_DURATION% +%DURATION% +%RESPONSE_FLAGS% +%UPSTREAM_HOST% +%UPSTREAM_CLUSTER% +%UPSTREAM_LOCAL_ADDRESS% +%DOWNSTREAM_LOCAL_ADDRESS% +%DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT% +%DOWNSTREAM_REMOTE_ADDRESS% +%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT% +%DOWNSTREAM_DIRECT_REMOTE_ADDRESS% +%DOWNSTREAM_DIRECT_REMOTE_ADDRESS_WITHOUT_PORT% +%REQUESTED_SERVER_NAME% +%ROUTE_NAME% +%DOWNSTREAM_PEER_URI_SAN% +%DOWNSTREAM_LOCAL_URI_SAN% +%DOWNSTREAM_PEER_SUBJECT% +%DOWNSTREAM_LOCAL_SUBJECT% +%DOWNSTREAM_TLS_SESSION_ID% +%DOWNSTREAM_TLS_CIPHER% +%DOWNSTREAM_TLS_VERSION% +%UPSTREAM_TRANSPORT_FAILURE_REASON% +%DOWNSTREAM_PEER_FINGERPRINT_256% +%DOWNSTREAM_PEER_SERIAL% +%DOWNSTREAM_PEER_ISSUER% +%DOWNSTREAM_PEER_CERT% +%DOWNSTREAM_PEER_CERT_V_START% +%DOWNSTREAM_PEER_CERT_V_END% +%HOSTNAME% +`, + }), + ) + }) }) diff --git a/pkg/envoy/accesslog/header_formatter.go b/pkg/envoy/accesslog/header_formatter.go index 167bb3c2acc8..2a8ed29d2442 100644 --- a/pkg/envoy/accesslog/header_formatter.go +++ b/pkg/envoy/accesslog/header_formatter.go @@ -1,5 +1,10 @@ package accesslog +import ( + "strconv" + "strings" +) + // Headers represents a set of headers // that might include both regular and pseudo headers. type Headers interface { @@ -48,3 +53,23 @@ func (f *HeaderFormatter) AppendTo(headers []string) []string { } return headers } + +// String returns the canonical representation of a header command operator +// arguments and max length constraint. +func (f *HeaderFormatter) String() string { + var builder []string + builder = append(builder, "(") + if f.Header != "" || f.AltHeader != "" { + builder = append(builder, f.Header) + if f.AltHeader != "" { + builder = append(builder, "?") + builder = append(builder, f.AltHeader) + } + } + builder = append(builder, ")") + if f.MaxLength != 0 { + builder = append(builder, ":") + builder = append(builder, strconv.FormatInt(int64(f.MaxLength), 10)) + } + return strings.Join(builder, "") +} diff --git a/pkg/envoy/accesslog/interfaces.go b/pkg/envoy/accesslog/interfaces.go index f55ca9943f8e..0b040c85f263 100644 --- a/pkg/envoy/accesslog/interfaces.go +++ b/pkg/envoy/accesslog/interfaces.go @@ -5,6 +5,14 @@ import ( accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" ) +// CommandOperator represents an Envoy access log command operator. +// +// See https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log#command-operators +type CommandOperator interface { + // String returns the canonical representation of this command operator. + String() string +} + // LogConfigurer adjusts configuration of // `envoy.http_grpc_access_log` and `envoy.tcp_grpc_access_log` // according to the format string, e.g. to capture additional HTTP headers. @@ -18,6 +26,7 @@ type LogConfigurer interface { type LogEntryFormatter interface { HttpLogEntryFormatter TcpLogEntryFormatter + CommandOperator } // LogConfigureFormatter is a convenience type. diff --git a/pkg/envoy/accesslog/request_header_formatter.go b/pkg/envoy/accesslog/request_header_formatter.go index 12acb50550c0..1bde27b49428 100644 --- a/pkg/envoy/accesslog/request_header_formatter.go +++ b/pkg/envoy/accesslog/request_header_formatter.go @@ -1,6 +1,8 @@ package accesslog import ( + "fmt" + accesslog_config "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v2" accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" @@ -25,6 +27,11 @@ func (f *RequestHeaderFormatter) ConfigureHttpLog(config *accesslog_config.HttpG return nil } +// String returns the canonical representation of this command operator. +func (f *RequestHeaderFormatter) String() string { + return fmt.Sprintf("%%REQ%s%%", f.HeaderFormatter.String()) +} + // RequestHeaders represents a set of HTTP request headers // that includes both regular headers, such as `referer` and `user-agent`, // and pseudo headers, such as `:method`, `:authority` and `:path`. diff --git a/pkg/envoy/accesslog/request_header_formatter_test.go b/pkg/envoy/accesslog/request_header_formatter_test.go index 15d201eedca7..3cfd5dba1683 100644 --- a/pkg/envoy/accesslog/request_header_formatter_test.go +++ b/pkg/envoy/accesslog/request_header_formatter_test.go @@ -262,4 +262,63 @@ var _ = Describe("RequestHeaderFormatter", func() { }), ) }) + + Describe("String()", func() { + type testCase struct { + header string + altHeader string + maxLength int + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := &RequestHeaderFormatter{HeaderFormatter{ + Header: given.header, AltHeader: given.altHeader, MaxLength: given.maxLength}} + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("%REQ()%", testCase{ + expected: `%REQ()%`, + }), + Entry("%REQ():10%", testCase{ + maxLength: 10, + expected: `%REQ():10%`, + }), + Entry("%REQ(:authority)%", testCase{ + header: `:authority`, + expected: `%REQ(:authority)%`, + }), + Entry("%REQ(:authority):10%", testCase{ + header: `:authority`, + maxLength: 10, + expected: `%REQ(:authority):10%`, + }), + Entry("%REQ(?origin)%", testCase{ + altHeader: `origin`, + expected: `%REQ(?origin)%`, + }), + Entry("%REQ(?origin):10%", testCase{ + altHeader: `origin`, + maxLength: 10, + expected: `%REQ(?origin):10%`, + }), + Entry("%REQ(:authority?origin)%", testCase{ + header: ":authority", + altHeader: `origin`, + expected: `%REQ(:authority?origin)%`, + }), + Entry("%REQ(:authority?origin):10%", testCase{ + header: ":authority", + altHeader: `origin`, + maxLength: 10, + expected: `%REQ(:authority?origin):10%`, + }), + ) + }) }) diff --git a/pkg/envoy/accesslog/response_header_formatter.go b/pkg/envoy/accesslog/response_header_formatter.go index 3319dd572619..ab731071cf89 100644 --- a/pkg/envoy/accesslog/response_header_formatter.go +++ b/pkg/envoy/accesslog/response_header_formatter.go @@ -1,6 +1,8 @@ package accesslog import ( + "fmt" + accesslog_config "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v2" accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" ) @@ -22,3 +24,8 @@ func (f *ResponseHeaderFormatter) ConfigureHttpLog(config *accesslog_config.Http config.AdditionalResponseHeadersToLog = f.AppendTo(config.AdditionalResponseHeadersToLog) return nil } + +// String returns the canonical representation of this command operator. +func (f *ResponseHeaderFormatter) String() string { + return fmt.Sprintf("%%RESP%s%%", f.HeaderFormatter.String()) +} diff --git a/pkg/envoy/accesslog/response_header_formatter_test.go b/pkg/envoy/accesslog/response_header_formatter_test.go index ff67bd3ab356..d1bafbda11ce 100644 --- a/pkg/envoy/accesslog/response_header_formatter_test.go +++ b/pkg/envoy/accesslog/response_header_formatter_test.go @@ -195,4 +195,63 @@ var _ = Describe("ResponseHeaderFormatter", func() { }), ) }) + + Describe("String()", func() { + type testCase struct { + header string + altHeader string + maxLength int + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := &ResponseHeaderFormatter{HeaderFormatter{ + Header: given.header, AltHeader: given.altHeader, MaxLength: given.maxLength}} + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("%RESP()%", testCase{ + expected: `%RESP()%`, + }), + Entry("%RESP():10%", testCase{ + maxLength: 10, + expected: `%RESP():10%`, + }), + Entry("%RESP(content-type)%", testCase{ + header: `content-type`, + expected: `%RESP(content-type)%`, + }), + Entry("%RESP(content-type):10%", testCase{ + header: `content-type`, + maxLength: 10, + expected: `%RESP(content-type):10%`, + }), + Entry("%RESP(?server)%", testCase{ + altHeader: `server`, + expected: `%RESP(?server)%`, + }), + Entry("%RESP(?server):10%", testCase{ + altHeader: `server`, + maxLength: 10, + expected: `%RESP(?server):10%`, + }), + Entry("%RESP(content-type?server)%", testCase{ + header: "content-type", + altHeader: `server`, + expected: `%RESP(content-type?server)%`, + }), + Entry("%RESP(content-type?server):10%", testCase{ + header: "content-type", + altHeader: `server`, + maxLength: 10, + expected: `%RESP(content-type?server):10%`, + }), + ) + }) }) diff --git a/pkg/envoy/accesslog/response_trailer_formatter.go b/pkg/envoy/accesslog/response_trailer_formatter.go index df47ca5f0efc..409d585cc1c4 100644 --- a/pkg/envoy/accesslog/response_trailer_formatter.go +++ b/pkg/envoy/accesslog/response_trailer_formatter.go @@ -1,6 +1,8 @@ package accesslog import ( + "fmt" + accesslog_config "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v2" accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" ) @@ -22,3 +24,8 @@ func (f *ResponseTrailerFormatter) ConfigureHttpLog(config *accesslog_config.Htt config.AdditionalResponseTrailersToLog = f.AppendTo(config.AdditionalResponseTrailersToLog) return nil } + +// String returns the canonical representation of this command operator. +func (f *ResponseTrailerFormatter) String() string { + return fmt.Sprintf("%%TRAILER%s%%", f.HeaderFormatter.String()) +} diff --git a/pkg/envoy/accesslog/response_trailer_formatter_test.go b/pkg/envoy/accesslog/response_trailer_formatter_test.go index 64525648be9b..13fa3de73ff3 100644 --- a/pkg/envoy/accesslog/response_trailer_formatter_test.go +++ b/pkg/envoy/accesslog/response_trailer_formatter_test.go @@ -195,4 +195,63 @@ var _ = Describe("ResponseTrailerFormatter", func() { }), ) }) + + Describe("String()", func() { + type testCase struct { + header string + altHeader string + maxLength int + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := &ResponseTrailerFormatter{HeaderFormatter{ + Header: given.header, AltHeader: given.altHeader, MaxLength: given.maxLength}} + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("%TRAILER()%", testCase{ + expected: `%TRAILER()%`, + }), + Entry("%TRAILER():10%", testCase{ + maxLength: 10, + expected: `%TRAILER():10%`, + }), + Entry("%TRAILER(grpc-status)%", testCase{ + header: `grpc-status`, + expected: `%TRAILER(grpc-status)%`, + }), + Entry("%TRAILER(grpc-status):10%", testCase{ + header: `grpc-status`, + maxLength: 10, + expected: `%TRAILER(grpc-status):10%`, + }), + Entry("%TRAILER(?grpc-message)%", testCase{ + altHeader: `grpc-message`, + expected: `%TRAILER(?grpc-message)%`, + }), + Entry("%TRAILER(?grpc-message):10%", testCase{ + altHeader: `grpc-message`, + maxLength: 10, + expected: `%TRAILER(?grpc-message):10%`, + }), + Entry("%TRAILER(grpc-status?grpc-message)%", testCase{ + header: "grpc-status", + altHeader: `grpc-message`, + expected: `%TRAILER(grpc-status?grpc-message)%`, + }), + Entry("%TRAILER(grpc-status?grpc-message):10%", testCase{ + header: "grpc-status", + altHeader: `grpc-message`, + maxLength: 10, + expected: `%TRAILER(grpc-status?grpc-message):10%`, + }), + ) + }) }) diff --git a/pkg/envoy/accesslog/start_time_formatter.go b/pkg/envoy/accesslog/start_time_formatter.go index b355318cb77e..0ee26c013838 100644 --- a/pkg/envoy/accesslog/start_time_formatter.go +++ b/pkg/envoy/accesslog/start_time_formatter.go @@ -1,6 +1,8 @@ package accesslog import ( + "fmt" + "github.com/golang/protobuf/ptypes" accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" @@ -33,3 +35,11 @@ func (f StartTimeFormatter) format(entry *accesslog_data.AccessLogCommon) (strin // TODO(yskopets): take format string parameter into account return startTime.Format(defaultStartTimeFormat), nil } + +// String returns the canonical representation of this command operator. +func (f StartTimeFormatter) String() string { + if f == "" { + return CommandOperatorDescriptor(CMD_START_TIME).String() + } + return fmt.Sprintf("%%START_TIME(%s)%%", string(f)) +} diff --git a/pkg/envoy/accesslog/start_time_formatter_test.go b/pkg/envoy/accesslog/start_time_formatter_test.go index 25774d7a013e..4d4e7ba05930 100644 --- a/pkg/envoy/accesslog/start_time_formatter_test.go +++ b/pkg/envoy/accesslog/start_time_formatter_test.go @@ -95,4 +95,36 @@ var _ = Describe("StartTimeFormatter", func() { Expect(err).To(HaveOccurred()) }) }) + + Describe("String()", func() { + type testCase struct { + timeFormat string + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := StartTimeFormatter(given.timeFormat) + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("%START_TIME%", testCase{ + timeFormat: "", // default time format + expected: `%START_TIME%`, + }), + Entry("%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)%", testCase{ + timeFormat: "%Y/%m/%dT%H:%M:%S%z %s", + expected: `%START_TIME(%Y/%m/%dT%H:%M:%S%z %s)%`, + }), + Entry("%START_TIME(%s.%3f)%", testCase{ + timeFormat: "%s.%3f", + expected: `%START_TIME(%s.%3f)%`, + }), + ) + }) }) diff --git a/pkg/envoy/accesslog/text_literal_formatter.go b/pkg/envoy/accesslog/text_literal_formatter.go index e8ceec3f7f5d..36839a29f961 100644 --- a/pkg/envoy/accesslog/text_literal_formatter.go +++ b/pkg/envoy/accesslog/text_literal_formatter.go @@ -18,3 +18,8 @@ func (f TextLiteralFormatter) FormatTcpLogEntry(entry *accesslog_data.TCPAccessL func (f TextLiteralFormatter) format() (string, error) { return string(f), nil } + +// String returns the canonical representation of this command operator. +func (f TextLiteralFormatter) String() string { + return string(f) +} diff --git a/pkg/envoy/accesslog/text_literal_formatter_test.go b/pkg/envoy/accesslog/text_literal_formatter_test.go new file mode 100644 index 000000000000..ec440ccb0f8f --- /dev/null +++ b/pkg/envoy/accesslog/text_literal_formatter_test.go @@ -0,0 +1,78 @@ +package accesslog_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + . "github.com/Kong/kuma/pkg/envoy/accesslog" + + accesslog_data "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v2" +) + +var _ = Describe("TextLiteralFormatter", func() { + + Describe("FormatHttpLogEntry() and FormatTcpLogEntry()", func() { + type testCase struct { + text string + expected string + } + + DescribeTable("should format properly", + func(given testCase) { + // setup + formatter := TextLiteralFormatter(given.text) + + // when + actual, err := formatter.FormatHttpLogEntry(&accesslog_data.HTTPAccessLogEntry{}) + // then + Expect(err).ToNot(HaveOccurred()) + // and + Expect(actual).To(Equal(given.expected)) + + // when + actual, err = formatter.FormatTcpLogEntry(&accesslog_data.TCPAccessLogEntry{}) + // then + Expect(err).ToNot(HaveOccurred()) + // and + Expect(actual).To(Equal(given.expected)) + }, + Entry("", testCase{ + text: "", + expected: ``, + }), + Entry("plain text", testCase{ + text: "plain text", + expected: `plain text`, + }), + ) + }) + + Describe("String()", func() { + type testCase struct { + text string + expected string + } + + DescribeTable("should return correct canonical representation", + func(given testCase) { + // setup + formatter := TextLiteralFormatter(given.text) + + // when + actual := formatter.String() + // then + Expect(actual).To(Equal(given.expected)) + + }, + Entry("", testCase{ + text: "", + expected: ``, + }), + Entry("plain text", testCase{ + text: "plain text", + expected: `plain text`, + }), + ) + }) +})