Skip to content
15 changes: 15 additions & 0 deletions api/envoy/config/endpoint/v3/endpoint_components.proto
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// [#protodoc-title: Endpoints]

// Upstream host identifier.
// [#next-free-field: 6]
message Endpoint {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.endpoint.Endpoint";

Expand Down Expand Up @@ -97,6 +98,20 @@ message Endpoint {
// sorted by preference order of the addresses. This will only be supported
// for STATIC and EDS clusters.
repeated AdditionalAddress additional_addresses = 4;

// Optional alternative stat name for this endpoint. If not specified, the main address will be used
// as the stat name and be extracted as ``envoy.endpoint_address`` tag value in generated stats.
// If specified, the ``observability_name`` here will be used to replace the main address.
//
// .. note::
//
// This field is ignored for logical DNS host implementation..
//
// This is useful when there are duplicate addresses in the cluster, for example when multiple
// endpoints share the same address but have different hostnames or metadata.
// In this case, the observability name can be used to differentiate between these endpoints in
// stats and logs.
string observability_name = 5;
}

// An Endpoint that Envoy can route traffic to.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added :ref:`observability_name <envoy_v3_api_field_config.endpoint.v3.Endpoint.observability_name>` to
:ref:`Endpoint <envoy_v3_api_msg_config.endpoint.v3.Endpoint>` so endpoints can override the
observability name used in per-endpoint stats. This allows clusters with duplicate
endpoint addresses to expose distinct per-endpoint stats.
9 changes: 9 additions & 0 deletions envoy/upstream/host_description.h
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ class HostDescription {
*/
virtual const std::string& hostname() const PURE;

/**
* @return the observability name associated with the host. Used in per-endpoint stats and other
* observability surfaces. This is configured with
* :ref:`stat_name <envoy_v3_api_field_config.endpoint.v3.Endpoint.stat_name>`. If this method
* returns an empty string view, then the host's address should be used as fallback for the
* observability name.
*/
virtual absl::string_view observabilityName() const PURE;

/**
* @return the transport socket factory responsible for this host.
*/
Expand Down
6 changes: 4 additions & 2 deletions source/common/upstream/health_discovery_service.cc
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,8 @@ HdsCluster::HdsCluster(Server::Configuration::ServerFactoryContext& server_conte
HostImpl::create(info_, "", std::move(address_or_error.value()), nullptr, nullptr, 1,
const_locality_shared_pool->getObject(locality_endpoints.locality()),
host.endpoint().health_check_config(), 0,
envoy::config::core::v3::UNKNOWN),
envoy::config::core::v3::UNKNOWN, {},
host.endpoint().observability_name()),
std::unique_ptr<HostImpl>));
// Add this host/endpoint pointer to our flat list of endpoints for health checking.
hosts_->push_back(endpoint);
Expand Down Expand Up @@ -512,7 +513,8 @@ void HdsCluster::updateHosts(
HostImpl::create(info_, "", std::move(address_or_error.value()), nullptr, nullptr, 1,
const_locality_shared_pool->getObject(endpoints.locality()),
endpoint.endpoint().health_check_config(), 0,
envoy::config::core::v3::UNKNOWN),
envoy::config::core::v3::UNKNOWN, {},
endpoint.endpoint().observability_name()),
std::unique_ptr<HostImpl>));

