diff --git a/CHANGELOG.md b/CHANGELOG.md index 16156529c6..1c4f68df9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Increment the: ## [Unreleased] +* [EXPORTER] Add OTLP/HTTP+JSON Protocol exporter ([#810](https://github.com/open-telemetry/opentelemetry-cpp/pull/810)) +* [EXPORTER] Rename `OtlpExporter` to `OtlpGrpcExporter`, rename `otlp_exporter.h` to `otlp_grpc_exporter.h` ([#810](https://github.com/open-telemetry/opentelemetry-cpp/pull/810)) + ## [1.0.0-rc1] 2021-06-04 * [BUILD] Enable Jaeger exporter build in Windows ([#815](https://github.com/open-telemetry/opentelemetry-cpp/pull/815)) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b78ca7ecb..c8bce5be1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -203,7 +203,7 @@ if(WITH_OTLP) find_package(Protobuf REQUIRED) endif() if(NOT gRPC_FOUND) - find_package(gRPC REQUIRED) + find_package(gRPC) endif() if(WIN32) # Always use x64 protoc.exe @@ -221,6 +221,15 @@ if(WITH_OTLP) message("PROTOBUF_PROTOC_EXECUTABLE=${PROTOBUF_PROTOC_EXECUTABLE}") include(cmake/opentelemetry-proto.cmake) + include(CMakeDependentOption) + find_package(CURL) + find_package(nlohmann_json) + cmake_dependent_option( + WITH_OTLP_GRPC "Whether to include the OTLP gRPC exporter in the SDK" ON + "gRPC_FOUND" OFF) + cmake_dependent_option( + WITH_OTLP_HTTP "Whether to include the OTLP http exporter in the SDK" ON + "CURL_FOUND;nlohmann_json_FOUND" OFF) endif() list(APPEND CMAKE_PREFIX_PATH "${CMAKE_BINARY_DIR}") @@ -268,11 +277,11 @@ if(NOT WITH_API_ONLY) include_directories(ext/include) add_subdirectory(sdk) + add_subdirectory(ext) add_subdirectory(exporters) if(WITH_EXAMPLES) add_subdirectory(examples) endif() - add_subdirectory(ext) endif() # Add nlohmann/json submodule to include directories diff --git a/WORKSPACE b/WORKSPACE index e1041d37dd..1bd7b57c39 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -29,9 +29,9 @@ load("@com_github_grpc_grpc//bazel:grpc_extra_deps.bzl", "grpc_extra_deps") grpc_extra_deps() -load("@upb//bazel:repository_defs.bzl", "bazel_version_repository") +load("@upb//bazel:workspace_deps.bzl", "upb_deps") -bazel_version_repository(name = "upb_bazel_version") +upb_deps() # Load prometheus C++ dependencies. load("@com_github_jupp0r_prometheus_cpp//bazel:repositories.bzl", "prometheus_cpp_repositories") diff --git a/bazel/repository.bzl b/bazel/repository.bzl index 59ed8b1ada..af35dece2d 100644 --- a/bazel/repository.bzl +++ b/bazel/repository.bzl @@ -29,13 +29,23 @@ def opentelemetry_cpp_deps(): ) # Load gRPC dependency + maybe( + http_archive, + name = "com_github_grpc_grpc_legacy", + sha256 = "2060769f2d4b0d3535ba594b2ab614d7f68a492f786ab94b4318788d45e3278a", + strip_prefix = "grpc-1.33.2", + urls = [ + "https://github.com/grpc/grpc/archive/v1.33.2.tar.gz", + ], + ) + maybe( http_archive, name = "com_github_grpc_grpc", - sha256 = "d6277f77e0bb922d3f6f56c0f93292bb4cfabfc3c92b31ee5ccea0e100303612", - strip_prefix = "grpc-1.28.0", + sha256 = "2060769f2d4b0d3535ba594b2ab614d7f68a492f786ab94b4318788d45e3278a", + strip_prefix = "grpc-1.33.2", urls = [ - "https://github.com/grpc/grpc/archive/v1.28.0.tar.gz", + "https://github.com/grpc/grpc/archive/v1.33.2.tar.gz", ], ) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f7eb276016..9ea0a544fb 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ -if(WITH_OTLP) +if(WITH_OTLP_GRPC OR WITH_OTLP_HTTP) add_subdirectory(otlp) add_subdirectory(grpc) endif() diff --git a/examples/otlp/BUILD b/examples/otlp/BUILD index 29a4035697..2892cfa86c 100644 --- a/examples/otlp/BUILD +++ b/examples/otlp/BUILD @@ -12,14 +12,27 @@ cc_library( ) cc_binary( - name = "example_otlp", + name = "example_otlp_grpc", srcs = [ - "main.cc", + "grpc_main.cc", ], deps = [ ":foo_library", "//api", - "//exporters/otlp:otlp_exporter", + "//exporters/otlp:otlp_grpc_exporter", + "//sdk/src/trace", + ], +) + +cc_binary( + name = "example_otlp_http", + srcs = [ + "http_main.cc", + ], + deps = [ + ":foo_library", + "//api", + "//exporters/otlp:otlp_http_exporter", "//sdk/src/trace", ], ) diff --git a/examples/otlp/CMakeLists.txt b/examples/otlp/CMakeLists.txt index 8b77baad38..f661108573 100644 --- a/examples/otlp/CMakeLists.txt +++ b/examples/otlp/CMakeLists.txt @@ -6,12 +6,21 @@ add_library(otlp_foo_library foo_library/foo_library.cc) target_link_libraries(otlp_foo_library ${CMAKE_THREAD_LIBS_INIT} ${CORE_RUNTIME_LIBS} opentelemetry_api) -add_executable(example_otlp main.cc) -target_link_libraries( - example_otlp - ${CMAKE_THREAD_LIBS_INIT} - otlp_foo_library - opentelemetry_trace - ${CORE_RUNTIME_LIBS} - opentelemetry_exporter_otprotocol - gRPC::grpc++) +if(WITH_OTLP_GRPC) + add_executable(example_otlp_grpc grpc_main.cc) + target_link_libraries( + example_otlp_grpc + ${CMAKE_THREAD_LIBS_INIT} + otlp_foo_library + opentelemetry_trace + ${CORE_RUNTIME_LIBS} + opentelemetry_exporter_otlp_grpc + gRPC::grpc++) +endif() + +if(WITH_OTLP_HTTP) + add_executable(example_otlp_http http_main.cc) + target_link_libraries( + example_otlp_http ${CMAKE_THREAD_LIBS_INIT} otlp_foo_library + opentelemetry_trace ${CORE_RUNTIME_LIBS} opentelemetry_exporter_otlp_http) +endif() diff --git a/examples/otlp/README.md b/examples/otlp/README.md index e4750ef875..03325cd681 100644 --- a/examples/otlp/README.md +++ b/examples/otlp/README.md @@ -4,15 +4,16 @@ This is an example of how to use the [OpenTelemetry Protocol](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/README.md) (OTLP) exporter. -The application in `main.cc` initializes an `OtlpExporter` instance and uses it -to register a tracer provider from the [OpenTelemetry +The application in `grpc_main.cc` initializes an `OtlpGrpcExporter` instance and +the application in `http_main.cc` initializes an `OtlpHttpExporter` instance +and they register a tracer provider from the [OpenTelemetry SDK](https://github.com/open-telemetry/opentelemetry-cpp). The application then calls a `foo_library` which has been instrumented using the [OpenTelemetry API](https://github.com/open-telemetry/opentelemetry-cpp/tree/main/api). To enable TLS authentication for OTLP grpc exporter, SslCredentials can be used by specifying the path to client certificate pem file, or the string containing -this certificate via OtlpExporterOptions. The path to such a .pem file can be +this certificate via OtlpGrpcExporterOptions. The path to such a .pem file can be provided as a command-line argument alongwith the collector endpoint to the main binary invocation above. @@ -31,18 +32,19 @@ OpenTelemetry Collector with an OTLP receiver by running: - On Unix based systems use: ```console -docker run --rm -it -p 4317:4317 -v $(pwd)/examples/otlp:/cfg otel/opentelemetry-collector:0.19.0 --config=/cfg/opentelemetry-collector-config/config.dev.yaml +docker run --rm -it -p 4317:4317 -p 55681:55681 -v $(pwd)/examples/otlp:/cfg otel/opentelemetry-collector:0.19.0 --config=/cfg/opentelemetry-collector-config/config.dev.yaml ``` - On Windows use: ```console -docker run --rm -it -p 4317:4317 -v "%cd%/examples/otlp":/cfg otel/opentelemetry-collector:0.19.0 --config=/cfg/opentelemetry-collector-config/config.dev.yaml +docker run --rm -it -p 4317:4317 -p 55681:55681 -v "%cd%/examples/otlp":/cfg otel/opentelemetry-collector:0.19.0 --config=/cfg/opentelemetry-collector-config/config.dev.yaml ``` Note that the OTLP exporter connects to the Collector at `localhost:4317` by default. This can be changed with first argument from command-line, for example: -`./example_otlp gateway.docker.internal:4317`. +`./example_otlp_grpc gateway.docker.internal:4317` and +`./example_otlp_http gateway.docker.internal:55681/v1/traces`. Once you have the Collector running, see [CONTRIBUTING.md](../../CONTRIBUTING.md) for instructions on building and diff --git a/examples/otlp/main.cc b/examples/otlp/grpc_main.cc similarity index 89% rename from examples/otlp/main.cc rename to examples/otlp/grpc_main.cc index 2acf2ccb8e..7df891ad0a 100644 --- a/examples/otlp/main.cc +++ b/examples/otlp/grpc_main.cc @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#include "opentelemetry/exporters/otlp/otlp_exporter.h" +#include "opentelemetry/exporters/otlp/otlp_grpc_exporter.h" #include "opentelemetry/sdk/trace/simple_processor.h" #include "opentelemetry/sdk/trace/tracer_provider.h" #include "opentelemetry/trace/provider.h" @@ -15,11 +15,11 @@ namespace otlp = opentelemetry::exporter::otlp; namespace { -opentelemetry::exporter::otlp::OtlpExporterOptions opts; +opentelemetry::exporter::otlp::OtlpGrpcExporterOptions opts; void InitTracer() { // Create OTLP exporter instance - auto exporter = std::unique_ptr(new otlp::OtlpExporter(opts)); + auto exporter = std::unique_ptr(new otlp::OtlpGrpcExporter(opts)); auto processor = std::unique_ptr( new sdktrace::SimpleSpanProcessor(std::move(exporter))); auto provider = diff --git a/examples/otlp/http_main.cc b/examples/otlp/http_main.cc new file mode 100644 index 0000000000..a35fb4b35b --- /dev/null +++ b/examples/otlp/http_main.cc @@ -0,0 +1,58 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/otlp/otlp_http_exporter.h" +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/trace/provider.h" + +#include + +#include "foo_library/foo_library.h" + +namespace trace = opentelemetry::trace; +namespace nostd = opentelemetry::nostd; +namespace sdktrace = opentelemetry::sdk::trace; +namespace otlp = opentelemetry::exporter::otlp; + +namespace +{ +opentelemetry::exporter::otlp::OtlpHttpExporterOptions opts; +void InitTracer() +{ + // Create OTLP exporter instance + auto exporter = std::unique_ptr(new otlp::OtlpHttpExporter(opts)); + auto processor = std::unique_ptr( + new sdktrace::SimpleSpanProcessor(std::move(exporter))); + auto provider = + nostd::shared_ptr(new sdktrace::TracerProvider(std::move(processor))); + // Set the global trace provider + trace::Provider::SetTracerProvider(provider); +} +} // namespace + +int main(int argc, char *argv[]) +{ + if (argc > 1) + { + opts.url = argv[1]; + if (argc > 2) + { + std::string debug = argv[2]; + opts.console_debug = debug != "" && debug != "0" && debug != "no"; + } + + if (argc > 3) + { + std::string binary_mode = argv[3]; + if (binary_mode.size() >= 3 && binary_mode.substr(0, 3) == "bin") + { + opts.content_type = opentelemetry::exporter::otlp::HttpRequestContentType::kBinary; + } + } + } + // Removing this line will leave the default noop TracerProvider in place. + InitTracer(); + + foo_library(); +} diff --git a/examples/otlp/opentelemetry-collector-config/config.dev.yaml b/examples/otlp/opentelemetry-collector-config/config.dev.yaml index 6807000f01..d3fbfd8834 100644 --- a/examples/otlp/opentelemetry-collector-config/config.dev.yaml +++ b/examples/otlp/opentelemetry-collector-config/config.dev.yaml @@ -6,6 +6,10 @@ receivers: protocols: grpc: endpoint: 0.0.0.0:4317 + http: + endpoint: "0.0.0.0:55681" + cors_allowed_origins: + - '*' service: pipelines: traces: diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index 731cb2335c..134950b0f0 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -35,12 +35,12 @@ cc_library( ) cc_library( - name = "otlp_exporter", + name = "otlp_grpc_exporter", srcs = [ - "src/otlp_exporter.cc", + "src/otlp_grpc_exporter.cc", ], hdrs = [ - "include/opentelemetry/exporters/otlp/otlp_exporter.h", + "include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h", "include/opentelemetry/exporters/otlp/protobuf_include_prefix.h", "include/opentelemetry/exporters/otlp/protobuf_include_suffix.h", ], @@ -55,6 +55,36 @@ cc_library( ], ) +cc_library( + name = "otlp_http_exporter", + srcs = [ + "src/otlp_http_exporter.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/otlp/otlp_http_exporter.h", + "include/opentelemetry/exporters/otlp/protobuf_include_prefix.h", + "include/opentelemetry/exporters/otlp/protobuf_include_suffix.h", + ], + copts = [ + "-DCURL_STATICLIB", + ], + linkopts = select({ + "//bazel:windows": [ + "-DEFAULTLIB:advapi32.lib", + "-DEFAULTLIB:crypt32.lib", + ], + "//conditions:default": [], + }), + strip_include_prefix = "include", + deps = [ + ":otlp_recordable", + "//ext/src/http/client/curl:http_client_curl", + "//sdk/src/trace", + "@com_github_opentelemetry_proto//:trace_service_proto_cc", + "@github_nlohmann_json//:json", + ], +) + cc_test( name = "otlp_recordable_test", srcs = ["test/otlp_recordable_test.cc"], @@ -65,19 +95,29 @@ cc_test( ) cc_test( - name = "otlp_exporter_test", - srcs = ["test/otlp_exporter_test.cc"], + name = "otlp_grpc_exporter_test", + srcs = ["test/otlp_grpc_exporter_test.cc"], + deps = [ + ":otlp_grpc_exporter", + "//api", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "otlp_http_exporter_test", + srcs = ["test/otlp_http_exporter_test.cc"], deps = [ - ":otlp_exporter", + ":otlp_http_exporter", "//api", "@com_google_googletest//:gtest_main", ], ) otel_cc_benchmark( - name = "otlp_exporter_benchmark", - srcs = ["test/otlp_exporter_benchmark.cc"], + name = "otlp_grpc_exporter_benchmark", + srcs = ["test/otlp_grpc_exporter_benchmark.cc"], deps = [ - ":otlp_exporter", + ":otlp_grpc_exporter", ], ) diff --git a/exporters/otlp/CMakeLists.txt b/exporters/otlp/CMakeLists.txt index 6c5bed5607..63b0e652db 100644 --- a/exporters/otlp/CMakeLists.txt +++ b/exporters/otlp/CMakeLists.txt @@ -1,16 +1,49 @@ -add_library(opentelemetry_exporter_otprotocol src/otlp_recordable.cc - src/otlp_exporter.cc) +add_library(opentelemetry_otlp_recordable src/otlp_recordable.cc) +set_target_properties(opentelemetry_otlp_recordable PROPERTIES EXPORT_NAME + otlp_recordable) -set_target_properties(opentelemetry_exporter_otprotocol - PROPERTIES EXPORT_NAME otlp_exporter) +target_include_directories( + opentelemetry_otlp_recordable + PUBLIC "$" + "$") +set(OPENTELEMETRY_OTLP_TARGETS opentelemetry_otlp_recordable) target_link_libraries( - opentelemetry_exporter_otprotocol - PUBLIC opentelemetry_trace opentelemetry_resources opentelemetry_proto - protobuf::libprotobuf gRPC::grpc++) + opentelemetry_otlp_recordable + PUBLIC opentelemetry_trace opentelemetry_resources opentelemetry_proto) + +if(WITH_OTLP_GRPC) + find_package(gRPC REQUIRED) + add_library(opentelemetry_exporter_otlp_grpc src/otlp_grpc_exporter.cc) + + set_target_properties(opentelemetry_exporter_otlp_grpc + PROPERTIES EXPORT_NAME otlp_grpc_exporter) + + target_link_libraries( + opentelemetry_exporter_otlp_grpc PUBLIC opentelemetry_otlp_recordable + protobuf::libprotobuf gRPC::grpc++) + + list(APPEND OPENTELEMETRY_OTLP_TARGETS opentelemetry_exporter_otlp_grpc) +endif() + +if(WITH_OTLP_HTTP) + find_package(CURL REQUIRED) + find_package(nlohmann_json REQUIRED) + add_library(opentelemetry_exporter_otlp_http src/otlp_http_exporter.cc) + + set_target_properties(opentelemetry_exporter_otlp_http + PROPERTIES EXPORT_NAME otlp_http_exporter) + + target_link_libraries( + opentelemetry_exporter_otlp_http + PUBLIC opentelemetry_otlp_recordable http_client_curl + nlohmann_json::nlohmann_json) + + list(APPEND OPENTELEMETRY_OTLP_TARGETS opentelemetry_exporter_otlp_http) +endif() install( - TARGETS opentelemetry_exporter_otprotocol + TARGETS ${OPENTELEMETRY_OTLP_TARGETS} EXPORT "${PROJECT_NAME}-target" RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} @@ -22,15 +55,10 @@ install( FILES_MATCHING PATTERN "*.h") -target_include_directories( - opentelemetry_exporter_otprotocol - PUBLIC "$") - if(BUILD_TESTING) add_executable(otlp_recordable_test test/otlp_recordable_test.cc) - target_link_libraries( - otlp_recordable_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} - opentelemetry_exporter_otprotocol) + target_link_libraries(otlp_recordable_test ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} opentelemetry_otlp_recordable) gtest_add_tests( TARGET otlp_recordable_test TEST_PREFIX exporter.otlp. @@ -54,12 +82,20 @@ if(BUILD_TESTING) else() find_library(GMOCK_LIB gmock PATH_SUFFIXES lib) endif() - add_executable(otlp_exporter_test test/otlp_exporter_test.cc) + add_executable(otlp_grpc_exporter_test test/otlp_grpc_exporter_test.cc) + target_link_libraries( + otlp_grpc_exporter_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} opentelemetry_exporter_otlp_grpc) + gtest_add_tests( + TARGET otlp_grpc_exporter_test + TEST_PREFIX exporter.otlp. + TEST_LIST otlp_grpc_exporter_test) + add_executable(otlp_http_exporter_test test/otlp_http_exporter_test.cc) target_link_libraries( - otlp_exporter_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} - ${GMOCK_LIB} opentelemetry_exporter_otprotocol) + otlp_http_exporter_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} opentelemetry_exporter_otlp_http) gtest_add_tests( - TARGET otlp_exporter_test + TARGET otlp_http_exporter_test TEST_PREFIX exporter.otlp. - TEST_LIST otlp_exporter_test) + TEST_LIST otlp_http_exporter_test) endif() # BUILD_TESTING diff --git a/exporters/otlp/README.md b/exporters/otlp/README.md index 7e9763fa81..8c6efe90d3 100644 --- a/exporters/otlp/README.md +++ b/exporters/otlp/README.md @@ -19,15 +19,27 @@ contributions](https://github.com/open-telemetry/opentelemetry-collector-contrib ## Configuration The OTLP exporter offers some configuration options. To configure the exporter, -create an `OtlpExporterOptions` struct (defined in -[exporter.h](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_exporter.h)), -set the options inside, and pass the struct to the `OtlpExporter` constructor, +create an `OtlpGrpcExporterOptions` struct (defined in +[otlp_grpc_exporter.h](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h)), +set the options inside, and pass the struct to the `OtlpGrpcExporter` constructor, like so: ```cpp -OtlpExporterOptions options; +OtlpGrpcExporterOptions options; options.endpoint = "localhost:12345"; -auto exporter = std::unique_ptr(new otlp::OtlpExporter(options)); +auto exporter = std::unique_ptr(new otlp::OtlpGrpcExporter(options)); +``` + +The OTLP HTTP exporter offers some configuration options. To configure the exporter, +create an `OtlpHttpExporterOptions` struct (defined in +[otlp_http_exporter.h](https://github.com/open-telemetry/opentelemetry-cpp/blob/main/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter.h)), +set the options inside, and pass the struct to the `OtlpHttpExporter` constructor, +like so: + +```cpp +OtlpHttpExporterOptions options; +options.url = "localhost:12345"; +auto exporter = std::unique_ptr(new otlp::OtlpHttpExporter(options)); ``` ### Configuration options diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_exporter.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h similarity index 81% rename from exporters/otlp/include/opentelemetry/exporters/otlp/otlp_exporter.h rename to exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h index edf3b59003..b3df30d259 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_exporter.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h @@ -19,7 +19,7 @@ namespace otlp /** * Struct to hold OTLP exporter options. */ -struct OtlpExporterOptions +struct OtlpGrpcExporterOptions { // The endpoint to export to. By default the OpenTelemetry Collector's default endpoint. std::string endpoint = "localhost:4317"; @@ -36,18 +36,18 @@ struct OtlpExporterOptions /** * The OTLP exporter exports span data in OpenTelemetry Protocol (OTLP) format. */ -class OtlpExporter final : public opentelemetry::sdk::trace::SpanExporter +class OtlpGrpcExporter final : public opentelemetry::sdk::trace::SpanExporter { public: /** - * Create an OtlpExporter using all default options. + * Create an OtlpGrpcExporter using all default options. */ - OtlpExporter(); + OtlpGrpcExporter(); /** - * Create an OtlpExporter using the given options. + * Create an OtlpGrpcExporter using the given options. */ - OtlpExporter(const OtlpExporterOptions &options); + explicit OtlpGrpcExporter(const OtlpGrpcExporterOptions &options); /** * Create a span recordable. @@ -76,20 +76,20 @@ class OtlpExporter final : public opentelemetry::sdk::trace::SpanExporter private: // The configuration options associated with this exporter. - const OtlpExporterOptions options_; + const OtlpGrpcExporterOptions options_; // For testing - friend class OtlpExporterTestPeer; + friend class OtlpGrpcExporterTestPeer; // Store service stub internally. Useful for testing. std::unique_ptr trace_service_stub_; /** - * Create an OtlpExporter using the specified service stub. + * Create an OtlpGrpcExporter using the specified service stub. * Only tests can call this constructor directly. * @param stub the service stub to be used for exporting */ - OtlpExporter(std::unique_ptr stub); + OtlpGrpcExporter(std::unique_ptr stub); }; } // namespace otlp } // namespace exporter diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter.h new file mode 100644 index 0000000000..7aa73f9290 --- /dev/null +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter.h @@ -0,0 +1,119 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +// We need include exporter.h first, which will include Windows.h with NOMINMAX on Windows +#include "opentelemetry/sdk/trace/exporter.h" + +#include +#include +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ +// The default URL path to post trace data. +constexpr char kDefaultTracePath[] = "/v1/traces"; +// The default URL path to post metric data. +constexpr char kDefaultMetricsPath[] = "/v1/metrics"; +// The default URL path to post metric data. +constexpr char kDefaultLogPath[] = "/v1/logs"; +// The HTTP header "Content-Type" +constexpr char kHttpJsonContentType[] = "application/json"; +constexpr char kHttpBinaryContentType[] = "application/x-protobuf"; + +enum class JsonBytesMappingKind +{ + kHexId, + kHex, + kBase64, +}; + +enum class HttpRequestContentType +{ + kJson, + kBinary, +}; + +/** + * Struct to hold OTLP exporter options. + */ +struct OtlpHttpExporterOptions +{ + // The endpoint to export to. By default + // @see + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md + // @see https://github.com/open-telemetry/opentelemetry-collector/tree/main/receiver/otlpreceiver + std::string url = std::string("http://localhost:4317") + kDefaultTracePath; + + // By default, post json data + HttpRequestContentType content_type = HttpRequestContentType::kJson; + + // If convert bytes into hex. By default, we will convert all bytes but id into base64 + // This option is ignored if content_type is not kJson + JsonBytesMappingKind json_bytes_mapping = JsonBytesMappingKind::kHexId; + + // If using the json name of protobuf field to set the key of json. By default, we will use the + // field name just like proto files. + bool use_json_name = false; + + // Whether to print the status of the exporter in the console + bool console_debug = false; + + // TODO: Enable/disable to verify SSL certificate + // TODO: Reuqest timeout +}; + +/** + * The OTLP exporter exports span data in OpenTelemetry Protocol (OTLP) format. + */ +class OtlpHttpExporter final : public opentelemetry::sdk::trace::SpanExporter +{ +public: + /** + * Create an OtlpHttpExporter using all default options. + */ + OtlpHttpExporter(); + + /** + * Create an OtlpHttpExporter using the given options. + */ + explicit OtlpHttpExporter(const OtlpHttpExporterOptions &options); + + /** + * Create a span recordable. + * @return a newly initialized Recordable object + */ + std::unique_ptr MakeRecordable() noexcept override; + + /** + * Export + * @param spans a span of unique pointers to span recordables + */ + sdk::common::ExportResult Export( + const nostd::span> &spans) noexcept override; + + /** + * Shut down the exporter. + * @param timeout an optional timeout, the default timeout of 0 means that no + * timeout is applied. + * @return return the status of this operation + */ + bool Shutdown(std::chrono::microseconds timeout = std::chrono::microseconds(0)) noexcept override; + +private: + // Stores if this exporter had its Shutdown() method called + bool is_shutdown_ = false; + + // For testing + friend class OtlpHttpExporterTestPeer; + + // The configuration options associated with this exporter. + const OtlpHttpExporterOptions options_; +}; +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/src/otlp_exporter.cc b/exporters/otlp/src/otlp_grpc_exporter.cc similarity index 86% rename from exporters/otlp/src/otlp_exporter.cc rename to exporters/otlp/src/otlp_grpc_exporter.cc index 995c2ba146..e0aaa0a0ab 100644 --- a/exporters/otlp/src/otlp_exporter.cc +++ b/exporters/otlp/src/otlp_grpc_exporter.cc @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#include "opentelemetry/exporters/otlp/otlp_exporter.h" +#include "opentelemetry/exporters/otlp/otlp_grpc_exporter.h" #include "opentelemetry/exporters/otlp/otlp_recordable.h" #include @@ -54,7 +54,7 @@ static std::string get_file_contents(const char *fpath) * Create service stub to communicate with the OpenTelemetry Collector. */ std::unique_ptr MakeServiceStub( - const OtlpExporterOptions &options) + const OtlpGrpcExporterOptions &options) { std::shared_ptr channel; if (options.use_ssl_credentials) @@ -79,25 +79,25 @@ std::unique_ptr MakeServiceStub // -------------------------------- Constructors -------------------------------- -OtlpExporter::OtlpExporter() : OtlpExporter(OtlpExporterOptions()) {} +OtlpGrpcExporter::OtlpGrpcExporter() : OtlpGrpcExporter(OtlpGrpcExporterOptions()) {} -OtlpExporter::OtlpExporter(const OtlpExporterOptions &options) +OtlpGrpcExporter::OtlpGrpcExporter(const OtlpGrpcExporterOptions &options) : options_(options), trace_service_stub_(MakeServiceStub(options)) {} -OtlpExporter::OtlpExporter( +OtlpGrpcExporter::OtlpGrpcExporter( std::unique_ptr stub) - : options_(OtlpExporterOptions()), trace_service_stub_(std::move(stub)) + : options_(OtlpGrpcExporterOptions()), trace_service_stub_(std::move(stub)) {} // ----------------------------- Exporter methods ------------------------------ -std::unique_ptr OtlpExporter::MakeRecordable() noexcept +std::unique_ptr OtlpGrpcExporter::MakeRecordable() noexcept { return std::unique_ptr(new OtlpRecordable); } -sdk::common::ExportResult OtlpExporter::Export( +sdk::common::ExportResult OtlpGrpcExporter::Export( const nostd::span> &spans) noexcept { proto::collector::trace::v1::ExportTraceServiceRequest request; diff --git a/exporters/otlp/src/otlp_http_exporter.cc b/exporters/otlp/src/otlp_http_exporter.cc new file mode 100644 index 0000000000..7d13a3a7a7 --- /dev/null +++ b/exporters/otlp/src/otlp_http_exporter.cc @@ -0,0 +1,524 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/otlp/otlp_http_exporter.h" +#include "opentelemetry/exporters/otlp/otlp_recordable.h" +#include "opentelemetry/ext/http/client/http_client_factory.h" + +#include "nlohmann/json.hpp" + +#include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" + +#include "google/protobuf/message.h" +#include "google/protobuf/reflection.h" +#include "google/protobuf/stubs/stl_util.h" +#include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" + +#include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" + +#include +#include +#include +#include + +#ifdef GetMessage +# undef GetMessage +#endif + +namespace nostd = opentelemetry::nostd; +namespace http_client = opentelemetry::ext::http::client; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +namespace +{ + +static inline char HexEncode(unsigned char byte) +{ + if (byte >= 10) + { + return byte - 10 + 'a'; + } + else + { + return byte + '0'; + } +} + +static std::string HexEncode(const std::string &bytes) +{ + std::string ret; + ret.reserve(bytes.size() * 2); + for (std::string::size_type i = 0; i < bytes.size(); ++i) + { + unsigned char byte = static_cast(bytes[i]); + ret.push_back(HexEncode(byte >> 4)); + ret.push_back(HexEncode(byte & 0x0f)); + } + return ret; +} + +static std::string BytesMapping(const std::string &bytes, + const google::protobuf::FieldDescriptor *field_descriptor, + JsonBytesMappingKind kind) +{ + switch (kind) + { + case JsonBytesMappingKind::kHexId: { + if (field_descriptor->lowercase_name() == "trace_id" || + field_descriptor->lowercase_name() == "span_id" || + field_descriptor->lowercase_name() == "parent_span_id") + { + return HexEncode(bytes); + } + else + { + std::string base64_value; + google::protobuf::Base64Escape(bytes, &base64_value); + return base64_value; + } + } + case JsonBytesMappingKind::kBase64: { + // Base64 is the default bytes mapping of protobuf + std::string base64_value; + google::protobuf::Base64Escape(bytes, &base64_value); + return base64_value; + } + 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 OtlpHttpExporterOptions &options); + +static void ConvertListFieldToJson(nlohmann::json &value, + const google::protobuf::Message &message, + const google::protobuf::FieldDescriptor *field_descriptor, + const OtlpHttpExporterOptions &options); + +static void ConvertGenericMessageToJson(nlohmann::json &value, + const google::protobuf::Message &message, + const OtlpHttpExporterOptions &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->name()]; + if (field_descriptor->is_repeated()) + { + ConvertListFieldToJson(child_value, message, field_descriptor, options); + } + else + { + ConvertGenericFieldToJson(child_value, message, field_descriptor, options); + } + } +} + +void ConvertGenericFieldToJson(nlohmann::json &value, + const google::protobuf::Message &message, + const google::protobuf::FieldDescriptor *field_descriptor, + const OtlpHttpExporterOptions &options) +{ + switch (field_descriptor->cpp_type()) + { + case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { + value = message.GetReflection()->GetInt32(message, field_descriptor); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as + // decimal strings, and either numbers or strings are accepted when decoding. + value = std::to_string(message.GetReflection()->GetInt64(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { + value = message.GetReflection()->GetUInt32(message, field_descriptor); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded as + // decimal strings, and either numbers or strings are accepted when decoding. + value = std::to_string(message.GetReflection()->GetUInt64(message, field_descriptor)); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { + std::string empty; + if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) + { + value = BytesMapping( + message.GetReflection()->GetStringReference(message, field_descriptor, &empty), + field_descriptor, options.json_bytes_mapping); + } + else + { + value = message.GetReflection()->GetStringReference(message, field_descriptor, &empty); + } + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { + ConvertGenericMessageToJson( + value, message.GetReflection()->GetMessage(message, field_descriptor, nullptr), options); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { + value = message.GetReflection()->GetDouble(message, field_descriptor); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { + value = message.GetReflection()->GetFloat(message, field_descriptor); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { + value = message.GetReflection()->GetBool(message, field_descriptor); + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { + value = message.GetReflection()->GetEnumValue(message, field_descriptor); + break; + } + default: { + break; + } + } +} + +void ConvertListFieldToJson(nlohmann::json &value, + const google::protobuf::Message &message, + const google::protobuf::FieldDescriptor *field_descriptor, + const OtlpHttpExporterOptions &options) +{ + auto field_size = message.GetReflection()->FieldSize(message, field_descriptor); + + switch (field_descriptor->cpp_type()) + { + case google::protobuf::FieldDescriptor::CPPTYPE_INT32: { + for (int i = 0; i < field_size; ++i) + { + value.push_back(message.GetReflection()->GetRepeatedInt32(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_INT64: { + for (int i = 0; i < field_size; ++i) + { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded + // as decimal strings, and either numbers or strings are accepted when decoding. + value.push_back(std::to_string( + message.GetReflection()->GetRepeatedInt64(message, field_descriptor, i))); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT32: { + for (int i = 0; i < field_size; ++i) + { + value.push_back(message.GetReflection()->GetRepeatedUInt32(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_UINT64: { + for (int i = 0; i < field_size; ++i) + { + // According to Protobuf specs 64-bit integer numbers in JSON-encoded payloads are encoded + // as decimal strings, and either numbers or strings are accepted when decoding. + value.push_back(std::to_string( + message.GetReflection()->GetRepeatedUInt64(message, field_descriptor, i))); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { + std::string empty; + if (field_descriptor->type() == google::protobuf::FieldDescriptor::TYPE_BYTES) + { + for (int i = 0; i < field_size; ++i) + { + value.push_back(BytesMapping(message.GetReflection()->GetRepeatedStringReference( + message, field_descriptor, i, &empty), + field_descriptor, options.json_bytes_mapping)); + } + } + else + { + for (int i = 0; i < field_size; ++i) + { + value.push_back(message.GetReflection()->GetRepeatedStringReference( + message, field_descriptor, i, &empty)); + } + } + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { + for (int i = 0; i < field_size; ++i) + { + nlohmann::json sub_value; + ConvertGenericMessageToJson( + sub_value, message.GetReflection()->GetRepeatedMessage(message, field_descriptor, i), + options); + value.push_back(std::move(sub_value)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_DOUBLE: { + for (int i = 0; i < field_size; ++i) + { + value.push_back(message.GetReflection()->GetRepeatedDouble(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_FLOAT: { + for (int i = 0; i < field_size; ++i) + { + value.push_back(message.GetReflection()->GetRepeatedFloat(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_BOOL: { + for (int i = 0; i < field_size; ++i) + { + value.push_back(message.GetReflection()->GetRepeatedBool(message, field_descriptor, i)); + } + + break; + } + case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { + for (int i = 0; i < field_size; ++i) + { + value.push_back( + message.GetReflection()->GetRepeatedEnumValue(message, field_descriptor, i)); + } + break; + } + default: { + break; + } + } +} + +/** + * Add span protobufs contained in recordables to request. + * @param spans the spans to export + * @param request the current request + */ +static void PopulateRequest( + const nostd::span> &spans, + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest *request) +{ + auto resource_span = request->add_resource_spans(); + auto instrumentation_lib = resource_span->add_instrumentation_library_spans(); + bool has_resource = false; + + for (auto &recordable : spans) + { + auto rec = std::unique_ptr( + static_cast(recordable.release())); + *instrumentation_lib->add_spans() = std::move(rec->span()); + + if (!has_resource) + { + *resource_span->mutable_resource() = rec->ProtoResource(); + has_resource = true; + } + } +} + +} // namespace + +OtlpHttpExporter::OtlpHttpExporter() : OtlpHttpExporter(OtlpHttpExporterOptions()) {} + +OtlpHttpExporter::OtlpHttpExporter(const OtlpHttpExporterOptions &options) : options_(options) {} + +// ----------------------------- Exporter methods ------------------------------ + +std::unique_ptr OtlpHttpExporter::MakeRecordable() noexcept +{ + return std::unique_ptr(new exporter::otlp::OtlpRecordable()); +} + +sdk::common::ExportResult OtlpHttpExporter::Export( + const nostd::span> &spans) noexcept +{ + // Return failure if this exporter has been shutdown + if (is_shutdown_) + { + if (options_.console_debug) + { + std::cerr << "[OTLP HTTP Exporter] Export failed, exporter is shutdown" << std::endl; + } + + return sdk::common::ExportResult::kFailure; + } + + proto::collector::trace::v1::ExportTraceServiceRequest service_request; + PopulateRequest(spans, &service_request); + + http_client::Body body_vec; + std::string content_type; + if (options_.content_type == HttpRequestContentType::kBinary) + { + body_vec.resize(service_request.ByteSizeLong()); + if (service_request.SerializeWithCachedSizesToArray( + reinterpret_cast(&body_vec[0]))) + { + if (options_.console_debug) + { + std::cout << "[OTLP HTTP Exporter] Request body(Binary):\n" + << service_request.Utf8DebugString() << std::endl; + } + } + else + { + if (options_.console_debug) + { + std::cout << "[OTLP HTTP Exporter] Serialize body failed(Binary):" + << service_request.InitializationErrorString() << std::endl; + } + return sdk::common::ExportResult::kFailure; + } + content_type = kHttpBinaryContentType; + } + else + { + nlohmann::json json_request; + + // Convert from proto into json object + ConvertGenericMessageToJson(json_request, service_request, options_); + + std::string post_body_json = + json_request.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace); + if (options_.console_debug) + { + std::cout << "[OTLP HTTP Exporter] Request body(Json):\n" << post_body_json << std::endl; + } + body_vec.assign(post_body_json.begin(), post_body_json.end()); + content_type = kHttpJsonContentType; + } + + // Send the request + auto client = http_client::HttpClientFactory::CreateSync(); + // TODO: Set timeout + auto result = client->Post(options_.url, body_vec, {{"Content-Type", content_type}}); + + // If an error occurred with the HTTP request + if (!result) + { + if (options_.console_debug) + { + switch (result.GetSessionState()) + { + case http_client::SessionState::CreateFailed: + std::cerr << "[OTLP HTTP Exporter] session state: session create failed" << std::endl; + break; + + case http_client::SessionState::Created: + std::cerr << "[OTLP HTTP Exporter] session state: session created" << std::endl; + break; + + case http_client::SessionState::Destroyed: + std::cerr << "[OTLP HTTP Exporter] session state: session destroyed" << std::endl; + break; + + case http_client::SessionState::Connecting: + std::cerr << "[OTLP HTTP Exporter] session state: connecting to peer" << std::endl; + break; + + case http_client::SessionState::ConnectFailed: + std::cerr << "[OTLP HTTP Exporter] session state: connection failed" << std::endl; + break; + + case http_client::SessionState::Connected: + std::cerr << "[OTLP HTTP Exporter] session state: connected" << std::endl; + break; + + case http_client::SessionState::Sending: + std::cerr << "[OTLP HTTP Exporter] session state: sending request" << std::endl; + break; + + case http_client::SessionState::SendFailed: + std::cerr << "[OTLP HTTP Exporter] session state: request send failed" << std::endl; + break; + + case http_client::SessionState::Response: + std::cerr << "[OTLP HTTP Exporter] session state: response received" << std::endl; + break; + + case http_client::SessionState::SSLHandshakeFailed: + std::cerr << "[OTLP HTTP Exporter] session state: SSL handshake failed" << std::endl; + break; + + case http_client::SessionState::TimedOut: + std::cerr << "[OTLP HTTP Exporter] session state: request time out" << std::endl; + break; + + case http_client::SessionState::NetworkError: + std::cerr << "[OTLP HTTP Exporter] session state: network error" << std::endl; + break; + + case http_client::SessionState::ReadError: + std::cerr << "[OTLP HTTP Exporter] session state: error reading response" << std::endl; + break; + + case http_client::SessionState::WriteError: + std::cerr << "[OTLP HTTP Exporter] session state: error writing request" << std::endl; + break; + + case http_client::SessionState::Cancelled: + std::cerr << "[OTLP HTTP Exporter] session state: (manually) cancelled" << std::endl; + break; + + default: + break; + } + } + // TODO: retry logic + return sdk::common::ExportResult::kFailure; + } + + if (options_.console_debug) + { + std::cout << "[OTLP HTTP Exporter] Status:" << result.GetResponse().GetStatusCode() << std::endl + << "Header:" << std::endl; + result.GetResponse().ForEachHeader([](opentelemetry::nostd::string_view header_name, + opentelemetry::nostd::string_view header_value) { + std::cout << "\t" << header_name.data() << " : " << header_value.data() << std::endl; + return true; + }); + std::cout << "Body:" << std::endl + << std::string(result.GetResponse().GetBody().begin(), + result.GetResponse().GetBody().end()) + << std::endl; + } + + return sdk::common::ExportResult::kSuccess; +} + +bool OtlpHttpExporter::Shutdown(std::chrono::microseconds) noexcept +{ + is_shutdown_ = true; + + // TODO: Shutdown the http request + + return true; +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/test/otlp_exporter_benchmark.cc b/exporters/otlp/test/otlp_grpc_exporter_benchmark.cc similarity index 92% rename from exporters/otlp/test/otlp_exporter_benchmark.cc rename to exporters/otlp/test/otlp_grpc_exporter_benchmark.cc index 6eee6dac83..e5dac38ba4 100644 --- a/exporters/otlp/test/otlp_exporter_benchmark.cc +++ b/exporters/otlp/test/otlp_grpc_exporter_benchmark.cc @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -#include "opentelemetry/exporters/otlp/otlp_exporter.h" +#include "opentelemetry/exporters/otlp/otlp_grpc_exporter.h" #include "opentelemetry/exporters/otlp/otlp_recordable.h" #include @@ -57,8 +57,8 @@ class FakeServiceStub : public proto::collector::trace::v1::TraceService::StubIn } }; -// OtlpExporterTestPeer is a friend class of OtlpExporter -class OtlpExporterTestPeer +// OtlpGrpcExporterTestPeer is a friend class of OtlpGrpcExporter +class OtlpGrpcExporterTestPeer { public: std::unique_ptr GetExporter() @@ -67,7 +67,7 @@ class OtlpExporterTestPeer std::unique_ptr stub_interface( mock_stub); return std::unique_ptr( - new exporter::otlp::OtlpExporter(std::move(stub_interface))); + new exporter::otlp::OtlpGrpcExporter(std::move(stub_interface))); } }; @@ -125,7 +125,7 @@ void CreateDenseSpans(std::array, kBatch // Benchmark Export() with empty spans void BM_OtlpExporterEmptySpans(benchmark::State &state) { - std::unique_ptr testpeer(new OtlpExporterTestPeer()); + std::unique_ptr testpeer(new OtlpGrpcExporterTestPeer()); auto exporter = testpeer->GetExporter(); while (state.KeepRunningBatch(kNumIterations)) @@ -140,7 +140,7 @@ BENCHMARK(BM_OtlpExporterEmptySpans); // Benchmark Export() with sparse spans void BM_OtlpExporterSparseSpans(benchmark::State &state) { - std::unique_ptr testpeer(new OtlpExporterTestPeer()); + std::unique_ptr testpeer(new OtlpGrpcExporterTestPeer()); auto exporter = testpeer->GetExporter(); while (state.KeepRunningBatch(kNumIterations)) @@ -155,7 +155,7 @@ BENCHMARK(BM_OtlpExporterSparseSpans); // Benchmark Export() with dense spans void BM_OtlpExporterDenseSpans(benchmark::State &state) { - std::unique_ptr testpeer(new OtlpExporterTestPeer()); + std::unique_ptr testpeer(new OtlpGrpcExporterTestPeer()); auto exporter = testpeer->GetExporter(); while (state.KeepRunningBatch(kNumIterations)) diff --git a/exporters/otlp/test/otlp_exporter_test.cc b/exporters/otlp/test/otlp_grpc_exporter_test.cc similarity index 85% rename from exporters/otlp/test/otlp_exporter_test.cc rename to exporters/otlp/test/otlp_grpc_exporter_test.cc index b9fe53b9ad..f7e82e6a46 100644 --- a/exporters/otlp/test/otlp_exporter_test.cc +++ b/exporters/otlp/test/otlp_grpc_exporter_test.cc @@ -16,7 +16,7 @@ // // That is because `std::result_of` has been removed in C++20. -# include "opentelemetry/exporters/otlp/otlp_exporter.h" +# include "opentelemetry/exporters/otlp/otlp_grpc_exporter.h" # include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" @@ -39,24 +39,25 @@ namespace exporter namespace otlp { -class OtlpExporterTestPeer : public ::testing::Test +class OtlpGrpcExporterTestPeer : public ::testing::Test { public: std::unique_ptr GetExporter( std::unique_ptr &stub_interface) { - return std::unique_ptr(new OtlpExporter(std::move(stub_interface))); + return std::unique_ptr( + new OtlpGrpcExporter(std::move(stub_interface))); } // Get the options associated with the given exporter. - const OtlpExporterOptions &GetOptions(std::unique_ptr &exporter) + const OtlpGrpcExporterOptions &GetOptions(std::unique_ptr &exporter) { return exporter->options_; } }; // Call Export() directly -TEST_F(OtlpExporterTestPeer, ExportUnitTest) +TEST_F(OtlpGrpcExporterTestPeer, ExportUnitTest) { auto mock_stub = new proto::collector::trace::v1::MockTraceServiceStub(); std::unique_ptr stub_interface( @@ -84,7 +85,7 @@ TEST_F(OtlpExporterTestPeer, ExportUnitTest) } // Create spans, let processor call Export() -TEST_F(OtlpExporterTestPeer, ExportIntegrationTest) +TEST_F(OtlpGrpcExporterTestPeer, ExportIntegrationTest) { auto mock_stub = new proto::collector::trace::v1::MockTraceServiceStub(); std::unique_ptr stub_interface( @@ -109,22 +110,22 @@ TEST_F(OtlpExporterTestPeer, ExportIntegrationTest) } // Test exporter configuration options -TEST_F(OtlpExporterTestPeer, ConfigTest) +TEST_F(OtlpGrpcExporterTestPeer, ConfigTest) { - OtlpExporterOptions opts; + OtlpGrpcExporterOptions opts; opts.endpoint = "localhost:45454"; - std::unique_ptr exporter(new OtlpExporter(opts)); + std::unique_ptr exporter(new OtlpGrpcExporter(opts)); EXPECT_EQ(GetOptions(exporter).endpoint, "localhost:45454"); } // Test exporter configuration options with use_ssl_credentials -TEST_F(OtlpExporterTestPeer, ConfigSslCredentialsTest) +TEST_F(OtlpGrpcExporterTestPeer, ConfigSslCredentialsTest) { std::string cacert_str = "--begin and end fake cert--"; - OtlpExporterOptions opts; + OtlpGrpcExporterOptions opts; opts.use_ssl_credentials = true; opts.ssl_credentials_cacert_as_string = cacert_str; - std::unique_ptr exporter(new OtlpExporter(opts)); + std::unique_ptr exporter(new OtlpGrpcExporter(opts)); EXPECT_EQ(GetOptions(exporter).ssl_credentials_cacert_as_string, cacert_str); EXPECT_EQ(GetOptions(exporter).use_ssl_credentials, true); } diff --git a/exporters/otlp/test/otlp_http_exporter_test.cc b/exporters/otlp/test/otlp_http_exporter_test.cc new file mode 100644 index 0000000000..ef4ab2094e --- /dev/null +++ b/exporters/otlp/test/otlp_http_exporter_test.cc @@ -0,0 +1,323 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifndef HAVE_CPP_STDLIB + +# include "opentelemetry/exporters/otlp/otlp_http_exporter.h" + +# include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" + +# include "opentelemetry/proto/collector/trace/v1/trace_service.pb.h" + +# include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" + +# include "opentelemetry/ext/http/server/http_server.h" +# include "opentelemetry/sdk/trace/batch_span_processor.h" +# include "opentelemetry/sdk/trace/tracer_provider.h" +# include "opentelemetry/trace/provider.h" + +# include + +# include "nlohmann/json.hpp" + +using namespace testing; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +template +static nostd::span MakeSpan(T (&array)[N]) +{ + return nostd::span(array); +} + +class OtlpHttpExporterTestPeer : public ::testing::Test, public HTTP_SERVER_NS::HttpRequestCallback +{ +protected: + HTTP_SERVER_NS::HttpServer server_; + std::string server_address_; + std::atomic is_setup_; + std::atomic is_running_; + std::mutex mtx_requests; + std::condition_variable cv_got_events; + std::vector received_requests_json_; + std::vector + received_requests_binary_; + +public: + OtlpHttpExporterTestPeer() : is_setup_(false), is_running_(false){}; + + virtual void SetUp() override + { + if (is_setup_.exchange(true)) + { + return; + } + int port = server_.addListeningPort(14371); + std::ostringstream os; + os << "localhost:" << port; + server_address_ = "http://" + os.str() + kDefaultTracePath; + server_.setServerName(os.str()); + server_.setKeepalive(false); + server_.addHandler(kDefaultTracePath, *this); + server_.start(); + is_running_ = true; + } + + virtual void TearDown() override + { + if (!is_setup_.exchange(false)) + return; + server_.stop(); + is_running_ = false; + } + + virtual int onHttpRequest(HTTP_SERVER_NS::HttpRequest const &request, + HTTP_SERVER_NS::HttpResponse &response) override + { + const std::string *request_content_type = nullptr; + { + auto it = request.headers.find("Content-Type"); + if (it != request.headers.end()) + { + request_content_type = &it->second; + } + } + + if (request.uri == kDefaultTracePath) + { + response.headers["Content-Type"] = "application/json"; + std::unique_lock lk(mtx_requests); + if (nullptr != request_content_type && *request_content_type == kHttpBinaryContentType) + { + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request_body; + if (request_body.ParseFromArray(&request.content[0], + static_cast(request.content.size()))) + { + received_requests_binary_.push_back(request_body); + response.body = "{\"code\": 0, \"message\": \"success\"}"; + } + else + { + response.body = "{\"code\": 400, \"message\": \"Parse binary failed\"}"; + return 400; + } + } + else if (nullptr != request_content_type && *request_content_type == kHttpJsonContentType) + { + auto json = nlohmann::json::parse(request.content, nullptr, false); + response.headers["Content-Type"] = "application/json"; + if (json.is_discarded()) + { + response.body = "{\"code\": 400, \"message\": \"Parse json failed\"}"; + return 400; + } + else + { + received_requests_json_.push_back(json); + response.body = "{\"code\": 0, \"message\": \"success\"}"; + } + } + else + { + response.body = "{\"code\": 400, \"message\": \"Unsupported content type\"}"; + return 400; + } + + return 200; + } + else + { + std::unique_lock lk(mtx_requests); + response.headers["Content-Type"] = "text/plain"; + response.body = "404 Not Found"; + return 200; + } + } + + bool waitForRequests(unsigned timeOutSec, size_t expected_count = 1) + { + std::unique_lock lk(mtx_requests); + if (cv_got_events.wait_for(lk, std::chrono::milliseconds(1000 * timeOutSec), + [&] { return getCurrentRequestCount() >= expected_count; })) + { + return true; + } + return false; + } + + size_t getCurrentRequestCount() const + { + return received_requests_json_.size() + received_requests_binary_.size(); + } + +public: + std::unique_ptr GetExporter(HttpRequestContentType content_type) + { + OtlpHttpExporterOptions opts; + opts.url = server_address_; + opts.content_type = content_type; + opts.console_debug = true; + return std::unique_ptr(new OtlpHttpExporter(opts)); + } + + // Get the options associated with the given exporter. + const OtlpHttpExporterOptions &GetOptions(std::unique_ptr &exporter) + { + return exporter->options_; + } +}; + +// Create spans, let processor call Export() +TEST_F(OtlpHttpExporterTestPeer, ExportJsonIntegrationTest) +{ + size_t old_count = getCurrentRequestCount(); + auto exporter = GetExporter(HttpRequestContentType::kJson); + + opentelemetry::sdk::resource::ResourceAttributes resource_attributes = { + {"service.name", "unit_test_service"}, {"tenant.id", "test_user"}}; + resource_attributes["bool_value"] = true; + resource_attributes["int32_value"] = static_cast(1); + resource_attributes["uint32_value"] = static_cast(2); + resource_attributes["int64_value"] = static_cast(0x1100000000LL); + resource_attributes["uint64_value"] = static_cast(0x1200000000ULL); + resource_attributes["double_value"] = static_cast(3.1); + resource_attributes["vec_bool_value"] = std::vector{true, false, true}; + resource_attributes["vec_int32_value"] = std::vector{1, 2}; + resource_attributes["vec_uint32_value"] = std::vector{3, 4}; + resource_attributes["vec_int64_value"] = std::vector{5, 6}; + resource_attributes["vec_uint64_value"] = std::vector{7, 8}; + resource_attributes["vec_double_value"] = std::vector{3.2, 3.3}; + resource_attributes["vec_string_value"] = std::vector{"vector", "string"}; + auto resource = opentelemetry::sdk::resource::Resource::Create(resource_attributes); + + auto processor_opts = sdk::trace::BatchSpanProcessorOptions(); + processor_opts.max_export_batch_size = 5; + processor_opts.max_queue_size = 5; + processor_opts.schedule_delay_millis = std::chrono::milliseconds(256); + auto processor = std::unique_ptr( + new sdk::trace::BatchSpanProcessor(std::move(exporter), processor_opts)); + auto provider = nostd::shared_ptr( + new sdk::trace::TracerProvider(std::move(processor), resource)); + + std::string report_trace_id; + { + char trace_id_hex[2 * opentelemetry::trace::TraceId::kSize] = {0}; + auto tracer = provider->GetTracer("test"); + auto parent_span = tracer->StartSpan("Test parent span"); + + opentelemetry::trace::StartSpanOptions child_span_opts = {}; + child_span_opts.parent = parent_span->GetContext(); + + auto child_span = tracer->StartSpan("Test child span", child_span_opts); + child_span->End(); + parent_span->End(); + + child_span_opts.parent.trace_id().ToLowerBase16(MakeSpan(trace_id_hex)); + report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); + } + + ASSERT_TRUE(waitForRequests(2, old_count + 1)); + auto check_json = received_requests_json_.back(); + auto resource_span = *check_json["resource_spans"].begin(); + auto instrumentation_library_span = *resource_span["instrumentation_library_spans"].begin(); + auto span = *instrumentation_library_span["spans"].begin(); + auto received_trace_id = span["trace_id"].get(); + EXPECT_EQ(received_trace_id, report_trace_id); +} + +// Create spans, let processor call Export() +TEST_F(OtlpHttpExporterTestPeer, ExportBinaryIntegrationTest) +{ + size_t old_count = getCurrentRequestCount(); + + auto exporter = GetExporter(HttpRequestContentType::kBinary); + + opentelemetry::sdk::resource::ResourceAttributes resource_attributes = { + {"service.name", "unit_test_service"}, {"tenant.id", "test_user"}}; + resource_attributes["bool_value"] = true; + resource_attributes["int32_value"] = static_cast(1); + resource_attributes["uint32_value"] = static_cast(2); + resource_attributes["int64_value"] = static_cast(0x1100000000LL); + resource_attributes["uint64_value"] = static_cast(0x1200000000ULL); + resource_attributes["double_value"] = static_cast(3.1); + resource_attributes["vec_bool_value"] = std::vector{true, false, true}; + resource_attributes["vec_int32_value"] = std::vector{1, 2}; + resource_attributes["vec_uint32_value"] = std::vector{3, 4}; + resource_attributes["vec_int64_value"] = std::vector{5, 6}; + resource_attributes["vec_uint64_value"] = std::vector{7, 8}; + resource_attributes["vec_double_value"] = std::vector{3.2, 3.3}; + resource_attributes["vec_string_value"] = std::vector{"vector", "string"}; + auto resource = opentelemetry::sdk::resource::Resource::Create(resource_attributes); + + auto processor_opts = sdk::trace::BatchSpanProcessorOptions(); + processor_opts.max_export_batch_size = 5; + processor_opts.max_queue_size = 5; + processor_opts.schedule_delay_millis = std::chrono::milliseconds(256); + + auto processor = std::unique_ptr( + new sdk::trace::BatchSpanProcessor(std::move(exporter), processor_opts)); + auto provider = nostd::shared_ptr( + new sdk::trace::TracerProvider(std::move(processor), resource)); + + std::string report_trace_id; + { + uint8_t trace_id_binary[opentelemetry::trace::TraceId::kSize] = {0}; + auto tracer = provider->GetTracer("test"); + auto parent_span = tracer->StartSpan("Test parent span"); + + opentelemetry::trace::StartSpanOptions child_span_opts = {}; + child_span_opts.parent = parent_span->GetContext(); + + auto child_span = tracer->StartSpan("Test child span", child_span_opts); + child_span->End(); + parent_span->End(); + + child_span_opts.parent.trace_id().CopyBytesTo(MakeSpan(trace_id_binary)); + report_trace_id.assign(reinterpret_cast(trace_id_binary), sizeof(trace_id_binary)); + } + + ASSERT_TRUE(waitForRequests(2, old_count + 1)); + + auto received_trace_id = received_requests_binary_.back() + .resource_spans(0) + .instrumentation_library_spans(0) + .spans(0) + .trace_id(); + EXPECT_EQ(received_trace_id, report_trace_id); +} + +// Test exporter configuration options +TEST_F(OtlpHttpExporterTestPeer, ConfigTest) +{ + OtlpHttpExporterOptions opts; + opts.url = "http://localhost:45455/v1/traces"; + std::unique_ptr exporter(new OtlpHttpExporter(opts)); + EXPECT_EQ(GetOptions(exporter).url, "http://localhost:45455/v1/traces"); +} + +// Test exporter configuration options with use_json_name +TEST_F(OtlpHttpExporterTestPeer, ConfigUseJsonNameTest) +{ + OtlpHttpExporterOptions opts; + opts.use_json_name = true; + std::unique_ptr exporter(new OtlpHttpExporter(opts)); + EXPECT_EQ(GetOptions(exporter).use_json_name, true); +} + +// Test exporter configuration options with json_bytes_mapping=JsonBytesMappingKind::kHex +TEST_F(OtlpHttpExporterTestPeer, ConfigJsonBytesMappingTest) +{ + OtlpHttpExporterOptions opts; + opts.json_bytes_mapping = JsonBytesMappingKind::kHex; + std::unique_ptr exporter(new OtlpHttpExporter(opts)); + EXPECT_EQ(GetOptions(exporter).json_bytes_mapping, JsonBytesMappingKind::kHex); +} + +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/opentelemetry-cpp-config.cmake.in b/opentelemetry-cpp-config.cmake.in index 3cd1e1c274..03951e6488 100644 --- a/opentelemetry-cpp-config.cmake.in +++ b/opentelemetry-cpp-config.cmake.in @@ -29,7 +29,8 @@ # opentelemetry-cpp::metrics - Imported target of opentelemetry-cpp::metrics # opentelemetry-cpp::logs - Imported target of opentelemetry-cpp::logs # opentelemetry-cpp::in_memory_span_exporter - Imported target of opentelemetry-cpp::in_memory_span_exporter -# opentelemetry-cpp::otlp_exporter - Imported target of opentelemetry-cpp::otlp_exporter +# opentelemetry-cpp::otlp_grpc_exporter - Imported target of opentelemetry-cpp::otlp_grpc_exporter +# opentelemetry-cpp::otlp_http_exporter - Imported target of opentelemetry-cpp::otlp_http_exporter # opentelemetry-cpp::ostream_log_exporter - Imported target of opentelemetry-cpp::ostream_log_exporter # opentelemetry-cpp::ostream_metrics_exporter - Imported target of opentelemetry-cpp::ostream_metrics_exporter # opentelemetry-cpp::ostream_span_exporter - Imported target of opentelemetry-cpp::ostream_span_exporter @@ -77,7 +78,8 @@ set(_OPENTELEMETRY_CPP_LIBRARIES_TEST_TARGETS metrics logs in_memory_span_exporter - otlp_exporter + otlp_grpc_exporter + otlp_http_exporter ostream_log_exporter ostream_metrics_exporter ostream_span_exporter