From aeec00f85e59c6f7699a2d2ccae2f151c21d6f1f Mon Sep 17 00:00:00 2001 From: Hasv07 Date: Fri, 19 Dec 2025 15:34:45 +0000 Subject: [PATCH 1/6] [SDK] Implement EnvEntityDetector for OTEL_ENTITIES parsing (#3652) Changes: Add EnvEntityDetector class to parse OTEL_ENTITIES environment variable Parse entity format: type{id_attrs}[desc_attrs]@schema_url Support percent-encoding for reserved characters Handle duplicate entities, conflicting attributes, and malformed input per spec Include comprehensive test coverage Fix schema URL handling to pass to Resource Details: EnvEntityDetector::Detect() reads OTEL_ENTITIES and returns Resource with parsed attributes ParseEntities() splits input by semicolons and parses each entity definition ParseSingleEntity() extracts type, id_attrs, desc_attrs, and schema_url from entity string ParseKeyValueList() parses comma-separated key=value pairs PercentDecode() decodes percent-encoded values BuildEntityIdentityKey() creates stable key for duplicate detection Error handling: malformed entities skipped, duplicates use last occurrence, conflicts log warnings Implements entity propagation spec: https://opentelemetry.io/docs/specs/otel/entities/entity-propagation --- .../sdk/resource/resource_detector.h | 11 + sdk/src/resource/CMakeLists.txt | 3 +- sdk/src/resource/env_entity_detector.cc | 336 ++++++++++++++ sdk/test/resource/resource_test.cc | 414 ++++++++++++++++++ 4 files changed, 763 insertions(+), 1 deletion(-) create mode 100644 sdk/src/resource/env_entity_detector.cc diff --git a/sdk/include/opentelemetry/sdk/resource/resource_detector.h b/sdk/include/opentelemetry/sdk/resource/resource_detector.h index f129d3d1bc..13b661092d 100644 --- a/sdk/include/opentelemetry/sdk/resource/resource_detector.h +++ b/sdk/include/opentelemetry/sdk/resource/resource_detector.h @@ -39,6 +39,17 @@ class OTELResourceDetector : public ResourceDetector Resource Detect() noexcept override; }; +/** + * EnvEntityDetector detects entities defined in the OTEL_ENTITIES environment + * variable as specified in the Entity Propagation spec: + * https://opentelemetry.io/docs/specs/otel/entities/entity-propagation/ + */ +class EnvEntityDetector : public ResourceDetector +{ +public: + Resource Detect() noexcept override; +}; + } // namespace resource } // namespace sdk OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/resource/CMakeLists.txt b/sdk/src/resource/CMakeLists.txt index 48b647ec41..42dd5b8c71 100644 --- a/sdk/src/resource/CMakeLists.txt +++ b/sdk/src/resource/CMakeLists.txt @@ -1,7 +1,8 @@ # 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 + env_entity_detector.cc) set_target_properties(opentelemetry_resources PROPERTIES EXPORT_NAME resources) set_target_version(opentelemetry_resources) diff --git a/sdk/src/resource/env_entity_detector.cc b/sdk/src/resource/env_entity_detector.cc new file mode 100644 index 0000000000..1f82113ef7 --- /dev/null +++ b/sdk/src/resource/env_entity_detector.cc @@ -0,0 +1,336 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/common/string_util.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/nostd/variant.h" +#include "opentelemetry/sdk/common/attribute_utils.h" +#include "opentelemetry/sdk/common/env_variables.h" +#include "opentelemetry/sdk/common/global_log_handler.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/resource_detector.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace resource +{ + +namespace +{ + +constexpr const char *kOtelEntities = "OTEL_ENTITIES"; + +struct ParsedEntity +{ + std::string type; + ResourceAttributes id_attrs; + ResourceAttributes desc_attrs; + std::string schema_url; + std::string identity_key; // Pre-computed identity key for duplicate detection +}; + +std::string BuildEntityIdentityKey(const std::string &type, const ResourceAttributes &id_attrs) +{ + using AttrPtr = + const std::pair *; + std::vector items; + items.reserve(id_attrs.size()); + for (const auto &kv : id_attrs) + { + items.push_back(&kv); + } + std::sort(items.begin(), items.end(), [](AttrPtr a, AttrPtr b) { return a->first < b->first; }); + + std::string key = type + "|"; + for (size_t i = 0; i < items.size(); ++i) + { + if (i > 0) + { + key += ","; + } + key += items[i]->first; + key += "="; + key += nostd::get(items[i]->second); + } + return key; +} + +std::string PercentDecode(nostd::string_view value) noexcept +{ + if (value.find('%') == nostd::string_view::npos) + { + return std::string(value); + } + + std::string result; + result.reserve(value.size()); + + auto IsHex = [](char c) { + return std::isdigit(static_cast(c)) || (c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f'); + }; + + auto FromHex = [](char c) -> char { + return static_cast(std::isdigit(static_cast(c)) + ? c - '0' + : std::toupper(static_cast(c)) - 'A' + 10); + }; + + for (size_t i = 0; i < value.size(); ++i) + { + if (value[i] == '%' && i + 2 < value.size() && IsHex(value[i + 1]) && IsHex(value[i + 2])) + { + result.push_back(static_cast((FromHex(value[i + 1]) << 4) | FromHex(value[i + 2]))); + i += 2; + } + else + { + result.push_back(value[i]); + } + } + + return result; +} + +void ParseKeyValueList(const std::string &input, ResourceAttributes &out) +{ + std::istringstream iss(input); + std::string token; + while (std::getline(iss, token, ',')) + { + token = std::string{opentelemetry::common::StringUtil::Trim(token)}; + if (token.empty()) + { + continue; + } + size_t pos = token.find('='); + if (pos == std::string::npos) + { + continue; + } + std::string key = token.substr(0, pos); + std::string value = token.substr(pos + 1); + key = std::string{opentelemetry::common::StringUtil::Trim(key)}; + value = std::string{opentelemetry::common::StringUtil::Trim(value)}; + if (key.empty()) + { + continue; + } + out[key] = PercentDecode(value); + } +} + +bool ParseSingleEntity(const std::string &entity_str, ParsedEntity &out) +{ + if (entity_str.empty()) + { + return false; + } + + // type is everything before first '{' + size_t brace_pos = entity_str.find('{'); + if (brace_pos == std::string::npos || brace_pos == 0) + { + return false; + } + + out.type = std::string{opentelemetry::common::StringUtil::Trim(entity_str.substr(0, brace_pos))}; + + // Validate type matches [a-zA-Z][a-zA-Z0-9._-]* + if (out.type.empty() || !std::isalpha(static_cast(out.type[0]))) + { + return false; + } + for (size_t i = 1; i < out.type.size(); ++i) + { + char c = out.type[i]; + if (!(std::isalnum(static_cast(c)) || c == '.' || c == '_' || c == '-')) + { + return false; + } + } + + // Extract id_attrs in {...} + size_t id_start = brace_pos + 1; + size_t id_end = entity_str.find('}', id_start); + if (id_end == std::string::npos || id_end <= id_start) + { + return false; + } + std::string id_block = std::string{ + opentelemetry::common::StringUtil::Trim(entity_str.substr(id_start, id_end - id_start))}; + ParseKeyValueList(id_block, out.id_attrs); + if (out.id_attrs.empty()) + { + return false; + } + + // Pre-compute identity key for duplicate detection. + out.identity_key = BuildEntityIdentityKey(out.type, out.id_attrs); + + size_t cursor = id_end + 1; + + // Optional desc_attrs in [...] + if (cursor < entity_str.size() && entity_str[cursor] == '[') + { + size_t desc_start = cursor + 1; + size_t desc_end = entity_str.find(']', desc_start); + if (desc_end == std::string::npos || desc_end <= desc_start) + { + return false; + } + std::string desc_block = std::string{opentelemetry::common::StringUtil::Trim( + entity_str.substr(desc_start, desc_end - desc_start))}; + ParseKeyValueList(desc_block, out.desc_attrs); + cursor = desc_end + 1; + } + + // Optional schema URL: '@...' + if (cursor < entity_str.size() && entity_str[cursor] == '@') + { + out.schema_url = + std::string{opentelemetry::common::StringUtil::Trim(entity_str.substr(cursor + 1))}; + + // TODO: Use a proper Schema URL validator when available. + if (out.schema_url.empty() || out.schema_url.find("://") == std::string::npos) + { + OTEL_INTERNAL_LOG_WARN( + "[EnvEntityDetector] Invalid schema URL in OTEL_ENTITIES, ignoring schema URL."); + out.schema_url.clear(); + } + } + + return true; +} + +std::vector ParseEntities(const std::string &entities_str) +{ + std::vector entities; + + std::istringstream iss(entities_str); + std::string token; + while (std::getline(iss, token, ';')) + { + token = std::string{opentelemetry::common::StringUtil::Trim(token)}; + if (token.empty()) + { + continue; + } + ParsedEntity entity; + if (ParseSingleEntity(token, entity)) + { + entities.push_back(std::move(entity)); + } + else + { + OTEL_INTERNAL_LOG_WARN( + "[EnvEntityDetector] Skipping malformed entity definition in OTEL_ENTITIES."); + } + } + + return entities; +} + +} // namespace + +Resource EnvEntityDetector::Detect() noexcept +{ + std::string entities_str; + bool exists = + opentelemetry::sdk::common::GetStringEnvironmentVariable(kOtelEntities, entities_str); + + if (!exists || entities_str.empty()) + { + return ResourceDetector::Create({}); + } + + auto parsed_entities = ParseEntities(entities_str); + if (parsed_entities.empty()) + { + return ResourceDetector::Create({}); + } + + ResourceAttributes resource_attrs; + std::string schema_url; + + std::unordered_map entity_index_by_identity; + entity_index_by_identity.reserve(parsed_entities.size()); + for (size_t i = 0; i < parsed_entities.size(); ++i) + { + const std::string &identity_key = parsed_entities[i].identity_key; + auto it = entity_index_by_identity.find(identity_key); + if (it != entity_index_by_identity.end()) + { + OTEL_INTERNAL_LOG_WARN( + "[EnvEntityDetector] Duplicate entity definition in OTEL_ENTITIES, using last " + "occurrence."); + it->second = i; + continue; + } + entity_index_by_identity.emplace(identity_key, i); + } + + for (size_t i = 0; i < parsed_entities.size(); ++i) + { + const std::string &identity_key = parsed_entities[i].identity_key; + auto it = entity_index_by_identity.find(identity_key); + + // Only process if this is the last occurrence for this identity. + if (it == entity_index_by_identity.end() || it->second != i) + { + continue; + } + + const auto &entity = parsed_entities[i]; + + // Add identifying attributes. + for (const auto &attr : entity.id_attrs) + { + auto existing = resource_attrs.find(attr.first); + if (existing != resource_attrs.end() && + nostd::get(existing->second) != nostd::get(attr.second)) + { + OTEL_INTERNAL_LOG_WARN( + "[EnvEntityDetector] Conflicting identifying attribute in OTEL_ENTITIES, " + "preserving value from last entity."); + } + resource_attrs[attr.first] = attr.second; + } + + // Add descriptive attributes. + for (const auto &attr : entity.desc_attrs) + { + auto existing = resource_attrs.find(attr.first); + if (existing != resource_attrs.end() && + nostd::get(existing->second) != nostd::get(attr.second)) + { + OTEL_INTERNAL_LOG_WARN( + "[EnvEntityDetector] Conflicting descriptive attribute in OTEL_ENTITIES, " + "using value from last entity."); + } + resource_attrs[attr.first] = attr.second; + } + + if (!entity.schema_url.empty()) + { + schema_url = entity.schema_url; + } + } + + return ResourceDetector::Create(resource_attrs, schema_url); +} + +} // namespace resource +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 696509f892..b8fdc42eec 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -292,3 +292,417 @@ TEST(ResourceTest, DerivedResourceDetector) EXPECT_EQ(resource.GetSchemaURL(), detector.schema_url); EXPECT_TRUE(received_attributes.find("key") != received_attributes.end()); } + +#ifndef NO_GETENV +TEST(ResourceTest, EnvEntityDetectorBasic) +{ + setenv("OTEL_ENTITIES", "service{service.name=my-app,service.instance.id=instance-1}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my-app"); + EXPECT_TRUE(received_attributes.find("service.instance.id") != received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "instance-1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorWithDescriptiveAttributes) +{ + setenv("OTEL_ENTITIES", + "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my-app"); + EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "instance-1"); + EXPECT_EQ(nostd::get(received_attributes["service.version"]), "1.0.0"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorMultipleEntities) +{ + setenv("OTEL_ENTITIES", + "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0];" + "host{host.id=host-123}[host.name=web-server-01]", + 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my-app"); + EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "instance-1"); + EXPECT_EQ(nostd::get(received_attributes["service.version"]), "1.0.0"); + EXPECT_EQ(nostd::get(received_attributes["host.id"]), "host-123"); + EXPECT_EQ(nostd::get(received_attributes["host.name"]), "web-server-01"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorPercentEncoding) +{ + setenv("OTEL_ENTITIES", "service{service.name=my%2Capp,service.instance.id=inst-1}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my,app"); + EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "inst-1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorDuplicateEntities) +{ + setenv("OTEL_ENTITIES", + "service{service.name=app1}[version=1.0];service{service.name=app1}[version=2.0]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Last occurrence should win + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes["version"]), "2.0"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorEmptyEnv) +{ + unsetenv("OTEL_ENTITIES"); + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.empty()); +} + +TEST(ResourceTest, EnvEntityDetectorEmptyString) +{ + setenv("OTEL_ENTITIES", "", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorMalformedEntity) +{ + setenv("OTEL_ENTITIES", "service{service.name=app1};invalid{syntax;service{service.name=app2}", + 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Should process valid entities and skip malformed ones + EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app2"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorWhitespaceHandling) +{ + setenv("OTEL_ENTITIES", " ; service { service.name = app1 } ; ", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorEmptySemicolons) +{ + // Test: Empty strings are allowed (leading, trailing, and consecutive semicolons are ignored) + setenv("OTEL_ENTITIES", ";service{service.name=app1};;host{host.id=host-123};", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes["host.id"]), "host-123"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorMissingRequiredFields) +{ + // Test: Missing required fields (type or identifying attributes) - should skip entity + setenv("OTEL_ENTITIES", "service{};host{host.id=123}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.find("service.name") == received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes["host.id"]), "123"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorConflictingIdentifyingAttributes) +{ + // Test: Conflicting identifying attributes - last entity wins + setenv("OTEL_ENTITIES", "service{service.name=app1};service{service.name=app2}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Last entity should win + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app2"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorConflictingDescriptiveAttributes) +{ + // Test: Conflicting descriptive attributes - last entity's value is used + setenv("OTEL_ENTITIES", + "service{service.name=app1}[version=1.0];service{service.name=app2}[version=2.0]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app2"); + EXPECT_EQ(nostd::get(received_attributes["version"]), "2.0"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorKubernetesPod) +{ + // Test: Kubernetes pod entity example from spec + setenv("OTEL_ENTITIES", + "k8s.pod{k8s.pod.uid=pod-abc123}[k8s.pod.name=my-pod,k8s.pod.label.app=my-app]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["k8s.pod.uid"]), "pod-abc123"); + EXPECT_EQ(nostd::get(received_attributes["k8s.pod.name"]), "my-pod"); + EXPECT_EQ(nostd::get(received_attributes["k8s.pod.label.app"]), "my-app"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorContainerWithHost) +{ + // Test: Container with host (minimal descriptive attributes) + setenv("OTEL_ENTITIES", + "container{container.id=cont-456};host{host.id=host-789}[host.name=docker-host]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["container.id"]), "cont-456"); + EXPECT_EQ(nostd::get(received_attributes["host.id"]), "host-789"); + EXPECT_EQ(nostd::get(received_attributes["host.name"]), "docker-host"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorMinimalEntity) +{ + // Test: Minimal entity (only required fields) + setenv("OTEL_ENTITIES", "service{service.name=minimal-app}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "minimal-app"); + EXPECT_EQ(received_attributes.size(), 1); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorPercentEncodingMultiple) +{ + // Test: Multiple percent-encoded characters in one value + setenv("OTEL_ENTITIES", + "service{service.name=my%2Capp,service.instance.id=inst-1}[config=key%3Dvalue%5Bprod%5D]", + 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my,app"); + EXPECT_EQ(nostd::get(received_attributes["config"]), "key=value[prod]"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorInvalidSchemaUrl) +{ + // Test: Invalid schema URL - should log warning and ignore URL + setenv("OTEL_ENTITIES", "service{service.name=app1}@invalid-url", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Entity should be processed but schema URL ignored + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorAllMalformedEntities) +{ + // Test: All entities are malformed - ParseEntities returns empty, should return empty resource + setenv("OTEL_ENTITIES", "invalid{syntax};{missing-type};123{invalid-type}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // All entities are invalid, so parsed_entities.empty() is true, should return empty resource + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorEmptySchemaUrl) +{ + // Test: Empty schema URL - should log warning and ignore URL + setenv("OTEL_ENTITIES", "service{service.name=app1}@", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Entity should be processed but empty schema URL ignored + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorValidSchemaUrl) +{ + // Test: Valid schema URL with "://" - should be accepted + setenv("OTEL_ENTITIES", "service{service.name=app1}@https://opentelemetry.io/schemas/1.0.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Entity should be processed and schema URL should be valid (not cleared) + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(resource.GetSchemaURL(), "https://opentelemetry.io/schemas/1.0.0"); + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorSchemaUrlWithWhitespace) +{ + // Test: Schema URL with only whitespace - should be treated as empty after trim + setenv("OTEL_ENTITIES", "service{service.name=app1}@ ", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Entity should be processed but whitespace-only schema URL should be ignored + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorMissingClosingBracket) +{ + // Test: Missing closing bracket ']' for desc_attrs - should be rejected + setenv("OTEL_ENTITIES", "service{service.name=app1}[version=1.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Entity with missing closing bracket should be rejected + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorMissingClosingBrace) +{ + // Test: Missing closing brace '}' for id_attrs - should be rejected + setenv("OTEL_ENTITIES", "service{service.name=app1", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Entity with missing closing brace should be rejected + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorInvalidTypeCharacters) +{ + // Test: Type with invalid characters (not alphanumeric, '.', '_', or '-') + setenv("OTEL_ENTITIES", "service@name{service.name=app1}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // Entity with invalid type characters should be rejected + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorEmptyKeyInAttributes) +{ + // Test: Key-value pair with empty key after trimming - should be skipped + setenv("OTEL_ENTITIES", "service{=value,service.name=app1, =another}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + // Empty key entries should not appear in attributes + EXPECT_EQ(received_attributes.size(), 1); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(ResourceTest, EnvEntityDetectorEmptyEntityString) +{ + // Test: Only empty entity strings - should all be skipped and return empty resource + setenv("OTEL_ENTITIES", ";;;", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + auto received_attributes = resource.GetAttributes(); + + // All empty strings should be skipped, resulting in empty resource + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} +#endif From 81c6db34d6fc079aaeeb0601aa75b4cbfd03a865 Mon Sep 17 00:00:00 2001 From: Hasv07 Date: Thu, 25 Dec 2025 17:51:58 +0000 Subject: [PATCH 2/6] [code health] Fix clang-tidy warnings --- sdk/test/resource/resource_test.cc | 180 ++++++++++++++--------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index b8fdc42eec..4261459308 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -299,13 +299,13 @@ TEST(ResourceTest, EnvEntityDetectorBasic) setenv("OTEL_ENTITIES", "service{service.name=my-app,service.instance.id=instance-1}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my-app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); EXPECT_TRUE(received_attributes.find("service.instance.id") != received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "instance-1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); unsetenv("OTEL_ENTITIES"); } @@ -316,12 +316,12 @@ TEST(ResourceTest, EnvEntityDetectorWithDescriptiveAttributes) "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0]", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my-app"); - EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "instance-1"); - EXPECT_EQ(nostd::get(received_attributes["service.version"]), "1.0.0"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.version")), "1.0.0"); unsetenv("OTEL_ENTITIES"); } @@ -334,14 +334,14 @@ TEST(ResourceTest, EnvEntityDetectorMultipleEntities) 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my-app"); - EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "instance-1"); - EXPECT_EQ(nostd::get(received_attributes["service.version"]), "1.0.0"); - EXPECT_EQ(nostd::get(received_attributes["host.id"]), "host-123"); - EXPECT_EQ(nostd::get(received_attributes["host.name"]), "web-server-01"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.version")), "1.0.0"); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-123"); + EXPECT_EQ(nostd::get(received_attributes.at("host.name")), "web-server-01"); unsetenv("OTEL_ENTITIES"); } @@ -351,11 +351,11 @@ TEST(ResourceTest, EnvEntityDetectorPercentEncoding) setenv("OTEL_ENTITIES", "service{service.name=my%2Capp,service.instance.id=inst-1}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my,app"); - EXPECT_EQ(nostd::get(received_attributes["service.instance.id"]), "inst-1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my,app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "inst-1"); unsetenv("OTEL_ENTITIES"); } @@ -366,12 +366,12 @@ TEST(ResourceTest, EnvEntityDetectorDuplicateEntities) "service{service.name=app1}[version=1.0];service{service.name=app1}[version=2.0]", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Last occurrence should win - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); - EXPECT_EQ(nostd::get(received_attributes["version"]), "2.0"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("version")), "2.0"); unsetenv("OTEL_ENTITIES"); } @@ -380,8 +380,8 @@ TEST(ResourceTest, EnvEntityDetectorEmptyEnv) { unsetenv("OTEL_ENTITIES"); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); EXPECT_TRUE(received_attributes.empty()); } @@ -391,8 +391,8 @@ TEST(ResourceTest, EnvEntityDetectorEmptyString) setenv("OTEL_ENTITIES", "", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); EXPECT_TRUE(received_attributes.empty()); @@ -405,12 +405,12 @@ TEST(ResourceTest, EnvEntityDetectorMalformedEntity) 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Should process valid entities and skip malformed ones EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app2"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); unsetenv("OTEL_ENTITIES"); } @@ -420,10 +420,10 @@ TEST(ResourceTest, EnvEntityDetectorWhitespaceHandling) setenv("OTEL_ENTITIES", " ; service { service.name = app1 } ; ", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); unsetenv("OTEL_ENTITIES"); } @@ -434,11 +434,11 @@ TEST(ResourceTest, EnvEntityDetectorEmptySemicolons) setenv("OTEL_ENTITIES", ";service{service.name=app1};;host{host.id=host-123};", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); - EXPECT_EQ(nostd::get(received_attributes["host.id"]), "host-123"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-123"); unsetenv("OTEL_ENTITIES"); } @@ -449,11 +449,11 @@ TEST(ResourceTest, EnvEntityDetectorMissingRequiredFields) setenv("OTEL_ENTITIES", "service{};host{host.id=123}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); EXPECT_TRUE(received_attributes.find("service.name") == received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes["host.id"]), "123"); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "123"); unsetenv("OTEL_ENTITIES"); } @@ -464,11 +464,11 @@ TEST(ResourceTest, EnvEntityDetectorConflictingIdentifyingAttributes) setenv("OTEL_ENTITIES", "service{service.name=app1};service{service.name=app2}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Last entity should win - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app2"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); unsetenv("OTEL_ENTITIES"); } @@ -480,11 +480,11 @@ TEST(ResourceTest, EnvEntityDetectorConflictingDescriptiveAttributes) "service{service.name=app1}[version=1.0];service{service.name=app2}[version=2.0]", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app2"); - EXPECT_EQ(nostd::get(received_attributes["version"]), "2.0"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); + EXPECT_EQ(nostd::get(received_attributes.at("version")), "2.0"); unsetenv("OTEL_ENTITIES"); } @@ -496,12 +496,12 @@ TEST(ResourceTest, EnvEntityDetectorKubernetesPod) "k8s.pod{k8s.pod.uid=pod-abc123}[k8s.pod.name=my-pod,k8s.pod.label.app=my-app]", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["k8s.pod.uid"]), "pod-abc123"); - EXPECT_EQ(nostd::get(received_attributes["k8s.pod.name"]), "my-pod"); - EXPECT_EQ(nostd::get(received_attributes["k8s.pod.label.app"]), "my-app"); + EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.uid")), "pod-abc123"); + EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.name")), "my-pod"); + EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.label.app")), "my-app"); unsetenv("OTEL_ENTITIES"); } @@ -513,12 +513,12 @@ TEST(ResourceTest, EnvEntityDetectorContainerWithHost) "container{container.id=cont-456};host{host.id=host-789}[host.name=docker-host]", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["container.id"]), "cont-456"); - EXPECT_EQ(nostd::get(received_attributes["host.id"]), "host-789"); - EXPECT_EQ(nostd::get(received_attributes["host.name"]), "docker-host"); + EXPECT_EQ(nostd::get(received_attributes.at("container.id")), "cont-456"); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-789"); + EXPECT_EQ(nostd::get(received_attributes.at("host.name")), "docker-host"); unsetenv("OTEL_ENTITIES"); } @@ -529,10 +529,10 @@ TEST(ResourceTest, EnvEntityDetectorMinimalEntity) setenv("OTEL_ENTITIES", "service{service.name=minimal-app}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "minimal-app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "minimal-app"); EXPECT_EQ(received_attributes.size(), 1); unsetenv("OTEL_ENTITIES"); @@ -546,11 +546,11 @@ TEST(ResourceTest, EnvEntityDetectorPercentEncodingMultiple) 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "my,app"); - EXPECT_EQ(nostd::get(received_attributes["config"]), "key=value[prod]"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my,app"); + EXPECT_EQ(nostd::get(received_attributes.at("config")), "key=value[prod]"); unsetenv("OTEL_ENTITIES"); } @@ -561,11 +561,11 @@ TEST(ResourceTest, EnvEntityDetectorInvalidSchemaUrl) setenv("OTEL_ENTITIES", "service{service.name=app1}@invalid-url", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Entity should be processed but schema URL ignored - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); unsetenv("OTEL_ENTITIES"); } @@ -576,8 +576,8 @@ TEST(ResourceTest, EnvEntityDetectorAllMalformedEntities) setenv("OTEL_ENTITIES", "invalid{syntax};{missing-type};123{invalid-type}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // All entities are invalid, so parsed_entities.empty() is true, should return empty resource EXPECT_TRUE(received_attributes.empty()); @@ -591,11 +591,11 @@ TEST(ResourceTest, EnvEntityDetectorEmptySchemaUrl) setenv("OTEL_ENTITIES", "service{service.name=app1}@", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Entity should be processed but empty schema URL ignored - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); unsetenv("OTEL_ENTITIES"); } @@ -606,11 +606,11 @@ TEST(ResourceTest, EnvEntityDetectorValidSchemaUrl) setenv("OTEL_ENTITIES", "service{service.name=app1}@https://opentelemetry.io/schemas/1.0.0", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Entity should be processed and schema URL should be valid (not cleared) - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); EXPECT_EQ(resource.GetSchemaURL(), "https://opentelemetry.io/schemas/1.0.0"); unsetenv("OTEL_ENTITIES"); } @@ -621,11 +621,11 @@ TEST(ResourceTest, EnvEntityDetectorSchemaUrlWithWhitespace) setenv("OTEL_ENTITIES", "service{service.name=app1}@ ", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Entity should be processed but whitespace-only schema URL should be ignored - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); unsetenv("OTEL_ENTITIES"); } @@ -636,8 +636,8 @@ TEST(ResourceTest, EnvEntityDetectorMissingClosingBracket) setenv("OTEL_ENTITIES", "service{service.name=app1}[version=1.0", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Entity with missing closing bracket should be rejected EXPECT_TRUE(received_attributes.empty()); @@ -651,8 +651,8 @@ TEST(ResourceTest, EnvEntityDetectorMissingClosingBrace) setenv("OTEL_ENTITIES", "service{service.name=app1", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Entity with missing closing brace should be rejected EXPECT_TRUE(received_attributes.empty()); @@ -666,8 +666,8 @@ TEST(ResourceTest, EnvEntityDetectorInvalidTypeCharacters) setenv("OTEL_ENTITIES", "service@name{service.name=app1}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // Entity with invalid type characters should be rejected EXPECT_TRUE(received_attributes.empty()); @@ -681,10 +681,10 @@ TEST(ResourceTest, EnvEntityDetectorEmptyKeyInAttributes) setenv("OTEL_ENTITIES", "service{=value,service.name=app1, =another}", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); - EXPECT_EQ(nostd::get(received_attributes["service.name"]), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); // Empty key entries should not appear in attributes EXPECT_EQ(received_attributes.size(), 1); @@ -697,8 +697,8 @@ TEST(ResourceTest, EnvEntityDetectorEmptyEntityString) setenv("OTEL_ENTITIES", ";;;", 1); EnvEntityDetector detector; - auto resource = detector.Detect(); - auto received_attributes = resource.GetAttributes(); + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); // All empty strings should be skipped, resulting in empty resource EXPECT_TRUE(received_attributes.empty()); From 7bdcc926fc59fdfb960c97c16725c813bdea9167 Mon Sep 17 00:00:00 2001 From: Hasv07 Date: Mon, 5 Jan 2026 20:29:50 +0000 Subject: [PATCH 3/6] [resource_detectors] Move EnvEntityDetector from SDK to resource_detectors component --- resource_detectors/BUILD | 1 + resource_detectors/CMakeLists.txt | 4 +- .../env_entity_detector.cc | 40 +- .../resource_detectors/env_entity_detector.h | 26 ++ resource_detectors/test/BUILD | 2 + resource_detectors/test/CMakeLists.txt | 7 +- .../test/env_entity_detector_test.cc | 432 ++++++++++++++++++ .../sdk/resource/resource_detector.h | 11 - sdk/src/resource/CMakeLists.txt | 3 +- sdk/test/resource/resource_test.cc | 414 ----------------- 10 files changed, 490 insertions(+), 450 deletions(-) rename {sdk/src/resource => resource_detectors}/env_entity_detector.cc (85%) create mode 100644 resource_detectors/include/opentelemetry/resource_detectors/env_entity_detector.h create mode 100644 resource_detectors/test/env_entity_detector_test.cc diff --git a/resource_detectors/BUILD b/resource_detectors/BUILD index f12e695647..d137736b18 100644 --- a/resource_detectors/BUILD +++ b/resource_detectors/BUILD @@ -16,6 +16,7 @@ cc_library( srcs = [ "container_detector.cc", "container_detector_utils.cc", + "env_entity_detector.cc", "process_detector.cc", "process_detector_utils.cc", ], diff --git a/resource_detectors/CMakeLists.txt b/resource_detectors/CMakeLists.txt index 7fdba7d73b..233984aa1e 100644 --- a/resource_detectors/CMakeLists.txt +++ b/resource_detectors/CMakeLists.txt @@ -3,8 +3,8 @@ add_library( opentelemetry_resource_detectors - container_detector_utils.cc container_detector.cc process_detector.cc - process_detector_utils.cc) + container_detector_utils.cc container_detector.cc env_entity_detector.cc + process_detector.cc process_detector_utils.cc) set_target_properties(opentelemetry_resource_detectors PROPERTIES EXPORT_NAME resource_detectors) diff --git a/sdk/src/resource/env_entity_detector.cc b/resource_detectors/env_entity_detector.cc similarity index 85% rename from sdk/src/resource/env_entity_detector.cc rename to resource_detectors/env_entity_detector.cc index 1f82113ef7..c23ca361ee 100644 --- a/sdk/src/resource/env_entity_detector.cc +++ b/resource_detectors/env_entity_detector.cc @@ -13,6 +13,7 @@ #include "opentelemetry/common/string_util.h" #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/nostd/variant.h" +#include "opentelemetry/resource_detectors/env_entity_detector.h" #include "opentelemetry/sdk/common/attribute_utils.h" #include "opentelemetry/sdk/common/env_variables.h" #include "opentelemetry/sdk/common/global_log_handler.h" @@ -21,9 +22,7 @@ #include "opentelemetry/version.h" OPENTELEMETRY_BEGIN_NAMESPACE -namespace sdk -{ -namespace resource +namespace resource_detector { namespace @@ -34,13 +33,14 @@ constexpr const char *kOtelEntities = "OTEL_ENTITIES"; struct ParsedEntity { std::string type; - ResourceAttributes id_attrs; - ResourceAttributes desc_attrs; + opentelemetry::sdk::resource::ResourceAttributes id_attrs; + opentelemetry::sdk::resource::ResourceAttributes desc_attrs; std::string schema_url; std::string identity_key; // Pre-computed identity key for duplicate detection }; -std::string BuildEntityIdentityKey(const std::string &type, const ResourceAttributes &id_attrs) +std::string BuildEntityIdentityKey(const std::string &type, + const opentelemetry::sdk::resource::ResourceAttributes &id_attrs) { using AttrPtr = const std::pair *; @@ -61,14 +61,14 @@ std::string BuildEntityIdentityKey(const std::string &type, const ResourceAttrib } key += items[i]->first; key += "="; - key += nostd::get(items[i]->second); + key += opentelemetry::nostd::get(items[i]->second); } return key; } -std::string PercentDecode(nostd::string_view value) noexcept +std::string PercentDecode(opentelemetry::nostd::string_view value) noexcept { - if (value.find('%') == nostd::string_view::npos) + if (value.find('%') == opentelemetry::nostd::string_view::npos) { return std::string(value); } @@ -103,7 +103,8 @@ std::string PercentDecode(nostd::string_view value) noexcept return result; } -void ParseKeyValueList(const std::string &input, ResourceAttributes &out) +void ParseKeyValueList(const std::string &input, + opentelemetry::sdk::resource::ResourceAttributes &out) { std::istringstream iss(input); std::string token; @@ -244,7 +245,7 @@ std::vector ParseEntities(const std::string &entities_str) } // namespace -Resource EnvEntityDetector::Detect() noexcept +opentelemetry::sdk::resource::Resource EnvEntityDetector::Detect() noexcept { std::string entities_str; bool exists = @@ -252,16 +253,16 @@ Resource EnvEntityDetector::Detect() noexcept if (!exists || entities_str.empty()) { - return ResourceDetector::Create({}); + return opentelemetry::sdk::resource::ResourceDetector::Create({}); } auto parsed_entities = ParseEntities(entities_str); if (parsed_entities.empty()) { - return ResourceDetector::Create({}); + return opentelemetry::sdk::resource::ResourceDetector::Create({}); } - ResourceAttributes resource_attrs; + opentelemetry::sdk::resource::ResourceAttributes resource_attrs; std::string schema_url; std::unordered_map entity_index_by_identity; @@ -299,7 +300,8 @@ Resource EnvEntityDetector::Detect() noexcept { auto existing = resource_attrs.find(attr.first); if (existing != resource_attrs.end() && - nostd::get(existing->second) != nostd::get(attr.second)) + opentelemetry::nostd::get(existing->second) != + opentelemetry::nostd::get(attr.second)) { OTEL_INTERNAL_LOG_WARN( "[EnvEntityDetector] Conflicting identifying attribute in OTEL_ENTITIES, " @@ -313,7 +315,8 @@ Resource EnvEntityDetector::Detect() noexcept { auto existing = resource_attrs.find(attr.first); if (existing != resource_attrs.end() && - nostd::get(existing->second) != nostd::get(attr.second)) + opentelemetry::nostd::get(existing->second) != + opentelemetry::nostd::get(attr.second)) { OTEL_INTERNAL_LOG_WARN( "[EnvEntityDetector] Conflicting descriptive attribute in OTEL_ENTITIES, " @@ -328,9 +331,8 @@ Resource EnvEntityDetector::Detect() noexcept } } - return ResourceDetector::Create(resource_attrs, schema_url); + return opentelemetry::sdk::resource::ResourceDetector::Create(resource_attrs, schema_url); } -} // namespace resource -} // namespace sdk +} // namespace resource_detector OPENTELEMETRY_END_NAMESPACE diff --git a/resource_detectors/include/opentelemetry/resource_detectors/env_entity_detector.h b/resource_detectors/include/opentelemetry/resource_detectors/env_entity_detector.h new file mode 100644 index 0000000000..a01ae07de8 --- /dev/null +++ b/resource_detectors/include/opentelemetry/resource_detectors/env_entity_detector.h @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/resource/resource_detector.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace resource_detector +{ + +/** + * EnvEntityDetector detects entities defined in the OTEL_ENTITIES environment + * variable as specified in the Entity Propagation spec: + * https://opentelemetry.io/docs/specs/otel/entities/entity-propagation/ + */ +class EnvEntityDetector : public opentelemetry::sdk::resource::ResourceDetector +{ +public: + opentelemetry::sdk::resource::Resource Detect() noexcept override; +}; + +} // namespace resource_detector +OPENTELEMETRY_END_NAMESPACE diff --git a/resource_detectors/test/BUILD b/resource_detectors/test/BUILD index b230901308..a10d05b02b 100644 --- a/resource_detectors/test/BUILD +++ b/resource_detectors/test/BUILD @@ -7,12 +7,14 @@ cc_test( name = "resource_detector_test", srcs = [ "container_detector_test.cc", + "env_entity_detector_test.cc", "process_detector_test.cc", ], tags = ["test"], deps = [ "//api", "//resource_detectors", + "//sdk/src/resource", "@com_google_googletest//:gtest_main", ], ) diff --git a/resource_detectors/test/CMakeLists.txt b/resource_detectors/test/CMakeLists.txt index 4062a278ef..b1281e904d 100644 --- a/resource_detectors/test/CMakeLists.txt +++ b/resource_detectors/test/CMakeLists.txt @@ -2,12 +2,15 @@ # SPDX-License-Identifier: Apache-2.0 add_executable(resource_detector_test container_detector_test.cc - process_detector_test.cc) + process_detector_test.cc + env_entity_detector_test.cc) # Link the required dependencies target_link_libraries( resource_detector_test PRIVATE opentelemetry_resource_detectors - opentelemetry_api GTest::gtest_main) + opentelemetry_api + opentelemetry_resources + GTest::gtest_main) gtest_add_tests( TARGET resource_detector_test diff --git a/resource_detectors/test/env_entity_detector_test.cc b/resource_detectors/test/env_entity_detector_test.cc new file mode 100644 index 0000000000..0585ca266c --- /dev/null +++ b/resource_detectors/test/env_entity_detector_test.cc @@ -0,0 +1,432 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "opentelemetry/nostd/variant.h" +#include "opentelemetry/resource_detectors/env_entity_detector.h" + +#if defined(_MSC_VER) +# include "opentelemetry/sdk/common/env_variables.h" +using opentelemetry::sdk::common::setenv; +using opentelemetry::sdk::common::unsetenv; +#endif + +using namespace opentelemetry::resource_detector; +namespace nostd = opentelemetry::nostd; + +#ifndef NO_GETENV +TEST(EnvEntityDetectorTest, Basic) +{ + setenv("OTEL_ENTITIES", "service{service.name=my-app,service.instance.id=instance-1}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); + EXPECT_TRUE(received_attributes.find("service.instance.id") != received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, WithDescriptiveAttributes) +{ + setenv("OTEL_ENTITIES", + "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.version")), "1.0.0"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, MultipleEntities) +{ + setenv("OTEL_ENTITIES", + "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0];" + "host{host.id=host-123}[host.name=web-server-01]", + 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); + EXPECT_EQ(nostd::get(received_attributes.at("service.version")), "1.0.0"); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-123"); + EXPECT_EQ(nostd::get(received_attributes.at("host.name")), "web-server-01"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, PercentEncoding) +{ + setenv("OTEL_ENTITIES", "service{service.name=my%2Capp,service.instance.id=inst-1}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my,app"); + EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "inst-1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, DuplicateEntities) +{ + setenv("OTEL_ENTITIES", + "service{service.name=app1}[version=1.0];service{service.name=app1}[version=2.0]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Last occurrence should win + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("version")), "2.0"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, EmptyEnv) +{ + unsetenv("OTEL_ENTITIES"); + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.empty()); +} + +TEST(EnvEntityDetectorTest, EmptyString) +{ + setenv("OTEL_ENTITIES", "", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, MalformedEntity) +{ + setenv("OTEL_ENTITIES", "service{service.name=app1};invalid{syntax;service{service.name=app2}", + 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Should process valid entities and skip malformed ones + EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, WhitespaceHandling) +{ + setenv("OTEL_ENTITIES", " ; service { service.name = app1 } ; ", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, EmptySemicolons) +{ + // Test: Empty strings are allowed (leading, trailing, and consecutive semicolons are ignored) + setenv("OTEL_ENTITIES", ";service{service.name=app1};;host{host.id=host-123};", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-123"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, MissingRequiredFields) +{ + // Test: Missing required fields (type or identifying attributes) - should skip entity + setenv("OTEL_ENTITIES", "service{};host{host.id=123}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_TRUE(received_attributes.find("service.name") == received_attributes.end()); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "123"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, ConflictingIdentifyingAttributes) +{ + // Test: Conflicting identifying attributes - last entity wins + setenv("OTEL_ENTITIES", "service{service.name=app1};service{service.name=app2}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Last entity should win + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, ConflictingDescriptiveAttributes) +{ + // Test: Conflicting descriptive attributes - last entity's value is used + setenv("OTEL_ENTITIES", + "service{service.name=app1}[version=1.0];service{service.name=app2}[version=2.0]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); + EXPECT_EQ(nostd::get(received_attributes.at("version")), "2.0"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, KubernetesPod) +{ + // Test: Kubernetes pod entity example from spec + setenv("OTEL_ENTITIES", + "k8s.pod{k8s.pod.uid=pod-abc123}[k8s.pod.name=my-pod,k8s.pod.label.app=my-app]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.uid")), "pod-abc123"); + EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.name")), "my-pod"); + EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.label.app")), "my-app"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, ContainerWithHost) +{ + // Test: Container with host (minimal descriptive attributes) + setenv("OTEL_ENTITIES", + "container{container.id=cont-456};host{host.id=host-789}[host.name=docker-host]", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("container.id")), "cont-456"); + EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-789"); + EXPECT_EQ(nostd::get(received_attributes.at("host.name")), "docker-host"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, MinimalEntity) +{ + // Test: Minimal entity (only required fields) + setenv("OTEL_ENTITIES", "service{service.name=minimal-app}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "minimal-app"); + EXPECT_EQ(received_attributes.size(), 1); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, PercentEncodingMultiple) +{ + // Test: Multiple percent-encoded characters in one value + setenv("OTEL_ENTITIES", + "service{service.name=my%2Capp,service.instance.id=inst-1}[config=key%3Dvalue%5Bprod%5D]", + 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my,app"); + EXPECT_EQ(nostd::get(received_attributes.at("config")), "key=value[prod]"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, InvalidSchemaUrl) +{ + // Test: Invalid schema URL - should log warning and ignore URL + setenv("OTEL_ENTITIES", "service{service.name=app1}@invalid-url", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity should be processed but schema URL ignored + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, AllMalformedEntities) +{ + // Test: All entities are malformed - ParseEntities returns empty, should return empty resource + setenv("OTEL_ENTITIES", "invalid{syntax};{missing-type};123{invalid-type}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // All entities are invalid, so parsed_entities.empty() is true, should return empty resource + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, EmptySchemaUrl) +{ + // Test: Empty schema URL - should log warning and ignore URL + setenv("OTEL_ENTITIES", "service{service.name=app1}@", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity should be processed but empty schema URL ignored + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, ValidSchemaUrl) +{ + // Test: Valid schema URL with "://" - should be accepted + setenv("OTEL_ENTITIES", "service{service.name=app1}@https://opentelemetry.io/schemas/1.0.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity should be processed and schema URL should be valid (not cleared) + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(resource.GetSchemaURL(), "https://opentelemetry.io/schemas/1.0.0"); + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, SchemaUrlWithWhitespace) +{ + // Test: Schema URL with only whitespace - should be treated as empty after trim + setenv("OTEL_ENTITIES", "service{service.name=app1}@ ", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity should be processed but whitespace-only schema URL should be ignored + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, MissingClosingBracket) +{ + // Test: Missing closing bracket ']' for desc_attrs - should be rejected + setenv("OTEL_ENTITIES", "service{service.name=app1}[version=1.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity with missing closing bracket should be rejected + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, MissingClosingBrace) +{ + // Test: Missing closing brace '}' for id_attrs - should be rejected + setenv("OTEL_ENTITIES", "service{service.name=app1", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity with missing closing brace should be rejected + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, InvalidTypeCharacters) +{ + // Test: Type with invalid characters (not alphanumeric, '.', '_', or '-') + setenv("OTEL_ENTITIES", "service@name{service.name=app1}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity with invalid type characters should be rejected + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, EmptyKeyInAttributes) +{ + // Test: Key-value pair with empty key after trimming - should be skipped + setenv("OTEL_ENTITIES", "service{=value,service.name=app1, =another}", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + // Empty key entries should not appear in attributes + EXPECT_EQ(received_attributes.size(), 1); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, EmptyEntityString) +{ + // Test: Only empty entity strings - should all be skipped and return empty resource + setenv("OTEL_ENTITIES", ";;;", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // All empty strings should be skipped, resulting in empty resource + EXPECT_TRUE(received_attributes.empty()); + + unsetenv("OTEL_ENTITIES"); +} +#endif diff --git a/sdk/include/opentelemetry/sdk/resource/resource_detector.h b/sdk/include/opentelemetry/sdk/resource/resource_detector.h index 13b661092d..f129d3d1bc 100644 --- a/sdk/include/opentelemetry/sdk/resource/resource_detector.h +++ b/sdk/include/opentelemetry/sdk/resource/resource_detector.h @@ -39,17 +39,6 @@ class OTELResourceDetector : public ResourceDetector Resource Detect() noexcept override; }; -/** - * EnvEntityDetector detects entities defined in the OTEL_ENTITIES environment - * variable as specified in the Entity Propagation spec: - * https://opentelemetry.io/docs/specs/otel/entities/entity-propagation/ - */ -class EnvEntityDetector : public ResourceDetector -{ -public: - Resource Detect() noexcept override; -}; - } // namespace resource } // namespace sdk OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/resource/CMakeLists.txt b/sdk/src/resource/CMakeLists.txt index 42dd5b8c71..48b647ec41 100644 --- a/sdk/src/resource/CMakeLists.txt +++ b/sdk/src/resource/CMakeLists.txt @@ -1,8 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -add_library(opentelemetry_resources resource.cc resource_detector.cc - env_entity_detector.cc) +add_library(opentelemetry_resources resource.cc resource_detector.cc) set_target_properties(opentelemetry_resources PROPERTIES EXPORT_NAME resources) set_target_version(opentelemetry_resources) diff --git a/sdk/test/resource/resource_test.cc b/sdk/test/resource/resource_test.cc index 4261459308..696509f892 100644 --- a/sdk/test/resource/resource_test.cc +++ b/sdk/test/resource/resource_test.cc @@ -292,417 +292,3 @@ TEST(ResourceTest, DerivedResourceDetector) EXPECT_EQ(resource.GetSchemaURL(), detector.schema_url); EXPECT_TRUE(received_attributes.find("key") != received_attributes.end()); } - -#ifndef NO_GETENV -TEST(ResourceTest, EnvEntityDetectorBasic) -{ - setenv("OTEL_ENTITIES", "service{service.name=my-app,service.instance.id=instance-1}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); - EXPECT_TRUE(received_attributes.find("service.instance.id") != received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorWithDescriptiveAttributes) -{ - setenv("OTEL_ENTITIES", - "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0]", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); - EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); - EXPECT_EQ(nostd::get(received_attributes.at("service.version")), "1.0.0"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorMultipleEntities) -{ - setenv("OTEL_ENTITIES", - "service{service.name=my-app,service.instance.id=instance-1}[service.version=1.0.0];" - "host{host.id=host-123}[host.name=web-server-01]", - 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my-app"); - EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "instance-1"); - EXPECT_EQ(nostd::get(received_attributes.at("service.version")), "1.0.0"); - EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-123"); - EXPECT_EQ(nostd::get(received_attributes.at("host.name")), "web-server-01"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorPercentEncoding) -{ - setenv("OTEL_ENTITIES", "service{service.name=my%2Capp,service.instance.id=inst-1}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my,app"); - EXPECT_EQ(nostd::get(received_attributes.at("service.instance.id")), "inst-1"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorDuplicateEntities) -{ - setenv("OTEL_ENTITIES", - "service{service.name=app1}[version=1.0];service{service.name=app1}[version=2.0]", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Last occurrence should win - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - EXPECT_EQ(nostd::get(received_attributes.at("version")), "2.0"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorEmptyEnv) -{ - unsetenv("OTEL_ENTITIES"); - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_TRUE(received_attributes.empty()); -} - -TEST(ResourceTest, EnvEntityDetectorEmptyString) -{ - setenv("OTEL_ENTITIES", "", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_TRUE(received_attributes.empty()); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorMalformedEntity) -{ - setenv("OTEL_ENTITIES", "service{service.name=app1};invalid{syntax;service{service.name=app2}", - 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Should process valid entities and skip malformed ones - EXPECT_TRUE(received_attributes.find("service.name") != received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorWhitespaceHandling) -{ - setenv("OTEL_ENTITIES", " ; service { service.name = app1 } ; ", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorEmptySemicolons) -{ - // Test: Empty strings are allowed (leading, trailing, and consecutive semicolons are ignored) - setenv("OTEL_ENTITIES", ";service{service.name=app1};;host{host.id=host-123};", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-123"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorMissingRequiredFields) -{ - // Test: Missing required fields (type or identifying attributes) - should skip entity - setenv("OTEL_ENTITIES", "service{};host{host.id=123}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_TRUE(received_attributes.find("service.name") == received_attributes.end()); - EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "123"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorConflictingIdentifyingAttributes) -{ - // Test: Conflicting identifying attributes - last entity wins - setenv("OTEL_ENTITIES", "service{service.name=app1};service{service.name=app2}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Last entity should win - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorConflictingDescriptiveAttributes) -{ - // Test: Conflicting descriptive attributes - last entity's value is used - setenv("OTEL_ENTITIES", - "service{service.name=app1}[version=1.0];service{service.name=app2}[version=2.0]", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app2"); - EXPECT_EQ(nostd::get(received_attributes.at("version")), "2.0"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorKubernetesPod) -{ - // Test: Kubernetes pod entity example from spec - setenv("OTEL_ENTITIES", - "k8s.pod{k8s.pod.uid=pod-abc123}[k8s.pod.name=my-pod,k8s.pod.label.app=my-app]", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.uid")), "pod-abc123"); - EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.name")), "my-pod"); - EXPECT_EQ(nostd::get(received_attributes.at("k8s.pod.label.app")), "my-app"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorContainerWithHost) -{ - // Test: Container with host (minimal descriptive attributes) - setenv("OTEL_ENTITIES", - "container{container.id=cont-456};host{host.id=host-789}[host.name=docker-host]", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("container.id")), "cont-456"); - EXPECT_EQ(nostd::get(received_attributes.at("host.id")), "host-789"); - EXPECT_EQ(nostd::get(received_attributes.at("host.name")), "docker-host"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorMinimalEntity) -{ - // Test: Minimal entity (only required fields) - setenv("OTEL_ENTITIES", "service{service.name=minimal-app}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "minimal-app"); - EXPECT_EQ(received_attributes.size(), 1); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorPercentEncodingMultiple) -{ - // Test: Multiple percent-encoded characters in one value - setenv("OTEL_ENTITIES", - "service{service.name=my%2Capp,service.instance.id=inst-1}[config=key%3Dvalue%5Bprod%5D]", - 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "my,app"); - EXPECT_EQ(nostd::get(received_attributes.at("config")), "key=value[prod]"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorInvalidSchemaUrl) -{ - // Test: Invalid schema URL - should log warning and ignore URL - setenv("OTEL_ENTITIES", "service{service.name=app1}@invalid-url", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Entity should be processed but schema URL ignored - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorAllMalformedEntities) -{ - // Test: All entities are malformed - ParseEntities returns empty, should return empty resource - setenv("OTEL_ENTITIES", "invalid{syntax};{missing-type};123{invalid-type}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // All entities are invalid, so parsed_entities.empty() is true, should return empty resource - EXPECT_TRUE(received_attributes.empty()); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorEmptySchemaUrl) -{ - // Test: Empty schema URL - should log warning and ignore URL - setenv("OTEL_ENTITIES", "service{service.name=app1}@", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Entity should be processed but empty schema URL ignored - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorValidSchemaUrl) -{ - // Test: Valid schema URL with "://" - should be accepted - setenv("OTEL_ENTITIES", "service{service.name=app1}@https://opentelemetry.io/schemas/1.0.0", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Entity should be processed and schema URL should be valid (not cleared) - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - EXPECT_EQ(resource.GetSchemaURL(), "https://opentelemetry.io/schemas/1.0.0"); - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorSchemaUrlWithWhitespace) -{ - // Test: Schema URL with only whitespace - should be treated as empty after trim - setenv("OTEL_ENTITIES", "service{service.name=app1}@ ", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Entity should be processed but whitespace-only schema URL should be ignored - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorMissingClosingBracket) -{ - // Test: Missing closing bracket ']' for desc_attrs - should be rejected - setenv("OTEL_ENTITIES", "service{service.name=app1}[version=1.0", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Entity with missing closing bracket should be rejected - EXPECT_TRUE(received_attributes.empty()); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorMissingClosingBrace) -{ - // Test: Missing closing brace '}' for id_attrs - should be rejected - setenv("OTEL_ENTITIES", "service{service.name=app1", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Entity with missing closing brace should be rejected - EXPECT_TRUE(received_attributes.empty()); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorInvalidTypeCharacters) -{ - // Test: Type with invalid characters (not alphanumeric, '.', '_', or '-') - setenv("OTEL_ENTITIES", "service@name{service.name=app1}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // Entity with invalid type characters should be rejected - EXPECT_TRUE(received_attributes.empty()); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorEmptyKeyInAttributes) -{ - // Test: Key-value pair with empty key after trimming - should be skipped - setenv("OTEL_ENTITIES", "service{=value,service.name=app1, =another}", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); - // Empty key entries should not appear in attributes - EXPECT_EQ(received_attributes.size(), 1); - - unsetenv("OTEL_ENTITIES"); -} - -TEST(ResourceTest, EnvEntityDetectorEmptyEntityString) -{ - // Test: Only empty entity strings - should all be skipped and return empty resource - setenv("OTEL_ENTITIES", ";;;", 1); - - EnvEntityDetector detector; - auto resource = detector.Detect(); - const auto &received_attributes = resource.GetAttributes(); - - // All empty strings should be skipped, resulting in empty resource - EXPECT_TRUE(received_attributes.empty()); - - unsetenv("OTEL_ENTITIES"); -} -#endif From 76328c8e4c65267bd5a2504672b024de362df6cd Mon Sep 17 00:00:00 2001 From: Hasv07 Date: Mon, 5 Jan 2026 21:53:17 +0000 Subject: [PATCH 4/6] [resource_detectors] Improve schema URL validation in EnvEntityDetector --- resource_detectors/env_entity_detector.cc | 38 +++++++++- .../test/env_entity_detector_test.cc | 69 +++++++++++++++++-- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/resource_detectors/env_entity_detector.cc b/resource_detectors/env_entity_detector.cc index c23ca361ee..a2676a82ee 100644 --- a/resource_detectors/env_entity_detector.cc +++ b/resource_detectors/env_entity_detector.cc @@ -103,6 +103,42 @@ std::string PercentDecode(opentelemetry::nostd::string_view value) noexcept return result; } +bool IsValidSchemaUrl(const std::string &url) noexcept +{ + if (url.empty()) + { + return false; + } + + // If absolute URI (has ://), validate scheme + size_t scheme_end = url.find("://"); + if (scheme_end != std::string::npos) + { + if (scheme_end == 0 || scheme_end + 3 >= url.size()) + { + return false; // Empty scheme or no content after :// + } + // Scheme must start with letter + if (!std::isalpha(static_cast(url[0]))) + { + return false; + } + // Scheme can contain letters, digits, +, -, . + for (size_t i = 1; i < scheme_end; ++i) + { + char c = url[i]; + if (!(std::isalnum(static_cast(c)) || c == '+' || c == '-' || c == '.')) + { + return false; + } + } + return true; + } + + // Relative URI - accept any non-empty string + return true; +} + void ParseKeyValueList(const std::string &input, opentelemetry::sdk::resource::ResourceAttributes &out) { @@ -204,7 +240,7 @@ bool ParseSingleEntity(const std::string &entity_str, ParsedEntity &out) std::string{opentelemetry::common::StringUtil::Trim(entity_str.substr(cursor + 1))}; // TODO: Use a proper Schema URL validator when available. - if (out.schema_url.empty() || out.schema_url.find("://") == std::string::npos) + if (!IsValidSchemaUrl(out.schema_url)) { OTEL_INTERNAL_LOG_WARN( "[EnvEntityDetector] Invalid schema URL in OTEL_ENTITIES, ignoring schema URL."); diff --git a/resource_detectors/test/env_entity_detector_test.cc b/resource_detectors/test/env_entity_detector_test.cc index 0585ca266c..5792e406fd 100644 --- a/resource_detectors/test/env_entity_detector_test.cc +++ b/resource_detectors/test/env_entity_detector_test.cc @@ -279,17 +279,48 @@ TEST(EnvEntityDetectorTest, PercentEncodingMultiple) unsetenv("OTEL_ENTITIES"); } -TEST(EnvEntityDetectorTest, InvalidSchemaUrl) +TEST(EnvEntityDetectorTest, SchemaUrlRelativePath) { - // Test: Invalid schema URL - should log warning and ignore URL - setenv("OTEL_ENTITIES", "service{service.name=app1}@invalid-url", 1); + // Test: Relative path schema URL should be accepted + setenv("OTEL_ENTITIES", "service{service.name=app1}@schemas/1.21.0", 1); EnvEntityDetector detector; auto resource = detector.Detect(); const auto &received_attributes = resource.GetAttributes(); - // Entity should be processed but schema URL ignored + // Entity should be processed and relative schema URL accepted EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(resource.GetSchemaURL(), "schemas/1.21.0"); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, InvalidSchemaUrlEmptyScheme) +{ + setenv("OTEL_ENTITIES", "service{service.name=app1}@://example.com/schemas/1.0.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity should be processed but schema URL ignored (invalid scheme) + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_TRUE(resource.GetSchemaURL().empty()); + + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, InvalidSchemaUrlInvalidScheme) +{ + setenv("OTEL_ENTITIES", "service{service.name=app1}@123://example.com/schemas/1.0.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + // Entity should be processed but schema URL ignored (scheme must start with letter) + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_TRUE(resource.GetSchemaURL().empty()); unsetenv("OTEL_ENTITIES"); } @@ -324,9 +355,9 @@ TEST(EnvEntityDetectorTest, EmptySchemaUrl) unsetenv("OTEL_ENTITIES"); } -TEST(EnvEntityDetectorTest, ValidSchemaUrl) +TEST(EnvEntityDetectorTest, ValidSchemaUrlAbsolute) { - // Test: Valid schema URL with "://" - should be accepted + // Test: Valid absolute schema URL with "://" - should be accepted setenv("OTEL_ENTITIES", "service{service.name=app1}@https://opentelemetry.io/schemas/1.0.0", 1); EnvEntityDetector detector; @@ -339,6 +370,32 @@ TEST(EnvEntityDetectorTest, ValidSchemaUrl) unsetenv("OTEL_ENTITIES"); } +TEST(EnvEntityDetectorTest, ValidSchemaUrlRelative) +{ + setenv("OTEL_ENTITIES", "service{service.name=app1}@/schemas/1.21.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(resource.GetSchemaURL(), "/schemas/1.21.0"); + unsetenv("OTEL_ENTITIES"); +} + +TEST(EnvEntityDetectorTest, ValidSchemaUrlHttp) +{ + setenv("OTEL_ENTITIES", "service{service.name=app1}@http://example.com/schemas/1.0.0", 1); + + EnvEntityDetector detector; + auto resource = detector.Detect(); + const auto &received_attributes = resource.GetAttributes(); + + EXPECT_EQ(nostd::get(received_attributes.at("service.name")), "app1"); + EXPECT_EQ(resource.GetSchemaURL(), "http://example.com/schemas/1.0.0"); + unsetenv("OTEL_ENTITIES"); +} + TEST(EnvEntityDetectorTest, SchemaUrlWithWhitespace) { // Test: Schema URL with only whitespace - should be treated as empty after trim From a8f90dc7cdab9024194880cc6037186e7d5d7ad0 Mon Sep 17 00:00:00 2001 From: Hasv07 Date: Mon, 5 Jan 2026 23:13:04 +0000 Subject: [PATCH 5/6] [resource_detectors] Use safe type handling for OwnedAttributeValue in EnvEntityDetector - Use nostd::get_if instead of nostd::get for safe string access - Use direct variant comparison instead of string conversion - Handles type mismatches correctly (variants with different types compare as unequal) --- resource_detectors/env_entity_detector.cc | 14 +++++++------- resource_detectors/test/CMakeLists.txt | 13 ++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/resource_detectors/env_entity_detector.cc b/resource_detectors/env_entity_detector.cc index a2676a82ee..6334cf08b0 100644 --- a/resource_detectors/env_entity_detector.cc +++ b/resource_detectors/env_entity_detector.cc @@ -61,7 +61,11 @@ std::string BuildEntityIdentityKey(const std::string &type, } key += items[i]->first; key += "="; - key += opentelemetry::nostd::get(items[i]->second); + const auto *str_val = opentelemetry::nostd::get_if(&items[i]->second); + if (str_val != nullptr) + { + key += *str_val; + } } return key; } @@ -335,9 +339,7 @@ opentelemetry::sdk::resource::Resource EnvEntityDetector::Detect() noexcept for (const auto &attr : entity.id_attrs) { auto existing = resource_attrs.find(attr.first); - if (existing != resource_attrs.end() && - opentelemetry::nostd::get(existing->second) != - opentelemetry::nostd::get(attr.second)) + if (existing != resource_attrs.end() && existing->second != attr.second) { OTEL_INTERNAL_LOG_WARN( "[EnvEntityDetector] Conflicting identifying attribute in OTEL_ENTITIES, " @@ -350,9 +352,7 @@ opentelemetry::sdk::resource::Resource EnvEntityDetector::Detect() noexcept for (const auto &attr : entity.desc_attrs) { auto existing = resource_attrs.find(attr.first); - if (existing != resource_attrs.end() && - opentelemetry::nostd::get(existing->second) != - opentelemetry::nostd::get(attr.second)) + if (existing != resource_attrs.end() && existing->second != attr.second) { OTEL_INTERNAL_LOG_WARN( "[EnvEntityDetector] Conflicting descriptive attribute in OTEL_ENTITIES, " diff --git a/resource_detectors/test/CMakeLists.txt b/resource_detectors/test/CMakeLists.txt index b1281e904d..53c793d045 100644 --- a/resource_detectors/test/CMakeLists.txt +++ b/resource_detectors/test/CMakeLists.txt @@ -1,16 +1,15 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -add_executable(resource_detector_test container_detector_test.cc - process_detector_test.cc - env_entity_detector_test.cc) +add_executable( + resource_detector_test container_detector_test.cc process_detector_test.cc + env_entity_detector_test.cc) # Link the required dependencies target_link_libraries( - resource_detector_test PRIVATE opentelemetry_resource_detectors - opentelemetry_api - opentelemetry_resources - GTest::gtest_main) + resource_detector_test + PRIVATE opentelemetry_resource_detectors opentelemetry_api + opentelemetry_resources GTest::gtest_main) gtest_add_tests( TARGET resource_detector_test From d37895a03669f7bc25fc77ba0d249e875ca313f0 Mon Sep 17 00:00:00 2001 From: Hasv07 Date: Tue, 6 Jan 2026 05:38:28 +0000 Subject: [PATCH 6/6] Fix IWYU warnings: add missing includes to env_entity_detector_test --- resource_detectors/test/env_entity_detector_test.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resource_detectors/test/env_entity_detector_test.cc b/resource_detectors/test/env_entity_detector_test.cc index 5792e406fd..33ad7fc0fc 100644 --- a/resource_detectors/test/env_entity_detector_test.cc +++ b/resource_detectors/test/env_entity_detector_test.cc @@ -4,9 +4,11 @@ #include #include #include +#include #include "opentelemetry/nostd/variant.h" #include "opentelemetry/resource_detectors/env_entity_detector.h" +#include "opentelemetry/sdk/resource/resource.h" #if defined(_MSC_VER) # include "opentelemetry/sdk/common/env_variables.h"