// Set the initial health status as in HdsCluster::initialize.
Expand Down
17 changes: 12 additions & 5 deletions source/common/upstream/host_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,20 @@ void HostUtility::forEachHostMetric(

for (auto& host_set : cluster.prioritySet().hostSetsPerPriority()) {
for (auto& host : host_set->hosts()) {
absl::string_view endpoint_observability_name = host->observabilityName();
Network::Address::InstanceConstSharedPtr address;
if (endpoint_observability_name.empty()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

observabilityName() returns address_->asStringView() as a fallback, so I think this condition is always false.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a special case: LogicHost, whose address may be updated dynamically. That will not support the stat_name and also won't fallback to ip:port by default.

My plan is to remove logic host in the future because current default host implementation also could support multiple addresses.

// Only logical host will have empty observability name for now.
address = host->address();
endpoint_observability_name = address->asStringView();
}
Comment thread
wbpcode marked this conversation as resolved.

Stats::TagVector tags;
tags.reserve(fixed_tags.size() + 3);
tags.insert(tags.end(), fixed_tags.begin(), fixed_tags.end());
tags.emplace_back(Stats::Tag{Envoy::Config::TagNames::get().CLUSTER_NAME, cluster_name});
tags.emplace_back(Stats::Tag{"envoy.endpoint_address", host->address()->asString()});
tags.emplace_back(
Stats::Tag{"envoy.endpoint_address", std::string(endpoint_observability_name)});

const auto& hostname = host->hostname();
if (!hostname.empty()) {
Expand All @@ -200,10 +208,9 @@ void HostUtility::forEachHostMetric(

auto set_metric_metadata = [&](absl::string_view metric_name,
Stats::PrimitiveMetricMetadata& metric) {
metric.setName(
absl::StrCat("cluster.", cluster_name, ".endpoint.",
Stats::Utility::sanitizeStatsName(host->address()->asStringView()),
".", metric_name));
metric.setName(absl::StrCat(
"cluster.", cluster_name, ".endpoint.",
Stats::Utility::sanitizeStatsName(endpoint_observability_name), ".", metric_name));
metric.setTagExtractedName(absl::StrCat("cluster.endpoint.", metric_name));
metric.setTags(tags);

Expand Down
20 changes: 11 additions & 9 deletions source/common/upstream/upstream_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -474,11 +474,11 @@ absl::StatusOr<std::unique_ptr<HostDescriptionImpl>> HostDescriptionImpl::create
MetadataConstSharedPtr locality_metadata,
std::shared_ptr<const envoy::config::core::v3::Locality> locality,
const envoy::config::endpoint::v3::Endpoint::HealthCheckConfig& health_check_config,
uint32_t priority, const AddressVector& address_list) {
uint32_t priority, const AddressVector& address_list, absl::string_view stat_name) {
absl::Status creation_status = absl::OkStatus();
auto ret = std::unique_ptr<HostDescriptionImpl>(new HostDescriptionImpl(
creation_status, cluster, hostname, dest_address, endpoint_metadata, locality_metadata,
locality, health_check_config, priority, address_list));
locality, health_check_config, priority, address_list, stat_name));
RETURN_IF_NOT_OK(creation_status);
return ret;
}
Expand All @@ -489,12 +489,13 @@ HostDescriptionImpl::HostDescriptionImpl(
MetadataConstSharedPtr locality_metadata,
std::shared_ptr<const envoy::config::core::v3::Locality> locality,
const envoy::config::endpoint::v3::Endpoint::HealthCheckConfig& health_check_config,
uint32_t priority, const AddressVector& address_list)
uint32_t priority, const AddressVector& address_list, absl::string_view stat_name)
: HostDescriptionImplBase(cluster, hostname, dest_address, endpoint_metadata, locality_metadata,
locality, health_check_config, priority, creation_status),
address_(dest_address),
address_list_or_null_(makeAddressListOrNull(dest_address, address_list)),
health_check_address_(resolveHealthCheckAddress(health_check_config, dest_address)) {}
health_check_address_(resolveHealthCheckAddress(health_check_config, dest_address)),
observability_name_(stat_name) {}

HostDescriptionImplBase::HostDescriptionImplBase(
ClusterInfoConstSharedPtr cluster, const std::string& hostname,
Expand Down Expand Up @@ -737,11 +738,12 @@ absl::StatusOr<std::unique_ptr<HostImpl>> HostImpl::create(
std::shared_ptr<const envoy::config::core::v3::Locality> locality,
const envoy::config::endpoint::v3::Endpoint::HealthCheckConfig& health_check_config,
uint32_t priority, const envoy::config::core::v3::HealthStatus health_status,
const AddressVector& address_list) {
const AddressVector& address_list, absl::string_view stat_name) {
absl::Status creation_status = absl::OkStatus();
auto ret = std::unique_ptr<HostImpl>(new HostImpl(
creation_status, cluster, hostname, address, endpoint_metadata, locality_metadata,
initial_weight, locality, health_check_config, priority, health_status, address_list));
auto ret = std::unique_ptr<HostImpl>(
new HostImpl(creation_status, cluster, hostname, address, endpoint_metadata,
locality_metadata, initial_weight, locality, health_check_config, priority,
health_status, address_list, stat_name));
RETURN_IF_NOT_OK(creation_status);
return ret;
}
Expand Down Expand Up @@ -2220,7 +2222,7 @@ void PriorityStateManager::registerHostForPriority(
lb_endpoint.load_balancing_weight().value(),
parent_.constLocalitySharedPool()->getObject(locality_lb_endpoint.locality()),
lb_endpoint.endpoint().health_check_config(), locality_lb_endpoint.priority(),
lb_endpoint.health_status(), address_list),
lb_endpoint.health_status(), address_list, lb_endpoint.endpoint().observability_name()),
std::unique_ptr<HostImpl>));
registerHostForPriority(host, locality_lb_endpoint);
}
Expand Down
19 changes: 14 additions & 5 deletions source/common/upstream/upstream_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ class HostDescriptionImpl : public HostDescriptionImplBase {
MetadataConstSharedPtr endpoint_metadata, MetadataConstSharedPtr locality_metadata,
std::shared_ptr<const envoy::config::core::v3::Locality> locality,
const envoy::config::endpoint::v3::Endpoint::HealthCheckConfig& health_check_config,
uint32_t priority, const AddressVector& address_list = {});
uint32_t priority, const AddressVector& address_list = {},
absl::string_view stat_name = {});

