diff --git a/CHANGELOG.md b/CHANGELOG.md index 098a96cc33..38ad289ffe 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. + [#4556](https://github.com/open-telemetry/opentelemetry-cpp/pull/4556) + ## [1.30.0] 2026-10-22 This release contains: diff --git a/CMakeLists.txt b/CMakeLists.txt index 789a7fb446..1003e8f708 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -351,6 +351,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 @@ -369,6 +380,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) @@ -425,8 +437,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) @@ -603,6 +614,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/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/BUILD b/exporters/otlp/BUILD index 0e6e8beb7e..cee5609207 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 = [ @@ -224,6 +289,8 @@ cc_library( "otlp_http_log", ], deps = [ + ":otlp_json_converter", + ":otlp_json_writer", "//api", "//ext/src/http/client/curl:http_client_curl", "//sdk:headers", @@ -231,7 +298,6 @@ cc_library( "@com_github_opentelemetry_proto//:common_proto_cc", "@com_google_absl//absl/strings", "@com_google_protobuf//:protobuf", - "@github_nlohmann_json//:json", ], ) @@ -305,12 +371,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", ], ) @@ -673,6 +740,44 @@ 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 = [ + "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", + ], +) + cc_test( name = "otlp_recordable_test", srcs = ["test/otlp_recordable_test.cc"], @@ -724,7 +829,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_stub_json_writer.h", + ], tags = [ "otlp", "otlp_file", @@ -789,7 +897,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", + ], tags = [ "otlp", "otlp_http", @@ -839,7 +950,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", @@ -855,7 +969,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", @@ -905,7 +1022,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", @@ -989,7 +1109,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", @@ -1036,7 +1159,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", @@ -1049,6 +1175,66 @@ 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", + ], + target_compatible_with = select({ + ":json_writer_nlohmann_enabled": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + 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", + ], + target_compatible_with = select({ + ":json_writer_nlohmann_enabled": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + 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", + ], + target_compatible_with = select({ + ":json_writer_nlohmann_enabled": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + 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"], @@ -1065,6 +1251,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 04098a9cea..04081a0c80 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -96,6 +96,109 @@ 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) + 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_OTLP_JSON_WRITER_PC_REQUIRES}") + + 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_common opentelemetry_exporter_otlp_json_writer opentelemetry_proto" + ) +endif() + if(OTELCPP_WITH_OTLP_GRPC) if(NOT DEFINED grpc_lib_type) message( @@ -359,13 +462,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 @@ -520,7 +622,9 @@ if(OTELCPP_WITH_OTLP_HTTP) opentelemetry_exporter_otlp_http_metric_builder) if(OTELCPP_INSTALL) - set(_otlp_http_client_pc_requires "opentelemetry_api") + set(_otlp_http_client_pc_requires + "opentelemetry_api opentelemetry_exporter_otlp_json_writer opentelemetry_exporter_otlp_json_converter" + ) if(OTELCPP_WITH_HTTP_CLIENT_CURL) string(APPEND _otlp_http_client_pc_requires " opentelemetry_http_client_curl") @@ -591,7 +695,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 @@ -745,8 +849,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 @@ -803,6 +910,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 @@ -924,6 +1033,29 @@ 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 + 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 + opentelemetry_test_common) + 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( @@ -1009,24 +1141,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) @@ -1038,25 +1172,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) @@ -1069,23 +1205,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) @@ -1130,36 +1268,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) @@ -1171,20 +1362,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) @@ -1197,21 +1390,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) @@ -1223,6 +1418,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 @@ -1239,6 +1469,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..fdf156f74d --- /dev/null +++ b/exporters/otlp/benchmark/otlp_file_client_json_benchmark.cc @@ -0,0 +1,142 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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; +} 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..570762d44a --- /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. " + "Set json_writer_factory in the exporter runtime options, " + "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..af9b4f2ae2 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,10 @@ 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. Null when the content + // type is binary and no factory was supplied. + 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..943dca8780 --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_converter.h @@ -0,0 +1,48 @@ +// 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" + +namespace google +{ +namespace protobuf +{ +class Message; +} // namespace protobuf +} // namespace google + +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..b78fc1d438 --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer.h @@ -0,0 +1,194 @@ +// 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 +{ + +/** + * 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: + JsonWriter() = default; + JsonWriter(const JsonWriter &) = delete; + JsonWriter(JsonWriter &&) = delete; + JsonWriter &operator=(const JsonWriter &) = delete; + JsonWriter &operator=(JsonWriter &&) = delete; + virtual ~JsonWriter() = default; + + /** + * Opens an object. Inside an object, the call is preceded by Key(). + */ + virtual void BeginObject() 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; + + /** + * 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; +}; + +} // 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..5b38dfcfba --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_json_writer_factory.h @@ -0,0 +1,45 @@ +// 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 +{ + +/** + * 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: + 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 f37e4a3f1c..d04431d916 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 @@ -97,36 +95,38 @@ # define OTLP_FILE_OPEN(f, path, mode) f = std::fopen(path, mode) #endif -#include "opentelemetry/common/detail/ascii.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_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 - // 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 { @@ -709,249 +709,6 @@ class OPENTELEMETRY_LOCAL_SYMBOL FileSystemUtil #endif }; -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(opentelemetry::common::detail::LowerHexDigit(byte >> 4)); - ret.push_back(opentelemetry::common::detail::LowerHexDigit(byte)); - } - 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: @@ -1614,7 +1371,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)) { @@ -1652,12 +1413,21 @@ 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(); + 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_request.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); 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 7a09e3762c..1a2fa4e4eb 100644 --- a/exporters/otlp/src/otlp_http_client.cc +++ b/exporters/otlp/src/otlp_http_client.cc @@ -15,13 +15,14 @@ #include #include #include -#include -#include "nlohmann/json.hpp" -#include "opentelemetry/common/detail/ascii.h" #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" @@ -29,7 +30,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" @@ -38,11 +38,9 @@ #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" // IWYU pragma: keep // clang-format on #include -#include #include #include #include -#include // IWYU pragma: no_include // IWYU pragma: no_include // IWYU pragma: no_include @@ -417,81 +415,6 @@ class ResponseHandler : public http_client::EventHandler bool console_debug_ = false; }; -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(opentelemetry::common::detail::LowerHexDigit(byte >> 4)); - ret.push_back(opentelemetry::common::detail::LowerHexDigit(byte)); - } - 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(); @@ -504,195 +427,19 @@ 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) +std::shared_ptr ResolveJsonWriterFactory(const OtlpHttpClientOptions &options) { - switch (field_descriptor->cpp_type()) + if (options.json_writer_factory) { - 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; - } + return options.json_writer_factory; } -} - -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()) + if (options.content_type == HttpRequestContentType::kJson) { - 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; - } + return detail::GetDefaultJsonWriterFactory(); } + return nullptr; } -// NOLINTEND(misc-no-recursion) suppressing for performance, if implemented iterative process needs -// Dynamic memory allocation - } // namespace OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options) @@ -709,6 +456,7 @@ OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options, : is_shutdown_(false), options_(std::move(options)), http_client_(std::move(http_client)), + json_writer_factory_(ResolveJsonWriterFactory(options_)), start_session_counter_(0), finished_session_counter_(0) { @@ -979,13 +727,27 @@ OtlpHttpClient::createSession( } else { - nlohmann::json json_request; + 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}); - // Convert from proto into json object - ConvertGenericMessageToJson(json_request, message, options_); + 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; + result_callback(result, response); + return result; + } - std::string post_body_json = - json_request.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); 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..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 @@ -45,79 +44,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 +90,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 +100,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 +117,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..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 @@ -45,6 +44,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 +79,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 +87,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 +95,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 +105,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 +114,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 +124,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..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" @@ -45,6 +44,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 +79,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 +89,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 +99,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 +111,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 +122,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 +134,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..d74ecfd3c8 --- /dev/null +++ b/exporters/otlp/src/otlp_json_converter.cc @@ -0,0 +1,367 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/otlp/otlp_json_converter.h" + +#include +#include +#include +#include + +#include "opentelemetry/common/detail/ascii.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 + +#ifdef GetMessage +# undef GetMessage +#endif + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace +{ + +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(opentelemetry::common::detail::LowerHexDigit(byte >> 4)); + ret.push_back(opentelemetry::common::detail::LowerHexDigit(byte)); + } + 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] = opentelemetry::common::detail::LowerHexDigit(byte >> 4); + buffer[2 * i + 1] = opentelemetry::common::detail::LowerHexDigit(byte); + } + 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, + 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") + { + WriteHexString(writer, 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: + WriteHexString(writer, bytes); + break; + default: + writer.WriteString(bytes); + break; + } +} + +// 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(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, reflection.GetInt64(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { + 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, 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, reflection.GetStringReference(message, field_descriptor, &empty), + field_descriptor, options.json_bytes_mapping); + } + else + { + writer.WriteString(reflection.GetStringReference(message, field_descriptor, &empty)); + } + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { + ConvertGenericMessageToJson(writer, reflection.GetMessage(message, field_descriptor, nullptr), + options); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { + writer.WriteDouble(reflection.GetDouble(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { + writer.WriteDouble(reflection.GetFloat(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { + writer.WriteBool(reflection.GetBool(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { + writer.WriteInt32(reflection.GetEnumValue(message, field_descriptor)); + break; + } + default: { + writer.WriteNull(); + break; + } + } +} + +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 = reflection.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(reflection.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. + WriteInt64String(writer, reflection.GetRepeatedInt64(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteUInt32(reflection.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. + WriteUInt64String(writer, reflection.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, reflection.GetRepeatedStringReference(message, field_descriptor, i, &empty), + field_descriptor, options.json_bytes_mapping); + } + } + else + { + for (int i = 0; i < field_size; ++i) + { + writer.WriteString( + reflection.GetRepeatedStringReference(message, field_descriptor, i, &empty)); + } + } + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { + for (int i = 0; i < field_size; ++i) + { + ConvertGenericMessageToJson( + writer, reflection.GetRepeatedMessage(message, field_descriptor, i), options); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteDouble(reflection.GetRepeatedDouble(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteDouble(reflection.GetRepeatedFloat(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteBool(reflection.GetRepeatedBool(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { + for (int i = 0; i < field_size; ++i) + { + writer.WriteInt32(reflection.GetRepeatedEnumValue(message, field_descriptor, i)); + } + break; + } + default: { + break; + } + } + + writer.EndArray(); +} + +} // namespace + +void ConvertGenericMessageToJson(JsonWriter &writer, + const google::protobuf::Message &message, + const JsonConverterOptions &options) +{ + const google::protobuf::Reflection &reflection = *message.GetReflection(); + std::vector 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()) + { + writer.WriteNull(); + return; + } + + writer.BeginObject(); + for (const google::protobuf::FieldDescriptor *field_descriptor : fields_with_data) + { + 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, reflection, field_descriptor, options); + } + else + { + ConvertGenericFieldToJson(writer, message, reflection, field_descriptor, options); + } + } + writer.EndObject(); +} +// NOLINTEND(misc-no-recursion) + +} // 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..80fbeb42da --- /dev/null +++ b/exporters/otlp/src/otlp_json_writer_nlohmann.cc @@ -0,0 +1,286 @@ +// 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 + +#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" +#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 +{ +namespace otlp +{ + +namespace +{ + +class NlohmannJsonWriter final : public JsonWriter +{ +public: + void BeginObject() noexcept override { OpenContainer(Container::kObject); } + + void EndObject() noexcept override { CloseContainer(Container::kObject); } + + void BeginArray() noexcept override { OpenContainer(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; + } +#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; + } + + 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( + nostd::string_view(reinterpret_cast(data), size)); + } + } + + bool ok() const noexcept override { return ok_; } + + std::string ToString() noexcept override + { +#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 : std::uint8_t + { + 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::kObject && !has_pending_key_) + { + Fail("a value was written without a preceding Key()"); + return nullptr; + } + +#if OPENTELEMETRY_HAVE_EXCEPTIONS + try + { +#endif + if (frame.kind == Container::kArray) + { + frame.node->push_back(nlohmann::json()); + return &frame.node->back(); + } + has_pending_key_ = false; + return pending_slot_; +#if OPENTELEMETRY_HAVE_EXCEPTIONS + } + catch (const std::exception &e) + { + Fail(e.what()); + } + catch (...) + { + Fail("unknown exception"); + } + return nullptr; +#endif + } + + void OpenContainer(Container kind) noexcept + { + nlohmann::json *slot = ClaimSlot(); + if (!slot) + { + return; + } +#if OPENTELEMETRY_HAVE_EXCEPTIONS + try + { +#endif + // 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 + } + catch (const std::exception &e) + { + Fail(e.what()); + } + catch (...) + { + Fail("unknown exception"); + } +#endif + } + + 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_; + nlohmann::json *pending_slot_ = nullptr; + 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..e25ecc730f 100644 --- a/exporters/otlp/test/otlp_file_client_test.cc +++ b/exporters/otlp/test/otlp_file_client_test.cc @@ -39,6 +39,7 @@ #include "opentelemetry/trace/trace_id.h" #include "opentelemetry/trace/trace_state.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 @@ -163,6 +164,90 @@ 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 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)); + 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(); +} + +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)); + + EXPECT_EQ(client->Export(request, 1), opentelemetry::sdk::common::ExportResult::kFailure); + EXPECT_EQ(output_stream.str(), ""); +} + TEST(OtlpFileClientTest, Shutdown) { opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request; 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..6a00c41ca7 --- /dev/null +++ b/exporters/otlp/test/otlp_file_exporter_default_json_writer_test.cc @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#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 1aab3ebbac..ff4c43e72b 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" @@ -26,17 +27,21 @@ # error "protobuf should not be included" #endif +#include "otlp_stub_json_writer.h" + OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { namespace otlp { -TEST(OtlpFileExporterFactoryTest, BuildTest) +TEST(OtlpFileExporterFactoryTest, BuildWithJsonWriterFactoryTest) { OtlpFileExporterOptions opts; - std::unique_ptr exporter = - OtlpFileExporterFactory::Create(opts); + OtlpFileExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + + auto exporter = OtlpFileExporterFactory::Create(opts, runtime_opts); EXPECT_TRUE(exporter != nullptr); } diff --git a/exporters/otlp/test/otlp_file_exporter_test.cc b/exporters/otlp/test/otlp_file_exporter_test.cc index 95cff477f7..7545489495 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" @@ -19,8 +21,10 @@ #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" #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 +34,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 +215,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_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..5cee500d43 --- /dev/null +++ b/exporters/otlp/test/otlp_file_log_record_exporter_default_json_writer_test.cc @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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" +#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 927e763826..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 @@ -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" @@ -26,17 +27,21 @@ # error "protobuf should not be included" #endif +#include "otlp_stub_json_writer.h" + OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { namespace otlp { -TEST(OtlpFileLogRecordExporterFactoryTest, BuildTest) +TEST(OtlpFileLogRecordExporterFactoryTest, BuildWithJsonWriterFactoryTest) { OtlpFileLogRecordExporterOptions opts; - std::unique_ptr exporter = - OtlpFileLogRecordExporterFactory::Create(opts); + OtlpFileLogRecordExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + + auto exporter = OtlpFileLogRecordExporterFactory::Create(opts, runtime_opts); EXPECT_TRUE(exporter != nullptr); } 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..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" @@ -21,11 +22,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 +196,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_default_json_writer_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc new file mode 100644 index 0000000000..63c1082d0c --- /dev/null +++ b/exporters/otlp/test/otlp_file_metric_exporter_default_json_writer_test.cc @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#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 78e3bf366f..427625db38 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" @@ -26,17 +27,21 @@ # error "protobuf should not be included" #endif +#include "otlp_stub_json_writer.h" + OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter { namespace otlp { -TEST(OtlpFileMetricExporterFactory, BuildTest) +TEST(OtlpFileMetricExporterFactory, BuildWithJsonWriterFactoryTest) { OtlpFileMetricExporterOptions opts; - std::unique_ptr exporter = - OtlpFileMetricExporterFactory::Create(opts); + OtlpFileMetricExporterRuntimeOptions runtime_opts; + runtime_opts.json_writer_factory = std::make_shared(); + + auto exporter = OtlpFileMetricExporterFactory::Create(opts, runtime_opts); EXPECT_TRUE(exporter != nullptr); } diff --git a/exporters/otlp/test/otlp_file_metric_exporter_test.cc b/exporters/otlp/test/otlp_file_metric_exporter_test.cc index bc6138c9c1..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 @@ -337,6 +338,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_client_test.cc b/exporters/otlp/test/otlp_http_exporter_custom_client_test.cc index b9c0157508..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" @@ -28,6 +29,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 +355,87 @@ 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(); + 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 new file mode 100644 index 0000000000..50ca4ff21a --- /dev/null +++ b/exporters/otlp/test/otlp_http_exporter_custom_json_writer_test.cc @@ -0,0 +1,119 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include +#include +#include "gmock/gmock.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/ext/http/client/http_client.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_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" + +using namespace testing; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +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(); + 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 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; + 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 e505c4ac25..e76d22b3a6 100644 --- a/exporters/otlp/test/otlp_http_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_test.cc @@ -20,6 +20,7 @@ # include #endif +#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" @@ -33,10 +34,12 @@ #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" #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" @@ -51,10 +54,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 @@ -959,6 +966,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; + test::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..d12e99f6af --- /dev/null +++ b/exporters/otlp/test/otlp_http_log_record_exporter_custom_json_writer_test.cc @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#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..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" @@ -28,12 +29,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 +44,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 +973,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; + test::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..2671672825 --- /dev/null +++ b/exporters/otlp/test/otlp_http_metric_exporter_custom_json_writer_test.cc @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#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..ffd13475cc 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; + test::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..dad54ce8ae --- /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 test +{ + +// 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 test + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE 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..e9a3d59158 --- /dev/null +++ b/exporters/otlp/test/otlp_json_converter_test.cc @@ -0,0 +1,200 @@ +// 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 +#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; +} + +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); + }); + +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.emplace_back(key_value->key()); + current = key_value->mutable_value(); + } + current->set_string_value("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.emplace_back("after"); + expected.emplace_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 +} // 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 new file mode 100644 index 0000000000..e3f23db25b --- /dev/null +++ b/exporters/otlp/test/otlp_json_writer_contract_test.cc @@ -0,0 +1,494 @@ +// 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 "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) +{ + static constexpr int kThreads = 8; + 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] { + 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(); + 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; + failures[t] += ", got "; + failures[t] += 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 new file mode 100644 index 0000000000..58ae6f2bc2 --- /dev/null +++ b/exporters/otlp/test/otlp_json_writer_nlohmann_test.cc @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include + +#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 test +{ + +INSTANTIATE_TEST_SUITE_P(Nlohmann, + JsonWriterContract, + ::testing::Values(std::make_shared())); + +TEST(NlohmannJsonWriter, SortsMembersByKey) +{ + auto writer = JsonWriterFactoryNlohmann().Create(); + writer->BeginObject(); + writer->Key("b"); + writer->BeginArray(); + writer->WriteInt32(1); + writer->WriteInt32(2); + writer->EndArray(); + writer->Key("a"); + writer->WriteString("one"); + writer->EndObject(); + EXPECT_EQ(writer->ToString(), R"({"a":"one","b":[1,2]})"); + EXPECT_TRUE(writer->ok()); +} + +} // 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 new file mode 100644 index 0000000000..70d6f2728d --- /dev/null +++ b/exporters/otlp/test/otlp_stub_json_writer.h @@ -0,0 +1,91 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include +#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 test +{ + +// 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 { 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: + 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 + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE