Skip to content

Commit

Permalink
Add quic_stats
Browse files Browse the repository at this point in the history
Signed-off-by: Greg Greenway <ggreenway@apple.com>
  • Loading branch information
ggreenway committed Oct 24, 2024
1 parent 45e083b commit ba7896f
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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 = ["@com_github_cncf_xds//udpa/annotations:pkg"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
syntax = "proto3";

package envoy.extensions.quic.connection_debug_visitor.quic_stats.v3;

import "google/protobuf/duration.proto";

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

option java_package = "io.envoyproxy.envoy.extensions.quic.connection_debug_visitor.quic_stats.v3";
option java_outer_classname = "QuicStatsProto";
option java_multiple_files = true;
option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/quic/connection_debug_visitor/quic_stats/v3;quic_statsv3";
option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: QUIC stats config]
// [#extension: envoy.quic.connection_debug_visitor.quic_stats]

// Configuration for a basic QUIC connection debug visitor.
message Config {
// Period to update stats while the connection is open. If unset, updates only happen when the
// connection is closed. Stats are always updated one final time when the connection is closed.
google.protobuf.Duration update_period = 2 [(validate.rules).duration = {gte {nanos: 1000000}}];
}
1 change: 1 addition & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ EXTENSIONS = {
"envoy.quic.server_preferred_address.fixed": "//source/extensions/quic/server_preferred_address:fixed_server_preferred_address_config_factory_config",
"envoy.quic.server_preferred_address.datasource": "//source/extensions/quic/server_preferred_address:datasource_server_preferred_address_config_factory_config",
"envoy.quic.connection_debug_visitor.basic": "//source/extensions/quic/connection_debug_visitor/basic:envoy_quic_connection_debug_visitor_basic",
"envoy.quic.connection_debug_visitor.quic_stats": "//source/extensions/quic/connection_debug_visitor/quic_stats:config",

#
# UDP packet writers
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 @@ -1813,6 +1813,13 @@ envoy.quic.connection_debug_visitor.basic:
status: alpha
type_urls:
- envoy.extensions.quic.connection_debug_visitor.v3.BasicConfig
envoy.quic.connection_debug_visitor.quic_stats:
categories:
- envoy.quic.connection_debug_visitor
security_posture: robust_to_untrusted_downstream_and_upstream
status: alpha
type_urls:
- envoy.extensions.quic.connection_debug_visitor.quic_stats.v3.Config
envoy.filters.network.generic_proxy:
categories:
- envoy.filters.network
Expand Down
49 changes: 49 additions & 0 deletions source/extensions/quic/connection_debug_visitor/quic_stats/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
load(
"@envoy_build_config//:extensions_build_config.bzl",
"LEGACY_ALWAYSLINK",
)
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_extension",
"envoy_cc_library",
"envoy_extension_package",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_library(
name = "quic_stats_lib",
srcs = ["quic_stats.cc"],
hdrs = ["quic_stats.h"],
tags = ["nofips"],
visibility = [
"//test:__subpackages__",
],
deps = [
"//envoy/registry",
"//source/common/protobuf:utility_lib",
"//source/common/quic:envoy_quic_connection_debug_visitor_factory_interface",
"@envoy_api//envoy/extensions/quic/connection_debug_visitor/quic_stats/v3:pkg_cc_proto",
],
alwayslink = LEGACY_ALWAYSLINK,
)

envoy_cc_extension(
name = "config",
extra_visibility = [
"//source/common/quic:__subpackages__",
"//test:__subpackages__",
],
tags = ["nofips"],
deps = select(
{
"//bazel:boringssl_fips": [],
"//bazel:boringssl_disabled": [],
"//conditions:default": [
":quic_stats_lib",
],
},
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#include "source/extensions/quic/connection_debug_visitor/quic_stats/quic_stats.h"

#include <memory>

#include "envoy/stream_info/stream_info.h"

#include "source/common/common/logger.h"
#include "source/common/quic/envoy_quic_connection_debug_visitor_factory_interface.h"

#include "quiche/quic/core/frames/quic_connection_close_frame.h"
#include "quiche/quic/core/quic_session.h"
#include "quiche/quic/core/quic_types.h"

namespace Envoy {
namespace Extensions {
namespace Quic {
namespace ConnectionDebugVisitors {
namespace QuicStats {

QuicStatsVisitor::QuicStatsVisitor(Config& config, Event::Dispatcher& dispatcher,
quic::QuicSession* session)
: config_(config), session_(*session) {
ASSERT(session != nullptr);
if (config_.update_period_.has_value()) {
timer_ = dispatcher.createTimer([this]() {
recordStats();
timer_->enableTimer(config_.update_period_.value());
});
timer_->enableTimer(config_.update_period_.value());
}
}

void QuicStatsVisitor::OnConnectionClosed(const quic::QuicConnectionCloseFrame&,
quic::ConnectionCloseSource) {
if (timer_ != nullptr) {
timer_->disableTimer();
}
recordStats();
}

const quic::QuicConnectionStats& QuicStatsVisitor::getQuicStats() {
return session_.connection()->GetStats();
}

void QuicStatsVisitor::recordStats() {
const quic::QuicConnectionStats& quic_stats = getQuicStats();

auto update_counter = [](Stats::Counter& counter, auto& last_value, auto current_value) {
int64_t diff = static_cast<int64_t>(current_value) - static_cast<int64_t>(last_value);
ASSERT(diff >= 0);
if (diff > 0) {
counter.add(diff);
}
last_value = current_value;
};

// This is before the update to `last_packets_sent_` and `last_packets_retransmitted_` because
// they use the same metrics, and `update_counter` will update `last_...`, so this needs to use
// those `last_...` values (and not update them) first.
//
// Don't record a value if the numerator is negative, or the denominator is zero or negative
// (prevent divide-by-zero).
if ((quic_stats.packets_sent > last_packets_sent_) &&
(quic_stats.packets_retransmitted >= last_packets_retransmitted_)) {
const uint64_t pkts_diff = quic_stats.packets_sent - last_packets_sent_;
const uint64_t retrans_diff = quic_stats.packets_retransmitted - last_packets_retransmitted_;

// The following math will not overflow unless the number of packets seen is well over 10
// trillion. In that case, the value recorded in the histogram may be incorrect. This case is
// left unhandled for performance, and it is extremely unlikely.
constexpr uint64_t max_supported = UINT64_MAX / Stats::Histogram::PercentScale;
constexpr uint64_t ten_trillion = (10ULL * 1000ULL * 1000ULL * 1000ULL * 1000ULL);
static_assert(max_supported > ten_trillion);
ASSERT(retrans_diff < max_supported);

const uint64_t percent_retransmissions =
(retrans_diff * static_cast<uint64_t>(Stats::Histogram::PercentScale)) / pkts_diff;
config_.stats_.cx_tx_percent_retransmitted_pkts_.recordValue(percent_retransmissions);
}

update_counter(config_.stats_.cx_tx_packets_total_, last_packets_sent_, quic_stats.packets_sent);
update_counter(config_.stats_.cx_packets_retransmitted_total_, last_packets_retransmitted_,
quic_stats.packets_retransmitted);
update_counter(config_.stats_.cx_amplification_throttling_total_,
last_num_amplification_throttling_, quic_stats.num_amplification_throttling);
update_counter(config_.stats_.cx_path_degrading_total_, last_num_path_degrading_,
quic_stats.num_path_degrading);
update_counter(config_.stats_.cx_forward_progress_after_path_degrading_total_,
last_num_forward_progress_after_path_degrading_,
quic_stats.num_forward_progress_after_path_degrading);

if (quic_stats.srtt_us > 0) {
config_.stats_.cx_srtt_us_.recordValue(quic_stats.srtt_us);
}
if (!quic_stats.estimated_bandwidth.IsZero() && !quic_stats.estimated_bandwidth.IsInfinite()) {
config_.stats_.cx_tx_estimated_bandwidth_bytes_per_second_.recordValue(
quic_stats.estimated_bandwidth.ToBytesPerPeriod(quic::QuicTime::Delta::FromSeconds(1)));
}
}

Config::Config(
const envoy::extensions::quic::connection_debug_visitor::quic_stats::v3::Config& config,
Server::Configuration::ListenerFactoryContext& listener_context)
: update_period_(PROTOBUF_GET_OPTIONAL_MS(config, update_period)),
stats_(generateStats(listener_context.listenerScope())) {}

QuicStats Config::generateStats(Stats::Scope& scope) {
constexpr absl::string_view prefix("quic_stats");
return QuicStats{ALL_QUIC_STATS(POOL_COUNTER_PREFIX(scope, prefix),
POOL_GAUGE_PREFIX(scope, prefix),
POOL_HISTOGRAM_PREFIX(scope, prefix))};
}

std::unique_ptr<quic::QuicConnectionDebugVisitor>
Config::createQuicConnectionDebugVisitor(Event::Dispatcher& dispatcher, quic::QuicSession* session,
const StreamInfo::StreamInfo&) {
return std::make_unique<QuicStatsVisitor>(*this, dispatcher, session);
}

Envoy::Quic::EnvoyQuicConnectionDebugVisitorFactoryInterfacePtr
QuicStatsFactoryFactory::createFactory(
const Protobuf::Message& config,
Server::Configuration::ListenerFactoryContext& server_context) {
return std::make_unique<Config>(
dynamic_cast<
const envoy::extensions::quic::connection_debug_visitor::quic_stats::v3::Config&>(config),
server_context);
}

REGISTER_FACTORY(QuicStatsFactoryFactory,
Envoy::Quic::EnvoyQuicConnectionDebugVisitorFactoryFactoryInterface);

} // namespace QuicStats
} // namespace ConnectionDebugVisitors
} // namespace Quic
} // namespace Extensions
} // namespace Envoy
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#pragma once

#include <memory>
#include <string>

#include "envoy/extensions/quic/connection_debug_visitor/quic_stats/v3/quic_stats.pb.h"
#include "envoy/registry/registry.h"

#include "source/common/protobuf/protobuf.h"
#include "source/common/quic/envoy_quic_connection_debug_visitor_factory_interface.h"

#include "quiche/quic/core/frames/quic_connection_close_frame.h"
#include "quiche/quic/core/quic_connection.h"
#include "quiche/quic/core/quic_session.h"
#include "quiche/quic/core/quic_types.h"

namespace Envoy {
namespace Extensions {
namespace Quic {
namespace ConnectionDebugVisitors {
namespace QuicStats {

#define ALL_QUIC_STATS(COUNTER, GAUGE, HISTOGRAM) \
COUNTER(cx_tx_packets_total) \
COUNTER(cx_packets_retransmitted_total) \
COUNTER(cx_amplification_throttling_total) \
COUNTER(cx_path_degrading_total) \
COUNTER(cx_forward_progress_after_path_degrading_total) \
HISTOGRAM(cx_tx_percent_retransmitted_pkts, Percent) \
HISTOGRAM(cx_srtt_us, Microseconds) \
HISTOGRAM(cx_tx_estimated_bandwidth_bytes_per_second, Bytes)

struct QuicStats {
ALL_QUIC_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, GENERATE_HISTOGRAM_STRUCT)
};

class Config;

// Visitor class that writes connection-level information to info logs.
class QuicStatsVisitor : public quic::QuicConnectionDebugVisitor {
public:
QuicStatsVisitor(Config& config, Event::Dispatcher& dispatcher, quic::QuicSession* session);

// quic::QuicConnectionDebugVisitor
void OnConnectionClosed(const quic::QuicConnectionCloseFrame& frame,
quic::ConnectionCloseSource source) override;

// This is virtual so that tests can override it.
virtual const quic::QuicConnectionStats& getQuicStats();

private:
void recordStats();

Config& config_;
Event::TimerPtr timer_;
quic::QuicSession& session_;

quic::QuicPacketCount last_packets_sent_{};
quic::QuicPacketCount last_packets_retransmitted_{};
size_t last_num_amplification_throttling_{};
size_t last_num_path_degrading_{};
size_t last_num_forward_progress_after_path_degrading_{};
};

class Config : public Envoy::Quic::EnvoyQuicConnectionDebugVisitorFactoryInterface {
public:
Config(const envoy::extensions::quic::connection_debug_visitor::quic_stats::v3::Config& config,
Server::Configuration::ListenerFactoryContext& listener_context);

std::unique_ptr<quic::QuicConnectionDebugVisitor>
createQuicConnectionDebugVisitor(Event::Dispatcher& dispatcher, quic::QuicSession* session,
const StreamInfo::StreamInfo& stream_info) override;

const absl::optional<std::chrono::milliseconds> update_period_;
QuicStats stats_;

private:
static QuicStats generateStats(Stats::Scope& scope);
};

class QuicStatsFactoryFactory
: public Envoy::Quic::EnvoyQuicConnectionDebugVisitorFactoryFactoryInterface {
public:
std::string name() const override { return "envoy.quic.connection_debug_visitor.quic_stats"; }

Envoy::ProtobufTypes::MessagePtr createEmptyConfigProto() override {
return std::make_unique<
envoy::extensions::quic::connection_debug_visitor::quic_stats::v3::Config>();
}

Envoy::Quic::EnvoyQuicConnectionDebugVisitorFactoryInterfacePtr
createFactory(const Protobuf::Message& config,
Server::Configuration::ListenerFactoryContext& listener_context) override;
};

DECLARE_FACTORY(QuicStatsFactoryFactory);

} // namespace QuicStats
} // namespace ConnectionDebugVisitors
} // namespace Quic
} // namespace Extensions
} // namespace Envoy
31 changes: 31 additions & 0 deletions test/extensions/quic/connection_debug_visitor/quic_stats/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_test",
"envoy_package",
)

licenses(["notice"]) # Apache 2

envoy_package()

envoy_cc_test(
name = "quic_stats_test",
srcs = ["quic_stats_test.cc"],
deps = [
"//source/extensions/quic/connection_debug_visitor/quic_stats:quic_stats_lib",
"//test/mocks/event:event_mocks",
"//test/mocks/server:listener_factory_context_mocks",
],
)

envoy_cc_test(
name = "integration_test",
size = "large",
srcs = ["integration_test.cc"],
data = ["//test/config/integration/certs"],
rbe_pool = "2core",
deps = [
"//source/extensions/quic/connection_debug_visitor/quic_stats:config",
"//test/integration:integration_lib",
],
)
Empty file.
Loading

0 comments on commit ba7896f

Please sign in to comment.