// HostDescription
Network::Address::InstanceConstSharedPtr address() const override { return address_; }
Expand All @@ -328,6 +329,13 @@ class HostDescriptionImpl : public HostDescriptionImplBase {
return address_;
}
SharedConstAddressVector addressListOrNull() const override { return address_list_or_null_; }
absl::string_view observabilityName() const override {
if (observability_name_.empty()) {
ASSERT(address_ != nullptr);
return address_->asStringView();
}
return observability_name_;
}

protected:
HostDescriptionImpl(
Expand All @@ -336,7 +344,7 @@ class HostDescriptionImpl : public HostDescriptionImplBase {
MetadataConstSharedPtr endpoint_metadata, MetadataConstSharedPtr locality_metadata,
std::shared_ptr<const envoy::config::core::v3::Locality> locality,
const envoy::config::endpoint::v3::Endpoint::HealthCheckConfig& health_check_config,
uint32_t priority, const AddressVector& address_list = {});
uint32_t priority, const AddressVector& address_list = {}, absl::string_view stat_name = {});

private:
// No locks are required in this implementation: all address-related member
Expand All @@ -346,6 +354,7 @@ class HostDescriptionImpl : public HostDescriptionImplBase {
const Network::Address::InstanceConstSharedPtr address_;
const SharedConstAddressVector address_list_or_null_;
const Network::Address::InstanceConstSharedPtr health_check_address_;
const std::string observability_name_;
};

