diff --git a/api/envoy/config/filter/accesslog/v2/accesslog.proto b/api/envoy/config/filter/accesslog/v2/accesslog.proto index b286ee8ad97ec..da13fb72fda60 100644 --- a/api/envoy/config/filter/accesslog/v2/accesslog.proto +++ b/api/envoy/config/filter/accesslog/v2/accesslog.proto @@ -73,6 +73,9 @@ message AccessLogFilter { // Response flag filter. ResponseFlagFilter response_flag_filter = 9; + + // gRPC status filter. + GrpcStatusFilter grpc_status_filter = 10; } } @@ -192,3 +195,34 @@ message ResponseFlagFilter { ] }]; } + +// Filters gRPC requests based on their response status. If a gRPC status is not provided, the +// filter will infer the status from the HTTP status code. +message GrpcStatusFilter { + enum Status { + OK = 0; + CANCELED = 1; + UNKNOWN = 2; + INVALID_ARGUMENT = 3; + DEADLINE_EXCEEDED = 4; + NOT_FOUND = 5; + ALREADY_EXISTS = 6; + PERMISSION_DENIED = 7; + RESOURCE_EXHAUSTED = 8; + FAILED_PRECONDITION = 9; + ABORTED = 10; + OUT_OF_RANGE = 11; + UNIMPLEMENTED = 12; + INTERNAL = 13; + UNAVAILABLE = 14; + DATA_LOSS = 15; + UNAUTHENTICATED = 16; + } + + // Logs only responses that have any one of the gRPC statuses in this field. + repeated Status statuses = 1 [(validate.rules).repeated .items.enum.defined_only = true]; + + // If included and set to true, the filter will instead block all responses with a gRPC status or + // inferred gRPC status enumerated in statuses, and allow all other responses. + bool exclude = 2; +} diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 19358c0a437d6..abbceb32d17f0 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -4,6 +4,7 @@ Version history 1.10.0 (pending) ================ * access log: added a new flag for upstream retry count exceeded. +* access log: added a :ref:`gRPC filter ` to allow filtering on gRPC status. * access log: added a new flag for stream idle timeout. * admin: the admin server can now be accessed via HTTP/2 (prior knowledge). * buffer: fix vulnerabilities when allocation fails. diff --git a/include/envoy/access_log/access_log.h b/include/envoy/access_log/access_log.h index 5f04026a2aae0..af53b244d82c2 100644 --- a/include/envoy/access_log/access_log.h +++ b/include/envoy/access_log/access_log.h @@ -41,8 +41,9 @@ class Filter { * Evaluate whether an access log should be written based on request and response data. * @return TRUE if the log should be written. */ - virtual bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) PURE; + virtual bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) PURE; }; typedef std::unique_ptr FilterPtr; diff --git a/include/envoy/grpc/status.h b/include/envoy/grpc/status.h index 24c200eb438a5..dbc7c0a016f1c 100644 --- a/include/envoy/grpc/status.h +++ b/include/envoy/grpc/status.h @@ -5,6 +5,8 @@ namespace Grpc { class Status { public: + // If this enum is changed, then the std::unordered_map in Envoy::Grpc::Utility::nameToGrpcStatus + // located at: //source/common/access_log/grpc/status.cc must also be changed. enum GrpcStatus { // The RPC completed successfully. Ok = 0, diff --git a/source/common/access_log/access_log_impl.cc b/source/common/access_log/access_log_impl.cc index aa335854eaa5e..da75fedb5fb78 100644 --- a/source/common/access_log/access_log_impl.cc +++ b/source/common/access_log/access_log_impl.cc @@ -74,19 +74,24 @@ FilterFactory::fromProto(const envoy::config::filter::accesslog::v2::AccessLogFi case envoy::config::filter::accesslog::v2::AccessLogFilter::kResponseFlagFilter: MessageUtil::validate(config); return FilterPtr{new ResponseFlagFilter(config.response_flag_filter())}; + case envoy::config::filter::accesslog::v2::AccessLogFilter::kGrpcStatusFilter: + MessageUtil::validate(config); + return FilterPtr{new GrpcStatusFilter(config.grpc_status_filter())}; default: NOT_REACHED_GCOVR_EXCL_LINE; } } bool TraceableRequestFilter::evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) { + const Http::HeaderMap& request_headers, + const Http::HeaderMap&, const Http::HeaderMap&) { Tracing::Decision decision = Tracing::HttpTracerUtility::isTracing(info, request_headers); return decision.traced && decision.reason == Tracing::Reason::ServiceForced; } -bool StatusCodeFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&) { +bool StatusCodeFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&, + const Http::HeaderMap&, const Http::HeaderMap&) { if (!info.responseCode()) { return compareAgainstValue(0ULL); } @@ -94,7 +99,8 @@ bool StatusCodeFilter::evaluate(const StreamInfo::StreamInfo& info, const Http:: return compareAgainstValue(info.responseCode().value()); } -bool DurationFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&) { +bool DurationFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&, + const Http::HeaderMap&, const Http::HeaderMap&) { absl::optional final = info.requestComplete(); ASSERT(final); @@ -108,7 +114,8 @@ RuntimeFilter::RuntimeFilter(const envoy::config::filter::accesslog::v2::Runtime percent_(config.percent_sampled()), use_independent_randomness_(config.use_independent_randomness()) {} -bool RuntimeFilter::evaluate(const StreamInfo::StreamInfo&, const Http::HeaderMap& request_header) { +bool RuntimeFilter::evaluate(const StreamInfo::StreamInfo&, const Http::HeaderMap& request_header, + const Http::HeaderMap&, const Http::HeaderMap&) { const Http::HeaderEntry* uuid = request_header.RequestId(); uint64_t random_value; if (use_independent_randomness_ || uuid == nullptr || @@ -139,11 +146,12 @@ AndFilter::AndFilter(const envoy::config::filter::accesslog::v2::AndFilter& conf Runtime::Loader& runtime, Runtime::RandomGenerator& random) : OperatorFilter(config.filters(), runtime, random) {} -bool OrFilter::evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) { +bool OrFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) { bool result = false; for (auto& filter : filters_) { - result |= filter->evaluate(info, request_headers); + result |= filter->evaluate(info, request_headers, response_headers, response_trailers); if (result) { break; @@ -153,11 +161,12 @@ bool OrFilter::evaluate(const StreamInfo::StreamInfo& info, return result; } -bool AndFilter::evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) { +bool AndFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) { bool result = true; for (auto& filter : filters_) { - result &= filter->evaluate(info, request_headers); + result &= filter->evaluate(info, request_headers, response_headers, response_trailers); if (!result) { break; @@ -167,7 +176,8 @@ bool AndFilter::evaluate(const StreamInfo::StreamInfo& info, return result; } -bool NotHealthCheckFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&) { +bool NotHealthCheckFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&, + const Http::HeaderMap&, const Http::HeaderMap&) { return !info.healthCheck(); } @@ -175,7 +185,8 @@ HeaderFilter::HeaderFilter(const envoy::config::filter::accesslog::v2::HeaderFil header_data_.push_back(Http::HeaderUtility::HeaderData(config.header())); } -bool HeaderFilter::evaluate(const StreamInfo::StreamInfo&, const Http::HeaderMap& request_headers) { +bool HeaderFilter::evaluate(const StreamInfo::StreamInfo&, const Http::HeaderMap& request_headers, + const Http::HeaderMap&, const Http::HeaderMap&) { return Http::HeaderUtility::matchHeaders(request_headers, header_data_); } @@ -190,13 +201,60 @@ ResponseFlagFilter::ResponseFlagFilter( } } -bool ResponseFlagFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&) { +bool ResponseFlagFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&, + const Http::HeaderMap&, const Http::HeaderMap&) { if (configured_flags_ != 0) { return info.intersectResponseFlags(configured_flags_); } return info.hasAnyResponseFlag(); } +GrpcStatusFilter::GrpcStatusFilter( + const envoy::config::filter::accesslog::v2::GrpcStatusFilter& config) { + for (int i = 0; i < config.statuses_size(); i++) { + statuses_.insert(protoToGrpcStatus(config.statuses(i))); + } + + exclude_ = config.exclude(); +} + +bool GrpcStatusFilter::evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap&, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) { + // The gRPC specification does not guarantee a gRPC status code will be returned from a gRPC + // request. When it is returned, it will be in the response trailers. With that said, Envoy will + // treat a trailers-only response as a headers-only response, so we have to check the following + // in order: + // 1. response_trailers gRPC status, if it exists. + // 2. response_headers gRPC status, if it exists. + // 3. Inferred from info HTTP status, if it exists. + // + // If none of those options exist, it will default to Grpc::Status::GrpcStatus::Unknown. + const std::array, 3> optional_statuses = {{ + {Grpc::Common::getGrpcStatus(response_trailers)}, + {Grpc::Common::getGrpcStatus(response_headers)}, + {info.responseCode() ? absl::optional( + Grpc::Utility::httpToGrpcStatus(info.responseCode().value())) + : absl::nullopt}, + }}; + + Grpc::Status::GrpcStatus status = Grpc::Status::GrpcStatus::Unknown; + for (const auto& optional_status : optional_statuses) { + if (optional_status.has_value()) { + status = optional_status.value(); + break; + } + } + + const bool found = statuses_.find(status) != statuses_.end(); + return exclude_ ? !found : found; +} + +Grpc::Status::GrpcStatus GrpcStatusFilter::protoToGrpcStatus( + envoy::config::filter::accesslog::v2::GrpcStatusFilter_Status status) const { + return static_cast(status); +} + InstanceSharedPtr AccessLogFactory::fromProto(const envoy::config::filter::accesslog::v2::AccessLog& config, Server::Configuration::FactoryContext& context) { diff --git a/source/common/access_log/access_log_impl.h b/source/common/access_log/access_log_impl.h index 34c77a22164f3..c6916abea87d2 100644 --- a/source/common/access_log/access_log_impl.h +++ b/source/common/access_log/access_log_impl.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "envoy/access_log/access_log.h" @@ -9,6 +10,7 @@ #include "envoy/runtime/runtime.h" #include "envoy/server/access_log_config.h" +#include "common/grpc/status.h" #include "common/http/header_utility.h" #include "common/protobuf/protobuf.h" @@ -51,8 +53,9 @@ class StatusCodeFilter : public ComparisonFilter { : ComparisonFilter(config.comparison(), runtime) {} // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; }; /** @@ -65,8 +68,9 @@ class DurationFilter : public ComparisonFilter { : ComparisonFilter(config.comparison(), runtime) {} // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; }; /** @@ -91,8 +95,9 @@ class AndFilter : public OperatorFilter { Runtime::RandomGenerator& random); // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; }; /** @@ -104,8 +109,9 @@ class OrFilter : public OperatorFilter { Runtime::RandomGenerator& random); // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; }; /** @@ -116,8 +122,9 @@ class NotHealthCheckFilter : public Filter { NotHealthCheckFilter() {} // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; }; /** @@ -126,8 +133,9 @@ class NotHealthCheckFilter : public Filter { class TraceableRequestFilter : public Filter { public: // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; }; /** @@ -139,8 +147,9 @@ class RuntimeFilter : public Filter { Runtime::Loader& runtime, Runtime::RandomGenerator& random); // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; private: Runtime::Loader& runtime_; @@ -158,8 +167,9 @@ class HeaderFilter : public Filter { HeaderFilter(const envoy::config::filter::accesslog::v2::HeaderFilter& config); // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; private: std::vector header_data_; @@ -173,13 +183,40 @@ class ResponseFlagFilter : public Filter { ResponseFlagFilter(const envoy::config::filter::accesslog::v2::ResponseFlagFilter& config); // AccessLog::Filter - bool evaluate(const StreamInfo::StreamInfo& info, - const Http::HeaderMap& request_headers) override; + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; private: uint64_t configured_flags_{}; }; +/** + * Filters requests that have a response with a gRPC status. Because the gRPC protocol does not + * guarantee a gRPC status code, if a gRPC status code is not available, then the filter will infer + * the gRPC status code from an HTTP status code if available. + */ +class GrpcStatusFilter : public Filter { +public: + GrpcStatusFilter(const envoy::config::filter::accesslog::v2::GrpcStatusFilter& config); + + // AccessLog::Filter + bool evaluate(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers) override; + +private: + std::unordered_set statuses_; + bool exclude_; + + /** + * Converts a Protobuf representation of a gRPC status into the equivalent code version of a gRPC + * status. + */ + Grpc::Status::GrpcStatus + protoToGrpcStatus(envoy::config::filter::accesslog::v2::GrpcStatusFilter_Status status) const; +}; + /** * Access log factory that reads the configuration from proto. */ diff --git a/source/extensions/access_loggers/file/file_access_log_impl.cc b/source/extensions/access_loggers/file/file_access_log_impl.cc index e2344d2a2806b..75409f34dadcd 100644 --- a/source/extensions/access_loggers/file/file_access_log_impl.cc +++ b/source/extensions/access_loggers/file/file_access_log_impl.cc @@ -30,7 +30,7 @@ void FileAccessLog::log(const Http::HeaderMap* request_headers, } if (filter_) { - if (!filter_->evaluate(stream_info, *request_headers)) { + if (!filter_->evaluate(stream_info, *request_headers, *response_headers, *response_trailers)) { return; } } diff --git a/source/extensions/access_loggers/http_grpc/grpc_access_log_impl.cc b/source/extensions/access_loggers/http_grpc/grpc_access_log_impl.cc index a68ca45a09b58..497afcaf079c9 100644 --- a/source/extensions/access_loggers/http_grpc/grpc_access_log_impl.cc +++ b/source/extensions/access_loggers/http_grpc/grpc_access_log_impl.cc @@ -177,7 +177,7 @@ void HttpGrpcAccessLog::log(const Http::HeaderMap* request_headers, } if (filter_) { - if (!filter_->evaluate(stream_info, *request_headers)) { + if (!filter_->evaluate(stream_info, *request_headers, *response_headers, *response_trailers)) { return; } } diff --git a/test/common/access_log/access_log_impl_test.cc b/test/common/access_log/access_log_impl_test.cc index 600efbdcbce32..b91fea7356f49 100644 --- a/test/common/access_log/access_log_impl_test.cc +++ b/test/common/access_log/access_log_impl_test.cc @@ -548,23 +548,25 @@ TEST(AccessLogFilterTest, DurationWithRuntimeKey) { Config::FilterJson::translateAccessLogFilter(*filter_object, config); DurationFilter filter(config.duration_filter(), runtime); Http::TestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/"}}; + Http::TestHeaderMapImpl response_headers; + Http::TestHeaderMapImpl response_trailers; TestStreamInfo stream_info; stream_info.end_time_ = stream_info.startTimeMonotonic() + std::chrono::microseconds(100000); EXPECT_CALL(runtime.snapshot_, getInteger("key", 1000000)).WillOnce(Return(1)); - EXPECT_TRUE(filter.evaluate(stream_info, request_headers)); + EXPECT_TRUE(filter.evaluate(stream_info, request_headers, response_headers, response_trailers)); EXPECT_CALL(runtime.snapshot_, getInteger("key", 1000000)).WillOnce(Return(1000)); - EXPECT_FALSE(filter.evaluate(stream_info, request_headers)); + EXPECT_FALSE(filter.evaluate(stream_info, request_headers, response_headers, response_trailers)); stream_info.end_time_ = stream_info.startTimeMonotonic() + std::chrono::microseconds(100000001000); EXPECT_CALL(runtime.snapshot_, getInteger("key", 1000000)).WillOnce(Return(100000000)); - EXPECT_TRUE(filter.evaluate(stream_info, request_headers)); + EXPECT_TRUE(filter.evaluate(stream_info, request_headers, response_headers, response_trailers)); stream_info.end_time_ = stream_info.startTimeMonotonic() + std::chrono::microseconds(10000); EXPECT_CALL(runtime.snapshot_, getInteger("key", 1000000)).WillOnce(Return(100000000)); - EXPECT_FALSE(filter.evaluate(stream_info, request_headers)); + EXPECT_FALSE(filter.evaluate(stream_info, request_headers, response_headers, response_trailers)); } TEST(AccessLogFilterTest, StatusCodeWithRuntimeKey) { @@ -583,14 +585,16 @@ TEST(AccessLogFilterTest, StatusCodeWithRuntimeKey) { StatusCodeFilter filter(config.status_code_filter(), runtime); Http::TestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/"}}; + Http::TestHeaderMapImpl response_headers; + Http::TestHeaderMapImpl response_trailers; TestStreamInfo info; info.response_code_ = 400; EXPECT_CALL(runtime.snapshot_, getInteger("key", 300)).WillOnce(Return(350)); - EXPECT_TRUE(filter.evaluate(info, request_headers)); + EXPECT_TRUE(filter.evaluate(info, request_headers, response_headers, response_trailers)); EXPECT_CALL(runtime.snapshot_, getInteger("key", 300)).WillOnce(Return(500)); - EXPECT_FALSE(filter.evaluate(info, request_headers)); + EXPECT_FALSE(filter.evaluate(info, request_headers, response_headers, response_trailers)); } TEST_F(AccessLogImplTest, StatusCodeLessThan) { @@ -920,6 +924,185 @@ name: envoy.file_access_log "response_flag_filter {\n flags: \"UnsupportedFlag\"\n}\n"); } +TEST_F(AccessLogImplTest, GrpcStatusFilterValues) { + const std::string yaml_template = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + statuses: + - {} +config: + path: /dev/null +)EOF"; + + const auto desc = envoy::config::filter::accesslog::v2::GrpcStatusFilter_Status_descriptor(); + const int grpcStatuses = static_cast(Grpc::Status::GrpcStatus::MaximumValid) + 1; + if (desc->value_count() != grpcStatuses) { + FAIL() << "Mismatch in number of gRPC statuses, GrpcStatus has " << grpcStatuses + << ", GrpcStatusFilter_Status has " << desc->value_count() << "."; + } + + for (int i = 0; i < desc->value_count(); i++) { + InstanceSharedPtr log = AccessLogFactory::fromProto( + parseAccessLogFromV2Yaml(fmt::format(yaml_template, desc->value(i)->name())), context_); + + EXPECT_CALL(*file_, write(_)); + + response_trailers_.addCopy(Http::Headers::get().GrpcStatus, std::to_string(i)); + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); + response_trailers_.remove(Http::Headers::get().GrpcStatus); + } +} + +TEST_F(AccessLogImplTest, GrpcStatusFilterUnsupportedValue) { + const std::string yaml = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + statuses: + - NOT_A_VALID_CODE +config: + path: /dev/null + )EOF"; + + EXPECT_THROW_WITH_REGEX(AccessLogFactory::fromProto(parseAccessLogFromV2Yaml(yaml), context_), + EnvoyException, ".*\"NOT_A_VALID_CODE\" for type TYPE_ENUM.*"); +} + +TEST_F(AccessLogImplTest, GrpcStatusFilterBlock) { + const std::string yaml = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + statuses: + - OK +config: + path: /dev/null + )EOF"; + + const InstanceSharedPtr log = + AccessLogFactory::fromProto(parseAccessLogFromV2Yaml(yaml), context_); + + response_trailers_.addCopy(Http::Headers::get().GrpcStatus, "1"); + + EXPECT_CALL(*file_, write(_)).Times(0); + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); +} + +TEST_F(AccessLogImplTest, GrpcStatusFilterHttpCodes) { + const std::string yaml_template = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + statuses: + - {} +config: + path: /dev/null +)EOF"; + + // This mapping includes UNKNOWN <-> 200 because we expect that gRPC should provide an explicit + // status code for successes. In general, the only status codes that receive an HTTP mapping are + // those enumerated below with a non-UNKNOWN mapping. See: //source/common/grpc/status.cc and + // https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. + const std::vector> statusMapping = { + {"UNKNOWN", 200}, {"INTERNAL", 400}, {"UNAUTHENTICATED", 401}, + {"PERMISSION_DENIED", 403}, {"UNAVAILABLE", 429}, {"UNIMPLEMENTED", 404}, + {"UNAVAILABLE", 502}, {"UNAVAILABLE", 503}, {"UNAVAILABLE", 504}}; + + for (const auto& pair : statusMapping) { + stream_info_.response_code_ = pair.second; + + const InstanceSharedPtr log = AccessLogFactory::fromProto( + parseAccessLogFromV2Yaml(fmt::format(yaml_template, pair.first)), context_); + + EXPECT_CALL(*file_, write(_)); + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); + } +} + +TEST_F(AccessLogImplTest, GrpcStatusFilterNoCode) { + const std::string yaml = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + statuses: + - UNKNOWN +config: + path: /dev/null + )EOF"; + + const InstanceSharedPtr log = + AccessLogFactory::fromProto(parseAccessLogFromV2Yaml(yaml), context_); + + EXPECT_CALL(*file_, write(_)); + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); +} + +TEST_F(AccessLogImplTest, GrpcStatusFilterExclude) { + const std::string yaml = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + exclude: true + statuses: + - OK +config: + path: /dev/null + )EOF"; + + const InstanceSharedPtr log = + AccessLogFactory::fromProto(parseAccessLogFromV2Yaml(yaml), context_); + + for (int i = 0; i <= static_cast(Grpc::Status::GrpcStatus::MaximumValid); i++) { + EXPECT_CALL(*file_, write(_)).Times(i == 0 ? 0 : 1); + + response_trailers_.addCopy(Http::Headers::get().GrpcStatus, std::to_string(i)); + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); + response_trailers_.remove(Http::Headers::get().GrpcStatus); + } +} + +TEST_F(AccessLogImplTest, GrpcStatusFilterExcludeFalse) { + const std::string yaml = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + exclude: false + statuses: + - OK +config: + path: /dev/null + )EOF"; + + const InstanceSharedPtr log = + AccessLogFactory::fromProto(parseAccessLogFromV2Yaml(yaml), context_); + + response_trailers_.addCopy(Http::Headers::get().GrpcStatus, "0"); + + EXPECT_CALL(*file_, write(_)); + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); +} + +TEST_F(AccessLogImplTest, GrpcStatusFilterHeader) { + const std::string yaml = R"EOF( +name: envoy.file_access_log +filter: + grpc_status_filter: + statuses: + - OK +config: + path: /dev/null + )EOF"; + + const InstanceSharedPtr log = + AccessLogFactory::fromProto(parseAccessLogFromV2Yaml(yaml), context_); + + EXPECT_CALL(*file_, write(_)); + + response_headers_.addCopy(Http::Headers::get().GrpcStatus, "0"); + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); +} + } // namespace } // namespace AccessLog } // namespace Envoy diff --git a/test/extensions/access_loggers/http_grpc/grpc_access_log_impl_test.cc b/test/extensions/access_loggers/http_grpc/grpc_access_log_impl_test.cc index 9c45957b62287..ddeff95213508 100644 --- a/test/extensions/access_loggers/http_grpc/grpc_access_log_impl_test.cc +++ b/test/extensions/access_loggers/http_grpc/grpc_access_log_impl_test.cc @@ -115,7 +115,7 @@ class MockGrpcAccessLogStreamer : public GrpcAccessLogStreamer { class HttpGrpcAccessLogTest : public TestBase { public: void init() { - ON_CALL(*filter_, evaluate(_, _)).WillByDefault(Return(true)); + ON_CALL(*filter_, evaluate(_, _, _, _)).WillByDefault(Return(true)); config_.mutable_common_config()->set_log_name("hello_log"); access_log_ = std::make_unique(AccessLog::FilterPtr{filter_}, config_, streamer_); diff --git a/test/mocks/access_log/mocks.h b/test/mocks/access_log/mocks.h index c96aee7c84289..cefd0c0c5435b 100644 --- a/test/mocks/access_log/mocks.h +++ b/test/mocks/access_log/mocks.h @@ -18,8 +18,10 @@ class MockFilter : public Filter { ~MockFilter(); // AccessLog::Filter - MOCK_METHOD2(evaluate, - bool(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers)); + MOCK_METHOD4(evaluate, + bool(const StreamInfo::StreamInfo& info, const Http::HeaderMap& request_headers, + const Http::HeaderMap& response_headers, + const Http::HeaderMap& response_trailers)); }; class MockAccessLogManager : public AccessLogManager {