From 838a3e78af109735fb7ddd8338696e61f1109bbf Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Wed, 26 Aug 2026 20:34:02 +0530 Subject: [PATCH 01/18] [RESOURCE] Add Entity value type --- .../opentelemetry/sdk/resource/entity.h | 52 +++++++ sdk/src/resource/CMakeLists.txt | 2 +- sdk/src/resource/entity.cc | 59 ++++++++ sdk/test/resource/BUILD | 13 ++ sdk/test/resource/CMakeLists.txt | 2 +- sdk/test/resource/entity_test.cc | 127 ++++++++++++++++++ 6 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 sdk/include/opentelemetry/sdk/resource/entity.h create mode 100644 sdk/src/resource/entity.cc create mode 100644 sdk/test/resource/entity_test.cc diff --git a/sdk/include/opentelemetry/sdk/resource/entity.h b/sdk/include/opentelemetry/sdk/resource/entity.h new file mode 100644 index 0000000000..7f9ba7e94e --- /dev/null +++ b/sdk/include/opentelemetry/sdk/resource/entity.h @@ -0,0 +1,52 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "opentelemetry/sdk/common/attribute_utils.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace resource +{ + +using ResourceAttributes = opentelemetry::sdk::common::AttributeMap; + +class Entity +{ +public: + Entity(const std::string &type, + const ResourceAttributes &identity, + const ResourceAttributes &description = ResourceAttributes{}, + const std::string &schema_url = std::string{}) noexcept; + + Entity(const Entity &) = default; + Entity(Entity &&) = default; + Entity &operator=(const Entity &) = default; + Entity &operator=(Entity &&) = default; + + ~Entity() = default; + + const std::string &GetType() const noexcept; + const ResourceAttributes &GetIdentity() const noexcept; + const ResourceAttributes &GetDescription() const noexcept; + const std::string &GetSchemaURL() const noexcept; + + bool IsValid() const noexcept; + + bool operator==(const Entity &other) const noexcept; + +private: + std::string type_; + ResourceAttributes identity_; + ResourceAttributes description_; + std::string schema_url_; +}; + +} // namespace resource +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/resource/CMakeLists.txt b/sdk/src/resource/CMakeLists.txt index 94e01d99a0..3e65bf48ed 100644 --- a/sdk/src/resource/CMakeLists.txt +++ b/sdk/src/resource/CMakeLists.txt @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 add_library(opentelemetry_resources resource.cc resource_detector.cc - detail/percent_decode.cc) + entity.cc detail/percent_decode.cc) set_target_properties(opentelemetry_resources PROPERTIES EXPORT_NAME resources) set_target_version(opentelemetry_resources) diff --git a/sdk/src/resource/entity.cc b/sdk/src/resource/entity.cc new file mode 100644 index 0000000000..2616c01977 --- /dev/null +++ b/sdk/src/resource/entity.cc @@ -0,0 +1,59 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/sdk/resource/entity.h" + +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace resource +{ + +Entity::Entity(const std::string &type, + const ResourceAttributes &identity, + const ResourceAttributes &description, + const std::string &schema_url) noexcept + : type_(type), identity_(identity), description_(description), schema_url_(schema_url) +{ + for (const auto &kv : identity_) + { + description_.erase(kv.first); + } +} + +const std::string &Entity::GetType() const noexcept +{ + return type_; +} + +const ResourceAttributes &Entity::GetIdentity() const noexcept +{ + return identity_; +} + +const ResourceAttributes &Entity::GetDescription() const noexcept +{ + return description_; +} + +const std::string &Entity::GetSchemaURL() const noexcept +{ + return schema_url_; +} + +bool Entity::IsValid() const noexcept +{ + return !type_.empty() && !identity_.empty(); +} + +bool Entity::operator==(const Entity &other) const noexcept +{ + return type_ == other.type_ && identity_ == other.identity_ && + description_ == other.description_ && schema_url_ == other.schema_url_; +} + +} // namespace resource +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/test/resource/BUILD b/sdk/test/resource/BUILD index 92005f3a0a..df97ea6725 100644 --- a/sdk/test/resource/BUILD +++ b/sdk/test/resource/BUILD @@ -15,3 +15,16 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_test( + name = "entity_test", + srcs = [ + "entity_test.cc", + ], + tags = ["test"], + deps = [ + "//api", + "//sdk/src/resource", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/sdk/test/resource/CMakeLists.txt b/sdk/test/resource/CMakeLists.txt index dfe78589f8..b35821ba03 100644 --- a/sdk/test/resource/CMakeLists.txt +++ b/sdk/test/resource/CMakeLists.txt @@ -1,7 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -foreach(testname resource_test) +foreach(testname resource_test entity_test) add_executable(${testname} "${testname}.cc") target_link_libraries(${testname} ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} opentelemetry_resources) diff --git a/sdk/test/resource/entity_test.cc b/sdk/test/resource/entity_test.cc new file mode 100644 index 0000000000..c77e3d075b --- /dev/null +++ b/sdk/test/resource/entity_test.cc @@ -0,0 +1,127 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "opentelemetry/nostd/variant.h" +#include "opentelemetry/sdk/resource/entity.h" + +using namespace opentelemetry::sdk::resource; +namespace nostd = opentelemetry::nostd; + +TEST(EntityTest, ConstructAndGetters) +{ + ResourceAttributes identity = {{"service.name", "my-app"}}; + ResourceAttributes description = {{"service.version", "1.0.0"}}; + const std::string schema_url = "https://opentelemetry.io/schemas/1.21.0"; + + Entity entity("service", identity, description, schema_url); + + EXPECT_EQ(entity.GetType(), "service"); + EXPECT_EQ(entity.GetSchemaURL(), schema_url); + ASSERT_EQ(entity.GetIdentity().size(), 1); + EXPECT_EQ(nostd::get(entity.GetIdentity().at("service.name")), "my-app"); + ASSERT_EQ(entity.GetDescription().size(), 1); + EXPECT_EQ(nostd::get(entity.GetDescription().at("service.version")), "1.0.0"); +} + +TEST(EntityTest, DefaultEmptyDescriptionAndSchemaUrl) +{ + ResourceAttributes identity = {{"host.id", "H1"}}; + Entity entity("host", identity); + + EXPECT_EQ(entity.GetType(), "host"); + EXPECT_TRUE(entity.GetDescription().empty()); + EXPECT_TRUE(entity.GetSchemaURL().empty()); + EXPECT_TRUE(entity.IsValid()); +} + +TEST(EntityTest, OverlappingIdentityDescriptionKeyIdentityWins) +{ + ResourceAttributes identity = {{"host.name", "from-identity"}}; + ResourceAttributes description = {{"host.name", "from-description"}, {"host.type", "machine"}}; + + Entity entity("host", identity, description); + + ASSERT_EQ(entity.GetIdentity().size(), 1); + EXPECT_EQ(nostd::get(entity.GetIdentity().at("host.name")), "from-identity"); + EXPECT_TRUE(entity.GetDescription().find("host.name") == entity.GetDescription().end()); + ASSERT_EQ(entity.GetDescription().size(), 1); + EXPECT_EQ(nostd::get(entity.GetDescription().at("host.type")), "machine"); +} + +TEST(EntityTest, EmptyTypeIsInvalid) +{ + ResourceAttributes identity = {{"service.name", "app"}}; + Entity entity("", identity); + + EXPECT_TRUE(entity.GetType().empty()); + EXPECT_FALSE(entity.GetIdentity().empty()); + EXPECT_FALSE(entity.IsValid()); +} + +TEST(EntityTest, EmptyIdentityIsInvalid) +{ + Entity entity("service", ResourceAttributes{}); + + EXPECT_EQ(entity.GetType(), "service"); + EXPECT_TRUE(entity.GetIdentity().empty()); + EXPECT_FALSE(entity.IsValid()); +} + +TEST(EntityTest, ValidMinimalEntity) +{ + ResourceAttributes identity = {{"service.name", "minimal-app"}}; + Entity entity("service", identity); + + EXPECT_TRUE(entity.IsValid()); +} + +TEST(EntityTest, CopyAndAssignment) +{ + ResourceAttributes identity = {{"service.name", "app"}}; + ResourceAttributes description = {{"service.version", "1.0.0"}}; + Entity original("service", identity, description, "https://opentelemetry.io/schemas/1.0.0"); + + Entity copied(original); + EXPECT_TRUE(copied == original); + EXPECT_EQ(copied.GetType(), original.GetType()); + EXPECT_EQ(copied.GetSchemaURL(), original.GetSchemaURL()); + EXPECT_EQ(copied.GetIdentity(), original.GetIdentity()); + EXPECT_EQ(copied.GetDescription(), original.GetDescription()); + + Entity assigned("host", ResourceAttributes{{"host.id", "H1"}}); + assigned = original; + EXPECT_TRUE(assigned == original); + + Entity moved(std::move(copied)); + EXPECT_TRUE(moved == original); +} + +TEST(EntityTest, Equality) +{ + ResourceAttributes identity = {{"service.name", "app"}}; + ResourceAttributes description = {{"service.version", "1.0.0"}}; + const std::string schema_url = "https://opentelemetry.io/schemas/1.0.0"; + + Entity a("service", identity, description, schema_url); + Entity b("service", identity, description, schema_url); + EXPECT_TRUE(a == b); + + Entity different_type("host", identity, description, schema_url); + EXPECT_FALSE(a == different_type); + + Entity different_identity("service", ResourceAttributes{{"service.name", "other"}}, description, + schema_url); + EXPECT_FALSE(a == different_identity); + + Entity different_description("service", identity, + ResourceAttributes{{"service.version", "2.0.0"}}, schema_url); + EXPECT_FALSE(a == different_description); + + Entity different_schema("service", identity, description, + "https://opentelemetry.io/schemas/1.1.0"); + EXPECT_FALSE(a == different_schema); +} From be6372ea6f613ab751f9e8ce68e4d1ae80b34e3d Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Wed, 26 Aug 2026 20:47:50 +0530 Subject: [PATCH 02/18] [RESOURCE] Add Entity storage to Resource --- .../opentelemetry/sdk/resource/resource.h | 8 ++ sdk/src/resource/resource.cc | 42 ++++++-- sdk/test/resource/resource_test.cc | 95 +++++++++++++++++++ 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/sdk/include/opentelemetry/sdk/resource/resource.h b/sdk/include/opentelemetry/sdk/resource/resource.h index 2e462122f5..509e3b6f46 100644 --- a/sdk/include/opentelemetry/sdk/resource/resource.h +++ b/sdk/include/opentelemetry/sdk/resource/resource.h @@ -4,8 +4,10 @@ #pragma once #include +#include #include "opentelemetry/sdk/common/attribute_utils.h" +#include "opentelemetry/sdk/resource/entity.h" #include "opentelemetry/version.h" OPENTELEMETRY_BEGIN_NAMESPACE @@ -34,6 +36,8 @@ class Resource const ResourceAttributes &GetAttributes() const noexcept; const std::string &GetSchemaURL() const noexcept; + const std::vector &GetEntities() const noexcept; + const ResourceAttributes &GetUnassociatedAttributes() const noexcept; /** * Returns a new, merged {@link Resource} by merging the current Resource @@ -74,6 +78,10 @@ class Resource static Resource &GetDefault(); private: + void RefreshFlattenedAttributes() noexcept; + + std::vector entities_; + ResourceAttributes unassociated_attributes_; ResourceAttributes attributes_; std::string schema_url_; }; diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index 95b41498f8..de9eb12b64 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -20,15 +20,27 @@ namespace sdk namespace resource { -Resource::Resource() noexcept : attributes_(), schema_url_() {} +Resource::Resource() noexcept : entities_(), unassociated_attributes_(), schema_url_() +{ + RefreshFlattenedAttributes(); +} Resource::Resource(const ResourceAttributes &attributes) noexcept - : attributes_(attributes), schema_url_() -{} + : entities_(), unassociated_attributes_(attributes), schema_url_() +{ + RefreshFlattenedAttributes(); +} Resource::Resource(const ResourceAttributes &attributes, const std::string &schema_url) noexcept - : attributes_(attributes), schema_url_(schema_url) -{} + : entities_(), unassociated_attributes_(attributes), schema_url_(schema_url) +{ + RefreshFlattenedAttributes(); +} + +void Resource::RefreshFlattenedAttributes() noexcept +{ + attributes_ = unassociated_attributes_; +} Resource Resource::Merge(const Resource &other) const noexcept { @@ -44,16 +56,18 @@ Resource Resource::Create(const ResourceAttributes &attributes, const std::strin auto resource = Resource::GetDefault().Merge(otel_resource).Merge(Resource{attributes, schema_url}); - if (resource.attributes_.find(semconv::service::kServiceName) == resource.attributes_.end()) + if (resource.unassociated_attributes_.find(semconv::service::kServiceName) == + resource.unassociated_attributes_.end()) { std::string default_service_name = "unknown_service"; auto it_process_executable_name = - resource.attributes_.find(semconv::process::kProcessExecutableName); - if (it_process_executable_name != resource.attributes_.end()) + resource.unassociated_attributes_.find(semconv::process::kProcessExecutableName); + if (it_process_executable_name != resource.unassociated_attributes_.end()) { default_service_name += ":" + nostd::get(it_process_executable_name->second); } - resource.attributes_[semconv::service::kServiceName] = default_service_name; + resource.unassociated_attributes_[semconv::service::kServiceName] = default_service_name; + resource.RefreshFlattenedAttributes(); } return resource; } @@ -84,6 +98,16 @@ const std::string &Resource::GetSchemaURL() const noexcept return schema_url_; } +const std::vector &Resource::GetEntities() const noexcept +{ + return entities_; +} + +const ResourceAttributes &Resource::GetUnassociatedAttributes() const noexcept +{ + return unassociated_attributes_; +} + } // namespace resource } // namespace sdk OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 4a80ca2a7a..bb1da7a85a 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -358,4 +358,99 @@ TEST(ResourceTest, DerivedResourceDetector) EXPECT_TRUE(received_attributes.find("key") != received_attributes.end()); } +TEST(ResourceTest, EmptyHasNoEntities) +{ + Resource empty; + EXPECT_TRUE(empty.GetEntities().empty()); + EXPECT_TRUE(empty.GetUnassociatedAttributes().empty()); + EXPECT_EQ(empty.GetUnassociatedAttributes(), empty.GetAttributes()); + + Resource &get_empty = Resource::GetEmpty(); + EXPECT_TRUE(get_empty.GetEntities().empty()); + EXPECT_TRUE(get_empty.GetUnassociatedAttributes().empty()); + EXPECT_EQ(get_empty.GetUnassociatedAttributes(), get_empty.GetAttributes()); +} + +TEST(ResourceTest, ConstructFromAttributes) +{ + ResourceAttributes attributes = {{"service", "backend"}, {"host", "service-host"}}; + Resource resource(attributes); + + EXPECT_TRUE(resource.GetEntities().empty()); + EXPECT_EQ(resource.GetUnassociatedAttributes(), attributes); + EXPECT_EQ(resource.GetAttributes(), resource.GetUnassociatedAttributes()); + EXPECT_TRUE(resource.GetSchemaURL().empty()); +} + +TEST(ResourceTest, ConstructFromAttributesAndSchemaUrl) +{ + ResourceAttributes attributes = {{"service", "backend"}}; + const std::string schema_url = "https://opentelemetry.io/schemas/1.2.0"; + Resource resource(attributes, schema_url); + + EXPECT_TRUE(resource.GetEntities().empty()); + EXPECT_EQ(resource.GetUnassociatedAttributes(), attributes); + EXPECT_EQ(resource.GetAttributes(), resource.GetUnassociatedAttributes()); + EXPECT_EQ(resource.GetSchemaURL(), schema_url); +} + +TEST(ResourceTest, GetDefaultHasNoEntities) +{ + Resource &resource = Resource::GetDefault(); + EXPECT_TRUE(resource.GetEntities().empty()); + EXPECT_EQ(resource.GetUnassociatedAttributes(), resource.GetAttributes()); + EXPECT_FALSE(resource.GetAttributes().empty()); +} + +TEST(ResourceTest, CreateUnassociatedMatchesFlattened) +{ + ResourceAttributes attributes = {{"service", "backend"}}; + auto without_name = Resource::Create(attributes); + EXPECT_TRUE(without_name.GetEntities().empty()); + EXPECT_EQ(without_name.GetUnassociatedAttributes(), without_name.GetAttributes()); + EXPECT_EQ( + nostd::get(without_name.GetAttributes().at(semconv::service::kServiceName)), + "unknown_service"); + + ResourceAttributes with_name = {{"service.name", "backend"}}; + auto named = Resource::Create(with_name); + EXPECT_TRUE(named.GetEntities().empty()); + EXPECT_EQ(named.GetUnassociatedAttributes(), named.GetAttributes()); + EXPECT_EQ(nostd::get(named.GetAttributes().at(semconv::service::kServiceName)), + "backend"); +} + +TEST(ResourceTest, MergeUnassociatedMatchesFlattened) +{ + TestResource resource1(ResourceAttributes({{"service", "backend"}})); + TestResource resource2(ResourceAttributes({{"host", "service-host"}})); + auto merged = resource1.Merge(resource2); + + EXPECT_TRUE(merged.GetEntities().empty()); + EXPECT_EQ(merged.GetUnassociatedAttributes(), merged.GetAttributes()); + EXPECT_EQ(merged.GetAttributes().size(), 2); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("service")), "backend"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("host")), "service-host"); +} + +TEST(ResourceTest, CopyAndAssignmentPreservesNewMembers) +{ + ResourceAttributes attributes = {{"service", "backend"}}; + const std::string schema_url = "https://opentelemetry.io/schemas/1.2.0"; + Resource original(attributes, schema_url); + + Resource copied(original); + EXPECT_TRUE(copied.GetEntities().empty()); + EXPECT_EQ(copied.GetUnassociatedAttributes(), original.GetUnassociatedAttributes()); + EXPECT_EQ(copied.GetAttributes(), original.GetAttributes()); + EXPECT_EQ(copied.GetSchemaURL(), original.GetSchemaURL()); + + Resource assigned; + assigned = original; + EXPECT_TRUE(assigned.GetEntities().empty()); + EXPECT_EQ(assigned.GetUnassociatedAttributes(), original.GetUnassociatedAttributes()); + EXPECT_EQ(assigned.GetAttributes(), original.GetAttributes()); + EXPECT_EQ(assigned.GetSchemaURL(), original.GetSchemaURL()); +} + } // namespace From 8d25553e1838836dbdefcd8f18ac2725db67c8ab Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Wed, 26 Aug 2026 21:09:30 +0530 Subject: [PATCH 03/18] [RESOURCE] Add Entity support to Resource --- .../opentelemetry/sdk/resource/resource.h | 15 ++ sdk/src/resource/resource.cc | 101 +++++++++- sdk/test/resource/resource_test.cc | 188 ++++++++++++++++++ 3 files changed, 303 insertions(+), 1 deletion(-) diff --git a/sdk/include/opentelemetry/sdk/resource/resource.h b/sdk/include/opentelemetry/sdk/resource/resource.h index 509e3b6f46..336dc2d751 100644 --- a/sdk/include/opentelemetry/sdk/resource/resource.h +++ b/sdk/include/opentelemetry/sdk/resource/resource.h @@ -27,6 +27,20 @@ class Resource Resource(const ResourceAttributes &attributes, const std::string &schema_url) noexcept; + /** + * Constructs a Resource from attributes, a schema URL, and entities. + * + * Invalid entities, later entities of a duplicate type, and entities that + * share attribute keys with a higher-priority (earlier) entity are dropped. + * Keys owned by surviving entities are removed from unassociated attributes. + * If any entity survives, the Resource schema URL is taken from those + * entities (common URL, or empty if they differ); the constructor schema + * URL is used only when no entity survives. + */ + Resource(const ResourceAttributes &attributes, + const std::string &schema_url, + const std::vector &entities) noexcept; + Resource(const Resource &) = default; Resource(Resource &&) = default; Resource &operator=(const Resource &) = default; @@ -78,6 +92,7 @@ class Resource static Resource &GetDefault(); private: + void NormalizeEntities(const std::vector &entities) noexcept; void RefreshFlattenedAttributes() noexcept; std::vector entities_; diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index de9eb12b64..0d0f5dba81 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -3,9 +3,11 @@ #include #include +#include #include #include "opentelemetry/nostd/variant.h" +#include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/resource/resource_detector.h" #include "opentelemetry/sdk/version/version.h" @@ -37,9 +39,106 @@ Resource::Resource(const ResourceAttributes &attributes, const std::string &sche RefreshFlattenedAttributes(); } +Resource::Resource(const ResourceAttributes &attributes, + const std::string &schema_url, + const std::vector &entities) noexcept + : entities_(), unassociated_attributes_(attributes), schema_url_(schema_url) +{ + NormalizeEntities(entities); + RefreshFlattenedAttributes(); +} + +void Resource::NormalizeEntities(const std::vector &entities) noexcept +{ + std::vector accepted; + std::unordered_set accepted_types; + std::unordered_set accepted_keys; + accepted.reserve(entities.size()); + + for (const auto &entity : entities) + { + if (!entity.IsValid()) + { + OTEL_INTERNAL_LOG_WARN("[Resource] Dropping invalid Entity."); + continue; + } + + if (accepted_types.find(entity.GetType()) != accepted_types.end()) + { + OTEL_INTERNAL_LOG_WARN("[Resource] Dropping Entity of duplicate type."); + continue; + } + + bool key_conflict = false; + for (const auto &kv : entity.GetIdentity()) + { + if (accepted_keys.find(kv.first) != accepted_keys.end()) + { + key_conflict = true; + break; + } + } + if (!key_conflict) + { + for (const auto &kv : entity.GetDescription()) + { + if (accepted_keys.find(kv.first) != accepted_keys.end()) + { + key_conflict = true; + break; + } + } + } + if (key_conflict) + { + OTEL_INTERNAL_LOG_WARN("[Resource] Dropping Entity due to attribute key conflict."); + continue; + } + + accepted_types.insert(entity.GetType()); + for (const auto &kv : entity.GetIdentity()) + { + accepted_keys.insert(kv.first); + } + for (const auto &kv : entity.GetDescription()) + { + accepted_keys.insert(kv.first); + } + accepted.push_back(entity); + } + + entities_ = std::move(accepted); + + for (const auto &key : accepted_keys) + { + unassociated_attributes_.erase(key); + } + + if (!entities_.empty()) + { + const std::string &common_schema_url = entities_.front().GetSchemaURL(); + bool all_equal = true; + for (const auto &entity : entities_) + { + if (entity.GetSchemaURL() != common_schema_url) + { + all_equal = false; + break; + } + } + schema_url_ = all_equal ? common_schema_url : std::string{}; + } +} + void Resource::RefreshFlattenedAttributes() noexcept { - attributes_ = unassociated_attributes_; + attributes_.clear(); + for (const auto &entity : entities_) + { + attributes_.insert(entity.GetIdentity().begin(), entity.GetIdentity().end()); + attributes_.insert(entity.GetDescription().begin(), entity.GetDescription().end()); + } + attributes_.insert(unassociated_attributes_.begin(), unassociated_attributes_.end()); } Resource Resource::Merge(const Resource &other) const noexcept diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index bb1da7a85a..bac5247acc 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -453,4 +453,192 @@ TEST(ResourceTest, CopyAndAssignmentPreservesNewMembers) EXPECT_EQ(assigned.GetSchemaURL(), original.GetSchemaURL()); } +TEST(ResourceTest, ConstructEntitiesOnly) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}); + const std::string constructor_schema = "https://opentelemetry.io/schemas/1.2.0"; + Resource resource(ResourceAttributes{}, constructor_schema, {host}); + + ASSERT_EQ(resource.GetEntities().size(), 1); + EXPECT_EQ(resource.GetEntities()[0], host); + EXPECT_TRUE(resource.GetUnassociatedAttributes().empty()); + ASSERT_EQ(resource.GetAttributes().size(), 1); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("host.id")), "H1"); + EXPECT_TRUE(resource.GetSchemaURL().empty()); +} + +TEST(ResourceTest, ConstructAttributesAndEntity) +{ + ResourceAttributes attributes = {{"env", "prod"}}; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, + ResourceAttributes{{"host.name", "entity-host"}}); + Resource resource(attributes, std::string{}, {host}); + + ASSERT_EQ(resource.GetEntities().size(), 1); + EXPECT_EQ(resource.GetUnassociatedAttributes().size(), 1); + EXPECT_EQ(nostd::get(resource.GetUnassociatedAttributes().at("env")), "prod"); + EXPECT_TRUE(resource.GetUnassociatedAttributes().find("host.id") == + resource.GetUnassociatedAttributes().end()); + EXPECT_TRUE(resource.GetUnassociatedAttributes().find("host.name") == + resource.GetUnassociatedAttributes().end()); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("host.id")), "H1"); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("host.name")), "entity-host"); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("env")), "prod"); +} + +TEST(ResourceTest, ConstructLooseAttributeYieldsToEntity) +{ + ResourceAttributes attributes = {{"host.name", "loose"}}; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, + ResourceAttributes{{"host.name", "entity"}}); + Resource resource(attributes, std::string{}, {host}); + + EXPECT_TRUE(resource.GetUnassociatedAttributes().empty()); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("host.name")), "entity"); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("host.id")), "H1"); +} + +TEST(ResourceTest, ConstructDropsInvalidEntities) +{ + Entity empty_type("", ResourceAttributes{{"service.name", "app"}}); + Entity empty_identity("host", ResourceAttributes{}); + Entity valid("service", ResourceAttributes{{"service.name", "app"}}); + + Resource empty_type_only(ResourceAttributes{}, std::string{}, {empty_type}); + EXPECT_TRUE(empty_type_only.GetEntities().empty()); + + Resource empty_identity_only(ResourceAttributes{}, std::string{}, {empty_identity}); + EXPECT_TRUE(empty_identity_only.GetEntities().empty()); + + Resource mixed(ResourceAttributes{{"env", "prod"}}, std::string{}, + {empty_type, valid, empty_identity}); + ASSERT_EQ(mixed.GetEntities().size(), 1); + EXPECT_EQ(mixed.GetEntities()[0], valid); + EXPECT_EQ(nostd::get(mixed.GetUnassociatedAttributes().at("env")), "prod"); +} + +TEST(ResourceTest, ConstructDuplicateTypeFirstWins) +{ + Entity first("host", ResourceAttributes{{"host.id", "H1"}}); + Entity second("host", ResourceAttributes{{"host.id", "H2"}}); + Resource resource(ResourceAttributes{}, std::string{}, {first, second}); + + ASSERT_EQ(resource.GetEntities().size(), 1); + EXPECT_EQ(resource.GetEntities()[0], first); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("host.id")), "H1"); +} + +TEST(ResourceTest, ConstructTwoEntityTypesNoKeyConflict) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}); + Entity service("service", ResourceAttributes{{"service.name", "app"}}); + Resource resource(ResourceAttributes{{"env", "prod"}}, std::string{}, {host, service}); + + ASSERT_EQ(resource.GetEntities().size(), 2); + EXPECT_EQ(resource.GetEntities()[0], host); + EXPECT_EQ(resource.GetEntities()[1], service); + EXPECT_EQ(nostd::get(resource.GetUnassociatedAttributes().at("env")), "prod"); + EXPECT_EQ(resource.GetAttributes().size(), 3); +} + +TEST(ResourceTest, ConstructSharedEntityKeyDropsLowerPriority) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{{"env", "prod"}}); + Entity service("service", ResourceAttributes{{"service.name", "app"}}, + ResourceAttributes{{"env", "dev"}}); + Resource description_conflict(ResourceAttributes{}, std::string{}, {host, service}); + + ASSERT_EQ(description_conflict.GetEntities().size(), 1); + EXPECT_EQ(description_conflict.GetEntities()[0], host); + EXPECT_EQ(nostd::get(description_conflict.GetAttributes().at("env")), "prod"); + EXPECT_TRUE(description_conflict.GetAttributes().find("service.name") == + description_conflict.GetAttributes().end()); + + Entity host_identity("host", ResourceAttributes{{"shared.id", "from-host"}}); + Entity service_identity("service", ResourceAttributes{{"shared.id", "from-service"}}); + Resource identity_conflict(ResourceAttributes{}, std::string{}, + {host_identity, service_identity}); + ASSERT_EQ(identity_conflict.GetEntities().size(), 1); + EXPECT_EQ(identity_conflict.GetEntities()[0], host_identity); +} + +TEST(ResourceTest, ConstructMixedEntitySchemaUrls) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, + "https://opentelemetry.io/schemas/1.21.0"); + Entity service("service", ResourceAttributes{{"service.name", "app"}}, ResourceAttributes{}, + "https://opentelemetry.io/schemas/1.22.0"); + Resource resource(ResourceAttributes{}, "https://opentelemetry.io/schemas/1.2.0", + {host, service}); + + EXPECT_TRUE(resource.GetSchemaURL().empty()); + ASSERT_EQ(resource.GetEntities().size(), 2); +} + +TEST(ResourceTest, ConstructEqualEntitySchemaUrls) +{ + const std::string entity_schema = "https://opentelemetry.io/schemas/1.21.0"; + const std::string constructor_schema = "https://opentelemetry.io/schemas/1.2.0"; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, entity_schema); + Entity service("service", ResourceAttributes{{"service.name", "app"}}, ResourceAttributes{}, + entity_schema); + Resource resource(ResourceAttributes{}, constructor_schema, {host, service}); + + EXPECT_EQ(resource.GetSchemaURL(), entity_schema); + ASSERT_EQ(resource.GetEntities().size(), 2); +} + +TEST(ResourceTest, ConstructEmptyEntityVector) +{ + ResourceAttributes attributes = {{"service", "backend"}}; + const std::string schema_url = "https://opentelemetry.io/schemas/1.2.0"; + Resource with_empty_vector(attributes, schema_url, {}); + Resource two_arg(attributes, schema_url); + + EXPECT_TRUE(with_empty_vector.GetEntities().empty()); + EXPECT_EQ(with_empty_vector.GetSchemaURL(), schema_url); + EXPECT_EQ(with_empty_vector.GetUnassociatedAttributes(), attributes); + EXPECT_EQ(with_empty_vector.GetUnassociatedAttributes(), with_empty_vector.GetAttributes()); + EXPECT_EQ(with_empty_vector.GetAttributes(), two_arg.GetAttributes()); + EXPECT_EQ(with_empty_vector.GetSchemaURL(), two_arg.GetSchemaURL()); +} + +TEST(ResourceTest, ConstructAllInvalidEntities) +{ + ResourceAttributes attributes = {{"service", "backend"}}; + const std::string schema_url = "https://opentelemetry.io/schemas/1.2.0"; + Entity empty_type("", ResourceAttributes{{"host.id", "H1"}}); + Entity empty_identity("host", ResourceAttributes{}); + Resource resource(attributes, schema_url, {empty_type, empty_identity}); + + EXPECT_TRUE(resource.GetEntities().empty()); + EXPECT_EQ(resource.GetUnassociatedAttributes(), attributes); + EXPECT_EQ(resource.GetAttributes(), resource.GetUnassociatedAttributes()); + EXPECT_EQ(resource.GetSchemaURL(), schema_url); +} + +TEST(ResourceTest, CopyAndAssignmentPreservesEntities) +{ + ResourceAttributes attributes = {{"env", "prod"}}; + const std::string schema_url = "https://opentelemetry.io/schemas/1.21.0"; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, schema_url); + Resource original(attributes, "https://opentelemetry.io/schemas/1.2.0", {host}); + + Resource copied(original); + ASSERT_EQ(copied.GetEntities().size(), 1); + EXPECT_EQ(copied.GetEntities()[0], original.GetEntities()[0]); + EXPECT_EQ(copied.GetUnassociatedAttributes(), original.GetUnassociatedAttributes()); + EXPECT_EQ(copied.GetAttributes(), original.GetAttributes()); + EXPECT_EQ(copied.GetSchemaURL(), original.GetSchemaURL()); + EXPECT_EQ(copied.GetSchemaURL(), schema_url); + + Resource assigned; + assigned = original; + ASSERT_EQ(assigned.GetEntities().size(), 1); + EXPECT_EQ(assigned.GetEntities()[0], original.GetEntities()[0]); + EXPECT_EQ(assigned.GetUnassociatedAttributes(), original.GetUnassociatedAttributes()); + EXPECT_EQ(assigned.GetAttributes(), original.GetAttributes()); + EXPECT_EQ(assigned.GetSchemaURL(), original.GetSchemaURL()); +} + } // namespace From f49b0625cf1bfedaec417f715c9a5fb83ff5f12b Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Thu, 27 Aug 2026 05:17:33 +0530 Subject: [PATCH 04/18] [RESOURCE] Add entity-aware Merge and Create support --- .../opentelemetry/sdk/resource/resource.h | 24 +- sdk/src/resource/resource.cc | 149 +++++++++- sdk/test/resource/resource_test.cc | 267 ++++++++++++++++++ 3 files changed, 426 insertions(+), 14 deletions(-) diff --git a/sdk/include/opentelemetry/sdk/resource/resource.h b/sdk/include/opentelemetry/sdk/resource/resource.h index 336dc2d751..5d233b86e1 100644 --- a/sdk/include/opentelemetry/sdk/resource/resource.h +++ b/sdk/include/opentelemetry/sdk/resource/resource.h @@ -55,12 +55,16 @@ class Resource /** * Returns a new, merged {@link Resource} by merging the current Resource - * with the other Resource. In case of a collision, the other Resource takes - * precedence. + * (old) with the other Resource (updating). In case of a collision, the + * other Resource takes precedence for unassociated attributes. * - * The specification notes that if schema urls collide, the resulting - * schema url is implementation-defined. In the C++ implementation, the - * schema url of @p other is picked. + * When neither Resource has entities, attributes and schema URLs follow the + * historical merge rules. If schema urls collide, the resulting schema url + * is implementation-defined; this implementation picks @p other. + * + * When either Resource has entities, merge follows the entity-aware + * resource data model: type-rank, description overlay, updating unassociated + * keys evicting entities, then construction-time key uniqueness. * * @param other the Resource that will be merged with this. * @returns the newly merged Resource. @@ -79,6 +83,16 @@ class Resource static Resource Create(const ResourceAttributes &attributes, const std::string &schema_url = std::string{}); + /** + * Returns a newly created Resource with the specified attributes and + * entities. SDK attributes and OTEL attributes are merged in as with the + * two-argument Create. + */ + + static Resource Create(const ResourceAttributes &attributes, + const std::string &schema_url, + const std::vector &entities); + /** * Returns an Empty resource. */ diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index 0d0f5dba81..6172b89369 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -1,10 +1,13 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#include +#include #include #include #include #include +#include #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/global_log_handler.h" @@ -21,6 +24,56 @@ namespace sdk { namespace resource { +namespace +{ + +bool EntityOwnsKey(const Entity &entity, const std::string &key) +{ + return entity.GetIdentity().find(key) != entity.GetIdentity().end() || + entity.GetDescription().find(key) != entity.GetDescription().end(); +} + +bool CanMergeEntities(const Entity &existing, const Entity &incoming) +{ + return existing.GetIdentity() == incoming.GetIdentity() && + existing.GetSchemaURL() == incoming.GetSchemaURL(); +} + +Entity OverlayDescription(const Entity &existing, const Entity &incoming) +{ + ResourceAttributes description = existing.GetDescription(); + for (const auto &kv : incoming.GetDescription()) + { + description[kv.first] = kv.second; + } + return Entity(existing.GetType(), existing.GetIdentity(), description, existing.GetSchemaURL()); +} + +std::unordered_map BuildTypeRanks(const Resource &old_resource, + const Resource &updating) +{ + std::unordered_map rank; + const auto &updating_entities = updating.GetEntities(); + for (std::size_t i = 0; i < updating_entities.size(); ++i) + { + const std::string &type = updating_entities[i].GetType(); + if (rank.find(type) == rank.end()) + { + rank[type] = i; + } + } + std::size_t old_only = updating_entities.size(); + for (const auto &entity : old_resource.GetEntities()) + { + if (rank.find(entity.GetType()) == rank.end()) + { + rank[entity.GetType()] = old_only++; + } + } + return rank; +} + +} // namespace Resource::Resource() noexcept : entities_(), unassociated_attributes_(), schema_url_() { @@ -143,25 +196,103 @@ void Resource::RefreshFlattenedAttributes() noexcept Resource Resource::Merge(const Resource &other) const noexcept { - ResourceAttributes merged_resource_attributes(other.attributes_); - merged_resource_attributes.insert(attributes_.begin(), attributes_.end()); - return Resource(merged_resource_attributes, - other.schema_url_.empty() ? schema_url_ : other.schema_url_); + if (entities_.empty() && other.entities_.empty()) + { + ResourceAttributes merged_resource_attributes(other.attributes_); + merged_resource_attributes.insert(attributes_.begin(), attributes_.end()); + return Resource(merged_resource_attributes, + other.schema_url_.empty() ? schema_url_ : other.schema_url_); + } + + const Resource &updating = other; + auto rank = BuildTypeRanks(*this, updating); + + std::vector merged_entities = GetEntities(); + for (const auto &incoming : updating.GetEntities()) + { + if (!incoming.IsValid()) + { + continue; + } + + auto existing = std::find_if( + merged_entities.begin(), merged_entities.end(), + [&incoming](const Entity &entity) { return entity.GetType() == incoming.GetType(); }); + if (existing != merged_entities.end()) + { + if (CanMergeEntities(*existing, incoming)) + { + *existing = OverlayDescription(*existing, incoming); + } + else + { + OTEL_INTERNAL_LOG_WARN( + "[Resource] Dropping Entity that cannot merge with an existing type."); + } + } + else + { + merged_entities.push_back(incoming); + } + } + + ResourceAttributes unassociated(updating.GetUnassociatedAttributes()); + unassociated.insert(GetUnassociatedAttributes().begin(), GetUnassociatedAttributes().end()); + + std::vector after_eviction; + after_eviction.reserve(merged_entities.size()); + for (const auto &entity : merged_entities) + { + bool evicted = false; + for (const auto &kv : updating.GetUnassociatedAttributes()) + { + if (EntityOwnsKey(entity, kv.first)) + { + evicted = true; + break; + } + } + if (evicted) + { + OTEL_INTERNAL_LOG_WARN("[Resource] Dropping Entity due to updating unassociated attribute."); + continue; + } + after_eviction.push_back(entity); + } + + std::stable_sort( + after_eviction.begin(), after_eviction.end(), [&rank](const Entity &lhs, const Entity &rhs) { + auto left = rank.find(lhs.GetType()); + auto right = rank.find(rhs.GetType()); + std::size_t left_rank = left == rank.end() ? static_cast(-1) : left->second; + std::size_t right_rank = right == rank.end() ? static_cast(-1) : right->second; + return left_rank < right_rank; + }); + + const std::string classic_schema = + updating.GetSchemaURL().empty() ? GetSchemaURL() : updating.GetSchemaURL(); + return Resource(unassociated, classic_schema, after_eviction); } Resource Resource::Create(const ResourceAttributes &attributes, const std::string &schema_url) +{ + return Create(attributes, schema_url, {}); +} + +Resource Resource::Create(const ResourceAttributes &attributes, + const std::string &schema_url, + const std::vector &entities) { static auto otel_resource = OTELResourceDetector().Detect(); auto resource = - Resource::GetDefault().Merge(otel_resource).Merge(Resource{attributes, schema_url}); + Resource::GetDefault().Merge(otel_resource).Merge(Resource{attributes, schema_url, entities}); - if (resource.unassociated_attributes_.find(semconv::service::kServiceName) == - resource.unassociated_attributes_.end()) + if (resource.attributes_.find(semconv::service::kServiceName) == resource.attributes_.end()) { std::string default_service_name = "unknown_service"; auto it_process_executable_name = - resource.unassociated_attributes_.find(semconv::process::kProcessExecutableName); - if (it_process_executable_name != resource.unassociated_attributes_.end()) + resource.attributes_.find(semconv::process::kProcessExecutableName); + if (it_process_executable_name != resource.attributes_.end()) { default_service_name += ":" + nostd::get(it_process_executable_name->second); } diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index bac5247acc..39501ad195 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -641,4 +641,271 @@ TEST(ResourceTest, CopyAndAssignmentPreservesEntities) EXPECT_EQ(assigned.GetSchemaURL(), original.GetSchemaURL()); } +TEST(ResourceTest, MergeExample1EntityReplacesLooseAttribute) +{ + Resource old_resource(ResourceAttributes{{"host.name", "old-name"}, {"env", "prod"}}, + std::string{}); + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, + ResourceAttributes{{"host.name", "new-name"}}); + Resource updating(ResourceAttributes{}, std::string{}, {host}); + auto merged = old_resource.Merge(updating); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetEntities()[0].GetType(), "host"); + EXPECT_EQ(merged.GetUnassociatedAttributes().size(), 1); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("env")), "prod"); + EXPECT_TRUE(merged.GetUnassociatedAttributes().find("host.name") == + merged.GetUnassociatedAttributes().end()); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("host.id")), "H1"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("host.name")), "new-name"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("env")), "prod"); +} + +TEST(ResourceTest, MergeExample2LooseAttributeEvictsEntity) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, + ResourceAttributes{{"host.name", "detected-name"}}); + Entity process("process", ResourceAttributes{{"process.pid", "12345"}}); + Resource old_resource(ResourceAttributes{}, std::string{}, {host, process}); + Resource updating(ResourceAttributes{{"host.id", "H2"}, {"env", "prod"}}, std::string{}); + auto merged = old_resource.Merge(updating); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetEntities()[0].GetType(), "process"); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("host.id")), "H2"); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("env")), "prod"); + EXPECT_TRUE(merged.GetUnassociatedAttributes().find("host.name") == + merged.GetUnassociatedAttributes().end()); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("process.pid")), "12345"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("host.id")), "H2"); + EXPECT_TRUE(merged.GetAttributes().find("host.name") == merged.GetAttributes().end()); +} + +TEST(ResourceTest, MergeExample3IdentityConflictKeepsUpdatingHostRank) +{ + Entity old_host("host", ResourceAttributes{{"host.id", "H1"}}, + ResourceAttributes{{"env", "prod"}}); + Resource old_resource(ResourceAttributes{}, std::string{}, {old_host}); + Entity updating_host("host", ResourceAttributes{{"host.id", "H2"}}); + Entity service("service", ResourceAttributes{{"service.name", "S1"}}, + ResourceAttributes{{"env", "dev"}}); + Resource updating(ResourceAttributes{}, std::string{}, {updating_host, service}); + auto merged = old_resource.Merge(updating); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetEntities()[0].GetType(), "host"); + EXPECT_EQ(nostd::get(merged.GetEntities()[0].GetIdentity().at("host.id")), "H1"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("env")), "prod"); + EXPECT_TRUE(merged.GetAttributes().find("service.name") == merged.GetAttributes().end()); +} + +TEST(ResourceTest, MergeSameTypeSameIdentityOverlaysDescription) +{ + const std::string schema_url = "https://opentelemetry.io/schemas/1.21.0"; + Entity old_host("host", ResourceAttributes{{"host.id", "H1"}}, + ResourceAttributes{{"env", "prod"}, {"host.type", "machine"}}, schema_url); + Entity updating_host("host", ResourceAttributes{{"host.id", "H1"}}, + ResourceAttributes{{"env", "dev"}}, schema_url); + Resource old_resource(ResourceAttributes{}, std::string{}, {old_host}); + Resource updating(ResourceAttributes{}, std::string{}, {updating_host}); + auto merged = old_resource.Merge(updating); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(nostd::get(merged.GetEntities()[0].GetDescription().at("env")), "dev"); + EXPECT_EQ(nostd::get(merged.GetEntities()[0].GetDescription().at("host.type")), + "machine"); + EXPECT_EQ(merged.GetEntities()[0].GetSchemaURL(), schema_url); +} + +TEST(ResourceTest, MergeSameTypeDifferentIdentityKeepsOld) +{ + Entity old_host("host", ResourceAttributes{{"host.id", "H1"}}); + Entity updating_host("host", ResourceAttributes{{"host.id", "H2"}}); + auto merged = Resource(ResourceAttributes{}, std::string{}, {old_host}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {updating_host})); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(nostd::get(merged.GetEntities()[0].GetIdentity().at("host.id")), "H1"); +} + +TEST(ResourceTest, MergeSameTypeDifferentSchemaKeepsOld) +{ + Entity old_host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, + "https://opentelemetry.io/schemas/1.21.0"); + Entity updating_host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, + "https://opentelemetry.io/schemas/1.22.0"); + auto merged = Resource(ResourceAttributes{}, std::string{}, {old_host}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {updating_host})); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetEntities()[0].GetSchemaURL(), "https://opentelemetry.io/schemas/1.21.0"); +} + +TEST(ResourceTest, MergeDifferentTypesAppend) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}); + Entity service("service", ResourceAttributes{{"service.name", "app"}}); + auto merged = Resource(ResourceAttributes{}, std::string{}, {host}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {service})); + + ASSERT_EQ(merged.GetEntities().size(), 2); + EXPECT_EQ(merged.GetEntities()[0].GetType(), "service"); + EXPECT_EQ(merged.GetEntities()[1].GetType(), "host"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("host.id")), "H1"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("service.name")), "app"); +} + +TEST(ResourceTest, MergeUpdatingTypeRankBeatsOldOnlyType) +{ + Entity service("service", ResourceAttributes{{"service.name", "app"}}, + ResourceAttributes{{"env", "prod"}}); + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{{"env", "dev"}}); + auto merged = Resource(ResourceAttributes{}, std::string{}, {service}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {host})); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetEntities()[0].GetType(), "host"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("env")), "dev"); +} + +TEST(ResourceTest, MergeCascadingKeyConflictKeepsHighestAndUnrelated) +{ + Entity mid("mid", ResourceAttributes{{"x", "from-mid"}, {"y", "from-mid"}}); + Entity a("a", ResourceAttributes{{"x", "from-a"}}); + Entity c("c", ResourceAttributes{{"y", "from-c"}}); + auto merged = Resource(ResourceAttributes{}, std::string{}, {mid}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {a, c})); + + ASSERT_EQ(merged.GetEntities().size(), 2); + EXPECT_EQ(merged.GetEntities()[0].GetType(), "a"); + EXPECT_EQ(merged.GetEntities()[1].GetType(), "c"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("x")), "from-a"); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("y")), "from-c"); +} + +TEST(ResourceTest, MergeAllEntitiesDroppedUsesClassicSchema) +{ + const std::string old_schema = "https://opentelemetry.io/schemas/1.21.0"; + const std::string updating_schema = "https://opentelemetry.io/schemas/1.22.0"; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, old_schema); + Resource old_resource(ResourceAttributes{}, old_schema, {host}); + Resource updating(ResourceAttributes{{"host.id", "H2"}}, updating_schema); + auto merged = old_resource.Merge(updating); + + EXPECT_TRUE(merged.GetEntities().empty()); + EXPECT_EQ(merged.GetSchemaURL(), updating_schema); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("host.id")), "H2"); +} + +TEST(ResourceTest, MergeSurvivingEntitiesSetResourceSchema) +{ + const std::string entity_schema = "https://opentelemetry.io/schemas/1.21.0"; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, entity_schema); + Resource old_resource(ResourceAttributes{}, "https://opentelemetry.io/schemas/1.2.0"); + Resource updating(ResourceAttributes{{"env", "prod"}}, "https://opentelemetry.io/schemas/9.9.9", + {host}); + auto merged = old_resource.Merge(updating); + + EXPECT_EQ(merged.GetSchemaURL(), entity_schema); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("env")), "prod"); +} + +TEST(ResourceTest, MergeMixedEntitySchemaUrls) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, + "https://opentelemetry.io/schemas/1.21.0"); + Entity service("service", ResourceAttributes{{"service.name", "app"}}, ResourceAttributes{}, + "https://opentelemetry.io/schemas/1.22.0"); + auto merged = Resource(ResourceAttributes{}, std::string{}, {host}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {service})); + + EXPECT_TRUE(merged.GetSchemaURL().empty()); + ASSERT_EQ(merged.GetEntities().size(), 2); +} + +TEST(ResourceTest, MergeUpdatingLooseBeatsOldLoose) +{ + Resource old_resource(ResourceAttributes{{"foo", "old"}, {"keep", "old"}}, std::string{}, + {Entity("host", ResourceAttributes{{"host.id", "H1"}})}); + Resource updating(ResourceAttributes{{"foo", "new"}}, std::string{}, + {Entity("service", ResourceAttributes{{"service.name", "app"}})}); + auto merged = old_resource.Merge(updating); + + ASSERT_EQ(merged.GetEntities().size(), 2); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("foo")), "new"); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("keep")), "old"); + EXPECT_TRUE(merged.GetUnassociatedAttributes().find("host.id") == + merged.GetUnassociatedAttributes().end()); + EXPECT_TRUE(merged.GetUnassociatedAttributes().find("service.name") == + merged.GetUnassociatedAttributes().end()); +} + +TEST(ResourceTest, MergeCopyAssignmentPreservesMergedEntities) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}); + auto merged = Resource(ResourceAttributes{{"env", "prod"}}, std::string{}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {host})); + + Resource copied(merged); + EXPECT_EQ(copied.GetEntities(), merged.GetEntities()); + EXPECT_EQ(copied.GetUnassociatedAttributes(), merged.GetUnassociatedAttributes()); + EXPECT_EQ(copied.GetAttributes(), merged.GetAttributes()); + EXPECT_EQ(copied.GetSchemaURL(), merged.GetSchemaURL()); + + Resource assigned; + assigned = merged; + EXPECT_EQ(assigned.GetEntities(), merged.GetEntities()); + EXPECT_EQ(assigned.GetAttributes(), merged.GetAttributes()); +} + +TEST(ResourceTest, CreateEmptyEntitiesMatchesTwoArg) +{ + ResourceAttributes attributes = {{"service", "backend"}}; + auto two_arg = Resource::Create(attributes); + auto three_arg = Resource::Create(attributes, std::string{}, {}); + + EXPECT_TRUE(two_arg.GetEntities().empty()); + EXPECT_TRUE(three_arg.GetEntities().empty()); + EXPECT_EQ(two_arg.GetAttributes(), three_arg.GetAttributes()); + EXPECT_EQ(two_arg.GetSchemaURL(), three_arg.GetSchemaURL()); +} + +TEST(ResourceTest, CreateWithEntityPreservesEntityAndSdkDefaults) +{ + Entity host("host", ResourceAttributes{{"host.id", "H1"}}); + auto resource = Resource::Create(ResourceAttributes{{"env", "prod"}}, std::string{}, {host}); + + ASSERT_EQ(resource.GetEntities().size(), 1); + EXPECT_EQ(resource.GetEntities()[0].GetType(), "host"); + EXPECT_EQ(nostd::get(resource.GetAttributes().at("host.id")), "H1"); + EXPECT_EQ(nostd::get(resource.GetUnassociatedAttributes().at("env")), "prod"); + EXPECT_EQ(nostd::get( + resource.GetAttributes().at(semconv::telemetry::kTelemetrySdkLanguage)), + "cpp"); + EXPECT_EQ(nostd::get(resource.GetAttributes().at(semconv::service::kServiceName)), + "unknown_service"); +} + +TEST(ResourceTest, CreateEntityOwnedServiceNameSkipsFallback) +{ + Entity service("service", ResourceAttributes{{"service.name", "from-entity"}}); + auto resource = Resource::Create(ResourceAttributes{}, std::string{}, {service}); + + ASSERT_EQ(resource.GetEntities().size(), 1); + EXPECT_EQ(nostd::get(resource.GetAttributes().at(semconv::service::kServiceName)), + "from-entity"); + EXPECT_TRUE(resource.GetUnassociatedAttributes().find(semconv::service::kServiceName) == + resource.GetUnassociatedAttributes().end()); +} + +TEST(ResourceTest, CreateWithEntitySchemaUrl) +{ + const std::string entity_schema = "https://opentelemetry.io/schemas/1.21.0"; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{}, entity_schema); + auto resource = + Resource::Create(ResourceAttributes{}, "https://opentelemetry.io/schemas/1.2.0", {host}); + + EXPECT_EQ(resource.GetSchemaURL(), entity_schema); +} + } // namespace From e167baf452c02c61bf153ec5e55620cb8aaa8d44 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Thu, 27 Aug 2026 11:03:52 +0530 Subject: [PATCH 05/18] Add Entity support entry to CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 462236afa6..5a0fad1d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Increment the: ## [Unreleased] +* [SDK] Add Entity support to Resource + [#3652](https://github.com/open-telemetry/opentelemetry-cpp/issues/3652) + * [CONFIGURATION] Add a configuration builder for the host resource detector [#4451](https://github.com/open-telemetry/opentelemetry-cpp/issues/4451) * [CONFIGURATION] Build the configured resource detectors in SdkBuilder, apply From fa79664cf732ae19749fd4899ac67ee66917642e Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Thu, 27 Aug 2026 15:26:51 +0530 Subject: [PATCH 06/18] Update CHANGELOG.md to remove empty space under the update --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a0fad1d4f..5284e1ac71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ Increment the: * [SDK] Add Entity support to Resource [#3652](https://github.com/open-telemetry/opentelemetry-cpp/issues/3652) - * [CONFIGURATION] Add a configuration builder for the host resource detector [#4451](https://github.com/open-telemetry/opentelemetry-cpp/issues/4451) * [CONFIGURATION] Build the configured resource detectors in SdkBuilder, apply From 3bac7ccaa5772148ff19cc34526c83e0edd11bc8 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Thu, 27 Aug 2026 15:35:21 +0530 Subject: [PATCH 07/18] Update CMakeLists.txt for linespace adjustment --- sdk/src/resource/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/resource/CMakeLists.txt b/sdk/src/resource/CMakeLists.txt index 3e65bf48ed..040736c464 100644 --- a/sdk/src/resource/CMakeLists.txt +++ b/sdk/src/resource/CMakeLists.txt @@ -1,8 +1,8 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -add_library(opentelemetry_resources resource.cc resource_detector.cc - entity.cc detail/percent_decode.cc) +add_library(opentelemetry_resources resource.cc resource_detector.cc entity.cc + detail/percent_decode.cc) set_target_properties(opentelemetry_resources PROPERTIES EXPORT_NAME resources) set_target_version(opentelemetry_resources) From 8a7792e8b483d0bd1a9df065dcd415f9299f9e4c Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Thu, 27 Aug 2026 15:51:23 +0530 Subject: [PATCH 08/18] Update resource.cc to make insertion function more efficient --- sdk/src/resource/resource.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index 6172b89369..4e93b5236f 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -59,7 +59,7 @@ std::unordered_map BuildTypeRanks(const Resource &old_ const std::string &type = updating_entities[i].GetType(); if (rank.find(type) == rank.end()) { - rank[type] = i; + rank.try_emplace(type,i); } } std::size_t old_only = updating_entities.size(); @@ -67,7 +67,7 @@ std::unordered_map BuildTypeRanks(const Resource &old_ { if (rank.find(entity.GetType()) == rank.end()) { - rank[entity.GetType()] = old_only++; + rank.try_emplace(entity.GetType(), old_only++); } } return rank; From 44e4f4d67c065d89de0c2d8a995b3be7db708064 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Thu, 27 Aug 2026 15:55:42 +0530 Subject: [PATCH 09/18] Update resource.cc emplace function --- sdk/src/resource/resource.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index 4e93b5236f..308ad56d0d 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -59,7 +59,7 @@ std::unordered_map BuildTypeRanks(const Resource &old_ const std::string &type = updating_entities[i].GetType(); if (rank.find(type) == rank.end()) { - rank.try_emplace(type,i); + rank.emplace(type,i); } } std::size_t old_only = updating_entities.size(); @@ -67,7 +67,7 @@ std::unordered_map BuildTypeRanks(const Resource &old_ { if (rank.find(entity.GetType()) == rank.end()) { - rank.try_emplace(entity.GetType(), old_only++); + rank.emplace(entity.GetType(), old_only++); } } return rank; From 6696d2f4a37c086ec5d3d2f09ef7ce9d8fcd3cf4 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Fri, 28 Aug 2026 05:07:57 +0530 Subject: [PATCH 10/18] [RESOURCE] Fix CI formatting and static analysis issues --- sdk/src/resource/CMakeLists.txt | 2 +- sdk/src/resource/entity.cc | 4 ++++ sdk/src/resource/resource.cc | 12 ++++++------ sdk/test/resource/entity_test.cc | 1 + sdk/test/resource/resource_test.cc | 3 ++- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sdk/src/resource/CMakeLists.txt b/sdk/src/resource/CMakeLists.txt index 040736c464..062dce856e 100644 --- a/sdk/src/resource/CMakeLists.txt +++ b/sdk/src/resource/CMakeLists.txt @@ -1,7 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -add_library(opentelemetry_resources resource.cc resource_detector.cc entity.cc +add_library(opentelemetry_resources resource.cc resource_detector.cc entity.cc detail/percent_decode.cc) set_target_properties(opentelemetry_resources PROPERTIES EXPORT_NAME resources) diff --git a/sdk/src/resource/entity.cc b/sdk/src/resource/entity.cc index 2616c01977..285fc42bef 100644 --- a/sdk/src/resource/entity.cc +++ b/sdk/src/resource/entity.cc @@ -4,7 +4,11 @@ #include "opentelemetry/sdk/resource/entity.h" #include +#include +#include +#include +#include "opentelemetry/nostd/variant.h" OPENTELEMETRY_BEGIN_NAMESPACE namespace sdk { diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index 308ad56d0d..07729ad147 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -11,6 +11,7 @@ #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/global_log_handler.h" +#include "opentelemetry/sdk/resource/entity.h" #include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/resource/resource_detector.h" #include "opentelemetry/sdk/version/version.h" @@ -57,17 +58,16 @@ std::unordered_map BuildTypeRanks(const Resource &old_ for (std::size_t i = 0; i < updating_entities.size(); ++i) { const std::string &type = updating_entities[i].GetType(); - if (rank.find(type) == rank.end()) - { - rank.emplace(type,i); - } + rank.emplace(type, i); } + std::size_t old_only = updating_entities.size(); for (const auto &entity : old_resource.GetEntities()) { - if (rank.find(entity.GetType()) == rank.end()) + auto result = rank.emplace(entity.GetType(), old_only); + if (result.second) { - rank.emplace(entity.GetType(), old_only++); + ++old_only; } } return rank; diff --git a/sdk/test/resource/entity_test.cc b/sdk/test/resource/entity_test.cc index c77e3d075b..e9d6d3761a 100644 --- a/sdk/test/resource/entity_test.cc +++ b/sdk/test/resource/entity_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include "opentelemetry/nostd/variant.h" diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 39501ad195..0bf2a9de57 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -8,9 +8,10 @@ #include #include #include - +#include #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/attribute_utils.h" +#include "opentelemetry/sdk/resource/entity.h" #include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/resource/resource_detector.h" #include "opentelemetry/sdk/version/version.h" From 1e9bc31655ccfad496e0220d588a4ff3c57c5ccb Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Fri, 28 Aug 2026 05:28:00 +0530 Subject: [PATCH 11/18] Update resource_test.cc for placement change of include statement --- sdk/test/resource/resource_test.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 0bf2a9de57..bfdbfb046f 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +#include "opentelemetry/sdk/resource/resource.h" #include #include #include @@ -12,7 +13,6 @@ #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/attribute_utils.h" #include "opentelemetry/sdk/resource/entity.h" -#include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/resource/resource_detector.h" #include "opentelemetry/sdk/version/version.h" #include "opentelemetry/semconv/service_attributes.h" From 7a749a22c8e8496c113937051b503b3cbf041467 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Fri, 28 Aug 2026 08:01:57 +0530 Subject: [PATCH 12/18] [RESOURCE] Fix clang-tidy copy warnings --- sdk/test/resource/resource_test.cc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index bfdbfb046f..972b963512 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -446,6 +446,12 @@ TEST(ResourceTest, CopyAndAssignmentPreservesNewMembers) EXPECT_EQ(copied.GetAttributes(), original.GetAttributes()); EXPECT_EQ(copied.GetSchemaURL(), original.GetSchemaURL()); + copied = Resource(); + EXPECT_TRUE(original.GetEntities().empty()); + EXPECT_EQ(original.GetUnassociatedAttributes(), attributes); + EXPECT_EQ(original.GetAttributes(), attributes); + EXPECT_EQ(original.GetSchemaURL(), schema_url); + Resource assigned; assigned = original; EXPECT_TRUE(assigned.GetEntities().empty()); @@ -633,6 +639,12 @@ TEST(ResourceTest, CopyAndAssignmentPreservesEntities) EXPECT_EQ(copied.GetSchemaURL(), original.GetSchemaURL()); EXPECT_EQ(copied.GetSchemaURL(), schema_url); + copied = Resource(); + ASSERT_EQ(original.GetEntities().size(), 1); + EXPECT_EQ(original.GetEntities()[0], host); + EXPECT_EQ(original.GetUnassociatedAttributes(), attributes); + EXPECT_EQ(original.GetSchemaURL(), schema_url); + Resource assigned; assigned = original; ASSERT_EQ(assigned.GetEntities().size(), 1); @@ -853,6 +865,11 @@ TEST(ResourceTest, MergeCopyAssignmentPreservesMergedEntities) EXPECT_EQ(copied.GetAttributes(), merged.GetAttributes()); EXPECT_EQ(copied.GetSchemaURL(), merged.GetSchemaURL()); + copied = Resource(); + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetEntities()[0], host); + EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("env")), "prod"); + Resource assigned; assigned = merged; EXPECT_EQ(assigned.GetEntities(), merged.GetEntities()); From 8fbcaab82fb46e01a6f8ebe53126522513421e8d Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Sat, 5 Sep 2026 07:25:23 +0530 Subject: [PATCH 13/18] Fix Entity constructor and special members --- sdk/include/opentelemetry/sdk/resource/entity.h | 9 +-------- sdk/src/resource/entity.cc | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/sdk/include/opentelemetry/sdk/resource/entity.h b/sdk/include/opentelemetry/sdk/resource/entity.h index 7f9ba7e94e..e6340cc553 100644 --- a/sdk/include/opentelemetry/sdk/resource/entity.h +++ b/sdk/include/opentelemetry/sdk/resource/entity.h @@ -22,14 +22,7 @@ class Entity Entity(const std::string &type, const ResourceAttributes &identity, const ResourceAttributes &description = ResourceAttributes{}, - const std::string &schema_url = std::string{}) noexcept; - - Entity(const Entity &) = default; - Entity(Entity &&) = default; - Entity &operator=(const Entity &) = default; - Entity &operator=(Entity &&) = default; - - ~Entity() = default; + const std::string &schema_url = std::string{}); const std::string &GetType() const noexcept; const ResourceAttributes &GetIdentity() const noexcept; diff --git a/sdk/src/resource/entity.cc b/sdk/src/resource/entity.cc index 285fc42bef..52f25b1e7c 100644 --- a/sdk/src/resource/entity.cc +++ b/sdk/src/resource/entity.cc @@ -18,7 +18,7 @@ namespace resource Entity::Entity(const std::string &type, const ResourceAttributes &identity, const ResourceAttributes &description, - const std::string &schema_url) noexcept + const std::string &schema_url) : type_(type), identity_(identity), description_(description), schema_url_(schema_url) { for (const auto &kv : identity_) From 721fd24d9fb5262bcd3719f9d6931d5e513447f6 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Wed, 9 Sep 2026 05:02:14 +0530 Subject: [PATCH 14/18] Document Resource merge algorithms --- .../opentelemetry/sdk/resource/resource.h | 32 +++++++++++++++++++ sdk/src/resource/resource.cc | 23 ++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/sdk/include/opentelemetry/sdk/resource/resource.h b/sdk/include/opentelemetry/sdk/resource/resource.h index 5d233b86e1..1eef6e0fd6 100644 --- a/sdk/include/opentelemetry/sdk/resource/resource.h +++ b/sdk/include/opentelemetry/sdk/resource/resource.h @@ -106,9 +106,41 @@ class Resource static Resource &GetDefault(); private: + /** + * Normalizes entities supplied at construction time. + * + * Drops invalid entities, duplicate types, and entities whose identity or + * description keys conflict with an already-accepted entity (first wins). + * Stores survivors in `entities_`, removes their attribute keys from + * `unassociated_attributes_`, and updates `schema_url_` when any entity survives + * (common entity schema URL, or empty if they differ). + */ void NormalizeEntities(const std::vector &entities) noexcept; + + /** + * Rebuilds the flattened `attributes_` cache from `entities_` (identity and + * description) followed by `unassociated_attributes_`. + */ void RefreshFlattenedAttributes() noexcept; + /** + * Legacy resource merge when neither resource contains entities. + * + * Spec: Resource SDK, "Merge behavior without Entities" (since 1.60.0): + * https://opentelemetry.io/docs/specs/otel/resource/sdk/#merge-behavior-without-entities + */ + Resource MergeWithoutEntities(const Resource &other) const noexcept; + + /** + * Entity-aware resource merge when either resource contains entities. + * + * Spec: Resource SDK, "Merge behavior with entities" (Development, 1.60.0), + * which requires the resource data model merge algorithm: + * https://opentelemetry.io/docs/specs/otel/resource/sdk/#merge-behavior-with-entities + * https://opentelemetry.io/docs/specs/otel/resource/data-model/#merging-resources + */ + Resource MergeWithEntities(const Resource &other) const noexcept; + std::vector entities_; ResourceAttributes unassociated_attributes_; ResourceAttributes attributes_; diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index 07729ad147..2b24bb264e 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -198,12 +198,27 @@ Resource Resource::Merge(const Resource &other) const noexcept { if (entities_.empty() && other.entities_.empty()) { - ResourceAttributes merged_resource_attributes(other.attributes_); - merged_resource_attributes.insert(attributes_.begin(), attributes_.end()); - return Resource(merged_resource_attributes, - other.schema_url_.empty() ? schema_url_ : other.schema_url_); + return MergeWithoutEntities(other); } + return MergeWithEntities(other); +} +// Legacy resource merge when neither resource contains entities. +// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#merge-behavior-without-entities +Resource Resource::MergeWithoutEntities(const Resource &other) const noexcept +{ + ResourceAttributes merged_resource_attributes(other.attributes_); + merged_resource_attributes.insert(attributes_.begin(), attributes_.end()); + return Resource(merged_resource_attributes, + other.schema_url_.empty() ? schema_url_ : other.schema_url_); +} + +// Entity-aware resource merge when either resource contains entities. +// Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#merge-behavior-with-entities +// Resource data model: +// https://opentelemetry.io/docs/specs/otel/resource/data-model/#merging-resources +Resource Resource::MergeWithEntities(const Resource &other) const noexcept +{ const Resource &updating = other; auto rank = BuildTypeRanks(*this, updating); From 8bc70ef4293c3cc42965b16a07485d970c8ebd00 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Wed, 9 Sep 2026 19:26:39 +0530 Subject: [PATCH 15/18] Fix Resource merge ordering for entity schema --- sdk/src/resource/resource.cc | 160 ++++++++++++++++++++--------- sdk/test/resource/resource_test.cc | 32 ++++++ 2 files changed, 141 insertions(+), 51 deletions(-) diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index 2b24bb264e..cdbcd81137 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -73,6 +73,88 @@ std::unordered_map BuildTypeRanks(const Resource &old_ return rank; } +std::vector EvictInterEntityKeyConflicts(const std::vector &entities) +{ + std::vector accepted; + std::unordered_set accepted_keys; + accepted.reserve(entities.size()); + + for (const auto &entity : entities) + { + bool key_conflict = false; + for (const auto &kv : entity.GetIdentity()) + { + if (accepted_keys.find(kv.first) != accepted_keys.end()) + { + key_conflict = true; + break; + } + } + if (!key_conflict) + { + for (const auto &kv : entity.GetDescription()) + { + if (accepted_keys.find(kv.first) != accepted_keys.end()) + { + key_conflict = true; + break; + } + } + } + if (key_conflict) + { + OTEL_INTERNAL_LOG_WARN("[Resource] Dropping Entity due to attribute key conflict."); + continue; + } + + for (const auto &kv : entity.GetIdentity()) + { + accepted_keys.insert(kv.first); + } + for (const auto &kv : entity.GetDescription()) + { + accepted_keys.insert(kv.first); + } + accepted.push_back(entity); + } + + return accepted; +} + +std::string SchemaUrlFromEntities(const std::vector &entities) +{ + if (entities.empty()) + { + return std::string{}; + } + + const std::string &common_schema_url = entities.front().GetSchemaURL(); + for (const auto &entity : entities) + { + if (entity.GetSchemaURL() != common_schema_url) + { + return std::string{}; + } + } + return common_schema_url; +} + +void RemoveEntityKeysFromUnassociated(ResourceAttributes &unassociated, + const std::vector &entities) +{ + for (const auto &entity : entities) + { + for (const auto &kv : entity.GetIdentity()) + { + unassociated.erase(kv.first); + } + for (const auto &kv : entity.GetDescription()) + { + unassociated.erase(kv.first); + } + } +} + } // namespace Resource::Resource() noexcept : entities_(), unassociated_attributes_(), schema_url_() @@ -103,10 +185,9 @@ Resource::Resource(const ResourceAttributes &attributes, void Resource::NormalizeEntities(const std::vector &entities) noexcept { - std::vector accepted; + std::vector filtered; std::unordered_set accepted_types; - std::unordered_set accepted_keys; - accepted.reserve(entities.size()); + filtered.reserve(entities.size()); for (const auto &entity : entities) { @@ -122,64 +203,27 @@ void Resource::NormalizeEntities(const std::vector &entities) noexcept continue; } - bool key_conflict = false; - for (const auto &kv : entity.GetIdentity()) - { - if (accepted_keys.find(kv.first) != accepted_keys.end()) - { - key_conflict = true; - break; - } - } - if (!key_conflict) - { - for (const auto &kv : entity.GetDescription()) - { - if (accepted_keys.find(kv.first) != accepted_keys.end()) - { - key_conflict = true; - break; - } - } - } - if (key_conflict) - { - OTEL_INTERNAL_LOG_WARN("[Resource] Dropping Entity due to attribute key conflict."); - continue; - } - accepted_types.insert(entity.GetType()); + filtered.push_back(entity); + } + + entities_ = EvictInterEntityKeyConflicts(filtered); + + for (const auto &entity : entities_) + { for (const auto &kv : entity.GetIdentity()) { - accepted_keys.insert(kv.first); + unassociated_attributes_.erase(kv.first); } for (const auto &kv : entity.GetDescription()) { - accepted_keys.insert(kv.first); + unassociated_attributes_.erase(kv.first); } - accepted.push_back(entity); - } - - entities_ = std::move(accepted); - - for (const auto &key : accepted_keys) - { - unassociated_attributes_.erase(key); } if (!entities_.empty()) { - const std::string &common_schema_url = entities_.front().GetSchemaURL(); - bool all_equal = true; - for (const auto &entity : entities_) - { - if (entity.GetSchemaURL() != common_schema_url) - { - all_equal = false; - break; - } - } - schema_url_ = all_equal ? common_schema_url : std::string{}; + schema_url_ = SchemaUrlFromEntities(entities_); } } @@ -286,7 +330,21 @@ Resource Resource::MergeWithEntities(const Resource &other) const noexcept const std::string classic_schema = updating.GetSchemaURL().empty() ? GetSchemaURL() : updating.GetSchemaURL(); - return Resource(unassociated, classic_schema, after_eviction); + if (after_eviction.empty()) + { + return Resource(unassociated, classic_schema); + } + + const std::string merged_schema = SchemaUrlFromEntities(after_eviction); + RemoveEntityKeysFromUnassociated(unassociated, after_eviction); + std::vector final_entities = EvictInterEntityKeyConflicts(after_eviction); + + Resource result; + result.unassociated_attributes_ = std::move(unassociated); + result.schema_url_ = merged_schema; + result.entities_ = std::move(final_entities); + result.RefreshFlattenedAttributes(); + return result; } Resource Resource::Create(const ResourceAttributes &attributes, const std::string &schema_url) diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 972b963512..f100a078fe 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -836,6 +836,38 @@ TEST(ResourceTest, MergeMixedEntitySchemaUrls) ASSERT_EQ(merged.GetEntities().size(), 2); } +TEST(ResourceTest, MergeInterEntityConflictPreservesPreEvictionBlankSchema) +{ + const std::string schema_a = "https://opentelemetry.io/schemas/1.21.0"; + const std::string schema_b = "https://opentelemetry.io/schemas/1.22.0"; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{{"env", "prod"}}, + schema_a); + Entity service("service", ResourceAttributes{{"service.name", "app"}}, + ResourceAttributes{{"env", "dev"}}, schema_b); + Resource old_resource(ResourceAttributes{}, std::string{}, {host}); + Resource updating(ResourceAttributes{}, std::string{}, {service}); + auto merged = old_resource.Merge(updating); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetEntities()[0].GetType(), "service"); + EXPECT_TRUE(merged.GetSchemaURL().empty()); +} + +TEST(ResourceTest, MergeInterEntityConflictPreservesPreEvictionCommonSchema) +{ + const std::string schema_url = "https://opentelemetry.io/schemas/1.21.0"; + Entity host("host", ResourceAttributes{{"host.id", "H1"}}, ResourceAttributes{{"env", "prod"}}, + schema_url); + Entity service("service", ResourceAttributes{{"service.name", "app"}}, + ResourceAttributes{{"env", "dev"}}, schema_url); + Resource old_resource(ResourceAttributes{}, std::string{}, {host}); + Resource updating(ResourceAttributes{}, std::string{}, {service}); + auto merged = old_resource.Merge(updating); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(merged.GetSchemaURL(), schema_url); +} + TEST(ResourceTest, MergeUpdatingLooseBeatsOldLoose) { Resource old_resource(ResourceAttributes{{"foo", "old"}, {"keep", "old"}}, std::string{}, From 7a0f12b731e4ddc7e42829251484c6dad6954bce Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Thu, 10 Sep 2026 19:22:33 +0530 Subject: [PATCH 16/18] Optimize attribute-only Resource storage --- .../opentelemetry/sdk/resource/resource.h | 6 +++-- sdk/src/resource/resource.cc | 24 +++++++++++++------ sdk/test/resource/resource_test.cc | 14 +++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/sdk/include/opentelemetry/sdk/resource/resource.h b/sdk/include/opentelemetry/sdk/resource/resource.h index 1eef6e0fd6..3ad51fd530 100644 --- a/sdk/include/opentelemetry/sdk/resource/resource.h +++ b/sdk/include/opentelemetry/sdk/resource/resource.h @@ -118,8 +118,10 @@ class Resource void NormalizeEntities(const std::vector &entities) noexcept; /** - * Rebuilds the flattened `attributes_` cache from `entities_` (identity and - * description) followed by `unassociated_attributes_`. + * Rebuilds the flattened `attributes_` cache for entity-aware Resources from + * `entities_` (identity and description) followed by + * `unassociated_attributes_`. Entity-free Resources leave the cache empty + * and return `unassociated_attributes_` directly from GetAttributes(). */ void RefreshFlattenedAttributes() noexcept; diff --git a/sdk/src/resource/resource.cc b/sdk/src/resource/resource.cc index cdbcd81137..ca8d55a3b3 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -230,6 +230,11 @@ void Resource::NormalizeEntities(const std::vector &entities) noexcept void Resource::RefreshFlattenedAttributes() noexcept { attributes_.clear(); + if (entities_.empty()) + { + return; + } + for (const auto &entity : entities_) { attributes_.insert(entity.GetIdentity().begin(), entity.GetIdentity().end()); @@ -251,8 +256,9 @@ Resource Resource::Merge(const Resource &other) const noexcept // Spec: https://opentelemetry.io/docs/specs/otel/resource/sdk/#merge-behavior-without-entities Resource Resource::MergeWithoutEntities(const Resource &other) const noexcept { - ResourceAttributes merged_resource_attributes(other.attributes_); - merged_resource_attributes.insert(attributes_.begin(), attributes_.end()); + ResourceAttributes merged_resource_attributes(other.unassociated_attributes_); + merged_resource_attributes.insert(unassociated_attributes_.begin(), + unassociated_attributes_.end()); return Resource(merged_resource_attributes, other.schema_url_.empty() ? schema_url_ : other.schema_url_); } @@ -360,17 +366,21 @@ Resource Resource::Create(const ResourceAttributes &attributes, auto resource = Resource::GetDefault().Merge(otel_resource).Merge(Resource{attributes, schema_url, entities}); - if (resource.attributes_.find(semconv::service::kServiceName) == resource.attributes_.end()) + const auto &resource_attributes = resource.GetAttributes(); + if (resource_attributes.find(semconv::service::kServiceName) == resource_attributes.end()) { std::string default_service_name = "unknown_service"; auto it_process_executable_name = - resource.attributes_.find(semconv::process::kProcessExecutableName); - if (it_process_executable_name != resource.attributes_.end()) + resource_attributes.find(semconv::process::kProcessExecutableName); + if (it_process_executable_name != resource_attributes.end()) { default_service_name += ":" + nostd::get(it_process_executable_name->second); } resource.unassociated_attributes_[semconv::service::kServiceName] = default_service_name; - resource.RefreshFlattenedAttributes(); + if (!resource.entities_.empty()) + { + resource.RefreshFlattenedAttributes(); + } } return resource; } @@ -393,7 +403,7 @@ Resource &Resource::GetDefault() const ResourceAttributes &Resource::GetAttributes() const noexcept { - return attributes_; + return entities_.empty() ? unassociated_attributes_ : attributes_; } const std::string &Resource::GetSchemaURL() const noexcept diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index f100a078fe..761a2f60b8 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -15,6 +15,7 @@ #include "opentelemetry/sdk/resource/entity.h" #include "opentelemetry/sdk/resource/resource_detector.h" #include "opentelemetry/sdk/version/version.h" +#include "opentelemetry/semconv/incubating/process_attributes.h" #include "opentelemetry/semconv/service_attributes.h" #include "opentelemetry/semconv/telemetry_attributes.h" @@ -421,6 +422,17 @@ TEST(ResourceTest, CreateUnassociatedMatchesFlattened) "backend"); } +TEST(ResourceTest, CreateServiceNameFallbackUsesProcessExecutableName) +{ + ResourceAttributes attributes = {{semconv::process::kProcessExecutableName, "otel-test"}}; + auto resource = Resource::Create(attributes); + + EXPECT_TRUE(resource.GetEntities().empty()); + EXPECT_EQ(resource.GetUnassociatedAttributes(), resource.GetAttributes()); + EXPECT_EQ(nostd::get(resource.GetAttributes().at(semconv::service::kServiceName)), + "unknown_service:otel-test"); +} + TEST(ResourceTest, MergeUnassociatedMatchesFlattened) { TestResource resource1(ResourceAttributes({{"service", "backend"}})); @@ -808,6 +820,8 @@ TEST(ResourceTest, MergeAllEntitiesDroppedUsesClassicSchema) EXPECT_TRUE(merged.GetEntities().empty()); EXPECT_EQ(merged.GetSchemaURL(), updating_schema); EXPECT_EQ(nostd::get(merged.GetUnassociatedAttributes().at("host.id")), "H2"); + EXPECT_EQ(merged.GetAttributes(), merged.GetUnassociatedAttributes()); + EXPECT_EQ(nostd::get(merged.GetAttributes().at("host.id")), "H2"); } TEST(ResourceTest, MergeSurvivingEntitiesSetResourceSchema) From f4bf0971ff74bd4a8d5876961728501f3e69fa47 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Sun, 13 Sep 2026 10:20:38 +0530 Subject: [PATCH 17/18] Normalize integer Entity identities --- .../opentelemetry/sdk/resource/entity.h | 5 ++ sdk/src/resource/entity.cc | 80 +++++++++++++++++- sdk/test/resource/entity_test.cc | 83 +++++++++++++++++++ sdk/test/resource/resource_test.cc | 17 ++++ 4 files changed, 184 insertions(+), 1 deletion(-) diff --git a/sdk/include/opentelemetry/sdk/resource/entity.h b/sdk/include/opentelemetry/sdk/resource/entity.h index e6340cc553..64012fdbd2 100644 --- a/sdk/include/opentelemetry/sdk/resource/entity.h +++ b/sdk/include/opentelemetry/sdk/resource/entity.h @@ -19,6 +19,11 @@ using ResourceAttributes = opentelemetry::sdk::common::AttributeMap; class Entity { public: + /** + * Constructs an Entity. Integer identity values (and integer arrays) are + * canonicalized to int64_t when they fit, so C++ integer widths do not + * affect identity equality or merge. Description values are not rewritten. + */ Entity(const std::string &type, const ResourceAttributes &identity, const ResourceAttributes &description = ResourceAttributes{}, diff --git a/sdk/src/resource/entity.cc b/sdk/src/resource/entity.cc index 52f25b1e7c..de47f954f3 100644 --- a/sdk/src/resource/entity.cc +++ b/sdk/src/resource/entity.cc @@ -3,17 +3,94 @@ #include "opentelemetry/sdk/resource/entity.h" +#include +#include #include -#include #include #include #include "opentelemetry/nostd/variant.h" +#include "opentelemetry/sdk/common/attribute_utils.h" + OPENTELEMETRY_BEGIN_NAMESPACE namespace sdk { namespace resource { +namespace +{ + +namespace nostd = opentelemetry::nostd; +using OwnedAttributeValue = opentelemetry::sdk::common::OwnedAttributeValue; + +bool Uint64FitsInt64(std::uint64_t value) noexcept +{ + return value <= static_cast(std::numeric_limits::max()); +} + +template +OwnedAttributeValue WidenIntegerArray(const std::vector &src) +{ + std::vector dst; + dst.reserve(src.size()); + for (T value : src) + { + dst.push_back(static_cast(value)); + } + return OwnedAttributeValue{std::move(dst)}; +} + +OwnedAttributeValue NormalizeIntegerIdentityValue(const OwnedAttributeValue &value) +{ + if (nostd::holds_alternative(value)) + { + return OwnedAttributeValue{static_cast(nostd::get(value))}; + } + if (nostd::holds_alternative(value)) + { + return OwnedAttributeValue{static_cast(nostd::get(value))}; + } + if (nostd::holds_alternative(value)) + { + const std::uint64_t raw = nostd::get(value); + if (Uint64FitsInt64(raw)) + { + return OwnedAttributeValue{static_cast(raw)}; + } + return value; + } + if (nostd::holds_alternative>(value)) + { + return WidenIntegerArray(nostd::get>(value)); + } + if (nostd::holds_alternative>(value)) + { + return WidenIntegerArray(nostd::get>(value)); + } + if (nostd::holds_alternative>(value)) + { + const auto &src = nostd::get>(value); + for (std::uint64_t element : src) + { + if (!Uint64FitsInt64(element)) + { + return value; + } + } + return WidenIntegerArray(src); + } + return value; +} + +void NormalizeIdentityIntegers(ResourceAttributes &identity) +{ + for (auto &kv : identity) + { + kv.second = NormalizeIntegerIdentityValue(kv.second); + } +} + +} // namespace Entity::Entity(const std::string &type, const ResourceAttributes &identity, @@ -21,6 +98,7 @@ Entity::Entity(const std::string &type, const std::string &schema_url) : type_(type), identity_(identity), description_(description), schema_url_(schema_url) { + NormalizeIdentityIntegers(identity_); for (const auto &kv : identity_) { description_.erase(kv.first); diff --git a/sdk/test/resource/entity_test.cc b/sdk/test/resource/entity_test.cc index e9d6d3761a..6e9d61e95f 100644 --- a/sdk/test/resource/entity_test.cc +++ b/sdk/test/resource/entity_test.cc @@ -2,9 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include +#include +#include #include #include #include +#include #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/resource/entity.h" @@ -126,3 +130,82 @@ TEST(EntityTest, Equality) "https://opentelemetry.io/schemas/1.1.0"); EXPECT_FALSE(a == different_schema); } + +TEST(EntityTest, IdentityInt32EqualsInt64) +{ + Entity as_int32("process", ResourceAttributes{{"process.pid", std::int32_t{123}}}); + Entity as_int64("process", ResourceAttributes{{"process.pid", std::int64_t{123}}}); + + EXPECT_TRUE(as_int32 == as_int64); + EXPECT_EQ(nostd::get(as_int32.GetIdentity().at("process.pid")), std::int64_t{123}); + EXPECT_TRUE(nostd::holds_alternative(as_int32.GetIdentity().at("process.pid"))); +} + +TEST(EntityTest, IdentityUint32EqualsInt64) +{ + Entity as_uint32("process", ResourceAttributes{{"process.pid", std::uint32_t{123}}}); + Entity as_int64("process", ResourceAttributes{{"process.pid", std::int64_t{123}}}); + + EXPECT_TRUE(as_uint32 == as_int64); + EXPECT_TRUE(nostd::holds_alternative(as_uint32.GetIdentity().at("process.pid"))); +} + +TEST(EntityTest, IdentityUint64InRangeEqualsInt64) +{ + const std::uint64_t in_range = + static_cast(std::numeric_limits::max()); + Entity as_uint64("process", ResourceAttributes{{"process.pid", in_range}}); + Entity as_int64("process", + ResourceAttributes{{"process.pid", std::numeric_limits::max()}}); + + EXPECT_TRUE(as_uint64 == as_int64); + EXPECT_TRUE(nostd::holds_alternative(as_uint64.GetIdentity().at("process.pid"))); +} + +TEST(EntityTest, IdentityUint64OverflowRemainsDistinct) +{ + const std::uint64_t overflow = std::numeric_limits::max(); + Entity as_uint64("process", ResourceAttributes{{"process.pid", overflow}}); + Entity as_int64("process", + ResourceAttributes{{"process.pid", std::numeric_limits::max()}}); + Entity as_negative("process", ResourceAttributes{{"process.pid", std::int64_t{-1}}}); + + EXPECT_TRUE(nostd::holds_alternative(as_uint64.GetIdentity().at("process.pid"))); + EXPECT_EQ(nostd::get(as_uint64.GetIdentity().at("process.pid")), overflow); + EXPECT_FALSE(as_uint64 == as_int64); + EXPECT_FALSE(as_uint64 == as_negative); +} + +TEST(EntityTest, IdentityIntegerArrayNormalization) +{ + const std::array ids32 = {1, 2, 3}; + const std::array ids_u32 = {1, 2, 3}; + const std::array ids64 = {1, 2, 3}; + const std::array ids_u64 = {1, 2, 3}; + Entity as_int32("host", ResourceAttributes{{"ids", nostd::span{ids32}}}); + Entity as_uint32("host", ResourceAttributes{{"ids", nostd::span{ids_u32}}}); + Entity as_int64("host", ResourceAttributes{{"ids", nostd::span{ids64}}}); + Entity as_uint64_fit("host", + ResourceAttributes{{"ids", nostd::span{ids_u64}}}); + + EXPECT_TRUE(as_int32 == as_int64); + EXPECT_TRUE(as_uint32 == as_int64); + EXPECT_TRUE(as_uint64_fit == as_int64); + EXPECT_TRUE( + nostd::holds_alternative>(as_int32.GetIdentity().at("ids"))); + + const std::uint64_t overflow = std::numeric_limits::max(); + const std::array overflow_ids = {1, overflow}; + Entity overflow_array( + "host", ResourceAttributes{{"ids", nostd::span{overflow_ids}}}); + EXPECT_TRUE( + nostd::holds_alternative>(overflow_array.GetIdentity().at("ids"))); + EXPECT_FALSE(overflow_array == as_int64); +} + +TEST(EntityTest, DescriptionIntegersAreNotNormalized) +{ + Entity identity("service", ResourceAttributes{{"service.name", "app"}}, + ResourceAttributes{{"count", std::int32_t{7}}}); + EXPECT_TRUE(nostd::holds_alternative(identity.GetDescription().at("count"))); +} diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 761a2f60b8..8f455deca5 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -742,6 +742,23 @@ TEST(ResourceTest, MergeSameTypeSameIdentityOverlaysDescription) EXPECT_EQ(merged.GetEntities()[0].GetSchemaURL(), schema_url); } +TEST(ResourceTest, MergeIntegerWidthIdentityOverlaysDescription) +{ + const std::string schema_url = "https://opentelemetry.io/schemas/1.21.0"; + Entity old_process("process", ResourceAttributes{{"process.pid", std::int32_t{123}}}, + ResourceAttributes{{"process.command", "old"}}, schema_url); + Entity updating_process("process", ResourceAttributes{{"process.pid", std::int64_t{123}}}, + ResourceAttributes{{"process.command", "new"}}, schema_url); + auto merged = Resource(ResourceAttributes{}, std::string{}, {old_process}) + .Merge(Resource(ResourceAttributes{}, std::string{}, {updating_process})); + + ASSERT_EQ(merged.GetEntities().size(), 1); + EXPECT_EQ(nostd::get(merged.GetEntities()[0].GetIdentity().at("process.pid")), + std::int64_t{123}); + EXPECT_EQ(nostd::get(merged.GetEntities()[0].GetDescription().at("process.command")), + "new"); +} + TEST(ResourceTest, MergeSameTypeDifferentIdentityKeepsOld) { Entity old_host("host", ResourceAttributes{{"host.id", "H1"}}); From 4f26c2175d3aea6f91a66f0305b514f959ef1278 Mon Sep 17 00:00:00 2001 From: Shashank RM Date: Sun, 13 Sep 2026 20:23:18 +0530 Subject: [PATCH 18/18] Include what you use fix --- sdk/src/resource/entity.cc | 1 + sdk/test/resource/entity_test.cc | 2 ++ 2 files changed, 3 insertions(+) diff --git a/sdk/src/resource/entity.cc b/sdk/src/resource/entity.cc index de47f954f3..e8fa70e9a7 100644 --- a/sdk/src/resource/entity.cc +++ b/sdk/src/resource/entity.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/sdk/test/resource/entity_test.cc b/sdk/test/resource/entity_test.cc index 6e9d61e95f..00b764c883 100644 --- a/sdk/test/resource/entity_test.cc +++ b/sdk/test/resource/entity_test.cc @@ -10,6 +10,8 @@ #include #include +#include "opentelemetry/nostd/span.h" +#include "opentelemetry/nostd/utility.h" #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/resource/entity.h"