/**
Expand Down Expand Up @@ -521,7 +530,7 @@ class HostImpl : public HostImplBase, public HostDescriptionImpl {
std::shared_ptr<const envoy::config::core::v3::Locality> locality,
const envoy::config::endpoint::v3::Endpoint::HealthCheckConfig& health_check_config,
uint32_t priority, const envoy::config::core::v3::HealthStatus health_status,
const AddressVector& address_list = {});
const AddressVector& address_list = {}, absl::string_view stat_name = {});

protected:
HostImpl(absl::Status& creation_status, ClusterInfoConstSharedPtr cluster,
Expand All @@ -531,11 +540,11 @@ class HostImpl : public HostImplBase, public HostDescriptionImpl {
std::shared_ptr<const envoy::config::core::v3::Locality> locality,
const envoy::config::endpoint::v3::Endpoint::HealthCheckConfig& health_check_config,
uint32_t priority, const envoy::config::core::v3::HealthStatus health_status,
const AddressVector& address_list = {})
const AddressVector& address_list = {}, absl::string_view stat_name = {})
: HostImplBase(initial_weight, health_check_config, health_status),
HostDescriptionImpl(creation_status, cluster, hostname, address, endpoint_metadata,
locality_metadata, locality, health_check_config, priority,
address_list) {}
address_list, stat_name) {}
};

class HostsPerLocalityImpl : public HostsPerLocality {
Expand Down
2 changes: 2 additions & 0 deletions source/extensions/clusters/common/logical_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class LogicalHost : public HostImplBase, public HostDescriptionImplBase {
SharedConstAddressVector addressListOrNull() const override;
Network::Address::InstanceConstSharedPtr address() const override;
Network::Address::InstanceConstSharedPtr healthCheckAddress() const override;
absl::string_view observabilityName() const override { return {}; }
Network::Address::InstanceConstSharedPtr orcaReportingAddress() const override;

protected:
Expand Down Expand Up @@ -112,6 +113,7 @@ class RealHostDescription : public HostDescription {
return logical_host_->hostnameForHealthChecks();
}
const std::string& hostname() const override { return logical_host_->hostname(); }
absl::string_view observabilityName() const override { return {}; }
Network::Address::InstanceConstSharedPtr address() const override { return address_; }
SharedConstAddressVector addressListOrNull() const override {
return logical_host_->addressListOrNull();
Expand Down
26 changes: 26 additions & 0 deletions test/common/upstream/host_utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,32 @@ TEST_F(PerEndpointMetricsTest, Tags) {
}));
}

TEST_F(PerEndpointMetricsTest, CustomEndpointStatName) {
auto& cluster = makeCluster("cluster1", 0);
auto& host = addHost(cluster);
host.observability_name_ = "shared.backend/a";

auto [counters, gauges] = run();

EXPECT_THAT(metricNamesAndValues(counters),
UnorderedElementsAreArray({
std::make_pair("cluster.cluster1.endpoint.shared.backend/a.c1", 11),
std::make_pair("cluster.cluster1.endpoint.shared.backend/a.c2", 12),
}));
EXPECT_THAT(metricNamesAndValues(gauges),
UnorderedElementsAreArray({
std::make_pair("cluster.cluster1.endpoint.shared.backend/a.g1", 13),
std::make_pair("cluster.cluster1.endpoint.shared.backend/a.g2", 14),
std::make_pair("cluster.cluster1.endpoint.shared.backend/a.healthy", 1),
}));

EXPECT_THAT(getMetric("cluster.cluster1.endpoint.shared.backend/a.c1", counters).tags(),
Comment thread
wbpcode marked this conversation as resolved.
UnorderedElementsAreArray({
Stats::Tag{"envoy.cluster_name", "cluster1"},
Stats::Tag{"envoy.endpoint_address", "shared.backend/a"},
}));
}

TEST_F(PerEndpointMetricsTest, FixedTags) {
auto& cluster = makeCluster("cluster1", 1);
Stats::TagVector fixed_tags{{"fixed1", "value1"}, {"fixed2", "value2"}};
Expand Down
30 changes: 30 additions & 0 deletions test/common/upstream/upstream_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2573,6 +2573,36 @@ TEST_F(StaticClusterImplTest, LoadAssignmentNonEmptyHostnameWithHealthChecks) {
EXPECT_FALSE(cluster->info()->addedViaApi());
}

TEST_F(StaticClusterImplTest, LoadAssignmentEndpointStatName) {
const std::string yaml = R"EOF(
name: staticcluster
connect_timeout: 0.25s
type: STATIC
lb_policy: ROUND_ROBIN
load_assignment:
endpoints:
- lb_endpoints:
- endpoint:
observability_name: shared.backend/a
address:
socket_address:
address: 10.0.0.1
port_value: 443
)EOF";

envoy::config::cluster::v3::Cluster cluster_config = parseClusterFromV3Yaml(yaml);

Envoy::Upstream::ClusterFactoryContextImpl factory_context(server_context_, nullptr, nullptr,
false);
std::shared_ptr<StaticClusterImpl> cluster = createCluster(cluster_config, factory_context);

cluster->initialize([] { return absl::OkStatus(); });

ASSERT_EQ(1UL, cluster->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size());
EXPECT_EQ("shared.backend/a",
cluster->prioritySet().hostSetsPerPriority()[0]->hosts()[0]->observabilityName());
}

TEST_F(StaticClusterImplTest, LoadAssignmentMultiplePriorities) {
const std::string yaml = R"EOF(
name: staticcluster
Expand Down
13 changes: 12 additions & 1 deletion test/mocks/upstream/host.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ MockHealthCheckHostMonitor::~MockHealthCheckHostMonitor() = default;
MockHostDescription::MockHostDescription()
: address_(*Network::Utility::resolveUrl("tcp://10.0.0.1:443")),
socket_factory_(new testing::NiceMock<Network::MockTransportSocketFactory>) {
observability_name_ = address_->asString();
ON_CALL(*this, hostname()).WillByDefault(ReturnRef(hostname_));
ON_CALL(*this, observabilityName()).WillByDefault(Invoke([this]() {
return absl::string_view(observability_name_);
}));
ON_CALL(*this, address()).WillByDefault(Return(address_));
ON_CALL(*this, outlierDetector()).WillByDefault(ReturnRef(outlier_detector_));
ON_CALL(*this, stats()).WillByDefault(ReturnRef(stats_));
Expand Down Expand Up @@ -65,10 +69,17 @@ MockHostDescription::MockHostDescription()

MockHostDescription::~MockHostDescription() = default;

MockHostLight::MockHostLight() = default;
MockHostLight::MockHostLight() {
ON_CALL(*this, observabilityName()).WillByDefault(Invoke([this]() {
return absl::string_view(observability_name_);
}));
}
MockHostLight::~MockHostLight() = default;

MockHost::MockHost() : socket_factory_(new testing::NiceMock<Network::MockTransportSocketFactory>) {
ON_CALL(*this, observabilityName()).WillByDefault(Invoke([this]() {
return absl::string_view(observability_name_);
}));
ON_CALL(*this, cluster()).WillByDefault(ReturnRef(cluster_));
ON_CALL(*this, outlierDetector()).WillByDefault(ReturnRef(outlier_detector_));
ON_CALL(*this, stats()).WillByDefault(ReturnRef(stats_));
Expand Down
4 changes: 4 additions & 0 deletions test/mocks/upstream/host.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class MockHostDescription : public HostDescription {
MOCK_METHOD(void, setHealthChecker, (HealthCheckHostMonitorPtr && health_checker));
MOCK_METHOD(const std::string&, hostnameForHealthChecks, (), (const));
MOCK_METHOD(const std::string&, hostname, (), (const));
MOCK_METHOD(absl::string_view, observabilityName, (), (const));
MOCK_METHOD(Network::UpstreamTransportSocketFactory&, transportSocketFactory, (), (const));
MOCK_METHOD(HostStats&, stats, (), (const));
MOCK_METHOD(LoadMetricStats&, loadMetricStats, (), (const));
Expand All @@ -123,6 +124,7 @@ class MockHostDescription : public HostDescription {
MOCK_METHOD(OptRef<HostLbPolicyData>, lbPolicyDataAt, (size_t index), (const));

std::string hostname_;
std::string observability_name_;
Network::Address::InstanceConstSharedPtr address_;
testing::NiceMock<Outlier::MockDetectorHostMonitor> outlier_detector_;
testing::NiceMock<MockHealthCheckHostMonitor> health_checker_;
Expand Down Expand Up @@ -210,6 +212,7 @@ class MockHostLight : public Host {

MOCK_METHOD(const std::string&, hostnameForHealthChecks, (), (const));
MOCK_METHOD(const std::string&, hostname, (), (const));
MOCK_METHOD(absl::string_view, observabilityName, (), (const));
MOCK_METHOD(Network::UpstreamTransportSocketFactory&, transportSocketFactory, (), (const));
MOCK_METHOD(Outlier::DetectorHostMonitor&, outlierDetector, (), (const));
MOCK_METHOD(void, setHealthChecker, (HealthCheckHostMonitorPtr && health_checker));
Expand All @@ -235,6 +238,7 @@ class MockHostLight : public Host {
MOCK_METHOD(absl::optional<uint64_t>, lastHealthCheckHttpStatus, (), (const));

bool disable_active_health_check_ = false;
std::string observability_name_;
};

class MockHost : public MockHostLight {
Expand Down
1 change: 1 addition & 0 deletions test/server/admin/stats_handler_speed_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class FastMockHost : public testing::StrictMock<Upstream::MockHostLight> {
public:
Network::Address::InstanceConstSharedPtr address() const override { return address_; }
const std::string& hostname() const override { return hostname_; }
absl::string_view observabilityName() const override { return address_->asStringView(); }

std::vector<std::pair<absl::string_view, Stats::PrimitiveCounterReference>>
counters() const override {
Expand Down
Loading
Loading