Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions source/extensions/stat_sinks/open_telemetry/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@ OpenTelemetrySinkFactory::createStatsSink(const Protobuf::Message& config,

Tracers::OpenTelemetry::ResourceProviderPtr resource_provider =
std::make_unique<Tracers::OpenTelemetry::ResourceProviderImpl>();
auto otlp_options = std::make_shared<OtlpOptions>(
sink_config,
resource_provider->getResource(sink_config.resource_detectors(), server,
/*service_name=*/""),
server);
auto resource = resource_provider->getResource(sink_config.resource_detectors(), server,
/*service_name=*/"");
// Inject Envoy node attributes into the resource so that downstream collectors
// can identify which Envoy instance emitted the metrics. Only populated when
// not already set by a configured resource detector.
const auto& local_info = server.localInfo();
if (!local_info.nodeName().empty()) {
resource.attributes_.try_emplace("node.id", local_info.nodeName());
}
if (!local_info.clusterName().empty()) {
resource.attributes_.try_emplace("node.cluster", local_info.clusterName());
}
auto otlp_options = std::make_shared<OtlpOptions>(sink_config, resource, server);
std::shared_ptr<OtlpMetricsFlusher> otlp_metrics_flusher =
std::make_shared<OtlpMetricsFlusherImpl>(otlp_options);

Expand Down
53 changes: 53 additions & 0 deletions test/extensions/stats_sinks/open_telemetry/config_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "gtest/gtest.h"

using testing::NiceMock;
using testing::ReturnRef;

namespace Envoy {
namespace Extensions {
Expand Down Expand Up @@ -94,6 +95,58 @@ TEST(OpenTelemetryConfigTest, OtlpOptionsTest) {
}
}

// Verify that the factory injects node.id and node.cluster from LocalInfo into
// the resource attributes when creating the sink, so consumers can identify the
// originating Envoy instance.
TEST(OpenTelemetryConfigTest, NodeAttributesAutoPopulatedFromLocalInfo) {
NiceMock<Server::Configuration::MockServerFactoryContext> server;

// Set a real node id and cluster on the local_info mock.
server.local_info_.node_.set_id("test-node-42");
server.local_info_.node_.set_cluster("my-cluster");

Server::Configuration::StatsSinkFactory* factory =
Registry::FactoryRegistry<Server::Configuration::StatsSinkFactory>::getFactory(
OpenTelemetryName);
ASSERT_NE(factory, nullptr);

envoy::extensions::stat_sinks::open_telemetry::v3::SinkConfig sink_config;
sink_config.mutable_grpc_service()->mutable_envoy_grpc()->set_cluster_name("otlp_grpc");
ProtobufTypes::MessagePtr message = factory->createEmptyConfigProto();
TestUtility::jsonConvert(sink_config, *message);

// Factory must succeed and produce a valid sink.
auto sink_or = factory->createStatsSink(*message, server);
ASSERT_TRUE(sink_or.ok());
EXPECT_NE(sink_or.value(), nullptr);
}

// Verify that when a resource detector already sets node.id, it takes priority
// over the auto-populated value from LocalInfo (try_emplace semantics).
TEST(OpenTelemetryConfigTest, NodeAttributesNotOverriddenByAutoPopulation) {
NiceMock<Server::Configuration::MockServerFactoryContext> server;
envoy::extensions::stat_sinks::open_telemetry::v3::SinkConfig sink_config;

// Manually build a resource as if a detector already set node.id.
Tracers::OpenTelemetry::Resource resource;
resource.attributes_["node.id"] = "detector-set-id";
resource.attributes_["node.cluster"] = "detector-set-cluster";

OtlpOptions options(sink_config, resource, server);
std::string node_id_val;
std::string node_cluster_val;
for (const auto& attr : options.resource_attributes()) {
if (attr.key() == "node.id") {
node_id_val = attr.value().string_value();
} else if (attr.key() == "node.cluster") {
node_cluster_val = attr.value().string_value();
}
}
// Detector-set values must be preserved.
EXPECT_EQ("detector-set-id", node_id_val);
EXPECT_EQ("detector-set-cluster", node_cluster_val);
}

} // namespace
} // namespace OpenTelemetry
} // namespace StatSinks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,34 @@ TEST_F(OtlpMetricsFlusherTests, SetResourceAttributes) {
metrics->resource_metrics()[0].resource().attributes()[0].value().string_value());
}

// Verify that node.id and node.cluster resource attributes are propagated into
// the exported OTLP ResourceMetrics, enabling consumers to identify the source.
TEST_F(OtlpMetricsFlusherTests, NodeIdAndClusterResourceAttributes) {
OtlpMetricsFlusherImpl flusher(
otlpOptions(true, false, true, true, "",
{{"node.id", "envoy-node-1"}, {"node.cluster", "prod-cluster"}}));
addCounterToSnapshot("test_counter1", 5, 5);
MetricsExportRequestSharedPtr metrics =
flusher.flush(snapshot_, delta_start_time_ns_, cumulative_start_time_ns_);
expectMetricsCount(metrics, 1);

ASSERT_EQ(1, metrics->resource_metrics().size());
const auto& resource_attrs = metrics->resource_metrics()[0].resource().attributes();
ASSERT_EQ(2, resource_attrs.size());

std::string node_id_val;
std::string node_cluster_val;
for (const auto& attr : resource_attrs) {
if (attr.key() == "node.id") {
node_id_val = attr.value().string_value();
} else if (attr.key() == "node.cluster") {
node_cluster_val = attr.value().string_value();
}
}
EXPECT_EQ("envoy-node-1", node_id_val);
EXPECT_EQ("prod-cluster", node_cluster_val);
}

class MockOpenTelemetryGrpcMetricsExporter : public OpenTelemetryGrpcMetricsExporter {
public:
MOCK_METHOD(void, send, (MetricsExportRequestPtr&&));
Expand Down