diff --git a/source/extensions/quic_listeners/quiche/platform/quic_server_stats_impl.h b/source/extensions/quic_listeners/quiche/platform/quic_server_stats_impl.h index 0d57ebd2809a1..d15806bd4def8 100644 --- a/source/extensions/quic_listeners/quiche/platform/quic_server_stats_impl.h +++ b/source/extensions/quic_listeners/quiche/platform/quic_server_stats_impl.h @@ -6,18 +6,184 @@ // consumed or referenced directly by other Envoy code. It serves purely as a // porting layer for QUICHE. -#define QUIC_SERVER_HISTOGRAM_ENUM_IMPL(name, sample, enum_size, docstring) \ +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +#define HISTOGRAM_UNIT_UNSPECIFIED Envoy::Stats::Histogram::Unit::Unspecified +#define HISTOGRAM_UNIT_MILLISECONDS Envoy::Stats::Histogram::Unit::Milliseconds +#define GAUGE_MODE_NEVER_IMPORT Envoy::Stats::Gauge::ImportMode::NeverImport + +// A full list of QUICHE stats names which is synchoronized with QUICHE. +// New stats needs to be added to the list otherwise the build will complain +// about undefined variable. +// Deprecated stats nees to be removed from the list otherwise clang will +// trigger the Wunused-member-function warning. +// If a QUICHE stats name doesn't contain '.', it can be declared as one of +// HISTOGRAM, GAUGE, COUNTER and ENUM_HISTOGRAM. The format for such type is +// TYPE(name_in_quiche, [other_param_for_envoy_stats,] "name_in_envoy") +// If a QIUCHE stats name contains '.', it need to be declared as nested STRUCT, one layer for +// each token before '.', and the last token as one of HISTOGRAM, GAUGE, COUNTER and ENUM_HISTOGRAM. +// The format for stats name "prefix1.prefix2.prefix3.token" looks like STRUCT(prefix1, +// STRUCT(prefix2, STRUCT(prefix3, TYPE(token, [other_param_for_envoy_stats,] "name_in_envoy")))) +// Two QUICHE stats names can share common prefix in which case the different +// parts after the common prefix are wrapped in the same STRUCT, i.e. +// For histogram names "prefix1.token1" and "prefix1.token2" are like +// STRUCT(prefix1, HISTOGRAM(token1, HISTOGRAM_UNIT_UNSPECIFIED, "unique_name1") +// HISTOGRAM(token2, HISTOGRAM_UNIT_MILLISECONDS, "unique_name2")) +#define QUICHE_STATS(COUNTER, GAUGE, HISTOGRAM, ENUM_HISTOGRAM, STRUCT) \ + HISTOGRAM(test_latency, HISTOGRAM_UNIT_MILLISECONDS, "test_latency") \ + STRUCT(test_trial, HISTOGRAM(success, HISTOGRAM_UNIT_UNSPECIFIED, "test_trial.success") \ + STRUCT(failure, ENUM_HISTOGRAM(reason, "test_trial.failure.reason")) \ + HISTOGRAM(cancel, HISTOGRAM_UNIT_UNSPECIFIED, "test_trial.cancel")) \ + STRUCT(test_session, STRUCT(a, STRUCT(b, STRUCT(c, HISTOGRAM(d, HISTOGRAM_UNIT_UNSPECIFIED, \ + "test_session.a.b.c.d"))))) \ + /* To be added when QUICHE interfaceis updated \ + COUNTER(test_counter , "quiche_test_counter") \ + GAUGE(test_gauge , GAUGE_MODE_NEVER_IMPORT, "quiche_test_gauge") \ + HISTOGRAM(quic_server_num_written_packets_per_write, HISTOGRAM_UNIT_UNSPECIFIED, \ + "quic_server_num_written_packets_per_write") \ + ENUM_HISTOGRAM(quic_server_connection_close_errors, "quic_server_connection_close_errors") \ + ENUM_HISTOGRAM(quic_client_connection_close_errors, "quic_client_connection_close_errors") \ + */ + +using CounterMap = absl::flat_hash_map>; + +#define STRUCT_MEMBER_HISTOGRAM(a, other_param, name_str) \ + Envoy::Stats::Histogram& a; \ + Envoy::Stats::Histogram& a##_() { return a; } +#define STRUCT_MEMBER_COUNTER(a, name_str) \ + Envoy::Stats::Counter& a; \ + Envoy::Stats::Counter& a##_() { return a; } +#define STRUCT_MEMBER_GAUGE(a, other_param, name_str) \ + Envoy::Stats::Gauge& a; \ + Envoy::Stats::Gauge& a##_() { return a; } +// Used to support QUIC_SERVER_HISTOGRAM_ENUM. Envoy histogram is not designed +// for collecting enum values. Because the bucketization is hidden from the interface. +// A walk around is to use a group of couters mapped by enum value. Each value +// mapped counter is not instantiated till it is used. +#define STRUCT_MEMBER_ENUM_HISTOGRAM(a, name_str) \ + CounterMap a; \ + CounterMap& a##_() { return a; } +// Used to support '.' concatinated stats name, i.e. QUIC_SERVER_HISTOGRAM_BOOL(a.b.c.d, false, ...) +#define STRUCT_MEMBER_STRUCT(a, X) \ + struct a##_t { \ + X \ + } a; + +#define EMPTY_MAP(args...) \ + {} + +#define INIT_STATS_HISTOGRAM(a, other_param, name_str) \ + scope.histogramFromString("quiche." name_str, other_param), +#define INIT_STATS_COUNTER(a, name_str) scope.counterFromString("quiche." name_str), +#define INIT_STATS_GAUGE(a, other_param, name_str) \ + scope.gaugeFromString("quiche." name_str, other_param), +#define INIT_STATS_ENUM_HISTOGRAM(a, name_str) EMPTY_MAP(a, name_str), +#define INIT_STATS_STRUCT(name, X...) {X}, + +namespace { + +// In order to detect deprecated stats, these entries need to be declared in a struct as public +// fields with corresponding getters. And they should be only accessed via their getters as if they +// are private field. The reason for them to be public is for the accessibility of stats like +// "a.b.c.d". When a stats is removed from QUICHE code, its getter is left unused. And this struct +// must be declared in an annonymous namespace. This struct looks like: +// struct QuicheStats { +// // QUIC_SERVER_HISTOGRAM_BOOL(a.b.c.d, ...) +// // QUIC_SERVER_HISTOGRAM_ENUM(a.b.c.e, ...) +// struct a_t{ +// struct b_t{ +// struct c_t{ +// Envoy::Stats::Metric& d; +// Envoy::Stats::Metric& d_() { return d; } +// EnumMap e; +// EnumMap& e_() { return e; } +// } c; +// } b; +// } a; +// +// // QUIC_SERVER_HISTOGRAM_COUNTER(aaa, ...) +// Envoy::Stats::Histogram& aaa; +// Envoy::Stats::Histogram& aaa_() { return aaa; } +// +// // QUIC_SERVER_HISTOGRAM_ENUM(bbb, ...) +// // Counters are not instantiated until incremented. +// EnumMap bbb; +// EnumMap& bbb_() { return bbb; } +// ... +// }; +struct QuicheStats { + QUICHE_STATS(STRUCT_MEMBER_COUNTER, STRUCT_MEMBER_GAUGE, STRUCT_MEMBER_HISTOGRAM, + STRUCT_MEMBER_ENUM_HISTOGRAM, STRUCT_MEMBER_STRUCT) +}; + +} // namespace + +class QuicStatsImpl { +public: + void initializeStats(Envoy::Stats::Scope& scope) { + scope_ = &scope; + std::string prefix("quiche"); + stats_.reset( + new QuicheStats{// // QUIC_SERVER_HISTOGRAM_COUNTER(aaa, ...) + // scope.histogram("quiche.aaa", ...), + // + // // QUIC_SERVER_HISTOGRAM_ENUM(bbb, ...) + // {}, + // + // // QUIC_SERVER_HISTOGRAM_ENUM(a.b.c.d, ...) + // // QUIC_SERVER_HISTOGRAM_ENUM(a.b.c.e, ...) + // {{{ {},{} }}}, + QUICHE_STATS(INIT_STATS_COUNTER, INIT_STATS_GAUGE, INIT_STATS_HISTOGRAM, + INIT_STATS_ENUM_HISTOGRAM, INIT_STATS_STRUCT)}); + } + + void resetForTest() { + stats_.reset(); + scope_ = nullptr; + } + + Envoy::Stats::Counter& createCounter(std::string name) { return scope_->counterFromString(name); } + + QuicheStats& stats() { + RELEASE_ASSERT(stats_ != nullptr, "Quiche stats is not initialized_."); + return *stats_; + } + +private: + Envoy::Stats::Scope* scope_; + std::unique_ptr stats_; +}; + +QuicStatsImpl& getQuicheStats() { + // Each worker thread has its own set of references to counters, gauges and + // histograms. + static thread_local QuicStatsImpl stats_impl = QuicStatsImpl(); + return stats_impl; +} + +#define QUIC_SERVER_HISTOGRAM_ENUM_IMPL(enum_name, sample, enum_size, docstring) \ do { \ + CounterMap& enum_map = getQuicheStats().stats().enum_name##_(); \ + uint32_t key = static_cast(sample); \ + auto it = enum_map.find(key); \ + if (it == enum_map.end()) { \ + auto result = enum_map.emplace( \ + key, getQuicheStats().createCounter(absl::StrCat("quiche." #enum_name ".", sample))); \ + ASSERT(result.second); \ + it = result.first; \ + } \ + it->second.get().inc(); \ } while (0) #define QUIC_SERVER_HISTOGRAM_BOOL_IMPL(name, sample, docstring) \ - do { \ - } while (0) + QUIC_SERVER_HISTOGRAM_TIMES_IMPL(name, sample, 0, 1, 2, docstring) -#define QUIC_SERVER_HISTOGRAM_TIMES_IMPL(name, sample, min, max, bucket_count, docstring) \ +#define QUIC_SERVER_HISTOGRAM_TIMES_IMPL(time_name, sample, min, max, bucket_count, docstring) \ do { \ + static_cast(getQuicheStats().stats().time_name##_()) \ + .recordValue(sample); \ } while (0) #define QUIC_SERVER_HISTOGRAM_COUNTS_IMPL(name, sample, min, max, bucket_count, docstring) \ - do { \ - } while (0) + QUIC_SERVER_HISTOGRAM_TIMES_IMPL(name, sample, min, max, bucket_count, docstring) diff --git a/test/extensions/quic_listeners/quiche/platform/BUILD b/test/extensions/quic_listeners/quiche/platform/BUILD index ced7bf94658d7..4e099a16f46d4 100644 --- a/test/extensions/quic_listeners/quiche/platform/BUILD +++ b/test/extensions/quic_listeners/quiche/platform/BUILD @@ -37,18 +37,24 @@ envoy_cc_test( "//bazel:linux": ["quic_platform_test.cc"], "//conditions:default": [], }), - copts = ["-Wno-unused-parameter"], + copts = [ + "-Wno-unused-parameter", + "-Wunused-member-function", + ], data = ["//test/extensions/transport_sockets/tls/test_data:certs"], external_deps = ["quiche_quic_platform"], tags = ["nofips"], deps = [ ":quic_platform_epoll_clock_lib", "//source/common/memory:stats_lib", + "//source/common/stats:thread_local_store_lib", "//source/extensions/quic_listeners/quiche/platform:flags_impl_lib", "//test/common/buffer:utility_lib", "//test/common/stats:stat_test_utility_lib", "//test/extensions/transport_sockets/tls:ssl_test_utils", "//test/mocks/api:api_mocks", + "//test/mocks/event:event_mocks", + "//test/mocks/thread_local:thread_local_mocks", "//test/test_common:logging_lib", "//test/test_common:threadsafe_singleton_injector_lib", "//test/test_common:utility_lib", diff --git a/test/extensions/quic_listeners/quiche/platform/quic_platform_test.cc b/test/extensions/quic_listeners/quiche/platform/quic_platform_test.cc index d15743831a8aa..d1ac5f1c38d0d 100644 --- a/test/extensions/quic_listeners/quiche/platform/quic_platform_test.cc +++ b/test/extensions/quic_listeners/quiche/platform/quic_platform_test.cc @@ -11,6 +11,8 @@ #include "common/memory/stats.h" #include "common/network/utility.h" +#include "common/stats/symbol_table_impl.h" +#include "common/stats/thread_local_store.h" #include "extensions/quic_listeners/quiche/platform/flags_impl.h" @@ -19,6 +21,8 @@ #include "test/extensions/quic_listeners/quiche/platform/quic_epoll_clock.h" #include "test/extensions/transport_sockets/tls/ssl_test_utility.h" #include "test/mocks/api/mocks.h" +#include "test/mocks/event/mocks.h" +#include "test/mocks/thread_local/mocks.h" #include "test/test_common/environment.h" #include "test/test_common/logging.h" #include "test/test_common/network_utility.h" @@ -70,19 +74,30 @@ using testing::_; using testing::HasSubstr; +using testing::NiceMock; +using testing::Ref; using testing::Return; namespace quic { namespace { - class QuicPlatformTest : public testing::Test { protected: QuicPlatformTest() - : log_level_(GetLogger().level()), verbosity_log_threshold_(GetVerbosityLogThreshold()) { + : log_level_(GetLogger().level()), verbosity_log_threshold_(GetVerbosityLogThreshold()), + symbol_table_(Envoy::Stats::SymbolTableCreator::makeSymbolTable()), alloc_(*symbol_table_), + store_(std::make_unique(alloc_)) { SetVerbosityLogThreshold(0); GetLogger().set_level(ERROR); + store_->addSink(sink_); + } + + void SetUp() override { + QuicStatsImpl& stats = getQuicheStats(); + stats.initializeStats(*store_); } + void TearDown() override { getQuicheStats().resetForTest(); } + ~QuicPlatformTest() override { SetVerbosityLogThreshold(verbosity_log_threshold_); GetLogger().set_level(log_level_); @@ -90,6 +105,12 @@ class QuicPlatformTest : public testing::Test { const QuicLogLevel log_level_; const int verbosity_log_threshold_; + Envoy::Stats::SymbolTablePtr symbol_table_; + NiceMock main_thread_dispatcher_; + NiceMock tls_; + Envoy::Stats::AllocatorImpl alloc_; + Envoy::Stats::MockSink sink_; + std::unique_ptr store_; }; TEST_F(QuicPlatformTest, QuicAlignOf) { EXPECT_LT(0, QUIC_ALIGN_OF(int)); } @@ -133,13 +154,14 @@ TEST_F(QuicPlatformTest, QuicExpectBug) { } TEST_F(QuicPlatformTest, QuicExportedStats) { - // Just make sure they compile. + /* QUIC_HISTOGRAM_ENUM("my.enum.histogram", TestEnum::ONE, TestEnum::COUNT, "doc"); QUIC_HISTOGRAM_BOOL("my.bool.histogram", false, "doc"); QUIC_HISTOGRAM_TIMES("my.timing.histogram", QuicTime::Delta::FromSeconds(5), QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSecond(3600), 100, "doc"); QUIC_HISTOGRAM_COUNTS("my.count.histogram", 123, 0, 1000, 100, "doc"); + */ } TEST_F(QuicPlatformTest, QuicHostnameUtils) { @@ -229,13 +251,53 @@ TEST_F(QuicPlatformTest, QuicMockLog) { } TEST_F(QuicPlatformTest, QuicServerStats) { - // Just make sure they compile. - QUIC_SERVER_HISTOGRAM_ENUM("my.enum.histogram", TestEnum::ONE, TestEnum::COUNT, "doc"); - QUIC_SERVER_HISTOGRAM_BOOL("my.bool.histogram", false, "doc"); - QUIC_SERVER_HISTOGRAM_TIMES("my.timing.histogram", QuicTime::Delta::FromSeconds(5), - QuicTime::Delta::FromSeconds(1), QuicTime::Delta::FromSecond(3600), - 100, "doc"); - QUIC_SERVER_HISTOGRAM_COUNTS("my.count.histogram", 123, 0, 1000, 100, "doc"); + Envoy::Stats::Histogram& h1 = store_->histogramFromString( + "quiche.test_latency", Envoy::Stats::Histogram::Unit::Milliseconds); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h1), 100)); + QUIC_SERVER_HISTOGRAM_TIMES(test_latency, 100, 0, 500, 10, "bbb"); + + Envoy::Stats::Histogram& h2 = store_->histogramFromString( + "quiche.test_trial.success", Envoy::Stats::Histogram::Unit::Unspecified); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h2), 0)); + QUIC_SERVER_HISTOGRAM_BOOL(test_trial.success, false, "just for test"); + + enum FailureReason { + Reason1 = 0, + Reason2, + }; + size_t num_counters = store_->counters().size(); + QUIC_SERVER_HISTOGRAM_ENUM(test_trial.failure.reason, Reason1, 2, "doc"); + EXPECT_EQ(num_counters + 1, store_->counters().size()); + Envoy::Stats::Counter& c1 = + store_->counterFromString(absl::StrCat("quiche.test_trial.failure.reason.", Reason1)); + EXPECT_EQ(1u, c1.value()); + + Envoy::Stats::Histogram& h3 = store_->histogramFromString( + "quiche.test_trial.cancel", Envoy::Stats::Histogram::Unit::Unspecified); + EXPECT_FALSE(h3.used()); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h3), 5)); + QUIC_SERVER_HISTOGRAM_COUNTS(test_trial.cancel, 5, 0, 10, 10, "aaaa"); + + Envoy::Stats::Histogram& h4 = store_->histogramFromString( + "quiche.test_session.a.b.c.d", Envoy::Stats::Histogram::Unit::Unspecified); + EXPECT_FALSE(h4.used()); + EXPECT_CALL(sink_, onHistogramComplete(Ref(h4), 16)); + QUIC_SERVER_HISTOGRAM_COUNTS(test_session.a.b.c.d, 16, 0, 100, 10, "bbbb"); + + /* + Envoy::Stats::Histogram& h1 = + store_->histogram("quiche.quic_server_num_written_packets_per_write", + Envoy::Stats::Histogram::Unit::Unspecified); EXPECT_FALSE(h1.used()); EXPECT_CALL(sink_, + onHistogramComplete(Ref(h1), 16)); + QUIC_SERVER_HISTOGRAM_COUNTS(quic_server_num_written_packets_per_write, 16, 0, 100, 10, "aaaa"); + + size_t num_counters = store_->counters().size(); + QUIC_SERVER_HISTOGRAM_ENUM(quic_server_connection_close_errors, QUIC_INTERNAL_ERROR, + QUIC_LAST_ERROR, "doc"); EXPECT_EQ(num_counters + 1, store_->counters().size()); + Envoy::Stats::Counter& c1 = + store_->counter(absl::StrCat("quiche.quic_server_connection_close_errors.", QUIC_INTERNAL_ERROR)); + EXPECT_EQ(1u, c1.value()); + */ } TEST_F(QuicPlatformTest, QuicStackTraceTest) {