From 3deb332bc7157d8801694ec1cb2165e1591b4290 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Mon, 7 Sep 2026 19:03:38 +0000 Subject: [PATCH 01/20] [EXPORTER] Add a JsonWriter interface for OTLP/JSON serialization The OTLP HTTP and file clients each carried their own copy of the same reflection-based protobuf-to-JSON converter. Replace both with a single converter that emits through a JsonWriter interface, and let callers supply their own writer through a JsonWriterFactory. The emitted JSON is unchanged. The factory is injected the way each family already injects thread_instrumentation: as a field on OtlpHttpClientOptions for the HTTP exporters, and on OtlpFileClientRuntimeOptions for the file ones. No existing constructor or factory signature changes, and a null factory selects the default backend. --- CHANGELOG.md | 5 + CMakeLists.txt | 18 +- exporters/otlp/BUILD | 104 ++++- exporters/otlp/CMakeLists.txt | 394 +++++++++++++----- .../otlp/detail/default_json_writer_factory.h | 40 ++ .../exporters/otlp/otlp_file_client.h | 4 + .../otlp/otlp_file_client_runtime_options.h | 3 + .../exporters/otlp/otlp_http_client.h | 6 + .../otlp/otlp_http_exporter_runtime_options.h | 3 + ...http_log_record_exporter_runtime_options.h | 3 + ...tlp_http_metric_exporter_runtime_options.h | 3 + .../exporters/otlp/otlp_json_converter.h | 46 ++ .../exporters/otlp/otlp_json_writer.h | 53 +++ .../exporters/otlp/otlp_json_writer_factory.h | 32 ++ .../otlp/otlp_json_writer_factory_nlohmann.h | 26 ++ exporters/otlp/src/otlp_file_client.cc | 287 +------------ exporters/otlp/src/otlp_http_client.cc | 306 +------------- exporters/otlp/src/otlp_http_exporter.cc | 226 ++-------- .../otlp/src/otlp_http_log_record_exporter.cc | 227 ++-------- .../otlp/src/otlp_http_metric_exporter.cc | 226 ++-------- exporters/otlp/src/otlp_json_converter.cc | 338 +++++++++++++++ .../otlp/src/otlp_json_writer_nlohmann.cc | 217 ++++++++++ exporters/otlp/test/otlp_file_client_test.cc | 36 ++ .../test/otlp_file_exporter_factory_test.cc | 13 + .../otlp/test/otlp_file_exporter_test.cc | 79 ++++ ...p_file_log_record_exporter_factory_test.cc | 13 + .../otlp_file_log_record_exporter_test.cc | 56 +++ .../otlp_file_metric_exporter_factory_test.cc | 13 + .../test/otlp_file_metric_exporter_test.cc | 88 ++++ ...p_http_exporter_custom_json_writer_test.cc | 117 ++++++ .../otlp/test/otlp_http_exporter_test.cc | 78 ++++ ...record_exporter_custom_json_writer_test.cc | 47 +++ .../otlp_http_log_record_exporter_test.cc | 55 +++ ...metric_exporter_custom_json_writer_test.cc | 47 +++ .../test/otlp_http_metric_exporter_test.cc | 85 ++++ .../test/otlp_http_request_body_capture.h | 43 ++ .../test/otlp_json_writer_nlohmann_test.cc | 223 ++++++++++ .../otlp/test/otlp_marking_json_writer.h | 67 +++ exporters/otlp/test/otlp_stub_json_writer.h | 58 +++ 39 files changed, 2437 insertions(+), 1248 deletions(-) create mode 100644 exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h create mode 100644 exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h create mode 100644 exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h create mode 100644 exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h create mode 100644 exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h create mode 100644 exporters/otlp/src/otlp_json_converter.cc create mode 100644 exporters/otlp/src/otlp_json_writer_nlohmann.cc create mode 100644 exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc create mode 100644 exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc create mode 100644 exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc create mode 100644 exporters/otlp/test/otlp_http_request_body_capture.h create mode 100644 exporters/otlp/test/otlp_json_writer_nlohmann_test.cc create mode 100644 exporters/otlp/test/otlp_marking_json_writer.h create mode 100644 exporters/otlp/test/otlp_stub_json_writer.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e93bc6dfc..6321b728ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ Increment the: ## [Unreleased] +* [EXPORTER] Add a `JsonWriter` interface for OTLP/JSON serialization, so a + consumer can supply their own JSON backend in place of the default + nlohmann-json one. + [#2541](https://github.com/open-telemetry/opentelemetry-cpp/issues/2541) + * [DOC] Fix and clarify the `StartSpanOptions` documentation [#4526](https://github.com/open-telemetry/opentelemetry-cpp/pull/4526) diff --git a/CMakeLists.txt b/CMakeLists.txt index 999b271207..7e322f4aa9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -357,6 +357,17 @@ otelcpp_dependent_option( "OTELCPP_WITH_OTLP_HTTP OR OTELCPP_WITH_ELASTICSEARCH OR OTELCPP_WITH_ZIPKIN OR OTELCPP_BUILD_W3CTRACECONTEXT_TEST OR OTELCPP_WITH_EXAMPLES_HTTP" OFF) +# +# Do we need the NLOHMANN JSON WRITER ? +# + +otelcpp_dependent_option( + OTELCPP_WITH_JSON_WRITER_NLOHMANN + "Use the nlohmann-json JsonWriter backend. Defaults to ON when an OTLP exporter that emits JSON is enabled; set OFF to supply a custom JsonWriterFactory." + ON + "OTELCPP_WITH_OTLP_HTTP OR OTELCPP_WITH_OTLP_FILE" + OFF) + # # Locally override options if OTELCPP_WITH_API_ONLY is set to prevent third # party dependencies from being imported @@ -375,6 +386,7 @@ if(OTELCPP_WITH_API_ONLY) otelcpp_set_option(OTELCPP_WITH_ETW OFF) otelcpp_set_option(OTELCPP_WITH_CONFIGURATION OFF) otelcpp_set_option(OTELCPP_WITH_HTTP_CLIENT_CURL OFF) + otelcpp_set_option(OTELCPP_WITH_JSON_WRITER_NLOHMANN OFF) otelcpp_set_option(OTELCPP_WITH_OTLP_HTTP_COMPRESSION OFF) otelcpp_set_option(OTELCPP_WITH_BENCHMARK OFF) otelcpp_set_option(OTELCPP_WITH_FUNC_TESTS OFF) @@ -431,8 +443,7 @@ endif() if(OTELCPP_WITH_ELASTICSEARCH OR OTELCPP_WITH_ZIPKIN - OR OTELCPP_WITH_OTLP_HTTP - OR OTELCPP_WITH_OTLP_FILE + OR OTELCPP_WITH_JSON_WRITER_NLOHMANN OR OTELCPP_BUILD_W3CTRACECONTEXT_TEST OR OTELCPP_WITH_ETW) set(USE_NLOHMANN_JSON ON) @@ -609,6 +620,9 @@ message(STATUS "OTELCPP_WITH_OTLP_HTTP: ${OTELCPP_WITH_OTLP_HTTP}") message(STATUS "OTELCPP_WITH_OTLP_FILE: ${OTELCPP_WITH_OTLP_FILE}") message( STATUS "OTELCPP_WITH_HTTP_CLIENT_CURL: ${OTELCPP_WITH_HTTP_CLIENT_CURL}") +message( + STATUS + "OTELCPP_WITH_JSON_WRITER_NLOHMANN: ${OTELCPP_WITH_JSON_WRITER_NLOHMANN}") message(STATUS "OTELCPP_WITH_ZIPKIN: ${OTELCPP_WITH_ZIPKIN}") message(STATUS "OTELCPP_WITH_PROMETHEUS: ${OTELCPP_WITH_PROMETHEUS}") message(STATUS "OTELCPP_WITH_ELASTICSEARCH: ${OTELCPP_WITH_ELASTICSEARCH}") diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index ad9ac5d294..e42eaa69be 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -71,6 +71,71 @@ cc_library( ], ) +cc_library( + name = "otlp_json_writer", + srcs = select({ + ":json_writer_nlohmann_enabled": ["src/otlp_json_writer_nlohmann.cc"], + "//conditions:default": [], + }), + hdrs = [ + "include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h", + "include/opentelemetry/exporters/otlp/otlp_json_writer.h", + "include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h", + ] + select({ + ":json_writer_nlohmann_enabled": [ + "include/opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h", + ], + "//conditions:default": [], + }), + defines = select({ + ":json_writer_nlohmann_enabled": ["ENABLE_JSON_WRITER_NLOHMANN"], + "//conditions:default": [], + }), + strip_include_prefix = "include", + tags = ["otlp"], + deps = [ + "//api", + "//sdk/src/common:base64", + "//sdk/src/common:global_log_handler", + ] + select({ + ":json_writer_nlohmann_enabled": ["@github_nlohmann_json//:json"], + "//conditions:default": [], + }), +) + +config_setting( + name = "json_writer_nlohmann_enabled", + flag_values = { + ":with_json_writer_nlohmann": "true", + }, +) + +bool_flag( + name = "with_json_writer_nlohmann", + build_setting_default = True, +) + +cc_library( + name = "otlp_json_converter", + srcs = [ + "src/otlp_json_converter.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/otlp/otlp_http.h", + "include/opentelemetry/exporters/otlp/otlp_json_converter.h", + "include/opentelemetry/exporters/otlp/protobuf_include_prefix.h", + "include/opentelemetry/exporters/otlp/protobuf_include_suffix.h", + ], + strip_include_prefix = "include", + tags = ["otlp"], + deps = [ + ":otlp_json_writer", + "//api", + "//sdk/src/common:base64", + "@com_google_protobuf//:protobuf", + ], +) + cc_library( name = "otlp_builder_utils", srcs = [ @@ -226,6 +291,8 @@ cc_library( "otlp_http_log", ], deps = [ + ":otlp_json_converter", + ":otlp_json_writer", "//api", "//ext/src/http/client/curl:http_client_curl", "//sdk:headers", @@ -233,7 +300,6 @@ cc_library( "@com_github_opentelemetry_proto//:common_proto_cc", "@com_google_absl//absl/strings", "@com_google_protobuf//:protobuf", - "@github_nlohmann_json//:json", ], ) @@ -307,12 +373,13 @@ cc_library( "otlp_file", ], deps = [ + ":otlp_json_converter", + ":otlp_json_writer", "//api", "//sdk:headers", "//sdk/src/common:base64", "@com_github_opentelemetry_proto//:common_proto_cc", "@com_google_absl//absl/strings", - "@github_nlohmann_json//:json", ], ) @@ -679,6 +746,19 @@ cc_library( ], ) +cc_test( + name = "otlp_json_writer_nlohmann_test", + srcs = ["test/otlp_json_writer_nlohmann_test.cc"], + tags = [ + "otlp", + "test", + ], + deps = [ + ":otlp_json_writer", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "otlp_recordable_test", srcs = ["test/otlp_recordable_test.cc"], @@ -730,7 +810,10 @@ cc_test( cc_test( name = "otlp_file_client_test", - srcs = ["test/otlp_file_client_test.cc"], + srcs = [ + "test/otlp_file_client_test.cc", + "test/otlp_marking_json_writer.h", + ], tags = [ "otlp", "otlp_file", @@ -795,7 +878,10 @@ cc_test( cc_test( name = "otlp_http_exporter_test", - srcs = ["test/otlp_http_exporter_test.cc"], + srcs = [ + "test/otlp_http_exporter_test.cc", + "test/otlp_http_request_body_capture.h", + ], defines = ["ENABLE_OTLP_RETRY_PREVIEW"], tags = [ "otlp", @@ -862,7 +948,10 @@ cc_test( cc_test( name = "otlp_http_log_record_exporter_test", - srcs = ["test/otlp_http_log_record_exporter_test.cc"], + srcs = [ + "test/otlp_http_log_record_exporter_test.cc", + "test/otlp_http_request_body_capture.h", + ], tags = [ "otlp", "otlp_http_log", @@ -996,7 +1085,10 @@ cc_test( cc_test( name = "otlp_http_metric_exporter_test", - srcs = ["test/otlp_http_metric_exporter_test.cc"], + srcs = [ + "test/otlp_http_metric_exporter_test.cc", + "test/otlp_http_request_body_capture.h", + ], tags = [ "otlp", "otlp_http_metric", diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index bb2d0c503f..0f64270362 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -96,6 +96,102 @@ if(OTELCPP_INSTALL) "Common utilities for OTLP exporter builders." "opentelemetry_otlp_common") endif() +# +# opentelemetry_exporter_otlp_json_writer +# + +add_library(opentelemetry_exporter_otlp_json_writer INTERFACE) + +set_target_properties(opentelemetry_exporter_otlp_json_writer + PROPERTIES EXPORT_NAME otlp_json_writer) + +target_include_directories( + opentelemetry_exporter_otlp_json_writer + INTERFACE "$" + "$") + +target_link_libraries(opentelemetry_exporter_otlp_json_writer + INTERFACE opentelemetry_common) + +set(OPENTELEMETRY_OTLP_JSON_WRITER_TARGETS + opentelemetry_exporter_otlp_json_writer) + +if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_library( + opentelemetry_exporter_otlp_json_writer_nlohmann + ${OPENTELEMETRY_OTLP_TARGETS_LIB_TYPE} src/otlp_json_writer_nlohmann.cc) + + set_target_properties(opentelemetry_exporter_otlp_json_writer_nlohmann + PROPERTIES EXPORT_NAME otlp_json_writer_nlohmann) + set_target_version(opentelemetry_exporter_otlp_json_writer_nlohmann) + + target_include_directories( + opentelemetry_exporter_otlp_json_writer_nlohmann + PUBLIC "$" + "$") + + target_link_libraries( + opentelemetry_exporter_otlp_json_writer_nlohmann + PUBLIC opentelemetry_common + PRIVATE opentelemetry_sdk "$") + + target_compile_definitions(opentelemetry_exporter_otlp_json_writer_nlohmann + PUBLIC ENABLE_JSON_WRITER_NLOHMANN) + + target_link_libraries( + opentelemetry_exporter_otlp_json_writer + INTERFACE opentelemetry_exporter_otlp_json_writer_nlohmann) + + list(APPEND OPENTELEMETRY_OTLP_JSON_WRITER_TARGETS + opentelemetry_exporter_otlp_json_writer_nlohmann) +endif() + +if(OTELCPP_INSTALL) + opentelemetry_add_pkgconfig( + exporter_otlp_json_writer "OpenTelemetry OTLP - JSON Writer" + "Token-stream JSON writer interface." "opentelemetry_common") + + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + opentelemetry_add_pkgconfig( + exporter_otlp_json_writer_nlohmann + "OpenTelemetry OTLP - JSON Writer (nlohmann backend)" + "The nlohmann-json backed default implementation of the JsonWriter interface." + "opentelemetry_common") + endif() +endif() + +# +# opentelemetry_exporter_otlp_json_converter +# + +add_library(opentelemetry_exporter_otlp_json_converter + ${OPENTELEMETRY_OTLP_TARGETS_LIB_TYPE} src/otlp_json_converter.cc) + +set_target_properties(opentelemetry_exporter_otlp_json_converter + PROPERTIES EXPORT_NAME otlp_json_converter) +set_target_version(opentelemetry_exporter_otlp_json_converter) + +target_include_directories( + opentelemetry_exporter_otlp_json_converter + PUBLIC "$" + "$") + +target_link_libraries( + opentelemetry_exporter_otlp_json_converter + PUBLIC opentelemetry_sdk opentelemetry_common + opentelemetry_exporter_otlp_json_writer + "$" + PRIVATE "$") + +if(OTELCPP_INSTALL) + opentelemetry_add_pkgconfig( + exporter_otlp_json_converter + "OpenTelemetry OTLP - JSON Converter" + "Shared reflection-based protobuf-to-JSON converter for OTLP/JSON exporters." + "opentelemetry_sdk opentelemetry_common opentelemetry_exporter_otlp_json_writer" + ) +endif() + if(OTELCPP_WITH_OTLP_GRPC) if(NOT DEFINED grpc_lib_type) message( @@ -359,13 +455,12 @@ if(OTELCPP_WITH_OTLP_HTTP) target_link_libraries( opentelemetry_exporter_otlp_http_client PUBLIC opentelemetry_ext "$" - PRIVATE "$" - "$") + PRIVATE "$") target_link_libraries( opentelemetry_exporter_otlp_http_client PUBLIC opentelemetry_http_client - PRIVATE opentelemetry_common) + PRIVATE opentelemetry_common opentelemetry_exporter_otlp_json_converter) target_include_directories( opentelemetry_exporter_otlp_http_client @@ -586,7 +681,7 @@ if(OTELCPP_WITH_OTLP_FILE) PUBLIC opentelemetry_sdk opentelemetry_common "$" PRIVATE "$" - "$") + opentelemetry_exporter_otlp_json_converter) target_include_directories( opentelemetry_exporter_otlp_file_client @@ -798,6 +893,8 @@ otel_add_component( opentelemetry_otlp_recordable opentelemetry_proto opentelemetry_exporter_otlp_builder_utils + opentelemetry_exporter_otlp_json_converter + ${OPENTELEMETRY_OTLP_JSON_WRITER_TARGETS} FILES_DIRECTORY "include/opentelemetry/exporters/otlp" FILES_DESTINATION @@ -919,6 +1016,18 @@ if(OTELCPP_BUILD_TESTING) TEST_PREFIX exporter.otlp. TEST_LIST otlp_metrics_serialization_test) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_json_writer_nlohmann_test + test/otlp_json_writer_nlohmann_test.cc) + target_link_libraries( + otlp_json_writer_nlohmann_test ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} opentelemetry_exporter_otlp_json_writer) + gtest_add_tests( + TARGET otlp_json_writer_nlohmann_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_json_writer_nlohmann_test) + endif() + if(OTELCPP_WITH_OTLP_GRPC) add_executable(otlp_grpc_exporter_test test/otlp_grpc_exporter_test.cc) target_link_libraries( @@ -1004,24 +1113,26 @@ if(OTELCPP_BUILD_TESTING) endif() if(OTELCPP_WITH_OTLP_HTTP) - add_executable(otlp_http_exporter_test test/otlp_http_exporter_test.cc) - target_link_libraries( - otlp_http_exporter_test - ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} - opentelemetry_exporter_otlp_http - opentelemetry_http_client_nosend - nlohmann_json::nlohmann_json - protobuf::libprotobuf - opentelemetry_test_common) - if(WIN32) - target_link_libraries(otlp_http_exporter_test ws2_32) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_http_exporter_test test/otlp_http_exporter_test.cc) + target_link_libraries( + otlp_http_exporter_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_http + opentelemetry_http_client_nosend + nlohmann_json::nlohmann_json + protobuf::libprotobuf + opentelemetry_test_common) + if(WIN32) + target_link_libraries(otlp_http_exporter_test ws2_32) + endif() + gtest_add_tests( + TARGET otlp_http_exporter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_http_exporter_test) endif() - gtest_add_tests( - TARGET otlp_http_exporter_test - TEST_PREFIX exporter.otlp. - TEST_LIST otlp_http_exporter_test) add_executable(otlp_http_exporter_factory_test test/otlp_http_exporter_factory_test.cc) @@ -1033,25 +1144,27 @@ if(OTELCPP_BUILD_TESTING) TEST_PREFIX exporter.otlp. TEST_LIST otlp_http_exporter_factory_test) - add_executable(otlp_http_log_record_exporter_test - test/otlp_http_log_record_exporter_test.cc) - target_link_libraries( - otlp_http_log_record_exporter_test - ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} - opentelemetry_exporter_otlp_http_log - opentelemetry_logs - opentelemetry_http_client_nosend - nlohmann_json::nlohmann_json - opentelemetry_test_common) - if(WIN32) - target_link_libraries(otlp_http_log_record_exporter_test ws2_32) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_http_log_record_exporter_test + test/otlp_http_log_record_exporter_test.cc) + target_link_libraries( + otlp_http_log_record_exporter_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_http_log + opentelemetry_logs + opentelemetry_http_client_nosend + nlohmann_json::nlohmann_json + opentelemetry_test_common) + if(WIN32) + target_link_libraries(otlp_http_log_record_exporter_test ws2_32) + endif() + gtest_add_tests( + TARGET otlp_http_log_record_exporter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_http_log_record_exporter_test) endif() - gtest_add_tests( - TARGET otlp_http_log_record_exporter_test - TEST_PREFIX exporter.otlp. - TEST_LIST otlp_http_log_record_exporter_test) add_executable(otlp_http_log_record_exporter_factory_test test/otlp_http_log_record_exporter_factory_test.cc) @@ -1064,23 +1177,25 @@ if(OTELCPP_BUILD_TESTING) TEST_PREFIX exporter.otlp. TEST_LIST otlp_http_log_record_exporter_factory_test) - add_executable(otlp_http_metric_exporter_test - test/otlp_http_metric_exporter_test.cc) - target_link_libraries( - otlp_http_metric_exporter_test - ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} - opentelemetry_exporter_otlp_http_metric - opentelemetry_metrics - opentelemetry_http_client_nosend - nlohmann_json::nlohmann_json - protobuf::libprotobuf - opentelemetry_test_common) - gtest_add_tests( - TARGET otlp_http_metric_exporter_test - TEST_PREFIX exporter.otlp. - TEST_LIST otlp_http_metric_exporter_test) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_http_metric_exporter_test + test/otlp_http_metric_exporter_test.cc) + target_link_libraries( + otlp_http_metric_exporter_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_http_metric + opentelemetry_metrics + opentelemetry_http_client_nosend + nlohmann_json::nlohmann_json + protobuf::libprotobuf + opentelemetry_test_common) + gtest_add_tests( + TARGET otlp_http_metric_exporter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_http_metric_exporter_test) + endif() add_executable(otlp_http_metric_exporter_factory_test test/otlp_http_metric_exporter_factory_test.cc) @@ -1125,36 +1240,89 @@ if(OTELCPP_BUILD_TESTING) TARGET otlp_http_log_record_exporter_custom_client_test TEST_PREFIX exporter.otlp. TEST_LIST otlp_http_log_record_exporter_custom_client_test) + + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_http_exporter_custom_json_writer_test + test/otlp_http_exporter_custom_json_writer_test.cc) + target_link_libraries( + otlp_http_exporter_custom_json_writer_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_http + opentelemetry_http_client_nosend + opentelemetry_exporter_otlp_json_writer) + gtest_add_tests( + TARGET otlp_http_exporter_custom_json_writer_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_http_exporter_custom_json_writer_test) + + add_executable(otlp_http_metric_exporter_custom_json_writer_test + test/otlp_http_metric_exporter_custom_json_writer_test.cc) + target_link_libraries( + otlp_http_metric_exporter_custom_json_writer_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + opentelemetry_exporter_otlp_http_metric + opentelemetry_metrics + opentelemetry_http_client_nosend + opentelemetry_exporter_otlp_json_writer) + gtest_add_tests( + TARGET otlp_http_metric_exporter_custom_json_writer_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_http_metric_exporter_custom_json_writer_test) + + add_executable( + otlp_http_log_record_exporter_custom_json_writer_test + test/otlp_http_log_record_exporter_custom_json_writer_test.cc) + target_link_libraries( + otlp_http_log_record_exporter_custom_json_writer_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + opentelemetry_exporter_otlp_http_log + opentelemetry_logs + opentelemetry_http_client_nosend + opentelemetry_exporter_otlp_json_writer) + gtest_add_tests( + TARGET otlp_http_log_record_exporter_custom_json_writer_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_http_log_record_exporter_custom_json_writer_test) + endif() endif() if(OTELCPP_WITH_OTLP_FILE) - add_executable(otlp_file_client_test test/otlp_file_client_test.cc) - target_link_libraries( - otlp_file_client_test - ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} - opentelemetry_exporter_otlp_file - opentelemetry_otlp_recordable - nlohmann_json::nlohmann_json) - gtest_add_tests( - TARGET otlp_file_client_test - TEST_PREFIX exporter.otlp. - TEST_LIST otlp_file_client_test) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_file_client_test test/otlp_file_client_test.cc) + target_link_libraries( + otlp_file_client_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_file + opentelemetry_otlp_recordable + opentelemetry_exporter_otlp_json_writer + nlohmann_json::nlohmann_json) + gtest_add_tests( + TARGET otlp_file_client_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_file_client_test) + endif() - add_executable(otlp_file_exporter_test test/otlp_file_exporter_test.cc) - target_link_libraries( - otlp_file_exporter_test - ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} - opentelemetry_exporter_otlp_file - nlohmann_json::nlohmann_json - protobuf::libprotobuf) - gtest_add_tests( - TARGET otlp_file_exporter_test - TEST_PREFIX exporter.otlp. - TEST_LIST otlp_file_exporter_test) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_file_exporter_test test/otlp_file_exporter_test.cc) + target_link_libraries( + otlp_file_exporter_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_file + nlohmann_json::nlohmann_json + protobuf::libprotobuf) + gtest_add_tests( + TARGET otlp_file_exporter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_file_exporter_test) + endif() add_executable(otlp_file_exporter_factory_test test/otlp_file_exporter_factory_test.cc) @@ -1166,20 +1334,22 @@ if(OTELCPP_BUILD_TESTING) TEST_PREFIX exporter.otlp. TEST_LIST otlp_file_exporter_factory_test) - add_executable(otlp_file_log_record_exporter_test - test/otlp_file_log_record_exporter_test.cc) - target_link_libraries( - otlp_file_log_record_exporter_test - ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} - opentelemetry_exporter_otlp_file_log - opentelemetry_logs - nlohmann_json::nlohmann_json) - gtest_add_tests( - TARGET otlp_file_log_record_exporter_test - TEST_PREFIX exporter.otlp. - TEST_LIST otlp_file_log_record_exporter_test) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_file_log_record_exporter_test + test/otlp_file_log_record_exporter_test.cc) + target_link_libraries( + otlp_file_log_record_exporter_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_file_log + opentelemetry_logs + nlohmann_json::nlohmann_json) + gtest_add_tests( + TARGET otlp_file_log_record_exporter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_file_log_record_exporter_test) + endif() add_executable(otlp_file_log_record_exporter_factory_test test/otlp_file_log_record_exporter_factory_test.cc) @@ -1192,21 +1362,23 @@ if(OTELCPP_BUILD_TESTING) TEST_PREFIX exporter.otlp. TEST_LIST otlp_file_log_record_exporter_factory_test) - add_executable(otlp_file_metric_exporter_test - test/otlp_file_metric_exporter_test.cc) - target_link_libraries( - otlp_file_metric_exporter_test - ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} - opentelemetry_exporter_otlp_file_metric - opentelemetry_metrics - nlohmann_json::nlohmann_json - protobuf::libprotobuf) - gtest_add_tests( - TARGET otlp_file_metric_exporter_test - TEST_PREFIX exporter.otlp. - TEST_LIST otlp_file_metric_exporter_test) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_file_metric_exporter_test + test/otlp_file_metric_exporter_test.cc) + target_link_libraries( + otlp_file_metric_exporter_test + ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} + opentelemetry_exporter_otlp_file_metric + opentelemetry_metrics + nlohmann_json::nlohmann_json + protobuf::libprotobuf) + gtest_add_tests( + TARGET otlp_file_metric_exporter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_file_metric_exporter_test) + endif() add_executable(otlp_file_metric_exporter_factory_test test/otlp_file_metric_exporter_factory_test.cc) diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h b/exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h new file mode 100644 index 0000000000..54438a4269 --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h @@ -0,0 +1,40 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" +#ifdef ENABLE_JSON_WRITER_NLOHMANN +# include "opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h" +#else +# include "opentelemetry/sdk/common/global_log_handler.h" +#endif + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ +namespace detail +{ + +inline std::shared_ptr GetDefaultJsonWriterFactory() +{ +#ifdef ENABLE_JSON_WRITER_NLOHMANN + static auto instance = std::make_shared(); + return instance; +#else + OTEL_INTERNAL_LOG_ERROR( + "No default JSON writer backend is compiled in. " + "Use the JsonWriterFactory constructor or factory overloads, " + "or enable the nlohmann backend (OTELCPP_WITH_JSON_WRITER_NLOHMANN=ON)."); + std::terminate(); +#endif +} + +} // namespace detail +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client.h index 55385b8d15..c46a53fad5 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client.h @@ -8,6 +8,7 @@ #include "opentelemetry/exporters/otlp/otlp_file_client_options.h" #include "opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/version.h" @@ -89,6 +90,9 @@ class OtlpFileClient // The runtime options associated with this file client. OtlpFileClientRuntimeOptions runtime_options_; + // Resolved from runtime_options_.json_writer_factory, or the default backend. + std::shared_ptr json_writer_factory_; + opentelemetry::nostd::shared_ptr backend_; }; } // namespace otlp diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h index b88ee3019d..0d0bbb0ed5 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h @@ -5,6 +5,7 @@ #include +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/sdk/common/thread_instrumentation.h" #include "opentelemetry/version.h" @@ -28,6 +29,8 @@ struct OtlpFileClientRuntimeOptions std::shared_ptr thread_instrumentation = std::shared_ptr(nullptr); + + std::shared_ptr json_writer_factory; }; } // namespace otlp diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h index da28fbfdec..70af8486bd 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h @@ -16,6 +16,7 @@ #include "opentelemetry/exporters/otlp/otlp_environment.h" #include "opentelemetry/exporters/otlp/otlp_http.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/ext/http/client/http_client.h" #include "opentelemetry/ext/http/client/http_client_factory.h" #include "opentelemetry/nostd/string_view.h" @@ -92,6 +93,8 @@ struct OtlpHttpClientOptions std::shared_ptr thread_instrumentation = std::shared_ptr(nullptr); + std::shared_ptr json_writer_factory; + inline OtlpHttpClientOptions( nostd::string_view input_url, bool input_ssl_insecure_skip_verify, @@ -295,6 +298,9 @@ class OtlpHttpClient // Object that stores the HTTP sessions that have been created std::shared_ptr http_client_; + // Resolved from options_.json_writer_factory, or the default backend. + std::shared_ptr json_writer_factory_; + // Cached parsed URI std::string http_uri_; diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter_runtime_options.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter_runtime_options.h index b5e11b377e..28b82fab7b 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter_runtime_options.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter_runtime_options.h @@ -5,6 +5,7 @@ #include +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/sdk/common/thread_instrumentation.h" #include "opentelemetry/version.h" @@ -23,6 +24,8 @@ struct OPENTELEMETRY_EXPORT OtlpHttpExporterRuntimeOptions std::shared_ptr thread_instrumentation = std::shared_ptr(nullptr); + + std::shared_ptr json_writer_factory; }; } // namespace otlp diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_record_exporter_runtime_options.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_record_exporter_runtime_options.h index 09213731af..f47c22e9f8 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_record_exporter_runtime_options.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_record_exporter_runtime_options.h @@ -5,6 +5,7 @@ #include +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/sdk/common/thread_instrumentation.h" #include "opentelemetry/version.h" @@ -23,6 +24,8 @@ struct OPENTELEMETRY_EXPORT OtlpHttpLogRecordExporterRuntimeOptions std::shared_ptr thread_instrumentation = std::shared_ptr(nullptr); + + std::shared_ptr json_writer_factory; }; } // namespace otlp diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_metric_exporter_runtime_options.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_metric_exporter_runtime_options.h index a26d54b9ff..0e231a7325 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_metric_exporter_runtime_options.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_metric_exporter_runtime_options.h @@ -5,6 +5,7 @@ #include +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/sdk/common/thread_instrumentation.h" #include "opentelemetry/version.h" @@ -23,6 +24,8 @@ struct OPENTELEMETRY_EXPORT OtlpHttpMetricExporterRuntimeOptions std::shared_ptr thread_instrumentation = std::shared_ptr(nullptr); + + std::shared_ptr json_writer_factory; }; } // namespace otlp diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h new file mode 100644 index 0000000000..d38de7f81f --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h @@ -0,0 +1,46 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "opentelemetry/exporters/otlp/otlp_http.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/version.h" + +// clang-format off +#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep +#include +#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep +// clang-format on + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +/** + * Options that parameterize how a protobuf message is mapped to OTLP/JSON. + */ +struct JsonConverterOptions +{ + // If true, use the JSON name of a protobuf field to set the key of the JSON + // object. If false, use the camel-case field name as generated from the + // proto field name. + bool use_json_name = false; + + // How `bytes` fields are mapped to JSON. + JsonBytesMappingKind json_bytes_mapping = JsonBytesMappingKind::kHexId; +}; + +/** + * Converts a protobuf message to OTLP/JSON using reflection, emitting through + * `writer`. + */ +void ConvertGenericMessageToJson(JsonWriter &writer, + const google::protobuf::Message &message, + const JsonConverterOptions &options); + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h new file mode 100644 index 0000000000..c2596ceaff --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h @@ -0,0 +1,53 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include + +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +class JsonWriter +{ +public: + JsonWriter() = default; + JsonWriter(const JsonWriter &) = delete; + JsonWriter(JsonWriter &&) = delete; + JsonWriter &operator=(const JsonWriter &) = delete; + JsonWriter &operator=(JsonWriter &&) = delete; + virtual ~JsonWriter() = default; + + virtual void BeginObject() noexcept = 0; + virtual void EndObject() noexcept = 0; + virtual void BeginArray() noexcept = 0; + virtual void EndArray() noexcept = 0; + + virtual void Key(nostd::string_view key) noexcept = 0; + + virtual void WriteNull() noexcept = 0; + virtual void WriteString(nostd::string_view value) noexcept = 0; + virtual void WriteInt32(std::int32_t value) noexcept = 0; + virtual void WriteInt64(std::int64_t value) noexcept = 0; + virtual void WriteUInt32(std::uint32_t value) noexcept = 0; + virtual void WriteUInt64(std::uint64_t value) noexcept = 0; + virtual void WriteDouble(double value) noexcept = 0; + virtual void WriteBool(bool value) noexcept = 0; + virtual void WriteBytes(const std::uint8_t *data, std::size_t size) noexcept = 0; + + virtual bool ok() const noexcept = 0; + + virtual std::string ToString() noexcept = 0; +}; + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h new file mode 100644 index 0000000000..e76d40729b --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h @@ -0,0 +1,32 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +class OPENTELEMETRY_EXPORT JsonWriterFactory +{ +public: + JsonWriterFactory() = default; + JsonWriterFactory(const JsonWriterFactory &) = delete; + JsonWriterFactory(JsonWriterFactory &&) = delete; + JsonWriterFactory &operator=(const JsonWriterFactory &) = delete; + JsonWriterFactory &operator=(JsonWriterFactory &&) = delete; + virtual ~JsonWriterFactory() = default; + + virtual std::unique_ptr Create() = 0; +}; + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h new file mode 100644 index 0000000000..c641f9e80c --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include + +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +class OPENTELEMETRY_EXPORT JsonWriterFactoryNlohmann : public JsonWriterFactory +{ +public: + std::unique_ptr Create() override; +}; + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/src/otlp_file_client.cc b/exporters/otlp/src/otlp_file_client.cc index 2bc24b301c..86264d233a 100644 --- a/exporters/otlp/src/otlp_file_client.cc +++ b/exporters/otlp/src/otlp_file_client.cc @@ -1,8 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#include - #include #include #include @@ -13,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -103,20 +102,22 @@ # define OTLP_FILE_OPEN(f, path, mode) f = std::fopen(path, mode) #endif +#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_client.h" #include "opentelemetry/exporters/otlp/otlp_file_client_options.h" #include "opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h" +#include "opentelemetry/exporters/otlp/otlp_json_converter.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/nostd/variant.h" -#include "opentelemetry/sdk/common/base64.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/version.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep -#include "google/protobuf/descriptor.h" #include "google/protobuf/message.h" #include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep // clang-format on @@ -714,266 +715,6 @@ class OPENTELEMETRY_LOCAL_SYMBOL FileSystemUtil #endif }; -static inline char HexEncode(unsigned char byte) -{ -#if defined(HAVE_GSL) - Expects(byte <= 16); -#else - assert(byte <= 16); -#endif - if (byte >= 10) - { - return static_cast(byte - 10 + 'a'); - } - else - { - return static_cast(byte + '0'); - } -} - -static std::string HexEncode(const std::string &bytes) -{ - std::string ret; - ret.reserve(bytes.size() * 2); - for (std::string::size_type i = 0; i < bytes.size(); ++i) - { - unsigned char byte = static_cast(bytes[i]); - ret.push_back(HexEncode(byte >> 4)); - ret.push_back(HexEncode(byte & 0x0f)); - } - return ret; -} - -static std::string BytesMapping(const std::string &bytes, - const google::protobuf::FieldDescriptor *field_descriptor) -{ - if (field_descriptor->lowercase_name() == "trace_id" || - field_descriptor->lowercase_name() == "span_id" || - field_descriptor->lowercase_name() == "parent_span_id") - { - return HexEncode(bytes); - } - else - { - return opentelemetry::sdk::common::Base64Escape(bytes); - } -} - -static void ConvertGenericFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor); - -static void ConvertListFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor); - -// NOLINTBEGIN(misc-no-recursion) -static void ConvertGenericMessageToJson(nlohmann::json &value, - const google::protobuf::Message &message) -{ - std::vector fields_with_data; - message.GetReflection()->ListFields(message, &fields_with_data); - for (std::size_t i = 0; i < fields_with_data.size(); ++i) - { - const google::protobuf::FieldDescriptor *field_descriptor = fields_with_data[i]; - nlohmann::json &child_value = value[field_descriptor->camelcase_name()]; - if (field_descriptor->is_repeated()) - { - ConvertListFieldToJson(child_value, message, field_descriptor); - } - else - { - ConvertGenericFieldToJson(child_value, message, field_descriptor); - } - } -} - -void ConvertGenericFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor) -{ - switch (field_descriptor->cpp_type()) - { - case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { - value = message.GetReflection()->GetInt32(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as - // decimal strings, and either numbers or strings are accepted when decoding. - value = std::to_string(message.GetReflection()->GetInt64(message, field_descriptor)); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { - value = message.GetReflection()->GetUInt32(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as - // decimal strings, and either numbers or strings are accepted when decoding. - value = std::to_string(message.GetReflection()->GetUInt64(message, field_descriptor)); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { - std::string empty; - if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) - { - value = BytesMapping( - message.GetReflection()->GetStringReference(message, field_descriptor, &empty), - field_descriptor); - } - else - { - value = message.GetReflection()->GetStringReference(message, field_descriptor, &empty); - } - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { - ConvertGenericMessageToJson( - value, message.GetReflection()->GetMessage(message, field_descriptor, nullptr)); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { - value = message.GetReflection()->GetDouble(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { - value = message.GetReflection()->GetFloat(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { - value = message.GetReflection()->GetBool(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { - value = message.GetReflection()->GetEnumValue(message, field_descriptor); - break; - } - default: { - break; - } - } -} - -void ConvertListFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor) -{ - auto field_size = message.GetReflection()->FieldSize(message, field_descriptor); - - switch (field_descriptor->cpp_type()) - { - case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedInt32(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { - for (int i = 0; i < field_size; ++i) - { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded - // as decimal strings, and either numbers or strings are accepted when decoding. - value.push_back(std::to_string( - message.GetReflection()->GetRepeatedInt64(message, field_descriptor, i))); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedUInt32(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { - for (int i = 0; i < field_size; ++i) - { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded - // as decimal strings, and either numbers or strings are accepted when decoding. - value.push_back(std::to_string( - message.GetReflection()->GetRepeatedUInt64(message, field_descriptor, i))); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { - std::string empty; - if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) - { - for (int i = 0; i < field_size; ++i) - { - value.push_back(BytesMapping(message.GetReflection()->GetRepeatedStringReference( - message, field_descriptor, i, &empty), - field_descriptor)); - } - } - else - { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedStringReference( - message, field_descriptor, i, &empty)); - } - } - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { - for (int i = 0; i < field_size; ++i) - { - nlohmann::json sub_value; - ConvertGenericMessageToJson( - sub_value, message.GetReflection()->GetRepeatedMessage(message, field_descriptor, i)); - value.push_back(std::move(sub_value)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedDouble(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedFloat(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedBool(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { - for (int i = 0; i < field_size; ++i) - { - value.push_back( - message.GetReflection()->GetRepeatedEnumValue(message, field_descriptor, i)); - } - break; - } - default: { - break; - } - } -} - -// NOLINTEND(misc-no-recursion) suppressing for performance as if implemented with stack needs -// Dynamic memory allocation - class OPENTELEMETRY_LOCAL_SYMBOL OtlpFileSystemBackend : public OtlpFileAppender { public: @@ -1636,7 +1377,11 @@ class OPENTELEMETRY_LOCAL_SYMBOL OtlpFileOstreamBackend : public OtlpFileAppende OtlpFileClient::OtlpFileClient(OtlpFileClientOptions &&options, OtlpFileClientRuntimeOptions &&runtime_options) - : options_{std::move(options)}, runtime_options_{std::move(runtime_options)} + : options_{std::move(options)}, + runtime_options_{std::move(runtime_options)}, + json_writer_factory_{runtime_options_.json_writer_factory + ? runtime_options_.json_writer_factory + : detail::GetDefaultJsonWriterFactory()} { if (nostd::holds_alternative(options_.backend_options)) { @@ -1674,12 +1419,14 @@ opentelemetry::sdk::common::ExportResult OtlpFileClient::Export( return ::opentelemetry::sdk::common::ExportResult::kFailure; } - nlohmann::json json_request; - // Convert from proto into json object - ConvertGenericMessageToJson(json_request, message); + std::unique_ptr json_writer = json_writer_factory_->Create(); + ConvertGenericMessageToJson(*json_writer, message, JsonConverterOptions{}); + if (!json_writer->ok()) + { + return ::opentelemetry::sdk::common::ExportResult::kFailure; + } - std::string post_body_json = - json_request.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); + std::string post_body_json = json_writer->ToString(); if (options_.console_debug) { OTEL_INTERNAL_LOG_DEBUG("[OTLP FILE Client] Write body(Json)" << post_body_json); diff --git a/exporters/otlp/src/otlp_http_client.cc b/exporters/otlp/src/otlp_http_client.cc index ee902e930a..dcedee7d56 100644 --- a/exporters/otlp/src/otlp_http_client.cc +++ b/exporters/otlp/src/otlp_http_client.cc @@ -23,10 +23,13 @@ # include #endif -#include "nlohmann/json.hpp" #include "opentelemetry/common/timestamp.h" +#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" #include "opentelemetry/exporters/otlp/otlp_http.h" #include "opentelemetry/exporters/otlp/otlp_http_client.h" +#include "opentelemetry/exporters/otlp/otlp_json_converter.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/ext/http/client/detail/default_factory.h" #include "opentelemetry/ext/http/client/http_client.h" #include "opentelemetry/ext/http/client/http_client_factory.h" @@ -34,7 +37,6 @@ #include "opentelemetry/nostd/function_ref.h" #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/nostd/variant.h" -#include "opentelemetry/sdk/common/base64.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/version.h" @@ -43,7 +45,6 @@ #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep // clang-format on #include -#include #include #include #include @@ -422,98 +423,6 @@ class ResponseHandler : public http_client::EventHandler bool console_debug_ = false; }; -static inline char HexEncode(unsigned char byte) -{ -#if defined(HAVE_GSL) - Expects(byte <= 16); -#else - assert(byte <= 16); -#endif - if (byte >= 10) - { - return static_cast(byte - 10 + 'a'); - } - else - { - return static_cast(byte + '0'); - } -} - -static std::string HexEncode(const std::string &bytes) -{ - std::string ret; - ret.reserve(bytes.size() * 2); - for (std::string::size_type i = 0; i < bytes.size(); ++i) - { - unsigned char byte = static_cast(bytes[i]); - ret.push_back(HexEncode(byte >> 4)); - ret.push_back(HexEncode(byte & 0x0f)); - } - return ret; -} - -static std::string BytesMapping(const std::string &bytes, - const google::protobuf::FieldDescriptor *field_descriptor, - JsonBytesMappingKind kind) -{ - switch (kind) - { - case JsonBytesMappingKind::kHexId: { - if (field_descriptor->lowercase_name() == "trace_id" || - field_descriptor->lowercase_name() == "span_id" || - field_descriptor->lowercase_name() == "parent_span_id") - { - return HexEncode(bytes); - } - else - { - return opentelemetry::sdk::common::Base64Escape(bytes); - } - } - case JsonBytesMappingKind::kBase64: { - // Base64 is the default bytes mapping of protobuf - return opentelemetry::sdk::common::Base64Escape(bytes); - } - case JsonBytesMappingKind::kHex: - return HexEncode(bytes); - default: - return bytes; - } -} - -static void ConvertGenericFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor, - const OtlpHttpClientOptions &options); - -static void ConvertListFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor, - const OtlpHttpClientOptions &options); - -// NOLINTBEGIN(misc-no-recursion) -static void ConvertGenericMessageToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const OtlpHttpClientOptions &options) -{ - std::vector fields_with_data; - message.GetReflection()->ListFields(message, &fields_with_data); - for (std::size_t i = 0; i < fields_with_data.size(); ++i) - { - const google::protobuf::FieldDescriptor *field_descriptor = fields_with_data[i]; - nlohmann::json &child_value = options.use_json_name ? value[field_descriptor->json_name()] - : value[field_descriptor->camelcase_name()]; - if (field_descriptor->is_repeated()) - { - ConvertListFieldToJson(child_value, message, field_descriptor, options); - } - else - { - ConvertGenericFieldToJson(child_value, message, field_descriptor, options); - } - } -} - bool SerializeToHttpBody(http_client::Body &output, const google::protobuf::Message &message) { auto body_size = message.ByteSizeLong(); @@ -526,195 +435,6 @@ bool SerializeToHttpBody(http_client::Body &output, const google::protobuf::Mess return true; } -void ConvertGenericFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor, - const OtlpHttpClientOptions &options) -{ - switch (field_descriptor->cpp_type()) - { - case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { - value = message.GetReflection()->GetInt32(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as - // decimal strings, and either numbers or strings are accepted when decoding. - value = std::to_string(message.GetReflection()->GetInt64(message, field_descriptor)); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { - value = message.GetReflection()->GetUInt32(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as - // decimal strings, and either numbers or strings are accepted when decoding. - value = std::to_string(message.GetReflection()->GetUInt64(message, field_descriptor)); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { - std::string empty; - if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) - { - value = BytesMapping( - message.GetReflection()->GetStringReference(message, field_descriptor, &empty), - field_descriptor, options.json_bytes_mapping); - } - else - { - value = message.GetReflection()->GetStringReference(message, field_descriptor, &empty); - } - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { - ConvertGenericMessageToJson( - value, message.GetReflection()->GetMessage(message, field_descriptor, nullptr), options); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { - value = message.GetReflection()->GetDouble(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { - value = message.GetReflection()->GetFloat(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { - value = message.GetReflection()->GetBool(message, field_descriptor); - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { - value = message.GetReflection()->GetEnumValue(message, field_descriptor); - break; - } - default: { - break; - } - } -} - -void ConvertListFieldToJson(nlohmann::json &value, - const google::protobuf::Message &message, - const google::protobuf::FieldDescriptor *field_descriptor, - const OtlpHttpClientOptions &options) -{ - auto field_size = message.GetReflection()->FieldSize(message, field_descriptor); - - switch (field_descriptor->cpp_type()) - { - case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedInt32(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { - for (int i = 0; i < field_size; ++i) - { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded - // as decimal strings, and either numbers or strings are accepted when decoding. - value.push_back(std::to_string( - message.GetReflection()->GetRepeatedInt64(message, field_descriptor, i))); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedUInt32(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { - for (int i = 0; i < field_size; ++i) - { - // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded - // as decimal strings, and either numbers or strings are accepted when decoding. - value.push_back(std::to_string( - message.GetReflection()->GetRepeatedUInt64(message, field_descriptor, i))); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { - std::string empty; - if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) - { - for (int i = 0; i < field_size; ++i) - { - value.push_back(BytesMapping(message.GetReflection()->GetRepeatedStringReference( - message, field_descriptor, i, &empty), - field_descriptor, options.json_bytes_mapping)); - } - } - else - { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedStringReference( - message, field_descriptor, i, &empty)); - } - } - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { - for (int i = 0; i < field_size; ++i) - { - nlohmann::json sub_value; - ConvertGenericMessageToJson( - sub_value, message.GetReflection()->GetRepeatedMessage(message, field_descriptor, i), - options); - value.push_back(std::move(sub_value)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedDouble(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedFloat(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { - for (int i = 0; i < field_size; ++i) - { - value.push_back(message.GetReflection()->GetRepeatedBool(message, field_descriptor, i)); - } - - break; - } - case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { - for (int i = 0; i < field_size; ++i) - { - value.push_back( - message.GetReflection()->GetRepeatedEnumValue(message, field_descriptor, i)); - } - break; - } - default: { - break; - } - } -} - -// NOLINTEND(misc-no-recursion) suppressing for performance, if implemented iterative process needs -// Dynamic memory allocation - } // namespace OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options) @@ -731,6 +451,8 @@ OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options, : is_shutdown_(false), options_(std::move(options)), http_client_(std::move(http_client)), + json_writer_factory_(options_.json_writer_factory ? options_.json_writer_factory + : detail::GetDefaultJsonWriterFactory()), start_session_counter_(0), finished_session_counter_(0) { @@ -1001,13 +723,19 @@ OtlpHttpClient::createSession( } else { - nlohmann::json json_request; + std::unique_ptr json_writer = json_writer_factory_->Create(); + ConvertGenericMessageToJson( + *json_writer, message, + JsonConverterOptions{options_.use_json_name, options_.json_bytes_mapping}); - // Convert from proto into json object - ConvertGenericMessageToJson(json_request, message, options_); + if (!json_writer->ok()) + { + const auto result = opentelemetry::sdk::common::ExportResult::kFailure; + result_callback(result, response); + return result; + } - std::string post_body_json = - json_request.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); + std::string post_body_json = json_writer->ToString(); if (options_.console_debug) { OTEL_INTERNAL_LOG_DEBUG("[OTLP HTTP Client] Request body(Json)" << post_body_json); diff --git a/exporters/otlp/src/otlp_http_exporter.cc b/exporters/otlp/src/otlp_http_exporter.cc index 1155b683bd..34a7d89c3f 100644 --- a/exporters/otlp/src/otlp_http_exporter.cc +++ b/exporters/otlp/src/otlp_http_exporter.cc @@ -45,79 +45,45 @@ namespace exporter namespace otlp { +namespace +{ + +OtlpHttpClientOptions MakeClientOptions(const OtlpHttpExporterOptions &options, + const OtlpHttpExporterRuntimeOptions &runtime_options) +{ + OtlpHttpClientOptions client_options( + options.url, options.ssl_insecure_skip_verify, options.ssl_ca_cert_path, + options.ssl_ca_cert_string, options.ssl_client_key_path, options.ssl_client_key_string, + options.ssl_client_cert_path, options.ssl_client_cert_string, options.ssl_min_tls, + options.ssl_max_tls, options.ssl_cipher, options.ssl_cipher_suite, options.content_type, + options.json_bytes_mapping, options.compression, options.use_json_name, options.console_debug, + options.timeout, options.http_headers, options.retry_policy_max_attempts, + options.retry_policy_initial_backoff, options.retry_policy_max_backoff, + options.retry_policy_backoff_multiplier, runtime_options.thread_instrumentation +#ifdef ENABLE_ASYNC_EXPORT + , + options.max_concurrent_requests, options.max_requests_per_connection +#endif + ); + client_options.json_writer_factory = runtime_options.json_writer_factory; + return client_options; +} + +} // namespace + OtlpHttpExporter::OtlpHttpExporter() : OtlpHttpExporter(OtlpHttpExporterOptions()) {} OtlpHttpExporter::OtlpHttpExporter(const OtlpHttpExporterOptions &options) : options_(options), runtime_options_(), - http_client_(std::make_unique(OtlpHttpClientOptions( - options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_))) {} OtlpHttpExporter::OtlpHttpExporter(const OtlpHttpExporterOptions &options, const OtlpHttpExporterRuntimeOptions &runtime_options) : options_(options), runtime_options_(runtime_options), - http_client_(std::make_unique(OtlpHttpClientOptions( - options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_))) {} OtlpHttpExporter::OtlpHttpExporter( @@ -125,38 +91,8 @@ OtlpHttpExporter::OtlpHttpExporter( const std::shared_ptr &factory) : options_(options), runtime_options_(), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - factory)) + http_client_( + std::make_unique(MakeClientOptions(options, runtime_options_), factory)) {} OtlpHttpExporter::OtlpHttpExporter( @@ -165,76 +101,16 @@ OtlpHttpExporter::OtlpHttpExporter( const std::shared_ptr &factory) : options_(options), runtime_options_(runtime_options), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - factory)) + http_client_( + std::make_unique(MakeClientOptions(options, runtime_options_), factory)) {} OtlpHttpExporter::OtlpHttpExporter(const OtlpHttpExporterOptions &options, std::shared_ptr http_client) : options_(options), runtime_options_(), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - std::move(http_client))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_), + std::move(http_client))) {} OtlpHttpExporter::OtlpHttpExporter(const OtlpHttpExporterOptions &options, @@ -242,38 +118,8 @@ OtlpHttpExporter::OtlpHttpExporter(const OtlpHttpExporterOptions &options, std::shared_ptr http_client) : options_(options), runtime_options_(runtime_options), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - std::move(http_client))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_), + std::move(http_client))) {} OtlpHttpExporter::OtlpHttpExporter(std::unique_ptr http_client) diff --git a/exporters/otlp/src/otlp_http_log_record_exporter.cc b/exporters/otlp/src/otlp_http_log_record_exporter.cc index 10d6af9653..6a646893de 100644 --- a/exporters/otlp/src/otlp_http_log_record_exporter.cc +++ b/exporters/otlp/src/otlp_http_log_record_exporter.cc @@ -45,6 +45,33 @@ namespace exporter namespace otlp { +namespace +{ + +OtlpHttpClientOptions MakeClientOptions( + const OtlpHttpLogRecordExporterOptions &options, + const OtlpHttpLogRecordExporterRuntimeOptions &runtime_options) +{ + OtlpHttpClientOptions client_options( + options.url, options.ssl_insecure_skip_verify, options.ssl_ca_cert_path, + options.ssl_ca_cert_string, options.ssl_client_key_path, options.ssl_client_key_string, + options.ssl_client_cert_path, options.ssl_client_cert_string, options.ssl_min_tls, + options.ssl_max_tls, options.ssl_cipher, options.ssl_cipher_suite, options.content_type, + options.json_bytes_mapping, options.compression, options.use_json_name, options.console_debug, + options.timeout, options.http_headers, options.retry_policy_max_attempts, + options.retry_policy_initial_backoff, options.retry_policy_max_backoff, + options.retry_policy_backoff_multiplier, runtime_options.thread_instrumentation +#ifdef ENABLE_ASYNC_EXPORT + , + options.max_concurrent_requests, options.max_requests_per_connection +#endif + ); + client_options.json_writer_factory = runtime_options.json_writer_factory; + return client_options; +} + +} // namespace + OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter() : OtlpHttpLogRecordExporter(OtlpHttpLogRecordExporterOptions()) {} @@ -53,37 +80,7 @@ OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( const OtlpHttpLogRecordExporterOptions &options) : options_(options), runtime_options_(), - http_client_(std::make_unique(OtlpHttpClientOptions( - options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_))) {} OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( @@ -91,37 +88,7 @@ OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( const OtlpHttpLogRecordExporterRuntimeOptions &runtime_options) : options_(options), runtime_options_(runtime_options), - http_client_(std::make_unique(OtlpHttpClientOptions( - options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_))) {} OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( @@ -129,38 +96,8 @@ OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( const std::shared_ptr &factory) : options_(options), runtime_options_(), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - factory)) + http_client_( + std::make_unique(MakeClientOptions(options, runtime_options_), factory)) {} OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( @@ -169,38 +106,8 @@ OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( const std::shared_ptr &factory) : options_(options), runtime_options_(runtime_options), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - factory)) + http_client_( + std::make_unique(MakeClientOptions(options, runtime_options_), factory)) {} OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( @@ -208,38 +115,8 @@ OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( std::shared_ptr http_client) : options_(options), runtime_options_(), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - std::move(http_client))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_), + std::move(http_client))) {} OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( @@ -248,38 +125,8 @@ OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter( std::shared_ptr http_client) : options_(options), runtime_options_(runtime_options), - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - std::move(http_client))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_), + std::move(http_client))) {} OtlpHttpLogRecordExporter::OtlpHttpLogRecordExporter(std::unique_ptr http_client) diff --git a/exporters/otlp/src/otlp_http_metric_exporter.cc b/exporters/otlp/src/otlp_http_metric_exporter.cc index 782fc20616..a36c1bc61f 100644 --- a/exporters/otlp/src/otlp_http_metric_exporter.cc +++ b/exporters/otlp/src/otlp_http_metric_exporter.cc @@ -45,6 +45,32 @@ namespace exporter namespace otlp { +namespace +{ + +OtlpHttpClientOptions MakeClientOptions(const OtlpHttpMetricExporterOptions &options, + const OtlpHttpMetricExporterRuntimeOptions &runtime_options) +{ + OtlpHttpClientOptions client_options( + options.url, options.ssl_insecure_skip_verify, options.ssl_ca_cert_path, + options.ssl_ca_cert_string, options.ssl_client_key_path, options.ssl_client_key_string, + options.ssl_client_cert_path, options.ssl_client_cert_string, options.ssl_min_tls, + options.ssl_max_tls, options.ssl_cipher, options.ssl_cipher_suite, options.content_type, + options.json_bytes_mapping, options.compression, options.use_json_name, options.console_debug, + options.timeout, options.http_headers, options.retry_policy_max_attempts, + options.retry_policy_initial_backoff, options.retry_policy_max_backoff, + options.retry_policy_backoff_multiplier, runtime_options.thread_instrumentation +#ifdef ENABLE_ASYNC_EXPORT + , + options.max_concurrent_requests, options.max_requests_per_connection +#endif + ); + client_options.json_writer_factory = runtime_options.json_writer_factory; + return client_options; +} + +} // namespace + OtlpHttpMetricExporter::OtlpHttpMetricExporter() : OtlpHttpMetricExporter(OtlpHttpMetricExporterOptions()) {} @@ -54,37 +80,7 @@ OtlpHttpMetricExporter::OtlpHttpMetricExporter(const OtlpHttpMetricExporterOptio runtime_options_(), aggregation_temporality_selector_{ OtlpMetricUtils::ChooseTemporalitySelector(options_.aggregation_temporality)}, - http_client_(std::make_unique(OtlpHttpClientOptions( - options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_))) {} OtlpHttpMetricExporter::OtlpHttpMetricExporter( @@ -94,37 +90,7 @@ OtlpHttpMetricExporter::OtlpHttpMetricExporter( runtime_options_(runtime_options), aggregation_temporality_selector_{ OtlpMetricUtils::ChooseTemporalitySelector(options_.aggregation_temporality)}, - http_client_(std::make_unique(OtlpHttpClientOptions( - options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_))) {} OtlpHttpMetricExporter::OtlpHttpMetricExporter( @@ -134,38 +100,8 @@ OtlpHttpMetricExporter::OtlpHttpMetricExporter( runtime_options_(), aggregation_temporality_selector_{ OtlpMetricUtils::ChooseTemporalitySelector(options_.aggregation_temporality)}, - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - factory)) + http_client_( + std::make_unique(MakeClientOptions(options, runtime_options_), factory)) {} OtlpHttpMetricExporter::OtlpHttpMetricExporter( @@ -176,38 +112,8 @@ OtlpHttpMetricExporter::OtlpHttpMetricExporter( runtime_options_(runtime_options), aggregation_temporality_selector_{ OtlpMetricUtils::ChooseTemporalitySelector(options_.aggregation_temporality)}, - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - factory)) + http_client_( + std::make_unique(MakeClientOptions(options, runtime_options_), factory)) {} OtlpHttpMetricExporter::OtlpHttpMetricExporter( @@ -217,38 +123,8 @@ OtlpHttpMetricExporter::OtlpHttpMetricExporter( runtime_options_(), aggregation_temporality_selector_{ OtlpMetricUtils::ChooseTemporalitySelector(options_.aggregation_temporality)}, - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - std::shared_ptr{nullptr} -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - std::move(http_client))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_), + std::move(http_client))) {} OtlpHttpMetricExporter::OtlpHttpMetricExporter( @@ -259,38 +135,8 @@ OtlpHttpMetricExporter::OtlpHttpMetricExporter( runtime_options_(runtime_options), aggregation_temporality_selector_{ OtlpMetricUtils::ChooseTemporalitySelector(options_.aggregation_temporality)}, - http_client_(std::make_unique( - OtlpHttpClientOptions(options.url, - options.ssl_insecure_skip_verify, - options.ssl_ca_cert_path, - options.ssl_ca_cert_string, - options.ssl_client_key_path, - options.ssl_client_key_string, - options.ssl_client_cert_path, - options.ssl_client_cert_string, - options.ssl_min_tls, - options.ssl_max_tls, - options.ssl_cipher, - options.ssl_cipher_suite, - options.content_type, - options.json_bytes_mapping, - options.compression, - options.use_json_name, - options.console_debug, - options.timeout, - options.http_headers, - options.retry_policy_max_attempts, - options.retry_policy_initial_backoff, - options.retry_policy_max_backoff, - options.retry_policy_backoff_multiplier, - runtime_options.thread_instrumentation -#ifdef ENABLE_ASYNC_EXPORT - , - options.max_concurrent_requests, - options.max_requests_per_connection -#endif - ), - std::move(http_client))) + http_client_(std::make_unique(MakeClientOptions(options, runtime_options_), + std::move(http_client))) {} OtlpHttpMetricExporter::OtlpHttpMetricExporter(std::unique_ptr http_client) diff --git a/exporters/otlp/src/otlp_json_converter.cc b/exporters/otlp/src/otlp_json_converter.cc new file mode 100644 index 0000000000..ff869aaef1 --- /dev/null +++ b/exporters/otlp/src/otlp_json_converter.cc @@ -0,0 +1,338 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/otlp/otlp_json_converter.h" + +#include +#include +#include +#include + +#if defined(HAVE_GSL) +# include +#else +# include +#endif + +#include "opentelemetry/sdk/common/base64.h" + +// clang-format off +#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep +// clang-format on +#include +#include +// clang-format off +#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep +// clang-format on + +#ifdef GetMessage +# undef GetMessage +#endif + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace +{ + +inline char HexEncode(unsigned char byte) +{ +#if defined(HAVE_GSL) + Expects(byte <= 16); +#else + assert(byte <= 16); +#endif + if (byte >= 10) + { + return static_cast(byte - 10 + 'a'); + } + else + { + return static_cast(byte + '0'); + } +} + +std::string HexEncode(const std::string &bytes) +{ + std::string ret; + ret.reserve(bytes.size() * 2); + for (std::string::size_type i = 0; i < bytes.size(); ++i) + { + unsigned char byte = static_cast(bytes[i]); + ret.push_back(HexEncode(byte >> 4)); + ret.push_back(HexEncode(byte & 0x0f)); + } + return ret; +} + +void WriteBytesField(JsonWriter &writer, + const std::string &bytes, + const google::protobuf::FieldDescriptor *field_descriptor, + JsonBytesMappingKind kind) +{ + switch (kind) + { + case JsonBytesMappingKind::kHexId: { + if (field_descriptor->lowercase_name() == "trace_id" || + field_descriptor->lowercase_name() == "span_id" || + field_descriptor->lowercase_name() == "parent_span_id") + { + writer.WriteString(HexEncode(bytes)); + } + else + { + writer.WriteBytes(reinterpret_cast(bytes.data()), bytes.size()); + } + break; + } + case JsonBytesMappingKind::kBase64: { + // Base64 is the default bytes mapping of protobuf + writer.WriteBytes(reinterpret_cast(bytes.data()), bytes.size()); + break; + } + case JsonBytesMappingKind::kHex: + writer.WriteString(HexEncode(bytes)); + break; + default: + writer.WriteString(bytes); + break; + } +} + +// NOLINTBEGIN(misc-no-recursion) +void ConvertGenericFieldToJson(JsonWriter &writer, + const google::protobuf::Message &message, + const google::protobuf::FieldDescriptor *field_descriptor, + const JsonConverterOptions &options) +{ + switch (field_descriptor->cpp_type()) + { + case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { + writer.WriteInt32(message.GetReflection()->GetInt32(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as + // decimal strings, and either numbers or strings are accepted when decoding. + writer.WriteString( + std::to_string(message.GetReflection()->GetInt64(message, field_descriptor))); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { + writer.WriteUInt32(message.GetReflection()->GetUInt32(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as + // decimal strings, and either numbers or strings are accepted when decoding. + writer.WriteString( + std::to_string(message.GetReflection()->GetUInt64(message, field_descriptor))); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { + std::string empty; + if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) + { + WriteBytesField( + writer, message.GetReflection()->GetStringReference(message, field_descriptor, &empty), + field_descriptor, options.json_bytes_mapping); + } + else + { + writer.WriteString( + message.GetReflection()->GetStringReference(message, field_descriptor, &empty)); + } + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { + ConvertGenericMessageToJson( + writer, message.GetReflection()->GetMessage(message, field_descriptor, nullptr), options); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { + writer.WriteDouble(message.GetReflection()->GetDouble(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { + writer.WriteDouble(message.GetReflection()->GetFloat(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { + writer.WriteBool(message.GetReflection()->GetBool(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { + writer.WriteInt32(message.GetReflection()->GetEnumValue(message, field_descriptor)); + break; + } + default: { + writer.WriteNull(); + break; + } + } +} + +void ConvertListFieldToJson(JsonWriter &writer, + const google::protobuf::Message &message, + const google::protobuf::FieldDescriptor *field_descriptor, + const JsonConverterOptions &options) +{ + auto field_size = message.GetReflection()->FieldSize(message, field_descriptor); + + writer.BeginArray(); + + switch (field_descriptor->cpp_type()) + { + case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteInt32(message.GetReflection()->GetRepeatedInt32(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { + for (int i = 0; i < field_size; ++i) + { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded + // as decimal strings, and either numbers or strings are accepted when decoding. + writer.WriteString(std::to_string( + message.GetReflection()->GetRepeatedInt64(message, field_descriptor, i))); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteUInt32( + message.GetReflection()->GetRepeatedUInt32(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { + for (int i = 0; i < field_size; ++i) + { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded + // as decimal strings, and either numbers or strings are accepted when decoding. + writer.WriteString(std::to_string( + message.GetReflection()->GetRepeatedUInt64(message, field_descriptor, i))); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { + std::string empty; + if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) + { + for (int i = 0; i < field_size; ++i) + { + WriteBytesField(writer, + message.GetReflection()->GetRepeatedStringReference( + message, field_descriptor, i, &empty), + field_descriptor, options.json_bytes_mapping); + } + } + else + { + for (int i = 0; i < field_size; ++i) + { + writer.WriteString(message.GetReflection()->GetRepeatedStringReference( + message, field_descriptor, i, &empty)); + } + } + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { + for (int i = 0; i < field_size; ++i) + { + ConvertGenericMessageToJson( + writer, message.GetReflection()->GetRepeatedMessage(message, field_descriptor, i), + options); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteDouble( + message.GetReflection()->GetRepeatedDouble(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteDouble(message.GetReflection()->GetRepeatedFloat(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteBool(message.GetReflection()->GetRepeatedBool(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteInt32( + message.GetReflection()->GetRepeatedEnumValue(message, field_descriptor, i)); + } + break; + } + default: { + break; + } + } + + writer.EndArray(); +} + +} // namespace +// NOLINTEND(misc-no-recursion) + +void ConvertGenericMessageToJson(JsonWriter &writer, + const google::protobuf::Message &message, + const JsonConverterOptions &options) +{ + std::vector fields_with_data; + message.GetReflection()->ListFields(message, &fields_with_data); + + // A message with no set fields serializes as null, not as an empty object. + if (fields_with_data.empty()) + { + writer.WriteNull(); + return; + } + + writer.BeginObject(); + for (std::size_t i = 0; i < fields_with_data.size(); ++i) + { + const google::protobuf::FieldDescriptor *field_descriptor = fields_with_data[i]; + writer.Key(options.use_json_name ? field_descriptor->json_name() + : field_descriptor->camelcase_name()); + if (field_descriptor->is_repeated()) + { + ConvertListFieldToJson(writer, message, field_descriptor, options); + } + else + { + ConvertGenericFieldToJson(writer, message, field_descriptor, options); + } + } + writer.EndObject(); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/src/otlp_json_writer_nlohmann.cc b/exporters/otlp/src/otlp_json_writer_nlohmann.cc new file mode 100644 index 0000000000..4cf68c01a7 --- /dev/null +++ b/exporters/otlp/src/otlp_json_writer_nlohmann.cc @@ -0,0 +1,217 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h" + +#include +#include +#include +#include +#include + +#include "nlohmann/json.hpp" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/sdk/common/base64.h" +#include "opentelemetry/sdk/common/global_log_handler.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace +{ + +class NlohmannJsonWriter final : public JsonWriter +{ +public: + void BeginObject() noexcept override + { + OpenContainer(nlohmann::json::object(), Container::kObject); + } + + void EndObject() noexcept override { CloseContainer(Container::kObject); } + + void BeginArray() noexcept override { OpenContainer(nlohmann::json::array(), Container::kArray); } + + void EndArray() noexcept override { CloseContainer(Container::kArray); } + + void Key(nostd::string_view key) noexcept override + { + if (!ok_) + { + return; + } + if (stack_.empty() || stack_.back().kind != Container::kObject) + { + Fail("Key() called outside an object"); + return; + } + if (has_pending_key_) + { + Fail("Key() called twice without an intervening value"); + return; + } + pending_key_ = std::string(key.data(), key.size()); + has_pending_key_ = true; + } + + void WriteNull() noexcept override { ClaimSlot(); } + + void WriteString(nostd::string_view value) noexcept override + { + if (nlohmann::json *slot = ClaimSlot()) + { + *slot = std::string(value.data(), value.size()); + } + } + + void WriteInt32(std::int32_t value) noexcept override { WriteScalar(value); } + + void WriteInt64(std::int64_t value) noexcept override { WriteScalar(value); } + + void WriteUInt32(std::uint32_t value) noexcept override { WriteScalar(value); } + + void WriteUInt64(std::uint64_t value) noexcept override { WriteScalar(value); } + + void WriteDouble(double value) noexcept override { WriteScalar(value); } + + void WriteBool(bool value) noexcept override { WriteScalar(value); } + + void WriteBytes(const std::uint8_t *data, std::size_t size) noexcept override + { + if (nlohmann::json *slot = ClaimSlot()) + { + *slot = opentelemetry::sdk::common::Base64Escape( + std::string(reinterpret_cast(data), size)); + } + } + + bool ok() const noexcept override { return ok_; } + + std::string ToString() noexcept override + { + return root_.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); + } + +private: + enum class Container + { + kObject, + kArray, + }; + + struct Frame + { + nlohmann::json *node; + Container kind; + }; + + template + void WriteScalar(T value) noexcept + { + if (nlohmann::json *slot = ClaimSlot()) + { + *slot = value; + } + } + + // Returns the slot the next value is written into, consuming the pending + // key or appending an array element. Fails and returns nullptr if the token + // stream is malformed. + nlohmann::json *ClaimSlot() noexcept + { + if (!ok_) + { + return nullptr; + } + + if (stack_.empty()) + { + if (root_set_) + { + Fail("a value was written after the top-level value was already complete"); + return nullptr; + } + root_set_ = true; + return &root_; + } + + Frame &frame = stack_.back(); + if (frame.kind == Container::kArray) + { + frame.node->push_back(nlohmann::json()); + return &frame.node->back(); + } + + // kObject + if (!has_pending_key_) + { + Fail("a value was written without a preceding Key()"); + return nullptr; + } + has_pending_key_ = false; + nlohmann::json &slot = (*frame.node)[pending_key_]; + return &slot; + } + + void OpenContainer(nlohmann::json &&empty_container, Container kind) noexcept + { + nlohmann::json *slot = ClaimSlot(); + if (!slot) + { + return; + } + *slot = std::move(empty_container); + stack_.push_back(Frame{slot, kind}); + } + + void CloseContainer(Container kind) noexcept + { + if (!ok_) + { + return; + } + if (stack_.empty() || stack_.back().kind != kind) + { + Fail("mismatched Begin/End call"); + return; + } + if (stack_.back().kind == Container::kObject && has_pending_key_) + { + Fail("object closed with a Key() that has no value"); + return; + } + stack_.pop_back(); + } + + void Fail(const char *reason) noexcept + { + if (!ok_) + { + return; + } + ok_ = false; + OTEL_INTERNAL_LOG_ERROR("[OTLP JSON Writer] " << reason); + } + + nlohmann::json root_; + std::vector stack_; + std::string pending_key_; + bool has_pending_key_ = false; + bool root_set_ = false; + bool ok_ = true; +}; + +} // namespace + +std::unique_ptr JsonWriterFactoryNlohmann::Create() +{ + return std::make_unique(); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_client_test.cc b/exporters/otlp/test/otlp_file_client_test.cc index 0a7e1e84dc..35170ea41f 100644 --- a/exporters/otlp/test/otlp_file_client_test.cc +++ b/exporters/otlp/test/otlp_file_client_test.cc @@ -20,9 +20,12 @@ // IWYU pragma: no_include #include "opentelemetry/common/timestamp.h" +#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_client.h" #include "opentelemetry/exporters/otlp/otlp_file_client_options.h" #include "opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/exporters/otlp/otlp_recordable.h" #include "opentelemetry/exporters/otlp/otlp_recordable_utils.h" #include "opentelemetry/nostd/span.h" @@ -39,6 +42,7 @@ #include "opentelemetry/trace/trace_id.h" #include "opentelemetry/trace/trace_state.h" #include "opentelemetry/version.h" +#include "otlp_marking_json_writer.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep @@ -163,6 +167,38 @@ static std::unique_ptr MakeRecordable( return {std::move(recordable)}; } +TEST(OtlpFileClientTest, CustomJsonWriterFactoryIsUsed) +{ + auto resource = MakeResource(); + auto instrumentation_scope = MakeInstrumentationScope(); + + std::unique_ptr recordable[] = { + MakeRecordable(resource, *instrumentation_scope)}; + + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request; + OtlpRecordableUtils::PopulateRequest(MakeSpan(recordable), &request); + + std::stringstream output_stream; + + opentelemetry::exporter::otlp::OtlpFileClientOptions opts; + opentelemetry::exporter::otlp::OtlpFileClientRuntimeOptions rt_opts; + opts.backend_options = std::ref(output_stream); + + rt_opts.json_writer_factory = std::make_shared(); + + auto client = std::make_unique(std::move(opts), + std::move(rt_opts)); + client->Export(request, 1); + + EXPECT_EQ(output_stream.str().rfind("/*custom-writer*/", 0), 0u) + << "output was not produced by the injected JsonWriter: " << output_stream.str(); + + // The line still ends with the marked writer's underlying JSON, unchanged. + auto marker_size = std::string("/*custom-writer*/").size(); + auto check_json = nlohmann::json::parse(output_stream.str().substr(marker_size), nullptr, false); + EXPECT_FALSE(check_json.is_discarded()); +} + TEST(OtlpFileClientTest, Shutdown) { opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request; diff --git a/exporters/otlp/test/otlp_file_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_exporter_factory_test.cc index 1aab3ebbac..da8be33989 100644 --- a/exporters/otlp/test/otlp_file_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_exporter_factory_test.cc @@ -26,6 +26,8 @@ # error "protobuf should not be included" #endif +#include "otlp_stub_json_writer.h" + OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { @@ -41,6 +43,17 @@ TEST(OtlpFileExporterFactoryTest, BuildTest) EXPECT_TRUE(exporter != nullptr); } +TEST(OtlpFileExporterFactoryTest, BuildWithJsonWriterFactoryTest) +{ + OtlpFileExporterOptions opts; + OtlpFileExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + + auto exporter = OtlpFileExporterFactory::Create(opts, runtime_opts); + + EXPECT_TRUE(exporter != nullptr); +} + } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_exporter_test.cc b/exporters/otlp/test/otlp_file_exporter_test.cc index 95cff477f7..6dfc1dafa5 100644 --- a/exporters/otlp/test/otlp_file_exporter_test.cc +++ b/exporters/otlp/test/otlp_file_exporter_test.cc @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include #include #include +#include "opentelemetry/common/key_value_iterable_view.h" #include "opentelemetry/common/timestamp.h" #include "opentelemetry/context/context.h" #include "opentelemetry/exporters/otlp/otlp_file_exporter.h" @@ -21,6 +23,7 @@ #include "opentelemetry/nostd/span.h" #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/exporter_utils.h" +#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" #include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/trace/batch_span_processor.h" #include "opentelemetry/sdk/trace/batch_span_processor_options.h" @@ -30,7 +33,10 @@ #include "opentelemetry/sdk/trace/tracer_provider.h" #include "opentelemetry/trace/span.h" #include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/span_id.h" +#include "opentelemetry/trace/span_metadata.h" #include "opentelemetry/trace/span_startoptions.h" +#include "opentelemetry/trace/trace_flags.h" #include "opentelemetry/trace/trace_id.h" #include "opentelemetry/trace/tracer.h" #include "opentelemetry/version.h" @@ -208,6 +214,79 @@ TEST_F(OtlpFileExporterTestPeer, ExportJsonIntegrationTestSync) ExportJsonIntegrationTest(); } +TEST(OtlpFileExporterTest, ExportJsonGoldenBody) +{ + static ProtobufGlobalSymbolGuard global_symbol_guard; + + std::stringstream output; + OtlpFileExporterOptions opts; + opts.backend_options = std::ref(output); + auto exporter = OtlpFileExporterFactory::Create(opts); + + auto recordable = exporter->MakeRecordable(); + + const uint8_t trace_id_bytes[trace_api::TraceId::kSize] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10}; + const uint8_t span_id_bytes[trace_api::SpanId::kSize] = {0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18}; + const uint8_t parent_span_id_bytes[trace_api::SpanId::kSize] = {0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28}; + const uint8_t link_trace_id_bytes[trace_api::TraceId::kSize] = { + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40}; + const uint8_t link_span_id_bytes[trace_api::SpanId::kSize] = {0x41, 0x42, 0x43, 0x44, + 0x45, 0x46, 0x47, 0x48}; + + trace_api::SpanContext span_context( + trace_api::TraceId(trace_id_bytes), trace_api::SpanId(span_id_bytes), + trace_api::TraceFlags(trace_api::TraceFlags::kIsSampled), false); + recordable->SetIdentity(span_context, trace_api::SpanId(parent_span_id_bytes)); + + recordable->SetName("golden span"); + recordable->SetSpanKind(trace_api::SpanKind::kServer); + recordable->SetStartTime( + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000))); + recordable->SetDuration(std::chrono::nanoseconds(500000000)); + recordable->SetStatus(trace_api::StatusCode::kOk, "all good"); + recordable->SetAttribute("bool_attr", true); + recordable->SetAttribute("int64_attr", static_cast(-42)); + recordable->SetAttribute("uint64_attr", static_cast(42)); + recordable->SetAttribute("double_attr", static_cast(3.5)); + recordable->SetAttribute("string_attr", "golden value"); + + std::map event_attribute_map = {{"event_key", "event_value"}}; + recordable->AddEvent("golden event", + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000500000)), + opentelemetry::common::MakeAttributes(event_attribute_map)); + + trace_api::SpanContext link_context( + trace_api::TraceId(link_trace_id_bytes), trace_api::SpanId(link_span_id_bytes), + trace_api::TraceFlags(trace_api::TraceFlags::kIsSampled), false); + std::map link_attribute_map = {{"link_key", "link_value"}}; + recordable->AddLink(link_context, opentelemetry::common::MakeAttributes(link_attribute_map)); + + auto instrumentation_scope = + opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create( + "golden_scope", "golden_scope_version"); + recordable->SetInstrumentationScope(*instrumentation_scope); + + nostd::span> batch(&recordable, 1); + EXPECT_EQ(opentelemetry::sdk::common::ExportResult::kSuccess, exporter->Export(batch)); + + output.flush(); + std::string captured_body = output.str(); + if (!captured_body.empty() && captured_body.back() == '\n') + { + captured_body.pop_back(); + } + + static constexpr char kExpectedBody[] = + R"({"resourceSpans":[{"scopeSpans":[{"scope":{"name":"golden_scope","version":"golden_scope_version"},"spans":[{"attributes":[{"key":"bool_attr","value":{"boolValue":true}},{"key":"int64_attr","value":{"intValue":"-42"}},{"key":"uint64_attr","value":{"intValue":"42"}},{"key":"double_attr","value":{"doubleValue":3.5}},{"key":"string_attr","value":{"stringValue":"golden value"}}],"endTimeUnixNano":"1500000000","events":[{"attributes":[{"key":"event_key","value":{"stringValue":"event_value"}}],"name":"golden event","timeUnixNano":"1000500000"}],"kind":2,"links":[{"attributes":[{"key":"link_key","value":{"stringValue":"link_value"}}],"spanId":"4142434445464748","traceId":"3132333435363738393a3b3c3d3e3f40"}],"name":"golden span","parentSpanId":"2122232425262728","spanId":"1112131415161718","startTimeUnixNano":"1000000000","status":{"code":1},"traceId":"0102030405060708090a0b0c0d0e0f10"}]}]}]})"; + + ASSERT_EQ(std::string(kExpectedBody), captured_body); +} + } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc index 927e763826..399f5e790f 100644 --- a/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc @@ -26,6 +26,8 @@ # error "protobuf should not be included" #endif +#include "otlp_stub_json_writer.h" + OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { @@ -41,6 +43,17 @@ TEST(OtlpFileLogRecordExporterFactoryTest, BuildTest) EXPECT_TRUE(exporter != nullptr); } +TEST(OtlpFileLogRecordExporterFactoryTest, BuildWithJsonWriterFactoryTest) +{ + OtlpFileLogRecordExporterOptions opts; + OtlpFileLogRecordExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + + auto exporter = OtlpFileLogRecordExporterFactory::Create(opts, runtime_opts); + + EXPECT_TRUE(exporter != nullptr); +} + } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_log_record_exporter_test.cc b/exporters/otlp/test/otlp_file_log_record_exporter_test.cc index ad7bfb708a..7fd6fa5ded 100644 --- a/exporters/otlp/test/otlp_file_log_record_exporter_test.cc +++ b/exporters/otlp/test/otlp_file_log_record_exporter_test.cc @@ -21,11 +21,13 @@ #include "opentelemetry/nostd/span.h" #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/sdk/common/exporter_utils.h" +#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" #include "opentelemetry/sdk/logs/batch_log_record_processor.h" #include "opentelemetry/sdk/logs/exporter.h" #include "opentelemetry/sdk/logs/logger_provider.h" #include "opentelemetry/sdk/logs/processor.h" #include "opentelemetry/sdk/logs/recordable.h" +#include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/trace/span_id.h" #include "opentelemetry/trace/trace_flags.h" #include "opentelemetry/trace/trace_id.h" @@ -193,6 +195,60 @@ TEST_F(OtlpFileLogRecordExporterTestPeer, ExportJsonIntegrationTestSync) ExportJsonIntegrationTest(); } +TEST(OtlpFileLogRecordExporterTest, ExportJsonGoldenBody) +{ + static ProtobufGlobalSymbolGuard global_symbol_guard; + + std::stringstream output; + OtlpFileLogRecordExporterOptions opts; + opts.backend_options = std::ref(output); + auto exporter = OtlpFileLogRecordExporterFactory::Create(opts); + + auto recordable = exporter->MakeRecordable(); + + const uint8_t trace_id_bytes[opentelemetry::trace::TraceId::kSize] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; + const uint8_t span_id_bytes[opentelemetry::trace::SpanId::kSize] = {0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18}; + + recordable->SetTimestamp( + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000))); + recordable->SetObservedTimestamp( + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000500000))); + recordable->SetSeverity(opentelemetry::logs::Severity::kError); + recordable->SetBody("golden log body"); + recordable->SetEventId(42, "golden_event"); + recordable->SetTraceId(opentelemetry::trace::TraceId(trace_id_bytes)); + recordable->SetSpanId(opentelemetry::trace::SpanId(span_id_bytes)); + recordable->SetTraceFlags( + opentelemetry::trace::TraceFlags(opentelemetry::trace::TraceFlags::kIsSampled)); + recordable->SetAttribute("bool_attr", true); + recordable->SetAttribute("int64_attr", static_cast(-42)); + recordable->SetAttribute("string_attr", "golden value"); + recordable->SetResource(opentelemetry::sdk::resource::Resource::GetEmpty()); + + auto instrumentation_scope = + opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create( + "golden_scope", "golden_scope_version"); + recordable->SetInstrumentationScope(*instrumentation_scope); + + nostd::span> batch(&recordable, 1); + EXPECT_EQ(opentelemetry::sdk::common::ExportResult::kSuccess, exporter->Export(batch)); + + output.flush(); + std::string captured_body = output.str(); + if (!captured_body.empty() && captured_body.back() == '\n') + { + captured_body.pop_back(); + } + + static constexpr char kExpectedBody[] = + R"({"resourceLogs":[{"resource":null,"scopeLogs":[{"logRecords":[{"attributes":[{"key":"bool_attr","value":{"boolValue":true}},{"key":"int64_attr","value":{"intValue":"-42"}},{"key":"string_attr","value":{"stringValue":"golden value"}}],"body":{"stringValue":"golden log body"},"eventName":"golden_event","flags":1,"observedTimeUnixNano":"1000500000","severityNumber":17,"severityText":"ERROR","spanId":"1112131415161718","timeUnixNano":"1000000000","traceId":"0102030405060708090a0b0c0d0e0f10"}],"scope":{"name":"golden_scope","version":"golden_scope_version"}}]}]})"; + + ASSERT_EQ(std::string(kExpectedBody), captured_body); +} + } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc index 78e3bf366f..2bba8cdd3b 100644 --- a/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc @@ -26,6 +26,8 @@ # error "protobuf should not be included" #endif +#include "otlp_stub_json_writer.h" + OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { @@ -41,6 +43,17 @@ TEST(OtlpFileMetricExporterFactory, BuildTest) EXPECT_TRUE(exporter != nullptr); } +TEST(OtlpFileMetricExporterFactory, BuildWithJsonWriterFactoryTest) +{ + OtlpFileMetricExporterOptions opts; + OtlpFileMetricExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + + auto exporter = OtlpFileMetricExporterFactory::Create(opts, runtime_opts); + + EXPECT_TRUE(exporter != nullptr); +} + } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_metric_exporter_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_test.cc index bc6138c9c1..2270cef142 100644 --- a/exporters/otlp/test/otlp_file_metric_exporter_test.cc +++ b/exporters/otlp/test/otlp_file_metric_exporter_test.cc @@ -337,6 +337,94 @@ TEST(OtlpFileMetricExporterTest, Shutdown) EXPECT_EQ(result, opentelemetry::sdk::common::ExportResult::kFailure); } +TEST(OtlpFileMetricExporterTest, ExportJsonGoldenBody) +{ + static ProtobufGlobalSymbolGuard global_symbol_guard; + + std::stringstream output; + OtlpFileMetricExporterOptions opts; + opts.backend_options = std::ref(output); + auto exporter = OtlpFileMetricExporterFactory::Create(opts); + + opentelemetry::sdk::metrics::SumPointData sum_point_data{}; + sum_point_data.value_ = static_cast(7); + sum_point_data.is_monotonic_ = true; + + opentelemetry::sdk::metrics::LastValuePointData last_value_point_data{}; + last_value_point_data.value_ = 3.5; + last_value_point_data.is_lastvalue_valid_ = true; + last_value_point_data.sample_ts_ = + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)); + + opentelemetry::sdk::metrics::HistogramPointData histogram_point_data{}; + histogram_point_data.boundaries_ = {1.0, 5.0}; + histogram_point_data.count_ = 2; + histogram_point_data.counts_ = {1, 1}; + histogram_point_data.sum_ = 6.0; + histogram_point_data.min_ = 2.0; + histogram_point_data.max_ = 4.0; + + opentelemetry::sdk::metrics::MetricData sum_metric_data{ + opentelemetry::sdk::metrics::InstrumentDescriptor{ + "golden_counter", "golden counter description", "1", + opentelemetry::sdk::metrics::InstrumentType::kCounter, + opentelemetry::sdk::metrics::InstrumentValueType::kLong}, + opentelemetry::sdk::metrics::AggregationTemporality::kCumulative, + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000)), + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)), + std::vector{ + {opentelemetry::sdk::metrics::PointAttributes{{"sum_key", "sum_value"}}, + sum_point_data}}}; + + opentelemetry::sdk::metrics::MetricData gauge_metric_data{ + opentelemetry::sdk::metrics::InstrumentDescriptor{ + "golden_gauge", "golden gauge description", "1", + opentelemetry::sdk::metrics::InstrumentType::kObservableGauge, + opentelemetry::sdk::metrics::InstrumentValueType::kDouble}, + opentelemetry::sdk::metrics::AggregationTemporality::kDelta, + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000)), + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)), + std::vector{ + {opentelemetry::sdk::metrics::PointAttributes{{"gauge_key", "gauge_value"}}, + last_value_point_data}}}; + + opentelemetry::sdk::metrics::MetricData histogram_metric_data{ + opentelemetry::sdk::metrics::InstrumentDescriptor{ + "golden_histogram", "golden histogram description", "1", + opentelemetry::sdk::metrics::InstrumentType::kHistogram, + opentelemetry::sdk::metrics::InstrumentValueType::kDouble}, + opentelemetry::sdk::metrics::AggregationTemporality::kCumulative, + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000)), + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)), + std::vector{ + {opentelemetry::sdk::metrics::PointAttributes{{"histogram_key", "histogram_value"}}, + histogram_point_data}}}; + + opentelemetry::sdk::metrics::ResourceMetrics data; + data.resource_ = &opentelemetry::sdk::resource::Resource::GetEmpty(); + auto scope = opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create( + "golden_scope", "golden_scope_version"); + data.scope_metric_data_ = std::vector{ + {scope.get(), std::vector{ + sum_metric_data, gauge_metric_data, histogram_metric_data}}}; + + EXPECT_EQ(opentelemetry::sdk::common::ExportResult::kSuccess, exporter->Export(data)); + exporter->ForceFlush(); + + output.flush(); + output.sync(); + std::string captured_body = output.str(); + if (!captured_body.empty() && captured_body.back() == '\n') + { + captured_body.pop_back(); + } + + static constexpr char kExpectedBody[] = + R"({"resourceMetrics":[{"resource":null,"scopeMetrics":[{"metrics":[{"description":"golden counter description","name":"golden_counter","sum":{"aggregationTemporality":2,"dataPoints":[{"asInt":"7","attributes":[{"key":"sum_key","value":{"stringValue":"sum_value"}}],"startTimeUnixNano":"1000000000","timeUnixNano":"2000000000"}],"isMonotonic":true},"unit":"1"},{"description":"golden gauge description","gauge":{"dataPoints":[{"asDouble":3.5,"attributes":[{"key":"gauge_key","value":{"stringValue":"gauge_value"}}],"startTimeUnixNano":"1000000000","timeUnixNano":"2000000000"}]},"name":"golden_gauge","unit":"1"},{"description":"golden histogram description","histogram":{"aggregationTemporality":2,"dataPoints":[{"attributes":[{"key":"histogram_key","value":{"stringValue":"histogram_value"}}],"bucketCounts":["1","1"],"count":"2","explicitBounds":[1.0,5.0],"max":4.0,"min":2.0,"startTimeUnixNano":"1000000000","sum":6.0,"timeUnixNano":"2000000000"}]},"name":"golden_histogram","unit":"1"}],"scope":{"name":"golden_scope","version":"golden_scope_version"}}]}]})"; + + ASSERT_EQ(std::string(kExpectedBody), captured_body); +} + TEST_F(OtlpFileMetricExporterTestPeer, ExportJsonIntegrationTestSumPointDataSync) { ExportJsonIntegrationTestExportSumPointData(); diff --git a/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc new file mode 100644 index 0000000000..cc77a7c194 --- /dev/null +++ b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc @@ -0,0 +1,117 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include "gmock/gmock.h" + +#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_client.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h" +#include "opentelemetry/exporters/otlp/otlp_http_exporter_runtime_options.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" +#include "opentelemetry/ext/http/client/http_client.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/sdk/common/exporter_utils.h" +#include "opentelemetry/sdk/trace/batch_span_processor.h" +#include "opentelemetry/sdk/trace/batch_span_processor_options.h" +#include "opentelemetry/sdk/trace/exporter.h" +#include "opentelemetry/sdk/trace/processor.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/trace/tracer.h" +#include "opentelemetry/version.h" +#include "otlp_marking_json_writer.h" + +#include "opentelemetry/test_common/ext/http/client/http_client_test_factory.h" +#include "opentelemetry/test_common/ext/http/client/nosend/http_client_factory_nosend.h" +#include "opentelemetry/test_common/ext/http/client/nosend/http_client_nosend.h" + +using namespace testing; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace http_client = opentelemetry::ext::http::client; + +class OtlpHttpExporterCustomJsonWriterTestPeer : public ::testing::Test +{}; + +TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, RuntimeOptionsInjectionCreatesExporter) +{ + OtlpHttpExporterOptions opts; + OtlpHttpExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + auto exporter = OtlpHttpExporterFactory::Create(opts, runtime_opts); + ASSERT_NE(exporter, nullptr); +} + +TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, RuntimeOptionsWithHttpClientCreatesExporter) +{ + OtlpHttpExporterOptions opts; + OtlpHttpExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + auto client = http_client::HttpClientTestFactory::Create(); + auto exporter = OtlpHttpExporterFactory::Create(opts, runtime_opts, std::move(client)); + ASSERT_NE(exporter, nullptr); +} + +TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, ExportUsesTheInjectedJsonWriter) +{ + auto client = http_client::HttpClientTestFactory::Create(); + auto no_send_client = std::static_pointer_cast(client); + auto session = std::static_pointer_cast(no_send_client->session_); + + OtlpHttpExporterOptions options; + options.content_type = HttpRequestContentType::kJson; + OtlpHttpExporterRuntimeOptions runtime_options; + runtime_options.json_writer_factory = std::make_shared(); + auto exporter = OtlpHttpExporterFactory::Create(options, runtime_options, std::move(client)); + + std::string captured_body; + EXPECT_CALL(*session, SendRequest) + .WillRepeatedly( + [&session, &captured_body]( + const std::shared_ptr &callback) { + captured_body = session->GetRequest()->body_.empty() + ? std::string() + : std::string(session->GetRequest()->body_.begin(), + session->GetRequest()->body_.end()); + http_client::nosend::Response response; + response.Finish(*callback); + }); + + sdk::trace::BatchSpanProcessorOptions processor_opts; + processor_opts.max_export_batch_size = 5; + processor_opts.max_queue_size = 5; + processor_opts.schedule_delay_millis = std::chrono::milliseconds(256); + + std::unique_ptr processor = + std::make_unique(std::move(exporter), processor_opts); + auto provider = nostd::shared_ptr( + new sdk::trace::TracerProvider(std::move(processor))); + + auto tracer = provider->GetTracer("test"); + auto span = tracer->StartSpan("custom-json-writer-span"); + span->End(); + + provider->ForceFlush(); + + ASSERT_FALSE(captured_body.empty()); + EXPECT_EQ(captured_body.rfind("/*custom-writer*/", 0), 0u) + << "request body was not produced by the injected JsonWriter: " << captured_body; +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_http_exporter_test.cc b/exporters/otlp/test/otlp_http_exporter_test.cc index 5e3eeb990d..f354c61ebd 100644 --- a/exporters/otlp/test/otlp_http_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_test.cc @@ -13,6 +13,7 @@ #include "gmock/gmock.h" #include "nlohmann/json.hpp" +#include "opentelemetry/common/key_value_iterable_view.h" #include "opentelemetry/common/timestamp.h" #include "opentelemetry/context/context.h" #include "opentelemetry/exporters/otlp/otlp_environment.h" @@ -28,6 +29,7 @@ #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/sdk/common/thread_instrumentation.h" +#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" #include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/sdk/trace/batch_span_processor.h" #include "opentelemetry/sdk/trace/batch_span_processor_options.h" @@ -40,10 +42,14 @@ #include "opentelemetry/test_common/sdk/common/scoped_test_log_handler.h" #include "opentelemetry/trace/span.h" #include "opentelemetry/trace/span_context.h" +#include "opentelemetry/trace/span_id.h" +#include "opentelemetry/trace/span_metadata.h" #include "opentelemetry/trace/span_startoptions.h" +#include "opentelemetry/trace/trace_flags.h" #include "opentelemetry/trace/trace_id.h" #include "opentelemetry/trace/tracer.h" #include "opentelemetry/version.h" +#include "otlp_http_request_body_capture.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep @@ -962,6 +968,78 @@ TEST_F(OtlpHttpExporterTestPeer, ExportPartialSuccess) EXPECT_TRUE(contains("too many spans!!")); } +TEST_F(OtlpHttpExporterTestPeer, ExportJsonGoldenBody) +{ + auto mock_otlp_client = + OtlpHttpExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); + auto exporter = GetExporter(std::unique_ptr{mock_otlp_client.first}); + + auto no_send_client = + std::static_pointer_cast(mock_otlp_client.second); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + + std::string captured_body; + CaptureRequestBody(mock_session, captured_body); + + auto recordable = exporter->MakeRecordable(); + + const uint8_t trace_id_bytes[trace_api::TraceId::kSize] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + 0x0d, 0x0e, 0x0f, 0x10}; + const uint8_t span_id_bytes[trace_api::SpanId::kSize] = {0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18}; + const uint8_t parent_span_id_bytes[trace_api::SpanId::kSize] = {0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28}; + const uint8_t link_trace_id_bytes[trace_api::TraceId::kSize] = { + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40}; + const uint8_t link_span_id_bytes[trace_api::SpanId::kSize] = {0x41, 0x42, 0x43, 0x44, + 0x45, 0x46, 0x47, 0x48}; + + trace_api::SpanContext span_context( + trace_api::TraceId(trace_id_bytes), trace_api::SpanId(span_id_bytes), + trace_api::TraceFlags(trace_api::TraceFlags::kIsSampled), false); + recordable->SetIdentity(span_context, trace_api::SpanId(parent_span_id_bytes)); + + recordable->SetName("golden span"); + recordable->SetSpanKind(trace_api::SpanKind::kServer); + recordable->SetStartTime( + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000))); + recordable->SetDuration(std::chrono::nanoseconds(500000000)); + recordable->SetStatus(trace_api::StatusCode::kOk, "all good"); + + recordable->SetAttribute("bool_attr", true); + recordable->SetAttribute("int64_attr", static_cast(-42)); + recordable->SetAttribute("uint64_attr", static_cast(42)); + recordable->SetAttribute("double_attr", static_cast(3.5)); + recordable->SetAttribute("string_attr", "golden value"); + + std::map event_attribute_map = {{"event_key", "event_value"}}; + recordable->AddEvent("golden event", + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000500000)), + opentelemetry::common::MakeAttributes(event_attribute_map)); + + trace_api::SpanContext link_context( + trace_api::TraceId(link_trace_id_bytes), trace_api::SpanId(link_span_id_bytes), + trace_api::TraceFlags(trace_api::TraceFlags::kIsSampled), false); + std::map link_attribute_map = {{"link_key", "link_value"}}; + recordable->AddLink(link_context, opentelemetry::common::MakeAttributes(link_attribute_map)); + + auto instrumentation_scope = + opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create( + "golden_scope", "golden_scope_version"); + recordable->SetInstrumentationScope(*instrumentation_scope); + + nostd::span> batch(&recordable, 1); + EXPECT_EQ(sdk::common::ExportResult::kSuccess, exporter->Export(batch)); + + static constexpr char kExpectedBody[] = + R"({"resourceSpans":[{"scopeSpans":[{"scope":{"name":"golden_scope","version":"golden_scope_version"},"spans":[{"attributes":[{"key":"bool_attr","value":{"boolValue":true}},{"key":"int64_attr","value":{"intValue":"-42"}},{"key":"uint64_attr","value":{"intValue":"42"}},{"key":"double_attr","value":{"doubleValue":3.5}},{"key":"string_attr","value":{"stringValue":"golden value"}}],"endTimeUnixNano":"1500000000","events":[{"attributes":[{"key":"event_key","value":{"stringValue":"event_value"}}],"name":"golden event","timeUnixNano":"1000500000"}],"kind":2,"links":[{"attributes":[{"key":"link_key","value":{"stringValue":"link_value"}}],"spanId":"4142434445464748","traceId":"3132333435363738393a3b3c3d3e3f40"}],"name":"golden span","parentSpanId":"2122232425262728","spanId":"1112131415161718","startTimeUnixNano":"1000000000","status":{"code":1},"traceId":"0102030405060708090a0b0c0d0e0f10"}]}]}]})"; + + ASSERT_EQ(std::string(kExpectedBody), captured_body); +} + // Exporter logs the rejection on partial_success when the response is JSON encoded. TEST_F(OtlpHttpExporterTestPeer, ExportPartialSuccessJson) { diff --git a/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc new file mode 100644 index 0000000000..d54d61f017 --- /dev/null +++ b/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_http_log_record_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_log_record_exporter_options.h" +#include "opentelemetry/exporters/otlp/otlp_http_log_record_exporter_runtime_options.h" +#include "opentelemetry/sdk/logs/exporter.h" +#include "opentelemetry/version.h" + +#include "opentelemetry/test_common/ext/http/client/http_client_test_factory.h" + +#include "otlp_stub_json_writer.h" + +namespace http_client = opentelemetry::ext::http::client; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +TEST(OtlpHttpLogRecordExporterCustomJsonWriterTest, RuntimeOptionsInjectionCreatesExporter) +{ + OtlpHttpLogRecordExporterOptions opts; + OtlpHttpLogRecordExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + auto exporter = OtlpHttpLogRecordExporterFactory::Create(opts, runtime_opts); + ASSERT_NE(exporter, nullptr); +} + +TEST(OtlpHttpLogRecordExporterCustomJsonWriterTest, RuntimeOptionsWithHttpClientCreatesExporter) +{ + OtlpHttpLogRecordExporterOptions opts; + OtlpHttpLogRecordExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + auto client = http_client::HttpClientTestFactory::Create(); + auto exporter = OtlpHttpLogRecordExporterFactory::Create(opts, runtime_opts, std::move(client)); + ASSERT_NE(exporter, nullptr); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_http_log_record_exporter_test.cc b/exporters/otlp/test/otlp_http_log_record_exporter_test.cc index 43d63ac9f8..d342effb1a 100644 --- a/exporters/otlp/test/otlp_http_log_record_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_log_record_exporter_test.cc @@ -28,12 +28,14 @@ #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/sdk/common/thread_instrumentation.h" +#include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" #include "opentelemetry/sdk/logs/batch_log_record_processor.h" #include "opentelemetry/sdk/logs/batch_log_record_processor_options.h" #include "opentelemetry/sdk/logs/exporter.h" #include "opentelemetry/sdk/logs/logger_provider.h" #include "opentelemetry/sdk/logs/processor.h" #include "opentelemetry/sdk/logs/recordable.h" +#include "opentelemetry/sdk/resource/resource.h" #include "opentelemetry/test_common/ext/http/client/http_client_test_factory.h" #include "opentelemetry/test_common/ext/http/client/nosend/http_client_nosend.h" #include "opentelemetry/test_common/sdk/common/scoped_test_log_handler.h" @@ -41,6 +43,7 @@ #include "opentelemetry/trace/trace_flags.h" #include "opentelemetry/trace/trace_id.h" #include "opentelemetry/version.h" +#include "otlp_http_request_body_capture.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep @@ -969,6 +972,58 @@ TEST_F(OtlpHttpLogRecordExporterTestPeer, ExportPartialSuccess) EXPECT_TRUE(contains("too many logs!!")); } +TEST_F(OtlpHttpLogRecordExporterTestPeer, ExportJsonGoldenBody) +{ + auto mock_otlp_client = + OtlpHttpLogRecordExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); + auto exporter = GetExporter(std::unique_ptr{mock_otlp_client.first}); + + auto no_send_client = + std::static_pointer_cast(mock_otlp_client.second); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + + std::string captured_body; + CaptureRequestBody(mock_session, captured_body); + + auto recordable = exporter->MakeRecordable(); + + const uint8_t trace_id_bytes[opentelemetry::trace::TraceId::kSize] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; + const uint8_t span_id_bytes[opentelemetry::trace::SpanId::kSize] = {0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18}; + + recordable->SetTimestamp( + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000))); + recordable->SetObservedTimestamp( + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000500000))); + recordable->SetSeverity(opentelemetry::logs::Severity::kError); + recordable->SetBody("golden log body"); + recordable->SetEventId(42, "golden_event"); + recordable->SetTraceId(opentelemetry::trace::TraceId(trace_id_bytes)); + recordable->SetSpanId(opentelemetry::trace::SpanId(span_id_bytes)); + recordable->SetTraceFlags( + opentelemetry::trace::TraceFlags(opentelemetry::trace::TraceFlags::kIsSampled)); + recordable->SetAttribute("bool_attr", true); + recordable->SetAttribute("int64_attr", static_cast(-42)); + recordable->SetAttribute("string_attr", "golden value"); + recordable->SetResource(opentelemetry::sdk::resource::Resource::GetEmpty()); + + auto instrumentation_scope = + opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create( + "golden_scope", "golden_scope_version"); + recordable->SetInstrumentationScope(*instrumentation_scope); + + nostd::span> batch(&recordable, 1); + EXPECT_EQ(opentelemetry::sdk::common::ExportResult::kSuccess, exporter->Export(batch)); + + static constexpr char kExpectedBody[] = + R"({"resourceLogs":[{"resource":null,"scopeLogs":[{"logRecords":[{"attributes":[{"key":"bool_attr","value":{"boolValue":true}},{"key":"int64_attr","value":{"intValue":"-42"}},{"key":"string_attr","value":{"stringValue":"golden value"}}],"body":{"stringValue":"golden log body"},"eventName":"golden_event","flags":1,"observedTimeUnixNano":"1000500000","severityNumber":17,"severityText":"ERROR","spanId":"1112131415161718","timeUnixNano":"1000000000","traceId":"0102030405060708090a0b0c0d0e0f10"}],"scope":{"name":"golden_scope","version":"golden_scope_version"}}]}]})"; + + ASSERT_EQ(std::string(kExpectedBody), captured_body); +} + // Exporter logs the rejection on partial_success when the response is JSON encoded. TEST_F(OtlpHttpLogRecordExporterTestPeer, ExportPartialSuccessJson) { diff --git a/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc new file mode 100644 index 0000000000..fcf0e3bfca --- /dev/null +++ b/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc @@ -0,0 +1,47 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_http_metric_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_http_metric_exporter_options.h" +#include "opentelemetry/exporters/otlp/otlp_http_metric_exporter_runtime_options.h" +#include "opentelemetry/sdk/metrics/push_metric_exporter.h" +#include "opentelemetry/version.h" + +#include "opentelemetry/test_common/ext/http/client/http_client_test_factory.h" + +#include "otlp_stub_json_writer.h" + +namespace http_client = opentelemetry::ext::http::client; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +TEST(OtlpHttpMetricExporterCustomJsonWriterTest, RuntimeOptionsInjectionCreatesExporter) +{ + OtlpHttpMetricExporterOptions opts; + OtlpHttpMetricExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + auto exporter = OtlpHttpMetricExporterFactory::Create(opts, runtime_opts); + ASSERT_NE(exporter, nullptr); +} + +TEST(OtlpHttpMetricExporterCustomJsonWriterTest, RuntimeOptionsWithHttpClientCreatesExporter) +{ + OtlpHttpMetricExporterOptions opts; + OtlpHttpMetricExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + auto client = http_client::HttpClientTestFactory::Create(); + auto exporter = OtlpHttpMetricExporterFactory::Create(opts, runtime_opts, std::move(client)); + ASSERT_NE(exporter, nullptr); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_http_metric_exporter_test.cc b/exporters/otlp/test/otlp_http_metric_exporter_test.cc index 787c982bf0..e776426bec 100644 --- a/exporters/otlp/test/otlp_http_metric_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_metric_exporter_test.cc @@ -38,6 +38,7 @@ #include "opentelemetry/test_common/ext/http/client/nosend/http_client_nosend.h" #include "opentelemetry/test_common/sdk/common/scoped_test_log_handler.h" #include "opentelemetry/version.h" +#include "otlp_http_request_body_capture.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep @@ -1322,6 +1323,90 @@ TEST_F(OtlpHttpMetricExporterTestPeer, ExportPartialSuccess) EXPECT_TRUE(contains("too many data points!!")); } +TEST_F(OtlpHttpMetricExporterTestPeer, ExportJsonGoldenBody) +{ + auto mock_otlp_client = + OtlpHttpMetricExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); + auto exporter = GetExporter(std::unique_ptr{mock_otlp_client.first}); + + auto no_send_client = + std::static_pointer_cast(mock_otlp_client.second); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + + opentelemetry::sdk::metrics::SumPointData sum_point_data{}; + sum_point_data.value_ = static_cast(7); + sum_point_data.is_monotonic_ = true; + + opentelemetry::sdk::metrics::LastValuePointData last_value_point_data{}; + last_value_point_data.value_ = 3.5; + last_value_point_data.is_lastvalue_valid_ = true; + last_value_point_data.sample_ts_ = + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)); + + opentelemetry::sdk::metrics::HistogramPointData histogram_point_data{}; + histogram_point_data.boundaries_ = {1.0, 5.0}; + histogram_point_data.count_ = 2; + histogram_point_data.counts_ = {1, 1}; + histogram_point_data.sum_ = 6.0; + histogram_point_data.min_ = 2.0; + histogram_point_data.max_ = 4.0; + + opentelemetry::sdk::metrics::MetricData sum_metric_data{ + opentelemetry::sdk::metrics::InstrumentDescriptor{ + "golden_counter", "golden counter description", "1", + opentelemetry::sdk::metrics::InstrumentType::kCounter, + opentelemetry::sdk::metrics::InstrumentValueType::kLong}, + opentelemetry::sdk::metrics::AggregationTemporality::kCumulative, + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000)), + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)), + std::vector{ + {opentelemetry::sdk::metrics::PointAttributes{{"sum_key", "sum_value"}}, + sum_point_data}}}; + + opentelemetry::sdk::metrics::MetricData gauge_metric_data{ + opentelemetry::sdk::metrics::InstrumentDescriptor{ + "golden_gauge", "golden gauge description", "1", + opentelemetry::sdk::metrics::InstrumentType::kObservableGauge, + opentelemetry::sdk::metrics::InstrumentValueType::kDouble}, + opentelemetry::sdk::metrics::AggregationTemporality::kDelta, + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000)), + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)), + std::vector{ + {opentelemetry::sdk::metrics::PointAttributes{{"gauge_key", "gauge_value"}}, + last_value_point_data}}}; + + opentelemetry::sdk::metrics::MetricData histogram_metric_data{ + opentelemetry::sdk::metrics::InstrumentDescriptor{ + "golden_histogram", "golden histogram description", "1", + opentelemetry::sdk::metrics::InstrumentType::kHistogram, + opentelemetry::sdk::metrics::InstrumentValueType::kDouble}, + opentelemetry::sdk::metrics::AggregationTemporality::kCumulative, + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(1000000000)), + opentelemetry::common::SystemTimestamp(std::chrono::nanoseconds(2000000000)), + std::vector{ + {opentelemetry::sdk::metrics::PointAttributes{{"histogram_key", "histogram_value"}}, + histogram_point_data}}}; + + opentelemetry::sdk::metrics::ResourceMetrics data; + data.resource_ = &opentelemetry::sdk::resource::Resource::GetEmpty(); + auto scope = opentelemetry::sdk::instrumentationscope::InstrumentationScope::Create( + "golden_scope", "golden_scope_version"); + data.scope_metric_data_ = std::vector{ + {scope.get(), std::vector{ + sum_metric_data, gauge_metric_data, histogram_metric_data}}}; + + std::string captured_body; + CaptureRequestBody(mock_session, captured_body); + + EXPECT_EQ(opentelemetry::sdk::common::ExportResult::kSuccess, exporter->Export(data)); + + static constexpr char kExpectedBody[] = + R"({"resourceMetrics":[{"resource":null,"scopeMetrics":[{"metrics":[{"description":"golden counter description","name":"golden_counter","sum":{"aggregationTemporality":2,"dataPoints":[{"asInt":"7","attributes":[{"key":"sum_key","value":{"stringValue":"sum_value"}}],"startTimeUnixNano":"1000000000","timeUnixNano":"2000000000"}],"isMonotonic":true},"unit":"1"},{"description":"golden gauge description","gauge":{"dataPoints":[{"asDouble":3.5,"attributes":[{"key":"gauge_key","value":{"stringValue":"gauge_value"}}],"startTimeUnixNano":"1000000000","timeUnixNano":"2000000000"}]},"name":"golden_gauge","unit":"1"},{"description":"golden histogram description","histogram":{"aggregationTemporality":2,"dataPoints":[{"attributes":[{"key":"histogram_key","value":{"stringValue":"histogram_value"}}],"bucketCounts":["1","1"],"count":"2","explicitBounds":[1.0,5.0],"max":4.0,"min":2.0,"startTimeUnixNano":"1000000000","sum":6.0,"timeUnixNano":"2000000000"}]},"name":"golden_histogram","unit":"1"}],"scope":{"name":"golden_scope","version":"golden_scope_version"}}]}]})"; + + ASSERT_EQ(std::string(kExpectedBody), captured_body); +} + // Exporter logs the rejection on partial_success when the response is JSON encoded. TEST_F(OtlpHttpMetricExporterTestPeer, ExportPartialSuccessJson) { diff --git a/exporters/otlp/test/otlp_http_request_body_capture.h b/exporters/otlp/test/otlp_http_request_body_capture.h new file mode 100644 index 0000000000..524a5a7890 --- /dev/null +++ b/exporters/otlp/test/otlp_http_request_body_capture.h @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include + +#include "opentelemetry/ext/http/client/http_client.h" +#include "opentelemetry/test_common/ext/http/client/nosend/http_client_nosend.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace +{ + +// Captures the body of the one request the session is asked to send, and +// completes it. The session must outlive the export under test. +inline void CaptureRequestBody(const std::shared_ptr &session, + std::string &captured_body) +{ + auto *raw_session = session.get(); + EXPECT_CALL(*session, SendRequest) + .WillOnce([&captured_body, + raw_session](const std::shared_ptr &callback) { + const auto &body = raw_session->GetRequest()->body_; + captured_body.assign(body.begin(), body.end()); + ext::http::client::nosend::Response response; + response.Finish(*callback.get()); + }); +} + +} // namespace + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc b/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc new file mode 100644 index 0000000000..339845fbaf --- /dev/null +++ b/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc @@ -0,0 +1,223 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ +namespace +{ + +std::unique_ptr MakeWriter() +{ + return detail::GetDefaultJsonWriterFactory()->Create(); +} + +} // namespace + +TEST(NlohmannJsonWriter, FactoryReturnsAWorkingWriter) +{ + auto writer = MakeWriter(); + ASSERT_NE(writer, nullptr); + EXPECT_TRUE(writer->ok()); +} + +struct ScalarCase +{ + const char *name; + std::function write; + const char *expected; +}; + +void PrintTo(const ScalarCase &scalar_case, std::ostream *os) +{ + *os << scalar_case.name; +} + +class NlohmannJsonWriterScalar : public ::testing::TestWithParam +{}; + +TEST_P(NlohmannJsonWriterScalar, WritesTopLevelScalar) +{ + auto writer = MakeWriter(); + GetParam().write(*writer); + EXPECT_TRUE(writer->ok()); + EXPECT_EQ(writer->ToString(), GetParam().expected); +} + +INSTANTIATE_TEST_SUITE_P( + Scalars, + NlohmannJsonWriterScalar, + ::testing::Values( + ScalarCase{"Null", [](JsonWriter &w) { w.WriteNull(); }, "null"}, + ScalarCase{"String", [](JsonWriter &w) { w.WriteString("hello"); }, "\"hello\""}, + ScalarCase{"Int32", [](JsonWriter &w) { w.WriteInt32(-7); }, "-7"}, + ScalarCase{"Int64", [](JsonWriter &w) { w.WriteInt64(-7000000000LL); }, "-7000000000"}, + ScalarCase{"UInt32", [](JsonWriter &w) { w.WriteUInt32(42u); }, "42"}, + ScalarCase{"UInt64", [](JsonWriter &w) { w.WriteUInt64(42000000000ULL); }, "42000000000"}, + ScalarCase{"Double", [](JsonWriter &w) { w.WriteDouble(3.5); }, "3.5"}, + ScalarCase{"Bool", [](JsonWriter &w) { w.WriteBool(true); }, "true"}, + // Base64 of 00 01 02 FF + ScalarCase{"Bytes", + [](JsonWriter &w) { + const std::uint8_t bytes[] = {0, 1, 2, 255}; + w.WriteBytes(bytes, sizeof(bytes)); + }, + "\"AAEC/w==\""}), + [](const ::testing::TestParamInfo &info) { return info.param.name; }); + +// The extremes of both 64-bit types are outside the range a double represents +// exactly. +TEST(NlohmannJsonWriter, PreservesFull64BitRangeExactly) +{ + { + auto writer = MakeWriter(); + writer->WriteInt64(-9223372036854775807LL - 1); + EXPECT_EQ(writer->ToString(), "-9223372036854775808"); + } + { + auto writer = MakeWriter(); + writer->WriteInt64(9223372036854775807LL); + EXPECT_EQ(writer->ToString(), "9223372036854775807"); + } + { + auto writer = MakeWriter(); + writer->WriteUInt64(18446744073709551615ULL); + EXPECT_EQ(writer->ToString(), "18446744073709551615"); + } +} + +TEST(NlohmannJsonWriter, WritesAnEmptyObject) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->EndObject(); + EXPECT_TRUE(writer->ok()); + EXPECT_EQ(writer->ToString(), "{}"); +} + +TEST(NlohmannJsonWriter, WritesAnEmptyArray) +{ + auto writer = MakeWriter(); + writer->BeginArray(); + writer->EndArray(); + EXPECT_TRUE(writer->ok()); + EXPECT_EQ(writer->ToString(), "[]"); +} + +TEST(NlohmannJsonWriter, WritesAFlatObject) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key("a"); + writer->WriteInt32(1); + writer->Key("b"); + writer->WriteString("two"); + writer->EndObject(); + EXPECT_TRUE(writer->ok()); + EXPECT_EQ(writer->ToString(), R"({"a":1,"b":"two"})"); +} + +TEST(NlohmannJsonWriter, WritesAFlatArray) +{ + auto writer = MakeWriter(); + writer->BeginArray(); + writer->WriteInt32(1); + writer->WriteInt32(2); + writer->WriteInt32(3); + writer->EndArray(); + EXPECT_TRUE(writer->ok()); + EXPECT_EQ(writer->ToString(), "[1,2,3]"); +} + +TEST(NlohmannJsonWriter, WritesNestedObjectsAndArrays) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key("items"); + writer->BeginArray(); + writer->BeginObject(); + writer->Key("id"); + writer->WriteInt32(1); + writer->EndObject(); + writer->BeginObject(); + writer->Key("id"); + writer->WriteInt32(2); + writer->EndObject(); + writer->EndArray(); + writer->EndObject(); + EXPECT_TRUE(writer->ok()); + EXPECT_EQ(writer->ToString(), R"({"items":[{"id":1},{"id":2}]})"); +} + +TEST(NlohmannJsonWriter, KeyOutsideObjectFailsStickily) +{ + auto writer = MakeWriter(); + writer->BeginArray(); + writer->Key("bad"); + EXPECT_FALSE(writer->ok()); + + // Once failed, ok() stays false even if later calls look well-formed. + writer->WriteInt32(1); + writer->EndArray(); + EXPECT_FALSE(writer->ok()); +} + +TEST(NlohmannJsonWriter, ValueWithoutPrecedingKeyFails) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->WriteInt32(1); + EXPECT_FALSE(writer->ok()); +} + +TEST(NlohmannJsonWriter, MismatchedEndFails) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->EndArray(); + EXPECT_FALSE(writer->ok()); +} + +TEST(NlohmannJsonWriter, EndWithoutBeginFails) +{ + auto writer = MakeWriter(); + writer->EndObject(); + EXPECT_FALSE(writer->ok()); +} + +TEST(NlohmannJsonWriter, SecondTopLevelValueFails) +{ + auto writer = MakeWriter(); + writer->WriteInt32(1); + EXPECT_TRUE(writer->ok()); + writer->WriteInt32(2); + EXPECT_FALSE(writer->ok()); +} + +TEST(NlohmannJsonWriter, MisuseDoesNotCrashToString) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->EndArray(); + EXPECT_FALSE(writer->ok()); + // Must not throw or crash even though the writer failed. + writer->ToString(); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_marking_json_writer.h b/exporters/otlp/test/otlp_marking_json_writer.h new file mode 100644 index 0000000000..52e5a64e2c --- /dev/null +++ b/exporters/otlp/test/otlp_marking_json_writer.h @@ -0,0 +1,67 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace +{ + +// Wraps the default writer and marks its output, so a test can tell the +// injected writer ran by looking at the emitted bytes alone. +class MarkingJsonWriter : public JsonWriter +{ +public: + MarkingJsonWriter() : inner_(detail::GetDefaultJsonWriterFactory()->Create()) {} + + void BeginObject() noexcept override { inner_->BeginObject(); } + void EndObject() noexcept override { inner_->EndObject(); } + void BeginArray() noexcept override { inner_->BeginArray(); } + void EndArray() noexcept override { inner_->EndArray(); } + void Key(nostd::string_view key) noexcept override { inner_->Key(key); } + void WriteNull() noexcept override { inner_->WriteNull(); } + void WriteString(nostd::string_view value) noexcept override { inner_->WriteString(value); } + void WriteInt32(std::int32_t value) noexcept override { inner_->WriteInt32(value); } + void WriteInt64(std::int64_t value) noexcept override { inner_->WriteInt64(value); } + void WriteUInt32(std::uint32_t value) noexcept override { inner_->WriteUInt32(value); } + void WriteUInt64(std::uint64_t value) noexcept override { inner_->WriteUInt64(value); } + void WriteDouble(double value) noexcept override { inner_->WriteDouble(value); } + void WriteBool(bool value) noexcept override { inner_->WriteBool(value); } + void WriteBytes(const std::uint8_t *data, std::size_t size) noexcept override + { + inner_->WriteBytes(data, size); + } + bool ok() const noexcept override { return inner_->ok(); } + std::string ToString() noexcept override { return "/*custom-writer*/" + inner_->ToString(); } + +private: + std::unique_ptr inner_; +}; + +class MarkingJsonWriterFactory : public JsonWriterFactory +{ +public: + std::unique_ptr Create() override { return std::make_unique(); } +}; + +} // namespace + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_stub_json_writer.h b/exporters/otlp/test/otlp_stub_json_writer.h new file mode 100644 index 0000000000..6ba5e31ee0 --- /dev/null +++ b/exporters/otlp/test/otlp_stub_json_writer.h @@ -0,0 +1,58 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace +{ + +// A writer distinct from the nlohmann default, just enough to show that +// injection reaches the constructor. +class StubJsonWriter : public JsonWriter +{ +public: + void BeginObject() noexcept override {} + void EndObject() noexcept override {} + void BeginArray() noexcept override {} + void EndArray() noexcept override {} + void Key(nostd::string_view) noexcept override {} + void WriteNull() noexcept override {} + void WriteString(nostd::string_view) noexcept override {} + void WriteInt32(std::int32_t) noexcept override {} + void WriteInt64(std::int64_t) noexcept override {} + void WriteUInt32(std::uint32_t) noexcept override {} + void WriteUInt64(std::uint64_t) noexcept override {} + void WriteDouble(double) noexcept override {} + void WriteBool(bool) noexcept override {} + void WriteBytes(const std::uint8_t *, std::size_t) noexcept override {} + bool ok() const noexcept override { return true; } + std::string ToString() noexcept override { return "{}"; } +}; + +class StubJsonWriterFactory : public JsonWriterFactory +{ +public: + std::unique_ptr Create() override { return std::make_unique(); } +}; + +} // namespace + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE From 7937ce26eba741463c52a04e8ed8421730278ea9 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Thu, 10 Sep 2026 22:01:20 +0000 Subject: [PATCH 02/20] Fix Bazel build --- exporters/otlp/src/otlp_json_converter.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/exporters/otlp/src/otlp_json_converter.cc b/exporters/otlp/src/otlp_json_converter.cc index ff869aaef1..636772fd32 100644 --- a/exporters/otlp/src/otlp_json_converter.cc +++ b/exporters/otlp/src/otlp_json_converter.cc @@ -319,8 +319,9 @@ void ConvertGenericMessageToJson(JsonWriter &writer, for (std::size_t i = 0; i < fields_with_data.size(); ++i) { const google::protobuf::FieldDescriptor *field_descriptor = fields_with_data[i]; - writer.Key(options.use_json_name ? field_descriptor->json_name() - : field_descriptor->camelcase_name()); + const auto &field_name = + options.use_json_name ? field_descriptor->json_name() : field_descriptor->camelcase_name(); + writer.Key(nostd::string_view(field_name.data(), field_name.size())); if (field_descriptor->is_repeated()) { ConvertListFieldToJson(writer, message, field_descriptor, options); From dd3964b842d823ed6e6043420f0b905d917b525f Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Fri, 11 Sep 2026 05:58:11 +0000 Subject: [PATCH 03/20] Fix CI --- exporters/otlp/BUILD | 15 +++- .../exporters/otlp/otlp_json_converter.h | 12 +-- exporters/otlp/src/otlp_file_client.cc | 22 ++--- exporters/otlp/src/otlp_http_client.cc | 8 -- exporters/otlp/src/otlp_http_exporter.cc | 1 - .../otlp/src/otlp_http_log_record_exporter.cc | 1 - .../otlp/src/otlp_http_metric_exporter.cc | 1 - exporters/otlp/src/otlp_json_converter.cc | 5 +- .../otlp/src/otlp_json_writer_nlohmann.cc | 90 ++++++++++++++----- exporters/otlp/test/otlp_file_client_test.cc | 5 +- .../test/otlp_file_exporter_factory_test.cc | 3 +- .../otlp/test/otlp_file_exporter_test.cc | 1 + ...p_file_log_record_exporter_factory_test.cc | 3 +- .../otlp_file_log_record_exporter_test.cc | 1 + .../otlp_file_metric_exporter_factory_test.cc | 3 +- .../test/otlp_file_metric_exporter_test.cc | 1 + ...p_http_exporter_custom_json_writer_test.cc | 25 +++--- .../otlp/test/otlp_http_exporter_test.cc | 3 +- ...record_exporter_custom_json_writer_test.cc | 5 +- .../otlp_http_log_record_exporter_test.cc | 3 +- ...metric_exporter_custom_json_writer_test.cc | 5 +- .../test/otlp_http_metric_exporter_test.cc | 2 +- .../test/otlp_http_request_body_capture.h | 4 +- .../test/otlp_json_writer_nlohmann_test.cc | 6 ++ .../otlp/test/otlp_marking_json_writer.h | 4 +- exporters/otlp/test/otlp_stub_json_writer.h | 4 +- 26 files changed, 144 insertions(+), 89 deletions(-) diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index e42eaa69be..386d056c32 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -932,7 +932,10 @@ cc_test( cc_test( name = "otlp_file_exporter_factory_test", - srcs = ["test/otlp_file_exporter_factory_test.cc"], + srcs = [ + "test/otlp_file_exporter_factory_test.cc", + "test/otlp_stub_json_writer.h", + ], tags = [ "otlp", "otlp_file", @@ -1001,7 +1004,10 @@ cc_test( cc_test( name = "otlp_file_log_record_exporter_factory_test", - srcs = ["test/otlp_file_log_record_exporter_factory_test.cc"], + srcs = [ + "test/otlp_file_log_record_exporter_factory_test.cc", + "test/otlp_stub_json_writer.h", + ], tags = [ "otlp", "otlp_file_log", @@ -1135,7 +1141,10 @@ cc_test( cc_test( name = "otlp_file_metric_exporter_factory_test", - srcs = ["test/otlp_file_metric_exporter_factory_test.cc"], + srcs = [ + "test/otlp_file_metric_exporter_factory_test.cc", + "test/otlp_stub_json_writer.h", + ], tags = [ "otlp", "otlp_file_metric", diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h index d38de7f81f..943dca8780 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h @@ -7,11 +7,13 @@ #include "opentelemetry/exporters/otlp/otlp_json_writer.h" #include "opentelemetry/version.h" -// clang-format off -#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep -#include -#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep -// clang-format on +namespace google +{ +namespace protobuf +{ +class Message; +} // namespace protobuf +} // namespace google OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter diff --git a/exporters/otlp/src/otlp_file_client.cc b/exporters/otlp/src/otlp_file_client.cc index 86264d233a..eb80ba52d5 100644 --- a/exporters/otlp/src/otlp_file_client.cc +++ b/exporters/otlp/src/otlp_file_client.cc @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -20,12 +19,6 @@ // IWYU pragma: no_include -#if defined(HAVE_GSL) -# include -#else -# include -#endif - #ifdef _MSC_VER # define strcasecmp _stricmp #else @@ -116,23 +109,24 @@ #include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/version.h" -// clang-format off -#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep -#include "google/protobuf/message.h" -#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep -// clang-format on - // Must be included after opentelemetry/version.h, // which exports opentelemetry/common/macros.h #if OPENTELEMETRY_HAVE_EXCEPTIONS # include -# include #endif #ifdef ENABLE_THREAD_INSTRUMENTATION_PREVIEW # include "opentelemetry/sdk/common/thread_instrumentation.h" #endif /* ENABLE_THREAD_INSTRUMENTATION_PREVIEW */ +namespace google +{ +namespace protobuf +{ +class Message; +} // namespace protobuf +} // namespace google + OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { diff --git a/exporters/otlp/src/otlp_http_client.cc b/exporters/otlp/src/otlp_http_client.cc index dcedee7d56..b49d50d001 100644 --- a/exporters/otlp/src/otlp_http_client.cc +++ b/exporters/otlp/src/otlp_http_client.cc @@ -15,13 +15,6 @@ #include #include #include -#include - -#if defined(HAVE_GSL) -# include -#else -# include -#endif #include "opentelemetry/common/timestamp.h" #include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" @@ -48,7 +41,6 @@ #include #include #include -#include // IWYU pragma: no_include // IWYU pragma: no_include // IWYU pragma: no_include diff --git a/exporters/otlp/src/otlp_http_exporter.cc b/exporters/otlp/src/otlp_http_exporter.cc index 34a7d89c3f..ca8da0f2d4 100644 --- a/exporters/otlp/src/otlp_http_exporter.cc +++ b/exporters/otlp/src/otlp_http_exporter.cc @@ -21,7 +21,6 @@ #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" -#include "opentelemetry/sdk/common/thread_instrumentation.h" #include "opentelemetry/version.h" // clang-format off diff --git a/exporters/otlp/src/otlp_http_log_record_exporter.cc b/exporters/otlp/src/otlp_http_log_record_exporter.cc index 6a646893de..81058182a8 100644 --- a/exporters/otlp/src/otlp_http_log_record_exporter.cc +++ b/exporters/otlp/src/otlp_http_log_record_exporter.cc @@ -21,7 +21,6 @@ #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" -#include "opentelemetry/sdk/common/thread_instrumentation.h" #include "opentelemetry/version.h" // clang-format off diff --git a/exporters/otlp/src/otlp_http_metric_exporter.cc b/exporters/otlp/src/otlp_http_metric_exporter.cc index a36c1bc61f..fd1cb37a3c 100644 --- a/exporters/otlp/src/otlp_http_metric_exporter.cc +++ b/exporters/otlp/src/otlp_http_metric_exporter.cc @@ -20,7 +20,6 @@ #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" -#include "opentelemetry/sdk/common/thread_instrumentation.h" #include "opentelemetry/sdk/metrics/export/metric_producer.h" #include "opentelemetry/version.h" diff --git a/exporters/otlp/src/otlp_json_converter.cc b/exporters/otlp/src/otlp_json_converter.cc index 636772fd32..8510bab986 100644 --- a/exporters/otlp/src/otlp_json_converter.cc +++ b/exporters/otlp/src/otlp_json_converter.cc @@ -14,13 +14,14 @@ # include #endif -#include "opentelemetry/sdk/common/base64.h" +#include "opentelemetry/nostd/string_view.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep // clang-format on #include #include +#include // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep // clang-format on @@ -299,7 +300,6 @@ void ConvertListFieldToJson(JsonWriter &writer, } } // namespace -// NOLINTEND(misc-no-recursion) void ConvertGenericMessageToJson(JsonWriter &writer, const google::protobuf::Message &message, @@ -333,6 +333,7 @@ void ConvertGenericMessageToJson(JsonWriter &writer, } writer.EndObject(); } +// NOLINTEND(misc-no-recursion) } // namespace otlp } // namespace exporter diff --git a/exporters/otlp/src/otlp_json_writer_nlohmann.cc b/exporters/otlp/src/otlp_json_writer_nlohmann.cc index 4cf68c01a7..9a37e07796 100644 --- a/exporters/otlp/src/otlp_json_writer_nlohmann.cc +++ b/exporters/otlp/src/otlp_json_writer_nlohmann.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -14,6 +15,13 @@ #include "opentelemetry/nostd/string_view.h" #include "opentelemetry/sdk/common/base64.h" #include "opentelemetry/sdk/common/global_log_handler.h" +#include "opentelemetry/version.h" + +// Must be included after opentelemetry/version.h, +// which exports opentelemetry/common/macros.h +#if OPENTELEMETRY_HAVE_EXCEPTIONS +# include +#endif OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter @@ -27,14 +35,11 @@ namespace class NlohmannJsonWriter final : public JsonWriter { public: - void BeginObject() noexcept override - { - OpenContainer(nlohmann::json::object(), Container::kObject); - } + void BeginObject() noexcept override { OpenContainer(Container::kObject); } void EndObject() noexcept override { CloseContainer(Container::kObject); } - void BeginArray() noexcept override { OpenContainer(nlohmann::json::array(), Container::kArray); } + void BeginArray() noexcept override { OpenContainer(Container::kArray); } void EndArray() noexcept override { CloseContainer(Container::kArray); } @@ -93,11 +98,27 @@ class NlohmannJsonWriter final : public JsonWriter std::string ToString() noexcept override { - return root_.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); +#if OPENTELEMETRY_HAVE_EXCEPTIONS + try + { +#endif + return root_.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); +#if OPENTELEMETRY_HAVE_EXCEPTIONS + } + catch (const std::exception &e) + { + Fail(e.what()); + } + catch (...) + { + Fail("unknown exception"); + } + return std::string(); +#endif } private: - enum class Container + enum class Container : std::uint8_t { kObject, kArray, @@ -140,32 +161,61 @@ class NlohmannJsonWriter final : public JsonWriter } Frame &frame = stack_.back(); - if (frame.kind == Container::kArray) + if (frame.kind == Container::kObject && !has_pending_key_) { - frame.node->push_back(nlohmann::json()); - return &frame.node->back(); + Fail("a value was written without a preceding Key()"); + return nullptr; } - // kObject - if (!has_pending_key_) +#if OPENTELEMETRY_HAVE_EXCEPTIONS + try { - Fail("a value was written without a preceding Key()"); - return nullptr; +#endif + if (frame.kind == Container::kArray) + { + frame.node->push_back(nlohmann::json()); + return &frame.node->back(); + } + has_pending_key_ = false; + return &(*frame.node)[pending_key_]; +#if OPENTELEMETRY_HAVE_EXCEPTIONS + } + catch (const std::exception &e) + { + Fail(e.what()); + } + catch (...) + { + Fail("unknown exception"); } - has_pending_key_ = false; - nlohmann::json &slot = (*frame.node)[pending_key_]; - return &slot; + return nullptr; +#endif } - void OpenContainer(nlohmann::json &&empty_container, Container kind) noexcept + void OpenContainer(Container kind) noexcept { nlohmann::json *slot = ClaimSlot(); if (!slot) { return; } - *slot = std::move(empty_container); - stack_.push_back(Frame{slot, kind}); +#if OPENTELEMETRY_HAVE_EXCEPTIONS + try + { +#endif + *slot = kind == Container::kObject ? nlohmann::json::object() : nlohmann::json::array(); + stack_.push_back(Frame{slot, kind}); +#if OPENTELEMETRY_HAVE_EXCEPTIONS + } + catch (const std::exception &e) + { + Fail(e.what()); + } + catch (...) + { + Fail("unknown exception"); + } +#endif } void CloseContainer(Container kind) noexcept diff --git a/exporters/otlp/test/otlp_file_client_test.cc b/exporters/otlp/test/otlp_file_client_test.cc index 35170ea41f..b96bcac398 100644 --- a/exporters/otlp/test/otlp_file_client_test.cc +++ b/exporters/otlp/test/otlp_file_client_test.cc @@ -20,12 +20,9 @@ // IWYU pragma: no_include #include "opentelemetry/common/timestamp.h" -#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_client.h" #include "opentelemetry/exporters/otlp/otlp_file_client_options.h" #include "opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h" -#include "opentelemetry/exporters/otlp/otlp_json_writer.h" -#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/exporters/otlp/otlp_recordable.h" #include "opentelemetry/exporters/otlp/otlp_recordable_utils.h" #include "opentelemetry/nostd/span.h" @@ -184,7 +181,7 @@ TEST(OtlpFileClientTest, CustomJsonWriterFactoryIsUsed) opentelemetry::exporter::otlp::OtlpFileClientRuntimeOptions rt_opts; opts.backend_options = std::ref(output_stream); - rt_opts.json_writer_factory = std::make_shared(); + rt_opts.json_writer_factory = std::make_shared(); auto client = std::make_unique(std::move(opts), std::move(rt_opts)); diff --git a/exporters/otlp/test/otlp_file_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_exporter_factory_test.cc index da8be33989..c3572489c7 100644 --- a/exporters/otlp/test/otlp_file_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_exporter_factory_test.cc @@ -7,6 +7,7 @@ #include "opentelemetry/exporters/otlp/otlp_file_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_exporter_options.h" +#include "opentelemetry/exporters/otlp/otlp_file_exporter_runtime_options.h" #include "opentelemetry/sdk/trace/exporter.h" #include "opentelemetry/version.h" @@ -47,7 +48,7 @@ TEST(OtlpFileExporterFactoryTest, BuildWithJsonWriterFactoryTest) { OtlpFileExporterOptions opts; OtlpFileExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto exporter = OtlpFileExporterFactory::Create(opts, runtime_opts); diff --git a/exporters/otlp/test/otlp_file_exporter_test.cc b/exporters/otlp/test/otlp_file_exporter_test.cc index 6dfc1dafa5..7545489495 100644 --- a/exporters/otlp/test/otlp_file_exporter_test.cc +++ b/exporters/otlp/test/otlp_file_exporter_test.cc @@ -21,6 +21,7 @@ #include "opentelemetry/exporters/otlp/otlp_file_exporter_options.h" #include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/nostd/span.h" +#include "opentelemetry/nostd/utility.h" // IWYU pragma: keep #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/instrumentationscope/instrumentation_scope.h" diff --git a/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc index 399f5e790f..9d42ec77b7 100644 --- a/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc @@ -7,6 +7,7 @@ #include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_options.h" +#include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_runtime_options.h" #include "opentelemetry/sdk/logs/exporter.h" #include "opentelemetry/version.h" @@ -47,7 +48,7 @@ TEST(OtlpFileLogRecordExporterFactoryTest, BuildWithJsonWriterFactoryTest) { OtlpFileLogRecordExporterOptions opts; OtlpFileLogRecordExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto exporter = OtlpFileLogRecordExporterFactory::Create(opts, runtime_opts); diff --git a/exporters/otlp/test/otlp_file_log_record_exporter_test.cc b/exporters/otlp/test/otlp_file_log_record_exporter_test.cc index 7fd6fa5ded..dcf07646e2 100644 --- a/exporters/otlp/test/otlp_file_log_record_exporter_test.cc +++ b/exporters/otlp/test/otlp_file_log_record_exporter_test.cc @@ -12,6 +12,7 @@ #include #include "opentelemetry/common/key_value_iterable_view.h" +#include "opentelemetry/common/timestamp.h" #include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter.h" #include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_options.h" diff --git a/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc index 2bba8cdd3b..c429699617 100644 --- a/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc @@ -7,6 +7,7 @@ #include "opentelemetry/exporters/otlp/otlp_file_metric_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_metric_exporter_options.h" +#include "opentelemetry/exporters/otlp/otlp_file_metric_exporter_runtime_options.h" #include "opentelemetry/sdk/metrics/push_metric_exporter.h" #include "opentelemetry/version.h" @@ -47,7 +48,7 @@ TEST(OtlpFileMetricExporterFactory, BuildWithJsonWriterFactoryTest) { OtlpFileMetricExporterOptions opts; OtlpFileMetricExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto exporter = OtlpFileMetricExporterFactory::Create(opts, runtime_opts); diff --git a/exporters/otlp/test/otlp_file_metric_exporter_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_test.cc index 2270cef142..2a6fc963b6 100644 --- a/exporters/otlp/test/otlp_file_metric_exporter_test.cc +++ b/exporters/otlp/test/otlp_file_metric_exporter_test.cc @@ -4,6 +4,7 @@ #include // IWYU pragma: keep #include // IWYU pragma: keep #include +#include #include #include #include diff --git a/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc index cc77a7c194..3ae3e4e6e6 100644 --- a/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc @@ -3,35 +3,27 @@ #include #include -#include -#include -#include #include #include #include "gmock/gmock.h" -#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" -#include "opentelemetry/exporters/otlp/otlp_http_client.h" -#include "opentelemetry/exporters/otlp/otlp_http_exporter.h" +#include "opentelemetry/exporters/otlp/otlp_http.h" #include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h" #include "opentelemetry/exporters/otlp/otlp_http_exporter_runtime_options.h" -#include "opentelemetry/exporters/otlp/otlp_json_writer.h" -#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" #include "opentelemetry/ext/http/client/http_client.h" -#include "opentelemetry/nostd/string_view.h" -#include "opentelemetry/sdk/common/exporter_utils.h" +#include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/sdk/trace/batch_span_processor.h" #include "opentelemetry/sdk/trace/batch_span_processor_options.h" #include "opentelemetry/sdk/trace/exporter.h" #include "opentelemetry/sdk/trace/processor.h" #include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/trace/span.h" #include "opentelemetry/trace/tracer.h" #include "opentelemetry/version.h" #include "otlp_marking_json_writer.h" #include "opentelemetry/test_common/ext/http/client/http_client_test_factory.h" -#include "opentelemetry/test_common/ext/http/client/nosend/http_client_factory_nosend.h" #include "opentelemetry/test_common/ext/http/client/nosend/http_client_nosend.h" using namespace testing; @@ -44,14 +36,19 @@ namespace otlp namespace http_client = opentelemetry::ext::http::client; +namespace +{ + class OtlpHttpExporterCustomJsonWriterTestPeer : public ::testing::Test {}; +} // namespace + TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, RuntimeOptionsInjectionCreatesExporter) { OtlpHttpExporterOptions opts; OtlpHttpExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto exporter = OtlpHttpExporterFactory::Create(opts, runtime_opts); ASSERT_NE(exporter, nullptr); } @@ -60,7 +57,7 @@ TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, RuntimeOptionsWithHttpClientCre { OtlpHttpExporterOptions opts; OtlpHttpExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto client = http_client::HttpClientTestFactory::Create(); auto exporter = OtlpHttpExporterFactory::Create(opts, runtime_opts, std::move(client)); ASSERT_NE(exporter, nullptr); @@ -75,7 +72,7 @@ TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, ExportUsesTheInjectedJsonWriter OtlpHttpExporterOptions options; options.content_type = HttpRequestContentType::kJson; OtlpHttpExporterRuntimeOptions runtime_options; - runtime_options.json_writer_factory = std::make_shared(); + runtime_options.json_writer_factory = std::make_shared(); auto exporter = OtlpHttpExporterFactory::Create(options, runtime_options, std::move(client)); std::string captured_body; diff --git a/exporters/otlp/test/otlp_http_exporter_test.cc b/exporters/otlp/test/otlp_http_exporter_test.cc index f354c61ebd..3a492c8fbf 100644 --- a/exporters/otlp/test/otlp_http_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_test.cc @@ -25,6 +25,7 @@ #include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/nostd/span.h" #include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/nostd/utility.h" #include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/sdk/common/global_log_handler.h" @@ -980,7 +981,7 @@ TEST_F(OtlpHttpExporterTestPeer, ExportJsonGoldenBody) std::static_pointer_cast(no_send_client->session_); std::string captured_body; - CaptureRequestBody(mock_session, captured_body); + test::CaptureRequestBody(mock_session, captured_body); auto recordable = exporter->MakeRecordable(); diff --git a/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc index d54d61f017..d12e99f6af 100644 --- a/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc +++ b/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include "opentelemetry/exporters/otlp/otlp_http_log_record_exporter_factory.h" @@ -27,7 +28,7 @@ TEST(OtlpHttpLogRecordExporterCustomJsonWriterTest, RuntimeOptionsInjectionCreat { OtlpHttpLogRecordExporterOptions opts; OtlpHttpLogRecordExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto exporter = OtlpHttpLogRecordExporterFactory::Create(opts, runtime_opts); ASSERT_NE(exporter, nullptr); } @@ -36,7 +37,7 @@ TEST(OtlpHttpLogRecordExporterCustomJsonWriterTest, RuntimeOptionsWithHttpClient { OtlpHttpLogRecordExporterOptions opts; OtlpHttpLogRecordExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto client = http_client::HttpClientTestFactory::Create(); auto exporter = OtlpHttpLogRecordExporterFactory::Create(opts, runtime_opts, std::move(client)); ASSERT_NE(exporter, nullptr); diff --git a/exporters/otlp/test/otlp_http_log_record_exporter_test.cc b/exporters/otlp/test/otlp_http_log_record_exporter_test.cc index d342effb1a..35cb7dfdd9 100644 --- a/exporters/otlp/test/otlp_http_log_record_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_log_record_exporter_test.cc @@ -14,6 +14,7 @@ #include "nlohmann/json.hpp" #include "opentelemetry/common/key_value_iterable_view.h" +#include "opentelemetry/common/timestamp.h" #include "opentelemetry/exporters/otlp/otlp_environment.h" #include "opentelemetry/exporters/otlp/otlp_http.h" #include "opentelemetry/exporters/otlp/otlp_http_client.h" @@ -984,7 +985,7 @@ TEST_F(OtlpHttpLogRecordExporterTestPeer, ExportJsonGoldenBody) std::static_pointer_cast(no_send_client->session_); std::string captured_body; - CaptureRequestBody(mock_session, captured_body); + test::CaptureRequestBody(mock_session, captured_body); auto recordable = exporter->MakeRecordable(); diff --git a/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc index fcf0e3bfca..2671672825 100644 --- a/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc +++ b/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include "opentelemetry/exporters/otlp/otlp_http_metric_exporter_factory.h" @@ -27,7 +28,7 @@ TEST(OtlpHttpMetricExporterCustomJsonWriterTest, RuntimeOptionsInjectionCreatesE { OtlpHttpMetricExporterOptions opts; OtlpHttpMetricExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto exporter = OtlpHttpMetricExporterFactory::Create(opts, runtime_opts); ASSERT_NE(exporter, nullptr); } @@ -36,7 +37,7 @@ TEST(OtlpHttpMetricExporterCustomJsonWriterTest, RuntimeOptionsWithHttpClientCre { OtlpHttpMetricExporterOptions opts; OtlpHttpMetricExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto client = http_client::HttpClientTestFactory::Create(); auto exporter = OtlpHttpMetricExporterFactory::Create(opts, runtime_opts, std::move(client)); ASSERT_NE(exporter, nullptr); diff --git a/exporters/otlp/test/otlp_http_metric_exporter_test.cc b/exporters/otlp/test/otlp_http_metric_exporter_test.cc index e776426bec..ffd13475cc 100644 --- a/exporters/otlp/test/otlp_http_metric_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_metric_exporter_test.cc @@ -1397,7 +1397,7 @@ TEST_F(OtlpHttpMetricExporterTestPeer, ExportJsonGoldenBody) sum_metric_data, gauge_metric_data, histogram_metric_data}}}; std::string captured_body; - CaptureRequestBody(mock_session, captured_body); + test::CaptureRequestBody(mock_session, captured_body); EXPECT_EQ(opentelemetry::sdk::common::ExportResult::kSuccess, exporter->Export(data)); diff --git a/exporters/otlp/test/otlp_http_request_body_capture.h b/exporters/otlp/test/otlp_http_request_body_capture.h index 524a5a7890..dad54ce8ae 100644 --- a/exporters/otlp/test/otlp_http_request_body_capture.h +++ b/exporters/otlp/test/otlp_http_request_body_capture.h @@ -17,7 +17,7 @@ namespace exporter namespace otlp { -namespace +namespace test { // Captures the body of the one request the session is asked to send, and @@ -36,7 +36,7 @@ inline void CaptureRequestBody(const std::shared_ptrok()); } +namespace +{ + struct ScalarCase { const char *name; @@ -50,6 +54,8 @@ void PrintTo(const ScalarCase &scalar_case, std::ostream *os) class NlohmannJsonWriterScalar : public ::testing::TestWithParam {}; +} // namespace + TEST_P(NlohmannJsonWriterScalar, WritesTopLevelScalar) { auto writer = MakeWriter(); diff --git a/exporters/otlp/test/otlp_marking_json_writer.h b/exporters/otlp/test/otlp_marking_json_writer.h index 52e5a64e2c..23e22f0770 100644 --- a/exporters/otlp/test/otlp_marking_json_writer.h +++ b/exporters/otlp/test/otlp_marking_json_writer.h @@ -20,7 +20,7 @@ namespace exporter namespace otlp { -namespace +namespace test { // Wraps the default writer and marks its output, so a test can tell the @@ -60,7 +60,7 @@ class MarkingJsonWriterFactory : public JsonWriterFactory std::unique_ptr Create() override { return std::make_unique(); } }; -} // namespace +} // namespace test } // namespace otlp } // namespace exporter diff --git a/exporters/otlp/test/otlp_stub_json_writer.h b/exporters/otlp/test/otlp_stub_json_writer.h index 6ba5e31ee0..0302229dd4 100644 --- a/exporters/otlp/test/otlp_stub_json_writer.h +++ b/exporters/otlp/test/otlp_stub_json_writer.h @@ -19,7 +19,7 @@ namespace exporter namespace otlp { -namespace +namespace test { // A writer distinct from the nlohmann default, just enough to show that @@ -51,7 +51,7 @@ class StubJsonWriterFactory : public JsonWriterFactory std::unique_ptr Create() override { return std::make_unique(); } }; -} // namespace +} // namespace test } // namespace otlp } // namespace exporter From 30f173893b66a0d4f2f8632cdcd2cfdbed3e9462 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 04/20] Pass a string_view to Base64Escape in the nlohmann JSON writer --- exporters/otlp/src/otlp_json_writer_nlohmann.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/otlp/src/otlp_json_writer_nlohmann.cc b/exporters/otlp/src/otlp_json_writer_nlohmann.cc index 9a37e07796..0c55c05c7b 100644 --- a/exporters/otlp/src/otlp_json_writer_nlohmann.cc +++ b/exporters/otlp/src/otlp_json_writer_nlohmann.cc @@ -90,7 +90,7 @@ class NlohmannJsonWriter final : public JsonWriter if (nlohmann::json *slot = ClaimSlot()) { *slot = opentelemetry::sdk::common::Base64Escape( - std::string(reinterpret_cast(data), size)); + nostd::string_view(reinterpret_cast(data), size)); } } From 7eec588d9f7add0aa7d319447301c27ecbaaa998 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 05/20] Reuse the pending key buffer in the nlohmann JSON writer --- exporters/otlp/src/otlp_json_writer_nlohmann.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/otlp/src/otlp_json_writer_nlohmann.cc b/exporters/otlp/src/otlp_json_writer_nlohmann.cc index 0c55c05c7b..07a4d9dc16 100644 --- a/exporters/otlp/src/otlp_json_writer_nlohmann.cc +++ b/exporters/otlp/src/otlp_json_writer_nlohmann.cc @@ -59,7 +59,7 @@ class NlohmannJsonWriter final : public JsonWriter Fail("Key() called twice without an intervening value"); return; } - pending_key_ = std::string(key.data(), key.size()); + pending_key_.assign(key.data(), key.size()); has_pending_key_ = true; } From 1a9780e86e88498473cc8d4b14743c274f4bf656 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 06/20] Fail the OTLP/JSON export when ToString() fails or the factory returns null --- exporters/otlp/BUILD | 2 +- exporters/otlp/src/otlp_file_client.cc | 9 ++- exporters/otlp/src/otlp_http_client.cc | 10 ++- exporters/otlp/test/otlp_file_client_test.cc | 64 +++++++++++++++-- .../otlp_http_exporter_custom_client_test.cc | 51 +++++++++++++ ...p_http_exporter_custom_json_writer_test.cc | 12 ++-- .../otlp/test/otlp_marking_json_writer.h | 67 ----------------- exporters/otlp/test/otlp_stub_json_writer.h | 71 ++++++++++++++----- 8 files changed, 187 insertions(+), 99 deletions(-) delete mode 100644 exporters/otlp/test/otlp_marking_json_writer.h diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index 386d056c32..675cd2385a 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -812,7 +812,7 @@ cc_test( name = "otlp_file_client_test", srcs = [ "test/otlp_file_client_test.cc", - "test/otlp_marking_json_writer.h", + "test/otlp_stub_json_writer.h", ], tags = [ "otlp", diff --git a/exporters/otlp/src/otlp_file_client.cc b/exporters/otlp/src/otlp_file_client.cc index eb80ba52d5..d04431d916 100644 --- a/exporters/otlp/src/otlp_file_client.cc +++ b/exporters/otlp/src/otlp_file_client.cc @@ -1414,13 +1414,20 @@ opentelemetry::sdk::common::ExportResult OtlpFileClient::Export( } std::unique_ptr json_writer = json_writer_factory_->Create(); + if (!json_writer) + { + OTEL_INTERNAL_LOG_ERROR("[OTLP FILE Client] JsonWriterFactory::Create() returned nullptr"); + return ::opentelemetry::sdk::common::ExportResult::kFailure; + } + ConvertGenericMessageToJson(*json_writer, message, JsonConverterOptions{}); + + std::string post_body_json = json_writer->ok() ? json_writer->ToString() : std::string(); if (!json_writer->ok()) { return ::opentelemetry::sdk::common::ExportResult::kFailure; } - std::string post_body_json = json_writer->ToString(); if (options_.console_debug) { OTEL_INTERNAL_LOG_DEBUG("[OTLP FILE Client] Write body(Json)" << post_body_json); diff --git a/exporters/otlp/src/otlp_http_client.cc b/exporters/otlp/src/otlp_http_client.cc index b49d50d001..f4250b772d 100644 --- a/exporters/otlp/src/otlp_http_client.cc +++ b/exporters/otlp/src/otlp_http_client.cc @@ -716,10 +716,19 @@ OtlpHttpClient::createSession( else { std::unique_ptr json_writer = json_writer_factory_->Create(); + if (!json_writer) + { + OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] JsonWriterFactory::Create() returned nullptr"); + const auto result = opentelemetry::sdk::common::ExportResult::kFailure; + result_callback(result, response); + return result; + } + ConvertGenericMessageToJson( *json_writer, message, JsonConverterOptions{options_.use_json_name, options_.json_bytes_mapping}); + std::string post_body_json = json_writer->ok() ? json_writer->ToString() : std::string(); if (!json_writer->ok()) { const auto result = opentelemetry::sdk::common::ExportResult::kFailure; @@ -727,7 +736,6 @@ OtlpHttpClient::createSession( return result; } - std::string post_body_json = json_writer->ToString(); if (options_.console_debug) { OTEL_INTERNAL_LOG_DEBUG("[OTLP HTTP Client] Request body(Json)" << post_body_json); diff --git a/exporters/otlp/test/otlp_file_client_test.cc b/exporters/otlp/test/otlp_file_client_test.cc index b96bcac398..e25ecc730f 100644 --- a/exporters/otlp/test/otlp_file_client_test.cc +++ b/exporters/otlp/test/otlp_file_client_test.cc @@ -39,7 +39,7 @@ #include "opentelemetry/trace/trace_id.h" #include "opentelemetry/trace/trace_state.h" #include "opentelemetry/version.h" -#include "otlp_marking_json_writer.h" +#include "otlp_stub_json_writer.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep @@ -181,7 +181,11 @@ TEST(OtlpFileClientTest, CustomJsonWriterFactoryIsUsed) opentelemetry::exporter::otlp::OtlpFileClientRuntimeOptions rt_opts; opts.backend_options = std::ref(output_stream); - rt_opts.json_writer_factory = std::make_shared(); + rt_opts.json_writer_factory = std::make_shared([] { + auto writer = std::make_unique(); + writer->on_to_string = [] { return std::string("/*custom-writer*/"); }; + return writer; + }); auto client = std::make_unique(std::move(opts), std::move(rt_opts)); @@ -189,11 +193,59 @@ TEST(OtlpFileClientTest, CustomJsonWriterFactoryIsUsed) EXPECT_EQ(output_stream.str().rfind("/*custom-writer*/", 0), 0u) << "output was not produced by the injected JsonWriter: " << output_stream.str(); +} + +TEST(OtlpFileClientTest, ExportFailsWhenTheWriterFailsInToString) +{ + auto resource = MakeResource(); + auto instrumentation_scope = MakeInstrumentationScope(); + + std::unique_ptr recordable[] = { + MakeRecordable(resource, *instrumentation_scope)}; + + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request; + OtlpRecordableUtils::PopulateRequest(MakeSpan(recordable), &request); + + std::stringstream output_stream; + + opentelemetry::exporter::otlp::OtlpFileClientOptions opts; + opentelemetry::exporter::otlp::OtlpFileClientRuntimeOptions rt_opts; + opts.backend_options = std::ref(output_stream); + rt_opts.json_writer_factory = std::make_shared([] { + auto writer = std::make_unique(); + auto failed = std::make_shared(false); + writer->on_ok = [failed] { return !*failed; }; + writer->on_to_string = [failed] { + *failed = true; + return std::string("/*failed-writer-output*/"); + }; + return writer; + }); + + auto client = std::make_unique(std::move(opts), + std::move(rt_opts)); + + EXPECT_EQ(client->Export(request, 1), opentelemetry::sdk::common::ExportResult::kFailure); + EXPECT_EQ(output_stream.str(), "") << "the output of a failed writer was written"; +} + +TEST(OtlpFileClientTest, ExportFailsWhenTheFactoryReturnsNull) +{ + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request; + + std::stringstream output_stream; + + opentelemetry::exporter::otlp::OtlpFileClientOptions opts; + opentelemetry::exporter::otlp::OtlpFileClientRuntimeOptions rt_opts; + opts.backend_options = std::ref(output_stream); + rt_opts.json_writer_factory = + std::make_shared([] { return nullptr; }); + + auto client = std::make_unique(std::move(opts), + std::move(rt_opts)); - // The line still ends with the marked writer's underlying JSON, unchanged. - auto marker_size = std::string("/*custom-writer*/").size(); - auto check_json = nlohmann::json::parse(output_stream.str().substr(marker_size), nullptr, false); - EXPECT_FALSE(check_json.is_discarded()); + EXPECT_EQ(client->Export(request, 1), opentelemetry::sdk::common::ExportResult::kFailure); + EXPECT_EQ(output_stream.str(), ""); } TEST(OtlpFileClientTest, Shutdown) diff --git a/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc index b9c0157508..1dd406b491 100644 --- a/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc @@ -28,6 +28,7 @@ #include "opentelemetry/trace/span.h" #include "opentelemetry/trace/tracer.h" #include "opentelemetry/version.h" +#include "otlp_stub_json_writer.h" // clang-format off #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep @@ -353,6 +354,56 @@ TEST_F(OtlpHttpExporterCustomClientTestPeer, ALateTerminalEventDoesNotReportASec } } +TEST_F(OtlpHttpExporterCustomClientTestPeer, JsonExportFailsWhenTheWriterFailsInToString) +{ + auto client = http_client::HttpClientTestFactory::Create(); + auto no_send_client = std::static_pointer_cast(client); + auto session = std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*session, SendRequest).Times(0); + + auto options = MakeOtlpHttpClientOptions(); + options.content_type = HttpRequestContentType::kJson; + options.json_writer_factory = std::make_shared([] { + auto writer = std::make_unique(); + auto failed = std::make_shared(false); + writer->on_ok = [failed] { return !*failed; }; + writer->on_to_string = [failed] { + *failed = true; + return std::string("/*failed-writer-output*/"); + }; + return writer; + }); + OtlpHttpClient otlp_client(std::move(options), client); + + auto calls = std::make_shared>(0); + auto result = std::make_shared(sdk::common::ExportResult::kSuccess); + ExportOneRequest(otlp_client, calls, result); + + EXPECT_EQ(1, calls->load(std::memory_order_acquire)); + EXPECT_EQ(sdk::common::ExportResult::kFailure, *result); +} + +TEST_F(OtlpHttpExporterCustomClientTestPeer, JsonExportFailsWhenTheFactoryReturnsNull) +{ + auto client = http_client::HttpClientTestFactory::Create(); + auto no_send_client = std::static_pointer_cast(client); + auto session = std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*session, SendRequest).Times(0); + + auto options = MakeOtlpHttpClientOptions(); + options.content_type = HttpRequestContentType::kJson; + options.json_writer_factory = + std::make_shared([] { return nullptr; }); + OtlpHttpClient otlp_client(std::move(options), client); + + auto calls = std::make_shared>(0); + auto result = std::make_shared(sdk::common::ExportResult::kSuccess); + ExportOneRequest(otlp_client, calls, result); + + EXPECT_EQ(1, calls->load(std::memory_order_acquire)); + EXPECT_EQ(sdk::common::ExportResult::kFailure, *result); +} + } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc index 3ae3e4e6e6..a9d87b4eb9 100644 --- a/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc @@ -21,7 +21,7 @@ #include "opentelemetry/trace/span.h" #include "opentelemetry/trace/tracer.h" #include "opentelemetry/version.h" -#include "otlp_marking_json_writer.h" +#include "otlp_stub_json_writer.h" #include "opentelemetry/test_common/ext/http/client/http_client_test_factory.h" #include "opentelemetry/test_common/ext/http/client/nosend/http_client_nosend.h" @@ -48,7 +48,7 @@ TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, RuntimeOptionsInjectionCreatesE { OtlpHttpExporterOptions opts; OtlpHttpExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto exporter = OtlpHttpExporterFactory::Create(opts, runtime_opts); ASSERT_NE(exporter, nullptr); } @@ -57,7 +57,7 @@ TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, RuntimeOptionsWithHttpClientCre { OtlpHttpExporterOptions opts; OtlpHttpExporterRuntimeOptions runtime_opts; - runtime_opts.json_writer_factory = std::make_shared(); + runtime_opts.json_writer_factory = std::make_shared(); auto client = http_client::HttpClientTestFactory::Create(); auto exporter = OtlpHttpExporterFactory::Create(opts, runtime_opts, std::move(client)); ASSERT_NE(exporter, nullptr); @@ -72,7 +72,11 @@ TEST_F(OtlpHttpExporterCustomJsonWriterTestPeer, ExportUsesTheInjectedJsonWriter OtlpHttpExporterOptions options; options.content_type = HttpRequestContentType::kJson; OtlpHttpExporterRuntimeOptions runtime_options; - runtime_options.json_writer_factory = std::make_shared(); + runtime_options.json_writer_factory = std::make_shared([] { + auto writer = std::make_unique(); + writer->on_to_string = [] { return std::string("/*custom-writer*/"); }; + return writer; + }); auto exporter = OtlpHttpExporterFactory::Create(options, runtime_options, std::move(client)); std::string captured_body; diff --git a/exporters/otlp/test/otlp_marking_json_writer.h b/exporters/otlp/test/otlp_marking_json_writer.h deleted file mode 100644 index 23e22f0770..0000000000 --- a/exporters/otlp/test/otlp_marking_json_writer.h +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -#include -#include -#include - -#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" -#include "opentelemetry/exporters/otlp/otlp_json_writer.h" -#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" -#include "opentelemetry/nostd/string_view.h" -#include "opentelemetry/version.h" - -OPENTELEMETRY_BEGIN_NAMESPACE -namespace exporter -{ -namespace otlp -{ - -namespace test -{ - -// Wraps the default writer and marks its output, so a test can tell the -// injected writer ran by looking at the emitted bytes alone. -class MarkingJsonWriter : public JsonWriter -{ -public: - MarkingJsonWriter() : inner_(detail::GetDefaultJsonWriterFactory()->Create()) {} - - void BeginObject() noexcept override { inner_->BeginObject(); } - void EndObject() noexcept override { inner_->EndObject(); } - void BeginArray() noexcept override { inner_->BeginArray(); } - void EndArray() noexcept override { inner_->EndArray(); } - void Key(nostd::string_view key) noexcept override { inner_->Key(key); } - void WriteNull() noexcept override { inner_->WriteNull(); } - void WriteString(nostd::string_view value) noexcept override { inner_->WriteString(value); } - void WriteInt32(std::int32_t value) noexcept override { inner_->WriteInt32(value); } - void WriteInt64(std::int64_t value) noexcept override { inner_->WriteInt64(value); } - void WriteUInt32(std::uint32_t value) noexcept override { inner_->WriteUInt32(value); } - void WriteUInt64(std::uint64_t value) noexcept override { inner_->WriteUInt64(value); } - void WriteDouble(double value) noexcept override { inner_->WriteDouble(value); } - void WriteBool(bool value) noexcept override { inner_->WriteBool(value); } - void WriteBytes(const std::uint8_t *data, std::size_t size) noexcept override - { - inner_->WriteBytes(data, size); - } - bool ok() const noexcept override { return inner_->ok(); } - std::string ToString() noexcept override { return "/*custom-writer*/" + inner_->ToString(); } - -private: - std::unique_ptr inner_; -}; - -class MarkingJsonWriterFactory : public JsonWriterFactory -{ -public: - std::unique_ptr Create() override { return std::make_unique(); } -}; - -} // namespace test - -} // namespace otlp -} // namespace exporter -OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_stub_json_writer.h b/exporters/otlp/test/otlp_stub_json_writer.h index 0302229dd4..70d6f2728d 100644 --- a/exporters/otlp/test/otlp_stub_json_writer.h +++ b/exporters/otlp/test/otlp_stub_json_writer.h @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include #include "opentelemetry/exporters/otlp/otlp_json_writer.h" #include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" @@ -22,33 +24,64 @@ namespace otlp namespace test { -// A writer distinct from the nlohmann default, just enough to show that -// injection reaches the constructor. +// A writer distinct from the nlohmann default, just enough to show that injection reaches the +// constructor. Each method calls the matching on_ member, so a test replaces only the behavior it +// needs. By default the tokens are ignored, ok() returns true and ToString() returns "{}". class StubJsonWriter : public JsonWriter { public: - void BeginObject() noexcept override {} - void EndObject() noexcept override {} - void BeginArray() noexcept override {} - void EndArray() noexcept override {} - void Key(nostd::string_view) noexcept override {} - void WriteNull() noexcept override {} - void WriteString(nostd::string_view) noexcept override {} - void WriteInt32(std::int32_t) noexcept override {} - void WriteInt64(std::int64_t) noexcept override {} - void WriteUInt32(std::uint32_t) noexcept override {} - void WriteUInt64(std::uint64_t) noexcept override {} - void WriteDouble(double) noexcept override {} - void WriteBool(bool) noexcept override {} - void WriteBytes(const std::uint8_t *, std::size_t) noexcept override {} - bool ok() const noexcept override { return true; } - std::string ToString() noexcept override { return "{}"; } + void BeginObject() noexcept override { on_begin_object(); } + void EndObject() noexcept override { on_end_object(); } + void BeginArray() noexcept override { on_begin_array(); } + void EndArray() noexcept override { on_end_array(); } + void Key(nostd::string_view key) noexcept override { on_key(key); } + void WriteNull() noexcept override { on_write_null(); } + void WriteString(nostd::string_view value) noexcept override { on_write_string(value); } + void WriteInt32(std::int32_t value) noexcept override { on_write_int32(value); } + void WriteInt64(std::int64_t value) noexcept override { on_write_int64(value); } + void WriteUInt32(std::uint32_t value) noexcept override { on_write_uint32(value); } + void WriteUInt64(std::uint64_t value) noexcept override { on_write_uint64(value); } + void WriteDouble(double value) noexcept override { on_write_double(value); } + void WriteBool(bool value) noexcept override { on_write_bool(value); } + void WriteBytes(const std::uint8_t *data, std::size_t size) noexcept override + { + on_write_bytes(data, size); + } + bool ok() const noexcept override { return on_ok(); } + std::string ToString() noexcept override { return on_to_string(); } + + std::function on_begin_object = [] {}; + std::function on_end_object = [] {}; + std::function on_begin_array = [] {}; + std::function on_end_array = [] {}; + std::function on_key = [](nostd::string_view) {}; + std::function on_write_null = [] {}; + std::function on_write_string = [](nostd::string_view) {}; + std::function on_write_int32 = [](std::int32_t) {}; + std::function on_write_int64 = [](std::int64_t) {}; + std::function on_write_uint32 = [](std::uint32_t) {}; + std::function on_write_uint64 = [](std::uint64_t) {}; + std::function on_write_double = [](double) {}; + std::function on_write_bool = [](bool) {}; + std::function on_write_bytes = [](const std::uint8_t *, + std::size_t) {}; + std::function on_ok = [] { return true; }; + std::function on_to_string = [] { return std::string("{}"); }; }; +// Creates writers with the given function, so a test can inject any writer, or nullptr. class StubJsonWriterFactory : public JsonWriterFactory { public: - std::unique_ptr Create() override { return std::make_unique(); } + explicit StubJsonWriterFactory(std::function()> create = + [] { return std::make_unique(); }) + : create_(std::move(create)) + {} + + std::unique_ptr Create() override { return create_(); } + +private: + std::function()> create_; }; } // namespace test From fc0f530285ba1e4f29dc9ef2c89648d70308621e Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 07/20] Resolve the default JSON writer only for JSON content in the OTLP HTTP client --- .../otlp/detail/default_json_writer_factory.h | 2 +- .../exporters/otlp/otlp_http_client.h | 3 +- exporters/otlp/src/otlp_http_client.cc | 16 ++++++++-- .../otlp_http_exporter_custom_client_test.cc | 31 +++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h b/exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h index 54438a4269..570762d44a 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/detail/default_json_writer_factory.h @@ -28,7 +28,7 @@ inline std::shared_ptr GetDefaultJsonWriterFactory() #else OTEL_INTERNAL_LOG_ERROR( "No default JSON writer backend is compiled in. " - "Use the JsonWriterFactory constructor or factory overloads, " + "Set json_writer_factory in the exporter runtime options, " "or enable the nlohmann backend (OTELCPP_WITH_JSON_WRITER_NLOHMANN=ON)."); std::terminate(); #endif diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h index 70af8486bd..af9b4f2ae2 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h @@ -298,7 +298,8 @@ class OtlpHttpClient // Object that stores the HTTP sessions that have been created std::shared_ptr http_client_; - // Resolved from options_.json_writer_factory, or the default backend. + // Resolved from options_.json_writer_factory, or the default backend. Null when the content + // type is binary and no factory was supplied. std::shared_ptr json_writer_factory_; // Cached parsed URI diff --git a/exporters/otlp/src/otlp_http_client.cc b/exporters/otlp/src/otlp_http_client.cc index f4250b772d..1a2fa4e4eb 100644 --- a/exporters/otlp/src/otlp_http_client.cc +++ b/exporters/otlp/src/otlp_http_client.cc @@ -427,6 +427,19 @@ bool SerializeToHttpBody(http_client::Body &output, const google::protobuf::Mess return true; } +std::shared_ptr ResolveJsonWriterFactory(const OtlpHttpClientOptions &options) +{ + if (options.json_writer_factory) + { + return options.json_writer_factory; + } + if (options.content_type == HttpRequestContentType::kJson) + { + return detail::GetDefaultJsonWriterFactory(); + } + return nullptr; +} + } // namespace OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options) @@ -443,8 +456,7 @@ OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options, : is_shutdown_(false), options_(std::move(options)), http_client_(std::move(http_client)), - json_writer_factory_(options_.json_writer_factory ? options_.json_writer_factory - : detail::GetDefaultJsonWriterFactory()), + json_writer_factory_(ResolveJsonWriterFactory(options_)), start_session_counter_(0), finished_session_counter_(0) { diff --git a/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc index 1dd406b491..4103586345 100644 --- a/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc @@ -354,6 +354,37 @@ TEST_F(OtlpHttpExporterCustomClientTestPeer, ALateTerminalEventDoesNotReportASec } } +// Only catches a regression when built without a default JSON backend. +TEST_F(OtlpHttpExporterCustomClientTestPeer, BinaryExportDoesNotNeedAJsonWriterFactory) +{ + auto client = http_client::HttpClientTestFactory::Create(); + auto no_send_client = std::static_pointer_cast(client); + auto session = std::static_pointer_cast(no_send_client->session_); + + std::shared_ptr pending; + EXPECT_CALL(*session, SendRequest) + .WillOnce( + [&pending](std::shared_ptr callback) { + pending = std::move(callback); + }); + + auto options = MakeOtlpHttpClientOptions(std::chrono::seconds{30}); + options.content_type = HttpRequestContentType::kBinary; + options.json_writer_factory = nullptr; + OtlpHttpClient otlp_client(std::move(options), client); + + auto calls = std::make_shared>(0); + auto result = std::make_shared(sdk::common::ExportResult::kFailure); + ExportOneRequest(otlp_client, calls, result); + ASSERT_NE(pending, nullptr); + + http_client::nosend::Response sent; + sent.Finish(*pending); + + EXPECT_EQ(1, calls->load(std::memory_order_acquire)); + EXPECT_EQ(sdk::common::ExportResult::kSuccess, *result); +} + TEST_F(OtlpHttpExporterCustomClientTestPeer, JsonExportFailsWhenTheWriterFailsInToString) { auto client = http_client::HttpClientTestFactory::Create(); From 803fcce452f741e06158289418e791661fd26e3e Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 08/20] Complete the pkg-config dependency chain of the OTLP JSON libraries --- ci/verify_packages.sh | 3 +++ exporters/otlp/CMakeLists.txt | 25 +++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/ci/verify_packages.sh b/ci/verify_packages.sh index 2386499f91..0cc7b5e898 100755 --- a/ci/verify_packages.sh +++ b/ci/verify_packages.sh @@ -27,6 +27,9 @@ done # OTLP exporters validate_package "opentelemetry_otlp_recordable" validate_package "opentelemetry_exporter_otlp_builder_utils" +for variant in json_writer json_writer_nlohmann json_converter; do + validate_package "opentelemetry_exporter_otlp_${variant}" +done # OTLP gRPC exporter validate_package "opentelemetry_exporter_otlp_grpc" diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index 0f64270362..5b255cfe02 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -147,9 +147,16 @@ if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) endif() if(OTELCPP_INSTALL) + set(OPENTELEMETRY_OTLP_JSON_WRITER_PC_REQUIRES "opentelemetry_common") + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + string(APPEND OPENTELEMETRY_OTLP_JSON_WRITER_PC_REQUIRES + " opentelemetry_exporter_otlp_json_writer_nlohmann") + endif() + opentelemetry_add_pkgconfig( exporter_otlp_json_writer "OpenTelemetry OTLP - JSON Writer" - "Token-stream JSON writer interface." "opentelemetry_common") + "Token-stream JSON writer interface." + "${OPENTELEMETRY_OTLP_JSON_WRITER_PC_REQUIRES}") if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) opentelemetry_add_pkgconfig( @@ -188,7 +195,7 @@ if(OTELCPP_INSTALL) exporter_otlp_json_converter "OpenTelemetry OTLP - JSON Converter" "Shared reflection-based protobuf-to-JSON converter for OTLP/JSON exporters." - "opentelemetry_sdk opentelemetry_common opentelemetry_exporter_otlp_json_writer" + "opentelemetry_common opentelemetry_exporter_otlp_json_writer opentelemetry_proto" ) endif() @@ -616,8 +623,11 @@ if(OTELCPP_WITH_OTLP_HTTP) if(OTELCPP_INSTALL) opentelemetry_add_pkgconfig( - exporter_otlp_http_client "OpenTelemetry OTLP - HTTP Client" - "HTTP client for OTLP protocol." "opentelemetry_api") + exporter_otlp_http_client + "OpenTelemetry OTLP - HTTP Client" + "HTTP client for OTLP protocol." + "opentelemetry_api opentelemetry_exporter_otlp_json_writer opentelemetry_exporter_otlp_json_converter" + ) opentelemetry_add_pkgconfig( exporter_otlp_http @@ -835,8 +845,11 @@ if(OTELCPP_WITH_OTLP_FILE) if(OTELCPP_INSTALL) opentelemetry_add_pkgconfig( - exporter_otlp_file_client "OpenTelemetry OTLP - File Client" - "File client for OTLP protocol." "opentelemetry_api opentelemetry_common") + exporter_otlp_file_client + "OpenTelemetry OTLP - File Client" + "File client for OTLP protocol." + "opentelemetry_api opentelemetry_common opentelemetry_exporter_otlp_json_writer opentelemetry_exporter_otlp_json_converter" + ) opentelemetry_add_pkgconfig( exporter_otlp_file From 6a869fcb60aa00490976b3a9d4a28d178b16ce44 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 09/20] Document the JsonWriter and JsonWriterFactory contracts --- .../exporters/otlp/otlp_json_writer.h | 163 ++++++++++++++++-- .../exporters/otlp/otlp_json_writer_factory.h | 13 ++ 2 files changed, 165 insertions(+), 11 deletions(-) diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h index c2596ceaff..b78fc1d438 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h @@ -16,6 +16,61 @@ namespace exporter namespace otlp { +/** + * Builds one JSON document from a stream of tokens. The OTLP/JSON exporters + * drive it through ConvertGenericMessageToJson() and send the result of + * ToString(). + * + * The interface carries no OTLP knowledge. Mapping decisions, such as + * hex-encoded trace and span IDs or 64-bit integers written as strings, are + * made by the caller, which picks the token accordingly. + * + * Lifecycle and threading + * + * A JsonWriterFactory creates one writer per document. The caller writes the + * whole document, calls ToString() once, and then destroys the writer. A + * writer is not reused for a second document, and it is used by one thread + * at a time. Implementations need no internal synchronization. + * + * Token stream + * + * - The stream holds exactly one top-level value: a scalar, or a container + * opened by BeginObject() or BeginArray(). + * - Every Begin call is matched by the End call of the same kind, and + * containers close in the reverse order of opening. + * - Directly inside an object, each value, including a nested container, is + * preceded by exactly one Key(). Directly inside an array, values are not + * preceded by Key(). + * - Tokens arrive in output order. A container cannot be extended once a + * later sibling has begun. + * - Callers do not write the same key twice in one object. If they do, the + * output is unspecified. + * + * Output + * + * - A well-formed stream produces a document that is valid JSON, encoded as + * UTF-8, with no insignificant whitespace. + * - Keys and string values escape only what JSON requires: quote, backslash, + * backspace, form feed, newline, carriage return and tab as `\"`, `\\`, + * `\b`, `\f`, `\n`, `\r` and `\t`, and the other control characters as + * `\u00xx` with lowercase hex digits. Every other character is written as + * UTF-8. Input + * bytes that are not valid UTF-8 are replaced with U+FFFD rather than + * treated as an error, so that invalid telemetry data cannot fail an + * export. + * - The order of members in an object is not guaranteed to match the order + * of the Key() calls, because JSON objects are unordered. The default + * nlohmann-json backend sorts members by key. + * + * Errors + * + * No method throws, including in builds with exceptions disabled. A + * malformed token stream or an internal failure, such as a failed + * allocation, marks the writer as failed instead. ok() returns false from + * then on, and every later call, including ToString(), is allowed to do + * nothing. An implementation should report the first failure through the SDK + * internal log handler. + */ class JsonWriter { public: @@ -26,25 +81,111 @@ class JsonWriter JsonWriter &operator=(JsonWriter &&) = delete; virtual ~JsonWriter() = default; + /** + * Opens an object. Inside an object, the call is preceded by Key(). + */ virtual void BeginObject() noexcept = 0; - virtual void EndObject() noexcept = 0; - virtual void BeginArray() noexcept = 0; - virtual void EndArray() noexcept = 0; + /** + * Closes the innermost open container, which must be an object whose last + * Key() has received its value. + */ + virtual void EndObject() noexcept = 0; + + /** + * Opens an array. Inside an object, the call is preceded by Key(). + */ + virtual void BeginArray() noexcept = 0; + + /** + * Closes the innermost open container, which must be an array. + */ + virtual void EndArray() noexcept = 0; + + /** + * Names the next value of the innermost open container, which must be an + * object. The key may hold any bytes and is escaped like a string value. + * + * @param key valid only for the duration of the call; copy it if it is + * needed afterwards. + */ virtual void Key(nostd::string_view key) noexcept = 0; - virtual void WriteNull() noexcept = 0; - virtual void WriteString(nostd::string_view value) noexcept = 0; - virtual void WriteInt32(std::int32_t value) noexcept = 0; - virtual void WriteInt64(std::int64_t value) noexcept = 0; - virtual void WriteUInt32(std::uint32_t value) noexcept = 0; - virtual void WriteUInt64(std::uint64_t value) noexcept = 0; - virtual void WriteDouble(double value) noexcept = 0; - virtual void WriteBool(bool value) noexcept = 0; + /** + * Writes the JSON literal null. + */ + virtual void WriteNull() noexcept = 0; + + /** + * Writes a JSON string. + * + * @param value valid only for the duration of the call; copy it if it is + * needed afterwards. It may contain any bytes, including NUL. + */ + virtual void WriteString(nostd::string_view value) noexcept = 0; + + /** + * Writes a JSON number with the exact value. + */ + virtual void WriteInt32(std::int32_t value) noexcept = 0; + + /** + * Writes a JSON number with the exact value over the full 64-bit range. An + * implementation whose number representation cannot hold every value, such + * as one that stores all numbers as double, must emit the decimal digits + * directly. + */ + virtual void WriteInt64(std::int64_t value) noexcept = 0; + + /** + * Writes a JSON number with the exact value. + */ + virtual void WriteUInt32(std::uint32_t value) noexcept = 0; + + /** + * Writes a JSON number with the exact value over the full 64-bit range. The + * requirement on WriteInt64() applies. + */ + virtual void WriteUInt64(std::uint64_t value) noexcept = 0; + + /** + * Writes a JSON number that parses back to the same double. JSON has no + * representation for NaN or infinity, so a non-finite value is written as + * null. + */ + virtual void WriteDouble(double value) noexcept = 0; + + /** + * Writes the JSON literal true or false. + */ + virtual void WriteBool(bool value) noexcept = 0; + + /** + * Writes the bytes as a JSON string holding their base64 encoding, the same + * as sdk::common::Base64Escape() produces. Callers that need another + * encoding, such as hex, encode the bytes themselves and call WriteString(). + * + * @param data valid only for the duration of the call; may be null when + * size is 0. + * @param size the number of bytes at data. + */ virtual void WriteBytes(const std::uint8_t *data, std::size_t size) noexcept = 0; + /** + * Returns false once the writer has failed. The state is sticky: after it + * becomes false, it stays false. The call is cheap and valid at any time. + */ virtual bool ok() const noexcept = 0; + /** + * Returns the serialized document. The caller calls it once, after the + * top-level value is complete; calling it earlier is a caller error with an + * unspecified result. + * + * Serialization can fail, so the caller checks ok() after the call. When + * ok() is false, the contents of the returned string are unspecified and + * must not be used. + */ virtual std::string ToString() noexcept = 0; }; diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h index e76d40729b..5b38dfcfba 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h @@ -14,6 +14,19 @@ namespace exporter namespace otlp { +/** + * Creates a JsonWriter for each OTLP/JSON export. Set it as + * json_writer_factory in the runtime options of an OTLP HTTP or file exporter + * to replace the default nlohmann-json backend. + * + * Create() is called once per export. One factory may be shared by several + * exporters, and an exporter may export from several threads, so Create() can + * be called concurrently and must be thread-safe. + * + * Create() must not throw: it is called from noexcept export paths, where an + * escaping exception terminates the program. To signal a failure it returns + * nullptr, and the export then fails. + */ class OPENTELEMETRY_EXPORT JsonWriterFactory { public: From 5c4e118966c8ae871bd52bd90788e00f18f66c8f Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 13:10:20 +0200 Subject: [PATCH 10/20] Test the JsonWriter contract with a suite shared by writer backends --- exporters/otlp/BUILD | 8 +- exporters/otlp/CMakeLists.txt | 8 +- .../test/otlp_json_writer_contract_test.cc | 488 ++++++++++++++++++ .../test/otlp_json_writer_contract_test.h | 49 ++ .../test/otlp_json_writer_nlohmann_test.cc | 212 +------- 5 files changed, 562 insertions(+), 203 deletions(-) create mode 100644 exporters/otlp/test/otlp_json_writer_contract_test.cc create mode 100644 exporters/otlp/test/otlp_json_writer_contract_test.h diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index 675cd2385a..d6fd820084 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -748,13 +748,19 @@ cc_library( cc_test( name = "otlp_json_writer_nlohmann_test", - srcs = ["test/otlp_json_writer_nlohmann_test.cc"], + srcs = [ + "test/otlp_json_writer_contract_test.cc", + "test/otlp_json_writer_contract_test.h", + "test/otlp_json_writer_nlohmann_test.cc", + ], tags = [ "otlp", "test", ], deps = [ ":otlp_json_writer", + "//sdk/src/common:global_log_handler", + "//test_common:headers", "@com_google_googletest//:gtest_main", ], ) diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index 5b255cfe02..c1882c282d 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -1030,11 +1030,13 @@ if(OTELCPP_BUILD_TESTING) TEST_LIST otlp_metrics_serialization_test) if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) - add_executable(otlp_json_writer_nlohmann_test - test/otlp_json_writer_nlohmann_test.cc) + add_executable( + otlp_json_writer_nlohmann_test test/otlp_json_writer_nlohmann_test.cc + test/otlp_json_writer_contract_test.cc) target_link_libraries( otlp_json_writer_nlohmann_test ${GTEST_BOTH_LIBRARIES} - ${CMAKE_THREAD_LIBS_INIT} opentelemetry_exporter_otlp_json_writer) + ${CMAKE_THREAD_LIBS_INIT} opentelemetry_exporter_otlp_json_writer + opentelemetry_test_common) gtest_add_tests( TARGET otlp_json_writer_nlohmann_test TEST_PREFIX exporter.otlp. diff --git a/exporters/otlp/test/otlp_json_writer_contract_test.cc b/exporters/otlp/test/otlp_json_writer_contract_test.cc new file mode 100644 index 0000000000..625e163830 --- /dev/null +++ b/exporters/otlp/test/otlp_json_writer_contract_test.cc @@ -0,0 +1,488 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "otlp_json_writer_contract_test.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/sdk/common/global_log_handler.h" +#include "opentelemetry/test_common/sdk/common/scoped_test_log_handler.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ +namespace test +{ +namespace +{ + +std::size_t CountErrors(const std::vector &entries) +{ + return static_cast(std::count_if( + entries.begin(), entries.end(), [](const test_common::ScopedTestLogHandler::Entry &entry) { + return entry.level == sdk::common::internal_log::LogLevel::Error; + })); +} + +// A finite double may be spelled in more than one way, so tests compare its parsed value. +double ParseDouble(const std::string &number) +{ + char *end = nullptr; + const double parsed = std::strtod(number.c_str(), &end); + EXPECT_TRUE(!number.empty() && end == number.c_str() + number.size()) + << "not a number: " << number; + return parsed; +} + +} // namespace + +std::unique_ptr JsonWriterContract::MakeWriter() const +{ + return GetParam()->Create(); +} + +std::string JsonWriterContract::Finish(JsonWriter &writer) +{ + EXPECT_TRUE(writer.ok()); + std::string json = writer.ToString(); + EXPECT_TRUE(writer.ok()); + EXPECT_EQ(CountErrors(log_.Drain()), 0u); + return json; +} + +void JsonWriterContract::ExpectFailedOnce(JsonWriter &writer) +{ + EXPECT_FALSE(writer.ok()); + + writer.BeginObject(); + writer.Key("a"); + writer.WriteInt32(1); + writer.EndObject(); + writer.EndArray(); + writer.Key("b"); + EXPECT_FALSE(writer.ok()); + + writer.ToString(); + EXPECT_FALSE(writer.ok()); + EXPECT_EQ(CountErrors(log_.Drain()), 1u); +} + +TEST_P(JsonWriterContract, FactoryCreatesAWorkingWriter) +{ + auto writer = MakeWriter(); + ASSERT_NE(writer, nullptr); + EXPECT_TRUE(writer->ok()); +} + +TEST_P(JsonWriterContract, FactoryCreatesWritersConcurrently) +{ + constexpr int kThreads = 8; + constexpr int kWritersPerThread = 200; + std::vector failures(kThreads); + std::vector threads; + for (int t = 0; t < kThreads; ++t) + { + threads.emplace_back([this, t, &failures] { + for (int i = 0; i < kWritersPerThread && failures[t].empty(); ++i) + { + auto writer = MakeWriter(); + if (!writer) + { + failures[t] = "Create() returned nullptr"; + break; + } + writer->BeginArray(); + writer->WriteInt32(t); + writer->WriteInt32(i); + writer->EndArray(); + const std::string expected = "[" + std::to_string(t) + "," + std::to_string(i) + "]"; + const std::string json = writer->ToString(); + if (!writer->ok() || json != expected) + { + failures[t] = "expected " + expected + ", got " + json; + } + } + }); + } + for (auto &thread : threads) + { + thread.join(); + } + for (const auto &failure : failures) + { + EXPECT_EQ(failure, ""); + } +} + +TEST_P(JsonWriterContract, WritesNull) +{ + auto writer = MakeWriter(); + writer->WriteNull(); + EXPECT_EQ(Finish(*writer), "null"); +} + +TEST_P(JsonWriterContract, WritesTrue) +{ + auto writer = MakeWriter(); + writer->WriteBool(true); + EXPECT_EQ(Finish(*writer), "true"); +} + +TEST_P(JsonWriterContract, WritesFalse) +{ + auto writer = MakeWriter(); + writer->WriteBool(false); + EXPECT_EQ(Finish(*writer), "false"); +} + +TEST_P(JsonWriterContract, WritesInt32Min) +{ + auto writer = MakeWriter(); + writer->WriteInt32(std::numeric_limits::min()); + EXPECT_EQ(Finish(*writer), "-2147483648"); +} + +TEST_P(JsonWriterContract, WritesInt32Max) +{ + auto writer = MakeWriter(); + writer->WriteInt32(std::numeric_limits::max()); + EXPECT_EQ(Finish(*writer), "2147483647"); +} + +TEST_P(JsonWriterContract, WritesUInt32Max) +{ + auto writer = MakeWriter(); + writer->WriteUInt32(std::numeric_limits::max()); + EXPECT_EQ(Finish(*writer), "4294967295"); +} + +// The extremes of both 64-bit types are outside the range a double represents exactly. +TEST_P(JsonWriterContract, WritesInt64Min) +{ + auto writer = MakeWriter(); + writer->WriteInt64(std::numeric_limits::min()); + EXPECT_EQ(Finish(*writer), "-9223372036854775808"); +} + +TEST_P(JsonWriterContract, WritesInt64Max) +{ + auto writer = MakeWriter(); + writer->WriteInt64(std::numeric_limits::max()); + EXPECT_EQ(Finish(*writer), "9223372036854775807"); +} + +TEST_P(JsonWriterContract, WritesUInt64Max) +{ + auto writer = MakeWriter(); + writer->WriteUInt64(std::numeric_limits::max()); + EXPECT_EQ(Finish(*writer), "18446744073709551615"); +} + +TEST_P(JsonWriterContract, WritesDouble) +{ + auto writer = MakeWriter(); + writer->WriteDouble(0.1); + EXPECT_EQ(ParseDouble(Finish(*writer)), 0.1); +} + +TEST_P(JsonWriterContract, WritesLargeNegativeDouble) +{ + auto writer = MakeWriter(); + writer->WriteDouble(-1.5e300); + EXPECT_EQ(ParseDouble(Finish(*writer)), -1.5e300); +} + +TEST_P(JsonWriterContract, WritesDenormalDouble) +{ + auto writer = MakeWriter(); + writer->WriteDouble(std::numeric_limits::denorm_min()); + EXPECT_EQ(ParseDouble(Finish(*writer)), std::numeric_limits::denorm_min()); +} + +TEST_P(JsonWriterContract, WritesNaNAsNull) +{ + auto writer = MakeWriter(); + writer->WriteDouble(std::numeric_limits::quiet_NaN()); + EXPECT_EQ(Finish(*writer), "null"); +} + +TEST_P(JsonWriterContract, WritesInfinityAsNull) +{ + auto writer = MakeWriter(); + writer->WriteDouble(std::numeric_limits::infinity()); + EXPECT_EQ(Finish(*writer), "null"); +} + +TEST_P(JsonWriterContract, WritesNegativeInfinityAsNull) +{ + auto writer = MakeWriter(); + writer->WriteDouble(-std::numeric_limits::infinity()); + EXPECT_EQ(Finish(*writer), "null"); +} + +TEST_P(JsonWriterContract, WritesString) +{ + auto writer = MakeWriter(); + writer->WriteString("hello"); + EXPECT_EQ(Finish(*writer), R"("hello")"); +} + +TEST_P(JsonWriterContract, WritesEmptyString) +{ + auto writer = MakeWriter(); + writer->WriteString(""); + EXPECT_EQ(Finish(*writer), R"("")"); +} + +TEST_P(JsonWriterContract, EscapesOnlyWhatJsonRequiresInString) +{ + auto writer = MakeWriter(); + writer->WriteString("\"\\\b\f\n\r\t\x01\x1F/\x7F"); + EXPECT_EQ(Finish(*writer), "\"\\\"\\\\\\b\\f\\n\\r\\t\\u0001\\u001f/\x7F\""); +} + +TEST_P(JsonWriterContract, WritesStringWithNul) +{ + auto writer = MakeWriter(); + writer->WriteString(std::string("a\0b", 3)); + EXPECT_EQ(Finish(*writer), "\"a\\u0000b\""); +} + +TEST_P(JsonWriterContract, WritesUtf8String) +{ + auto writer = MakeWriter(); + writer->WriteString("\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80"); + EXPECT_EQ(Finish(*writer), "\"\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80\""); +} + +TEST_P(JsonWriterContract, ReplacesInvalidUtf8InString) +{ + auto writer = MakeWriter(); + writer->WriteString( + "a\xFF" + "b"); + EXPECT_EQ(Finish(*writer), + "\"a\xEF\xBF\xBD" + "b\""); +} + +TEST_P(JsonWriterContract, CopiesString) +{ + std::string value = "value"; + auto writer = MakeWriter(); + writer->WriteString(value); + value.assign("xxxxx"); + EXPECT_EQ(Finish(*writer), R"("value")"); +} + +TEST_P(JsonWriterContract, WritesBytesAsBase64) +{ + const std::uint8_t bytes[] = {0x00, 0x01, 0x02, 0xFF}; + auto writer = MakeWriter(); + writer->WriteBytes(bytes, sizeof(bytes)); + EXPECT_EQ(Finish(*writer), R"("AAEC/w==")"); +} + +TEST_P(JsonWriterContract, WritesEmptyBytesFromNull) +{ + auto writer = MakeWriter(); + writer->WriteBytes(nullptr, 0); + EXPECT_EQ(Finish(*writer), R"("")"); +} + +TEST_P(JsonWriterContract, CopiesBytes) +{ + std::uint8_t bytes[] = {'a', 'b', 'c'}; + auto writer = MakeWriter(); + writer->WriteBytes(bytes, sizeof(bytes)); + bytes[0] = bytes[1] = bytes[2] = 0; + EXPECT_EQ(Finish(*writer), R"("YWJj")"); +} + +TEST_P(JsonWriterContract, WritesEmptyObject) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->EndObject(); + EXPECT_EQ(Finish(*writer), "{}"); +} + +TEST_P(JsonWriterContract, WritesEmptyArray) +{ + auto writer = MakeWriter(); + writer->BeginArray(); + writer->EndArray(); + EXPECT_EQ(Finish(*writer), "[]"); +} + +TEST_P(JsonWriterContract, WritesArray) +{ + auto writer = MakeWriter(); + writer->BeginArray(); + writer->WriteInt32(1); + writer->WriteString("two"); + writer->WriteNull(); + writer->EndArray(); + EXPECT_EQ(Finish(*writer), R"([1,"two",null])"); +} + +TEST_P(JsonWriterContract, WritesObjectInAnyMemberOrder) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key("b"); + writer->WriteString("two"); + writer->Key("a"); + writer->WriteInt32(1); + writer->EndObject(); + const std::string json = Finish(*writer); + EXPECT_TRUE(json == R"({"b":"two","a":1})" || json == R"({"a":1,"b":"two"})") << json; +} + +TEST_P(JsonWriterContract, WritesNestedContainers) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key("items"); + writer->BeginArray(); + writer->BeginObject(); + writer->Key("id"); + writer->WriteInt32(1); + writer->EndObject(); + writer->BeginArray(); + writer->EndArray(); + writer->BeginObject(); + writer->Key("empty"); + writer->BeginObject(); + writer->EndObject(); + writer->EndObject(); + writer->EndArray(); + writer->EndObject(); + EXPECT_EQ(Finish(*writer), R"({"items":[{"id":1},[],{"empty":{}}]})"); +} + +TEST_P(JsonWriterContract, EscapesKey) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key(std::string("\"\\\n\x01\0", 5)); + writer->WriteInt32(1); + writer->EndObject(); + EXPECT_EQ(Finish(*writer), "{\"\\\"\\\\\\n\\u0001\\u0000\":1}"); +} + +TEST_P(JsonWriterContract, ReplacesInvalidUtf8InKey) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key( + "a\xFF" + "b"); + writer->WriteInt32(1); + writer->EndObject(); + EXPECT_EQ(Finish(*writer), + "{\"a\xEF\xBF\xBD" + "b\":1}"); +} + +TEST_P(JsonWriterContract, CopiesKey) +{ + std::string key = "key"; + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key(key); + key.assign("xxx"); + writer->WriteInt32(1); + writer->EndObject(); + EXPECT_EQ(Finish(*writer), R"({"key":1})"); +} + +TEST_P(JsonWriterContract, KeyAtTopLevelFails) +{ + auto writer = MakeWriter(); + writer->Key("a"); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, KeyInArrayFails) +{ + auto writer = MakeWriter(); + writer->BeginArray(); + writer->Key("a"); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, KeyTwiceFails) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key("a"); + writer->Key("b"); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, ValueWithoutKeyFails) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->WriteInt32(1); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, ContainerWithoutKeyFails) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->BeginArray(); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, ObjectClosedAfterKeyFails) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->Key("a"); + writer->EndObject(); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, MismatchedEndFails) +{ + auto writer = MakeWriter(); + writer->BeginObject(); + writer->EndArray(); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, EndWithoutBeginFails) +{ + auto writer = MakeWriter(); + writer->EndObject(); + ExpectFailedOnce(*writer); +} + +TEST_P(JsonWriterContract, SecondTopLevelValueFails) +{ + auto writer = MakeWriter(); + writer->WriteInt32(1); + writer->WriteInt32(2); + ExpectFailedOnce(*writer); +} + +} // namespace test +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_json_writer_contract_test.h b/exporters/otlp/test/otlp_json_writer_contract_test.h new file mode 100644 index 0000000000..5ad985293b --- /dev/null +++ b/exporters/otlp/test/otlp_json_writer_contract_test.h @@ -0,0 +1,49 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" +#include "opentelemetry/sdk/common/global_log_handler.h" +#include "opentelemetry/test_common/sdk/common/scoped_test_log_handler.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ +namespace test +{ + +// Tests a JsonWriter backend against the contract documented in otlp_json_writer.h. The test of a +// backend compiles otlp_json_writer_contract_test.cc and instantiates the suite with its factory: +// +// INSTANTIATE_TEST_SUITE_P(Name, JsonWriterContract, +// ::testing::Values(std::make_shared())); +class JsonWriterContract : public ::testing::TestWithParam> +{ +protected: + std::unique_ptr MakeWriter() const; + + // Expects the writer to have accepted a well-formed stream without logging an error, and returns + // its document. + std::string Finish(JsonWriter &writer); + + // Expects the writer to have failed on a malformed stream, to stay failed through more tokens + // and ToString(), and to have logged the failure once. + void ExpectFailedOnce(JsonWriter &writer); + +private: + test_common::ScopedTestLogHandler log_{sdk::common::internal_log::LogLevel::Error}; +}; + +} // namespace test +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc b/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc index b874ead0ac..151866e049 100644 --- a/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc +++ b/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc @@ -1,229 +1,43 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#include "opentelemetry/exporters/otlp/otlp_json_writer.h" - #include -#include -#include #include -#include -#include -#include "opentelemetry/exporters/otlp/detail/default_json_writer_factory.h" -#include "opentelemetry/exporters/otlp/otlp_json_writer_factory.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer.h" +#include "opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h" #include "opentelemetry/version.h" +#include "otlp_json_writer_contract_test.h" OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { namespace otlp { -namespace -{ - -std::unique_ptr MakeWriter() -{ - return detail::GetDefaultJsonWriterFactory()->Create(); -} - -} // namespace - -TEST(NlohmannJsonWriter, FactoryReturnsAWorkingWriter) -{ - auto writer = MakeWriter(); - ASSERT_NE(writer, nullptr); - EXPECT_TRUE(writer->ok()); -} - -namespace -{ - -struct ScalarCase -{ - const char *name; - std::function write; - const char *expected; -}; - -void PrintTo(const ScalarCase &scalar_case, std::ostream *os) -{ - *os << scalar_case.name; -} - -class NlohmannJsonWriterScalar : public ::testing::TestWithParam -{}; - -} // namespace - -TEST_P(NlohmannJsonWriterScalar, WritesTopLevelScalar) -{ - auto writer = MakeWriter(); - GetParam().write(*writer); - EXPECT_TRUE(writer->ok()); - EXPECT_EQ(writer->ToString(), GetParam().expected); -} - -INSTANTIATE_TEST_SUITE_P( - Scalars, - NlohmannJsonWriterScalar, - ::testing::Values( - ScalarCase{"Null", [](JsonWriter &w) { w.WriteNull(); }, "null"}, - ScalarCase{"String", [](JsonWriter &w) { w.WriteString("hello"); }, "\"hello\""}, - ScalarCase{"Int32", [](JsonWriter &w) { w.WriteInt32(-7); }, "-7"}, - ScalarCase{"Int64", [](JsonWriter &w) { w.WriteInt64(-7000000000LL); }, "-7000000000"}, - ScalarCase{"UInt32", [](JsonWriter &w) { w.WriteUInt32(42u); }, "42"}, - ScalarCase{"UInt64", [](JsonWriter &w) { w.WriteUInt64(42000000000ULL); }, "42000000000"}, - ScalarCase{"Double", [](JsonWriter &w) { w.WriteDouble(3.5); }, "3.5"}, - ScalarCase{"Bool", [](JsonWriter &w) { w.WriteBool(true); }, "true"}, - // Base64 of 00 01 02 FF - ScalarCase{"Bytes", - [](JsonWriter &w) { - const std::uint8_t bytes[] = {0, 1, 2, 255}; - w.WriteBytes(bytes, sizeof(bytes)); - }, - "\"AAEC/w==\""}), - [](const ::testing::TestParamInfo &info) { return info.param.name; }); - -// The extremes of both 64-bit types are outside the range a double represents -// exactly. -TEST(NlohmannJsonWriter, PreservesFull64BitRangeExactly) +namespace test { - { - auto writer = MakeWriter(); - writer->WriteInt64(-9223372036854775807LL - 1); - EXPECT_EQ(writer->ToString(), "-9223372036854775808"); - } - { - auto writer = MakeWriter(); - writer->WriteInt64(9223372036854775807LL); - EXPECT_EQ(writer->ToString(), "9223372036854775807"); - } - { - auto writer = MakeWriter(); - writer->WriteUInt64(18446744073709551615ULL); - EXPECT_EQ(writer->ToString(), "18446744073709551615"); - } -} -TEST(NlohmannJsonWriter, WritesAnEmptyObject) -{ - auto writer = MakeWriter(); - writer->BeginObject(); - writer->EndObject(); - EXPECT_TRUE(writer->ok()); - EXPECT_EQ(writer->ToString(), "{}"); -} +INSTANTIATE_TEST_SUITE_P(Nlohmann, + JsonWriterContract, + ::testing::Values(std::make_shared())); -TEST(NlohmannJsonWriter, WritesAnEmptyArray) +TEST(NlohmannJsonWriter, SortsMembersByKey) { - auto writer = MakeWriter(); - writer->BeginArray(); - writer->EndArray(); - EXPECT_TRUE(writer->ok()); - EXPECT_EQ(writer->ToString(), "[]"); -} - -TEST(NlohmannJsonWriter, WritesAFlatObject) -{ - auto writer = MakeWriter(); + auto writer = JsonWriterFactoryNlohmann().Create(); writer->BeginObject(); - writer->Key("a"); - writer->WriteInt32(1); writer->Key("b"); - writer->WriteString("two"); - writer->EndObject(); - EXPECT_TRUE(writer->ok()); - EXPECT_EQ(writer->ToString(), R"({"a":1,"b":"two"})"); -} - -TEST(NlohmannJsonWriter, WritesAFlatArray) -{ - auto writer = MakeWriter(); writer->BeginArray(); writer->WriteInt32(1); writer->WriteInt32(2); - writer->WriteInt32(3); writer->EndArray(); - EXPECT_TRUE(writer->ok()); - EXPECT_EQ(writer->ToString(), "[1,2,3]"); -} - -TEST(NlohmannJsonWriter, WritesNestedObjectsAndArrays) -{ - auto writer = MakeWriter(); - writer->BeginObject(); - writer->Key("items"); - writer->BeginArray(); - writer->BeginObject(); - writer->Key("id"); - writer->WriteInt32(1); - writer->EndObject(); - writer->BeginObject(); - writer->Key("id"); - writer->WriteInt32(2); - writer->EndObject(); - writer->EndArray(); - writer->EndObject(); - EXPECT_TRUE(writer->ok()); - EXPECT_EQ(writer->ToString(), R"({"items":[{"id":1},{"id":2}]})"); -} - -TEST(NlohmannJsonWriter, KeyOutsideObjectFailsStickily) -{ - auto writer = MakeWriter(); - writer->BeginArray(); - writer->Key("bad"); - EXPECT_FALSE(writer->ok()); - - // Once failed, ok() stays false even if later calls look well-formed. - writer->WriteInt32(1); - writer->EndArray(); - EXPECT_FALSE(writer->ok()); -} - -TEST(NlohmannJsonWriter, ValueWithoutPrecedingKeyFails) -{ - auto writer = MakeWriter(); - writer->BeginObject(); - writer->WriteInt32(1); - EXPECT_FALSE(writer->ok()); -} - -TEST(NlohmannJsonWriter, MismatchedEndFails) -{ - auto writer = MakeWriter(); - writer->BeginObject(); - writer->EndArray(); - EXPECT_FALSE(writer->ok()); -} - -TEST(NlohmannJsonWriter, EndWithoutBeginFails) -{ - auto writer = MakeWriter(); + writer->Key("a"); + writer->WriteString("one"); writer->EndObject(); - EXPECT_FALSE(writer->ok()); -} - -TEST(NlohmannJsonWriter, SecondTopLevelValueFails) -{ - auto writer = MakeWriter(); - writer->WriteInt32(1); + EXPECT_EQ(writer->ToString(), R"({"a":"one","b":[1,2]})"); EXPECT_TRUE(writer->ok()); - writer->WriteInt32(2); - EXPECT_FALSE(writer->ok()); -} - -TEST(NlohmannJsonWriter, MisuseDoesNotCrashToString) -{ - auto writer = MakeWriter(); - writer->BeginObject(); - writer->EndArray(); - EXPECT_FALSE(writer->ok()); - // Must not throw or crash even though the writer failed. - writer->ToString(); } +} // namespace test } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE From f35605b2b914be6ab82923a5afa3d516ea010b97 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 11/20] Add a benchmark for OTLP/JSON serialization without I/O --- exporters/otlp/BUILD | 17 +++ exporters/otlp/CMakeLists.txt | 13 ++ .../otlp_file_client_json_benchmark.cc | 143 ++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index d6fd820084..331cf31aaa 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -1179,6 +1179,23 @@ otel_cc_benchmark( ], ) +otel_cc_benchmark( + name = "otlp_file_client_json_benchmark", + srcs = ["benchmark/otlp_file_client_json_benchmark.cc"], + tags = [ + "benchmark", + "otlp", + "otlp_file", + "test", + ], + deps = [ + ":otlp_file_client", + "//sdk/src/common:global_log_handler", + "@com_github_opentelemetry_proto//:common_proto_cc", + "@com_github_opentelemetry_proto//:trace_service_proto_cc", + ], +) + otel_cc_benchmark( name = "otlp_recordable_benchmark", srcs = ["test/otlp_recordable_benchmark.cc"], diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index c1882c282d..dad2c79cc8 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -1421,6 +1421,19 @@ if(OTELCPP_BUILD_TESTING ${CMAKE_THREAD_LIBS_INIT}) endif() +if(OTELCPP_BUILD_TESTING + AND OTELCPP_WITH_BENCHMARK + AND OTELCPP_WITH_OTLP_FILE + AND OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_file_client_json_benchmark + benchmark/otlp_file_client_json_benchmark.cc) + + target_link_libraries( + otlp_file_client_json_benchmark + PRIVATE opentelemetry_exporter_otlp_file_client benchmark::benchmark + ${CMAKE_THREAD_LIBS_INIT}) +endif() + if(OTELCPP_BUILD_TESTING AND OTELCPP_WITH_BENCHMARK) add_executable(otlp_recordable_benchmark test/otlp_recordable_benchmark.cc) diff --git a/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc b/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc new file mode 100644 index 0000000000..594a34a043 --- /dev/null +++ b/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc @@ -0,0 +1,143 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_file_client.h" +#include "opentelemetry/exporters/otlp/otlp_file_client_options.h" +#include "opentelemetry/exporters/otlp/otlp_file_client_runtime_options.h" +#include "opentelemetry/nostd/shared_ptr.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/sdk/common/global_log_handler.h" + +// clang-format off +#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep +#include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" +#include "opentelemetry/proto/common/v1/common.pb.h" +#include "opentelemetry/proto/resource/v1/resource.pb.h" +#include "opentelemetry/proto/trace/v1/trace.pb.h" +#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep +// clang-format on + +namespace otlp = opentelemetry::exporter::otlp; +namespace proto = opentelemetry::proto; +namespace nostd = opentelemetry::nostd; +namespace common_log = opentelemetry::sdk::common::internal_log; + +namespace +{ + +// Discards the serialized body, so a run measures the protobuf to JSON conversion and the +// serialization to a string, without file I/O. +class DiscardingAppender : public otlp::OtlpFileAppender +{ +public: + void Export(nostd::string_view data, std::size_t) override + { + benchmark::DoNotOptimize(data.data()); + } + + bool ForceFlush(std::chrono::microseconds) noexcept override { return true; } + + bool Shutdown(std::chrono::microseconds) noexcept override { return true; } +}; + +void AddAttribute(proto::common::v1::KeyValue *attribute, const std::string &key, std::int64_t i) +{ + attribute->set_key(key); + attribute->mutable_value()->set_int_value(i); +} + +void AddAttribute(proto::common::v1::KeyValue *attribute, + const std::string &key, + const std::string &value) +{ + attribute->set_key(key); + attribute->mutable_value()->set_string_value(value); +} + +// One resource and scope holding n_spans spans, each with a mix of attribute types, an event +// and a parent, so every JSON token kind and the bytes mapping take part. +proto::collector::trace::v1::ExportTraceServiceRequest MakeRequest(std::size_t n_spans) +{ + proto::collector::trace::v1::ExportTraceServiceRequest request; + auto *resource_spans = request.add_resource_spans(); + AddAttribute(resource_spans->mutable_resource()->add_attributes(), "service.name", + "otlp-json-benchmark"); + + auto *scope_spans = resource_spans->add_scope_spans(); + scope_spans->mutable_scope()->set_name("benchmark"); + scope_spans->mutable_scope()->set_version("1.0.0"); + + const std::string trace_id(16, '\x5a'); + for (std::size_t i = 0; i < n_spans; ++i) + { + auto *span = scope_spans->add_spans(); + span->set_trace_id(trace_id); + span->set_span_id(std::string(8, static_cast(i))); + span->set_parent_span_id(std::string(8, '\x01')); + span->set_name("GET /api/v1/resource"); + span->set_kind(proto::trace::v1::Span::SPAN_KIND_SERVER); + span->set_start_time_unix_nano(1700000000000000000ULL + i); + span->set_end_time_unix_nano(1700000000000100000ULL + i); + + AddAttribute(span->add_attributes(), "http.request.method", "GET"); + AddAttribute(span->add_attributes(), "url.path", "/api/v1/resource"); + AddAttribute(span->add_attributes(), "http.response.status_code", 200); + auto *ratio = span->add_attributes(); + ratio->set_key("sample.ratio"); + ratio->mutable_value()->set_double_value(0.25); + auto *retried = span->add_attributes(); + retried->set_key("retried"); + retried->mutable_value()->set_bool_value(false); + + auto *event = span->add_events(); + event->set_name("cache.miss"); + event->set_time_unix_nano(1700000000000050000ULL + i); + AddAttribute(event->add_attributes(), "cache.key", "resource:" + std::to_string(i)); + + span->mutable_status()->set_code(proto::trace::v1::Status::STATUS_CODE_OK); + } + return request; +} + +void BM_OtlpFileClient_Export_Json(benchmark::State &state) +{ + otlp::OtlpFileClientOptions options; + options.backend_options = nostd::shared_ptr(new DiscardingAppender()); + otlp::OtlpFileClient client(std::move(options), otlp::OtlpFileClientRuntimeOptions()); + + const auto n_spans = static_cast(state.range(0)); + const auto request = MakeRequest(n_spans); + + for (auto _ : state) + { + benchmark::DoNotOptimize(client.Export(request, n_spans)); + } + + state.SetItemsProcessed(state.iterations() * state.range(0)); +} + +BENCHMARK(BM_OtlpFileClient_Export_Json) + ->RangeMultiplier(10) + ->Range(1, 1000) + ->Unit(benchmark::kMicrosecond); + +} // namespace + +int main(int argc, char **argv) +{ + common_log::GlobalLogHandler::SetLogLevel(common_log::LogLevel::None); + + benchmark::Initialize(&argc, argv); + benchmark::RunSpecifiedBenchmarks(); + benchmark::Shutdown(); + return 0; +} From b9530dbbb6921efb39b06cc7e3fc6cc9945c6f9e Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 12/20] Avoid key copies and initializer_list construction in the nlohmann JSON writer --- .../otlp/src/otlp_json_writer_nlohmann.cc | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/exporters/otlp/src/otlp_json_writer_nlohmann.cc b/exporters/otlp/src/otlp_json_writer_nlohmann.cc index 07a4d9dc16..80fbeb42da 100644 --- a/exporters/otlp/src/otlp_json_writer_nlohmann.cc +++ b/exporters/otlp/src/otlp_json_writer_nlohmann.cc @@ -59,7 +59,24 @@ class NlohmannJsonWriter final : public JsonWriter Fail("Key() called twice without an intervening value"); return; } - pending_key_.assign(key.data(), key.size()); +#if OPENTELEMETRY_HAVE_EXCEPTIONS + try + { +#endif + pending_slot_ = &(*stack_.back().node)[std::string(key.data(), key.size())]; +#if OPENTELEMETRY_HAVE_EXCEPTIONS + } + catch (const std::exception &e) + { + Fail(e.what()); + return; + } + catch (...) + { + Fail("unknown exception"); + return; + } +#endif has_pending_key_ = true; } @@ -177,7 +194,7 @@ class NlohmannJsonWriter final : public JsonWriter return &frame.node->back(); } has_pending_key_ = false; - return &(*frame.node)[pending_key_]; + return pending_slot_; #if OPENTELEMETRY_HAVE_EXCEPTIONS } catch (const std::exception &e) @@ -203,7 +220,9 @@ class NlohmannJsonWriter final : public JsonWriter try { #endif - *slot = kind == Container::kObject ? nlohmann::json::object() : nlohmann::json::array(); + // Constructing from value_t skips the initializer_list path of object()/array(). + *slot = kind == Container::kObject ? nlohmann::json::value_t::object + : nlohmann::json::value_t::array; stack_.push_back(Frame{slot, kind}); #if OPENTELEMETRY_HAVE_EXCEPTIONS } @@ -249,10 +268,10 @@ class NlohmannJsonWriter final : public JsonWriter nlohmann::json root_; std::vector stack_; - std::string pending_key_; - bool has_pending_key_ = false; - bool root_set_ = false; - bool ok_ = true; + nlohmann::json *pending_slot_ = nullptr; + bool has_pending_key_ = false; + bool root_set_ = false; + bool ok_ = true; }; } // namespace From 94b2f641fa23a52ac1953a507132fd3cf5dcc5f0 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 13/20] Format hex IDs and 64-bit integers into stack buffers in the JSON converter --- exporters/otlp/BUILD | 19 ++ exporters/otlp/CMakeLists.txt | 9 + exporters/otlp/src/otlp_json_converter.cc | 74 ++++++-- .../otlp/test/otlp_json_converter_test.cc | 165 ++++++++++++++++++ 4 files changed, 257 insertions(+), 10 deletions(-) create mode 100644 exporters/otlp/test/otlp_json_converter_test.cc diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index 331cf31aaa..53d80de268 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -746,6 +746,25 @@ cc_library( ], ) +cc_test( + name = "otlp_json_converter_test", + srcs = [ + "test/otlp_json_converter_test.cc", + "test/otlp_stub_json_writer.h", + ], + tags = [ + "otlp", + "test", + ], + deps = [ + ":otlp_json_converter", + "@com_github_opentelemetry_proto//:common_proto_cc", + "@com_github_opentelemetry_proto//:metrics_service_proto_cc", + "@com_github_opentelemetry_proto//:trace_service_proto_cc", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "otlp_json_writer_nlohmann_test", srcs = [ diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index dad2c79cc8..bfa48d40d4 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -1029,6 +1029,15 @@ if(OTELCPP_BUILD_TESTING) TEST_PREFIX exporter.otlp. TEST_LIST otlp_metrics_serialization_test) + add_executable(otlp_json_converter_test test/otlp_json_converter_test.cc) + target_link_libraries( + otlp_json_converter_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + opentelemetry_exporter_otlp_json_converter protobuf::libprotobuf) + gtest_add_tests( + TARGET otlp_json_converter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_json_converter_test) + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) add_executable( otlp_json_writer_nlohmann_test test/otlp_json_writer_nlohmann_test.cc diff --git a/exporters/otlp/src/otlp_json_converter.cc b/exporters/otlp/src/otlp_json_converter.cc index 8510bab986..e8c3ee0690 100644 --- a/exporters/otlp/src/otlp_json_converter.cc +++ b/exporters/otlp/src/otlp_json_converter.cc @@ -69,6 +69,62 @@ std::string HexEncode(const std::string &bytes) return ret; } +// Hex-encodes into a stack buffer when the value fits, so trace and span IDs +// do not allocate a temporary string before the writer copies them. +void WriteHexString(JsonWriter &writer, const std::string &bytes) +{ + static constexpr std::size_t kStackBytes = 32; + if (bytes.size() > kStackBytes) + { + writer.WriteString(HexEncode(bytes)); + return; + } + char buffer[kStackBytes * 2]; + for (std::size_t i = 0; i < bytes.size(); ++i) + { + unsigned char byte = static_cast(bytes[i]); + buffer[2 * i] = HexEncode(static_cast(byte >> 4)); + buffer[2 * i + 1] = HexEncode(static_cast(byte & 0x0f)); + } + writer.WriteString(nostd::string_view(buffer, bytes.size() * 2)); +} + +// Formats right-aligned into the end of buffer and returns the first digit. +char *FormatDecimal(char *end, std::uint64_t value) +{ + do + { + *--end = static_cast('0' + value % 10); + value /= 10; + } while (value != 0); + return end; +} + +// 64-bit integers are JSON strings in OTLP/JSON; formatting into a stack +// buffer avoids a temporary std::string that the writer would copy again. +void WriteUInt64String(JsonWriter &writer, std::uint64_t value) +{ + char buffer[20]; + char *end = buffer + sizeof(buffer); + char *begin = FormatDecimal(end, value); + writer.WriteString(nostd::string_view(begin, static_cast(end - begin))); +} + +void WriteInt64String(JsonWriter &writer, std::int64_t value) +{ + char buffer[21]; + char *end = buffer + sizeof(buffer); + // Negate as unsigned so INT64_MIN does not overflow. + std::uint64_t magnitude = + value < 0 ? 0 - static_cast(value) : static_cast(value); + char *begin = FormatDecimal(end, magnitude); + if (value < 0) + { + *--begin = '-'; + } + writer.WriteString(nostd::string_view(begin, static_cast(end - begin))); +} + void WriteBytesField(JsonWriter &writer, const std::string &bytes, const google::protobuf::FieldDescriptor *field_descriptor, @@ -81,7 +137,7 @@ void WriteBytesField(JsonWriter &writer, field_descriptor->lowercase_name() == "span_id" || field_descriptor->lowercase_name() == "parent_span_id") { - writer.WriteString(HexEncode(bytes)); + WriteHexString(writer, bytes); } else { @@ -95,7 +151,7 @@ void WriteBytesField(JsonWriter &writer, break; } case JsonBytesMappingKind::kHex: - writer.WriteString(HexEncode(bytes)); + WriteHexString(writer, bytes); break; default: writer.WriteString(bytes); @@ -118,8 +174,7 @@ void ConvertGenericFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as // decimal strings, and either numbers or strings are accepted when decoding. - writer.WriteString( - std::to_string(message.GetReflection()->GetInt64(message, field_descriptor))); + WriteInt64String(writer, message.GetReflection()->GetInt64(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { @@ -129,8 +184,7 @@ void ConvertGenericFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as // decimal strings, and either numbers or strings are accepted when decoding. - writer.WriteString( - std::to_string(message.GetReflection()->GetUInt64(message, field_descriptor))); + WriteUInt64String(writer, message.GetReflection()->GetUInt64(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { @@ -200,8 +254,8 @@ void ConvertListFieldToJson(JsonWriter &writer, { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded // as decimal strings, and either numbers or strings are accepted when decoding. - writer.WriteString(std::to_string( - message.GetReflection()->GetRepeatedInt64(message, field_descriptor, i))); + WriteInt64String(writer, + message.GetReflection()->GetRepeatedInt64(message, field_descriptor, i)); } break; @@ -220,8 +274,8 @@ void ConvertListFieldToJson(JsonWriter &writer, { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded // as decimal strings, and either numbers or strings are accepted when decoding. - writer.WriteString(std::to_string( - message.GetReflection()->GetRepeatedUInt64(message, field_descriptor, i))); + WriteUInt64String(writer, + message.GetReflection()->GetRepeatedUInt64(message, field_descriptor, i)); } break; diff --git a/exporters/otlp/test/otlp_json_converter_test.cc b/exporters/otlp/test/otlp_json_converter_test.cc new file mode 100644 index 0000000000..9652974d14 --- /dev/null +++ b/exporters/otlp/test/otlp_json_converter_test.cc @@ -0,0 +1,165 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/otlp/otlp_json_converter.h" + +#include +#include +#include +#include +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_http.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/version.h" +#include "otlp_stub_json_writer.h" + +// clang-format off +#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep +#include "opentelemetry/proto/common/v1/common.pb.h" +#include "opentelemetry/proto/metrics/v1/metrics.pb.h" +#include "opentelemetry/proto/trace/v1/trace.pb.h" +#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" // IWYU pragma: keep +// clang-format on + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ +namespace +{ + +std::vector ConvertStrings(const google::protobuf::Message &message, + JsonBytesMappingKind bytes_mapping) +{ + std::vector strings; + test::StubJsonWriter writer; + writer.on_write_string = [&strings](nostd::string_view value) { + strings.emplace_back(value.data(), value.size()); + }; + ConvertGenericMessageToJson(writer, message, JsonConverterOptions{false, bytes_mapping}); + return strings; +} + +} // namespace + +class OtlpJsonConverterSigned64BitInteger : public ::testing::TestWithParam +{}; + +TEST_P(OtlpJsonConverterSigned64BitInteger, WritesAnExactDecimalString) +{ + proto::common::v1::AnyValue any_value; + any_value.set_int_value(GetParam()); + EXPECT_EQ(ConvertStrings(any_value, JsonBytesMappingKind::kHexId), + std::vector{std::to_string(GetParam())}); +} + +INSTANTIATE_TEST_SUITE_P(Values, + OtlpJsonConverterSigned64BitInteger, + ::testing::Values(std::numeric_limits::min(), + std::int64_t{-1}, + std::int64_t{0}, + std::int64_t{10}, + std::numeric_limits::max())); + +class OtlpJsonConverterUnsigned64BitInteger : public ::testing::TestWithParam +{}; + +// count is a singular field and bucket_counts a repeated one, so both code paths are checked. +TEST_P(OtlpJsonConverterUnsigned64BitInteger, WritesAnExactDecimalString) +{ + proto::metrics::v1::HistogramDataPoint point; + point.set_count(GetParam()); + point.add_bucket_counts(GetParam()); + + const std::string decimal = std::to_string(GetParam()); + std::vector expected; + // proto3 does not serialize a singular field left at its default, so a zero count is omitted. + if (GetParam() != 0) + { + expected.push_back(decimal); + } + expected.push_back(decimal); + EXPECT_EQ(ConvertStrings(point, JsonBytesMappingKind::kHexId), expected); +} + +INSTANTIATE_TEST_SUITE_P(Values, + OtlpJsonConverterUnsigned64BitInteger, + ::testing::Values(std::uint64_t{0}, + std::uint64_t{9}, + std::numeric_limits::max())); + +struct HexIdCase +{ + const char *name; + void (*set)(proto::trace::v1::Span &span, const std::string &id); + std::string id; + const char *expected; +}; + +void PrintTo(const HexIdCase &hex_id_case, std::ostream *os) +{ + *os << hex_id_case.name; +} + +class OtlpJsonConverterHexId : public ::testing::TestWithParam +{}; + +TEST_P(OtlpJsonConverterHexId, WritesLowercaseHex) +{ + proto::trace::v1::Span span; + GetParam().set(span, GetParam().id); + EXPECT_EQ(ConvertStrings(span, JsonBytesMappingKind::kHexId), + std::vector{GetParam().expected}); +} + +INSTANTIATE_TEST_SUITE_P( + Fields, + OtlpJsonConverterHexId, + ::testing::Values( + HexIdCase{ + "TraceId", + [](proto::trace::v1::Span &span, const std::string &id) { span.set_trace_id(id); }, + std::string("\x00\x01\x7f\x80\xab\xcd\xef\xff\x10\x20\x30\x40\x50\x60\x70\x90", 16), + "00017f80abcdefff1020304050607090"}, + HexIdCase{"SpanId", + [](proto::trace::v1::Span &span, const std::string &id) { span.set_span_id(id); }, + std::string("\xde\xad\xbe\xef\x00\x00\x00\x01", 8), "deadbeef00000001"}, + HexIdCase{"ParentSpanId", + [](proto::trace::v1::Span &span, const std::string &id) { + span.set_parent_span_id(id); + }, + std::string("\x00\xff\x00\xff\x12\x34\x56\x78", 8), "00ff00ff12345678"}), + [](const ::testing::TestParamInfo &info) { return info.param.name; }); + +class OtlpJsonConverterHexBytes : public ::testing::TestWithParam +{}; + +TEST_P(OtlpJsonConverterHexBytes, HexEncodesEveryByte) +{ + proto::common::v1::AnyValue any_value; + any_value.set_bytes_value(std::string(GetParam(), '\xa5')); + + std::string expected; + for (std::size_t i = 0; i < GetParam(); ++i) + { + expected += "a5"; + } + EXPECT_EQ(ConvertStrings(any_value, JsonBytesMappingKind::kHex), + std::vector{expected}); +} + +// 32 and 33 bytes sit on either side of the converter's stack buffer. +INSTANTIATE_TEST_SUITE_P( + Sizes, + OtlpJsonConverterHexBytes, + ::testing::Values(std::size_t{0}, std::size_t{32}, std::size_t{33}, std::size_t{100}), + [](const ::testing::TestParamInfo &info) { + return "Bytes" + std::to_string(info.param); + }); + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE From ff92b5bb893209046205e10c6c52dff10bc2f288 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 12:21:34 +0200 Subject: [PATCH 14/20] Fetch reflection once per message in the JSON converter --- exporters/otlp/src/otlp_json_converter.cc | 79 +++++++++---------- .../otlp/test/otlp_json_converter_test.cc | 34 ++++++++ 2 files changed, 71 insertions(+), 42 deletions(-) diff --git a/exporters/otlp/src/otlp_json_converter.cc b/exporters/otlp/src/otlp_json_converter.cc index e8c3ee0690..b7ed8dc28f 100644 --- a/exporters/otlp/src/otlp_json_converter.cc +++ b/exporters/otlp/src/otlp_json_converter.cc @@ -160,67 +160,68 @@ void WriteBytesField(JsonWriter &writer, } // NOLINTBEGIN(misc-no-recursion) +// The reflection is passed in because Message::GetReflection() is not free: every call goes +// through the descriptor table's std::call_once. void ConvertGenericFieldToJson(JsonWriter &writer, const google::protobuf::Message &message, + const google::protobuf::Reflection &reflection, const google::protobuf::FieldDescriptor *field_descriptor, const JsonConverterOptions &options) { switch (field_descriptor->cpp_type()) { case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { - writer.WriteInt32(message.GetReflection()->GetInt32(message, field_descriptor)); + writer.WriteInt32(reflection.GetInt32(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as // decimal strings, and either numbers or strings are accepted when decoding. - WriteInt64String(writer, message.GetReflection()->GetInt64(message, field_descriptor)); + WriteInt64String(writer, reflection.GetInt64(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { - writer.WriteUInt32(message.GetReflection()->GetUInt32(message, field_descriptor)); + writer.WriteUInt32(reflection.GetUInt32(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as // decimal strings, and either numbers or strings are accepted when decoding. - WriteUInt64String(writer, message.GetReflection()->GetUInt64(message, field_descriptor)); + WriteUInt64String(writer, reflection.GetUInt64(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { std::string empty; if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) { - WriteBytesField( - writer, message.GetReflection()->GetStringReference(message, field_descriptor, &empty), - field_descriptor, options.json_bytes_mapping); + WriteBytesField(writer, reflection.GetStringReference(message, field_descriptor, &empty), + field_descriptor, options.json_bytes_mapping); } else { - writer.WriteString( - message.GetReflection()->GetStringReference(message, field_descriptor, &empty)); + writer.WriteString(reflection.GetStringReference(message, field_descriptor, &empty)); } break; } case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { - ConvertGenericMessageToJson( - writer, message.GetReflection()->GetMessage(message, field_descriptor, nullptr), options); + ConvertGenericMessageToJson(writer, reflection.GetMessage(message, field_descriptor, nullptr), + options); break; } case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { - writer.WriteDouble(message.GetReflection()->GetDouble(message, field_descriptor)); + writer.WriteDouble(reflection.GetDouble(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { - writer.WriteDouble(message.GetReflection()->GetFloat(message, field_descriptor)); + writer.WriteDouble(reflection.GetFloat(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { - writer.WriteBool(message.GetReflection()->GetBool(message, field_descriptor)); + writer.WriteBool(reflection.GetBool(message, field_descriptor)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { - writer.WriteInt32(message.GetReflection()->GetEnumValue(message, field_descriptor)); + writer.WriteInt32(reflection.GetEnumValue(message, field_descriptor)); break; } default: { @@ -232,10 +233,11 @@ void ConvertGenericFieldToJson(JsonWriter &writer, void ConvertListFieldToJson(JsonWriter &writer, const google::protobuf::Message &message, + const google::protobuf::Reflection &reflection, const google::protobuf::FieldDescriptor *field_descriptor, const JsonConverterOptions &options) { - auto field_size = message.GetReflection()->FieldSize(message, field_descriptor); + auto field_size = reflection.FieldSize(message, field_descriptor); writer.BeginArray(); @@ -244,7 +246,7 @@ void ConvertListFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { for (int i = 0; i < field_size; ++i) { - writer.WriteInt32(message.GetReflection()->GetRepeatedInt32(message, field_descriptor, i)); + writer.WriteInt32(reflection.GetRepeatedInt32(message, field_descriptor, i)); } break; @@ -254,8 +256,7 @@ void ConvertListFieldToJson(JsonWriter &writer, { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded // as decimal strings, and either numbers or strings are accepted when decoding. - WriteInt64String(writer, - message.GetReflection()->GetRepeatedInt64(message, field_descriptor, i)); + WriteInt64String(writer, reflection.GetRepeatedInt64(message, field_descriptor, i)); } break; @@ -263,8 +264,7 @@ void ConvertListFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { for (int i = 0; i < field_size; ++i) { - writer.WriteUInt32( - message.GetReflection()->GetRepeatedUInt32(message, field_descriptor, i)); + writer.WriteUInt32(reflection.GetRepeatedUInt32(message, field_descriptor, i)); } break; @@ -274,8 +274,7 @@ void ConvertListFieldToJson(JsonWriter &writer, { // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded // as decimal strings, and either numbers or strings are accepted when decoding. - WriteUInt64String(writer, - message.GetReflection()->GetRepeatedUInt64(message, field_descriptor, i)); + WriteUInt64String(writer, reflection.GetRepeatedUInt64(message, field_descriptor, i)); } break; @@ -286,18 +285,17 @@ void ConvertListFieldToJson(JsonWriter &writer, { for (int i = 0; i < field_size; ++i) { - WriteBytesField(writer, - message.GetReflection()->GetRepeatedStringReference( - message, field_descriptor, i, &empty), - field_descriptor, options.json_bytes_mapping); + WriteBytesField( + writer, reflection.GetRepeatedStringReference(message, field_descriptor, i, &empty), + field_descriptor, options.json_bytes_mapping); } } else { for (int i = 0; i < field_size; ++i) { - writer.WriteString(message.GetReflection()->GetRepeatedStringReference( - message, field_descriptor, i, &empty)); + writer.WriteString( + reflection.GetRepeatedStringReference(message, field_descriptor, i, &empty)); } } break; @@ -306,8 +304,7 @@ void ConvertListFieldToJson(JsonWriter &writer, for (int i = 0; i < field_size; ++i) { ConvertGenericMessageToJson( - writer, message.GetReflection()->GetRepeatedMessage(message, field_descriptor, i), - options); + writer, reflection.GetRepeatedMessage(message, field_descriptor, i), options); } break; @@ -315,8 +312,7 @@ void ConvertListFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { for (int i = 0; i < field_size; ++i) { - writer.WriteDouble( - message.GetReflection()->GetRepeatedDouble(message, field_descriptor, i)); + writer.WriteDouble(reflection.GetRepeatedDouble(message, field_descriptor, i)); } break; @@ -324,7 +320,7 @@ void ConvertListFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { for (int i = 0; i < field_size; ++i) { - writer.WriteDouble(message.GetReflection()->GetRepeatedFloat(message, field_descriptor, i)); + writer.WriteDouble(reflection.GetRepeatedFloat(message, field_descriptor, i)); } break; @@ -332,7 +328,7 @@ void ConvertListFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { for (int i = 0; i < field_size; ++i) { - writer.WriteBool(message.GetReflection()->GetRepeatedBool(message, field_descriptor, i)); + writer.WriteBool(reflection.GetRepeatedBool(message, field_descriptor, i)); } break; @@ -340,8 +336,7 @@ void ConvertListFieldToJson(JsonWriter &writer, case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { for (int i = 0; i < field_size; ++i) { - writer.WriteInt32( - message.GetReflection()->GetRepeatedEnumValue(message, field_descriptor, i)); + writer.WriteInt32(reflection.GetRepeatedEnumValue(message, field_descriptor, i)); } break; } @@ -359,8 +354,9 @@ void ConvertGenericMessageToJson(JsonWriter &writer, const google::protobuf::Message &message, const JsonConverterOptions &options) { + const google::protobuf::Reflection &reflection = *message.GetReflection(); std::vector fields_with_data; - message.GetReflection()->ListFields(message, &fields_with_data); + reflection.ListFields(message, &fields_with_data); // A message with no set fields serializes as null, not as an empty object. if (fields_with_data.empty()) @@ -370,19 +366,18 @@ void ConvertGenericMessageToJson(JsonWriter &writer, } writer.BeginObject(); - for (std::size_t i = 0; i < fields_with_data.size(); ++i) + for (const google::protobuf::FieldDescriptor *field_descriptor : fields_with_data) { - const google::protobuf::FieldDescriptor *field_descriptor = fields_with_data[i]; const auto &field_name = options.use_json_name ? field_descriptor->json_name() : field_descriptor->camelcase_name(); writer.Key(nostd::string_view(field_name.data(), field_name.size())); if (field_descriptor->is_repeated()) { - ConvertListFieldToJson(writer, message, field_descriptor, options); + ConvertListFieldToJson(writer, message, reflection, field_descriptor, options); } else { - ConvertGenericFieldToJson(writer, message, field_descriptor, options); + ConvertGenericFieldToJson(writer, message, reflection, field_descriptor, options); } } writer.EndObject(); diff --git a/exporters/otlp/test/otlp_json_converter_test.cc b/exporters/otlp/test/otlp_json_converter_test.cc index 9652974d14..d0d6efcf19 100644 --- a/exporters/otlp/test/otlp_json_converter_test.cc +++ b/exporters/otlp/test/otlp_json_converter_test.cc @@ -160,6 +160,40 @@ INSTANTIATE_TEST_SUITE_P( return "Bytes" + std::to_string(info.param); }); +class OtlpJsonConverterNesting : public ::testing::TestWithParam +{}; + +TEST_P(OtlpJsonConverterNesting, ContinuesOuterFieldsAfterANestedMessage) +{ + proto::common::v1::AnyValue root; + proto::common::v1::AnyValue *current = &root; + std::vector expected; + for (int level = 0; level < GetParam(); ++level) + { + auto *key_value = current->mutable_kvlist_value()->add_values(); + key_value->set_key("k" + std::to_string(level)); + expected.push_back(key_value->key()); + current = key_value->mutable_value(); + } + current->set_string_value("leaf"); + expected.push_back("leaf"); + + auto *after = root.mutable_kvlist_value()->add_values(); + after->set_key("after"); + after->mutable_value()->set_string_value("after-value"); + expected.push_back("after"); + expected.push_back("after-value"); + + EXPECT_EQ(ConvertStrings(root, JsonBytesMappingKind::kHexId), expected); +} + +INSTANTIATE_TEST_SUITE_P(Levels, + OtlpJsonConverterNesting, + ::testing::Values(1, 4, 12), + [](const ::testing::TestParamInfo &info) { + return "Levels" + std::to_string(info.param); + }); + } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE From 07b30c869b5c89fd445a9ae5a2bbfdb261cbeb81 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 17:42:50 +0200 Subject: [PATCH 15/20] Make the concurrency contract test constants static for MSVC --- exporters/otlp/test/otlp_json_writer_contract_test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exporters/otlp/test/otlp_json_writer_contract_test.cc b/exporters/otlp/test/otlp_json_writer_contract_test.cc index 625e163830..23ff962a2e 100644 --- a/exporters/otlp/test/otlp_json_writer_contract_test.cc +++ b/exporters/otlp/test/otlp_json_writer_contract_test.cc @@ -89,8 +89,8 @@ TEST_P(JsonWriterContract, FactoryCreatesAWorkingWriter) TEST_P(JsonWriterContract, FactoryCreatesWritersConcurrently) { - constexpr int kThreads = 8; - constexpr int kWritersPerThread = 200; + static constexpr int kThreads = 8; + static constexpr int kWritersPerThread = 200; std::vector failures(kThreads); std::vector threads; for (int t = 0; t < kThreads; ++t) From a07346a2a6380218c0c3c866f4a3ba184991306b Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 13 Sep 2026 19:37:21 +0200 Subject: [PATCH 16/20] Fix clang-tidy and include-what-you-use findings in the OTLP JSON tests --- .../benchmark/otlp_file_client_json_benchmark.cc | 1 - .../test/otlp_http_exporter_custom_client_test.cc | 1 + .../otlp_http_exporter_custom_json_writer_test.cc | 1 + exporters/otlp/test/otlp_json_converter_test.cc | 13 +++++++------ .../otlp/test/otlp_json_writer_contract_test.cc | 14 ++++++++++---- .../otlp/test/otlp_json_writer_nlohmann_test.cc | 1 + 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc b/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc index 594a34a043..fdf156f74d 100644 --- a/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc +++ b/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc @@ -6,7 +6,6 @@ #include #include #include -#include #include #include diff --git a/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc index 4103586345..e919e74d05 100644 --- a/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc @@ -10,6 +10,7 @@ #include #include "gmock/gmock.h" +#include "opentelemetry/exporters/otlp/otlp_http.h" #include "opentelemetry/exporters/otlp/otlp_http_client.h" #include "opentelemetry/exporters/otlp/otlp_http_exporter.h" #include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" diff --git a/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc index a9d87b4eb9..50ca4ff21a 100644 --- a/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include #include "gmock/gmock.h" diff --git a/exporters/otlp/test/otlp_json_converter_test.cc b/exporters/otlp/test/otlp_json_converter_test.cc index d0d6efcf19..e9a3d59158 100644 --- a/exporters/otlp/test/otlp_json_converter_test.cc +++ b/exporters/otlp/test/otlp_json_converter_test.cc @@ -4,7 +4,9 @@ #include "opentelemetry/exporters/otlp/otlp_json_converter.h" #include +#include #include +#include #include #include #include @@ -43,8 +45,6 @@ std::vector ConvertStrings(const google::protobuf::Message &message return strings; } -} // namespace - class OtlpJsonConverterSigned64BitInteger : public ::testing::TestWithParam {}; @@ -172,17 +172,17 @@ TEST_P(OtlpJsonConverterNesting, ContinuesOuterFieldsAfterANestedMessage) { auto *key_value = current->mutable_kvlist_value()->add_values(); key_value->set_key("k" + std::to_string(level)); - expected.push_back(key_value->key()); + expected.emplace_back(key_value->key()); current = key_value->mutable_value(); } current->set_string_value("leaf"); - expected.push_back("leaf"); + expected.emplace_back("leaf"); auto *after = root.mutable_kvlist_value()->add_values(); after->set_key("after"); after->mutable_value()->set_string_value("after-value"); - expected.push_back("after"); - expected.push_back("after-value"); + expected.emplace_back("after"); + expected.emplace_back("after-value"); EXPECT_EQ(ConvertStrings(root, JsonBytesMappingKind::kHexId), expected); } @@ -194,6 +194,7 @@ INSTANTIATE_TEST_SUITE_P(Levels, return "Levels" + std::to_string(info.param); }); +} // namespace } // namespace otlp } // namespace exporter OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_json_writer_contract_test.cc b/exporters/otlp/test/otlp_json_writer_contract_test.cc index 23ff962a2e..e3f23db25b 100644 --- a/exporters/otlp/test/otlp_json_writer_contract_test.cc +++ b/exporters/otlp/test/otlp_json_writer_contract_test.cc @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -93,6 +92,7 @@ TEST_P(JsonWriterContract, FactoryCreatesWritersConcurrently) static constexpr int kWritersPerThread = 200; std::vector failures(kThreads); std::vector threads; + threads.reserve(kThreads); for (int t = 0; t < kThreads; ++t) { threads.emplace_back([this, t, &failures] { @@ -108,11 +108,17 @@ TEST_P(JsonWriterContract, FactoryCreatesWritersConcurrently) writer->WriteInt32(t); writer->WriteInt32(i); writer->EndArray(); - const std::string expected = "[" + std::to_string(t) + "," + std::to_string(i) + "]"; - const std::string json = writer->ToString(); + std::string expected = "["; + expected += std::to_string(t); + expected += ","; + expected += std::to_string(i); + expected += "]"; + const std::string json = writer->ToString(); if (!writer->ok() || json != expected) { - failures[t] = "expected " + expected + ", got " + json; + failures[t] = "expected " + expected; + failures[t] += ", got "; + failures[t] += json; } } }); diff --git a/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc b/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc index 151866e049..58ae6f2bc2 100644 --- a/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc +++ b/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include "opentelemetry/exporters/otlp/otlp_json_writer.h" #include "opentelemetry/exporters/otlp/otlp_json_writer_factory_nlohmann.h" From 660bdebb49798807187be73914009d12de6712e1 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Sun, 20 Sep 2026 23:02:24 +0200 Subject: [PATCH 17/20] Build the default file exporter factory tests only with a JSON writer backend --- exporters/otlp/BUILD | 48 +++++++++++++++++++ exporters/otlp/CMakeLists.txt | 35 ++++++++++++++ ..._file_exporter_default_json_writer_test.cc | 29 +++++++++++ .../test/otlp_file_exporter_factory_test.cc | 9 ---- ...ecord_exporter_default_json_writer_test.cc | 29 +++++++++++ ...p_file_log_record_exporter_factory_test.cc | 9 ---- ...etric_exporter_default_json_writer_test.cc | 29 +++++++++++ .../otlp_file_metric_exporter_factory_test.cc | 9 ---- 8 files changed, 170 insertions(+), 27 deletions(-) create mode 100644 exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc create mode 100644 exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc create mode 100644 exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index 53d80de268..d21418a86a 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -1182,6 +1182,54 @@ cc_test( ], ) +cc_test( + name = "otlp_file_exporter_default_json_writer_test", + srcs = ["test/otlp_file_exporter_default_json_writer_test.cc"], + tags = [ + "otlp", + "otlp_file", + "test", + ], + deps = [ + ":otlp_file_exporter", + "//api", + "//sdk/src/metrics", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "otlp_file_log_record_exporter_default_json_writer_test", + srcs = ["test/otlp_file_log_record_exporter_default_json_writer_test.cc"], + tags = [ + "otlp", + "otlp_file", + "test", + ], + deps = [ + ":otlp_file_log_record_exporter", + "//api", + "//sdk/src/metrics", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "otlp_file_metric_exporter_default_json_writer_test", + srcs = ["test/otlp_file_metric_exporter_default_json_writer_test.cc"], + tags = [ + "otlp", + "otlp_file", + "test", + ], + deps = [ + ":otlp_file_metric_exporter", + "//api", + "//sdk/src/metrics", + "@com_google_googletest//:gtest_main", + ], +) + otel_cc_benchmark( name = "otlp_grpc_exporter_benchmark", srcs = ["test/otlp_grpc_exporter_benchmark.cc"], diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index bfa48d40d4..1764272eff 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -1414,6 +1414,41 @@ if(OTELCPP_BUILD_TESTING) TARGET otlp_file_metric_exporter_factory_test TEST_PREFIX exporter.otlp. TEST_LIST otlp_file_metric_exporter_factory_test) + + if(OTELCPP_WITH_JSON_WRITER_NLOHMANN) + add_executable(otlp_file_exporter_default_json_writer_test + test/otlp_file_exporter_default_json_writer_test.cc) + target_link_libraries( + otlp_file_exporter_default_json_writer_test ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} ${GMOCK_LIB} opentelemetry_exporter_otlp_file) + gtest_add_tests( + TARGET otlp_file_exporter_default_json_writer_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_file_exporter_default_json_writer_test) + + add_executable( + otlp_file_log_record_exporter_default_json_writer_test + test/otlp_file_log_record_exporter_default_json_writer_test.cc) + target_link_libraries( + otlp_file_log_record_exporter_default_json_writer_test + ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${GMOCK_LIB} + opentelemetry_exporter_otlp_file_log opentelemetry_logs) + gtest_add_tests( + TARGET otlp_file_log_record_exporter_default_json_writer_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_file_log_record_exporter_default_json_writer_test) + + add_executable(otlp_file_metric_exporter_default_json_writer_test + test/otlp_file_metric_exporter_default_json_writer_test.cc) + target_link_libraries( + otlp_file_metric_exporter_default_json_writer_test + ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${GMOCK_LIB} + opentelemetry_exporter_otlp_file_metric opentelemetry_metrics) + gtest_add_tests( + TARGET otlp_file_metric_exporter_default_json_writer_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_file_metric_exporter_default_json_writer_test) + endif() endif() endif() # OTELCPP_BUILD_TESTING diff --git a/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc b/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc new file mode 100644 index 0000000000..ae30394d52 --- /dev/null +++ b/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_file_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_file_exporter_options.h" +#include "opentelemetry/sdk/trace/exporter.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +TEST(OtlpFileExporterFactoryTest, BuildTest) +{ + OtlpFileExporterOptions opts; + std::unique_ptr exporter = + OtlpFileExporterFactory::Create(opts); + + EXPECT_TRUE(exporter != nullptr); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_exporter_factory_test.cc index c3572489c7..ff4c43e72b 100644 --- a/exporters/otlp/test/otlp_file_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_exporter_factory_test.cc @@ -35,15 +35,6 @@ namespace exporter namespace otlp { -TEST(OtlpFileExporterFactoryTest, BuildTest) -{ - OtlpFileExporterOptions opts; - std::unique_ptr exporter = - OtlpFileExporterFactory::Create(opts); - - EXPECT_TRUE(exporter != nullptr); -} - TEST(OtlpFileExporterFactoryTest, BuildWithJsonWriterFactoryTest) { OtlpFileExporterOptions opts; diff --git a/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc b/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc new file mode 100644 index 0000000000..30802dd394 --- /dev/null +++ b/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_options.h" +#include "opentelemetry/sdk/logs/exporter.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +TEST(OtlpFileLogRecordExporterFactoryTest, BuildTest) +{ + OtlpFileLogRecordExporterOptions opts; + std::unique_ptr exporter = + OtlpFileLogRecordExporterFactory::Create(opts); + + EXPECT_TRUE(exporter != nullptr); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc index 9d42ec77b7..9feb01e233 100644 --- a/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_log_record_exporter_factory_test.cc @@ -35,15 +35,6 @@ namespace exporter namespace otlp { -TEST(OtlpFileLogRecordExporterFactoryTest, BuildTest) -{ - OtlpFileLogRecordExporterOptions opts; - std::unique_ptr exporter = - OtlpFileLogRecordExporterFactory::Create(opts); - - EXPECT_TRUE(exporter != nullptr); -} - TEST(OtlpFileLogRecordExporterFactoryTest, BuildWithJsonWriterFactoryTest) { OtlpFileLogRecordExporterOptions opts; diff --git a/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc new file mode 100644 index 0000000000..4ae6767d2c --- /dev/null +++ b/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include "opentelemetry/exporters/otlp/otlp_file_metric_exporter_factory.h" +#include "opentelemetry/exporters/otlp/otlp_file_metric_exporter_options.h" +#include "opentelemetry/sdk/metrics/push_metric_exporter.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +TEST(OtlpFileMetricExporterFactory, BuildTest) +{ + OtlpFileMetricExporterOptions opts; + std::unique_ptr exporter = + OtlpFileMetricExporterFactory::Create(opts); + + EXPECT_TRUE(exporter != nullptr); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc index c429699617..427625db38 100644 --- a/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc +++ b/exporters/otlp/test/otlp_file_metric_exporter_factory_test.cc @@ -35,15 +35,6 @@ namespace exporter namespace otlp { -TEST(OtlpFileMetricExporterFactory, BuildTest) -{ - OtlpFileMetricExporterOptions opts; - std::unique_ptr exporter = - OtlpFileMetricExporterFactory::Create(opts); - - EXPECT_TRUE(exporter != nullptr); -} - TEST(OtlpFileMetricExporterFactory, BuildWithJsonWriterFactoryTest) { OtlpFileMetricExporterOptions opts; From 5e5c295f5b3411d48b1400194f638b22ae582204 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Mon, 21 Sep 2026 12:55:46 +0200 Subject: [PATCH 18/20] Guard the default file exporter factory Bazel tests without a JSON writer backend --- exporters/otlp/BUILD | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index d21418a86a..fbf40f2e4d 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -1190,6 +1190,10 @@ cc_test( "otlp_file", "test", ], + target_compatible_with = select({ + ":json_writer_nlohmann_enabled": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ ":otlp_file_exporter", "//api", @@ -1206,6 +1210,10 @@ cc_test( "otlp_file", "test", ], + target_compatible_with = select({ + ":json_writer_nlohmann_enabled": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ ":otlp_file_log_record_exporter", "//api", @@ -1222,6 +1230,10 @@ cc_test( "otlp_file", "test", ], + target_compatible_with = select({ + ":json_writer_nlohmann_enabled": [], + "//conditions:default": ["@platforms//:incompatible"], + }), deps = [ ":otlp_file_metric_exporter", "//api", From 08f3cdf4c42943d9c5ddd9aa0f932bf4a2cccba9 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Mon, 21 Sep 2026 12:55:46 +0200 Subject: [PATCH 19/20] Add missing string includes reported by include-what-you-use --- .../otlp/test/otlp_file_exporter_default_json_writer_test.cc | 1 + .../otlp_file_log_record_exporter_default_json_writer_test.cc | 1 + .../test/otlp_file_metric_exporter_default_json_writer_test.cc | 1 + 3 files changed, 3 insertions(+) diff --git a/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc b/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc index ae30394d52..6a00c41ca7 100644 --- a/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc +++ b/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include "opentelemetry/exporters/otlp/otlp_file_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_exporter_options.h" diff --git a/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc b/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc index 30802dd394..5cee500d43 100644 --- a/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc +++ b/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_log_record_exporter_options.h" diff --git a/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc index 4ae6767d2c..63c1082d0c 100644 --- a/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc +++ b/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc @@ -3,6 +3,7 @@ #include #include +#include #include "opentelemetry/exporters/otlp/otlp_file_metric_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_file_metric_exporter_options.h" From 8cd1f224aff48ad95beb23b5a48c0b0c863a9494 Mon Sep 17 00:00:00 2001 From: Lukasz Towarek Date: Wed, 21 Oct 2026 22:46:46 +0200 Subject: [PATCH 20/20] Link the CHANGELOG entry to the pull request --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e0f68077..c462969edb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ Increment the: * [EXPORTER] Add a `JsonWriter` interface for OTLP/JSON serialization, so a consumer can supply their own JSON backend in place of the default nlohmann-json one. - [#2541](https://github.com/open-telemetry/opentelemetry-cpp/issues/2541) + [#4556](https://github.com/open-telemetry/opentelemetry-cpp/pull/4556) * [API/SDK] Add ASCII helpers and remove regex from InstrumentMetaDataValidator [#4715](https://github.com/open-telemetry/opentelemetry-cpp/pull/4715)