Skip to content

Commit

Permalink
Thrift: Payload to metadata filter (envoyproxy#23409)
Browse files Browse the repository at this point in the history
This filter is configured with request_rules that will be matched against requests. A field_selector of a rule represents the head of a linked list, each node of the linked list has a name for logging and an id for matching. The field_selector is tied to a payload field when the linked list corresponds to a downward path which rooted in the top-level of the request message structure. on_present is triggered when corresponding the payload is present. Otherwise, on_missing is triggered.

This filter is designed to support payload passthrough. By performing payload to metadata filter can do deserialization once, and pass the metadata to other filters. This means that load balancing decisions, consumed from log and routing could all use payload information with a single parse. Also notably performing the parsing in payload passthrough buffer will mean deserialization once and not re-serializing, which is the most performant outcome.

Risk Level: low
Testing: unit
Docs Changes: multiple rst
Fixes envoyproxy#23322

Signed-off-by: kuochunghsu <[email protected]>
  • Loading branch information
JuniorHsu authored Oct 28, 2022
1 parent 2e3e6c5 commit cd208a5
Show file tree
Hide file tree
Showing 20 changed files with 2,419 additions and 2 deletions.
1 change: 1 addition & 0 deletions api/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ proto_library(
"//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/filters/network/tcp_proxy/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/filters/header_to_metadata/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/router/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/v3:pkg",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py.

load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")

licenses(["notice"]) # Apache 2

api_proto_package(
deps = [
"//envoy/type/matcher/v3:pkg",
"@com_github_cncf_udpa//udpa/annotations:pkg",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
syntax = "proto3";

package envoy.extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3;

import "envoy/type/matcher/v3/regex.proto";

import "udpa/annotations/status.proto";
import "validate/validate.proto";

option java_package = "io.envoyproxy.envoy.extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3";
option java_outer_classname = "PayloadToMetadataProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/v3;payload_to_metadatav3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Payload-To-Metadata Filter]
//
// The configuration for transforming payloads into metadata. This is useful
// for matching load balancer subsets, logging, etc.
//
// Payload to Metadata :ref:`configuration overview <config_thrift_filters_payload_to_metadata>`.
// [#extension: envoy.filters.thrift.payload_to_metadata]

message PayloadToMetadata {
enum ValueType {
STRING = 0;
NUMBER = 1;
}

// [#next-free-field: 6]
message KeyValuePair {
// The namespace — if this is empty, the filter's namespace will be used.
string metadata_namespace = 1;

// The key to use within the namespace.
string key = 2 [(validate.rules).string = {min_len: 1}];

oneof value_type {
// The value to pair with the given key.
//
// When used for on_present case, if value is non-empty it'll be used instead
// of the field value. If both are empty, the field value is used as-is.
//
// When used for on_missing case, a non-empty value must be provided.
string value = 3;

// If present, the header's value will be matched and substituted with this.
// If there is no match or substitution, the field value is used as-is.
//
// This is only used for on_present.
type.matcher.v3.RegexMatchAndSubstitute regex_value_rewrite = 4;
}

// The value's type — defaults to string.
ValueType type = 5 [(validate.rules).enum = {defined_only: true}];
}

// A Rule defines what metadata to apply when a field is present or missing.
// [#next-free-field: 6]
message Rule {
oneof match_specifier {
option (validate.required) = true;

// If specified, the route must exactly match the request method name. As a special case,
// an empty string matches any request method name.
string method_name = 1;

// If specified, the route must have the service name as the request method name prefix.
// As a special case, an empty string matches any service name. Only relevant when service
// multiplexing.
string service_name = 2;
}

// Specifies that a match will be performed on the value of a field.
FieldSelector field_selector = 3 [(validate.rules).message = {required: true}];

// If the field is present, apply this metadata KeyValuePair.
KeyValuePair on_present = 4;

// If the field is missing, apply this metadata KeyValuePair.
//
// The value in the KeyValuePair must be set, since it'll be used in lieu
// of the missing field value.
KeyValuePair on_missing = 5;
}

message FieldSelector {
// field name to log
string name = 1 [(validate.rules).string = {min_len: 1}];

// field id to match
int32 id = 2 [(validate.rules).int32 = {lte: 32767 gte: -32768}];

// next node of the field selector
FieldSelector child = 3;
}

// The list of rules to apply to requests.
repeated Rule request_rules = 1 [(validate.rules).repeated = {min_items: 1}];
}
1 change: 1 addition & 0 deletions api/versioning/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ proto_library(
"//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3:pkg",
"//envoy/extensions/filters/network/tcp_proxy/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/filters/header_to_metadata/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/router/v3:pkg",
"//envoy/extensions/filters/network/thrift_proxy/v3:pkg",
Expand Down
4 changes: 3 additions & 1 deletion changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ new_features:
- area: upstream
change: |
added a new field :ref:`socket_options <envoy_v3_api_field_config.core.v3.ExtraSourceAddress.socket_options>` to the ExtraSourceAddress, allowing specifying discrete socket options for each source address.
- area: thrift
change: |
added payload to metadata filter which matches a given payload field's value would be extracted and attached to the request as dynamic metadata.
- area: http
change: |
allowing the dynamic forward proxy cluster to :ref:`allow_coalesced_connections <envoy_v3_api_field_extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig.allow_coalesced_connections>` for HTTP/2 and HTTP/3 connections.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 9090
filter_chains:
- filters:
- name: envoy.filters.network.thrift_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.thrift_proxy.v3.ThriftProxy
stat_prefix: ingress_thrift
route_config:
name: local_route
routes:
- match:
method_name: ""
route:
cluster: versioned-cluster
thrift_filters:
- name: envoy.filters.thrift.payload_to_metadata
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata
request_rules:
- method_name: foo
field_selector:
name: info
id: 2
child:
name: version
id: 1
on_present:
metadata_namespace: envoy.lb
key: version
on_missing:
metadata_namespace: envoy.lb
key: default
value: 'unknown'
clusters:
- name: versioned-cluster
type: STRICT_DNS
lb_policy: ROUND_ROBIN
lb_subset_config:
fallback_policy: NO_FALLBACK
subset_selectors:
- keys:
- default
- keys:
- version
load_assignment:
cluster_name: versioned-cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 19090
metadata:
filter_metadata:
envoy.lb:
default: "true"
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 19091
metadata:
filter_metadata:
envoy.lb:
version: "1.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
syntax = "proto3";

package request;

message Request {
message Info {
string version = 1;
}
string data = 1;
Info info = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
.. _config_thrift_filters_payload_to_metadata:

Envoy Payload-To-Metadata Filter
================================
* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata``.
* :ref:`v3 API reference <envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata>`

A typical use case for this filter is to dynamically match a specified payload field of requests
with load balancer subsets. For this, a given payload field's value would be extracted and attached
to the request as dynamic metadata which would then be used to match a subset of endpoints.

We already have :ref:`header-To-metadata filter <envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.header_to_metadata.v3.HeaderToMetadata>`
to achieve the similar goal. However, we have two reasons for introducing new :ref:`payload-To-metadata filter
<envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata>`:

1. Transports like framed transport don't support THeaders, which is unable to use :ref:`Header-To-Metadata filter
<envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.header_to_metadata.v3.HeaderToMetadata>`.

2. Directly referring to payload field stops envoy relying on that the downstream service always copies the field
to the THeader correctly and guarantees single truth of source.

This filter is configured with :ref:`request_rules
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.request_rules>`
that will be matched against requests. A
:ref:`field_selector
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule.field_selector>`
of a :ref:`rule
<envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule>`
represents the head of a linked list, each node of the linked list has a :ref:`name
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.FieldSelector.name>`
for logging and an :ref:`id
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.FieldSelector.id>`
for matching. The :ref:`field_selector
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule.field_selector>`
is tied to a payload field when the linked list corresponds to a downward path which rooted in the top-level of the
request message structure. :ref:`on_present
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule.on_present>`
is triggered when corresponding the payload is present. Otherwise, :ref:`on_missing
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule.on_missing>`
is triggered.

Note that if the corresponding payload for a :ref:`rule
<envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule>`
is present but :ref:`on_present
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule.on_present>`
is missing, no metadata is added for this :ref:`rule
<envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule>`.
. If the corresponding payload for a :ref:`rule
<envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule>`
is an empty string, neither :ref:`on_present
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule.on_present>`
nor :ref:`on_missing
<envoy_v3_api_field_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule.on_missing>`
is triggered. i.e., no metadata is added for this :ref:`rule
<envoy_v3_api_msg_extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata.Rule>`.

Currently payload to metadata filter doesn't support container type payload, i.e., list, set and map.

We limit the size of a single metadata value which is added by this filter to 1024 bytes.

This filter is designed to support payload passthrough. Performing payload to metadata filter
can do deserialization once, and pass the metadata to other filters. This means that load balancing
decisions, consumed from log and routing could all use payload information with a single parse.
Also notably performing the parsing in payload passthrough buffer will mean deserialization once
and not re-serializing, which is the most performant outcome.

If any of the filter chain doesn't support payload passthrough, a customized non-passthrough
filter to setup metadata is encouraged from point of performance view.

Example
-------

A sample filter configuration to route traffic to endpoints based on the presence or
absence of a version payload could be:

.. literalinclude:: _include/payload-to-metadata-filter.yaml
:language: yaml
:lines: 20-38
:lineno-start: 20
:linenos:
:caption: :download:`payload-to-metadata-filter.yaml <_include/payload-to-metadata-filter.yaml>`

A corresponding upstream cluster configuration could be:

.. literalinclude:: _include/header-to-metadata-filter.yaml
:language: yaml
:lines: 39-49
:lineno-start: 37
:linenos:
:caption: :download:`header-to-metadata-filter.yaml <_include/header-to-metadata-filter.yaml>`

The request thrift structure could be:

.. literalinclude:: _include/request.proto
:language: proto

This would then allow requests of method name ``foo`` with the ``version`` payload field which is
under ``info`` field set to be matched against endpoints with the corresponding version. Whereas
requests with that payload missing would be matched with the default endpoints.

The regex matching and substitution is similiar with :ref:`header to metadata filter <config_thrift_filters_header_to_metadata>`.


Statistics
----------

Currently, this filter generates no statistics.
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ Envoy has the following builtin Thrift filters.
:maxdepth: 2

header_to_metadata_filter
payload_to_metadata_filter
rate_limit_filter
router_filter
1 change: 1 addition & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ EXTENSIONS = {

"envoy.filters.thrift.router": "//source/extensions/filters/network/thrift_proxy/router:config",
"envoy.filters.thrift.header_to_metadata": "//source/extensions/filters/network/thrift_proxy/filters/header_to_metadata:config",
"envoy.filters.thrift.payload_to_metadata": "//source/extensions/filters/network/thrift_proxy/filters/payload_to_metadata:config",
"envoy.filters.thrift.rate_limit": "//source/extensions/filters/network/thrift_proxy/filters/ratelimit:config",

#
Expand Down
7 changes: 7 additions & 0 deletions source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,13 @@ envoy.filters.thrift.header_to_metadata:
status: alpha
type_urls:
- envoy.extensions.filters.network.thrift_proxy.filters.header_to_metadata.v3.HeaderToMetadata
envoy.filters.thrift.payload_to_metadata:
categories:
- envoy.thrift_proxy.filters
security_posture: requires_trusted_downstream_and_upstream
status: alpha
type_urls:
- envoy.extensions.filters.network.thrift_proxy.filters.payload_to_metadata.v3.PayloadToMetadata
envoy.filters.thrift.rate_limit:
categories:
- envoy.thrift_proxy.filters
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_extension",
"envoy_cc_library",
"envoy_extension_package",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_extension(
name = "config",
srcs = ["config.cc"],
hdrs = ["config.h"],
deps = [
":payload_to_metadata_filter_lib",
"//envoy/registry",
"//source/extensions/filters/network/thrift_proxy/filters:factory_base_lib",
"@envoy_api//envoy/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/v3:pkg_cc_proto",
],
)

envoy_cc_library(
name = "payload_to_metadata_filter_lib",
srcs = ["payload_to_metadata_filter.cc"],
hdrs = ["payload_to_metadata_filter.h"],
deps = [
"//envoy/server:filter_config_interface",
"//source/extensions/filters/network/thrift_proxy:auto_protocol_lib",
"//source/extensions/filters/network/thrift_proxy:auto_transport_lib",
"//source/extensions/filters/network/thrift_proxy:decoder_lib",
"//source/extensions/filters/network/thrift_proxy/filters:pass_through_filter_lib",
"@envoy_api//envoy/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/v3:pkg_cc_proto",
],
)
Loading

0 comments on commit cd208a5

Please sign in to comment.