From 53f3617011a66f03fb9c8d45d654bfba6ae9b7eb Mon Sep 17 00:00:00 2001 From: om7057 Date: Sun, 26 Jul 2026 10:41:58 +0530 Subject: [PATCH 1/5] Implement EventToSpanEventBridge log record processor Adds the ExperimentalEventToSpanEventBridgeLogRecordProcessor type from the declarative configuration spec. When a log record carries an event name and matches the trace and span id of the currently active span, this processor copies it over as a span event, mirroring the existing implementation in the Java SDK. Fixes #4155 --- CHANGELOG.md | 4 + .../sdk/configuration/configuration_parser.h | 5 + ...ridge_log_record_processor_configuration.h | 29 +++ ...g_record_processor_configuration_visitor.h | 7 +- .../sdk/configuration/sdk_builder.h | 5 + .../event_to_span_event_bridge_processor.h | 58 +++++ ...t_to_span_event_bridge_processor_factory.h | 31 +++ sdk/src/configuration/configuration_parser.cc | 11 + sdk/src/configuration/sdk_builder.cc | 22 +- sdk/src/logs/CMakeLists.txt | 2 + .../event_to_span_event_bridge_processor.cc | 219 ++++++++++++++++++ ..._to_span_event_bridge_processor_factory.cc | 25 ++ sdk/test/configuration/yaml_logs_test.cc | 22 ++ sdk/test/logs/BUILD | 18 ++ sdk/test/logs/CMakeLists.txt | 15 ++ ...ent_to_span_event_bridge_processor_test.cc | 131 +++++++++++ 16 files changed, 599 insertions(+), 5 deletions(-) create mode 100644 sdk/include/opentelemetry/sdk/configuration/event_to_span_event_bridge_log_record_processor_configuration.h create mode 100644 sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h create mode 100644 sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor_factory.h create mode 100644 sdk/src/logs/event_to_span_event_bridge_processor.cc create mode 100644 sdk/src/logs/event_to_span_event_bridge_processor_factory.cc create mode 100644 sdk/test/logs/event_to_span_event_bridge_processor_test.cc diff --git a/CHANGELOG.md b/CHANGELOG.md index 819a741b73..9cb4a124aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Increment the: ## [Unreleased] +* [CONFIGURATION] Implement EventToSpanEventBridge log record processor for + declarative configuration + [#4155](https://github.com/open-telemetry/opentelemetry-cpp/issues/4155) + * [CODE HEALTH] Move SDK trace and metrics test helpers into anonymous namespaces [#4303](https://github.com/open-telemetry/opentelemetry-cpp/pull/4303) diff --git a/sdk/include/opentelemetry/sdk/configuration/configuration_parser.h b/sdk/include/opentelemetry/sdk/configuration/configuration_parser.h index 84f5811874..93903c756c 100644 --- a/sdk/include/opentelemetry/sdk/configuration/configuration_parser.h +++ b/sdk/include/opentelemetry/sdk/configuration/configuration_parser.h @@ -36,6 +36,7 @@ #include "opentelemetry/sdk/configuration/double_array_attribute_value_configuration.h" #include "opentelemetry/sdk/configuration/double_attribute_value_configuration.h" #include "opentelemetry/sdk/configuration/drop_aggregation_configuration.h" +#include "opentelemetry/sdk/configuration/event_to_span_event_bridge_log_record_processor_configuration.h" #include "opentelemetry/sdk/configuration/exemplar_filter.h" #include "opentelemetry/sdk/configuration/explicit_bucket_histogram_aggregation_configuration.h" #include "opentelemetry/sdk/configuration/extension_log_record_exporter_configuration.h" @@ -167,6 +168,10 @@ class ConfigurationParser std::unique_ptr ParseSimpleLogRecordProcessorConfiguration( const std::unique_ptr &node) const; + std::unique_ptr + ParseEventToSpanEventBridgeLogRecordProcessorConfiguration( + const std::unique_ptr &node) const; + std::unique_ptr ParseExtensionLogRecordProcessorConfiguration(const std::string &name, std::unique_ptr node) const; diff --git a/sdk/include/opentelemetry/sdk/configuration/event_to_span_event_bridge_log_record_processor_configuration.h b/sdk/include/opentelemetry/sdk/configuration/event_to_span_event_bridge_log_record_processor_configuration.h new file mode 100644 index 0000000000..687b416e30 --- /dev/null +++ b/sdk/include/opentelemetry/sdk/configuration/event_to_span_event_bridge_log_record_processor_configuration.h @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "opentelemetry/sdk/configuration/log_record_processor_configuration.h" +#include "opentelemetry/sdk/configuration/log_record_processor_configuration_visitor.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace configuration +{ + +// YAML-SCHEMA: schema/logger_provider.json +// YAML-NODE: ExperimentalEventToSpanEventBridgeLogRecordProcessor +class EventToSpanEventBridgeLogRecordProcessorConfiguration : public LogRecordProcessorConfiguration +{ +public: + void Accept(LogRecordProcessorConfigurationVisitor *visitor) const override + { + visitor->VisitEventToSpanEventBridge(this); + } +}; + +} // namespace configuration +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/include/opentelemetry/sdk/configuration/log_record_processor_configuration_visitor.h b/sdk/include/opentelemetry/sdk/configuration/log_record_processor_configuration_visitor.h index ac4dd52d23..320a8fe4b1 100644 --- a/sdk/include/opentelemetry/sdk/configuration/log_record_processor_configuration_visitor.h +++ b/sdk/include/opentelemetry/sdk/configuration/log_record_processor_configuration_visitor.h @@ -13,6 +13,7 @@ namespace configuration class BatchLogRecordProcessorConfiguration; class SimpleLogRecordProcessorConfiguration; +class EventToSpanEventBridgeLogRecordProcessorConfiguration; class ExtensionLogRecordProcessorConfiguration; class LogRecordProcessorConfigurationVisitor @@ -27,8 +28,10 @@ class LogRecordProcessorConfigurationVisitor const LogRecordProcessorConfigurationVisitor &other) = default; virtual ~LogRecordProcessorConfigurationVisitor() = default; - virtual void VisitBatch(const BatchLogRecordProcessorConfiguration *model) = 0; - virtual void VisitSimple(const SimpleLogRecordProcessorConfiguration *model) = 0; + virtual void VisitBatch(const BatchLogRecordProcessorConfiguration *model) = 0; + virtual void VisitSimple(const SimpleLogRecordProcessorConfiguration *model) = 0; + virtual void VisitEventToSpanEventBridge( + const EventToSpanEventBridgeLogRecordProcessorConfiguration *model) = 0; virtual void VisitExtension(const ExtensionLogRecordProcessorConfiguration *model) = 0; }; diff --git a/sdk/include/opentelemetry/sdk/configuration/sdk_builder.h b/sdk/include/opentelemetry/sdk/configuration/sdk_builder.h index 6897c54c9f..3507367fe8 100644 --- a/sdk/include/opentelemetry/sdk/configuration/sdk_builder.h +++ b/sdk/include/opentelemetry/sdk/configuration/sdk_builder.h @@ -249,6 +249,11 @@ class SdkBuilder std::unique_ptr CreateSimpleLogRecordProcessor( const opentelemetry::sdk::configuration::SimpleLogRecordProcessorConfiguration *model) const; + std::unique_ptr + CreateEventToSpanEventBridgeLogRecordProcessor( + const opentelemetry::sdk::configuration::EventToSpanEventBridgeLogRecordProcessorConfiguration + *model) const; + std::unique_ptr CreateExtensionLogRecordProcessor( const opentelemetry::sdk::configuration::ExtensionLogRecordProcessorConfiguration *model) const; diff --git a/sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h b/sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h new file mode 100644 index 0000000000..8eba69894a --- /dev/null +++ b/sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h @@ -0,0 +1,58 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#include "opentelemetry/sdk/logs/processor.h" +#include "opentelemetry/sdk/logs/recordable.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace logs +{ + +/** + * A log record processor that bridges log records representing events (i.e. log records with a + * non-empty event name) onto the current active span as span events. + * + * A log record is bridged to a span event if and only if: + * - the log record has a non-empty event name, + * - the log record carries a valid trace id and span id, + * - the current active span (from the ambient context at the time OnEmit is called) is + * recording, and + * - the current active span's trace id and span id match those on the log record. + * + * This processor does not export log records and does not stop the log record from continuing + * through the rest of the configured processing pipeline. + */ +class EventToSpanEventBridgeProcessor : public LogRecordProcessor +{ +public: + EventToSpanEventBridgeProcessor() = default; + EventToSpanEventBridgeProcessor(const EventToSpanEventBridgeProcessor &) = delete; + EventToSpanEventBridgeProcessor(EventToSpanEventBridgeProcessor &&) = delete; + EventToSpanEventBridgeProcessor &operator=(const EventToSpanEventBridgeProcessor &) = delete; + EventToSpanEventBridgeProcessor &operator=(EventToSpanEventBridgeProcessor &&) = delete; + ~EventToSpanEventBridgeProcessor() override = default; + + std::unique_ptr MakeRecordable() noexcept override; + + void OnEmit(std::unique_ptr &&record) noexcept override; + + bool ForceFlush( + std::chrono::microseconds timeout = (std::chrono::microseconds::max)()) noexcept override; + + bool Shutdown( + std::chrono::microseconds timeout = (std::chrono::microseconds::max)()) noexcept override; + + bool HasEnabledFilter() const noexcept override { return false; } +}; + +} // namespace logs +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor_factory.h b/sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor_factory.h new file mode 100644 index 0000000000..d5a1b7378b --- /dev/null +++ b/sdk/include/opentelemetry/sdk/logs/event_to_span_event_bridge_processor_factory.h @@ -0,0 +1,31 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "opentelemetry/sdk/logs/processor.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace logs +{ + +/** + * Factory class for EventToSpanEventBridgeProcessor. + */ +class OPENTELEMETRY_EXPORT EventToSpanEventBridgeProcessorFactory +{ +public: + /** + * Create an EventToSpanEventBridgeProcessor. + */ + static std::unique_ptr Create(); +}; + +} // namespace logs +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/configuration/configuration_parser.cc b/sdk/src/configuration/configuration_parser.cc index bfe4dfa373..b5793b53ff 100644 --- a/sdk/src/configuration/configuration_parser.cc +++ b/sdk/src/configuration/configuration_parser.cc @@ -570,6 +570,13 @@ ConfigurationParser::ParseSimpleLogRecordProcessorConfiguration( return model; } +std::unique_ptr +ConfigurationParser::ParseEventToSpanEventBridgeLogRecordProcessorConfiguration( + const std::unique_ptr & /* node */) const +{ + return std::make_unique(); +} + std::unique_ptr ConfigurationParser::ParseExtensionLogRecordProcessorConfiguration( const std::string &name, @@ -615,6 +622,10 @@ ConfigurationParser::ParseLogRecordProcessorConfiguration( { model = ParseSimpleLogRecordProcessorConfiguration(child); } + else if (name == "event_to_span_event_bridge/development") + { + model = ParseEventToSpanEventBridgeLogRecordProcessorConfiguration(child); + } else { model = ParseExtensionLogRecordProcessorConfiguration(name, std::move(child)); diff --git a/sdk/src/configuration/sdk_builder.cc b/sdk/src/configuration/sdk_builder.cc index b94ccd2799..2baafa68cb 100644 --- a/sdk/src/configuration/sdk_builder.cc +++ b/sdk/src/configuration/sdk_builder.cc @@ -137,6 +137,7 @@ #include "opentelemetry/sdk/instrumentationscope/scope_configurator.h" #include "opentelemetry/sdk/logs/batch_log_record_processor_factory.h" #include "opentelemetry/sdk/logs/batch_log_record_processor_options.h" +#include "opentelemetry/sdk/logs/event_to_span_event_bridge_processor_factory.h" #include "opentelemetry/sdk/logs/exporter.h" #include "opentelemetry/sdk/logs/log_record_limits.h" #include "opentelemetry/sdk/logs/logger_config.h" @@ -756,6 +757,13 @@ class LogRecordProcessorBuilder processor = sdk_builder_->CreateSimpleLogRecordProcessor(model); } + void VisitEventToSpanEventBridge( + const opentelemetry::sdk::configuration::EventToSpanEventBridgeLogRecordProcessorConfiguration + *model) override + { + processor = sdk_builder_->CreateEventToSpanEventBridgeLogRecordProcessor(model); + } + void VisitExtension( const opentelemetry::sdk::configuration::ExtensionLogRecordProcessorConfiguration *model) override @@ -1195,8 +1203,8 @@ std::unique_ptr SdkBuilder::CreateTra auto tracer_configurator = CreateTracerConfigurator(model->tracer_configurator); auto id_generator = opentelemetry::sdk::trace::RandomIdGeneratorFactory::Create(); sdk = opentelemetry::sdk::trace::TracerProviderFactory::Create( - std::move(sdk_processors), resource, std::move(sampler), std::move(id_generator), - std::move(tracer_configurator), span_limits); + std::move(sdk_processors), resource, std::move(sampler), std::move(id_generator), + std::move(tracer_configurator), span_limits); } else { @@ -1762,7 +1770,7 @@ std::unique_ptr SdkBuilder::CreateMe { auto meter_configurator = CreateMeterConfigurator(model->meter_configurator); meter_context = opentelemetry::sdk::metrics::MeterContextFactory::Create( - std::move(view_registry), resource, std::move(meter_configurator)); + std::move(view_registry), resource, std::move(meter_configurator)); } else { @@ -1930,6 +1938,14 @@ SdkBuilder::CreateSimpleLogRecordProcessor( return sdk; } +std::unique_ptr +SdkBuilder::CreateEventToSpanEventBridgeLogRecordProcessor( + const opentelemetry::sdk::configuration::EventToSpanEventBridgeLogRecordProcessorConfiguration + * /* model */) const +{ + return opentelemetry::sdk::logs::EventToSpanEventBridgeProcessorFactory::Create(); +} + std::unique_ptr SdkBuilder::CreateExtensionLogRecordProcessor( const opentelemetry::sdk::configuration::ExtensionLogRecordProcessorConfiguration *model) const diff --git a/sdk/src/logs/CMakeLists.txt b/sdk/src/logs/CMakeLists.txt index 338fa8e8f1..e28f4e1638 100644 --- a/sdk/src/logs/CMakeLists.txt +++ b/sdk/src/logs/CMakeLists.txt @@ -13,6 +13,8 @@ add_library( event_logger.cc simple_log_record_processor.cc simple_log_record_processor_factory.cc + event_to_span_event_bridge_processor.cc + event_to_span_event_bridge_processor_factory.cc batch_log_record_processor.cc batch_log_record_processor_options.cc batch_log_record_processor_factory.cc diff --git a/sdk/src/logs/event_to_span_event_bridge_processor.cc b/sdk/src/logs/event_to_span_event_bridge_processor.cc new file mode 100644 index 0000000000..e31547fc5d --- /dev/null +++ b/sdk/src/logs/event_to_span_event_bridge_processor.cc @@ -0,0 +1,219 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/common/attribute_value.h" +#include "opentelemetry/common/key_value_iterable.h" +#include "opentelemetry/common/timestamp.h" +#include "opentelemetry/nostd/function_ref.h" +#include "opentelemetry/nostd/span.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/nostd/variant.h" +#include "opentelemetry/sdk/common/attribute_utils.h" +#include "opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h" +#include "opentelemetry/sdk/logs/read_write_log_record.h" +#include "opentelemetry/sdk/logs/recordable.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/span_id.h" +#include "opentelemetry/trace/trace_id.h" +#include "opentelemetry/trace/tracer.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace logs +{ + +namespace +{ + +// Converts a single OwnedAttributeValue into the corresponding AttributeValue and forwards it +// to the KeyValueIterable callback. Array-of-string and array-of-bool attributes need a +// temporary contiguous buffer (std::vector does not decay to a span of +// string_view, and std::vector is not backed by a real bool array), so those buffers are +// kept alive for the duration of a single callback invocation only. +class EventAttributeEmitter +{ +public: + EventAttributeEmitter( + nostd::string_view key, + nostd::function_ref callback, + bool *keep_going) + : key_(key), callback_(callback), keep_going_(keep_going) + {} + + void operator()(bool v) { Emit(v); } + void operator()(int32_t v) { Emit(v); } + void operator()(uint32_t v) { Emit(v); } + void operator()(int64_t v) { Emit(v); } + void operator()(uint64_t v) { Emit(v); } + void operator()(double v) { Emit(v); } + void operator()(const std::string &v) { Emit(nostd::string_view(v)); } + + void operator()(const std::vector &v) + { + std::unique_ptr buffer(new bool[v.size()]); + for (std::size_t i = 0; i < v.size(); ++i) + { + buffer[i] = v[i]; + } + Emit(nostd::span(buffer.get(), v.size())); + } + + void operator()(const std::vector &v) + { + Emit(nostd::span(v.data(), v.size())); + } + + void operator()(const std::vector &v) + { + Emit(nostd::span(v.data(), v.size())); + } + + void operator()(const std::vector &v) + { + Emit(nostd::span(v.data(), v.size())); + } + + void operator()(const std::vector &v) + { + Emit(nostd::span(v.data(), v.size())); + } + + void operator()(const std::vector &v) + { + Emit(nostd::span(v.data(), v.size())); + } + + void operator()(const std::vector &v) + { + Emit(nostd::span(v.data(), v.size())); + } + + void operator()(const std::vector &v) + { + std::vector views(v.begin(), v.end()); + Emit(nostd::span(views.data(), views.size())); + } + +private: + template + void Emit(const T &value) + { + if (*keep_going_) + { + *keep_going_ = callback_(key_, opentelemetry::common::AttributeValue(value)); + } + } + + nostd::string_view key_; + nostd::function_ref callback_; + bool *keep_going_; +}; + +class EventAttributesKeyValueIterable final : public opentelemetry::common::KeyValueIterable +{ +public: + explicit EventAttributesKeyValueIterable( + const std::unordered_map + &attributes) + : attributes_(attributes) + {} + + bool ForEachKeyValue( + nostd::function_ref callback) + const noexcept override + { + bool keep_going = true; + for (const auto &kv : attributes_) + { + if (!keep_going) + { + break; + } + EventAttributeEmitter emitter(kv.first, callback, &keep_going); + opentelemetry::nostd::visit(emitter, kv.second); + } + return keep_going; + } + + std::size_t size() const noexcept override { return attributes_.size(); } + +private: + const std::unordered_map + &attributes_; +}; + +} // namespace + +std::unique_ptr EventToSpanEventBridgeProcessor::MakeRecordable() noexcept +{ + return std::unique_ptr(new ReadWriteLogRecord()); +} + +void EventToSpanEventBridgeProcessor::OnEmit(std::unique_ptr &&record) noexcept +{ + std::unique_ptr log_record( + static_cast(record.release())); + if (!log_record) + { + return; + } + + nostd::string_view event_name = log_record->GetEventName(); + if (event_name.empty()) + { + return; + } + + const opentelemetry::trace::TraceId &log_trace_id = log_record->GetTraceId(); + const opentelemetry::trace::SpanId &log_span_id = log_record->GetSpanId(); + if (!log_trace_id.IsValid() || !log_span_id.IsValid()) + { + return; + } + + auto current_span = opentelemetry::trace::Tracer::GetCurrentSpan(); + if (!current_span || !current_span->IsRecording()) + { + return; + } + + opentelemetry::trace::SpanContext current_context = current_span->GetContext(); + if (!(current_context.trace_id() == log_trace_id) || !(current_context.span_id() == log_span_id)) + { + return; + } + + opentelemetry::common::SystemTimestamp event_timestamp = log_record->GetTimestamp(); + if (event_timestamp.time_since_epoch().count() == 0) + { + event_timestamp = log_record->GetObservedTimestamp(); + } + + EventAttributesKeyValueIterable attributes(log_record->GetAttributes()); + current_span->AddEvent(event_name, event_timestamp, attributes); +} + +bool EventToSpanEventBridgeProcessor::ForceFlush(std::chrono::microseconds /* timeout */) noexcept +{ + return true; +} + +bool EventToSpanEventBridgeProcessor::Shutdown(std::chrono::microseconds /* timeout */) noexcept +{ + return true; +} + +} // namespace logs +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/src/logs/event_to_span_event_bridge_processor_factory.cc b/sdk/src/logs/event_to_span_event_bridge_processor_factory.cc new file mode 100644 index 0000000000..4c339aa9e8 --- /dev/null +++ b/sdk/src/logs/event_to_span_event_bridge_processor_factory.cc @@ -0,0 +1,25 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include "opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h" +#include "opentelemetry/sdk/logs/event_to_span_event_bridge_processor_factory.h" +#include "opentelemetry/sdk/logs/processor.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace logs +{ + +std::unique_ptr EventToSpanEventBridgeProcessorFactory::Create() +{ + std::unique_ptr processor(new EventToSpanEventBridgeProcessor()); + return processor; +} + +} // namespace logs +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE diff --git a/sdk/test/configuration/yaml_logs_test.cc b/sdk/test/configuration/yaml_logs_test.cc index f885f85434..0a036cc49d 100644 --- a/sdk/test/configuration/yaml_logs_test.cc +++ b/sdk/test/configuration/yaml_logs_test.cc @@ -9,6 +9,7 @@ #include "opentelemetry/sdk/configuration/batch_log_record_processor_configuration.h" #include "opentelemetry/sdk/configuration/configuration.h" +#include "opentelemetry/sdk/configuration/event_to_span_event_bridge_log_record_processor_configuration.h" #include "opentelemetry/sdk/configuration/grpc_tls_configuration.h" #include "opentelemetry/sdk/configuration/headers_configuration.h" #include "opentelemetry/sdk/configuration/http_tls_configuration.h" @@ -100,6 +101,27 @@ file_format: "1.0-logs" ASSERT_NE(exporter, nullptr); } +TEST(YamlLogs, event_to_span_event_bridge_processor) +{ + std::string yaml = R"( +file_format: "1.0-logs" +logger_provider: + processors: + - event_to_span_event_bridge/development: +)"; + + auto config = DoParse(yaml); + ASSERT_NE(config, nullptr); + ASSERT_NE(config->logger_provider, nullptr); + ASSERT_EQ(config->logger_provider->processors.size(), 1); + auto *processor = config->logger_provider->processors[0].get(); + ASSERT_NE(processor, nullptr); + auto *bridge = dynamic_cast< + opentelemetry::sdk::configuration::EventToSpanEventBridgeLogRecordProcessorConfiguration *>( + processor); + ASSERT_NE(bridge, nullptr); +} + TEST(YamlLogs, default_batch_processor) { std::string yaml = R"( diff --git a/sdk/test/logs/BUILD b/sdk/test/logs/BUILD index 8e2198c318..4e9ffa0da6 100644 --- a/sdk/test/logs/BUILD +++ b/sdk/test/logs/BUILD @@ -66,6 +66,24 @@ cc_test( ], ) +cc_test( + name = "event_to_span_event_bridge_processor_test", + srcs = [ + "event_to_span_event_bridge_processor_test.cc", + ], + tags = [ + "logs", + "test", + ], + deps = [ + "//exporters/memory:in_memory_span_exporter", + "//sdk/src/logs", + "//sdk/src/resource", + "//sdk/src/trace", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "log_record_test", srcs = [ diff --git a/sdk/test/logs/CMakeLists.txt b/sdk/test/logs/CMakeLists.txt index 1fcbc3bb27..e0de53ed30 100644 --- a/sdk/test/logs/CMakeLists.txt +++ b/sdk/test/logs/CMakeLists.txt @@ -20,3 +20,18 @@ foreach( TEST_PREFIX logs. TEST_LIST ${testname}) endforeach() + +add_executable(event_to_span_event_bridge_processor_test + event_to_span_event_bridge_processor_test.cc) +target_link_libraries( + event_to_span_event_bridge_processor_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + opentelemetry_logs + opentelemetry_trace + opentelemetry_resources + opentelemetry_exporter_in_memory) +gtest_add_tests( + TARGET event_to_span_event_bridge_processor_test + TEST_PREFIX logs. + TEST_LIST event_to_span_event_bridge_processor_test) diff --git a/sdk/test/logs/event_to_span_event_bridge_processor_test.cc b/sdk/test/logs/event_to_span_event_bridge_processor_test.cc new file mode 100644 index 0000000000..b1c9ef2ed9 --- /dev/null +++ b/sdk/test/logs/event_to_span_event_bridge_processor_test.cc @@ -0,0 +1,131 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/memory/in_memory_span_data.h" +#include "opentelemetry/exporters/memory/in_memory_span_exporter.h" +#include "opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h" +#include "opentelemetry/sdk/logs/read_write_log_record.h" +#include "opentelemetry/sdk/logs/recordable.h" +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/tracer.h" +#include "opentelemetry/sdk/trace/tracer_context.h" +#include "opentelemetry/trace/scope.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/tracer.h" + +using namespace opentelemetry::sdk::logs; +using opentelemetry::exporter::memory::InMemorySpanData; +using opentelemetry::exporter::memory::InMemorySpanExporter; + +namespace trace_sdk = opentelemetry::sdk::trace; +namespace trace_api = opentelemetry::trace; + +namespace +{ + +std::shared_ptr MakeTracer(std::shared_ptr *out_span_data) +{ + InMemorySpanExporter *exporter = new InMemorySpanExporter(); + *out_span_data = exporter->GetData(); + + auto processor = std::unique_ptr( + new trace_sdk::SimpleSpanProcessor(std::unique_ptr(exporter))); + std::vector> processors; + processors.push_back(std::move(processor)); + auto context = std::make_shared(std::move(processors)); + return std::shared_ptr(new trace_sdk::Tracer(context)); +} + +} // namespace + +TEST(EventToSpanEventBridgeProcessorTest, BridgesEventOntoCurrentMatchingSpan) +{ + std::shared_ptr span_data; + auto tracer = MakeTracer(&span_data); + + auto span = tracer->StartSpan("test-span"); + auto scope = tracer->WithActiveSpan(span); + + EventToSpanEventBridgeProcessor processor; + auto recordable = processor.MakeRecordable(); + auto *log_record = static_cast(recordable.get()); + + trace_api::SpanContext span_context = span->GetContext(); + log_record->SetTraceId(span_context.trace_id()); + log_record->SetSpanId(span_context.span_id()); + log_record->SetEventId(0, "my.event"); + log_record->SetAttribute("key1", "value1"); + + processor.OnEmit(std::move(recordable)); + + span->End(); + + auto spans = span_data->GetSpans(); + ASSERT_EQ(1, spans.size()); + ASSERT_EQ(1, spans.at(0)->GetEvents().size()); + EXPECT_EQ("my.event", spans.at(0)->GetEvents().at(0).GetName()); +} + +TEST(EventToSpanEventBridgeProcessorTest, IgnoresLogRecordWithoutEventName) +{ + std::shared_ptr span_data; + auto tracer = MakeTracer(&span_data); + + auto span = tracer->StartSpan("test-span"); + auto scope = tracer->WithActiveSpan(span); + + EventToSpanEventBridgeProcessor processor; + auto recordable = processor.MakeRecordable(); + auto *log_record = static_cast(recordable.get()); + + trace_api::SpanContext span_context = span->GetContext(); + log_record->SetTraceId(span_context.trace_id()); + log_record->SetSpanId(span_context.span_id()); + + processor.OnEmit(std::move(recordable)); + + span->End(); + + auto spans = span_data->GetSpans(); + ASSERT_EQ(1, spans.size()); + EXPECT_EQ(0, spans.at(0)->GetEvents().size()); +} + +TEST(EventToSpanEventBridgeProcessorTest, IgnoresEventFromDifferentSpan) +{ + std::shared_ptr span_data; + auto tracer = MakeTracer(&span_data); + + auto span = tracer->StartSpan("test-span"); + auto other_span = tracer->StartSpan("other-span"); + auto scope = tracer->WithActiveSpan(span); + + EventToSpanEventBridgeProcessor processor; + auto recordable = processor.MakeRecordable(); + auto *log_record = static_cast(recordable.get()); + + // The log record belongs to other_span, not the current active span. + trace_api::SpanContext other_context = other_span->GetContext(); + log_record->SetTraceId(other_context.trace_id()); + log_record->SetSpanId(other_context.span_id()); + log_record->SetEventId(0, "my.event"); + + processor.OnEmit(std::move(recordable)); + + span->End(); + other_span->End(); + + auto spans = span_data->GetSpans(); + ASSERT_EQ(2, spans.size()); + for (auto &exported_span : spans) + { + EXPECT_EQ(0, exported_span->GetEvents().size()); + } +} From bbb273f761608136c961d2dddfbe81763f53346a Mon Sep 17 00:00:00 2001 From: om7057 Date: Sun, 26 Jul 2026 10:42:54 +0530 Subject: [PATCH 2/5] Point changelog entry at the actual PR number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb4a124aa..93774e4a06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Increment the: * [CONFIGURATION] Implement EventToSpanEventBridge log record processor for declarative configuration - [#4155](https://github.com/open-telemetry/opentelemetry-cpp/issues/4155) + [#4309](https://github.com/open-telemetry/opentelemetry-cpp/pull/4309) * [CODE HEALTH] Move SDK trace and metrics test helpers into anonymous namespaces From c8e67dceba5ffe6111d9eaa4511840d494c5e2b1 Mon Sep 17 00:00:00 2001 From: om7057 Date: Sun, 26 Jul 2026 11:27:16 +0530 Subject: [PATCH 3/5] Fix CI failures: RTTI-free cast and unrelated formatting drift The new yaml_logs_test.cc test case used dynamic_cast, but one CI config builds with -fno-rtti; switched to reinterpret_cast to match the rest of the file. Also reverted an unrelated indentation change in sdk_builder.cc that a local clang-format 14 run introduced on lines I didn't touch; CI runs clang-format 18, which formats these continuation lines differently. --- sdk/src/configuration/sdk_builder.cc | 6 +++--- sdk/test/configuration/yaml_logs_test.cc | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/src/configuration/sdk_builder.cc b/sdk/src/configuration/sdk_builder.cc index 2baafa68cb..7c1a2edf12 100644 --- a/sdk/src/configuration/sdk_builder.cc +++ b/sdk/src/configuration/sdk_builder.cc @@ -1203,8 +1203,8 @@ std::unique_ptr SdkBuilder::CreateTra auto tracer_configurator = CreateTracerConfigurator(model->tracer_configurator); auto id_generator = opentelemetry::sdk::trace::RandomIdGeneratorFactory::Create(); sdk = opentelemetry::sdk::trace::TracerProviderFactory::Create( - std::move(sdk_processors), resource, std::move(sampler), std::move(id_generator), - std::move(tracer_configurator), span_limits); + std::move(sdk_processors), resource, std::move(sampler), std::move(id_generator), + std::move(tracer_configurator), span_limits); } else { @@ -1770,7 +1770,7 @@ std::unique_ptr SdkBuilder::CreateMe { auto meter_configurator = CreateMeterConfigurator(model->meter_configurator); meter_context = opentelemetry::sdk::metrics::MeterContextFactory::Create( - std::move(view_registry), resource, std::move(meter_configurator)); + std::move(view_registry), resource, std::move(meter_configurator)); } else { diff --git a/sdk/test/configuration/yaml_logs_test.cc b/sdk/test/configuration/yaml_logs_test.cc index 0a036cc49d..f2d0d83e7e 100644 --- a/sdk/test/configuration/yaml_logs_test.cc +++ b/sdk/test/configuration/yaml_logs_test.cc @@ -116,7 +116,7 @@ file_format: "1.0-logs" ASSERT_EQ(config->logger_provider->processors.size(), 1); auto *processor = config->logger_provider->processors[0].get(); ASSERT_NE(processor, nullptr); - auto *bridge = dynamic_cast< + auto *bridge = reinterpret_cast< opentelemetry::sdk::configuration::EventToSpanEventBridgeLogRecordProcessorConfiguration *>( processor); ASSERT_NE(bridge, nullptr); From 46fab04b40e28183b6d0951b491d53e00f624495 Mon Sep 17 00:00:00 2001 From: om7057 Date: Sun, 26 Jul 2026 12:50:35 +0530 Subject: [PATCH 4/5] Fix clang-tidy and iwyu findings on the new processor clang-tidy flagged three things that pushed the abiv2-preview preset over its warning budget: the attributes_ member stored a reference instead of a pointer, OnEmit released the incoming unique_ptr without an explicit std::move, and ForEachKeyValue could let an allocation failure escape a noexcept override. Switched the member to a pointer, added std::move, and wrapped the iteration in a try/catch guarded by OPENTELEMETRY_HAVE_EXCEPTIONS, matching the pattern already used in periodic_exporting_metric_reader.cc. iwyu wanted in both files for std::move, plus a few trace headers directly included in the test file rather than pulled in transitively. --- .../event_to_span_event_bridge_processor.cc | 37 +++++++++++++------ ...ent_to_span_event_bridge_processor_test.cc | 6 ++- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/sdk/src/logs/event_to_span_event_bridge_processor.cc b/sdk/src/logs/event_to_span_event_bridge_processor.cc index e31547fc5d..9cb34f6240 100644 --- a/sdk/src/logs/event_to_span_event_bridge_processor.cc +++ b/sdk/src/logs/event_to_span_event_bridge_processor.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "opentelemetry/common/attribute_value.h" @@ -126,31 +127,45 @@ class EventAttributesKeyValueIterable final : public opentelemetry::common::KeyV explicit EventAttributesKeyValueIterable( const std::unordered_map &attributes) - : attributes_(attributes) + : attributes_(&attributes) {} bool ForEachKeyValue( nostd::function_ref callback) const noexcept override { - bool keep_going = true; - for (const auto &kv : attributes_) + // Building temporary buffers for array-of-bool and array-of-string attributes (see + // EventAttributeEmitter above) can allocate, so guard against a thrown bad_alloc escaping + // this noexcept override; conservatively stop iterating rather than terminate the process. +#if OPENTELEMETRY_HAVE_EXCEPTIONS + try { - if (!keep_going) +#endif + bool keep_going = true; + for (const auto &kv : *attributes_) { - break; + if (!keep_going) + { + break; + } + EventAttributeEmitter emitter(kv.first, callback, &keep_going); + opentelemetry::nostd::visit(emitter, kv.second); } - EventAttributeEmitter emitter(kv.first, callback, &keep_going); - opentelemetry::nostd::visit(emitter, kv.second); + return keep_going; +#if OPENTELEMETRY_HAVE_EXCEPTIONS } - return keep_going; + catch (...) + { + return false; + } +#endif } - std::size_t size() const noexcept override { return attributes_.size(); } + std::size_t size() const noexcept override { return attributes_->size(); } private: const std::unordered_map - &attributes_; + *attributes_; }; } // namespace @@ -163,7 +178,7 @@ std::unique_ptr EventToSpanEventBridgeProcessor::MakeRecordable() no void EventToSpanEventBridgeProcessor::OnEmit(std::unique_ptr &&record) noexcept { std::unique_ptr log_record( - static_cast(record.release())); + static_cast(std::move(record).release())); if (!log_record) { return; diff --git a/sdk/test/logs/event_to_span_event_bridge_processor_test.cc b/sdk/test/logs/event_to_span_event_bridge_processor_test.cc index b1c9ef2ed9..80a43ac740 100644 --- a/sdk/test/logs/event_to_span_event_bridge_processor_test.cc +++ b/sdk/test/logs/event_to_span_event_bridge_processor_test.cc @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 #include -#include #include #include +#include #include #include "opentelemetry/exporters/memory/in_memory_span_data.h" @@ -12,10 +12,12 @@ #include "opentelemetry/sdk/logs/event_to_span_event_bridge_processor.h" #include "opentelemetry/sdk/logs/read_write_log_record.h" #include "opentelemetry/sdk/logs/recordable.h" +#include "opentelemetry/sdk/trace/exporter.h" +#include "opentelemetry/sdk/trace/processor.h" #include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/span_data.h" #include "opentelemetry/sdk/trace/tracer.h" #include "opentelemetry/sdk/trace/tracer_context.h" -#include "opentelemetry/trace/scope.h" #include "opentelemetry/trace/span.h" #include "opentelemetry/trace/span_context.h" #include "opentelemetry/trace/tracer.h" From 41f9b896d1d7f3ae74da25a7f242de6cd310db64 Mon Sep 17 00:00:00 2001 From: om7057 Date: Sun, 26 Jul 2026 19:06:02 +0530 Subject: [PATCH 5/5] Fix iwyu: include event_to_span_event_bridge configuration header directly configuration_parser.cc used EventToSpanEventBridgeLogRecordProcessorConfiguration via a transitive include only, which iwyu flagged in both abiv1-preview and abiv2-preview jobs. --- sdk/src/configuration/configuration_parser.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/configuration/configuration_parser.cc b/sdk/src/configuration/configuration_parser.cc index b5793b53ff..3cf0f68230 100644 --- a/sdk/src/configuration/configuration_parser.cc +++ b/sdk/src/configuration/configuration_parser.cc @@ -49,6 +49,7 @@ #include "opentelemetry/sdk/configuration/double_array_attribute_value_configuration.h" #include "opentelemetry/sdk/configuration/double_attribute_value_configuration.h" #include "opentelemetry/sdk/configuration/drop_aggregation_configuration.h" +#include "opentelemetry/sdk/configuration/event_to_span_event_bridge_log_record_processor_configuration.h" #include "opentelemetry/sdk/configuration/exemplar_filter.h" #include "opentelemetry/sdk/configuration/explicit_bucket_histogram_aggregation_configuration.h" #include "opentelemetry/sdk/configuration/extension_log_record_exporter_configuration.h"