diff --git a/CHANGELOG.md b/CHANGELOG.md index 462236afa6..5284e1ac71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ 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 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/include/opentelemetry/sdk/resource/resource.h b/sdk/include/opentelemetry/sdk/resource/resource.h index 2e462122f5..5d233b86e1 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 @@ -25,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; @@ -34,15 +50,21 @@ 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 - * 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. + * + * 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. * - * 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 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. @@ -61,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. */ @@ -74,6 +106,11 @@ class Resource static Resource &GetDefault(); private: + void NormalizeEntities(const std::vector &entities) noexcept; + void RefreshFlattenedAttributes() noexcept; + + std::vector entities_; + ResourceAttributes unassociated_attributes_; ResourceAttributes attributes_; std::string schema_url_; }; diff --git a/sdk/src/resource/CMakeLists.txt b/sdk/src/resource/CMakeLists.txt index 94e01d99a0..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 +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 new file mode 100644 index 0000000000..285fc42bef --- /dev/null +++ b/sdk/src/resource/entity.cc @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/sdk/resource/entity.h" + +#include +#include +#include +#include + +#include "opentelemetry/nostd/variant.h" +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/src/resource/resource.cc b/sdk/src/resource/resource.cc index 95b41498f8..07729ad147 100644 --- a/sdk/src/resource/resource.cc +++ b/sdk/src/resource/resource.cc @@ -1,11 +1,17 @@ // 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" +#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" @@ -19,30 +25,267 @@ 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(); + rank.emplace(type, i); + } -Resource::Resource() noexcept : attributes_(), schema_url_() {} + std::size_t old_only = updating_entities.size(); + for (const auto &entity : old_resource.GetEntities()) + { + auto result = rank.emplace(entity.GetType(), old_only); + if (result.second) + { + ++old_only; + } + } + return rank; +} + +} // namespace + +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(); +} + +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_.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 { - 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.attributes_.find(semconv::service::kServiceName) == resource.attributes_.end()) { @@ -53,7 +296,8 @@ Resource Resource::Create(const ResourceAttributes &attributes, const std::strin { 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 +328,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/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..e9d6d3761a --- /dev/null +++ b/sdk/test/resource/entity_test.cc @@ -0,0 +1,128 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#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); +} diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 4a80ca2a7a..972b963512 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 @@ -8,10 +9,10 @@ #include #include #include - +#include #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/attribute_utils.h" -#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/entity.h" #include "opentelemetry/sdk/resource/resource_detector.h" #include "opentelemetry/sdk/version/version.h" #include "opentelemetry/semconv/service_attributes.h" @@ -358,4 +359,571 @@ 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()); + + 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()); + EXPECT_EQ(assigned.GetUnassociatedAttributes(), original.GetUnassociatedAttributes()); + EXPECT_EQ(assigned.GetAttributes(), original.GetAttributes()); + 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); + + 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); + 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()); +} + +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()); + + 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()); + 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