From 4603f206cc7788abdb23ce8ad13f861774e541c0 Mon Sep 17 00:00:00 2001 From: WenTao Ou Date: Tue, 7 Jun 2022 11:06:56 +0800 Subject: [PATCH] Squashed commit of the following: commit c484d162b9196de9524225bf13e0933ffb187881 Merge: 0abf1626 7e90dae3 Author: WenTao Ou Date: Tue Jun 7 10:57:25 2022 +0800 Merge remote-tracking branch 'opentelemetry/main' into async-changes Signed-off-by: WenTao Ou # Conflicts: # CHANGELOG.md commit 0abf1626fbf9948c57393556e5b3e96270d90f77 Merge: cd3655f4 5c8f4764 Author: WenTao Ou Date: Wed May 25 12:11:04 2022 +0800 Merge remote-tracking branch 'opentelemetry/main' into merge_main_into_async-changes commit cd3655f4fe950aa3a8d17c8b5b7bb85718352e77 Merge: d7b03e8d 63803d10 Author: owent Date: Fri May 20 14:28:31 2022 +0800 Merge branch 'merge_async-changes_into_main' into merge_main_into_async-changes Signed-off-by: owent # Conflicts: # CHANGELOG.md # CMakeLists.txt # api/CMakeLists.txt # api/include/opentelemetry/common/spin_lock_mutex.h # ci/do_ci.ps1 # ci/do_ci.sh # examples/common/metrics_foo_library/foo_library.cc # examples/prometheus/prometheus.yml # exporters/otlp/test/otlp_http_log_exporter_test.cc # ext/src/http/client/curl/CMakeLists.txt commit d7b03e8daed698a26dcdf35da420d2cf11daab32 Author: WenTao Ou Date: Fri May 20 13:25:24 2022 +0800 Merge main into async changes (#1411) commit 0aebd6e84107d166d952148cbc2bafc9e9b2c4dc Author: WenTao Ou Date: Mon May 16 23:26:41 2022 +0800 Merge main into async changes (#1395) commit 08a12b5e554a9fbc86fa3c32f4b30b2084b81653 Author: WenTao Ou Date: Thu May 12 00:51:48 2022 +0800 Cocurrency otlp http session (#1317) commit c614258e5633b2bf21e16fc8d3a479fcadfceea0 Author: DEBAJIT DAS <85024550+DebajitDas@users.noreply.github.com> Date: Wed May 4 22:55:20 2022 +0530 Added max async export support using separate AsyncBatchSpan/LogProcessor (#1306) commit 465158c75c30b2750da713e5ec05b1c0064790ea Author: WenTao Ou Date: Mon Apr 25 23:48:02 2022 +0800 Merge `main` into `async-changes` (#1348) * install sdk config (#1273) * Bump actions/cache from 2 to 3 (#1277) * Add owent as an Approver (#1276) * add owent as reviewer * fix order * Disable benchmark action failure (#1284) * metrics exemplar round 1 (#1264) * [Metrics SDK] - fix spelling (AggregationTemporarily to AggregationTemporality) (#1288) * fix compilation error with protobuf 3.5 (#1289) * Fix span SetAttribute crash (#1283) * Synchronous Metric collection (Delta , Cumulative) (#1265) * Rename `http_client_curl` to `opentelemetry_http_client_curl` (#1301) Signed-off-by: owent * Don't show coverage annotation for pull requests (#1304) * Implement periodic exporting metric reader (#1286) * Add `async-changes` branch to pull_request of github action (#1309) Signed-off-by: owent * Add InstrumentationInfo and Resource to the metrics data to be exported. (#1299) * Excempt should be applied on issue instead of PR (#1316) * Bump codecov/codecov-action from 2.1.0 to 3 (#1318) * Move public definitions into `opentelemetry_api`. (#1314) Signed-off-by: owent * Add building test without RTTI (#1294) * Remove implicitly deleted default constructor (#1267) Co-authored-by: Tom Tan Co-authored-by: Lalit Kumar Bhasin * [ETW Exporter] - ETW provider handle cleanup (#1322) * Bump actions/stale from 4 to 5 (#1323) * ostream metrics example (#1312) * Prepare v1.3.0 release (#1324) * Update yield logic for ARM processor (#1325) * Fix for #1292 (#1326) * Implement Merge and Diff operation for Histogram Aggregation (#1303) * fix metrics compiler warnings (#1328) * Replace deprecated googletest API (#1327) * Remove redundant tail / in CMake install (#1329) * dependencies image as artifact (#1333) Co-authored-by: Lalit Kumar Bhasin * metrics histogram example (#1330) * Link `opentelemetry_ext` with `opentelemetry_api` (#1336) * ostream metrics cmake (#1344) * Fix conflicts Signed-off-by: owent * Using clang-format-10 to format codes(clang-format-14 has a different output) Signed-off-by: owent Co-authored-by: Ehsan Saei <71217171+esigo@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Lalit Kumar Bhasin Co-authored-by: Tom Tan Co-authored-by: Ben Landrum <71329856+benlandrum@users.noreply.github.com> Co-authored-by: jmanjon <67091862+juandemanjon@users.noreply.github.com> commit 73f3515b044f8e47db52b4db526daed88d07d1b4 Author: WenTao Ou Date: Wed Apr 6 15:41:42 2022 +0800 Merge main into async changes (#1321) commit ad3bdfe4053051bdeb5788e3fa4457cb22a27851 Author: DEBAJIT DAS <85024550+DebajitDas@users.noreply.github.com> Date: Thu Mar 31 09:56:24 2022 +0530 Added feature flag for asynchronous export (#1295) commit 15e7725bcf654d96865214566c4070ae0a79e2f6 Author: WenTao Ou Date: Tue Mar 29 13:09:11 2022 +0800 Cocurrency otlp http session (#1281) commit 729c2f8e60ef95b4976f061a1715c10b2383d92a Author: DEBAJIT DAS <85024550+DebajitDas@users.noreply.github.com> Date: Tue Mar 22 23:47:14 2022 +0530 Changing the type of callback function in Export function to std::function (#1278) commit 6f53da3ccdb73882a6b252d073f474f6c1f9f30c Author: DEBAJIT DAS <85024550+DebajitDas@users.noreply.github.com> Date: Mon Mar 21 19:31:51 2022 +0530 Async callback Exporter to Processor changes (#1275) commit c3eaa9d4c97ccf2eee5634c1710e731f85467657 Author: WenTao Ou Date: Mon Mar 21 14:41:51 2022 +0800 Cocurrency otlp http session (#1274) --- .github/workflows/ci.yml | 22 + CHANGELOG.md | 4 + CMakeLists.txt | 1 + api/CMakeLists.txt | 4 + .../opentelemetry/common/spin_lock_mutex.h | 33 +- api/include/opentelemetry/common/timestamp.h | 34 + ci/do_ci.ps1 | 24 +- ci/do_ci.sh | 43 +- .../common/metrics_foo_library/foo_library.cc | 1 - examples/prometheus/prometheus.yml | 2 +- .../exporters/elasticsearch/es_log_exporter.h | 13 + .../elasticsearch/src/es_log_exporter.cc | 136 +++- .../exporters/jaeger/jaeger_exporter.h | 11 + exporters/jaeger/src/jaeger_exporter.cc | 11 + .../memory/in_memory_span_exporter.h | 16 + .../exporters/ostream/log_exporter.h | 9 + .../exporters/ostream/span_exporter.h | 8 + exporters/ostream/src/log_exporter.cc | 13 +- exporters/ostream/src/span_exporter.cc | 10 + .../exporters/otlp/otlp_grpc_exporter.h | 11 + .../exporters/otlp/otlp_grpc_log_exporter.h | 12 + .../exporters/otlp/otlp_http_client.h | 125 +++- .../exporters/otlp/otlp_http_exporter.h | 22 + .../exporters/otlp/otlp_http_log_exporter.h | 22 + exporters/otlp/src/otlp_grpc_exporter.cc | 12 + exporters/otlp/src/otlp_grpc_log_exporter.cc | 12 + exporters/otlp/src/otlp_http_client.cc | 448 +++++++++--- exporters/otlp/src/otlp_http_exporter.cc | 26 +- exporters/otlp/src/otlp_http_log_exporter.cc | 25 +- .../otlp/test/otlp_http_exporter_test.cc | 513 +++++++++---- .../otlp/test/otlp_http_log_exporter_test.cc | 575 ++++++++++----- .../exporters/zipkin/zipkin_exporter.h | 11 + exporters/zipkin/src/zipkin_exporter.cc | 11 + .../ext/http/client/curl/http_client_curl.h | 208 +++--- .../http/client/curl/http_operation_curl.h | 576 ++++----------- .../ext/http/client/http_client.h | 4 +- .../http/client/nosend/http_client_nosend.h | 68 +- ext/src/http/client/curl/BUILD | 1 + ext/src/http/client/curl/CMakeLists.txt | 5 +- ext/src/http/client/curl/http_client_curl.cc | 569 ++++++++++++++- .../http/client/curl/http_operation_curl.cc | 684 ++++++++++++++++++ .../http/client/nosend/http_client_nosend.cc | 27 + ext/test/http/curl_http_test.cc | 240 +++++- ext/test/w3c_tracecontext_test/main.cc | 4 +- .../sdk/logs/async_batch_log_processor.h | 104 +++ .../sdk/logs/batch_log_processor.h | 79 +- sdk/include/opentelemetry/sdk/logs/exporter.h | 11 + .../sdk/logs/simple_log_processor.h | 11 +- .../sdk/metrics/state/sync_metric_storage.h | 5 + .../sdk/metrics/view/attributes_processor.h | 2 - .../sdk/trace/async_batch_span_processor.h | 103 +++ .../sdk/trace/batch_span_processor.h | 44 +- .../opentelemetry/sdk/trace/exporter.h | 11 + .../sdk/trace/simple_processor.h | 35 +- sdk/src/logs/CMakeLists.txt | 1 + sdk/src/logs/async_batch_log_processor.cc | 194 +++++ sdk/src/logs/batch_log_processor.cc | 253 +++++-- sdk/src/logs/simple_log_processor.cc | 30 +- sdk/src/trace/CMakeLists.txt | 1 + sdk/src/trace/async_batch_span_processor.cc | 187 +++++ sdk/src/trace/batch_span_processor.cc | 235 ++++-- sdk/test/logs/BUILD | 15 + sdk/test/logs/CMakeLists.txt | 7 +- .../logs/async_batch_log_processor_test.cc | 374 ++++++++++ sdk/test/logs/batch_log_processor_test.cc | 13 + sdk/test/logs/simple_log_processor_test.cc | 19 + sdk/test/trace/CMakeLists.txt | 3 +- .../trace/async_batch_span_processor_test.cc | 375 ++++++++++ sdk/test/trace/batch_span_processor_test.cc | 13 + sdk/test/trace/simple_processor_test.cc | 9 + 70 files changed, 5501 insertions(+), 1239 deletions(-) create mode 100644 ext/src/http/client/curl/http_operation_curl.cc create mode 100644 sdk/include/opentelemetry/sdk/logs/async_batch_log_processor.h create mode 100644 sdk/include/opentelemetry/sdk/trace/async_batch_span_processor.h create mode 100644 sdk/src/logs/async_batch_log_processor.cc create mode 100644 sdk/src/trace/async_batch_span_processor.cc create mode 100644 sdk/test/logs/async_batch_log_processor_test.cc create mode 100644 sdk/test/trace/async_batch_span_processor_test.cc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7030c719d5..dea6b14c92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,6 +150,28 @@ jobs: - name: run tests run: ./ci/do_ci.sh bazel.test + bazel_test_async: + name: Bazel with async export + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + - name: Mount Bazel Cache + uses: actions/cache@v3 + env: + cache-name: bazel_cache + with: + path: /home/runner/.cache/bazel + key: bazel_test + - name: setup + run: | + sudo ./ci/setup_thrift.sh dependencies_only + sudo ./ci/setup_ci_environment.sh + sudo ./ci/install_bazelisk.sh + - name: run tests + run: ./ci/do_ci.sh bazel.with_async_export + bazel_with_abseil: name: Bazel with external abseil runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index aa491b1afb..934320ee2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Increment the: ## [Unreleased] +* [SDK] Async Batch Span/Log processor with max async support ([#1306](https://github.com/open-telemetry/opentelemetry-cpp/pull/1306)) +* [EXPORTER] OTLP http exporter allow concurrency session ([#1209](https://github.com/open-telemetry/opentelemetry-cpp/pull/1209)) +* [EXT] `curl::HttpClient` use `curl_multi_handle` instead of creating a thread + for every request and it's able to reuse connections now. ([#1317](https://github.com/open-telemetry/opentelemetry-cpp/pull/1317)) * [METRICS] Only record non-negative / finite / Non-NAN histogram values([#1427](https://github.com/open-telemetry/opentelemetry-cpp/pull/1427)) ## [1.4.0] 2022-05-17 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d2b274358..26387aee60 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,7 @@ option(WITH_EXAMPLES "Whether to build examples" ON) option(WITH_METRICS_PREVIEW "Whether to build metrics preview" OFF) option(WITH_LOGS_PREVIEW "Whether to build logs preview" OFF) +option(WITH_ASYNC_EXPORT_PREVIEW "Whether enable async export" OFF) find_package(Threads) diff --git a/api/CMakeLists.txt b/api/CMakeLists.txt index 375e8458e8..a87bf54cb3 100644 --- a/api/CMakeLists.txt +++ b/api/CMakeLists.txt @@ -92,3 +92,7 @@ if(WIN32) target_compile_definitions(opentelemetry_api INTERFACE HAVE_MSGPACK) endif() endif() + +if(WITH_ASYNC_EXPORT_PREVIEW) + target_compile_definitions(opentelemetry_api INTERFACE ENABLE_ASYNC_EXPORT) +endif() diff --git a/api/include/opentelemetry/common/spin_lock_mutex.h b/api/include/opentelemetry/common/spin_lock_mutex.h index d38d5791d5..764b5fc6b8 100644 --- a/api/include/opentelemetry/common/spin_lock_mutex.h +++ b/api/include/opentelemetry/common/spin_lock_mutex.h @@ -59,6 +59,24 @@ class SpinLockMutex SpinLockMutex &operator=(const SpinLockMutex &) = delete; SpinLockMutex &operator=(const SpinLockMutex &) volatile = delete; + static inline void fast_yield() noexcept + { +// Issue a Pause/Yield instruction while spinning. +#if defined(_MSC_VER) + YieldProcessor(); +#elif defined(__i386__) || defined(__x86_64__) +# if defined(__clang__) + _mm_pause(); +# else + __builtin_ia32_pause(); +# endif +#elif defined(__arm__) + __asm__ volatile("yield" ::: "memory"); +#else + // TODO: Issue PAGE/YIELD on other architectures. +#endif + } + /** * Attempts to lock the mutex. Return immediately with `true` (success) or `false` (failure). */ @@ -91,20 +109,7 @@ class SpinLockMutex { return; } -// Issue a Pause/Yield instruction while spinning. -#if defined(_MSC_VER) - YieldProcessor(); -#elif defined(__i386__) || defined(__x86_64__) -# if defined(__clang__) - _mm_pause(); -# else - __builtin_ia32_pause(); -# endif -#elif defined(__arm__) - __asm__ volatile("yield" ::: "memory"); -#else - // TODO: Issue PAGE/YIELD on other architectures. -#endif + fast_yield(); } // Yield then try again (goal ~100ns) std::this_thread::yield(); diff --git a/api/include/opentelemetry/common/timestamp.h b/api/include/opentelemetry/common/timestamp.h index 54e7b7aa6c..da8765b9bc 100644 --- a/api/include/opentelemetry/common/timestamp.h +++ b/api/include/opentelemetry/common/timestamp.h @@ -169,5 +169,39 @@ class SteadyTimestamp private: int64_t nanos_since_epoch_; }; + +class DurationUtil +{ +public: + template + static std::chrono::duration AdjustWaitForTimeout( + std::chrono::duration timeout, + std::chrono::duration indefinite_value) noexcept + { + // Do not call now() when this duration is max value, now() may have a expensive cost. + if (timeout == std::chrono::duration::max()) + { + return indefinite_value; + } + + // std::future::wait_for, std::this_thread::sleep_for, and std::condition_variable::wait_for + // may use steady_clock or system_clock.We need make sure now() + timeout do not overflow. + auto max_timeout = std::chrono::duration_cast>( + std::chrono::steady_clock::time_point::max() - std::chrono::steady_clock::now()); + if (timeout >= max_timeout) + { + return indefinite_value; + } + max_timeout = std::chrono::duration_cast>( + std::chrono::system_clock::time_point::max() - std::chrono::system_clock::now()); + if (timeout >= max_timeout) + { + return indefinite_value; + } + + return timeout; + } +}; + } // namespace common OPENTELEMETRY_END_NAMESPACE diff --git a/ci/do_ci.ps1 b/ci/do_ci.ps1 index b32c9ff54b..7663ccee40 100644 --- a/ci/do_ci.ps1 +++ b/ci/do_ci.ps1 @@ -5,7 +5,7 @@ $action = $args[0] $SRC_DIR = (Get-Item -Path ".\").FullName -$BAZEL_OPTIONS = "--copt=-DENABLE_METRICS_PREVIEW --copt=-DENABLE_LOGS_PREVIEW" +$BAZEL_OPTIONS = "--copt=-DENABLE_METRICS_PREVIEW --copt=-DENABLE_LOGS_PREVIEW --copt=-DENABLE_ASYNC_EXPORT" $BAZEL_TEST_OPTIONS = "$BAZEL_OPTIONS --test_output=errors" if (!(test-path build)) { @@ -31,8 +31,9 @@ switch ($action) { "cmake.test" { cd "$BUILD_DIR" cmake $SRC_DIR ` - -DVCPKG_TARGET_TRIPLET=x64-windows ` - "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" + -DVCPKG_TARGET_TRIPLET=x64-windows ` + -DWITH_ASYNC_EXPORT_PREVIEW=ON ` + "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" $exit = $LASTEXITCODE if ($exit -ne 0) { exit $exit @@ -51,9 +52,10 @@ switch ($action) { "cmake.exporter.otprotocol.test" { cd "$BUILD_DIR" cmake $SRC_DIR ` - -DVCPKG_TARGET_TRIPLET=x64-windows ` - -DWITH_OTPROTCOL=ON ` - "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" + -DVCPKG_TARGET_TRIPLET=x64-windows ` + -DWITH_ASYNC_EXPORT_PREVIEW=ON ` + -DWITH_OTPROTCOL=ON ` + "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" $exit = $LASTEXITCODE if ($exit -ne 0) { exit $exit @@ -72,8 +74,9 @@ switch ($action) { "cmake.build_example_plugin" { cd "$BUILD_DIR" cmake $SRC_DIR ` - -DVCPKG_TARGET_TRIPLET=x64-windows ` - "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" + -DVCPKG_TARGET_TRIPLET=x64-windows ` + -DWITH_ASYNC_EXPORT_PREVIEW=ON ` + "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" $exit = $LASTEXITCODE if ($exit -ne 0) { exit $exit @@ -88,8 +91,9 @@ switch ($action) { "cmake.test_example_plugin" { cd "$BUILD_DIR" cmake $SRC_DIR ` - -DVCPKG_TARGET_TRIPLET=x64-windows ` - "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" + -DVCPKG_TARGET_TRIPLET=x64-windows ` + -DWITH_ASYNC_EXPORT_PREVIEW=ON ` + "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_DIR/scripts/buildsystems/vcpkg.cmake" $exit = $LASTEXITCODE if ($exit -ne 0) { exit $exit diff --git a/ci/do_ci.sh b/ci/do_ci.sh index e11ca0d2ec..47b556c342 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -25,7 +25,7 @@ function run_benchmarks [ -z "${BENCHMARK_DIR}" ] && export BENCHMARK_DIR=$HOME/benchmark mkdir -p $BENCHMARK_DIR - bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS -c opt -- \ + bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS_ASYNC -c opt -- \ $(bazel query 'attr("tags", "benchmark_result", ...)') echo "" echo "Benchmark results in $BENCHMARK_DIR:" @@ -66,8 +66,11 @@ if [[ "$1" != "bazel.nortti" ]]; then fi BAZEL_TEST_OPTIONS="$BAZEL_OPTIONS --test_output=errors" +BAZEL_OPTIONS_ASYNC="$BAZEL_OPTIONS --copt=-DENABLE_ASYNC_EXPORT" +BAZEL_TEST_OPTIONS_ASYNC="$BAZEL_OPTIONS_ASYNC --test_output=errors" + # https://github.com/bazelbuild/bazel/issues/4341 -BAZEL_MACOS_OPTIONS="$BAZEL_OPTIONS --features=-supports_dynamic_linker --build_tag_filters=-jaeger" +BAZEL_MACOS_OPTIONS="$BAZEL_OPTIONS_ASYNC --features=-supports_dynamic_linker --build_tag_filters=-jaeger" BAZEL_MACOS_TEST_OPTIONS="$BAZEL_MACOS_OPTIONS --test_output=errors" BAZEL_STARTUP_OPTIONS="--output_user_root=$HOME/.cache/bazel" @@ -85,6 +88,7 @@ if [[ "$1" == "cmake.test" ]]; then -DWITH_METRICS_PREVIEW=ON \ -DWITH_LOGS_PREVIEW=ON \ -DCMAKE_CXX_FLAGS="-Werror" \ + -DWITH_ASYNC_EXPORT_PREVIEW=ON \ "${SRC_DIR}" make make test @@ -96,6 +100,7 @@ elif [[ "$1" == "cmake.abseil.test" ]]; then -DWITH_METRICS_PREVIEW=ON \ -DWITH_LOGS_PREVIEW=ON \ -DCMAKE_CXX_FLAGS="-Werror" \ + -DWITH_ASYNC_EXPORT_PREVIEW=ON \ -DWITH_ABSEIL=ON \ "${SRC_DIR}" make @@ -106,6 +111,7 @@ elif [[ "$1" == "cmake.c++20.test" ]]; then rm -rf * cmake -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_FLAGS="-Werror" \ + -DWITH_ASYNC_EXPORT_PREVIEW=ON \ -DCMAKE_CXX_STANDARD=20 \ "${SRC_DIR}" make @@ -118,6 +124,7 @@ elif [[ "$1" == "cmake.c++20.stl.test" ]]; then -DWITH_METRICS_PREVIEW=ON \ -DWITH_LOGS_PREVIEW=ON \ -DCMAKE_CXX_FLAGS="-Werror" \ + -DWITH_ASYNC_EXPORT_PREVIEW=ON \ -DWITH_STL=ON \ "${SRC_DIR}" make @@ -145,6 +152,7 @@ elif [[ "$1" == "cmake.legacy.exporter.otprotocol.test" ]]; then cmake -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_STANDARD=11 \ -DWITH_OTLP=ON \ + -DWITH_ASYNC_EXPORT_PREVIEW=ON \ "${SRC_DIR}" grpc_cpp_plugin=`which grpc_cpp_plugin` proto_make_file="CMakeFiles/opentelemetry_proto.dir/build.make" @@ -157,6 +165,7 @@ elif [[ "$1" == "cmake.exporter.otprotocol.test" ]]; then rm -rf * cmake -DCMAKE_BUILD_TYPE=Debug \ -DWITH_OTLP=ON \ + -DWITH_ASYNC_EXPORT_PREVIEW=ON \ "${SRC_DIR}" grpc_cpp_plugin=`which grpc_cpp_plugin` proto_make_file="CMakeFiles/opentelemetry_proto.dir/build.make" @@ -165,8 +174,8 @@ elif [[ "$1" == "cmake.exporter.otprotocol.test" ]]; then cd exporters/otlp && make test exit 0 elif [[ "$1" == "bazel.with_abseil" ]]; then - bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS --//api:with_abseil=true //... - bazel $BAZEL_STARTUP_OPTIONS test $BAZEL_TEST_OPTIONS --//api:with_abseil=true //... + bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS_ASYNC --//api:with_abseil=true //... + bazel $BAZEL_STARTUP_OPTIONS test $BAZEL_TEST_OPTIONS_ASYNC --//api:with_abseil=true //... exit 0 elif [[ "$1" == "cmake.test_example_plugin" ]]; then # Build the plugin @@ -206,6 +215,10 @@ elif [[ "$1" == "bazel.test" ]]; then bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS //... bazel $BAZEL_STARTUP_OPTIONS test $BAZEL_TEST_OPTIONS //... exit 0 +elif [[ "$1" == "bazel.with_async_export" ]]; then + bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS_ASYNC //... + bazel $BAZEL_STARTUP_OPTIONS test $BAZEL_TEST_OPTIONS_ASYNC //... + exit 0 elif [[ "$1" == "bazel.benchmark" ]]; then run_benchmarks exit 0 @@ -216,34 +229,34 @@ elif [[ "$1" == "bazel.macos.test" ]]; then elif [[ "$1" == "bazel.legacy.test" ]]; then # we uses C++ future and async() function to test the Prometheus Exporter functionality, # that make this test always fail. ignore Prometheus exporter here. - bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS -- //... -//exporters/otlp/... -//exporters/prometheus/... - bazel $BAZEL_STARTUP_OPTIONS test $BAZEL_TEST_OPTIONS -- //... -//exporters/otlp/... -//exporters/prometheus/... + bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS_ASYNC -- //... -//exporters/otlp/... -//exporters/prometheus/... + bazel $BAZEL_STARTUP_OPTIONS test $BAZEL_TEST_OPTIONS_ASYNC -- //... -//exporters/otlp/... -//exporters/prometheus/... exit 0 elif [[ "$1" == "bazel.noexcept" ]]; then # there are some exceptions and error handling code from the Prometheus and Jaeger Clients # that make this test always fail. ignore Prometheus and Jaeger exporters in the noexcept here. - bazel $BAZEL_STARTUP_OPTIONS build --copt=-fno-exceptions --build_tag_filters=-jaeger $BAZEL_OPTIONS -- //... -//exporters/prometheus/... -//exporters/jaeger/... -//examples/prometheus/... - bazel $BAZEL_STARTUP_OPTIONS test --copt=-fno-exceptions --build_tag_filters=-jaeger $BAZEL_TEST_OPTIONS -- //... -//exporters/prometheus/... -//exporters/jaeger/... -//examples/prometheus/... + bazel $BAZEL_STARTUP_OPTIONS build --copt=-fno-exceptions --build_tag_filters=-jaeger $BAZEL_OPTIONS_ASYNC -- //... -//exporters/prometheus/... -//exporters/jaeger/... -//examples/prometheus/... + bazel $BAZEL_STARTUP_OPTIONS test --copt=-fno-exceptions --build_tag_filters=-jaeger $BAZEL_TEST_OPTIONS_ASYNC -- //... -//exporters/prometheus/... -//exporters/jaeger/... -//examples/prometheus/... exit 0 elif [[ "$1" == "bazel.nortti" ]]; then # there are some exceptions and error handling code from the Prometheus and Jaeger Clients # that make this test always fail. ignore Prometheus and Jaeger exporters in the noexcept here. - bazel $BAZEL_STARTUP_OPTIONS build --cxxopt=-fno-rtti --build_tag_filters=-jaeger $BAZEL_OPTIONS -- //... -//exporters/prometheus/... -//exporters/jaeger/... - bazel $BAZEL_STARTUP_OPTIONS test --cxxopt=-fno-rtti --build_tag_filters=-jaeger $BAZEL_TEST_OPTIONS -- //... -//exporters/prometheus/... -//exporters/jaeger/... + bazel $BAZEL_STARTUP_OPTIONS build --cxxopt=-fno-rtti --build_tag_filters=-jaeger $BAZEL_OPTIONS_ASYNC -- //... -//exporters/prometheus/... -//exporters/jaeger/... + bazel $BAZEL_STARTUP_OPTIONS test --cxxopt=-fno-rtti --build_tag_filters=-jaeger $BAZEL_TEST_OPTIONS_ASYNC -- //... -//exporters/prometheus/... -//exporters/jaeger/... exit 0 elif [[ "$1" == "bazel.asan" ]]; then - bazel $BAZEL_STARTUP_OPTIONS test --config=asan $BAZEL_TEST_OPTIONS //... + bazel $BAZEL_STARTUP_OPTIONS test --config=asan $BAZEL_TEST_OPTIONS_ASYNC //... exit 0 elif [[ "$1" == "bazel.tsan" ]]; then - bazel $BAZEL_STARTUP_OPTIONS test --config=tsan $BAZEL_TEST_OPTIONS //... + bazel $BAZEL_STARTUP_OPTIONS test --config=tsan $BAZEL_TEST_OPTIONS_ASYNC //... exit 0 elif [[ "$1" == "bazel.valgrind" ]]; then - bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS //... - bazel $BAZEL_STARTUP_OPTIONS test --run_under="/usr/bin/valgrind --leak-check=full --error-exitcode=1 --suppressions=\"${SRC_DIR}/ci/valgrind-suppressions\"" $BAZEL_TEST_OPTIONS //... + bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS_ASYNC //... + bazel $BAZEL_STARTUP_OPTIONS test --run_under="/usr/bin/valgrind --leak-check=full --error-exitcode=1 --suppressions=\"${SRC_DIR}/ci/valgrind-suppressions\"" $BAZEL_TEST_OPTIONS_ASYNC //... exit 0 elif [[ "$1" == "benchmark" ]]; then [ -z "${BENCHMARK_DIR}" ] && export BENCHMARK_DIR=$HOME/benchmark - bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS -c opt -- \ + bazel $BAZEL_STARTUP_OPTIONS build $BAZEL_OPTIONS_ASYNC -c opt -- \ $(bazel query 'attr("tags", "benchmark_result", ...)') echo "" echo "Benchmark results in $BENCHMARK_DIR:" diff --git a/examples/common/metrics_foo_library/foo_library.cc b/examples/common/metrics_foo_library/foo_library.cc index 2fcbd660e0..b895331d7e 100644 --- a/examples/common/metrics_foo_library/foo_library.cc +++ b/examples/common/metrics_foo_library/foo_library.cc @@ -15,7 +15,6 @@ namespace metrics_api = opentelemetry::metrics; namespace { - std::map get_random_attr() { static const std::vector> labels = {{"key1", "value1"}, diff --git a/examples/prometheus/prometheus.yml b/examples/prometheus/prometheus.yml index 17c42fda3f..382854818d 100644 --- a/examples/prometheus/prometheus.yml +++ b/examples/prometheus/prometheus.yml @@ -13,4 +13,4 @@ alerting: scrape_configs: - job_name: otel static_configs: - - targets: ['localhost:9464'] \ No newline at end of file + - targets: ['localhost:9464'] diff --git a/exporters/elasticsearch/include/opentelemetry/exporters/elasticsearch/es_log_exporter.h b/exporters/elasticsearch/include/opentelemetry/exporters/elasticsearch/es_log_exporter.h index ea58807e96..a5c6623228 100644 --- a/exporters/elasticsearch/include/opentelemetry/exporters/elasticsearch/es_log_exporter.h +++ b/exporters/elasticsearch/include/opentelemetry/exporters/elasticsearch/es_log_exporter.h @@ -89,6 +89,19 @@ class ElasticsearchLogExporter final : public opentelemetry::sdk::logs::LogExpor const opentelemetry::nostd::span> &records) noexcept override; +# ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a vector of log records to the Elasticsearch instance asynchronously. + * @param records A list of log records to send to Elasticsearch. + * @param result_callback callback function accepting ExportResult as argument + */ + void Export( + const opentelemetry::nostd::span> + &records, + std::function &&result_callback) noexcept + override; +# endif + /** * Shutdown this exporter. * @param timeout The maximum time to wait for the shutdown method to return diff --git a/exporters/elasticsearch/src/es_log_exporter.cc b/exporters/elasticsearch/src/es_log_exporter.cc index a5a66ebe01..1abe4ca00e 100644 --- a/exporters/elasticsearch/src/es_log_exporter.cc +++ b/exporters/elasticsearch/src/es_log_exporter.cc @@ -110,6 +110,91 @@ class ResponseHandler : public http_client::EventHandler bool console_debug_ = false; }; +# ifdef ENABLE_ASYNC_EXPORT +/** + * This class handles the async response message from the Elasticsearch request + */ +class AsyncResponseHandler : public http_client::EventHandler +{ +public: + /** + * Creates a response handler, that by default doesn't display to console + */ + AsyncResponseHandler( + std::shared_ptr session, + std::function &&result_callback, + bool console_debug = false) + : console_debug_{console_debug}, + session_{std::move(session)}, + result_callback_{std::move(result_callback)} + {} + + /** + * Cleans up the session in the destructor. + */ + ~AsyncResponseHandler() { session_->FinishSession(); } + + /** + * Automatically called when the response is received + */ + void OnResponse(http_client::Response &response) noexcept override + { + + // Store the body of the request + body_ = std::string(response.GetBody().begin(), response.GetBody().end()); + if (body_.find("\"failed\" : 0") == std::string::npos) + { + OTEL_INTERNAL_LOG_ERROR( + "[ES Trace Exporter] Logs were not written to Elasticsearch correctly, response body: " + << body_); + result_callback_(sdk::common::ExportResult::kFailure); + } + else + { + result_callback_(sdk::common::ExportResult::kSuccess); + } + } + + // Callback method when an http event occurs + void OnEvent(http_client::SessionState state, nostd::string_view reason) noexcept override + { + // If any failure event occurs, release the condition variable to unblock main thread + switch (state) + { + case http_client::SessionState::ConnectFailed: + OTEL_INTERNAL_LOG_ERROR("[ES Trace Exporter] Connection to elasticsearch failed"); + break; + case http_client::SessionState::SendFailed: + OTEL_INTERNAL_LOG_ERROR("[ES Trace Exporter] Request failed to be sent to elasticsearch"); + + break; + case http_client::SessionState::TimedOut: + OTEL_INTERNAL_LOG_ERROR("[ES Trace Exporter] Request to elasticsearch timed out"); + + break; + case http_client::SessionState::NetworkError: + OTEL_INTERNAL_LOG_ERROR("[ES Trace Exporter] Network error to elasticsearch"); + break; + default: + break; + } + result_callback_(sdk::common::ExportResult::kFailure); + } + +private: + // Stores the session object for the request + std::shared_ptr session_; + // Callback to call to on receiving events + std::function result_callback_; + + // A string to store the response body + std::string body_ = ""; + + // Whether to print the results from the callback + bool console_debug_ = false; +}; +# endif + ElasticsearchLogExporter::ElasticsearchLogExporter() : options_{ElasticsearchExporterOptions()}, http_client_{new ext::http::client::curl::HttpClient()} @@ -162,8 +247,8 @@ sdk::common::ExportResult ElasticsearchLogExporter::Export( request->SetBody(body_vec); // Send the request - std::unique_ptr handler(new ResponseHandler(options_.console_debug_)); - session->SendRequest(*handler); + auto handler = std::make_shared(options_.console_debug_); + session->SendRequest(handler); // Wait for the response to be received if (options_.console_debug_) @@ -198,6 +283,53 @@ sdk::common::ExportResult ElasticsearchLogExporter::Export( return sdk::common::ExportResult::kSuccess; } +# ifdef ENABLE_ASYNC_EXPORT +void ElasticsearchLogExporter::Export( + const opentelemetry::nostd::span> + &records, + std::function &&result_callback) noexcept +{ + // Return failure if this exporter has been shutdown + if (isShutdown()) + { + OTEL_INTERNAL_LOG_ERROR("[ES Log Exporter] Exporting " + << records.size() << " log(s) failed, exporter is shutdown"); + return; + } + + // Create a connection to the ElasticSearch instance + auto session = http_client_->CreateSession(options_.host_ + std::to_string(options_.port_)); + auto request = session->CreateRequest(); + + // Populate the request with headers and methods + request->SetUri(options_.index_ + "/_bulk?pretty"); + request->SetMethod(http_client::Method::Post); + request->AddHeader("Content-Type", "application/json"); + request->SetTimeoutMs(std::chrono::milliseconds(1000 * options_.response_timeout_)); + + // Create the request body + std::string body = ""; + for (auto &record : records) + { + // Append {"index":{}} before JSON body, which tells Elasticsearch to write to index specified + // in URI + body += "{\"index\" : {}}\n"; + + // Add the context of the Recordable + auto json_record = std::unique_ptr( + static_cast(record.release())); + body += json_record->GetJSON().dump() + "\n"; + } + std::vector body_vec(body.begin(), body.end()); + request->SetBody(body_vec); + + // Send the request + auto handler = std::make_shared(session, std::move(result_callback), + options_.console_debug_); + session->SendRequest(handler); +} +# endif + bool ElasticsearchLogExporter::Shutdown(std::chrono::microseconds timeout) noexcept { const std::lock_guard locked(lock_); diff --git a/exporters/jaeger/include/opentelemetry/exporters/jaeger/jaeger_exporter.h b/exporters/jaeger/include/opentelemetry/exporters/jaeger/jaeger_exporter.h index 284bab2cab..85003689c5 100644 --- a/exporters/jaeger/include/opentelemetry/exporters/jaeger/jaeger_exporter.h +++ b/exporters/jaeger/include/opentelemetry/exporters/jaeger/jaeger_exporter.h @@ -61,6 +61,17 @@ class JaegerExporter final : public opentelemetry::sdk::trace::SpanExporter const nostd::span> &spans) noexcept override; +#ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a batch of span recordables asynchronously. + * @param spans a span of unique pointers to span recordables + * @param result_callback callback function accepting ExportResult as argument + */ + void Export(const nostd::span> &spans, + std::function + &&result_callback) noexcept override; +#endif + /** * Shutdown the exporter. * @param timeout an option timeout, default to max. diff --git a/exporters/jaeger/src/jaeger_exporter.cc b/exporters/jaeger/src/jaeger_exporter.cc index c07f2f0100..b77b445100 100644 --- a/exporters/jaeger/src/jaeger_exporter.cc +++ b/exporters/jaeger/src/jaeger_exporter.cc @@ -70,6 +70,17 @@ sdk_common::ExportResult JaegerExporter::Export( return sdk_common::ExportResult::kSuccess; } +#ifdef ENABLE_ASYNC_EXPORT +void JaegerExporter::Export( + const nostd::span> &spans, + std::function &&result_callback) noexcept +{ + OTEL_INTERNAL_LOG_WARN(" async not supported. Making sync interface call"); + auto status = Export(spans); + result_callback(status); +} +#endif + void JaegerExporter::InitializeEndpoint() { if (options_.transport_format == TransportFormat::kThriftUdpCompact) diff --git a/exporters/memory/include/opentelemetry/exporters/memory/in_memory_span_exporter.h b/exporters/memory/include/opentelemetry/exporters/memory/in_memory_span_exporter.h index 28b7bc34e8..2eae4fc3c0 100644 --- a/exporters/memory/include/opentelemetry/exporters/memory/in_memory_span_exporter.h +++ b/exporters/memory/include/opentelemetry/exporters/memory/in_memory_span_exporter.h @@ -64,6 +64,22 @@ class InMemorySpanExporter final : public opentelemetry::sdk::trace::SpanExporte return sdk::common::ExportResult::kSuccess; } +#ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a batch of span recordables asynchronously. + * @param spans a span of unique pointers to span recordables + * @param result_callback callback function accepting ExportResult as argument + */ + void Export(const nostd::span> &spans, + std::function + &&result_callback) noexcept override + { + OTEL_INTERNAL_LOG_WARN(" async not supported. Making sync interface call"); + auto status = Export(spans); + result_callback(status); + } +#endif + /** * @param timeout an optional value containing the timeout of the exporter * note: passing custom timeout values is not currently supported for this exporter diff --git a/exporters/ostream/include/opentelemetry/exporters/ostream/log_exporter.h b/exporters/ostream/include/opentelemetry/exporters/ostream/log_exporter.h index ad1d54a215..65969a40a9 100644 --- a/exporters/ostream/include/opentelemetry/exporters/ostream/log_exporter.h +++ b/exporters/ostream/include/opentelemetry/exporters/ostream/log_exporter.h @@ -39,6 +39,15 @@ class OStreamLogExporter final : public opentelemetry::sdk::logs::LogExporter const opentelemetry::nostd::span> &records) noexcept override; +# ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a span of logs sent from the processor asynchronously. + */ + void Export( + const opentelemetry::nostd::span> &records, + std::function &&result_callback) noexcept; +# endif + /** * Marks the OStream Log Exporter as shut down. */ diff --git a/exporters/ostream/include/opentelemetry/exporters/ostream/span_exporter.h b/exporters/ostream/include/opentelemetry/exporters/ostream/span_exporter.h index 8122b6777a..334c4d6419 100644 --- a/exporters/ostream/include/opentelemetry/exporters/ostream/span_exporter.h +++ b/exporters/ostream/include/opentelemetry/exporters/ostream/span_exporter.h @@ -38,6 +38,14 @@ class OStreamSpanExporter final : public opentelemetry::sdk::trace::SpanExporter const opentelemetry::nostd::span> &spans) noexcept override; +#ifdef ENABLE_ASYNC_EXPORT + void Export( + const opentelemetry::nostd::span> + &spans, + std::function &&result_callback) noexcept + override; +#endif + bool Shutdown( std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override; diff --git a/exporters/ostream/src/log_exporter.cc b/exporters/ostream/src/log_exporter.cc index ba6997aa7e..acd507980e 100644 --- a/exporters/ostream/src/log_exporter.cc +++ b/exporters/ostream/src/log_exporter.cc @@ -179,7 +179,18 @@ sdk::common::ExportResult OStreamLogExporter::Export( return sdk::common::ExportResult::kSuccess; } -bool OStreamLogExporter::Shutdown(std::chrono::microseconds timeout) noexcept +# ifdef ENABLE_ASYNC_EXPORT +void OStreamLogExporter::Export( + const opentelemetry::nostd::span> &records, + std::function &&result_callback) noexcept +{ + // Do not have async support + auto result = Export(records); + result_callback(result); +} +# endif + +bool OStreamLogExporter::Shutdown(std::chrono::microseconds) noexcept { const std::lock_guard locked(lock_); is_shutdown_ = true; diff --git a/exporters/ostream/src/span_exporter.cc b/exporters/ostream/src/span_exporter.cc index dea72f57f8..67c1a51a4b 100644 --- a/exporters/ostream/src/span_exporter.cc +++ b/exporters/ostream/src/span_exporter.cc @@ -96,6 +96,16 @@ sdk::common::ExportResult OStreamSpanExporter::Export( return sdk::common::ExportResult::kSuccess; } +#ifdef ENABLE_ASYNC_EXPORT +void OStreamSpanExporter::Export( + const opentelemetry::nostd::span> &spans, + std::function &&result_callback) noexcept +{ + auto result = Export(spans); + result_callback(result); +} +#endif + bool OStreamSpanExporter::Shutdown(std::chrono::microseconds timeout) noexcept { const std::lock_guard locked(lock_); diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h index a28e6fca85..4660903595 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_exporter.h @@ -52,6 +52,17 @@ class OtlpGrpcExporter final : public opentelemetry::sdk::trace::SpanExporter sdk::common::ExportResult Export( const nostd::span> &spans) noexcept override; +#ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a batch of span recordables asynchronously. + * @param spans a span of unique pointers to span recordables + * @param result_callback callback function accepting ExportResult as argument + */ + virtual void Export(const nostd::span> &spans, + std::function + &&result_callback) noexcept override; +#endif + /** * Shut down the exporter. * @param timeout an optional timeout, the default timeout of 0 means that no diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_log_exporter.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_log_exporter.h index a8aeda85b8..5652a21f72 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_log_exporter.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_grpc_log_exporter.h @@ -55,6 +55,18 @@ class OtlpGrpcLogExporter : public opentelemetry::sdk::logs::LogExporter const nostd::span> &records) noexcept override; +# ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a vector of log records asynchronously. + * @param records A list of log records. + * @param result_callback callback function accepting ExportResult as argument + */ + virtual void Export( + const nostd::span> &records, + std::function &&result_callback) noexcept + override; +# endif + /** * Shutdown this exporter. * @param timeout The maximum time to wait for the shutdown method to return. 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 1a199bed48..edb0f59afc 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_client.h @@ -11,14 +11,21 @@ #include "opentelemetry/common/spin_lock_mutex.h" #include "opentelemetry/ext/http/client/http_client.h" +#include "opentelemetry/nostd/variant.h" #include "opentelemetry/sdk/common/exporter_utils.h" #include "opentelemetry/exporters/otlp/otlp_environment.h" +#include #include +#include +#include +#include +#include #include #include #include +#include OPENTELEMETRY_BEGIN_NAMESPACE namespace exporter @@ -71,20 +78,30 @@ struct OtlpHttpClientOptions // Additional HTTP headers OtlpHeaders http_headers = GetOtlpDefaultHeaders(); + // Concurrent requests + std::size_t max_concurrent_requests = 64; + + // Concurrent requests + std::size_t max_requests_per_connection = 8; + inline OtlpHttpClientOptions(nostd::string_view input_url, HttpRequestContentType input_content_type, JsonBytesMappingKind input_json_bytes_mapping, bool input_use_json_name, bool input_console_debug, std::chrono::system_clock::duration input_timeout, - const OtlpHeaders &input_http_headers) + const OtlpHeaders &input_http_headers, + std::size_t input_concurrent_sessions = 64, + std::size_t input_max_requests_per_connection = 8) : url(input_url), content_type(input_content_type), json_bytes_mapping(input_json_bytes_mapping), use_json_name(input_use_json_name), console_debug(input_console_debug), timeout(input_timeout), - http_headers(input_http_headers) + http_headers(input_http_headers), + max_concurrent_requests(input_concurrent_sessions), + max_requests_per_connection(input_max_requests_per_connection) {} }; @@ -99,13 +116,25 @@ class OtlpHttpClient */ explicit OtlpHttpClient(OtlpHttpClientOptions &&options); + ~OtlpHttpClient(); + /** - * Export + * Sync export * @param message message to export, it should be ExportTraceServiceRequest, * ExportMetricsServiceRequest or ExportLogsServiceRequest */ sdk::common::ExportResult Export(const google::protobuf::Message &message) noexcept; + /** + * Async export + * @param message message to export, it should be ExportTraceServiceRequest, + * ExportMetricsServiceRequest or ExportLogsServiceRequest + * @param result_callback callback to call when the exporting is done + */ + void Export( + const google::protobuf::Message &message, + std::function &&result_callback) noexcept; + /** * Shut down the HTTP client. * @param timeout an optional timeout, the default timeout of 0 means that no @@ -114,19 +143,68 @@ class OtlpHttpClient */ bool Shutdown(std::chrono::microseconds timeout = std::chrono::microseconds(0)) noexcept; + /** + * @brief Release the lifetime of specify session. + * + * @param session the session to release + */ + void ReleaseSession(const opentelemetry::ext::http::client::Session &session) noexcept; + private: - // Stores if this HTTP client had its Shutdown() method called - bool is_shutdown_ = false; + struct HttpSessionData + { + std::shared_ptr session; + std::shared_ptr event_handle; + + inline HttpSessionData() = default; + + inline explicit HttpSessionData( + std::shared_ptr &&input_session, + std::shared_ptr &&input_handle) + { + session.swap(input_session); + event_handle.swap(input_handle); + } + + inline explicit HttpSessionData(HttpSessionData &&other) + { + session.swap(other.session); + event_handle.swap(other.event_handle); + } + + inline HttpSessionData &operator=(HttpSessionData &&other) noexcept + { + session.swap(other.session); + event_handle.swap(other.event_handle); + return *this; + } + }; - // The configuration options associated with this HTTP client. - const OtlpHttpClientOptions options_; + /** + * @brief Create a Session object or return a error result + * + * @param message The message to send + */ + nostd::variant createSession( + const google::protobuf::Message &message, + std::function &&result_callback) noexcept; + + /** + * Add http session and hold it's lifetime. + * @param session_data the session to add + */ + void addSession(HttpSessionData &&session_data) noexcept; + + /** + * @brief Real delete all sessions and event handles. + * @note This function is called in the same thread where we create sessions and handles + * + * @return return true if there are more sessions to delete + */ + bool cleanupGCSessions() noexcept; - // Object that stores the HTTP sessions that have been created - std::shared_ptr http_client_; - // Cached parsed URI - std::string http_uri_; - mutable opentelemetry::common::SpinLockMutex lock_; bool isShutdown() const noexcept; + // For testing friend class OtlpHttpExporterTestPeer; friend class OtlpHttpLogExporterTestPeer; @@ -138,6 +216,29 @@ class OtlpHttpClient */ OtlpHttpClient(OtlpHttpClientOptions &&options, std::shared_ptr http_client); + + // Stores if this HTTP client had its Shutdown() method called + bool is_shutdown_; + + // The configuration options associated with this HTTP client. + const OtlpHttpClientOptions options_; + + // Object that stores the HTTP sessions that have been created + std::shared_ptr http_client_; + + // Cached parsed URI + std::string http_uri_; + + // Running sessions and event handles + std::unordered_map + running_sessions_; + // Sessions and event handles that are waiting to be deleted + std::list gc_sessions_; + // Lock for running_sessions_, gc_sessions_ and http_client_ + std::recursive_mutex session_manager_lock_; + // Condition variable and mutex to control the concurrency count of running sessions + std::mutex session_waker_lock_; + std::condition_variable session_waker_; }; } // 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 index 3e6a521194..6dfe8f1f34 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_exporter.h @@ -11,6 +11,7 @@ #include "opentelemetry/exporters/otlp/otlp_environment.h" #include +#include #include #include @@ -50,6 +51,15 @@ struct OtlpHttpExporterOptions // Additional HTTP headers OtlpHeaders http_headers = GetOtlpDefaultHeaders(); + +#ifdef ENABLE_ASYNC_EXPORT + // Concurrent requests + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#otlpgrpc-concurrent-requests + std::size_t max_concurrent_requests = 64; + + // Concurrent requests + std::size_t max_requests_per_connection = 8; +#endif }; /** @@ -82,6 +92,18 @@ class OtlpHttpExporter final : public opentelemetry::sdk::trace::SpanExporter const nostd::span> &spans) noexcept override; +#ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a batch of span recordables asynchronously. + * @param spans a span of unique pointers to span recordables + * @param result_callback callback function accepting ExportResult as argument + */ + virtual void Export( + const nostd::span> &spans, + std::function &&result_callback) noexcept + override; +#endif + /** * Shut down the exporter. * @param timeout an optional timeout, the default timeout of 0 means that no diff --git a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_exporter.h b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_exporter.h index d330e62be4..91e4ccf714 100644 --- a/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_exporter.h +++ b/exporters/otlp/include/opentelemetry/exporters/otlp/otlp_http_log_exporter.h @@ -11,6 +11,7 @@ # include "opentelemetry/exporters/otlp/otlp_environment.h" # include +# include # include # include @@ -50,6 +51,15 @@ struct OtlpHttpLogExporterOptions // Additional HTTP headers OtlpHeaders http_headers = GetOtlpDefaultLogHeaders(); + +# ifdef ENABLE_ASYNC_EXPORT + // Concurrent requests + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#otlpgrpc-concurrent-requests + std::size_t max_concurrent_requests = 64; + + // Concurrent requests + std::size_t max_requests_per_connection = 8; +# endif }; /** @@ -83,6 +93,18 @@ class OtlpHttpLogExporter final : public opentelemetry::sdk::logs::LogExporter const nostd::span> &records) noexcept override; +# ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a vector of log records asynchronously. + * @param records A list of log records. + * @param result_callback callback function accepting ExportResult as argument + */ + virtual void Export( + const nostd::span> &records, + std::function &&result_callback) noexcept + override; +# endif + /** * Shutdown this exporter. * @param timeout The maximum time to wait for the shutdown method to return diff --git a/exporters/otlp/src/otlp_grpc_exporter.cc b/exporters/otlp/src/otlp_grpc_exporter.cc index 32f4a60a52..c7734ad3e2 100644 --- a/exporters/otlp/src/otlp_grpc_exporter.cc +++ b/exporters/otlp/src/otlp_grpc_exporter.cc @@ -143,6 +143,18 @@ sdk::common::ExportResult OtlpGrpcExporter::Export( return sdk::common::ExportResult::kSuccess; } +#ifdef ENABLE_ASYNC_EXPORT +void OtlpGrpcExporter::Export( + const nostd::span> &spans, + std::function &&result_callback) noexcept +{ + OTEL_INTERNAL_LOG_WARN( + "[OTLP TRACE GRPC Exporter] async not supported. Making sync interface call"); + auto status = Export(spans); + result_callback(status); +} +#endif + bool OtlpGrpcExporter::Shutdown(std::chrono::microseconds timeout) noexcept { const std::lock_guard locked(lock_); diff --git a/exporters/otlp/src/otlp_grpc_log_exporter.cc b/exporters/otlp/src/otlp_grpc_log_exporter.cc index 38bfb0a5bb..e506cde0c5 100644 --- a/exporters/otlp/src/otlp_grpc_log_exporter.cc +++ b/exporters/otlp/src/otlp_grpc_log_exporter.cc @@ -161,6 +161,18 @@ opentelemetry::sdk::common::ExportResult OtlpGrpcLogExporter::Export( return sdk::common::ExportResult::kSuccess; } +# ifdef ENABLE_ASYNC_EXPORT +void OtlpGrpcLogExporter::Export( + const nostd::span> &logs, + std::function &&result_callback) noexcept +{ + OTEL_INTERNAL_LOG_WARN( + "[OTLP LOG GRPC Exporter] async not supported. Making sync interface call"); + auto status = Export(logs); + result_callback(status); +} +# endif + bool OtlpGrpcLogExporter::Shutdown(std::chrono::microseconds timeout) noexcept { const std::lock_guard locked(lock_); diff --git a/exporters/otlp/src/otlp_http_client.cc b/exporters/otlp/src/otlp_http_client.cc index e3e1a8c467..b2fc75838d 100644 --- a/exporters/otlp/src/otlp_http_client.cc +++ b/exporters/otlp/src/otlp_http_client.cc @@ -14,7 +14,6 @@ #include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" -#include #include "google/protobuf/message.h" #include "google/protobuf/reflection.h" #include "google/protobuf/stubs/common.h" @@ -36,9 +35,11 @@ LIBPROTOBUF_EXPORT void Base64Escape(StringPiece src, std::string *dest); #include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" +#include "opentelemetry/common/timestamp.h" #include "opentelemetry/sdk/common/global_log_handler.h" #include "opentelemetry/sdk_config.h" +#include #include #include #include @@ -71,7 +72,12 @@ class ResponseHandler : public http_client::EventHandler /** * Creates a response handler, that by default doesn't display to console */ - ResponseHandler(bool console_debug = false) : console_debug_{console_debug} {} + ResponseHandler(std::function &&callback, + bool console_debug = false) + : result_callback_{std::move(callback)}, console_debug_{console_debug} + { + stoping_.store(false); + } /** * Automatically called when the response is received, store the body into a string and notify any @@ -101,20 +107,15 @@ class ResponseHandler : public http_client::EventHandler // Set the response_received_ flag to true and notify any threads waiting on this result response_received_ = true; - stop_waiting_ = true; } - cv_.notify_all(); - } - /**resource - * A method the user calls to block their thread until the response is received. The longest - * duration is the timeout of the request, set by SetTimeoutMs() - */ - bool waitForResponse() - { - std::unique_lock lk(mutex_); - cv_.wait(lk, [this] { return stop_waiting_; }); - return response_received_; + { + bool expected = false; + if (stoping_.compare_exchange_strong(expected, true, std::memory_order_release)) + { + Unbind(sdk::common::ExportResult::kSuccess); + } + } } /** @@ -131,7 +132,8 @@ class ResponseHandler : public http_client::EventHandler void OnEvent(http_client::SessionState state, opentelemetry::nostd::string_view reason) noexcept override { - // need to modify stop_waiting_ under lock before calling notify_all + // need to modify stoping_ under lock before calling notify_all + bool need_stop = false; switch (state) { case http_client::SessionState::CreateFailed: @@ -141,8 +143,7 @@ class ResponseHandler : public http_client::EventHandler case http_client::SessionState::TimedOut: case http_client::SessionState::NetworkError: case http_client::SessionState::Cancelled: { - std::unique_lock lk(mutex_); - stop_waiting_ = true; + need_stop = true; } break; @@ -153,10 +154,16 @@ class ResponseHandler : public http_client::EventHandler // If any failure event occurs, release the condition variable to unblock main thread switch (state) { - case http_client::SessionState::CreateFailed: - OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] Session state: session create failed"); - cv_.notify_all(); - break; + case http_client::SessionState::CreateFailed: { + std::stringstream error_message; + error_message << "[OTLP HTTP Client] Session state: session create failed."; + if (!reason.empty()) + { + error_message.write(reason.data(), reason.size()); + } + OTEL_INTERNAL_LOG_ERROR(error_message.str()); + } + break; case http_client::SessionState::Created: if (console_debug_) @@ -179,10 +186,16 @@ class ResponseHandler : public http_client::EventHandler } break; - case http_client::SessionState::ConnectFailed: - OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] Session state: connection failed"); - cv_.notify_all(); - break; + case http_client::SessionState::ConnectFailed: { + std::stringstream error_message; + error_message << "[OTLP HTTP Client] Session state: connection failed."; + if (!reason.empty()) + { + error_message.write(reason.data(), reason.size()); + } + OTEL_INTERNAL_LOG_ERROR(error_message.str()); + } + break; case http_client::SessionState::Connected: if (console_debug_) @@ -198,10 +211,16 @@ class ResponseHandler : public http_client::EventHandler } break; - case http_client::SessionState::SendFailed: - OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] Session state: request send failed"); - cv_.notify_all(); - break; + case http_client::SessionState::SendFailed: { + std::stringstream error_message; + error_message << "[OTLP HTTP Client] Session state: request send failed."; + if (!reason.empty()) + { + error_message.write(reason.data(), reason.size()); + } + OTEL_INTERNAL_LOG_ERROR(error_message.str()); + } + break; case http_client::SessionState::Response: if (console_debug_) @@ -210,20 +229,38 @@ class ResponseHandler : public http_client::EventHandler } break; - case http_client::SessionState::SSLHandshakeFailed: - OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] Session state: SSL handshake failed"); - cv_.notify_all(); - break; + case http_client::SessionState::SSLHandshakeFailed: { + std::stringstream error_message; + error_message << "[OTLP HTTP Client] Session state: SSL handshake failed."; + if (!reason.empty()) + { + error_message.write(reason.data(), reason.size()); + } + OTEL_INTERNAL_LOG_ERROR(error_message.str()); + } + break; - case http_client::SessionState::TimedOut: - OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] Session state: request time out"); - cv_.notify_all(); - break; + case http_client::SessionState::TimedOut: { + std::stringstream error_message; + error_message << "[OTLP HTTP Client] Session state: request time out."; + if (!reason.empty()) + { + error_message.write(reason.data(), reason.size()); + } + OTEL_INTERNAL_LOG_ERROR(error_message.str()); + } + break; - case http_client::SessionState::NetworkError: - OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] Session state: network error"); - cv_.notify_all(); - break; + case http_client::SessionState::NetworkError: { + std::stringstream error_message; + error_message << "[OTLP HTTP Client] Session state: network error."; + if (!reason.empty()) + { + error_message.write(reason.data(), reason.size()); + } + OTEL_INTERNAL_LOG_ERROR(error_message.str()); + } + break; case http_client::SessionState::ReadError: if (console_debug_) @@ -239,23 +276,68 @@ class ResponseHandler : public http_client::EventHandler } break; - case http_client::SessionState::Cancelled: - OTEL_INTERNAL_LOG_ERROR("[OTLP HTTP Client] Session state: (manually) cancelled\n"); - cv_.notify_all(); - break; + case http_client::SessionState::Cancelled: { + std::stringstream error_message; + error_message << "[OTLP HTTP Client] Session state: (manually) cancelled."; + if (!reason.empty()) + { + error_message.write(reason.data(), reason.size()); + } + OTEL_INTERNAL_LOG_ERROR(error_message.str()); + } + break; default: break; } + + if (need_stop) + { + bool expected = false; + if (stoping_.compare_exchange_strong(expected, true, std::memory_order_release)) + { + Unbind(sdk::common::ExportResult::kFailure); + } + } } + void Unbind(sdk::common::ExportResult result) + { + // ReleaseSession may destroy this object, so we need to move owner and session into stack + // first. + OtlpHttpClient *owner = owner_; + const opentelemetry::ext::http::client::Session *session = session_; + + owner_ = nullptr; + session_ = nullptr; + + if (nullptr != owner && nullptr != session) + { + // Release the session at last + owner->ReleaseSession(*session); + + if (result_callback_) + { + result_callback_(result); + } + } + } + + void Bind(OtlpHttpClient *owner, + const opentelemetry::ext::http::client::Session &session) noexcept + { + session_ = &session; + owner_ = owner; + }; + private: // Define a condition variable and mutex - std::condition_variable cv_; std::mutex mutex_; + OtlpHttpClient *owner_ = nullptr; + const opentelemetry::ext::http::client::Session *session_ = nullptr; // Whether notify has been called - bool stop_waiting_ = false; + std::atomic stoping_; // Whether the response has been received bool response_received_ = false; @@ -263,6 +345,9 @@ class ResponseHandler : public http_client::EventHandler // A string to store the response body std::string body_ = ""; + // Result callback when in async mode + std::function result_callback_; + // Whether to print the results from the callback bool console_debug_ = false; }; @@ -563,31 +648,207 @@ void ConvertListFieldToJson(nlohmann::json &value, } // namespace OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options) - : options_(options), http_client_(http_client::HttpClientFactory::Create()) -{} + : is_shutdown_(false), options_(options), http_client_(http_client::HttpClientFactory::Create()) +{ + http_client_->SetMaxSessionsPerConnection(options_.max_requests_per_connection); +} + +OtlpHttpClient::~OtlpHttpClient() +{ + if (!isShutdown()) + { + Shutdown(); + } + + // Wait for all the sessions to finish + std::unique_lock lock(session_waker_lock_); + while (true) + { + { + std::lock_guard guard{session_manager_lock_}; + if (running_sessions_.empty()) + { + break; + } + } + // When changes of running_sessions_ and notify_one/notify_all happen between predicate + // checking and waiting, we should not wait forever. + session_waker_.wait_for(lock, options_.timeout); + } + + // And then remove all session datas + while (cleanupGCSessions()) + ; +} OtlpHttpClient::OtlpHttpClient(OtlpHttpClientOptions &&options, std::shared_ptr http_client) - : options_(options), http_client_(http_client) -{} + : is_shutdown_(false), options_(options), http_client_(http_client) +{ + http_client_->SetMaxSessionsPerConnection(options_.max_requests_per_connection); +} // ----------------------------- HTTP Client methods ------------------------------ opentelemetry::sdk::common::ExportResult OtlpHttpClient::Export( const google::protobuf::Message &message) noexcept { - // Return failure if this exporter has been shutdown - if (isShutdown()) + opentelemetry::sdk::common::ExportResult result = + opentelemetry::sdk::common::ExportResult::kSuccess; + auto session = + createSession(message, [&result](opentelemetry::sdk::common::ExportResult export_result) { + result = export_result; + return export_result == opentelemetry::sdk::common::ExportResult::kSuccess; + }); + + if (opentelemetry::nostd::holds_alternative(session)) { - const char *error_message = "[OTLP HTTP Client] Export failed, exporter is shutdown"; - if (options_.console_debug) + return opentelemetry::nostd::get(session); + } + + // Wait for the response to be received + if (options_.console_debug) + { + OTEL_INTERNAL_LOG_DEBUG( + "[OTLP HTTP Client] DEBUG: Waiting for response from " + << options_.url << " (timeout = " + << std::chrono::duration_cast(options_.timeout).count() + << " milliseconds)"); + } + + addSession(std::move(opentelemetry::nostd::get(session))); + + // Wait for any session to finish if there are to many sessions + std::unique_lock lock(session_waker_lock_); + bool wait_successful = session_waker_.wait_for(lock, options_.timeout, [this] { + std::lock_guard guard{session_manager_lock_}; + return running_sessions_.empty(); + }); + + cleanupGCSessions(); + + // If an error occurred with the HTTP request + if (!wait_successful) + { + return opentelemetry::sdk::common::ExportResult::kFailure; + } + + return result; +} + +void OtlpHttpClient::Export( + const google::protobuf::Message &message, + std::function &&result_callback) noexcept +{ + auto session = createSession(message, std::move(result_callback)); + if (opentelemetry::nostd::holds_alternative(session)) + { + if (result_callback) { - std::cerr << error_message << std::endl; + result_callback(opentelemetry::nostd::get(session)); } - OTEL_INTERNAL_LOG_ERROR(error_message); + return; + } - return opentelemetry::sdk::common::ExportResult::kFailure; + addSession(std::move(opentelemetry::nostd::get(session))); + + // Wait for the response to be received + if (options_.console_debug) + { + OTEL_INTERNAL_LOG_DEBUG( + "[OTLP HTTP Client] DEBUG: Waiting for response from " + << options_.url << " (timeout = " + << std::chrono::duration_cast(options_.timeout).count() + << " milliseconds)"); + } + + // Wait for any session to finish if there are to many sessions + std::unique_lock lock(session_waker_lock_); + session_waker_.wait_for(lock, options_.timeout, [this] { + std::lock_guard guard{session_manager_lock_}; + return running_sessions_.size() <= options_.max_concurrent_requests; + }); + + cleanupGCSessions(); +} + +bool OtlpHttpClient::Shutdown(std::chrono::microseconds timeout) noexcept +{ + { + std::lock_guard guard{session_manager_lock_}; + is_shutdown_ = true; + + // Shutdown the session manager + http_client_->CancelAllSessions(); + http_client_->FinishAllSessions(); + } + + // ASAN will report chrono: runtime error: signed integer overflow: A + B cannot be represented + // in type 'long int' here. So we reset timeout to meet signed long int limit here. + timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout( + timeout, std::chrono::microseconds::zero()); + + // Wait for all the sessions to finish + std::unique_lock lock(session_waker_lock_); + if (timeout <= std::chrono::microseconds::zero()) + { + while (true) + { + { + std::lock_guard guard{session_manager_lock_}; + if (running_sessions_.empty()) + { + break; + } + } + // When changes of running_sessions_ and notify_one/notify_all happen between predicate + // checking and waiting, we should not wait forever. + session_waker_.wait_for(lock, options_.timeout); + } + } + else + { + session_waker_.wait_for(lock, timeout, [this] { + std::lock_guard guard{session_manager_lock_}; + return running_sessions_.empty(); + }); } + while (cleanupGCSessions()) + ; + return true; +} + +void OtlpHttpClient::ReleaseSession( + const opentelemetry::ext::http::client::Session &session) noexcept +{ + bool has_session = false; + + { + std::lock_guard guard{session_manager_lock_}; + + auto session_iter = running_sessions_.find(&session); + if (session_iter != running_sessions_.end()) + { + // Move session and handle into gc list, and they will be destroyed later + gc_sessions_.emplace_back(std::move(session_iter->second)); + running_sessions_.erase(session_iter); + + has_session = true; + } + } + + if (has_session) + { + session_waker_.notify_all(); + } +} + +opentelemetry::nostd::variant +OtlpHttpClient::createSession( + const google::protobuf::Message &message, + std::function &&result_callback) noexcept +{ // Parse uri and store it to cache if (http_uri_.empty()) { @@ -655,6 +916,20 @@ opentelemetry::sdk::common::ExportResult OtlpHttpClient::Export( } // Send the request + std::lock_guard guard{session_manager_lock_}; + // Return failure if this exporter has been shutdown + if (isShutdown()) + { + const char *error_message = "[OTLP HTTP Client] Export failed, exporter is shutdown"; + if (options_.console_debug) + { + std::cerr << error_message << std::endl; + } + OTEL_INTERNAL_LOG_ERROR(error_message); + + return opentelemetry::sdk::common::ExportResult::kFailure; + } + auto session = http_client_->CreateSession(options_.url); auto request = session->CreateRequest(); @@ -669,50 +944,51 @@ opentelemetry::sdk::common::ExportResult OtlpHttpClient::Export( request->ReplaceHeader("Content-Type", content_type); // Send the request - std::unique_ptr handler(new ResponseHandler(options_.console_debug)); - session->SendRequest(*handler); + return HttpSessionData{ + std::move(session), + std::shared_ptr{ + new ResponseHandler(std::move(result_callback), options_.console_debug)}}; +} - // Wait for the response to be received - if (options_.console_debug) +void OtlpHttpClient::addSession(HttpSessionData &&session_data) noexcept +{ + if (!session_data.session || !session_data.event_handle) { - OTEL_INTERNAL_LOG_DEBUG( - "[OTLP HTTP Client] DEBUG: Waiting for response from " - << options_.url << " (timeout = " - << std::chrono::duration_cast(options_.timeout).count() - << " milliseconds)"); + return; } - bool write_successful = handler->waitForResponse(); - // End the session - session->FinishSession(); + opentelemetry::ext::http::client::Session *key = session_data.session.get(); + ResponseHandler *handle = static_cast(session_data.event_handle.get()); - // If an error occurred with the HTTP request - if (!write_successful) - { - // TODO: retry logic - return opentelemetry::sdk::common::ExportResult::kFailure; - } + handle->Bind(this, *key); + + HttpSessionData &store_session_data = running_sessions_[key]; + store_session_data = std::move(session_data); - return opentelemetry::sdk::common::ExportResult::kSuccess; + // Send request after the session is added + key->SendRequest(store_session_data.event_handle); } -bool OtlpHttpClient::Shutdown(std::chrono::microseconds) noexcept +bool OtlpHttpClient::cleanupGCSessions() noexcept { + std::lock_guard guard{session_manager_lock_}; + std::list gc_sessions; + gc_sessions_.swap(gc_sessions); + + for (auto &session_data : gc_sessions) { - const std::lock_guard locked(lock_); - is_shutdown_ = true; + // FinishSession must be called with same thread and before the session is destroyed + if (session_data.session) + { + session_data.session->FinishSession(); + } } - // Shutdown the session manager - http_client_->CancelAllSessions(); - http_client_->FinishAllSessions(); - - return true; + return !gc_sessions_.empty(); } bool OtlpHttpClient::isShutdown() const noexcept { - const std::lock_guard locked(lock_); return is_shutdown_; } diff --git a/exporters/otlp/src/otlp_http_exporter.cc b/exporters/otlp/src/otlp_http_exporter.cc index 92155dd00d..f5b50162f3 100644 --- a/exporters/otlp/src/otlp_http_exporter.cc +++ b/exporters/otlp/src/otlp_http_exporter.cc @@ -11,6 +11,8 @@ #include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" +#include "opentelemetry/sdk/common/global_log_handler.h" + namespace nostd = opentelemetry::nostd; OPENTELEMETRY_BEGIN_NAMESPACE @@ -29,7 +31,13 @@ OtlpHttpExporter::OtlpHttpExporter(const OtlpHttpExporterOptions &options) options.use_json_name, options.console_debug, options.timeout, - options.http_headers))) + options.http_headers +#ifdef ENABLE_ASYNC_EXPORT + , + options.max_concurrent_requests, + options.max_requests_per_connection +#endif + ))) {} OtlpHttpExporter::OtlpHttpExporter(std::unique_ptr http_client) @@ -56,6 +64,22 @@ opentelemetry::sdk::common::ExportResult OtlpHttpExporter::Export( return http_client_->Export(service_request); } +#ifdef ENABLE_ASYNC_EXPORT +void OtlpHttpExporter::Export( + const nostd::span> &spans, + std::function &&result_callback) noexcept +{ + if (spans.empty()) + { + return; + } + + proto::collector::trace::v1::ExportTraceServiceRequest service_request; + OtlpRecordableUtils::PopulateRequest(spans, &service_request); + http_client_->Export(service_request, std::move(result_callback)); +} +#endif + bool OtlpHttpExporter::Shutdown(std::chrono::microseconds timeout) noexcept { return http_client_->Shutdown(timeout); diff --git a/exporters/otlp/src/otlp_http_log_exporter.cc b/exporters/otlp/src/otlp_http_log_exporter.cc index 436c77beaa..d2430bf924 100644 --- a/exporters/otlp/src/otlp_http_log_exporter.cc +++ b/exporters/otlp/src/otlp_http_log_exporter.cc @@ -13,6 +13,8 @@ # include "opentelemetry/exporters/otlp/protobuf_include_suffix.h" +# include "opentelemetry/sdk/common/global_log_handler.h" + namespace nostd = opentelemetry::nostd; OPENTELEMETRY_BEGIN_NAMESPACE @@ -31,7 +33,13 @@ OtlpHttpLogExporter::OtlpHttpLogExporter(const OtlpHttpLogExporterOptions &optio options.use_json_name, options.console_debug, options.timeout, - options.http_headers))) + options.http_headers +# ifdef ENABLE_ASYNC_EXPORT + , + options.max_concurrent_requests, + options.max_requests_per_connection +# endif + ))) {} OtlpHttpLogExporter::OtlpHttpLogExporter(std::unique_ptr http_client) @@ -57,6 +65,21 @@ opentelemetry::sdk::common::ExportResult OtlpHttpLogExporter::Export( return http_client_->Export(service_request); } +# ifdef ENABLE_ASYNC_EXPORT +void OtlpHttpLogExporter::Export( + const nostd::span> &logs, + std::function &&result_callback) noexcept +{ + if (logs.empty()) + { + return; + } + proto::collector::logs::v1::ExportLogsServiceRequest service_request; + OtlpRecordableUtils::PopulateRequest(logs, &service_request); + http_client_->Export(service_request, std::move(result_callback)); +} +# endif + bool OtlpHttpLogExporter::Shutdown(std::chrono::microseconds timeout) noexcept { return http_client_->Shutdown(timeout); diff --git a/exporters/otlp/test/otlp_http_exporter_test.cc b/exporters/otlp/test/otlp_http_exporter_test.cc index ef0b5a509e..c25c0111b2 100644 --- a/exporters/otlp/test/otlp_http_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_exporter_test.cc @@ -3,6 +3,9 @@ #ifndef HAVE_CPP_STDLIB +# include +# include + # include "opentelemetry/exporters/otlp/otlp_http_exporter.h" # include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" @@ -14,6 +17,7 @@ # include "opentelemetry/ext/http/client/http_client_factory.h" # include "opentelemetry/ext/http/client/nosend/http_client_nosend.h" # include "opentelemetry/ext/http/server/http_server.h" +# include "opentelemetry/sdk/trace/async_batch_span_processor.h" # include "opentelemetry/sdk/trace/batch_span_processor.h" # include "opentelemetry/sdk/trace/tracer_provider.h" # include "opentelemetry/trace/provider.h" @@ -81,166 +85,375 @@ class OtlpHttpExporterTestPeer : public ::testing::Test auto http_client = http_client::HttpClientFactory::CreateNoSend(); return {new OtlpHttpClient(MakeOtlpHttpClientOptions(content_type), http_client), http_client}; } + + void ExportJsonIntegrationTest() + { + auto mock_otlp_client = + OtlpHttpExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + 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 = 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 * trace_api::TraceId::kSize] = {0}; + auto tracer = provider->GetTracer("test"); + auto parent_span = tracer->StartSpan("Test parent span"); + + trace_api::StartSpanOptions child_span_opts = {}; + child_span_opts.parent = parent_span->GetContext(); + + auto child_span = tracer->StartSpan("Test child span", child_span_opts); + + nostd::get(child_span_opts.parent) + .trace_id() + .ToLowerBase16(MakeSpan(trace_id_hex)); + report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id]( + std::shared_ptr callback) { + auto check_json = + nlohmann::json::parse(mock_session->GetRequest()->body_, nullptr, false); + 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); + + auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); + ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); + if (custom_header != mock_session->GetRequest()->headers_.end()) + { + EXPECT_EQ("Custom-Header-Value", custom_header->second); + } + + // let the otlp_http_client to continue + http_client::nosend::Response response; + response.Finish(*callback.get()); + }); + + child_span->End(); + parent_span->End(); + + static_cast(provider.get())->ForceFlush(); + } + +# ifdef ENABLE_ASYNC_EXPORT + void ExportJsonIntegrationTestAsync() + { + auto mock_otlp_client = + OtlpHttpExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + 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 = resource::Resource::Create(resource_attributes); + + auto processor_opts = sdk::trace::AsyncBatchSpanProcessorOptions(); + 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::AsyncBatchSpanProcessor(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 * trace_api::TraceId::kSize] = {0}; + auto tracer = provider->GetTracer("test"); + auto parent_span = tracer->StartSpan("Test parent span"); + + trace_api::StartSpanOptions child_span_opts = {}; + child_span_opts.parent = parent_span->GetContext(); + + auto child_span = tracer->StartSpan("Test child span", child_span_opts); + + nostd::get(child_span_opts.parent) + .trace_id() + .ToLowerBase16(MakeSpan(trace_id_hex)); + report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id]( + std::shared_ptr callback) { + auto check_json = + nlohmann::json::parse(mock_session->GetRequest()->body_, nullptr, false); + 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); + + auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); + ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); + if (custom_header != mock_session->GetRequest()->headers_.end()) + { + EXPECT_EQ("Custom-Header-Value", custom_header->second); + } + + // let the otlp_http_client to continue + std::thread async_finish{[callback]() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + http_client::nosend::Response response; + response.Finish(*callback.get()); + }}; + async_finish.detach(); + }); + + child_span->End(); + parent_span->End(); + + static_cast(provider.get())->ForceFlush(); + } +# endif + + void ExportBinaryIntegrationTest() + { + auto mock_otlp_client = + OtlpHttpExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kBinary); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + 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 = 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[trace_api::TraceId::kSize] = {0}; + auto tracer = provider->GetTracer("test"); + auto parent_span = tracer->StartSpan("Test parent span"); + + trace_api::StartSpanOptions child_span_opts = {}; + child_span_opts.parent = parent_span->GetContext(); + + auto child_span = tracer->StartSpan("Test child span", child_span_opts); + nostd::get(child_span_opts.parent) + .trace_id() + .CopyBytesTo(MakeSpan(trace_id_binary)); + report_trace_id.assign(reinterpret_cast(trace_id_binary), sizeof(trace_id_binary)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id]( + std::shared_ptr callback) { + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request_body; + request_body.ParseFromArray(&mock_session->GetRequest()->body_[0], + static_cast(mock_session->GetRequest()->body_.size())); + auto received_trace_id = + request_body.resource_spans(0).instrumentation_library_spans(0).spans(0).trace_id(); + EXPECT_EQ(received_trace_id, report_trace_id); + + auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); + ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); + if (custom_header != mock_session->GetRequest()->headers_.end()) + { + EXPECT_EQ("Custom-Header-Value", custom_header->second); + } + + http_client::nosend::Response response; + response.Finish(*callback.get()); + }); + + child_span->End(); + parent_span->End(); + + static_cast(provider.get())->ForceFlush(); + } + +# ifdef ENABLE_ASYNC_EXPORT + void ExportBinaryIntegrationTestAsync() + { + auto mock_otlp_client = + OtlpHttpExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kBinary); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + 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 = resource::Resource::Create(resource_attributes); + + auto processor_opts = sdk::trace::AsyncBatchSpanProcessorOptions(); + 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::AsyncBatchSpanProcessor(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[trace_api::TraceId::kSize] = {0}; + auto tracer = provider->GetTracer("test"); + auto parent_span = tracer->StartSpan("Test parent span"); + + trace_api::StartSpanOptions child_span_opts = {}; + child_span_opts.parent = parent_span->GetContext(); + + auto child_span = tracer->StartSpan("Test child span", child_span_opts); + nostd::get(child_span_opts.parent) + .trace_id() + .CopyBytesTo(MakeSpan(trace_id_binary)); + report_trace_id.assign(reinterpret_cast(trace_id_binary), sizeof(trace_id_binary)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id]( + std::shared_ptr callback) { + opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request_body; + request_body.ParseFromArray(&mock_session->GetRequest()->body_[0], + static_cast(mock_session->GetRequest()->body_.size())); + auto received_trace_id = + request_body.resource_spans(0).instrumentation_library_spans(0).spans(0).trace_id(); + EXPECT_EQ(received_trace_id, report_trace_id); + + auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); + ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); + if (custom_header != mock_session->GetRequest()->headers_.end()) + { + EXPECT_EQ("Custom-Header-Value", custom_header->second); + } + + // let the otlp_http_client to continue + std::thread async_finish{[callback]() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + http_client::nosend::Response response; + response.Finish(*callback.get()); + }}; + async_finish.detach(); + }); + + child_span->End(); + parent_span->End(); + + static_cast(provider.get())->ForceFlush(); + } +# endif }; // Create spans, let processor call Export() -TEST_F(OtlpHttpExporterTestPeer, ExportJsonIntegrationTest) +TEST_F(OtlpHttpExporterTestPeer, ExportJsonIntegrationTestSync) { - auto mock_otlp_client = - OtlpHttpExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); - auto mock_otlp_http_client = mock_otlp_client.first; - auto client = mock_otlp_client.second; - auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); - - 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 = 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 * trace_api::TraceId::kSize] = {0}; - auto tracer = provider->GetTracer("test"); - auto parent_span = tracer->StartSpan("Test parent span"); - - trace_api::StartSpanOptions child_span_opts = {}; - child_span_opts.parent = parent_span->GetContext(); - - auto child_span = tracer->StartSpan("Test child span", child_span_opts); - - nostd::get(child_span_opts.parent) - .trace_id() - .ToLowerBase16(MakeSpan(trace_id_hex)); - report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); - - auto no_send_client = std::static_pointer_cast(client); - auto mock_session = - std::static_pointer_cast(no_send_client->session_); - EXPECT_CALL(*mock_session, SendRequest) - .WillOnce([&mock_session, - report_trace_id](opentelemetry::ext::http::client::EventHandler &callback) { - auto check_json = nlohmann::json::parse(mock_session->GetRequest()->body_, nullptr, false); - 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); - - auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); - ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); - if (custom_header != mock_session->GetRequest()->headers_.end()) - { - EXPECT_EQ("Custom-Header-Value", custom_header->second); - } - // let the otlp_http_client to continue - http_client::nosend::Response response; - callback.OnResponse(response); - }); - - child_span->End(); - parent_span->End(); + ExportJsonIntegrationTest(); } +# ifdef ENABLE_ASYNC_EXPORT +TEST_F(OtlpHttpExporterTestPeer, ExportJsonIntegrationTestAsync) +{ + ExportJsonIntegrationTestAsync(); +} +# endif + // Create spans, let processor call Export() -TEST_F(OtlpHttpExporterTestPeer, ExportBinaryIntegrationTest) +TEST_F(OtlpHttpExporterTestPeer, ExportBinaryIntegrationTestSync) { - auto mock_otlp_client = - OtlpHttpExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kBinary); - auto mock_otlp_http_client = mock_otlp_client.first; - auto client = mock_otlp_client.second; - auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); - - 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 = 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[trace_api::TraceId::kSize] = {0}; - auto tracer = provider->GetTracer("test"); - auto parent_span = tracer->StartSpan("Test parent span"); - - trace_api::StartSpanOptions child_span_opts = {}; - child_span_opts.parent = parent_span->GetContext(); - - auto child_span = tracer->StartSpan("Test child span", child_span_opts); - nostd::get(child_span_opts.parent) - .trace_id() - .CopyBytesTo(MakeSpan(trace_id_binary)); - report_trace_id.assign(reinterpret_cast(trace_id_binary), sizeof(trace_id_binary)); - - auto no_send_client = std::static_pointer_cast(client); - auto mock_session = - std::static_pointer_cast(no_send_client->session_); - EXPECT_CALL(*mock_session, SendRequest) - .WillOnce([&mock_session, - report_trace_id](opentelemetry::ext::http::client::EventHandler &callback) { - opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest request_body; - request_body.ParseFromArray(&mock_session->GetRequest()->body_[0], - static_cast(mock_session->GetRequest()->body_.size())); - auto received_trace_id = - request_body.resource_spans(0).instrumentation_library_spans(0).spans(0).trace_id(); - EXPECT_EQ(received_trace_id, report_trace_id); - - auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); - ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); - if (custom_header != mock_session->GetRequest()->headers_.end()) - { - EXPECT_EQ("Custom-Header-Value", custom_header->second); - } - // let the otlp_http_client to continue - http_client::nosend::Response response; - callback.OnResponse(response); - }); - - child_span->End(); - parent_span->End(); + ExportBinaryIntegrationTest(); } +# ifdef ENABLE_ASYNC_EXPORT +TEST_F(OtlpHttpExporterTestPeer, ExportBinaryIntegrationTestAsync) +{ + ExportBinaryIntegrationTestAsync(); +} +# endif + // Test exporter configuration options TEST_F(OtlpHttpExporterTestPeer, ConfigTest) { diff --git a/exporters/otlp/test/otlp_http_log_exporter_test.cc b/exporters/otlp/test/otlp_http_log_exporter_test.cc index 7e2c0808e2..77106f3e47 100644 --- a/exporters/otlp/test/otlp_http_log_exporter_test.cc +++ b/exporters/otlp/test/otlp_http_log_exporter_test.cc @@ -4,6 +4,9 @@ #ifndef HAVE_CPP_STDLIB # ifdef ENABLE_LOGS_PREVIEW +# include +# include + # include "opentelemetry/exporters/otlp/otlp_http_log_exporter.h" # include "opentelemetry/exporters/otlp/protobuf_include_prefix.h" @@ -17,6 +20,7 @@ # include "opentelemetry/ext/http/client/nosend/http_client_nosend.h" # include "opentelemetry/ext/http/server/http_server.h" # include "opentelemetry/logs/provider.h" +# include "opentelemetry/sdk/logs/async_batch_log_processor.h" # include "opentelemetry/sdk/logs/batch_log_processor.h" # include "opentelemetry/sdk/logs/exporter.h" # include "opentelemetry/sdk/logs/log_record.h" @@ -82,180 +86,423 @@ class OtlpHttpLogExporterTestPeer : public ::testing::Test auto http_client = http_client::HttpClientFactory::CreateNoSend(); return {new OtlpHttpClient(MakeOtlpHttpClientOptions(content_type), http_client), http_client}; } + + void ExportJsonIntegrationTest() + { + auto mock_otlp_client = + OtlpHttpLogExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + bool attribute_storage_bool_value[] = {true, false, true}; + int32_t attribute_storage_int32_value[] = {1, 2}; + uint32_t attribute_storage_uint32_value[] = {3, 4}; + int64_t attribute_storage_int64_value[] = {5, 6}; + uint64_t attribute_storage_uint64_value[] = {7, 8}; + double attribute_storage_double_value[] = {3.2, 3.3}; + std::string attribute_storage_string_value[] = {"vector", "string"}; + + auto provider = nostd::shared_ptr(new sdk::logs::LoggerProvider()); + + provider->AddProcessor( + std::unique_ptr(new sdk::logs::BatchLogProcessor( + std::move(exporter), 5, std::chrono::milliseconds(256), 5))); + + std::string report_trace_id; + std::string report_span_id; + uint8_t trace_id_bin[opentelemetry::trace::TraceId::kSize] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + char trace_id_hex[2 * opentelemetry::trace::TraceId::kSize] = {0}; + opentelemetry::trace::TraceId trace_id{trace_id_bin}; + uint8_t span_id_bin[opentelemetry::trace::SpanId::kSize] = {'7', '6', '5', '4', + '3', '2', '1', '0'}; + char span_id_hex[2 * opentelemetry::trace::SpanId::kSize] = {0}; + opentelemetry::trace::SpanId span_id{span_id_bin}; + + const std::string schema_url{"https://opentelemetry.io/schemas/1.2.0"}; + auto logger = provider->GetLogger("test", "", "opentelelemtry_library", "", schema_url); + + trace_id.ToLowerBase16(MakeSpan(trace_id_hex)); + report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); + + span_id.ToLowerBase16(MakeSpan(span_id_hex)); + report_span_id.assign(span_id_hex, sizeof(span_id_hex)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id, report_span_id]( + std::shared_ptr callback) { + auto check_json = + nlohmann::json::parse(mock_session->GetRequest()->body_, nullptr, false); + auto resource_logs = *check_json["resource_logs"].begin(); + auto scope_logs = *resource_logs["scope_logs"].begin(); + auto log = *scope_logs["log_records"].begin(); + auto received_trace_id = log["trace_id"].get(); + auto received_span_id = log["span_id"].get(); + EXPECT_EQ(received_trace_id, report_trace_id); + EXPECT_EQ(received_span_id, report_span_id); + EXPECT_EQ("Log message", log["body"]["string_value"].get()); + EXPECT_LE(15, log["attributes"].size()); + auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); + ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); + if (custom_header != mock_session->GetRequest()->headers_.end()) + { + EXPECT_EQ("Custom-Header-Value", custom_header->second); + } + + http_client::nosend::Response response; + response.Finish(*callback.get()); + }); + + logger->Log(opentelemetry::logs::Severity::kInfo, "Log message", + {{"service.name", "unit_test_service"}, + {"tenant.id", "test_user"}, + {"bool_value", true}, + {"int32_value", static_cast(1)}, + {"uint32_value", static_cast(2)}, + {"int64_value", static_cast(0x1100000000LL)}, + {"uint64_value", static_cast(0x1200000000ULL)}, + {"double_value", static_cast(3.1)}, + {"vec_bool_value", attribute_storage_bool_value}, + {"vec_int32_value", attribute_storage_int32_value}, + {"vec_uint32_value", attribute_storage_uint32_value}, + {"vec_int64_value", attribute_storage_int64_value}, + {"vec_uint64_value", attribute_storage_uint64_value}, + {"vec_double_value", attribute_storage_double_value}, + {"vec_string_value", attribute_storage_string_value}}, + trace_id, span_id, + opentelemetry::trace::TraceFlags{opentelemetry::trace::TraceFlags::kIsSampled}, + std::chrono::system_clock::now()); + + provider->ForceFlush(); + } + +# ifdef ENABLE_ASYNC_EXPORT + void ExportJsonIntegrationTestAsync() + { + auto mock_otlp_client = + OtlpHttpLogExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + bool attribute_storage_bool_value[] = {true, false, true}; + int32_t attribute_storage_int32_value[] = {1, 2}; + uint32_t attribute_storage_uint32_value[] = {3, 4}; + int64_t attribute_storage_int64_value[] = {5, 6}; + uint64_t attribute_storage_uint64_value[] = {7, 8}; + double attribute_storage_double_value[] = {3.2, 3.3}; + std::string attribute_storage_string_value[] = {"vector", "string"}; + + auto provider = nostd::shared_ptr(new sdk::logs::LoggerProvider()); + sdk::logs::AsyncBatchLogProcessorOptions options; + options.max_queue_size = 5; + options.schedule_delay_millis = std::chrono::milliseconds(256); + options.max_export_batch_size = 5; + + provider->AddProcessor(std::unique_ptr( + new sdk::logs::AsyncBatchLogProcessor(std::move(exporter), options))); + + std::string report_trace_id; + std::string report_span_id; + uint8_t trace_id_bin[opentelemetry::trace::TraceId::kSize] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + char trace_id_hex[2 * opentelemetry::trace::TraceId::kSize] = {0}; + opentelemetry::trace::TraceId trace_id{trace_id_bin}; + uint8_t span_id_bin[opentelemetry::trace::SpanId::kSize] = {'7', '6', '5', '4', + '3', '2', '1', '0'}; + char span_id_hex[2 * opentelemetry::trace::SpanId::kSize] = {0}; + opentelemetry::trace::SpanId span_id{span_id_bin}; + + const std::string schema_url{"https://opentelemetry.io/schemas/1.2.0"}; + auto logger = provider->GetLogger("test", "", "opentelelemtry_library", "", schema_url); + + trace_id.ToLowerBase16(MakeSpan(trace_id_hex)); + report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); + + span_id.ToLowerBase16(MakeSpan(span_id_hex)); + report_span_id.assign(span_id_hex, sizeof(span_id_hex)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id, report_span_id]( + std::shared_ptr callback) { + auto check_json = + nlohmann::json::parse(mock_session->GetRequest()->body_, nullptr, false); + auto resource_logs = *check_json["resource_logs"].begin(); + auto scope_logs = *resource_logs["scope_logs"].begin(); + auto log = *scope_logs["log_records"].begin(); + auto received_trace_id = log["trace_id"].get(); + auto received_span_id = log["span_id"].get(); + EXPECT_EQ(received_trace_id, report_trace_id); + EXPECT_EQ(received_span_id, report_span_id); + EXPECT_EQ("Log message", log["body"]["string_value"].get()); + EXPECT_LE(15, log["attributes"].size()); + auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); + ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); + if (custom_header != mock_session->GetRequest()->headers_.end()) + { + EXPECT_EQ("Custom-Header-Value", custom_header->second); + } + + // let the otlp_http_client to continue + std::thread async_finish{[callback]() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + http_client::nosend::Response response; + response.Finish(*callback.get()); + }}; + async_finish.detach(); + }); + + logger->Log(opentelemetry::logs::Severity::kInfo, "Log message", + {{"service.name", "unit_test_service"}, + {"tenant.id", "test_user"}, + {"bool_value", true}, + {"int32_value", static_cast(1)}, + {"uint32_value", static_cast(2)}, + {"int64_value", static_cast(0x1100000000LL)}, + {"uint64_value", static_cast(0x1200000000ULL)}, + {"double_value", static_cast(3.1)}, + {"vec_bool_value", attribute_storage_bool_value}, + {"vec_int32_value", attribute_storage_int32_value}, + {"vec_uint32_value", attribute_storage_uint32_value}, + {"vec_int64_value", attribute_storage_int64_value}, + {"vec_uint64_value", attribute_storage_uint64_value}, + {"vec_double_value", attribute_storage_double_value}, + {"vec_string_value", attribute_storage_string_value}}, + trace_id, span_id, + opentelemetry::trace::TraceFlags{opentelemetry::trace::TraceFlags::kIsSampled}, + std::chrono::system_clock::now()); + + provider->ForceFlush(); + } +# endif + + void ExportBinaryIntegrationTest() + { + auto mock_otlp_client = + OtlpHttpLogExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kBinary); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + bool attribute_storage_bool_value[] = {true, false, true}; + int32_t attribute_storage_int32_value[] = {1, 2}; + uint32_t attribute_storage_uint32_value[] = {3, 4}; + int64_t attribute_storage_int64_value[] = {5, 6}; + uint64_t attribute_storage_uint64_value[] = {7, 8}; + double attribute_storage_double_value[] = {3.2, 3.3}; + std::string attribute_storage_string_value[] = {"vector", "string"}; + + auto provider = nostd::shared_ptr(new sdk::logs::LoggerProvider()); + sdk::logs::BatchLogProcessorOptions processor_options; + processor_options.max_export_batch_size = 5; + processor_options.max_queue_size = 5; + processor_options.schedule_delay_millis = std::chrono::milliseconds(256); + provider->AddProcessor(std::unique_ptr( + new sdk::logs::BatchLogProcessor(std::move(exporter), processor_options))); + + std::string report_trace_id; + std::string report_span_id; + uint8_t trace_id_bin[opentelemetry::trace::TraceId::kSize] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + opentelemetry::trace::TraceId trace_id{trace_id_bin}; + uint8_t span_id_bin[opentelemetry::trace::SpanId::kSize] = {'7', '6', '5', '4', + '3', '2', '1', '0'}; + opentelemetry::trace::SpanId span_id{span_id_bin}; + + const std::string schema_url{"https://opentelemetry.io/schemas/1.2.0"}; + auto logger = provider->GetLogger("test", "", "opentelelemtry_library", "", schema_url); + + report_trace_id.assign(reinterpret_cast(trace_id_bin), sizeof(trace_id_bin)); + report_span_id.assign(reinterpret_cast(span_id_bin), sizeof(span_id_bin)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id, report_span_id]( + std::shared_ptr callback) { + opentelemetry::proto::collector::logs::v1::ExportLogsServiceRequest request_body; + request_body.ParseFromArray(&mock_session->GetRequest()->body_[0], + static_cast(mock_session->GetRequest()->body_.size())); + auto received_log = request_body.resource_logs(0).scope_logs(0).log_records(0); + EXPECT_EQ(received_log.trace_id(), report_trace_id); + EXPECT_EQ(received_log.span_id(), report_span_id); + EXPECT_EQ("Log message", received_log.body().string_value()); + EXPECT_LE(15, received_log.attributes_size()); + bool check_service_name = false; + for (auto &attribute : received_log.attributes()) + { + if ("service.name" == attribute.key()) + { + check_service_name = true; + EXPECT_EQ("unit_test_service", attribute.value().string_value()); + } + } + ASSERT_TRUE(check_service_name); + + // let the otlp_http_client to continue + + http_client::nosend::Response response; + response.Finish(*callback.get()); + }); + + logger->Log(opentelemetry::logs::Severity::kInfo, "Log message", + {{"service.name", "unit_test_service"}, + {"tenant.id", "test_user"}, + {"bool_value", true}, + {"int32_value", static_cast(1)}, + {"uint32_value", static_cast(2)}, + {"int64_value", static_cast(0x1100000000LL)}, + {"uint64_value", static_cast(0x1200000000ULL)}, + {"double_value", static_cast(3.1)}, + {"vec_bool_value", attribute_storage_bool_value}, + {"vec_int32_value", attribute_storage_int32_value}, + {"vec_uint32_value", attribute_storage_uint32_value}, + {"vec_int64_value", attribute_storage_int64_value}, + {"vec_uint64_value", attribute_storage_uint64_value}, + {"vec_double_value", attribute_storage_double_value}, + {"vec_string_value", attribute_storage_string_value}}, + trace_id, span_id, + opentelemetry::trace::TraceFlags{opentelemetry::trace::TraceFlags::kIsSampled}, + std::chrono::system_clock::now()); + + provider->ForceFlush(); + } + +# ifdef ENABLE_ASYNC_EXPORT + void ExportBinaryIntegrationTestAsync() + { + auto mock_otlp_client = + OtlpHttpLogExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kBinary); + auto mock_otlp_http_client = mock_otlp_client.first; + auto client = mock_otlp_client.second; + auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); + + bool attribute_storage_bool_value[] = {true, false, true}; + int32_t attribute_storage_int32_value[] = {1, 2}; + uint32_t attribute_storage_uint32_value[] = {3, 4}; + int64_t attribute_storage_int64_value[] = {5, 6}; + uint64_t attribute_storage_uint64_value[] = {7, 8}; + double attribute_storage_double_value[] = {3.2, 3.3}; + std::string attribute_storage_string_value[] = {"vector", "string"}; + + auto provider = nostd::shared_ptr(new sdk::logs::LoggerProvider()); + + sdk::logs::AsyncBatchLogProcessorOptions processor_options; + processor_options.max_export_batch_size = 5; + processor_options.max_queue_size = 5; + processor_options.schedule_delay_millis = std::chrono::milliseconds(256); + provider->AddProcessor(std::unique_ptr( + new sdk::logs::AsyncBatchLogProcessor(std::move(exporter), processor_options))); + + std::string report_trace_id; + std::string report_span_id; + uint8_t trace_id_bin[opentelemetry::trace::TraceId::kSize] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + opentelemetry::trace::TraceId trace_id{trace_id_bin}; + uint8_t span_id_bin[opentelemetry::trace::SpanId::kSize] = {'7', '6', '5', '4', + '3', '2', '1', '0'}; + opentelemetry::trace::SpanId span_id{span_id_bin}; + + const std::string schema_url{"https://opentelemetry.io/schemas/1.2.0"}; + auto logger = provider->GetLogger("test", "", "opentelelemtry_library", "", schema_url); + + report_trace_id.assign(reinterpret_cast(trace_id_bin), sizeof(trace_id_bin)); + report_span_id.assign(reinterpret_cast(span_id_bin), sizeof(span_id_bin)); + + auto no_send_client = std::static_pointer_cast(client); + auto mock_session = + std::static_pointer_cast(no_send_client->session_); + EXPECT_CALL(*mock_session, SendRequest) + .WillOnce([&mock_session, report_trace_id, report_span_id]( + std::shared_ptr callback) { + opentelemetry::proto::collector::logs::v1::ExportLogsServiceRequest request_body; + request_body.ParseFromArray(&mock_session->GetRequest()->body_[0], + static_cast(mock_session->GetRequest()->body_.size())); + auto received_log = request_body.resource_logs(0).scope_logs(0).log_records(0); + EXPECT_EQ(received_log.trace_id(), report_trace_id); + EXPECT_EQ(received_log.span_id(), report_span_id); + EXPECT_EQ("Log message", received_log.body().string_value()); + EXPECT_LE(15, received_log.attributes_size()); + bool check_service_name = false; + for (auto &attribute : received_log.attributes()) + { + if ("service.name" == attribute.key()) + { + check_service_name = true; + EXPECT_EQ("unit_test_service", attribute.value().string_value()); + } + } + ASSERT_TRUE(check_service_name); + + // let the otlp_http_client to continue + + std::thread async_finish{[callback]() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + http_client::nosend::Response response; + response.Finish(*callback.get()); + }}; + async_finish.detach(); + }); + + logger->Log(opentelemetry::logs::Severity::kInfo, "Log message", + {{"service.name", "unit_test_service"}, + {"tenant.id", "test_user"}, + {"bool_value", true}, + {"int32_value", static_cast(1)}, + {"uint32_value", static_cast(2)}, + {"int64_value", static_cast(0x1100000000LL)}, + {"uint64_value", static_cast(0x1200000000ULL)}, + {"double_value", static_cast(3.1)}, + {"vec_bool_value", attribute_storage_bool_value}, + {"vec_int32_value", attribute_storage_int32_value}, + {"vec_uint32_value", attribute_storage_uint32_value}, + {"vec_int64_value", attribute_storage_int64_value}, + {"vec_uint64_value", attribute_storage_uint64_value}, + {"vec_double_value", attribute_storage_double_value}, + {"vec_string_value", attribute_storage_string_value}}, + trace_id, span_id, + opentelemetry::trace::TraceFlags{opentelemetry::trace::TraceFlags::kIsSampled}, + std::chrono::system_clock::now()); + + provider->ForceFlush(); + } +# endif }; // Create log records, let processor call Export() -TEST_F(OtlpHttpLogExporterTestPeer, ExportJsonIntegrationTest) +TEST_F(OtlpHttpLogExporterTestPeer, ExportJsonIntegrationTestSync) { - auto mock_otlp_client = - OtlpHttpLogExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kJson); - auto mock_otlp_http_client = mock_otlp_client.first; - auto client = mock_otlp_client.second; - auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); - - bool attribute_storage_bool_value[] = {true, false, true}; - int32_t attribute_storage_int32_value[] = {1, 2}; - uint32_t attribute_storage_uint32_value[] = {3, 4}; - int64_t attribute_storage_int64_value[] = {5, 6}; - uint64_t attribute_storage_uint64_value[] = {7, 8}; - double attribute_storage_double_value[] = {3.2, 3.3}; - std::string attribute_storage_string_value[] = {"vector", "string"}; - - auto provider = nostd::shared_ptr(new sdk::logs::LoggerProvider()); - provider->AddProcessor(std::unique_ptr( - new sdk::logs::BatchLogProcessor(std::move(exporter), 5, std::chrono::milliseconds(256), 5))); - - std::string report_trace_id; - std::string report_span_id; - uint8_t trace_id_bin[opentelemetry::trace::TraceId::kSize] = { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - char trace_id_hex[2 * opentelemetry::trace::TraceId::kSize] = {0}; - opentelemetry::trace::TraceId trace_id{trace_id_bin}; - uint8_t span_id_bin[opentelemetry::trace::SpanId::kSize] = {'7', '6', '5', '4', - '3', '2', '1', '0'}; - char span_id_hex[2 * opentelemetry::trace::SpanId::kSize] = {0}; - opentelemetry::trace::SpanId span_id{span_id_bin}; - - const std::string schema_url{"https://opentelemetry.io/schemas/1.2.0"}; - auto logger = provider->GetLogger("test", "", "opentelelemtry_library", "", schema_url); - logger->Log(opentelemetry::logs::Severity::kInfo, "Log message", - {{"service.name", "unit_test_service"}, - {"tenant.id", "test_user"}, - {"bool_value", true}, - {"int32_value", static_cast(1)}, - {"uint32_value", static_cast(2)}, - {"int64_value", static_cast(0x1100000000LL)}, - {"uint64_value", static_cast(0x1200000000ULL)}, - {"double_value", static_cast(3.1)}, - {"vec_bool_value", attribute_storage_bool_value}, - {"vec_int32_value", attribute_storage_int32_value}, - {"vec_uint32_value", attribute_storage_uint32_value}, - {"vec_int64_value", attribute_storage_int64_value}, - {"vec_uint64_value", attribute_storage_uint64_value}, - {"vec_double_value", attribute_storage_double_value}, - {"vec_string_value", attribute_storage_string_value}}, - trace_id, span_id, - opentelemetry::trace::TraceFlags{opentelemetry::trace::TraceFlags::kIsSampled}, - std::chrono::system_clock::now()); - - trace_id.ToLowerBase16(MakeSpan(trace_id_hex)); - report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); - - span_id.ToLowerBase16(MakeSpan(span_id_hex)); - report_span_id.assign(span_id_hex, sizeof(span_id_hex)); - - auto no_send_client = std::static_pointer_cast(client); - auto mock_session = - std::static_pointer_cast(no_send_client->session_); - EXPECT_CALL(*mock_session, SendRequest) - .WillOnce([&mock_session, report_trace_id, - report_span_id](opentelemetry::ext::http::client::EventHandler &callback) { - auto check_json = nlohmann::json::parse(mock_session->GetRequest()->body_, nullptr, false); - auto resource_logs = *check_json["resource_logs"].begin(); - auto scope_logs = *resource_logs["scope_logs"].begin(); - auto log = *scope_logs["log_records"].begin(); - auto received_trace_id = log["trace_id"].get(); - auto received_span_id = log["span_id"].get(); - EXPECT_EQ(received_trace_id, report_trace_id); - EXPECT_EQ(received_span_id, report_span_id); - EXPECT_EQ("Log message", log["body"]["string_value"].get()); - EXPECT_LE(15, log["attributes"].size()); - auto custom_header = mock_session->GetRequest()->headers_.find("Custom-Header-Key"); - ASSERT_TRUE(custom_header != mock_session->GetRequest()->headers_.end()); - if (custom_header != mock_session->GetRequest()->headers_.end()) - { - EXPECT_EQ("Custom-Header-Value", custom_header->second); - } - // let the otlp_http_client to continue - http_client::nosend::Response response; - callback.OnResponse(response); - }); + ExportJsonIntegrationTest(); } +# ifdef ENABLE_ASYNC_EXPORT +TEST_F(OtlpHttpLogExporterTestPeer, ExportJsonIntegrationTestAsync) +{ + ExportJsonIntegrationTestAsync(); +} +# endif + // Create log records, let processor call Export() -TEST_F(OtlpHttpLogExporterTestPeer, ExportBinaryIntegrationTest) +TEST_F(OtlpHttpLogExporterTestPeer, ExportBinaryIntegrationTestSync) { - auto mock_otlp_client = - OtlpHttpLogExporterTestPeer::GetMockOtlpHttpClient(HttpRequestContentType::kBinary); - auto mock_otlp_http_client = mock_otlp_client.first; - auto client = mock_otlp_client.second; - auto exporter = GetExporter(std::unique_ptr{mock_otlp_http_client}); - - bool attribute_storage_bool_value[] = {true, false, true}; - int32_t attribute_storage_int32_value[] = {1, 2}; - uint32_t attribute_storage_uint32_value[] = {3, 4}; - int64_t attribute_storage_int64_value[] = {5, 6}; - uint64_t attribute_storage_uint64_value[] = {7, 8}; - double attribute_storage_double_value[] = {3.2, 3.3}; - std::string attribute_storage_string_value[] = {"vector", "string"}; - - auto provider = nostd::shared_ptr(new sdk::logs::LoggerProvider()); - provider->AddProcessor(std::unique_ptr( - new sdk::logs::BatchLogProcessor(std::move(exporter), 5, std::chrono::milliseconds(256), 5))); - - std::string report_trace_id; - std::string report_span_id; - uint8_t trace_id_bin[opentelemetry::trace::TraceId::kSize] = { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - opentelemetry::trace::TraceId trace_id{trace_id_bin}; - uint8_t span_id_bin[opentelemetry::trace::SpanId::kSize] = {'7', '6', '5', '4', - '3', '2', '1', '0'}; - opentelemetry::trace::SpanId span_id{span_id_bin}; - - const std::string schema_url{"https://opentelemetry.io/schemas/1.2.0"}; - auto logger = provider->GetLogger("test", "", "opentelelemtry_library", "", schema_url); - logger->Log(opentelemetry::logs::Severity::kInfo, "Log message", - {{"service.name", "unit_test_service"}, - {"tenant.id", "test_user"}, - {"bool_value", true}, - {"int32_value", static_cast(1)}, - {"uint32_value", static_cast(2)}, - {"int64_value", static_cast(0x1100000000LL)}, - {"uint64_value", static_cast(0x1200000000ULL)}, - {"double_value", static_cast(3.1)}, - {"vec_bool_value", attribute_storage_bool_value}, - {"vec_int32_value", attribute_storage_int32_value}, - {"vec_uint32_value", attribute_storage_uint32_value}, - {"vec_int64_value", attribute_storage_int64_value}, - {"vec_uint64_value", attribute_storage_uint64_value}, - {"vec_double_value", attribute_storage_double_value}, - {"vec_string_value", attribute_storage_string_value}}, - trace_id, span_id, - opentelemetry::trace::TraceFlags{opentelemetry::trace::TraceFlags::kIsSampled}, - std::chrono::system_clock::now()); - - report_trace_id.assign(reinterpret_cast(trace_id_bin), sizeof(trace_id_bin)); - report_span_id.assign(reinterpret_cast(span_id_bin), sizeof(span_id_bin)); - - auto no_send_client = std::static_pointer_cast(client); - auto mock_session = - std::static_pointer_cast(no_send_client->session_); - EXPECT_CALL(*mock_session, SendRequest) - .WillOnce([&mock_session, report_trace_id, - report_span_id](opentelemetry::ext::http::client::EventHandler &callback) { - opentelemetry::proto::collector::logs::v1::ExportLogsServiceRequest request_body; - request_body.ParseFromArray(&mock_session->GetRequest()->body_[0], - static_cast(mock_session->GetRequest()->body_.size())); - auto &received_log = request_body.resource_logs(0).scope_logs(0).log_records(0); - EXPECT_EQ(received_log.trace_id(), report_trace_id); - EXPECT_EQ(received_log.span_id(), report_span_id); - EXPECT_EQ("Log message", received_log.body().string_value()); - EXPECT_LE(15, received_log.attributes_size()); - bool check_service_name = false; - for (auto &attribute : received_log.attributes()) - { - if ("service.name" == attribute.key()) - { - check_service_name = true; - EXPECT_EQ("unit_test_service", attribute.value().string_value()); - } - } - ASSERT_TRUE(check_service_name); - http_client::nosend::Response response; - callback.OnResponse(response); - }); + ExportBinaryIntegrationTest(); } +# ifdef ENABLE_ASYNC_EXPORT +TEST_F(OtlpHttpLogExporterTestPeer, ExportBinaryIntegrationTestAsync) +{ + ExportBinaryIntegrationTestAsync(); +} +# endif + // Test exporter configuration options TEST_F(OtlpHttpLogExporterTestPeer, ConfigTest) { diff --git a/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h b/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h index ae0e8173f9..f730f1e111 100644 --- a/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h +++ b/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h @@ -78,6 +78,17 @@ class ZipkinExporter final : public opentelemetry::sdk::trace::SpanExporter const nostd::span> &spans) noexcept override; +#ifdef ENABLE_ASYNC_EXPORT + /** + * Export asynchronosly a batch of span recordables in JSON format + * @param spans a span of unique pointers to span recordables + * @param result_callback callback function accepting ExportResult as argument + */ + void Export(const nostd::span> &spans, + std::function + &&result_callback) noexcept override; +#endif + /** * Shut down the exporter. * @param timeout an optional timeout, default to max. diff --git a/exporters/zipkin/src/zipkin_exporter.cc b/exporters/zipkin/src/zipkin_exporter.cc index 240144599f..92a6bfc37f 100644 --- a/exporters/zipkin/src/zipkin_exporter.cc +++ b/exporters/zipkin/src/zipkin_exporter.cc @@ -93,6 +93,17 @@ sdk::common::ExportResult ZipkinExporter::Export( return sdk::common::ExportResult::kSuccess; } +#ifdef ENABLE_ASYNC_EXPORT +void ZipkinExporter::Export( + const nostd::span> &spans, + std::function &&result_callback) noexcept +{ + OTEL_INTERNAL_LOG_WARN("[ZIPKIN EXPORTER] async not supported. Making sync interface call"); + auto status = Export(spans); + result_callback(status); +} +#endif + void ZipkinExporter::InitializeLocalEndpoint() { if (options_.service_name.length()) diff --git a/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h b/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h index 9f2f05f3f0..888b7e676f 100644 --- a/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h +++ b/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h @@ -3,13 +3,19 @@ #pragma once -#include "http_operation_curl.h" +#include "opentelemetry/ext/http/client/curl/http_operation_curl.h" #include "opentelemetry/ext/http/client/http_client.h" #include "opentelemetry/ext/http/common/url_parser.h" +#include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/version.h" -#include +#include +#include +#include #include +#include +#include +#include #include OPENTELEMETRY_BEGIN_NAMESPACE @@ -24,6 +30,23 @@ namespace curl const opentelemetry::ext::http::client::StatusCode Http_Ok = 200; +class HttpCurlGlobalInitializer +{ +private: + HttpCurlGlobalInitializer(const HttpCurlGlobalInitializer &) = delete; + HttpCurlGlobalInitializer(HttpCurlGlobalInitializer &&) = delete; + + HttpCurlGlobalInitializer &operator=(const HttpCurlGlobalInitializer &) = delete; + HttpCurlGlobalInitializer &operator=(HttpCurlGlobalInitializer &&) = delete; + + HttpCurlGlobalInitializer(); + +public: + ~HttpCurlGlobalInitializer(); + + static nostd::shared_ptr GetInstance(); +}; + class Request : public opentelemetry::ext::http::client::Request { public: @@ -53,10 +76,7 @@ class Request : public opentelemetry::ext::http::client::Request AddHeader(name, value); } - virtual void SetUri(nostd::string_view uri) noexcept override - { - uri_ = static_cast(uri); - } + void SetUri(nostd::string_view uri) noexcept override { uri_ = static_cast(uri); } void SetTimeoutMs(std::chrono::milliseconds timeout_ms) noexcept override { @@ -76,14 +96,10 @@ class Response : public opentelemetry::ext::http::client::Response public: Response() : status_code_(Http_Ok) {} - virtual const opentelemetry::ext::http::client::Body &GetBody() const noexcept override - { - return body_; - } + const opentelemetry::ext::http::client::Body &GetBody() const noexcept override { return body_; } - virtual bool ForEachHeader( - nostd::function_ref callable) - const noexcept override + bool ForEachHeader(nostd::function_ref + callable) const noexcept override { for (const auto &header : headers_) { @@ -95,10 +111,9 @@ class Response : public opentelemetry::ext::http::client::Response return true; } - virtual bool ForEachHeader( - const nostd::string_view &name, - nostd::function_ref callable) - const noexcept override + bool ForEachHeader(const nostd::string_view &name, + nostd::function_ref + callable) const noexcept override { auto range = headers_.equal_range(static_cast(name)); for (auto it = range.first; it != range.second; ++it) @@ -111,7 +126,7 @@ class Response : public opentelemetry::ext::http::client::Response return true; } - virtual opentelemetry::ext::http::client::StatusCode GetStatusCode() const noexcept override + opentelemetry::ext::http::client::StatusCode GetStatusCode() const noexcept override { return status_code_; } @@ -124,7 +139,8 @@ class Response : public opentelemetry::ext::http::client::Response class HttpClient; -class Session : public opentelemetry::ext::http::client::Session +class Session : public opentelemetry::ext::http::client::Session, + public std::enable_shared_from_this { public: Session(HttpClient &http_client, @@ -142,40 +158,17 @@ class Session : public opentelemetry::ext::http::client::Session return http_request_; } - virtual void SendRequest( - opentelemetry::ext::http::client::EventHandler &callback) noexcept override - { - is_session_active_ = true; - std::string url = host_ + std::string(http_request_->uri_); - auto callback_ptr = &callback; - curl_operation_.reset(new HttpOperation( - http_request_->method_, url, callback_ptr, RequestMode::Async, http_request_->headers_, - http_request_->body_, false, http_request_->timeout_ms_)); - curl_operation_->SendAsync([this, callback_ptr](HttpOperation &operation) { - if (operation.WasAborted()) - { - // Manually cancelled - callback_ptr->OnEvent(opentelemetry::ext::http::client::SessionState::Cancelled, ""); - } - - if (operation.GetResponseCode() >= CURL_LAST) - { - // we have a http response - auto response = std::unique_ptr(new Response()); - response->headers_ = operation.GetResponseHeaders(); - response->body_ = operation.GetResponseBody(); - response->status_code_ = operation.GetResponseCode(); - callback_ptr->OnResponse(*response); - } - is_session_active_ = false; - }); - } + void SendRequest( + std::shared_ptr callback) noexcept override; - virtual bool CancelSession() noexcept override; + bool CancelSession() noexcept override; - virtual bool FinishSession() noexcept override; + bool FinishSession() noexcept override; - virtual bool IsSessionActive() noexcept override { return is_session_active_; } + bool IsSessionActive() noexcept override + { + return is_session_active_.load(std::memory_order_acquire); + } void SetId(uint64_t session_id) { session_id_ = session_id; } @@ -188,19 +181,36 @@ class Session : public opentelemetry::ext::http::client::Session #ifdef ENABLE_TEST std::shared_ptr GetRequest() { return http_request_; } #endif + + inline HttpClient &GetHttpClient() noexcept { return http_client_; } + inline const HttpClient &GetHttpClient() const noexcept { return http_client_; } + + inline uint64_t GetSessionId() const noexcept { return session_id_; } + + inline const std::unique_ptr &GetOperation() const noexcept + { + return curl_operation_; + } + inline std::unique_ptr &GetOperation() noexcept { return curl_operation_; } + + /** + * Finish and cleanup the operation.It will remove curl easy handle in it from HttpClient + */ + void FinishOperation(); + private: std::shared_ptr http_request_; std::string host_; std::unique_ptr curl_operation_; uint64_t session_id_; HttpClient &http_client_; - bool is_session_active_; + std::atomic is_session_active_; }; class HttpClientSync : public opentelemetry::ext::http::client::HttpClientSync { public: - HttpClientSync() { curl_global_init(CURL_GLOBAL_ALL); } + HttpClientSync() : curl_global_initializer_(HttpCurlGlobalInitializer::GetInstance()) {} opentelemetry::ext::http::client::Result Get( const nostd::string_view &url, @@ -208,7 +218,7 @@ class HttpClientSync : public opentelemetry::ext::http::client::HttpClientSync { opentelemetry::ext::http::client::Body body; HttpOperation curl_operation(opentelemetry::ext::http::client::Method::Get, url.data(), nullptr, - RequestMode::Sync, headers, body); + headers, body); curl_operation.SendSync(); auto session_state = curl_operation.GetSessionState(); if (curl_operation.WasAborted()) @@ -233,7 +243,7 @@ class HttpClientSync : public opentelemetry::ext::http::client::HttpClientSync const opentelemetry::ext::http::client::Headers &headers) noexcept override { HttpOperation curl_operation(opentelemetry::ext::http::client::Method::Post, url.data(), - nullptr, RequestMode::Sync, headers, body); + nullptr, headers, body); curl_operation.SendSync(); auto session_state = curl_operation.GetSessionState(); if (curl_operation.WasAborted()) @@ -253,60 +263,84 @@ class HttpClientSync : public opentelemetry::ext::http::client::HttpClientSync return opentelemetry::ext::http::client::Result(std::move(response), session_state); } - ~HttpClientSync() { curl_global_cleanup(); } + ~HttpClientSync() {} + +private: + nostd::shared_ptr curl_global_initializer_; }; class HttpClient : public opentelemetry::ext::http::client::HttpClient { public: // The call (curl_global_init) is not thread safe. Ensure this is called only once. - HttpClient() : next_session_id_{0} { curl_global_init(CURL_GLOBAL_ALL); } + HttpClient(); + ~HttpClient(); std::shared_ptr CreateSession( - nostd::string_view url) noexcept override + nostd::string_view url) noexcept override; + + bool CancelAllSessions() noexcept override; + + bool FinishAllSessions() noexcept override; + + void SetMaxSessionsPerConnection(std::size_t max_requests_per_connection) noexcept override; + + inline uint64_t GetMaxSessionsPerConnection() const noexcept { - auto parsedUrl = common::UrlParser(std::string(url)); - if (!parsedUrl.success_) - { - return std::make_shared(*this); - } - auto session = - std::make_shared(*this, parsedUrl.scheme_, parsedUrl.host_, parsedUrl.port_); - auto session_id = ++next_session_id_; - session->SetId(session_id); - sessions_.insert({session_id, session}); - return session; + return max_sessions_per_connection_; } - bool CancelAllSessions() noexcept override + void CleanupSession(uint64_t session_id); + + inline CURLM *GetMultiHandle() noexcept { return multi_handle_; } + + void MaybeSpawnBackgroundThread(); + + void ScheduleAddSession(uint64_t session_id); + void ScheduleAbortSession(uint64_t session_id); + void ScheduleRemoveSession(uint64_t session_id, HttpCurlEasyResource &&resource); + +#ifdef ENABLE_TEST + void WaitBackgroundThreadExit() { - for (auto &session : sessions_) + std::unique_ptr background_thread; { - session.second->CancelSession(); + std::lock_guard lock_guard{background_thread_m_}; + background_thread.swap(background_thread_); } - return true; - } - bool FinishAllSessions() noexcept override - { - for (auto &session : sessions_) + if (background_thread && background_thread->joinable()) { - session.second->FinishSession(); + background_thread->join(); } - return true; - } - - void CleanupSession(uint64_t session_id) - { - // TBD = Need to be thread safe - sessions_.erase(session_id); } - - ~HttpClient() { curl_global_cleanup(); } +#endif private: + void wakeupBackgroundThread(); + bool doAddSessions(); + bool doAbortSessions(); + bool doRemoveSessions(); + void resetMultiHandle(); + + std::mutex multi_handle_m_; + CURLM *multi_handle_; std::atomic next_session_id_; - std::map> sessions_; + uint64_t max_sessions_per_connection_; + + std::mutex sessions_m_; + std::recursive_mutex session_ids_m_; + std::unordered_map> sessions_; + std::unordered_set pending_to_add_session_ids_; + std::unordered_set pending_to_abort_session_ids_; + std::unordered_map pending_to_remove_session_handles_; + std::list> pending_to_remove_sessions_; + + std::mutex background_thread_m_; + std::unique_ptr background_thread_; + std::chrono::milliseconds scheduled_delay_milliseconds_; + + nostd::shared_ptr curl_global_initializer_; }; } // namespace curl diff --git a/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h b/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h index 679251cfeb..57d0d2129f 100644 --- a/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h +++ b/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h @@ -3,7 +3,6 @@ #pragma once -#include "http_client_curl.h" #include "opentelemetry/ext/http/client/http_client.h" #include "opentelemetry/version.h" @@ -12,6 +11,7 @@ #include #include #include +#include #include #ifdef _WIN32 # include @@ -35,29 +35,89 @@ const std::chrono::milliseconds default_http_conn_timeout(5000); // ms const std::string http_status_regexp = "HTTP\\/\\d\\.\\d (\\d+)\\ .*"; const std::string http_header_regexp = "(.*)\\: (.*)\\n*"; -enum class RequestMode +class HttpClient; +class Session; + +struct HttpCurlEasyResource { - Sync, - Async + CURL *easy_handle; + curl_slist *headers_chunk; + + HttpCurlEasyResource(CURL *curl = nullptr, curl_slist *headers = nullptr) + : easy_handle{curl}, headers_chunk{headers} + {} + + HttpCurlEasyResource(HttpCurlEasyResource &&other) + : easy_handle{other.easy_handle}, headers_chunk{other.headers_chunk} + { + other.easy_handle = nullptr; + other.headers_chunk = nullptr; + } + + HttpCurlEasyResource &operator=(HttpCurlEasyResource &&other) + { + using std::swap; + swap(easy_handle, other.easy_handle); + swap(headers_chunk, other.headers_chunk); + + return *this; + } + + HttpCurlEasyResource(const HttpCurlEasyResource &other) = delete; + HttpCurlEasyResource &operator=(const HttpCurlEasyResource &other) = delete; }; class HttpOperation { -public: - void DispatchEvent(opentelemetry::ext::http::client::SessionState type, std::string reason = "") - { - if (request_mode_ == RequestMode::Async && callback_ != nullptr) - { - callback_->OnEvent(type, reason); - } - else - { - session_state_ = type; - } - } +private: + /** + * Old-school memory allocator + * + * @param contents + * @param size + * @param nmemb + * @param userp + * @return + */ + static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp); - std::atomic is_aborted_; // Set to 'true' when async callback is aborted - std::atomic is_finished_; // Set to 'true' when async callback is finished. + /** + * C++ STL std::vector allocator + * + * @param ptr + * @param size + * @param nmemb + * @param data + * @return + */ + static size_t WriteVectorHeaderCallback(void *ptr, size_t size, size_t nmemb, void *userp); + static size_t WriteVectorBodyCallback(void *ptr, size_t size, size_t nmemb, void *userp); + + static size_t ReadMemoryCallback(char *buffer, size_t size, size_t nitems, void *userp); + +#if LIBCURL_VERSION_NUM >= 0x075000 + static int PreRequestCallback(void *clientp, + char *conn_primary_ip, + char *conn_local_ip, + int conn_primary_port, + int conn_local_port); +#endif + +#if LIBCURL_VERSION_NUM >= 0x072000 + static int OnProgressCallback(void *clientp, + curl_off_t dltotal, + curl_off_t dlnow, + curl_off_t ultotal, + curl_off_t ulnow); +#else + static int OnProgressCallback(void *clientp, + double dltotal, + double dlnow, + double ultotal, + double ulnow); +#endif +public: + void DispatchEvent(opentelemetry::ext::http::client::SessionState type, std::string reason = ""); /** * Create local CURL instance for url and body @@ -72,8 +132,7 @@ class HttpOperation */ HttpOperation(opentelemetry::ext::http::client::Method method, std::string url, - opentelemetry::ext::http::client::EventHandler *callback, - RequestMode request_mode = RequestMode::Async, + opentelemetry::ext::http::client::EventHandler *event_handle, // Default empty headers and empty request body const opentelemetry::ext::http::client::Headers &request_headers = opentelemetry::ext::http::client::Headers(), @@ -81,238 +140,52 @@ class HttpOperation opentelemetry::ext::http::client::Body(), // Default connectivity and response size options bool is_raw_response = false, - std::chrono::milliseconds http_conn_timeout = default_http_conn_timeout) - : is_aborted_(false), - is_finished_(false), - // Optional connection params - is_raw_response_(is_raw_response), - http_conn_timeout_(http_conn_timeout), - request_mode_(request_mode), - curl_(nullptr), - // Result - res_(CURLE_OK), - callback_(callback), - method_(method), - url_(url), - // Local vars - request_headers_(request_headers), - request_body_(request_body), - sockfd_(0), - nread_(0) - { - /* get a curl handle */ - curl_ = curl_easy_init(); - if (!curl_) - { - res_ = CURLE_FAILED_INIT; - DispatchEvent(opentelemetry::ext::http::client::SessionState::CreateFailed); - return; - } - - curl_easy_setopt(curl_, CURLOPT_VERBOSE, 0); - - // Specify target URL - curl_easy_setopt(curl_, CURLOPT_URL, url_.c_str()); - - // TODO: support ssl cert verification for https request - curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYPEER, 0); // 1L - curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYHOST, 0); // 2L - - // Specify our custom headers - for (auto &kv : this->request_headers_) - { - std::string header = std::string(kv.first); - header += ": "; - header += std::string(kv.second); - headers_chunk_ = curl_slist_append(headers_chunk_, header.c_str()); - } - - if (headers_chunk_ != nullptr) - { - curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, headers_chunk_); - } - - DispatchEvent(opentelemetry::ext::http::client::SessionState::Created); - } + std::chrono::milliseconds http_conn_timeout = default_http_conn_timeout, + bool reuse_connection = false); /** * Destroy CURL instance */ - virtual ~HttpOperation() - { - // Given the request has not been aborted we should wait for completion here - // This guarantees the lifetime of this request. - if (result_.valid()) - { - result_.wait(); - } - // TBD - Need to be uncomment. This will callback instance is deleted. - // DispatchEvent(opentelemetry::ext::http::client::SessionState::Destroy); - res_ = CURLE_OK; - curl_easy_cleanup(curl_); - curl_slist_free_all(headers_chunk_); - ReleaseResponse(); - } + virtual ~HttpOperation(); /** * Finish CURL instance */ - virtual void Finish() - { - if (result_.valid() && !is_finished_) - { - result_.wait(); - is_finished_ = true; - } - } + virtual void Finish(); + + /** + * Cleanup all resource of curl + */ + void Cleanup(); + + /** + * Setup request + */ + CURLcode Setup(); /** * Send request synchronously */ - long Send() - { - ReleaseResponse(); - // Request buffer - const void *request = (request_body_.empty()) ? NULL : &request_body_[0]; - const size_t req_size = request_body_.size(); - if (!curl_) - { - res_ = CURLE_FAILED_INIT; - DispatchEvent(opentelemetry::ext::http::client::SessionState::SendFailed); - return res_; - } - - // TODO: control local port to use - // curl_easy_setopt(curl, CURLOPT_LOCALPORT, dcf_port); - - // Perform initial connect, handling the timeout if needed - curl_easy_setopt(curl_, CURLOPT_CONNECT_ONLY, 1L); - curl_easy_setopt(curl_, CURLOPT_TIMEOUT_MS, http_conn_timeout_.count()); - DispatchEvent(opentelemetry::ext::http::client::SessionState::Connecting); - res_ = curl_easy_perform(curl_); - if (CURLE_OK != res_) - { - DispatchEvent(opentelemetry::ext::http::client::SessionState::ConnectFailed, - curl_easy_strerror(res_)); // couldn't connect - stage 1 - return res_; - } - - /* Extract the socket from the curl handle - we'll need it for waiting. - * Note that this API takes a pointer to a 'long' while we use - * curl_socket_t for sockets otherwise. - */ - long sockextr = 0; - res_ = curl_easy_getinfo(curl_, CURLINFO_LASTSOCKET, &sockextr); - - if (CURLE_OK != res_) - { - DispatchEvent(opentelemetry::ext::http::client::SessionState::ConnectFailed, - curl_easy_strerror(res_)); // couldn't connect - stage 2 - return res_; - } - - /* wait for the socket to become ready for sending */ - sockfd_ = sockextr; - if (!WaitOnSocket(sockfd_, 0, static_cast(http_conn_timeout_.count())) || is_aborted_) - { - res_ = CURLE_OPERATION_TIMEDOUT; - DispatchEvent( - opentelemetry::ext::http::client::SessionState::ConnectFailed, - " Is aborted: " + std::to_string(is_aborted_.load())); // couldn't connect - stage 3 - return res_; - } - - DispatchEvent(opentelemetry::ext::http::client::SessionState::Connected); - // once connection is there - switch back to easy perform for HTTP post - curl_easy_setopt(curl_, CURLOPT_CONNECT_ONLY, 0); - - // send all data to our callback function - if (is_raw_response_) - { - curl_easy_setopt(curl_, CURLOPT_HEADER, true); - curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, (void *)&WriteMemoryCallback); - curl_easy_setopt(curl_, CURLOPT_WRITEDATA, (void *)&raw_response_); - } - else - { - curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, (void *)&WriteVectorCallback); - curl_easy_setopt(curl_, CURLOPT_HEADERDATA, (void *)&resp_headers_); - curl_easy_setopt(curl_, CURLOPT_WRITEDATA, (void *)&resp_body_); - } - - // TODO: only two methods supported for now - POST and GET - if (method_ == opentelemetry::ext::http::client::Method::Post) - { - // POST - curl_easy_setopt(curl_, CURLOPT_POST, true); - curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, (const char *)request); - curl_easy_setopt(curl_, CURLOPT_POSTFIELDSIZE, req_size); - } - else if (method_ == opentelemetry::ext::http::client::Method::Get) - { - // GET - } - else - { - res_ = CURLE_UNSUPPORTED_PROTOCOL; - return res_; - } - - // abort if slower than 4kb/sec during 30 seconds - curl_easy_setopt(curl_, CURLOPT_LOW_SPEED_TIME, 30L); - curl_easy_setopt(curl_, CURLOPT_LOW_SPEED_LIMIT, 4096); - DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); - - res_ = curl_easy_perform(curl_); - if (CURLE_OK != res_) - { - DispatchEvent(opentelemetry::ext::http::client::SessionState::SendFailed, - curl_easy_strerror(res_)); - return res_; - } - - /* Code snippet to parse raw HTTP response. This might come in handy - * if we ever consider to handle the raw upload instead of curl_easy_perform - ... - std::string resp((const char *)response); - std::regex http_status_regex(HTTP_STATUS_REGEXP); - std::smatch match; - if(std::regex_search(resp, match, http_status_regex)) - http_code = std::stol(match[1]); - ... - */ - - /* libcurl is nice enough to parse the http response code itself: */ - curl_easy_getinfo(curl_, CURLINFO_RESPONSE_CODE, &res_); - // We got some response from server. Dump the contents. - DispatchEvent(opentelemetry::ext::http::client::SessionState::Response); - - // This function returns: - // - on success: HTTP status code. - // - on failure: CURL error code. - // The two sets of enums (CURLE, HTTP codes) - do not intersect, so we collapse them in one set. - return res_; - } + CURLcode Send(); - std::future &SendAsync(std::function callback = nullptr) - { - result_ = std::async(std::launch::async, [this, callback] { - long result = Send(); - if (callback != nullptr) - { - callback(*this); - } - return result; - }); - return result_; - } + /** + * Send request asynchronously + * @param session This operator must be binded to a Session + * @param callback callback when start async request success and got response + */ + CURLcode SendAsync(Session *session, std::function callback = nullptr); - void SendSync() { Send(); } + inline void SendSync() { Send(); } /** - * Get HTTP response code. This function returns CURL error code if HTTP response code is invalid. + * Get HTTP response code. This function returns 0. */ - uint16_t GetResponseCode() { return res_; } + inline StatusCode GetResponseCode() const noexcept + { + return static_cast(response_code_); + } + + CURLcode GetLastResultCode() { return last_curl_result_; } /** * Get last session state. @@ -322,244 +195,87 @@ class HttpOperation /** * Get whether or not response was programmatically aborted */ - bool WasAborted() { return is_aborted_.load(); } + bool WasAborted() { return is_aborted_.load(std::memory_order_acquire); } /** * Return a copy of resposne headers * * @return */ - Headers GetResponseHeaders() - { - Headers result; - if (resp_headers_.size() == 0) - return result; - - std::stringstream ss; - std::string headers((const char *)&resp_headers_[0], resp_headers_.size()); - ss.str(headers); - - std::string header; - while (std::getline(ss, header, '\n')) - { - // TODO - Regex below crashes with out-of-memory on CI docker container, so - // switching to string comparison. Need to debug and revert back. - - /*std::smatch match; - std::regex http_headers_regex(http_header_regexp); - if (std::regex_search(header, match, http_headers_regex)) - result.insert(std::pair( - static_cast(match[1]), static_cast(match[2]))); - */ - size_t pos = header.find(": "); - if (pos != std::string::npos) - result.insert( - std::pair(header.substr(0, pos), header.substr(pos + 2))); - } - return result; - } + Headers GetResponseHeaders(); /** * Return a copy of response body * * @return */ - std::vector GetResponseBody() { return resp_body_; } + inline const std::vector &GetResponseBody() const noexcept { return response_body_; } /** * Return a raw copy of response headers+body * * @return */ - std::vector GetRawResponse() { return raw_response_; } + inline const std::vector &GetRawResponse() const noexcept { return raw_response_; } /** * Release memory allocated for response */ - void ReleaseResponse() - { - resp_headers_.clear(); - resp_body_.clear(); - raw_response_.clear(); - } + void ReleaseResponse(); /** * Abort request in connecting or reading state. */ - void Abort() - { - is_aborted_ = true; - if (curl_ != nullptr) - { - // Simply close the socket - connection reset by peer - if (sockfd_) - { -#if defined(_WIN32) - ::closesocket(sockfd_); -#else - ::close(sockfd_); -#endif - sockfd_ = 0; - } - } - } + void Abort(); + + /** + * Perform curl message, this function only can be called in the polling thread and it can only + * be called when got a CURLMSG_DONE. + * + * @param code + */ + void PerformCurlMessage(CURLcode code); - CURL *GetHandle() { return curl_; } + inline CURL *GetCurlEasyHandle() noexcept { return curl_resource_.easy_handle; } -protected: - const bool is_raw_response_; // Do not split response headers from response body +private: + std::atomic is_aborted_; // Set to 'true' when async callback is aborted + std::atomic is_finished_; // Set to 'true' when async callback is finished. + std::atomic is_cleaned_; // Set to 'true' when async callback is cleaned. + const bool is_raw_response_; // Do not split response headers from response body + const bool reuse_connection_; // Reuse connection const std::chrono::milliseconds http_conn_timeout_; // Timeout for connect. Default: 5000ms - RequestMode request_mode_; - CURL *curl_; // Local curl instance - CURLcode res_; // Curl result OR HTTP status code if successful + HttpCurlEasyResource curl_resource_; + CURLcode last_curl_result_; // Curl result OR HTTP status code if successful - opentelemetry::ext::http::client::EventHandler *callback_; + opentelemetry::ext::http::client::EventHandler *event_handle_; // Request values opentelemetry::ext::http::client::Method method_; std::string url_; const Headers &request_headers_; const opentelemetry::ext::http::client::Body &request_body_; - struct curl_slist *headers_chunk_ = nullptr; + size_t request_nwrite_; opentelemetry::ext::http::client::SessionState session_state_; // Processed response headers and body - std::vector resp_headers_; - std::vector resp_body_; + long response_code_; + std::vector response_headers_; + std::vector response_body_; std::vector raw_response_; - // Socket parameters - curl_socket_t sockfd_; - - curl_off_t nread_; - size_t sendlen_ = 0; // # bytes sent by client - size_t acklen_ = 0; // # bytes ack by server - - std::future result_; - - /** - * Helper routine to wait for data on socket - * - * @param sockfd - * @param for_recv - * @param timeout_ms - * @return true if expected events occur, false if timeout or error happen - */ - static bool WaitOnSocket(curl_socket_t sockfd, int for_recv, long timeout_ms) - { - bool res = false; - -#if defined(_WIN32) - - if (sockfd > FD_SETSIZE) - return false; - - struct timeval tv; - fd_set infd, outfd, errfd; - - tv.tv_sec = timeout_ms / 1000; - tv.tv_usec = (timeout_ms % 1000) * 1000; - - FD_ZERO(&infd); - FD_ZERO(&outfd); - FD_ZERO(&errfd); - - FD_SET(sockfd, &errfd); /* always check for error */ - - if (for_recv) - { - FD_SET(sockfd, &infd); - } - else - { - FD_SET(sockfd, &outfd); - } - - /* select() returns the number of signalled sockets or -1 */ - if (select((int)sockfd + 1, &infd, &outfd, &errfd, &tv) > 0) - { - if (for_recv) - { - res = (0 != FD_ISSET(sockfd, &infd)); - } - else - { - res = (0 != FD_ISSET(sockfd, &outfd)); - } - } - -#else - - struct pollfd fds[1]; - ::memset(fds, 0, sizeof(fds)); - - fds[0].fd = sockfd; - if (for_recv) - { - fds[0].events = POLLIN; - } - else - { - fds[0].events = POLLOUT; - } - - if (poll(fds, 1, timeout_ms) > 0) - { - if (for_recv) - { - res = (0 != (fds[0].revents & POLLIN)); - } - else - { - res = (0 != (fds[0].revents & POLLOUT)); - } - } - -#endif - - return res; - } - - /** - * Old-school memory allocator - * - * @param contents - * @param size - * @param nmemb - * @param userp - * @return - */ - static size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) - { - std::vector *buf = static_cast *>(userp); - buf->insert(buf->end(), static_cast(contents), - static_cast(contents) + (size * nmemb)); - return size * nmemb; - } - - /** - * C++ STL std::vector allocator - * - * @param ptr - * @param size - * @param nmemb - * @param data - * @return - */ - static size_t WriteVectorCallback(void *ptr, - size_t size, - size_t nmemb, - std::vector *data) + struct AsyncData { - if (data != nullptr) - { - const unsigned char *begin = (unsigned char *)(ptr); - const unsigned char *end = begin + size * nmemb; - data->insert(data->end(), begin, end); - } - return size * nmemb; - } + Session *session; // Owner Session + + std::thread::id callback_thread; + std::function callback; + std::atomic is_promise_running; + std::promise result_promise; + std::future result_future; + }; + std::unique_ptr async_data_; }; } // namespace curl } // namespace client diff --git a/ext/include/opentelemetry/ext/http/client/http_client.h b/ext/include/opentelemetry/ext/http/client/http_client.h index 308335e492..34564affc3 100644 --- a/ext/include/opentelemetry/ext/http/client/http_client.h +++ b/ext/include/opentelemetry/ext/http/client/http_client.h @@ -212,7 +212,7 @@ class Session public: virtual std::shared_ptr CreateRequest() noexcept = 0; - virtual void SendRequest(EventHandler &) noexcept = 0; + virtual void SendRequest(std::shared_ptr) noexcept = 0; virtual bool IsSessionActive() noexcept = 0; @@ -232,6 +232,8 @@ class HttpClient virtual bool FinishAllSessions() noexcept = 0; + virtual void SetMaxSessionsPerConnection(std::size_t max_requests_per_connection) noexcept = 0; + virtual ~HttpClient() = default; }; diff --git a/ext/include/opentelemetry/ext/http/client/nosend/http_client_nosend.h b/ext/include/opentelemetry/ext/http/client/nosend/http_client_nosend.h index 02433d75ce..f32a075879 100644 --- a/ext/include/opentelemetry/ext/http/client/nosend/http_client_nosend.h +++ b/ext/include/opentelemetry/ext/http/client/nosend/http_client_nosend.h @@ -51,10 +51,7 @@ class Request : public opentelemetry::ext::http::client::Request void ReplaceHeader(nostd::string_view name, nostd::string_view value) noexcept override; - virtual void SetUri(nostd::string_view uri) noexcept override - { - uri_ = static_cast(uri); - } + void SetUri(nostd::string_view uri) noexcept override { uri_ = static_cast(uri); } void SetTimeoutMs(std::chrono::milliseconds timeout_ms) noexcept override { @@ -74,25 +71,33 @@ class Response : public opentelemetry::ext::http::client::Response public: Response() : status_code_(Http_Ok) {} - virtual const opentelemetry::ext::http::client::Body &GetBody() const noexcept override - { - return body_; - } + const opentelemetry::ext::http::client::Body &GetBody() const noexcept override { return body_; } - virtual bool ForEachHeader( - nostd::function_ref callable) - const noexcept override; + bool ForEachHeader(nostd::function_ref + callable) const noexcept override; - virtual bool ForEachHeader( - const nostd::string_view &name, - nostd::function_ref callable) - const noexcept override; + bool ForEachHeader(const nostd::string_view &name, + nostd::function_ref + callable) const noexcept override; - virtual opentelemetry::ext::http::client::StatusCode GetStatusCode() const noexcept override + opentelemetry::ext::http::client::StatusCode GetStatusCode() const noexcept override { return status_code_; } + void Finish(opentelemetry::ext::http::client::EventHandler &callback) noexcept + { + callback.OnEvent(opentelemetry::ext::http::client::SessionState::Created, ""); + callback.OnEvent(opentelemetry::ext::http::client::SessionState::Connecting, ""); + callback.OnEvent(opentelemetry::ext::http::client::SessionState::Connected, ""); + callback.OnEvent(opentelemetry::ext::http::client::SessionState::Sending, ""); + + // let the otlp_http_client to continue + callback.OnResponse(*this); + + callback.OnEvent(opentelemetry::ext::http::client::SessionState::Response, ""); + } + public: Headers headers_; opentelemetry::ext::http::client::Body body_; @@ -121,14 +126,14 @@ class Session : public opentelemetry::ext::http::client::Session MOCK_METHOD(void, SendRequest, - (opentelemetry::ext::http::client::EventHandler &), + (std::shared_ptr), (noexcept, override)); - virtual bool CancelSession() noexcept override; + bool CancelSession() noexcept override; - virtual bool FinishSession() noexcept override; + bool FinishSession() noexcept override; - virtual bool IsSessionActive() noexcept override { return is_session_active_; } + bool IsSessionActive() noexcept override { return is_session_active_; } void SetId(uint64_t session_id) { session_id_ = session_id; } @@ -151,27 +156,18 @@ class Session : public opentelemetry::ext::http::client::Session class HttpClient : public opentelemetry::ext::http::client::HttpClient { public: - HttpClient() { session_ = std::shared_ptr{new Session(*this)}; } + HttpClient(); std::shared_ptr CreateSession( - nostd::string_view) noexcept override - { - return session_; - } + nostd::string_view) noexcept override; - bool CancelAllSessions() noexcept override - { - session_->CancelSession(); - return true; - } + bool CancelAllSessions() noexcept override; - bool FinishAllSessions() noexcept override - { - session_->FinishSession(); - return true; - } + bool FinishAllSessions() noexcept override; + + void SetMaxSessionsPerConnection(std::size_t max_requests_per_connection) noexcept override; - void CleanupSession(uint64_t session_id) {} + void CleanupSession(uint64_t session_id); std::shared_ptr session_; }; diff --git a/ext/src/http/client/curl/BUILD b/ext/src/http/client/curl/BUILD index 33ab814b91..c0557fe99c 100644 --- a/ext/src/http/client/curl/BUILD +++ b/ext/src/http/client/curl/BUILD @@ -5,6 +5,7 @@ cc_library( srcs = [ "http_client_curl.cc", "http_client_factory_curl.cc", + "http_operation_curl.cc", ], copts = [ "-DWITH_CURL", diff --git a/ext/src/http/client/curl/CMakeLists.txt b/ext/src/http/client/curl/CMakeLists.txt index 78a81cfe3e..424f649f1a 100644 --- a/ext/src/http/client/curl/CMakeLists.txt +++ b/ext/src/http/client/curl/CMakeLists.txt @@ -1,7 +1,8 @@ find_package(CURL) if(CURL_FOUND) - add_library(opentelemetry_http_client_curl http_client_factory_curl.cc - http_client_curl.cc) + add_library( + opentelemetry_http_client_curl http_client_factory_curl.cc + http_client_curl.cc http_operation_curl.cc) set_target_properties(opentelemetry_http_client_curl PROPERTIES EXPORT_NAME http_client_curl) diff --git a/ext/src/http/client/curl/http_client_curl.cc b/ext/src/http/client/curl/http_client_curl.cc index 74ad86ea4b..20607cf582 100644 --- a/ext/src/http/client/curl/http_client_curl.cc +++ b/ext/src/http/client/curl/http_client_curl.cc @@ -3,16 +3,577 @@ #include "opentelemetry/ext/http/client/curl/http_client_curl.h" -bool opentelemetry::ext::http::client::curl::Session::CancelSession() noexcept +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace ext +{ +namespace http +{ +namespace client +{ +namespace curl +{ + +HttpCurlGlobalInitializer::HttpCurlGlobalInitializer() +{ + curl_global_init(CURL_GLOBAL_ALL); +} + +HttpCurlGlobalInitializer::~HttpCurlGlobalInitializer() +{ + curl_global_cleanup(); +} + +nostd::shared_ptr HttpCurlGlobalInitializer::GetInstance() +{ + static nostd::shared_ptr shared_initializer{ + new HttpCurlGlobalInitializer()}; + return shared_initializer; +} + +void Session::SendRequest( + std::shared_ptr callback) noexcept { - curl_operation_->Abort(); + is_session_active_.store(true, std::memory_order_release); + std::string url = host_ + std::string(http_request_->uri_); + auto callback_ptr = callback.get(); + bool reuse_connection = false; + if (http_client_.GetMaxSessionsPerConnection() > 0) + { + reuse_connection = session_id_ % http_client_.GetMaxSessionsPerConnection() != 0; + } + + curl_operation_.reset(new HttpOperation(http_request_->method_, url, callback_ptr, + http_request_->headers_, http_request_->body_, false, + http_request_->timeout_ms_, reuse_connection)); + bool success = + CURLE_OK == curl_operation_->SendAsync(this, [this, callback](HttpOperation &operation) { + if (operation.WasAborted()) + { + // Manually cancelled + callback->OnEvent(opentelemetry::ext::http::client::SessionState::Cancelled, ""); + } + + if (operation.GetSessionState() == opentelemetry::ext::http::client::SessionState::Response) + { + // we have a http response + auto response = std::unique_ptr(new Response()); + response->headers_ = operation.GetResponseHeaders(); + response->body_ = operation.GetResponseBody(); + response->status_code_ = operation.GetResponseCode(); + callback->OnResponse(*response); + } + is_session_active_.store(false, std::memory_order_release); + }); + + if (success) + { + http_client_.MaybeSpawnBackgroundThread(); + } + else if (callback) + { + callback->OnEvent(opentelemetry::ext::http::client::SessionState::CreateFailed, ""); + is_session_active_.store(false, std::memory_order_release); + } +} + +bool Session::CancelSession() noexcept +{ + if (curl_operation_) + { + curl_operation_->Abort(); + } http_client_.CleanupSession(session_id_); return true; } -bool opentelemetry::ext::http::client::curl::Session::FinishSession() noexcept +bool Session::FinishSession() noexcept { - curl_operation_->Finish(); + if (curl_operation_) + { + curl_operation_->Finish(); + } http_client_.CleanupSession(session_id_); return true; } + +void Session::FinishOperation() +{ + if (curl_operation_) + { + curl_operation_->Cleanup(); + } +} + +HttpClient::HttpClient() + : next_session_id_{0}, + max_sessions_per_connection_{8}, + scheduled_delay_milliseconds_{std::chrono::milliseconds(256)}, + curl_global_initializer_(HttpCurlGlobalInitializer::GetInstance()) +{ + multi_handle_ = curl_multi_init(); +} + +HttpClient::~HttpClient() +{ + while (true) + { + std::unique_ptr background_thread; + { + std::lock_guard lock_guard{background_thread_m_}; + background_thread.swap(background_thread_); + } + + // Force to abort all sessions + CancelAllSessions(); + + if (!background_thread) + { + break; + } + if (background_thread->joinable()) + { + background_thread->join(); + } + } + { + std::lock_guard lock_guard{multi_handle_m_}; + curl_multi_cleanup(multi_handle_); + } +} + +std::shared_ptr HttpClient::CreateSession( + nostd::string_view url) noexcept +{ + auto parsedUrl = common::UrlParser(std::string(url)); + if (!parsedUrl.success_) + { + return std::make_shared(*this); + } + auto session = + std::make_shared(*this, parsedUrl.scheme_, parsedUrl.host_, parsedUrl.port_); + auto session_id = ++next_session_id_; + session->SetId(session_id); + + std::lock_guard lock_guard{sessions_m_}; + sessions_.insert({session_id, session}); + + // FIXME: Session may leak if it do not SendRequest + return session; +} + +bool HttpClient::CancelAllSessions() noexcept +{ + // CancelSession may change sessions_, we can not change a container while iterating it. + while (true) + { + std::unordered_map> sessions; + { + std::lock_guard lock_guard{sessions_m_}; + sessions.swap(sessions_); + } + + if (sessions.empty()) + { + break; + } + + for (auto &session : sessions) + { + session.second->CancelSession(); + } + } + return true; +} + +bool HttpClient::FinishAllSessions() noexcept +{ + // FinishSession may change sessions_, we can not change a container while iterating it. + while (true) + { + std::unordered_map> sessions; + { + std::lock_guard lock_guard{sessions_m_}; + sessions.swap(sessions_); + } + + if (sessions.empty()) + { + break; + } + + for (auto &session : sessions) + { + session.second->FinishSession(); + } + } + return true; +} + +void HttpClient::SetMaxSessionsPerConnection(std::size_t max_requests_per_connection) noexcept +{ + max_sessions_per_connection_ = max_requests_per_connection; +} + +void HttpClient::CleanupSession(uint64_t session_id) +{ + std::shared_ptr session; + { + std::lock_guard lock_guard{sessions_m_}; + auto it = sessions_.find(session_id); + if (it != sessions_.end()) + { + session = it->second; + sessions_.erase(it); + } + } + + { + std::lock_guard lock_guard{session_ids_m_}; + pending_to_add_session_ids_.erase(session_id); + + if (session) + { + if (pending_to_remove_session_handles_.end() != + pending_to_remove_session_handles_.find(session_id)) + { + pending_to_remove_sessions_.emplace_back(std::move(session)); + } + else if (session->IsSessionActive() && session->GetOperation()) + { + session->FinishOperation(); + } + } + } +} + +void HttpClient::MaybeSpawnBackgroundThread() +{ + std::lock_guard lock_guard{background_thread_m_}; + if (background_thread_) + { + return; + } + + background_thread_.reset(new std::thread( + [](HttpClient *self) { + int still_running = 1; + while (true) + { + CURLMsg *msg; + int queued; + CURLMcode mc = curl_multi_perform(self->multi_handle_, &still_running); + // According to https://curl.se/libcurl/c/curl_multi_perform.html, when mc is not OK, we + // can not curl_multi_perform it again + if (mc != CURLM_OK) + { + self->resetMultiHandle(); + } + else if (still_running) + { + // curl_multi_poll is added from libcurl 7.66.0, before 7.68.0, we can only wait util + // timeout to do the rest jobs +#if LIBCURL_VERSION_NUM >= 0x074200 + /* wait for activity, timeout or "nothing" */ + mc = curl_multi_poll(self->multi_handle_, nullptr, 0, + static_cast(self->scheduled_delay_milliseconds_.count()), + nullptr); +#else + mc = curl_multi_wait(self->multi_handle_, nullptr, 0, + static_cast(self->scheduled_delay_milliseconds_.count()), + nullptr); +#endif + } + + do + { + msg = curl_multi_info_read(self->multi_handle_, &queued); + if (msg == nullptr) + { + break; + } + + if (msg->msg == CURLMSG_DONE) + { + CURL *easy_handle = msg->easy_handle; + CURLcode result = msg->data.result; + Session *session = nullptr; + curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, &session); + // If it's already moved into pending_to_remove_session_handles_, we just ingore this + // message. + if (nullptr != session && session->GetOperation()) + { + // Session can not be destroyed when calling PerformCurlMessage + auto hold_session = session->shared_from_this(); + session->GetOperation()->PerformCurlMessage(result); + } + } + } while (true); + + // Abort all pending easy handles + if (self->doAbortSessions()) + { + still_running = 1; + } + + // Remove all pending easy handles + if (self->doRemoveSessions()) + { + still_running = 1; + } + + // Add all pending easy handles + if (self->doAddSessions()) + { + still_running = 1; + } + + if (still_running == 0) + { + std::lock_guard lock_guard{self->background_thread_m_}; + // Double check, make sure no more pending sessions after locking background thread + // management + + // Abort all pending easy handles + if (self->doAbortSessions()) + { + still_running = 1; + } + + // Remove all pending easy handles + if (self->doRemoveSessions()) + { + still_running = 1; + } + + // Add all pending easy handles + if (self->doAddSessions()) + { + still_running = 1; + } + if (still_running == 0) + { + if (self->background_thread_) + { + self->background_thread_->detach(); + self->background_thread_.reset(); + } + break; + } + } + } + }, + this)); +} + +void HttpClient::ScheduleAddSession(uint64_t session_id) +{ + { + std::lock_guard lock_guard{session_ids_m_}; + pending_to_add_session_ids_.insert(session_id); + pending_to_remove_session_handles_.erase(session_id); + pending_to_abort_session_ids_.erase(session_id); + } + + wakeupBackgroundThread(); +} + +void HttpClient::ScheduleAbortSession(uint64_t session_id) +{ + { + std::lock_guard lock_guard{session_ids_m_}; + pending_to_abort_session_ids_.insert(session_id); + pending_to_add_session_ids_.erase(session_id); + } + + wakeupBackgroundThread(); +} + +void HttpClient::ScheduleRemoveSession(uint64_t session_id, HttpCurlEasyResource &&resource) +{ + { + std::lock_guard lock_guard{session_ids_m_}; + pending_to_add_session_ids_.erase(session_id); + pending_to_remove_session_handles_[session_id] = std::move(resource); + } + + wakeupBackgroundThread(); +} + +void HttpClient::wakeupBackgroundThread() +{ +// Before libcurl 7.68.0, we can only wait for timeout and do the rest jobs +// See https://curl.se/libcurl/c/curl_multi_wakeup.html +#if LIBCURL_VERSION_NUM >= 0x074400 + std::lock_guard lock_guard{multi_handle_m_}; + if (nullptr != multi_handle_) + { + curl_multi_wakeup(multi_handle_); + } +#endif +} + +bool HttpClient::doAddSessions() +{ + std::unordered_set pending_to_add_session_ids; + { + std::lock_guard session_id_lock_guard{session_ids_m_}; + pending_to_add_session_ids_.swap(pending_to_add_session_ids); + } + + bool has_data = false; + + std::lock_guard lock_guard{sessions_m_}; + for (auto &session_id : pending_to_add_session_ids) + { + auto session = sessions_.find(session_id); + if (session == sessions_.end()) + { + continue; + } + + if (!session->second->GetOperation()) + { + continue; + } + + CURL *easy_handle = session->second->GetOperation()->GetCurlEasyHandle(); + if (nullptr == easy_handle) + { + continue; + } + + curl_multi_add_handle(multi_handle_, easy_handle); + has_data = true; + } + + return has_data; +} + +bool HttpClient::doAbortSessions() +{ + std::list> abort_sessions; + std::unordered_set pending_to_abort_session_ids; + { + std::lock_guard session_id_lock_guard{session_ids_m_}; + pending_to_abort_session_ids_.swap(pending_to_abort_session_ids); + } + + { + std::lock_guard lock_guard{sessions_m_}; + for (auto &session_id : pending_to_abort_session_ids) + { + auto session = sessions_.find(session_id); + if (session == sessions_.end()) + { + continue; + } + + abort_sessions.push_back(session->second); + } + } + + bool has_data = false; + for (auto session : abort_sessions) + { + if (session->GetOperation()) + { + session->FinishOperation(); + has_data = true; + } + } + return has_data; +} + +bool HttpClient::doRemoveSessions() +{ + bool has_data = false; + bool should_continue; + do + { + std::unordered_map pending_to_remove_session_handles; + std::list> pending_to_remove_sessions; + { + std::lock_guard session_id_lock_guard{session_ids_m_}; + pending_to_remove_session_handles_.swap(pending_to_remove_session_handles); + pending_to_remove_sessions_.swap(pending_to_remove_sessions); + + // If user callback do not call CancelSession or FinishSession, We still need to remove it + // from sessions_ + std::lock_guard session_lock_guard{sessions_m_}; + for (auto &removing_handle : pending_to_remove_session_handles) + { + auto session = sessions_.find(removing_handle.first); + if (session != sessions_.end()) + { + pending_to_remove_sessions.emplace_back(std::move(session->second)); + sessions_.erase(session); + } + } + } + + for (auto &removing_handle : pending_to_remove_session_handles) + { + if (nullptr != removing_handle.second.headers_chunk) + { + curl_slist_free_all(removing_handle.second.headers_chunk); + } + + curl_multi_remove_handle(multi_handle_, removing_handle.second.easy_handle); + curl_easy_cleanup(removing_handle.second.easy_handle); + } + + for (auto &removing_session : pending_to_remove_sessions) + { + // This operation may add more pending_to_remove_session_handles + removing_session->FinishOperation(); + } + + should_continue = + !pending_to_remove_session_handles.empty() || !pending_to_remove_sessions.empty(); + if (should_continue) + { + has_data = true; + } + } while (should_continue); + + return has_data; +} + +void HttpClient::resetMultiHandle() +{ + std::list> sessions; + std::lock_guard session_lock_guard{sessions_m_}; + { + std::lock_guard session_id_lock_guard{session_ids_m_}; + for (auto &session : sessions_) + { + if (pending_to_add_session_ids_.end() == pending_to_add_session_ids_.find(session.first)) + { + sessions.push_back(session.second); + } + } + } + + for (auto &session : sessions) + { + session->CancelSession(); + session->FinishOperation(); + } + + doRemoveSessions(); + + // We will modify the multi_handle_, so we need to lock it + std::lock_guard lock_guard{multi_handle_m_}; + curl_multi_cleanup(multi_handle_); + + // Create a another multi handle to continue pending sessions + multi_handle_ = curl_multi_init(); +} + +} // namespace curl +} // namespace client +} // namespace http +} // namespace ext +OPENTELEMETRY_END_NAMESPACE diff --git a/ext/src/http/client/curl/http_operation_curl.cc b/ext/src/http/client/curl/http_operation_curl.cc new file mode 100644 index 0000000000..cb32e5f916 --- /dev/null +++ b/ext/src/http/client/curl/http_operation_curl.cc @@ -0,0 +1,684 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/ext/http/client/curl/http_operation_curl.h" + +#include "opentelemetry/ext/http/client/curl/http_client_curl.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace ext +{ +namespace http +{ +namespace client +{ +namespace curl +{ + +size_t HttpOperation::WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp) +{ + HttpOperation *self = reinterpret_cast(userp); + if (nullptr == self) + { + return 0; + } + + self->raw_response_.insert(self->raw_response_.end(), static_cast(contents), + static_cast(contents) + (size * nmemb)); + + if (self->WasAborted()) + { + return 0; + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connecting) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Connected); + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connected) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); + } + + return size * nmemb; +} + +size_t HttpOperation::WriteVectorHeaderCallback(void *ptr, size_t size, size_t nmemb, void *userp) +{ + HttpOperation *self = reinterpret_cast(userp); + if (nullptr == self) + { + return 0; + } + + const unsigned char *begin = (unsigned char *)(ptr); + const unsigned char *end = begin + size * nmemb; + self->response_headers_.insert(self->response_headers_.end(), begin, end); + + if (self->WasAborted()) + { + return 0; + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connecting) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Connected); + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connected) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); + } + + return size * nmemb; +} + +size_t HttpOperation::WriteVectorBodyCallback(void *ptr, size_t size, size_t nmemb, void *userp) +{ + HttpOperation *self = reinterpret_cast(userp); + if (nullptr == self) + { + return 0; + } + + const unsigned char *begin = (unsigned char *)(ptr); + const unsigned char *end = begin + size * nmemb; + self->response_body_.insert(self->response_body_.end(), begin, end); + + if (self->WasAborted()) + { + return 0; + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connecting) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Connected); + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connected) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); + } + + return size * nmemb; +} + +size_t HttpOperation::ReadMemoryCallback(char *buffer, size_t size, size_t nitems, void *userp) +{ + HttpOperation *self = reinterpret_cast(userp); + if (nullptr == self) + { + return 0; + } + + if (self->WasAborted()) + { + return CURL_READFUNC_ABORT; + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connecting) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Connected); + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connected) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); + } + + // EOF + if (self->request_nwrite_ >= self->request_body_.size()) + { + return 0; + } + + size_t nwrite = size * nitems; + if (nwrite > self->request_body_.size() - self->request_nwrite_) + { + nwrite = self->request_body_.size() - self->request_nwrite_; + } + + memcpy(buffer, &self->request_body_[self->request_nwrite_], nwrite); + self->request_nwrite_ += nwrite; + return nwrite; +} + +#if LIBCURL_VERSION_NUM >= 0x075000 +int HttpOperation::PreRequestCallback(void *clientp, char *, char *, int, int) +{ + HttpOperation *self = reinterpret_cast(clientp); + if (nullptr == self) + { + return CURL_PREREQFUNC_ABORT; + } + + if (self->GetSessionState() == opentelemetry::ext::http::client::SessionState::Connecting) + { + self->DispatchEvent(opentelemetry::ext::http::client::SessionState::Connected); + } + + if (self->WasAborted()) + { + return CURL_PREREQFUNC_ABORT; + } + + return CURL_PREREQFUNC_OK; +} +#endif + +#if LIBCURL_VERSION_NUM >= 0x072000 +int HttpOperation::OnProgressCallback(void *clientp, + curl_off_t dltotal, + curl_off_t dlnow, + curl_off_t ultotal, + curl_off_t ulnow) +{ + HttpOperation *self = reinterpret_cast(clientp); + if (nullptr == self) + { + return -1; + } + + if (self->WasAborted()) + { + return -1; + } + + // CURL_PROGRESSFUNC_CONTINUE is added in 7.68.0 +# if defined(CURL_PROGRESSFUNC_CONTINUE) + return CURL_PROGRESSFUNC_CONTINUE; +# else + return 0; +# endif +} +#else +int HttpOperation::OnProgressCallback(void *clientp, + double dltotal, + double dlnow, + double ultotal, + double ulnow) +{ + HttpOperation *self = reinterpret_cast(clientp); + if (nullptr == self) + { + return -1; + } + + if (self->WasAborted()) + { + return -1; + } + + return 0; +} +#endif + +void HttpOperation::DispatchEvent(opentelemetry::ext::http::client::SessionState type, + std::string reason) +{ + if (event_handle_ != nullptr) + { + event_handle_->OnEvent(type, reason); + } + + session_state_ = type; +} + +HttpOperation::HttpOperation(opentelemetry::ext::http::client::Method method, + std::string url, + opentelemetry::ext::http::client::EventHandler *event_handle, + // Default empty headers and empty request body + const opentelemetry::ext::http::client::Headers &request_headers, + const opentelemetry::ext::http::client::Body &request_body, + // Default connectivity and response size options + bool is_raw_response, + std::chrono::milliseconds http_conn_timeout, + bool reuse_connection) + : is_aborted_(false), + is_finished_(false), + is_cleaned_(false), + // Optional connection params + is_raw_response_(is_raw_response), + reuse_connection_(reuse_connection), + http_conn_timeout_(http_conn_timeout), + // Result + last_curl_result_(CURLE_OK), + event_handle_(event_handle), + method_(method), + url_(url), + // Local vars + request_headers_(request_headers), + request_body_(request_body), + request_nwrite_(0), + session_state_(opentelemetry::ext::http::client::SessionState::Created), + response_code_(0) +{ + /* get a curl handle */ + curl_resource_.easy_handle = curl_easy_init(); + if (!curl_resource_.easy_handle) + { + last_curl_result_ = CURLE_FAILED_INIT; + DispatchEvent(opentelemetry::ext::http::client::SessionState::CreateFailed, + curl_easy_strerror(last_curl_result_)); + return; + } + + // Specify our custom headers + if (!this->request_headers_.empty()) + { + for (auto &kv : this->request_headers_) + { + std::string header = std::string(kv.first); + header += ": "; + header += std::string(kv.second); + curl_resource_.headers_chunk = + curl_slist_append(curl_resource_.headers_chunk, header.c_str()); + } + } + + DispatchEvent(opentelemetry::ext::http::client::SessionState::Created); +} + +HttpOperation::~HttpOperation() +{ + // Given the request has not been aborted we should wait for completion here + // This guarantees the lifetime of this request. + switch (GetSessionState()) + { + case opentelemetry::ext::http::client::SessionState::Connecting: + case opentelemetry::ext::http::client::SessionState::Connected: + case opentelemetry::ext::http::client::SessionState::Sending: { + if (async_data_ && async_data_->result_future.valid()) + { + if (async_data_->callback_thread != std::this_thread::get_id()) + { + async_data_->result_future.wait(); + last_curl_result_ = async_data_->result_future.get(); + } + } + break; + } + default: + break; + } + + Cleanup(); +} + +void HttpOperation::Finish() +{ + if (is_finished_.exchange(true, std::memory_order_acq_rel)) + { + return; + } + + if (async_data_ && async_data_->result_future.valid()) + { + // We should not wait in callback from Cleanup() + if (async_data_->callback_thread != std::this_thread::get_id()) + { + async_data_->result_future.wait(); + last_curl_result_ = async_data_->result_future.get(); + } + } +} + +void HttpOperation::Cleanup() +{ + if (is_cleaned_.exchange(true, std::memory_order_acq_rel)) + { + return; + } + + switch (GetSessionState()) + { + case opentelemetry::ext::http::client::SessionState::Created: + case opentelemetry::ext::http::client::SessionState::Connecting: + case opentelemetry::ext::http::client::SessionState::Connected: + case opentelemetry::ext::http::client::SessionState::Sending: { + DispatchEvent(opentelemetry::ext::http::client::SessionState::Cancelled, + curl_easy_strerror(last_curl_result_)); + break; + } + default: + break; + } + + std::function callback; + + // Only cleanup async once even in recursive calls + if (async_data_) + { + // Just reset and move easy_handle to owner if in async mode + if (async_data_->session != nullptr) + { + auto session = async_data_->session; + async_data_->session = nullptr; + + if (curl_resource_.easy_handle != nullptr) + { + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PRIVATE, NULL); + curl_easy_reset(curl_resource_.easy_handle); + } + session->GetHttpClient().ScheduleRemoveSession(session->GetSessionId(), + std::move(curl_resource_)); + } + + callback.swap(async_data_->callback); + if (callback) + { + async_data_->callback_thread = std::this_thread::get_id(); + callback(*this); + async_data_->callback_thread = std::thread::id(); + } + + // Set value to promise to continue Finish() + if (true == async_data_->is_promise_running.exchange(false, std::memory_order_acq_rel)) + { + async_data_->result_promise.set_value(last_curl_result_); + } + + return; + } + + // Sync mode + if (curl_resource_.easy_handle != nullptr) + { + curl_easy_cleanup(curl_resource_.easy_handle); + curl_resource_.easy_handle = nullptr; + } + + if (curl_resource_.headers_chunk != nullptr) + { + curl_slist_free_all(curl_resource_.headers_chunk); + curl_resource_.headers_chunk = nullptr; + } +} + +CURLcode HttpOperation::Setup() +{ + if (!curl_resource_.easy_handle) + { + return CURLE_FAILED_INIT; + } + + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_VERBOSE, 0); + + // Specify target URL + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_URL, url_.c_str()); + + // TODO: support ssl cert verification for https request + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_SSL_VERIFYPEER, 0); // 1L + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_SSL_VERIFYHOST, 0); // 2L + + if (curl_resource_.headers_chunk != nullptr) + { + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_HTTPHEADER, curl_resource_.headers_chunk); + } + + // TODO: control local port to use + // curl_easy_setopt(curl, CURLOPT_LOCALPORT, dcf_port); + + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_TIMEOUT_MS, http_conn_timeout_.count()); + + // abort if slower than 4kb/sec during 30 seconds + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_LOW_SPEED_TIME, 30L); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_LOW_SPEED_LIMIT, 4096); + if (reuse_connection_) + { + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_FRESH_CONNECT, 0L); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_FORBID_REUSE, 0L); + } + else + { + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_FRESH_CONNECT, 1L); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_FORBID_REUSE, 1L); + } + + if (is_raw_response_) + { + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_HEADER, true); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_WRITEFUNCTION, + (void *)&HttpOperation::WriteMemoryCallback); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_WRITEDATA, (void *)this); + } + else + { + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_WRITEFUNCTION, + (void *)&HttpOperation::WriteVectorBodyCallback); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_WRITEDATA, (void *)this); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_HEADERFUNCTION, + (void *)&HttpOperation::WriteVectorHeaderCallback); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_HEADERDATA, (void *)this); + } + + // TODO: only two methods supported for now - POST and GET + if (method_ == opentelemetry::ext::http::client::Method::Post) + { + // Request buffer + const curl_off_t req_size = static_cast(request_body_.size()); + // POST + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_POST, 1L); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_POSTFIELDS, NULL); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_POSTFIELDSIZE_LARGE, req_size); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_READFUNCTION, + (void *)&HttpOperation::ReadMemoryCallback); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_READDATA, (void *)this); + } + else if (method_ == opentelemetry::ext::http::client::Method::Get) + { + // GET + } + else + { + return CURLE_UNSUPPORTED_PROTOCOL; + } + +#if LIBCURL_VERSION_NUM >= 0x072000 + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_XFERINFOFUNCTION, + (void *)&HttpOperation::OnProgressCallback); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_XFERINFODATA, (void *)this); +#else + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PROGRESSFUNCTION, + (void *)&HttpOperation::OnProgressCallback); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PROGRESSDATA, (void *)this); +#endif + +#if LIBCURL_VERSION_NUM >= 0x075000 + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PREREQFUNCTION, + (void *)&HttpOperation::PreRequestCallback); + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PREREQDATA, (void *)this); +#endif + + return CURLE_OK; +} + +CURLcode HttpOperation::Send() +{ + // If it is async sending, just return error + if (async_data_ && async_data_->is_promise_running.load(std::memory_order_acquire)) + { + return CURLE_FAILED_INIT; + } + + ReleaseResponse(); + + last_curl_result_ = Setup(); + if (last_curl_result_ != CURLE_OK) + { + DispatchEvent(opentelemetry::ext::http::client::SessionState::ConnectFailed, + curl_easy_strerror(last_curl_result_)); + return last_curl_result_; + } + + // Perform initial connect, handling the timeout if needed + // We can not use CURLOPT_CONNECT_ONLY because it will disable the reuse of connections. + DispatchEvent(opentelemetry::ext::http::client::SessionState::Connecting); + is_finished_.store(false, std::memory_order_release); + is_aborted_.store(false, std::memory_order_release); + is_cleaned_.store(false, std::memory_order_release); + + CURLcode code = curl_easy_perform(curl_resource_.easy_handle); + PerformCurlMessage(code); + if (CURLE_OK != code) + { + return code; + } + + return code; +} + +CURLcode HttpOperation::SendAsync(Session *session, std::function callback) +{ + if (nullptr == session) + { + return CURLE_FAILED_INIT; + } + + if (async_data_ && async_data_->is_promise_running.load(std::memory_order_acquire)) + { + return CURLE_FAILED_INIT; + } + else + { + async_data_.reset(new AsyncData()); + async_data_->is_promise_running.store(false, std::memory_order_release); + async_data_->session = nullptr; + } + + ReleaseResponse(); + + CURLcode code = Setup(); + last_curl_result_ = code; + if (code != CURLE_OK) + { + return code; + } + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_PRIVATE, session); + + DispatchEvent(opentelemetry::ext::http::client::SessionState::Connecting); + is_finished_.store(false, std::memory_order_release); + is_aborted_.store(false, std::memory_order_release); + is_cleaned_.store(false, std::memory_order_release); + + async_data_->session = session; + if (false == async_data_->is_promise_running.exchange(true, std::memory_order_acq_rel)) + { + async_data_->result_promise = std::promise(); + async_data_->result_future = async_data_->result_promise.get_future(); + } + async_data_->callback = std::move(callback); + + session->GetHttpClient().ScheduleAddSession(session->GetSessionId()); + return code; +} + +Headers HttpOperation::GetResponseHeaders() +{ + Headers result; + if (response_headers_.size() == 0) + return result; + + std::stringstream ss; + std::string headers((const char *)&response_headers_[0], response_headers_.size()); + ss.str(headers); + + std::string header; + while (std::getline(ss, header, '\n')) + { + // TODO - Regex below crashes with out-of-memory on CI docker container, so + // switching to string comparison. Need to debug and revert back. + + /*std::smatch match; + std::regex http_headers_regex(http_header_regexp); + if (std::regex_search(header, match, http_headers_regex)) + result.insert(std::pair( + static_cast(match[1]), static_cast(match[2]))); + */ + size_t pos = header.find(": "); + if (pos != std::string::npos) + result.insert( + std::pair(header.substr(0, pos), header.substr(pos + 2))); + } + return result; +} + +void HttpOperation::ReleaseResponse() +{ + response_headers_.clear(); + response_body_.clear(); + raw_response_.clear(); +} + +void HttpOperation::Abort() +{ + is_aborted_.store(true, std::memory_order_release); + if (curl_resource_.easy_handle != nullptr) + { + // Enable progress callback to abort from polling thread + curl_easy_setopt(curl_resource_.easy_handle, CURLOPT_NOPROGRESS, 0L); + if (async_data_ && nullptr != async_data_->session) + { + async_data_->session->GetHttpClient().ScheduleAbortSession( + async_data_->session->GetSessionId()); + } + } +} + +void HttpOperation::PerformCurlMessage(CURLcode code) +{ + last_curl_result_ = code; + if (code != CURLE_OK) + { + switch (GetSessionState()) + { + case opentelemetry::ext::http::client::SessionState::Connecting: { + DispatchEvent(opentelemetry::ext::http::client::SessionState::ConnectFailed, + curl_easy_strerror(code)); // couldn't connect - stage 1 + break; + } + case opentelemetry::ext::http::client::SessionState::Connected: + case opentelemetry::ext::http::client::SessionState::Sending: { + if (GetSessionState() == opentelemetry::ext::http::client::SessionState::Connected) + { + DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); + } + + DispatchEvent(opentelemetry::ext::http::client::SessionState::SendFailed, + curl_easy_strerror(code)); + } + default: + break; + } + } + else if (curl_resource_.easy_handle != nullptr) + { + curl_easy_getinfo(curl_resource_.easy_handle, CURLINFO_RESPONSE_CODE, &response_code_); + } + + // Transform state + if (GetSessionState() == opentelemetry::ext::http::client::SessionState::Connecting) + { + DispatchEvent(opentelemetry::ext::http::client::SessionState::Connected); + } + + if (GetSessionState() == opentelemetry::ext::http::client::SessionState::Connected) + { + DispatchEvent(opentelemetry::ext::http::client::SessionState::Sending); + } + + if (GetSessionState() == opentelemetry::ext::http::client::SessionState::Sending) + { + DispatchEvent(opentelemetry::ext::http::client::SessionState::Response); + } + + // Cleanup and unbind easy handle from multi handle, and finish callback + Cleanup(); +} + +} // namespace curl +} // namespace client +} // namespace http +} // namespace ext +OPENTELEMETRY_END_NAMESPACE diff --git a/ext/src/http/client/nosend/http_client_nosend.cc b/ext/src/http/client/nosend/http_client_nosend.cc index c2b1c6acf9..021af33095 100644 --- a/ext/src/http/client/nosend/http_client_nosend.cc +++ b/ext/src/http/client/nosend/http_client_nosend.cc @@ -63,6 +63,33 @@ bool Session::FinishSession() noexcept return true; } +HttpClient::HttpClient() +{ + session_ = std::shared_ptr{new Session(*this)}; +} + +std::shared_ptr HttpClient::CreateSession( + nostd::string_view) noexcept +{ + return session_; +} + +bool HttpClient::CancelAllSessions() noexcept +{ + session_->CancelSession(); + return true; +} + +bool HttpClient::FinishAllSessions() noexcept +{ + session_->FinishSession(); + return true; +} + +void HttpClient::SetMaxSessionsPerConnection(std::size_t max_requests_per_connection) noexcept {} + +void HttpClient::CleanupSession(uint64_t session_id) {} + } // namespace nosend } // namespace client } // namespace http diff --git a/ext/test/http/curl_http_test.cc b/ext/test/http/curl_http_test.cc index f8d248bae4..0bb5aa09a8 100644 --- a/ext/test/http/curl_http_test.cc +++ b/ext/test/http/curl_http_test.cc @@ -25,12 +25,27 @@ namespace nostd = opentelemetry::nostd; class CustomEventHandler : public http_client::EventHandler { public: - virtual void OnResponse(http_client::Response &response) noexcept override{}; + virtual void OnResponse(http_client::Response &response) noexcept override + { + got_response_ = true; + }; virtual void OnEvent(http_client::SessionState state, nostd::string_view reason) noexcept override - {} + { + switch (state) + { + case http_client::SessionState::ConnectFailed: + case http_client::SessionState::SendFailed: { + is_called_ = true; + break; + } + default: + break; + } + } virtual void OnConnecting(const http_client::SSLCertificate &) noexcept {} virtual ~CustomEventHandler() = default; bool is_called_ = false; + bool got_response_ = false; }; class GetEventHandler : public CustomEventHandler @@ -39,7 +54,8 @@ class GetEventHandler : public CustomEventHandler { ASSERT_EQ(200, response.GetStatusCode()); ASSERT_EQ(response.GetBody().size(), 0); - is_called_ = true; + is_called_ = true; + got_response_ = true; }; }; @@ -50,10 +66,34 @@ class PostEventHandler : public CustomEventHandler ASSERT_EQ(200, response.GetStatusCode()); std::string body(response.GetBody().begin(), response.GetBody().end()); ASSERT_EQ(body, "{'k1':'v1', 'k2':'v2', 'k3':'v3'}"); - is_called_ = true; + is_called_ = true; + got_response_ = true; } }; +class FinishInCallbackHandler : public CustomEventHandler +{ +public: + FinishInCallbackHandler(std::shared_ptr session) : session_(session) {} + + void OnResponse(http_client::Response &response) noexcept override + { + ASSERT_EQ(200, response.GetStatusCode()); + ASSERT_EQ(response.GetBody().size(), 0); + is_called_ = true; + got_response_ = true; + + if (session_) + { + session_->FinishSession(); + session_.reset(); + } + } + +private: + std::shared_ptr session_; +}; + class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS::HttpRequestCallback { protected: @@ -108,7 +148,7 @@ class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS::HttpRe response.headers["Content-Type"] = "text/plain"; response_status = 200; } - if (request.uri == "/post/") + else if (request.uri == "/post/") { std::unique_lock lk(mtx_requests); received_requests_.push_back(request); @@ -125,8 +165,10 @@ class BasicCurlHttpTests : public ::testing::Test, public HTTP_SERVER_NS::HttpRe bool waitForRequests(unsigned timeOutSec, unsigned expected_count = 1) { std::unique_lock lk(mtx_requests); - if (cv_got_events.wait_for(lk, std::chrono::milliseconds(1000 * timeOutSec), - [&] { return received_requests_.size() >= expected_count; })) + if (cv_got_events.wait_for(lk, std::chrono::milliseconds(1000 * timeOutSec), [&] { + // + return received_requests_.size() >= expected_count; + })) { return true; } @@ -196,12 +238,12 @@ TEST_F(BasicCurlHttpTests, SendGetRequest) auto session = session_manager->CreateSession("http://127.0.0.1:19000"); auto request = session->CreateRequest(); request->SetUri("get/"); - GetEventHandler *handler = new GetEventHandler(); - session->SendRequest(*handler); + auto handler = std::make_shared(); + session->SendRequest(handler); ASSERT_TRUE(waitForRequests(30, 1)); session->FinishSession(); ASSERT_TRUE(handler->is_called_); - delete handler; + ASSERT_TRUE(handler->got_response_); } TEST_F(BasicCurlHttpTests, SendPostRequest) @@ -219,16 +261,15 @@ TEST_F(BasicCurlHttpTests, SendPostRequest) http_client::Body body = {b, b + strlen(b)}; request->SetBody(body); request->AddHeader("Content-Type", "text/plain"); - PostEventHandler *handler = new PostEventHandler(); - session->SendRequest(*handler); + auto handler = std::make_shared(); + session->SendRequest(handler); ASSERT_TRUE(waitForRequests(30, 1)); session->FinishSession(); ASSERT_TRUE(handler->is_called_); + ASSERT_TRUE(handler->got_response_); session_manager->CancelAllSessions(); session_manager->FinishAllSessions(); - - delete handler; } TEST_F(BasicCurlHttpTests, RequestTimeout) @@ -240,11 +281,11 @@ TEST_F(BasicCurlHttpTests, RequestTimeout) auto session = session_manager->CreateSession("222.222.222.200:19000"); // Non Existing address auto request = session->CreateRequest(); request->SetUri("get/"); - GetEventHandler *handler = new GetEventHandler(); - session->SendRequest(*handler); + auto handler = std::make_shared(); + session->SendRequest(handler); session->FinishSession(); - ASSERT_FALSE(handler->is_called_); - delete handler; + ASSERT_TRUE(handler->is_called_); + ASSERT_FALSE(handler->got_response_); } TEST_F(BasicCurlHttpTests, CurlHttpOperations) @@ -257,16 +298,16 @@ TEST_F(BasicCurlHttpTests, CurlHttpOperations) http_client::Headers headers = { {"name1", "value1_1"}, {"name1", "value1_2"}, {"name2", "value3"}, {"name3", "value3"}}; - curl::HttpOperation http_operations1(http_client::Method::Head, "/get", handler, - curl::RequestMode::Async, headers, body, true); + curl::HttpOperation http_operations1(http_client::Method::Head, "/get", handler, headers, body, + true); http_operations1.Send(); - curl::HttpOperation http_operations2(http_client::Method::Get, "/get", handler, - curl::RequestMode::Async, headers, body, true); + curl::HttpOperation http_operations2(http_client::Method::Get, "/get", handler, headers, body, + true); http_operations2.Send(); - curl::HttpOperation http_operations3(http_client::Method::Get, "/get", handler, - curl::RequestMode::Async, headers, body, false); + curl::HttpOperation http_operations3(http_client::Method::Get, "/get", handler, headers, body, + false); http_operations3.Send(); delete handler; } @@ -323,3 +364,154 @@ TEST_F(BasicCurlHttpTests, GetBaseUri) ASSERT_EQ(std::static_pointer_cast(session)->GetBaseUri(), "http://127.0.0.1:31339/"); } + +TEST_F(BasicCurlHttpTests, SendGetRequestAsync) +{ + curl::HttpClient http_client; + + for (int round = 0; round < 2; ++round) + { + received_requests_.clear(); + static constexpr const unsigned batch_count = 5; + std::shared_ptr sessions[batch_count]; + std::shared_ptr handlers[batch_count]; + for (unsigned i = 0; i < batch_count; ++i) + { + sessions[i] = http_client.CreateSession("http://127.0.0.1:19000/get/"); + auto request = sessions[i]->CreateRequest(); + request->SetMethod(http_client::Method::Get); + request->SetUri("get/"); + + handlers[i] = std::make_shared(); + + // Lock mtx_requests to prevent response, we will check IsSessionActive() in the end + std::unique_lock lock_requests(mtx_requests); + sessions[i]->SendRequest(handlers[i]); + ASSERT_TRUE(sessions[i]->IsSessionActive()); + } + + ASSERT_TRUE(waitForRequests(30, batch_count)); + + for (unsigned i = 0; i < batch_count; ++i) + { + sessions[i]->FinishSession(); + ASSERT_FALSE(sessions[i]->IsSessionActive()); + + ASSERT_TRUE(handlers[i]->is_called_); + ASSERT_TRUE(handlers[i]->got_response_); + } + + http_client.WaitBackgroundThreadExit(); + } +} + +TEST_F(BasicCurlHttpTests, SendGetRequestAsyncTimeout) +{ + received_requests_.clear(); + curl::HttpClient http_client; + + static constexpr const unsigned batch_count = 5; + std::shared_ptr sessions[batch_count]; + std::shared_ptr handlers[batch_count]; + for (unsigned i = 0; i < batch_count; ++i) + { + sessions[i] = http_client.CreateSession("http://222.222.222.200:19000/get/"); + auto request = sessions[i]->CreateRequest(); + request->SetMethod(http_client::Method::Get); + request->SetUri("get/"); + request->SetTimeoutMs(std::chrono::milliseconds(256)); + + handlers[i] = std::make_shared(); + + // Lock mtx_requests to prevent response, we will check IsSessionActive() in the end + std::unique_lock lock_requests(mtx_requests); + sessions[i]->SendRequest(handlers[i]); + ASSERT_TRUE(sessions[i]->IsSessionActive()); + } + + for (unsigned i = 0; i < batch_count; ++i) + { + sessions[i]->FinishSession(); + ASSERT_FALSE(sessions[i]->IsSessionActive()); + + ASSERT_TRUE(handlers[i]->is_called_); + ASSERT_FALSE(handlers[i]->got_response_); + } +} + +TEST_F(BasicCurlHttpTests, SendPostRequestAsync) +{ + curl::HttpClient http_client; + + for (int round = 0; round < 2; ++round) + { + received_requests_.clear(); + auto handler = std::make_shared(); + + static constexpr const unsigned batch_count = 5; + std::shared_ptr sessions[batch_count]; + for (auto &session : sessions) + { + session = http_client.CreateSession("http://127.0.0.1:19000/post/"); + auto request = session->CreateRequest(); + request->SetMethod(http_client::Method::Post); + request->SetUri("post/"); + + // Lock mtx_requests to prevent response, we will check IsSessionActive() in the end + std::unique_lock lock_requests(mtx_requests); + session->SendRequest(handler); + ASSERT_TRUE(session->IsSessionActive()); + } + + ASSERT_TRUE(waitForRequests(30, batch_count)); + + for (auto &session : sessions) + { + session->FinishSession(); + ASSERT_FALSE(session->IsSessionActive()); + } + + ASSERT_TRUE(handler->is_called_); + ASSERT_TRUE(handler->got_response_); + + http_client.WaitBackgroundThreadExit(); + } +} + +TEST_F(BasicCurlHttpTests, FinishInAsyncCallback) +{ + curl::HttpClient http_client; + + for (int round = 0; round < 2; ++round) + { + received_requests_.clear(); + static constexpr const unsigned batch_count = 5; + std::shared_ptr sessions[batch_count]; + std::shared_ptr handlers[batch_count]; + for (unsigned i = 0; i < batch_count; ++i) + { + sessions[i] = http_client.CreateSession("http://127.0.0.1:19000/get/"); + auto request = sessions[i]->CreateRequest(); + request->SetMethod(http_client::Method::Get); + request->SetUri("get/"); + + handlers[i] = std::make_shared(sessions[i]); + + // Lock mtx_requests to prevent response, we will check IsSessionActive() in the end + std::unique_lock lock_requests(mtx_requests); + sessions[i]->SendRequest(handlers[i]); + ASSERT_TRUE(sessions[i]->IsSessionActive()); + } + + http_client.WaitBackgroundThreadExit(); + ASSERT_TRUE(waitForRequests(300, batch_count)); + + for (unsigned i = 0; i < batch_count; ++i) + { + ASSERT_FALSE(sessions[i]->IsSessionActive()); + + ASSERT_TRUE(handlers[i]->is_called_); + ASSERT_TRUE(handlers[i]->got_response_); + } + } +} diff --git a/ext/test/w3c_tracecontext_test/main.cc b/ext/test/w3c_tracecontext_test/main.cc index 79aa4c9169..ca54475540 100644 --- a/ext/test/w3c_tracecontext_test/main.cc +++ b/ext/test/w3c_tracecontext_test/main.cc @@ -100,7 +100,7 @@ class NoopEventHandler : public http_client::EventHandler // Sends an HTTP POST request to the given url, with the given body. void send_request(curl::HttpClient &client, const std::string &url, const std::string &body) { - static std::unique_ptr handler(new NoopEventHandler()); + static std::shared_ptr handler(new NoopEventHandler()); auto request_span = get_tracer()->StartSpan(__func__); trace_api::Scope scope(request_span); @@ -126,7 +126,7 @@ void send_request(curl::HttpClient &client, const std::string &url, const std::s request->AddHeader(hdr.first, hdr.second); } - session->SendRequest(*handler); + session->SendRequest(handler); session->FinishSession(); } diff --git a/sdk/include/opentelemetry/sdk/logs/async_batch_log_processor.h b/sdk/include/opentelemetry/sdk/logs/async_batch_log_processor.h new file mode 100644 index 0000000000..7cc6af8738 --- /dev/null +++ b/sdk/include/opentelemetry/sdk/logs/async_batch_log_processor.h @@ -0,0 +1,104 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#ifdef ENABLE_LOGS_PREVIEW +# ifdef ENABLE_ASYNC_EXPORT + +# include "opentelemetry/sdk/common/circular_buffer.h" +# include "opentelemetry/sdk/logs/batch_log_processor.h" +# include "opentelemetry/sdk/logs/exporter.h" + +# include +# include +# include +# include +# include +# include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ + +namespace logs +{ + +/** + * Struct to hold batch SpanProcessor options. + */ +struct AsyncBatchLogProcessorOptions : public BatchLogProcessorOptions +{ + /* Denotes the maximum number of async exports to continue + */ + size_t max_export_async = 8; +}; + +/** + * This is an implementation of the LogProcessor which creates batches of finished logs and passes + * the export-friendly log data representations to the configured LogExporter. + */ +class AsyncBatchLogProcessor : public BatchLogProcessor +{ +public: + /** + * Creates a batch log processor by configuring the specified exporter and other parameters + * as per the official, language-agnostic opentelemetry specs. + * + * @param exporter - The backend exporter to pass the logs to + * @param options - The batch SpanProcessor options. + */ + explicit AsyncBatchLogProcessor(std::unique_ptr &&exporter, + const AsyncBatchLogProcessorOptions &options); + + /** + * Shuts down the processor and does any cleanup required. Completely drains the buffer/queue of + * all its logs and passes them to the exporter. Any subsequent calls to + * ForceFlush or Shutdown will return immediately without doing anything. + * + * NOTE: Timeout functionality not supported yet. + */ + bool Shutdown( + std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override; + + /** + * Class destructor which invokes the Shutdown() method. + */ + virtual ~AsyncBatchLogProcessor(); + +private: + /** + * Exports all logs to the configured exporter. + * + */ + void Export() override; + + struct ExportDataStorage + { + std::queue export_ids; + std::vector export_ids_flag; + + std::condition_variable async_export_waker; + std::mutex async_export_data_m; + }; + std::shared_ptr export_data_storage_; + + const size_t max_export_async_; + static constexpr size_t kInvalidExportId = static_cast(-1); + + /** + * @brief Notify completion of shutdown and force flush. This may be called from the any thread at + * any time + * + * @param notify_force_flush Flag to indicate whether to notify force flush completion. + * @param synchronization_data Synchronization data to be notified. + */ + static void NotifyCompletion(bool notify_force_flush, + const std::shared_ptr &synchronization_data, + const std::shared_ptr &export_data_storage); +}; + +} // namespace logs +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE +# endif +#endif diff --git a/sdk/include/opentelemetry/sdk/logs/batch_log_processor.h b/sdk/include/opentelemetry/sdk/logs/batch_log_processor.h index 1b6d443c8a..2a781b937c 100644 --- a/sdk/include/opentelemetry/sdk/logs/batch_log_processor.h +++ b/sdk/include/opentelemetry/sdk/logs/batch_log_processor.h @@ -10,6 +10,8 @@ # include # include +# include +# include # include OPENTELEMETRY_BEGIN_NAMESPACE @@ -19,6 +21,27 @@ namespace sdk namespace logs { +/** + * Struct to hold batch SpanProcessor options. + */ +struct BatchLogProcessorOptions +{ + /** + * The maximum buffer/queue size. After the size is reached, spans are + * dropped. + */ + size_t max_queue_size = 2048; + + /* The time interval between two consecutive exports. */ + std::chrono::milliseconds schedule_delay_millis = std::chrono::milliseconds(5000); + + /** + * The maximum batch size of every export. It must be smaller or + * equal to max_queue_size. + */ + size_t max_export_batch_size = 512; +}; + /** * This is an implementation of the LogProcessor which creates batches of finished logs and passes * the export-friendly log data representations to the configured LogExporter. @@ -43,6 +66,16 @@ class BatchLogProcessor : public LogProcessor const std::chrono::milliseconds scheduled_delay_millis = std::chrono::milliseconds(5000), const size_t max_export_batch_size = 512); + /** + * Creates a batch log processor by configuring the specified exporter and other parameters + * as per the official, language-agnostic opentelemetry specs. + * + * @param exporter - The backend exporter to pass the logs to + * @param options - The batch SpanProcessor options. + */ + explicit BatchLogProcessor(std::unique_ptr &&exporter, + const BatchLogProcessorOptions &options); + /** Makes a new recordable **/ std::unique_ptr MakeRecordable() noexcept override; @@ -74,9 +107,9 @@ class BatchLogProcessor : public LogProcessor /** * Class destructor which invokes the Shutdown() method. */ - virtual ~BatchLogProcessor() override; + virtual ~BatchLogProcessor(); -private: +protected: /** * The background routine performed by the worker thread. */ @@ -85,12 +118,8 @@ class BatchLogProcessor : public LogProcessor /** * Exports all logs to the configured exporter. * - * @param was_force_flush_called - A flag to check if the current export is the result - * of a call to ForceFlush method. If true, then we have to - * notify the main thread to wake it up in the ForceFlush - * method. */ - void Export(const bool was_for_flush_called); + virtual void Export(); /** * Called when Shutdown() is invoked. Completely drains the queue of all log records and @@ -98,6 +127,32 @@ class BatchLogProcessor : public LogProcessor */ void DrainQueue(); + struct SynchronizationData + { + /* Synchronization primitives */ + std::condition_variable cv, force_flush_cv; + std::mutex cv_m, force_flush_cv_m, shutdown_m; + + /* Important boolean flags to handle the workflow of the processor */ + std::atomic is_force_wakeup_background_worker; + std::atomic is_force_flush_pending; + std::atomic is_force_flush_notified; + std::atomic is_shutdown; + }; + + /** + * @brief Notify completion of shutdown and force flush. This may be called from the any thread at + * any time + * + * @param notify_force_flush Flag to indicate whether to notify force flush completion. + * @param synchronization_data Synchronization data to be notified. + */ + static void NotifyCompletion(bool notify_force_flush, + const std::shared_ptr &synchronization_data); + + void GetWaitAdjustedTime(std::chrono::microseconds &timeout, + std::chrono::time_point &start_time); + /* The configured backend log exporter */ std::unique_ptr exporter_; @@ -105,18 +160,10 @@ class BatchLogProcessor : public LogProcessor const size_t max_queue_size_; const std::chrono::milliseconds scheduled_delay_millis_; const size_t max_export_batch_size_; - - /* Synchronization primitives */ - std::condition_variable cv_, force_flush_cv_; - std::mutex cv_m_, force_flush_cv_m_, shutdown_m_; - /* The buffer/queue to which the ended logs are added */ common::CircularBuffer buffer_; - /* Important boolean flags to handle the workflow of the processor */ - std::atomic is_shutdown_{false}; - std::atomic is_force_flush_{false}; - std::atomic is_force_flush_notified_{false}; + std::shared_ptr synchronization_data_; /* The background worker thread */ std::thread worker_thread_; diff --git a/sdk/include/opentelemetry/sdk/logs/exporter.h b/sdk/include/opentelemetry/sdk/logs/exporter.h index 85c58e9f12..5c28d73538 100644 --- a/sdk/include/opentelemetry/sdk/logs/exporter.h +++ b/sdk/include/opentelemetry/sdk/logs/exporter.h @@ -46,6 +46,17 @@ class LogExporter virtual sdk::common::ExportResult Export( const nostd::span> &records) noexcept = 0; +# ifdef ENABLE_ASYNC_EXPORT + /** + * Exports asynchronously the batch of log records to their export destination + * @param records a span of unique pointers to log records + * @param result_callback callback function accepting ExportResult as argument + */ + virtual void Export( + const nostd::span> &records, + std::function &&result_callback) noexcept = 0; +# endif + /** * Marks the exporter as ShutDown and cleans up any resources as required. * Shutdown should be called only once for each Exporter instance. diff --git a/sdk/include/opentelemetry/sdk/logs/simple_log_processor.h b/sdk/include/opentelemetry/sdk/logs/simple_log_processor.h index cc3aec47b2..a92380cd01 100644 --- a/sdk/include/opentelemetry/sdk/logs/simple_log_processor.h +++ b/sdk/include/opentelemetry/sdk/logs/simple_log_processor.h @@ -28,7 +28,12 @@ class SimpleLogProcessor : public LogProcessor { public: - explicit SimpleLogProcessor(std::unique_ptr &&exporter); + explicit SimpleLogProcessor(std::unique_ptr &&exporter +# ifdef ENABLE_ASYNC_EXPORT + , + bool is_export_async = false +# endif + ); virtual ~SimpleLogProcessor() = default; std::unique_ptr MakeRecordable() noexcept override; @@ -48,6 +53,10 @@ class SimpleLogProcessor : public LogProcessor opentelemetry::common::SpinLockMutex lock_; // The atomic boolean flag to ensure the ShutDown() function is only called once std::atomic_flag shutdown_latch_ = ATOMIC_FLAG_INIT; + +# ifdef ENABLE_ASYNC_EXPORT + bool is_export_async_ = false; +# endif }; } // namespace logs } // namespace sdk diff --git a/sdk/include/opentelemetry/sdk/metrics/state/sync_metric_storage.h b/sdk/include/opentelemetry/sdk/metrics/state/sync_metric_storage.h index 278f7dbe3f..37f485997c 100644 --- a/sdk/include/opentelemetry/sdk/metrics/state/sync_metric_storage.h +++ b/sdk/include/opentelemetry/sdk/metrics/state/sync_metric_storage.h @@ -107,6 +107,11 @@ class SyncMetricStorage : public MetricStorage, public WritableMetricStorage // hashmap to maintain the metrics for delta collection (i.e, collection since last Collect call) std::unique_ptr attributes_hashmap_; + // unreported metrics stash for all the collectors + std::unordered_map>> + unreported_metrics_; + // last reported metrics stash for all the collectors. + std::unordered_map last_reported_metrics_; const AttributesProcessor *attributes_processor_; std::function()> create_default_aggregation_; nostd::shared_ptr exemplar_reservoir_; diff --git a/sdk/include/opentelemetry/sdk/metrics/view/attributes_processor.h b/sdk/include/opentelemetry/sdk/metrics/view/attributes_processor.h index d82607357f..fdc4e35c53 100644 --- a/sdk/include/opentelemetry/sdk/metrics/view/attributes_processor.h +++ b/sdk/include/opentelemetry/sdk/metrics/view/attributes_processor.h @@ -23,8 +23,6 @@ class AttributesProcessor // @returns The processed attributes virtual MetricAttributes process( const opentelemetry::common::KeyValueIterable &attributes) const noexcept = 0; - - virtual ~AttributesProcessor() = default; }; /** diff --git a/sdk/include/opentelemetry/sdk/trace/async_batch_span_processor.h b/sdk/include/opentelemetry/sdk/trace/async_batch_span_processor.h new file mode 100644 index 0000000000..4cbb234e71 --- /dev/null +++ b/sdk/include/opentelemetry/sdk/trace/async_batch_span_processor.h @@ -0,0 +1,103 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#ifdef ENABLE_ASYNC_EXPORT + +# include "opentelemetry/sdk/common/circular_buffer.h" +# include "opentelemetry/sdk/trace/batch_span_processor.h" +# include "opentelemetry/sdk/trace/exporter.h" + +# include +# include +# include +# include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ + +namespace trace +{ + +/** + * Struct to hold batch SpanProcessor options. + */ +struct AsyncBatchSpanProcessorOptions : public BatchSpanProcessorOptions +{ + /* Denotes the maximum number of async exports to continue + */ + size_t max_export_async = 8; +}; + +/** + * This is an implementation of the SpanProcessor which creates batches of finished spans and passes + * the export-friendly span data representations to the configured SpanExporter. + */ +class AsyncBatchSpanProcessor : public BatchSpanProcessor +{ +public: + /** + * Creates a batch span processor by configuring the specified exporter and other parameters + * as per the official, language-agnostic opentelemetry specs. + * + * @param exporter - The backend exporter to pass the ended spans to. + * @param options - The batch SpanProcessor options. + */ + AsyncBatchSpanProcessor(std::unique_ptr &&exporter, + const AsyncBatchSpanProcessorOptions &options); + + /** + * Shuts down the processor and does any cleanup required. Completely drains the buffer/queue of + * all its ended spans and passes them to the exporter. Any subsequent calls to OnStart, OnEnd, + * ForceFlush or Shutdown will return immediately without doing anything. + * + * NOTE: Timeout functionality not supported yet. + */ + bool Shutdown( + std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override; + + /** + * Class destructor which invokes the Shutdown() method. The Shutdown() method is supposed to be + * invoked when the Tracer is shutdown (as per other languages), but the C++ Tracer only takes + * shared ownership of the processor, and thus doesn't call Shutdown (as the processor might be + * shared with other Tracers). + */ + virtual ~AsyncBatchSpanProcessor(); + +private: + /** + * Exports all ended spans to the configured exporter. + * + */ + void Export() override; + + struct ExportDataStorage + { + std::queue export_ids; + std::vector export_ids_flag; + + std::condition_variable async_export_waker; + std::mutex async_export_data_m; + }; + std::shared_ptr export_data_storage_; + + const size_t max_export_async_; + static constexpr size_t kInvalidExportId = static_cast(-1); + + /** + * @brief Notify completion of shutdown and force flush. This may be called from the any thread at + * any time + * + * @param notify_force_flush Flag to indicate whether to notify force flush completion. + * @param synchronization_data Synchronization data to be notified. + */ + static void NotifyCompletion(bool notify_force_flush, + const std::shared_ptr &synchronization_data, + const std::shared_ptr &export_data_storage); +}; + +} // namespace trace +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE +#endif \ No newline at end of file diff --git a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h index d25ff2d950..f18fc78346 100644 --- a/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/batch_span_processor.h @@ -105,9 +105,9 @@ class BatchSpanProcessor : public SpanProcessor * shared ownership of the processor, and thus doesn't call Shutdown (as the processor might be * shared with other Tracers). */ - ~BatchSpanProcessor(); + virtual ~BatchSpanProcessor(); -private: +protected: /** * The background routine performed by the worker thread. */ @@ -116,12 +116,8 @@ class BatchSpanProcessor : public SpanProcessor /** * Exports all ended spans to the configured exporter. * - * @param was_force_flush_called - A flag to check if the current export is the result - * of a call to ForceFlush method. If true, then we have to - * notify the main thread to wake it up in the ForceFlush - * method. */ - void Export(const bool was_for_flush_called); + virtual void Export(); /** * Called when Shutdown() is invoked. Completely drains the queue of all its ended spans and @@ -129,6 +125,31 @@ class BatchSpanProcessor : public SpanProcessor */ void DrainQueue(); + struct SynchronizationData + { + /* Synchronization primitives */ + std::condition_variable cv, force_flush_cv; + std::mutex cv_m, force_flush_cv_m, shutdown_m; + + /* Important boolean flags to handle the workflow of the processor */ + std::atomic is_force_wakeup_background_worker; + std::atomic is_force_flush_pending; + std::atomic is_force_flush_notified; + std::atomic is_shutdown; + }; + + /** + * @brief Notify completion of shutdown and force flush. This may be called from the any thread at + * any time + * + * @param notify_force_flush Flag to indicate whether to notify force flush completion. + * @param synchronization_data Synchronization data to be notified. + */ + static void NotifyCompletion(bool notify_force_flush, + const std::shared_ptr &synchronization_data); + + void GetWaitAdjustedTime(std::chrono::microseconds &timeout, + std::chrono::time_point &start_time); /* The configured backend exporter */ std::unique_ptr exporter_; @@ -137,17 +158,10 @@ class BatchSpanProcessor : public SpanProcessor const std::chrono::milliseconds schedule_delay_millis_; const size_t max_export_batch_size_; - /* Synchronization primitives */ - std::condition_variable cv_, force_flush_cv_; - std::mutex cv_m_, force_flush_cv_m_, shutdown_m_; - /* The buffer/queue to which the ended spans are added */ common::CircularBuffer buffer_; - /* Important boolean flags to handle the workflow of the processor */ - std::atomic is_shutdown_{false}; - std::atomic is_force_flush_{false}; - std::atomic is_force_flush_notified_{false}; + std::shared_ptr synchronization_data_; /* The background worker thread */ std::thread worker_thread_; diff --git a/sdk/include/opentelemetry/sdk/trace/exporter.h b/sdk/include/opentelemetry/sdk/trace/exporter.h index 5826b5f454..b58bbc8717 100644 --- a/sdk/include/opentelemetry/sdk/trace/exporter.h +++ b/sdk/include/opentelemetry/sdk/trace/exporter.h @@ -42,6 +42,17 @@ class SpanExporter const nostd::span> &spans) noexcept = 0; +#ifdef ENABLE_ASYNC_EXPORT + /** + * Exports a batch of span recordables asynchronously. + * @param spans a span of unique pointers to span recordables + * @param result_callback callback function accepting ExportResult as argument + */ + virtual void Export( + const nostd::span> &spans, + std::function &&result_callback) noexcept = 0; +#endif + /** * Shut down the exporter. * @param timeout an optional timeout. diff --git a/sdk/include/opentelemetry/sdk/trace/simple_processor.h b/sdk/include/opentelemetry/sdk/trace/simple_processor.h index accc685965..576f9ebe9f 100644 --- a/sdk/include/opentelemetry/sdk/trace/simple_processor.h +++ b/sdk/include/opentelemetry/sdk/trace/simple_processor.h @@ -31,8 +31,17 @@ class SimpleSpanProcessor : public SpanProcessor * Initialize a simple span processor. * @param exporter the exporter used by the span processor */ - explicit SimpleSpanProcessor(std::unique_ptr &&exporter) noexcept + explicit SimpleSpanProcessor(std::unique_ptr &&exporter +#ifdef ENABLE_ASYNC_EXPORT + , + bool is_export_async = false +#endif + ) noexcept : exporter_(std::move(exporter)) +#ifdef ENABLE_ASYNC_EXPORT + , + is_export_async_(is_export_async) +#endif {} std::unique_ptr MakeRecordable() noexcept override @@ -48,11 +57,26 @@ class SimpleSpanProcessor : public SpanProcessor { nostd::span> batch(&span, 1); const std::lock_guard locked(lock_); - if (exporter_->Export(batch) == sdk::common::ExportResult::kFailure) +#ifdef ENABLE_ASYNC_EXPORT + if (is_export_async_ == false) { - /* Once it is defined how the SDK does logging, an error should be - * logged in this case. */ +#endif + if (exporter_->Export(batch) == sdk::common::ExportResult::kFailure) + { + /* Once it is defined how the SDK does logging, an error should be + * logged in this case. */ + } +#ifdef ENABLE_ASYNC_EXPORT } + else + { + exporter_->Export(batch, [](sdk::common::ExportResult result) { + /* Log the result + */ + return true; + }); + } +#endif } bool ForceFlush( @@ -78,6 +102,9 @@ class SimpleSpanProcessor : public SpanProcessor std::unique_ptr exporter_; opentelemetry::common::SpinLockMutex lock_; std::atomic_flag shutdown_latch_ = ATOMIC_FLAG_INIT; +#ifdef ENABLE_ASYNC_EXPORT + bool is_export_async_ = false; +#endif }; } // namespace trace } // namespace sdk diff --git a/sdk/src/logs/CMakeLists.txt b/sdk/src/logs/CMakeLists.txt index 20f13324e7..1b1db9e778 100644 --- a/sdk/src/logs/CMakeLists.txt +++ b/sdk/src/logs/CMakeLists.txt @@ -4,6 +4,7 @@ add_library( logger.cc simple_log_processor.cc batch_log_processor.cc + async_batch_log_processor.cc logger_context.cc multi_log_processor.cc multi_recordable.cc) diff --git a/sdk/src/logs/async_batch_log_processor.cc b/sdk/src/logs/async_batch_log_processor.cc new file mode 100644 index 0000000000..5d99754c12 --- /dev/null +++ b/sdk/src/logs/async_batch_log_processor.cc @@ -0,0 +1,194 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifdef ENABLE_LOGS_PREVIEW +# ifdef ENABLE_ASYNC_EXPORT +# include "opentelemetry/sdk/logs/async_batch_log_processor.h" +# include "opentelemetry/common/spin_lock_mutex.h" + +# include + +using opentelemetry::sdk::common::AtomicUniquePtr; +using opentelemetry::sdk::common::CircularBufferRange; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace logs +{ +AsyncBatchLogProcessor::AsyncBatchLogProcessor(std::unique_ptr &&exporter, + const AsyncBatchLogProcessorOptions &options) + : BatchLogProcessor(std::move(exporter), options), + max_export_async_(options.max_export_async), + export_data_storage_(std::make_shared()) +{ + export_data_storage_->export_ids_flag.resize(max_export_async_, true); + for (int i = 1; i <= max_export_async_; i++) + { + export_data_storage_->export_ids.push(i); + } +} + +void AsyncBatchLogProcessor::Export() +{ + do + { + std::vector> records_arr; + size_t num_records_to_export; + bool notify_force_flush = + synchronization_data_->is_force_flush_pending.exchange(false, std::memory_order_acq_rel); + if (notify_force_flush) + { + num_records_to_export = buffer_.size(); + } + else + { + num_records_to_export = + buffer_.size() >= max_export_batch_size_ ? max_export_batch_size_ : buffer_.size(); + } + + if (num_records_to_export == 0) + { + NotifyCompletion(notify_force_flush, synchronization_data_, export_data_storage_); + break; + } + + buffer_.Consume(num_records_to_export, + [&](CircularBufferRange> range) noexcept { + range.ForEach([&](AtomicUniquePtr &ptr) { + std::unique_ptr swap_ptr = std::unique_ptr(nullptr); + ptr.Swap(swap_ptr); + records_arr.push_back(std::unique_ptr(swap_ptr.release())); + return true; + }); + }); + + size_t id = kInvalidExportId; + { + std::unique_lock lock(export_data_storage_->async_export_data_m); + export_data_storage_->async_export_waker.wait_for(lock, scheduled_delay_millis_, [this] { + return export_data_storage_->export_ids.size() > 0; + }); + if (export_data_storage_->export_ids.size() > 0) + { + id = export_data_storage_->export_ids.front(); + export_data_storage_->export_ids.pop(); + export_data_storage_->export_ids_flag[id - 1] = false; + } + } + if (id != kInvalidExportId) + { + std::weak_ptr export_data_watcher = export_data_storage_; + std::weak_ptr synchronization_data_watcher = synchronization_data_; + exporter_->Export( + nostd::span>(records_arr.data(), records_arr.size()), + [notify_force_flush, synchronization_data_watcher, export_data_watcher, + id](sdk::common::ExportResult result) { + // TODO: Print result + if (synchronization_data_watcher.expired()) + { + return true; + } + if (export_data_watcher.expired()) + { + return true; + } + bool is_already_notified = false; + auto synchronization_data = synchronization_data_watcher.lock(); + auto export_data = export_data_watcher.lock(); + { + std::unique_lock lk(export_data->async_export_data_m); + // In case callback is called more than once due to some bug in exporter + // we need to ensure export_ids do not contain duplicate. + if (export_data->export_ids_flag[id - 1] == false) + { + export_data->export_ids.push(id); + export_data->export_ids_flag[id - 1] = true; + } + else + { + is_already_notified = true; + } + } + if (is_already_notified == false) + { + NotifyCompletion(notify_force_flush, synchronization_data, export_data); + } + return true; + }); + } + } while (true); +} + +void AsyncBatchLogProcessor::NotifyCompletion( + bool notify_force_flush, + const std::shared_ptr &synchronization_data, + const std::shared_ptr &export_data_storage) +{ + BatchLogProcessor::NotifyCompletion(notify_force_flush, synchronization_data); + export_data_storage->async_export_waker.notify_all(); +} + +bool AsyncBatchLogProcessor::Shutdown(std::chrono::microseconds timeout) noexcept +{ + if (synchronization_data_->is_shutdown.load() == true) + { + return true; + } + auto start_time = std::chrono::system_clock::now(); + + std::lock_guard shutdown_guard{synchronization_data_->shutdown_m}; + bool already_shutdown = synchronization_data_->is_shutdown.exchange(true); + if (worker_thread_.joinable()) + { + synchronization_data_->is_force_wakeup_background_worker.store(true, std::memory_order_release); + synchronization_data_->cv.notify_one(); + worker_thread_.join(); + } + + timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout( + timeout, std::chrono::microseconds::zero()); + // wait for all async exports to complete and return if timeout reached. + { + std::unique_lock lock(export_data_storage_->async_export_data_m); + if (timeout <= std::chrono::microseconds::zero()) + { + auto is_wait = false; + while (!is_wait) + { + is_wait = export_data_storage_->async_export_waker.wait_for( + lock, scheduled_delay_millis_, + [this] { return export_data_storage_->export_ids.size() == max_export_async_; }); + } + } + else + { + export_data_storage_->async_export_waker.wait_for(lock, timeout, [this] { + return export_data_storage_->export_ids.size() == max_export_async_; + }); + } + } + + GetWaitAdjustedTime(timeout, start_time); + // Should only shutdown exporter ONCE. + if (!already_shutdown && exporter_ != nullptr) + { + return exporter_->Shutdown(timeout); + } + + return true; +} + +AsyncBatchLogProcessor::~AsyncBatchLogProcessor() +{ + if (synchronization_data_->is_shutdown.load() == false) + { + Shutdown(); + } +} + +} // namespace logs +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE +# endif +#endif diff --git a/sdk/src/logs/batch_log_processor.cc b/sdk/src/logs/batch_log_processor.cc index 9b20705b0a..af2f6cae59 100644 --- a/sdk/src/logs/batch_log_processor.cc +++ b/sdk/src/logs/batch_log_processor.cc @@ -3,6 +3,7 @@ #ifdef ENABLE_LOGS_PREVIEW # include "opentelemetry/sdk/logs/batch_log_processor.h" +# include "opentelemetry/common/spin_lock_mutex.h" # include using opentelemetry::sdk::common::AtomicUniquePtr; @@ -22,8 +23,30 @@ BatchLogProcessor::BatchLogProcessor(std::unique_ptr &&exporter, scheduled_delay_millis_(scheduled_delay_millis), max_export_batch_size_(max_export_batch_size), buffer_(max_queue_size_), + synchronization_data_(std::make_shared()), worker_thread_(&BatchLogProcessor::DoBackgroundWork, this) -{} +{ + synchronization_data_->is_force_wakeup_background_worker.store(false); + synchronization_data_->is_force_flush_pending.store(false); + synchronization_data_->is_force_flush_notified.store(false); + synchronization_data_->is_shutdown.store(false); +} + +BatchLogProcessor::BatchLogProcessor(std::unique_ptr &&exporter, + const BatchLogProcessorOptions &options) + : exporter_(std::move(exporter)), + max_queue_size_(options.max_queue_size), + scheduled_delay_millis_(options.schedule_delay_millis), + max_export_batch_size_(options.max_export_batch_size), + buffer_(options.max_queue_size), + synchronization_data_(std::make_shared()), + worker_thread_(&BatchLogProcessor::DoBackgroundWork, this) +{ + synchronization_data_->is_force_wakeup_background_worker.store(false); + synchronization_data_->is_force_flush_pending.store(false); + synchronization_data_->is_force_flush_notified.store(false); + synchronization_data_->is_shutdown.store(false); +} std::unique_ptr BatchLogProcessor::MakeRecordable() noexcept { @@ -32,7 +55,7 @@ std::unique_ptr BatchLogProcessor::MakeRecordable() noexcept void BatchLogProcessor::OnReceive(std::unique_ptr &&record) noexcept { - if (is_shutdown_.load() == true) + if (synchronization_data_->is_shutdown.load() == true) { return; } @@ -44,39 +67,83 @@ void BatchLogProcessor::OnReceive(std::unique_ptr &&record) noexcept // If the queue gets at least half full a preemptive notification is // sent to the worker thread to start a new export cycle. - if (buffer_.size() >= max_queue_size_ / 2) + size_t buffer_size = buffer_.size(); + if (buffer_size >= max_queue_size_ / 2 || buffer_size >= max_export_batch_size_) { // signal the worker thread - cv_.notify_one(); + synchronization_data_->is_force_wakeup_background_worker.store(true, std::memory_order_release); + synchronization_data_->cv.notify_one(); } } bool BatchLogProcessor::ForceFlush(std::chrono::microseconds timeout) noexcept { - if (is_shutdown_.load() == true) + if (synchronization_data_->is_shutdown.load() == true) { return false; } - is_force_flush_ = true; + // Now wait for the worker thread to signal back from the Export method + std::unique_lock lk_cv(synchronization_data_->force_flush_cv_m); - // Keep attempting to wake up the worker thread - while (is_force_flush_.load() == true) + synchronization_data_->is_force_flush_pending.store(true, std::memory_order_release); + auto break_condition = [this]() { + if (synchronization_data_->is_shutdown.load() == true) + { + return true; + } + + // Wake up the worker thread once. + if (synchronization_data_->is_force_flush_pending.load(std::memory_order_acquire)) + { + synchronization_data_->cv.notify_one(); + } + + return synchronization_data_->is_force_flush_notified.load(std::memory_order_acquire); + }; + + // Fix timeout to meet requirement of wait_for + timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout( + timeout, std::chrono::microseconds::zero()); + bool result; + if (timeout <= std::chrono::microseconds::zero()) + { + bool wait_result = false; + while (!wait_result) + { + // When is_force_flush_notified.store(true) and force_flush_cv.notify_all() is called + // between is_force_flush_pending.load() and force_flush_cv.wait(). We must not wait + // for ever + wait_result = synchronization_data_->force_flush_cv.wait_for(lk_cv, scheduled_delay_millis_, + break_condition); + } + result = true; + } + else { - cv_.notify_one(); + result = synchronization_data_->force_flush_cv.wait_for(lk_cv, timeout, break_condition); } - // Now wait for the worker thread to signal back from the Export method - std::unique_lock lk(force_flush_cv_m_); - while (is_force_flush_notified_.load() == false) + // If it's already signaled, we must wait util notified. + // We use a spin lock here + if (false == + synchronization_data_->is_force_flush_pending.exchange(false, std::memory_order_acq_rel)) { - force_flush_cv_.wait(lk); + for (int retry_waiting_times = 0; + false == synchronization_data_->is_force_flush_notified.load(std::memory_order_acquire); + ++retry_waiting_times) + { + opentelemetry::common::SpinLockMutex::fast_yield(); + if ((retry_waiting_times & 127) == 127) + { + std::this_thread::yield(); + } + } } - // Notify the worker thread - is_force_flush_notified_ = false; + synchronization_data_->is_force_flush_notified.store(false, std::memory_order_release); - return true; + return result; } void BatchLogProcessor::DoBackgroundWork() @@ -86,38 +153,26 @@ void BatchLogProcessor::DoBackgroundWork() while (true) { // Wait for `timeout` milliseconds - std::unique_lock lk(cv_m_); - cv_.wait_for(lk, timeout); + std::unique_lock lk(synchronization_data_->cv_m); + synchronization_data_->cv.wait_for(lk, timeout, [this] { + if (synchronization_data_->is_force_wakeup_background_worker.load(std::memory_order_acquire)) + { + return true; + } + + return !buffer_.empty(); + }); + synchronization_data_->is_force_wakeup_background_worker.store(false, + std::memory_order_release); - if (is_shutdown_.load() == true) + if (synchronization_data_->is_shutdown.load() == true) { DrainQueue(); return; } - bool was_force_flush_called = is_force_flush_.load(); - - // Check if this export was the result of a force flush. - if (was_force_flush_called == true) - { - // Since this export was the result of a force flush, signal the - // main thread that the worker thread has been notified - is_force_flush_ = false; - } - else - { - // If the buffer was empty during the entire `timeout` time interval, - // go back to waiting. If this was a spurious wake-up, we export only if - // `buffer_` is not empty. This is acceptable because batching is a best - // mechanism effort here. - if (buffer_.empty() == true) - { - continue; - } - } - auto start = std::chrono::steady_clock::now(); - Export(was_force_flush_called); + Export(); auto end = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(end - start); @@ -126,69 +181,117 @@ void BatchLogProcessor::DoBackgroundWork() } } -void BatchLogProcessor::Export(const bool was_force_flush_called) +void BatchLogProcessor::Export() { - std::vector> records_arr; + uint64_t current_pending; + uint64_t current_notified; + do + { + std::vector> records_arr; + size_t num_records_to_export; + bool notify_force_flush = + synchronization_data_->is_force_flush_pending.exchange(false, std::memory_order_acq_rel); + if (notify_force_flush) + { + num_records_to_export = buffer_.size(); + } + else + { + num_records_to_export = + buffer_.size() >= max_export_batch_size_ ? max_export_batch_size_ : buffer_.size(); + } + + if (num_records_to_export == 0) + { + NotifyCompletion(notify_force_flush, synchronization_data_); + break; + } + + buffer_.Consume(num_records_to_export, + [&](CircularBufferRange> range) noexcept { + range.ForEach([&](AtomicUniquePtr &ptr) { + std::unique_ptr swap_ptr = std::unique_ptr(nullptr); + ptr.Swap(swap_ptr); + records_arr.push_back(std::unique_ptr(swap_ptr.release())); + return true; + }); + }); - size_t num_records_to_export; + exporter_->Export( + nostd::span>(records_arr.data(), records_arr.size())); + NotifyCompletion(notify_force_flush, synchronization_data_); + } while (true); +} - if (was_force_flush_called == true) +void BatchLogProcessor::NotifyCompletion( + bool notify_force_flush, + const std::shared_ptr &synchronization_data) +{ + if (!synchronization_data) { - num_records_to_export = buffer_.size(); + return; } - else + + if (notify_force_flush) { - num_records_to_export = - buffer_.size() >= max_export_batch_size_ ? max_export_batch_size_ : buffer_.size(); + synchronization_data->is_force_flush_notified.store(true, std::memory_order_release); + synchronization_data->force_flush_cv.notify_one(); } +} - buffer_.Consume(num_records_to_export, - [&](CircularBufferRange> range) noexcept { - range.ForEach([&](AtomicUniquePtr &ptr) { - std::unique_ptr swap_ptr = std::unique_ptr(nullptr); - ptr.Swap(swap_ptr); - records_arr.push_back(std::unique_ptr(swap_ptr.release())); - return true; - }); - }); - - exporter_->Export( - nostd::span>(records_arr.data(), records_arr.size())); - - // Notify the main thread in case this export was the result of a force flush. - if (was_force_flush_called == true) +void BatchLogProcessor::DrainQueue() +{ + while (true) { - is_force_flush_notified_ = true; - while (is_force_flush_notified_.load() == true) + if (buffer_.empty() && + false == synchronization_data_->is_force_flush_pending.load(std::memory_order_acquire)) { - force_flush_cv_.notify_one(); + break; } + + Export(); } } -void BatchLogProcessor::DrainQueue() +void BatchLogProcessor::GetWaitAdjustedTime( + std::chrono::microseconds &timeout, + std::chrono::time_point &start_time) { - while (buffer_.empty() == false) + auto end_time = std::chrono::system_clock::now(); + auto offset = std::chrono::duration_cast(end_time - start_time); + start_time = end_time; + timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout( + timeout, std::chrono::microseconds::zero()); + if (timeout > offset && timeout > std::chrono::microseconds::zero()) { - Export(false); + timeout -= offset; + } + else + { + // Some module use zero as indefinite timeout.So we can not reset timeout to zero here + timeout = std::chrono::microseconds(1); } } bool BatchLogProcessor::Shutdown(std::chrono::microseconds timeout) noexcept { - std::lock_guard shutdown_guard{shutdown_m_}; - bool already_shutdown = is_shutdown_.exchange(true); + auto start_time = std::chrono::system_clock::now(); + + std::lock_guard shutdown_guard{synchronization_data_->shutdown_m}; + bool already_shutdown = synchronization_data_->is_shutdown.exchange(true); if (worker_thread_.joinable()) { - cv_.notify_one(); + synchronization_data_->is_force_wakeup_background_worker.store(true, std::memory_order_release); + synchronization_data_->cv.notify_one(); worker_thread_.join(); } + GetWaitAdjustedTime(timeout, start_time); // Should only shutdown exporter ONCE. if (!already_shutdown && exporter_ != nullptr) { - return exporter_->Shutdown(); + return exporter_->Shutdown(timeout); } return true; @@ -196,7 +299,7 @@ bool BatchLogProcessor::Shutdown(std::chrono::microseconds timeout) noexcept BatchLogProcessor::~BatchLogProcessor() { - if (is_shutdown_.load() == false) + if (synchronization_data_->is_shutdown.load() == false) { Shutdown(); } diff --git a/sdk/src/logs/simple_log_processor.cc b/sdk/src/logs/simple_log_processor.cc index 6e2fde9f14..844a84c6fb 100644 --- a/sdk/src/logs/simple_log_processor.cc +++ b/sdk/src/logs/simple_log_processor.cc @@ -16,8 +16,17 @@ namespace logs * Initialize a simple log processor. * @param exporter the configured exporter where log records are sent */ -SimpleLogProcessor::SimpleLogProcessor(std::unique_ptr &&exporter) +SimpleLogProcessor::SimpleLogProcessor(std::unique_ptr &&exporter +# ifdef ENABLE_ASYNC_EXPORT + , + bool is_export_async +# endif + ) : exporter_(std::move(exporter)) +# ifdef ENABLE_ASYNC_EXPORT + , + is_export_async_(is_export_async) +# endif {} std::unique_ptr SimpleLogProcessor::MakeRecordable() noexcept @@ -35,10 +44,25 @@ void SimpleLogProcessor::OnReceive(std::unique_ptr &&record) noexcep // Get lock to ensure Export() is never called concurrently const std::lock_guard locked(lock_); - if (exporter_->Export(batch) != sdk::common::ExportResult::kSuccess) +# ifdef ENABLE_ASYNC_EXPORT + if (is_export_async_ == false) { - /* Alert user of the failed export */ +# endif + if (exporter_->Export(batch) != sdk::common::ExportResult::kSuccess) + { + /* Alert user of the failed export */ + } +# ifdef ENABLE_ASYNC_EXPORT } + else + { + exporter_->Export(batch, [](sdk::common::ExportResult result) { + /* Log the result + */ + return true; + }); + } +# endif } /** * The simple processor does not have any log records to flush so this method is not used diff --git a/sdk/src/trace/CMakeLists.txt b/sdk/src/trace/CMakeLists.txt index ddef00fb42..bd906b1fab 100644 --- a/sdk/src/trace/CMakeLists.txt +++ b/sdk/src/trace/CMakeLists.txt @@ -5,6 +5,7 @@ add_library( tracer.cc span.cc batch_span_processor.cc + async_batch_span_processor.cc samplers/parent.cc samplers/trace_id_ratio.cc random_id_generator.cc) diff --git a/sdk/src/trace/async_batch_span_processor.cc b/sdk/src/trace/async_batch_span_processor.cc new file mode 100644 index 0000000000..e28fe34e70 --- /dev/null +++ b/sdk/src/trace/async_batch_span_processor.cc @@ -0,0 +1,187 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +#ifdef ENABLE_ASYNC_EXPORT + +# include "opentelemetry/sdk/trace/async_batch_span_processor.h" +# include "opentelemetry/common/spin_lock_mutex.h" + +# include +using opentelemetry::sdk::common::AtomicUniquePtr; +using opentelemetry::sdk::common::CircularBuffer; +using opentelemetry::sdk::common::CircularBufferRange; +using opentelemetry::trace::SpanContext; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace sdk +{ +namespace trace +{ + +AsyncBatchSpanProcessor::AsyncBatchSpanProcessor(std::unique_ptr &&exporter, + const AsyncBatchSpanProcessorOptions &options) + : BatchSpanProcessor(std::move(exporter), options), + export_data_storage_(std::make_shared()), + max_export_async_(options.max_export_async) +{ + export_data_storage_->export_ids_flag.resize(max_export_async_, true); + for (size_t i = 1; i <= max_export_async_; i++) + { + export_data_storage_->export_ids.push(i); + } +} + +void AsyncBatchSpanProcessor::Export() +{ + do + { + std::vector> spans_arr; + size_t num_records_to_export; + bool notify_force_flush = + synchronization_data_->is_force_flush_pending.exchange(false, std::memory_order_acq_rel); + if (notify_force_flush) + { + num_records_to_export = buffer_.size(); + } + else + { + num_records_to_export = + buffer_.size() >= max_export_batch_size_ ? max_export_batch_size_ : buffer_.size(); + } + + if (num_records_to_export == 0) + { + NotifyCompletion(notify_force_flush, synchronization_data_, export_data_storage_); + break; + } + buffer_.Consume(num_records_to_export, + [&](CircularBufferRange> range) noexcept { + range.ForEach([&](AtomicUniquePtr &ptr) { + std::unique_ptr swap_ptr = std::unique_ptr(nullptr); + ptr.Swap(swap_ptr); + spans_arr.push_back(std::unique_ptr(swap_ptr.release())); + return true; + }); + }); + + size_t id = kInvalidExportId; + { + std::unique_lock lock(export_data_storage_->async_export_data_m); + export_data_storage_->async_export_waker.wait_for(lock, schedule_delay_millis_, [this] { + return export_data_storage_->export_ids.size() > 0; + }); + if (export_data_storage_->export_ids.size() > 0) + { + id = export_data_storage_->export_ids.front(); + export_data_storage_->export_ids.pop(); + export_data_storage_->export_ids_flag[id - 1] = false; + } + } + if (id != kInvalidExportId) + { + std::weak_ptr export_data_watcher = export_data_storage_; + + std::weak_ptr synchronization_data_watcher = synchronization_data_; + exporter_->Export( + nostd::span>(spans_arr.data(), spans_arr.size()), + [notify_force_flush, synchronization_data_watcher, export_data_watcher, + id](sdk::common::ExportResult result) { + // TODO: Print result + if (synchronization_data_watcher.expired()) + { + return true; + } + + if (export_data_watcher.expired()) + { + return true; + } + + auto synchronization_data = synchronization_data_watcher.lock(); + auto export_data = export_data_watcher.lock(); + { + std::unique_lock lk(export_data->async_export_data_m); + if (export_data->export_ids_flag[id - 1] == false) + { + export_data->export_ids.push(id); + export_data->export_ids_flag[id - 1] = true; + } + } + NotifyCompletion(notify_force_flush, synchronization_data, export_data); + return true; + }); + } + } while (true); +} + +void AsyncBatchSpanProcessor::NotifyCompletion( + bool notify_force_flush, + const std::shared_ptr &synchronization_data, + const std::shared_ptr &export_data_storage) +{ + BatchSpanProcessor::NotifyCompletion(notify_force_flush, synchronization_data); + export_data_storage->async_export_waker.notify_all(); +} + +bool AsyncBatchSpanProcessor::Shutdown(std::chrono::microseconds timeout) noexcept +{ + if (synchronization_data_->is_shutdown.load() == true) + { + return true; + } + + auto start_time = std::chrono::system_clock::now(); + std::lock_guard shutdown_guard{synchronization_data_->shutdown_m}; + bool already_shutdown = synchronization_data_->is_shutdown.exchange(true); + + if (worker_thread_.joinable()) + { + synchronization_data_->is_force_wakeup_background_worker.store(true, std::memory_order_release); + synchronization_data_->cv.notify_one(); + worker_thread_.join(); + } + + timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout( + timeout, std::chrono::microseconds::zero()); + // wait for all async exports to complete and return if timeout reached. + { + std::unique_lock lock(export_data_storage_->async_export_data_m); + if (timeout <= std::chrono::microseconds::zero()) + { + auto is_wait = false; + while (!is_wait) + { + is_wait = export_data_storage_->async_export_waker.wait_for( + lock, schedule_delay_millis_, + [this] { return export_data_storage_->export_ids.size() == max_export_async_; }); + } + } + else + { + export_data_storage_->async_export_waker.wait_for(lock, timeout, [this] { + return export_data_storage_->export_ids.size() == max_export_async_; + }); + } + } + + GetWaitAdjustedTime(timeout, start_time); + // Should only shutdown exporter ONCE. + if (!already_shutdown && exporter_ != nullptr) + { + return exporter_->Shutdown(timeout); + } + + return true; +} + +AsyncBatchSpanProcessor::~AsyncBatchSpanProcessor() +{ + if (synchronization_data_->is_shutdown.load() == false) + { + Shutdown(); + } +} + +} // namespace trace +} // namespace sdk +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/sdk/src/trace/batch_span_processor.cc b/sdk/src/trace/batch_span_processor.cc index 0ab042b9ab..4609eae95f 100644 --- a/sdk/src/trace/batch_span_processor.cc +++ b/sdk/src/trace/batch_span_processor.cc @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "opentelemetry/sdk/trace/batch_span_processor.h" +#include "opentelemetry/common/spin_lock_mutex.h" #include using opentelemetry::sdk::common::AtomicUniquePtr; @@ -14,6 +15,7 @@ namespace sdk { namespace trace { + BatchSpanProcessor::BatchSpanProcessor(std::unique_ptr &&exporter, const BatchSpanProcessorOptions &options) : exporter_(std::move(exporter)), @@ -21,8 +23,14 @@ BatchSpanProcessor::BatchSpanProcessor(std::unique_ptr &&exporter, schedule_delay_millis_(options.schedule_delay_millis), max_export_batch_size_(options.max_export_batch_size), buffer_(max_queue_size_), + synchronization_data_(std::make_shared()), worker_thread_(&BatchSpanProcessor::DoBackgroundWork, this) -{} +{ + synchronization_data_->is_force_wakeup_background_worker.store(false); + synchronization_data_->is_force_flush_pending.store(false); + synchronization_data_->is_force_flush_notified.store(false); + synchronization_data_->is_shutdown.store(false); +} std::unique_ptr BatchSpanProcessor::MakeRecordable() noexcept { @@ -36,7 +44,7 @@ void BatchSpanProcessor::OnStart(Recordable &, const SpanContext &) noexcept void BatchSpanProcessor::OnEnd(std::unique_ptr &&span) noexcept { - if (is_shutdown_.load() == true) + if (synchronization_data_->is_shutdown.load() == true) { return; } @@ -48,39 +56,83 @@ void BatchSpanProcessor::OnEnd(std::unique_ptr &&span) noexcept // If the queue gets at least half full a preemptive notification is // sent to the worker thread to start a new export cycle. - if (buffer_.size() >= max_queue_size_ / 2) + size_t buffer_size = buffer_.size(); + if (buffer_size >= max_queue_size_ / 2 || buffer_size >= max_export_batch_size_) { // signal the worker thread - cv_.notify_one(); + synchronization_data_->cv.notify_one(); } } bool BatchSpanProcessor::ForceFlush(std::chrono::microseconds timeout) noexcept { - if (is_shutdown_.load() == true) + if (synchronization_data_->is_shutdown.load() == true) { return false; } - is_force_flush_ = true; + // Now wait for the worker thread to signal back from the Export method + std::unique_lock lk_cv(synchronization_data_->force_flush_cv_m); + + synchronization_data_->is_force_flush_pending.store(true, std::memory_order_release); + auto break_condition = [this]() { + if (synchronization_data_->is_shutdown.load() == true) + { + return true; + } + + // Wake up the worker thread once. + if (synchronization_data_->is_force_flush_pending.load(std::memory_order_acquire)) + { + synchronization_data_->is_force_wakeup_background_worker.store(true, + std::memory_order_release); + synchronization_data_->cv.notify_one(); + } - // Keep attempting to wake up the worker thread - while (is_force_flush_.load() == true) + return synchronization_data_->is_force_flush_notified.load(std::memory_order_acquire); + }; + + // Fix timeout to meet requirement of wait_for + timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout( + timeout, std::chrono::microseconds::zero()); + bool result; + if (timeout <= std::chrono::microseconds::zero()) { - cv_.notify_one(); + bool wait_result = false; + while (!wait_result) + { + // When is_force_flush_notified.store(true) and force_flush_cv.notify_all() is called + // between is_force_flush_pending.load() and force_flush_cv.wait(). We must not wait + // for ever + wait_result = synchronization_data_->force_flush_cv.wait_for(lk_cv, schedule_delay_millis_, + break_condition); + } + result = true; } - - // Now wait for the worker thread to signal back from the Export method - std::unique_lock lk(force_flush_cv_m_); - while (is_force_flush_notified_.load() == false) + else { - force_flush_cv_.wait(lk); + result = synchronization_data_->force_flush_cv.wait_for(lk_cv, timeout, break_condition); } - // Notify the worker thread - is_force_flush_notified_ = false; + // If it will be already signaled, we must wait util notified. + // We use a spin lock here + if (false == + synchronization_data_->is_force_flush_pending.exchange(false, std::memory_order_acq_rel)) + { + for (int retry_waiting_times = 0; + false == synchronization_data_->is_force_flush_notified.load(std::memory_order_acquire); + ++retry_waiting_times) + { + opentelemetry::common::SpinLockMutex::fast_yield(); + if ((retry_waiting_times & 127) == 127) + { + std::this_thread::yield(); + } + } + } + synchronization_data_->is_force_flush_notified.store(false, std::memory_order_release); - return true; + return result; } void BatchSpanProcessor::DoBackgroundWork() @@ -90,39 +142,26 @@ void BatchSpanProcessor::DoBackgroundWork() while (true) { // Wait for `timeout` milliseconds - std::unique_lock lk(cv_m_); - cv_.wait_for(lk, timeout); + std::unique_lock lk(synchronization_data_->cv_m); + synchronization_data_->cv.wait_for(lk, timeout, [this] { + if (synchronization_data_->is_force_wakeup_background_worker.load(std::memory_order_acquire)) + { + return true; + } + + return !buffer_.empty(); + }); + synchronization_data_->is_force_wakeup_background_worker.store(false, + std::memory_order_release); - if (is_shutdown_.load() == true) + if (synchronization_data_->is_shutdown.load() == true) { DrainQueue(); return; } - bool was_force_flush_called = is_force_flush_.load(); - - // Check if this export was the result of a force flush. - if (was_force_flush_called == true) - { - // Since this export was the result of a force flush, signal the - // main thread that the worker thread has been notified - is_force_flush_ = false; - } - else - { - // If the buffer was empty during the entire `timeout` time interval, - // go back to waiting. If this was a spurious wake-up, we export only if - // `buffer_` is not empty. This is acceptable because batching is a best - // mechanism effort here. - if (buffer_.empty() == true) - { - timeout = schedule_delay_millis_; - continue; - } - } - auto start = std::chrono::steady_clock::now(); - Export(was_force_flush_called); + Export(); auto end = std::chrono::steady_clock::now(); auto duration = std::chrono::duration_cast(end - start); @@ -131,68 +170,112 @@ void BatchSpanProcessor::DoBackgroundWork() } } -void BatchSpanProcessor::Export(const bool was_force_flush_called) +void BatchSpanProcessor::Export() { - std::vector> spans_arr; + do + { + std::vector> spans_arr; + size_t num_records_to_export; + bool notify_force_flush = + synchronization_data_->is_force_flush_pending.exchange(false, std::memory_order_acq_rel); + if (notify_force_flush) + { + num_records_to_export = buffer_.size(); + } + else + { + num_records_to_export = + buffer_.size() >= max_export_batch_size_ ? max_export_batch_size_ : buffer_.size(); + } + + if (num_records_to_export == 0) + { + NotifyCompletion(notify_force_flush, synchronization_data_); + break; + } + buffer_.Consume(num_records_to_export, + [&](CircularBufferRange> range) noexcept { + range.ForEach([&](AtomicUniquePtr &ptr) { + std::unique_ptr swap_ptr = std::unique_ptr(nullptr); + ptr.Swap(swap_ptr); + spans_arr.push_back(std::unique_ptr(swap_ptr.release())); + return true; + }); + }); - size_t num_spans_to_export; + exporter_->Export(nostd::span>(spans_arr.data(), spans_arr.size())); + NotifyCompletion(notify_force_flush, synchronization_data_); + } while (true); +} - if (was_force_flush_called == true) +void BatchSpanProcessor::NotifyCompletion( + bool notify_force_flush, + const std::shared_ptr &synchronization_data) +{ + if (!synchronization_data) { - num_spans_to_export = buffer_.size(); + return; } - else + + if (notify_force_flush) { - num_spans_to_export = - buffer_.size() >= max_export_batch_size_ ? max_export_batch_size_ : buffer_.size(); + synchronization_data->is_force_flush_notified.store(true, std::memory_order_release); + synchronization_data->force_flush_cv.notify_one(); } +} - buffer_.Consume(num_spans_to_export, - [&](CircularBufferRange> range) noexcept { - range.ForEach([&](AtomicUniquePtr &ptr) { - std::unique_ptr swap_ptr = std::unique_ptr(nullptr); - ptr.Swap(swap_ptr); - spans_arr.push_back(std::unique_ptr(swap_ptr.release())); - return true; - }); - }); - - exporter_->Export(nostd::span>(spans_arr.data(), spans_arr.size())); - - // Notify the main thread in case this export was the result of a force flush. - if (was_force_flush_called == true) +void BatchSpanProcessor::DrainQueue() +{ + while (true) { - is_force_flush_notified_ = true; - while (is_force_flush_notified_.load() == true) + if (buffer_.empty() && + false == synchronization_data_->is_force_flush_pending.load(std::memory_order_acquire)) { - force_flush_cv_.notify_one(); + break; } + + Export(); } } -void BatchSpanProcessor::DrainQueue() +void BatchSpanProcessor::GetWaitAdjustedTime( + std::chrono::microseconds &timeout, + std::chrono::time_point &start_time) { - while (buffer_.empty() == false) + auto end_time = std::chrono::system_clock::now(); + auto offset = std::chrono::duration_cast(end_time - start_time); + start_time = end_time; + timeout = opentelemetry::common::DurationUtil::AdjustWaitForTimeout( + timeout, std::chrono::microseconds::zero()); + if (timeout > offset && timeout > std::chrono::microseconds::zero()) + { + timeout -= offset; + } + else { - Export(false); + // Some module use zero as indefinite timeout.So we can not reset timeout to zero here + timeout = std::chrono::microseconds(1); } } bool BatchSpanProcessor::Shutdown(std::chrono::microseconds timeout) noexcept { - std::lock_guard shutdown_guard{shutdown_m_}; - bool already_shutdown = is_shutdown_.exchange(true); + auto start_time = std::chrono::system_clock::now(); + std::lock_guard shutdown_guard{synchronization_data_->shutdown_m}; + bool already_shutdown = synchronization_data_->is_shutdown.exchange(true); if (worker_thread_.joinable()) { - cv_.notify_one(); + synchronization_data_->is_force_wakeup_background_worker.store(true, std::memory_order_release); + synchronization_data_->cv.notify_one(); worker_thread_.join(); } + GetWaitAdjustedTime(timeout, start_time); // Should only shutdown exporter ONCE. if (!already_shutdown && exporter_ != nullptr) { - return exporter_->Shutdown(); + return exporter_->Shutdown(timeout); } return true; @@ -200,7 +283,7 @@ bool BatchSpanProcessor::Shutdown(std::chrono::microseconds timeout) noexcept BatchSpanProcessor::~BatchSpanProcessor() { - if (is_shutdown_.load() == false) + if (synchronization_data_->is_shutdown.load() == false) { Shutdown(); } diff --git a/sdk/test/logs/BUILD b/sdk/test/logs/BUILD index c8f051070f..f620eaf613 100644 --- a/sdk/test/logs/BUILD +++ b/sdk/test/logs/BUILD @@ -73,3 +73,18 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_test( + name = "async_batch_log_processor_test", + srcs = [ + "async_batch_log_processor_test.cc", + ], + tags = [ + "logs", + "test", + ], + deps = [ + "//sdk/src/logs", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/sdk/test/logs/CMakeLists.txt b/sdk/test/logs/CMakeLists.txt index 84b865d226..550b48edd8 100644 --- a/sdk/test/logs/CMakeLists.txt +++ b/sdk/test/logs/CMakeLists.txt @@ -1,5 +1,8 @@ -foreach(testname logger_provider_sdk_test logger_sdk_test log_record_test - simple_log_processor_test batch_log_processor_test) +foreach( + testname + logger_provider_sdk_test logger_sdk_test log_record_test + simple_log_processor_test batch_log_processor_test + async_batch_log_processor_test) add_executable(${testname} "${testname}.cc") target_link_libraries(${testname} ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} opentelemetry_logs) diff --git a/sdk/test/logs/async_batch_log_processor_test.cc b/sdk/test/logs/async_batch_log_processor_test.cc new file mode 100644 index 0000000000..3c71ce743e --- /dev/null +++ b/sdk/test/logs/async_batch_log_processor_test.cc @@ -0,0 +1,374 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +#ifndef ENABLE_ASYNC_EXPORT +# include +TEST(AsyncBatchLogProcessor, DummyTest) +{ + // For linking +} +#endif + +#ifdef ENABLE_LOGS_PREVIEW +# ifdef ENABLE_ASYNC_EXPORT + +# include "opentelemetry/sdk/logs/async_batch_log_processor.h" +# include "opentelemetry/sdk/logs/exporter.h" +# include "opentelemetry/sdk/logs/log_record.h" + +# include +# include +# include +# include +# include + +using namespace opentelemetry::sdk::logs; +using namespace opentelemetry::sdk::common; + +/** + * A sample log exporter + * for testing the batch log processor + */ +class MockLogExporter final : public LogExporter +{ +public: + MockLogExporter(std::shared_ptr>> logs_received, + std::shared_ptr> is_shutdown, + std::shared_ptr> is_export_completed, + const std::chrono::milliseconds export_delay = std::chrono::milliseconds(0), + int callback_count = 1) + : logs_received_(logs_received), + is_shutdown_(is_shutdown), + is_export_completed_(is_export_completed), + export_delay_(export_delay), + callback_count_(callback_count) + {} + + std::unique_ptr MakeRecordable() noexcept + { + return std::unique_ptr(new LogRecord()); + } + + // Export method stores the logs received into a shared list of record names + ExportResult Export( + const opentelemetry::nostd::span> &records) noexcept override + { + *is_export_completed_ = false; // Meant exclusively to test scheduled_delay_millis + + for (auto &record : records) + { + auto log = std::unique_ptr(static_cast(record.release())); + if (log != nullptr) + { + logs_received_->push_back(std::move(log)); + } + } + + *is_export_completed_ = true; + return ExportResult::kSuccess; + } + + void Export(const opentelemetry::nostd::span> &records, + std::function + &&result_callback) noexcept override + { + // We should keep the order of test records + auto result = Export(records); + async_threads_.emplace_back(std::make_shared( + [this, + result](std::function &&result_callback) { + for (int i = 0; i < callback_count_; i++) + { + result_callback(result); + } + }, + std::move(result_callback))); + } + + // toggles the boolean flag marking this exporter as shut down + bool Shutdown( + std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override + { + while (!async_threads_.empty()) + { + std::list> async_threads; + async_threads.swap(async_threads_); + for (auto &async_thread : async_threads) + { + if (async_thread && async_thread->joinable()) + { + async_thread->join(); + } + } + } + *is_shutdown_ = true; + return true; + } + +private: + std::shared_ptr>> logs_received_; + std::shared_ptr> is_shutdown_; + std::shared_ptr> is_export_completed_; + const std::chrono::milliseconds export_delay_; + std::list> async_threads_; + int callback_count_; +}; + +/** + * A fixture class for testing the BatchLogProcessor class that uses the TestExporter defined above. + */ +class AsyncBatchLogProcessorTest : public testing::Test // ::testing::Test +{ +public: + // returns a batch log processor that received a batch of log records, a shared pointer to a + // is_shutdown flag, and the processor configuration options (default if unspecified) + std::shared_ptr GetMockProcessor( + std::shared_ptr>> logs_received, + std::shared_ptr> is_shutdown, + std::shared_ptr> is_export_completed = + std::shared_ptr>(new std::atomic(false)), + const std::chrono::milliseconds export_delay = std::chrono::milliseconds(0), + const std::chrono::milliseconds scheduled_delay_millis = std::chrono::milliseconds(5000), + const size_t max_queue_size = 2048, + const size_t max_export_batch_size = 512, + const size_t max_export_async = 8, + int callback_count = 1) + { + AsyncBatchLogProcessorOptions options; + options.max_queue_size = max_queue_size; + options.schedule_delay_millis = scheduled_delay_millis; + options.max_export_batch_size = max_export_batch_size; + options.max_export_async = max_export_async; + return std::shared_ptr(new AsyncBatchLogProcessor( + std::unique_ptr(new MockLogExporter( + logs_received, is_shutdown, is_export_completed, export_delay, callback_count)), + options)); + } +}; + +TEST_F(AsyncBatchLogProcessorTest, TestAsyncShutdown) +{ + // initialize a batch log processor with the test exporter + std::shared_ptr>> logs_received( + new std::vector>); + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr> is_export_completed(new std::atomic(false)); + + const std::chrono::milliseconds export_delay(0); + const std::chrono::milliseconds scheduled_delay_millis(5000); + const size_t max_export_batch_size = 512; + const size_t max_queue_size = 2048; + const size_t max_export_async = 5; + + auto batch_processor = GetMockProcessor(logs_received, is_shutdown, is_export_completed, + export_delay, scheduled_delay_millis, max_queue_size, + max_export_batch_size, max_export_async); + + // Create a few test log records and send them to the processor + const int num_logs = 2048; + + for (int i = 0; i < num_logs; ++i) + { + auto log = batch_processor->MakeRecordable(); + log->SetBody("Log" + std::to_string(i)); + batch_processor->OnReceive(std::move(log)); + } + + // Test that shutting down the processor will first wait for the + // current batch of logs to be sent to the log exporter + // by checking the number of logs sent and the names of the logs sent + EXPECT_EQ(true, batch_processor->Shutdown()); + // It's safe to shutdown again + EXPECT_TRUE(batch_processor->Shutdown()); + + EXPECT_EQ(num_logs, logs_received->size()); + + // Assume logs are received by exporter in same order as sent by processor + for (int i = 0; i < num_logs; ++i) + { + EXPECT_EQ("Log" + std::to_string(i), logs_received->at(i)->GetBody()); + } + + // Also check that the processor is shut down at the end + EXPECT_TRUE(is_shutdown->load()); +} + +TEST_F(AsyncBatchLogProcessorTest, TestAsyncShutdownNoCallback) +{ + // initialize a batch log processor with the test exporter + std::shared_ptr>> logs_received( + new std::vector>); + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr> is_export_completed(new std::atomic(false)); + + const std::chrono::milliseconds export_delay(0); + const std::chrono::milliseconds scheduled_delay_millis(5000); + const size_t max_export_batch_size = 512; + const size_t max_queue_size = 2048; + const size_t max_export_async = 5; + + auto batch_processor = GetMockProcessor(logs_received, is_shutdown, is_export_completed, + export_delay, scheduled_delay_millis, max_queue_size, + max_export_batch_size, max_export_async, 0); + + // Create a few test log records and send them to the processor + const int num_logs = 2048; + + for (int i = 0; i < num_logs; ++i) + { + auto log = batch_processor->MakeRecordable(); + log->SetBody("Log" + std::to_string(i)); + batch_processor->OnReceive(std::move(log)); + } + + EXPECT_EQ(true, batch_processor->Shutdown(std::chrono::milliseconds(5000))); + // It's safe to shutdown again + EXPECT_TRUE(batch_processor->Shutdown()); + + // Also check that the processor is shut down at the end + EXPECT_TRUE(is_shutdown->load()); +} + +TEST_F(AsyncBatchLogProcessorTest, TestAsyncForceFlush) +{ + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr>> logs_received( + new std::vector>); + std::shared_ptr> is_export_completed(new std::atomic(false)); + + const std::chrono::milliseconds export_delay(0); + const std::chrono::milliseconds scheduled_delay_millis(5000); + const size_t max_export_batch_size = 512; + const size_t max_queue_size = 2048; + + auto batch_processor = + GetMockProcessor(logs_received, is_shutdown, is_export_completed, export_delay, + scheduled_delay_millis, max_queue_size, max_export_batch_size); + + const int num_logs = 2048; + + for (int i = 0; i < num_logs; ++i) + { + auto log = batch_processor->MakeRecordable(); + log->SetBody("Log" + std::to_string(i)); + batch_processor->OnReceive(std::move(log)); + } + + EXPECT_TRUE(batch_processor->ForceFlush()); + + EXPECT_EQ(num_logs, logs_received->size()); + for (int i = 0; i < num_logs; ++i) + { + EXPECT_EQ("Log" + std::to_string(i), logs_received->at(i)->GetBody()); + } + + // Create some more logs to make sure that the processor still works + for (int i = 0; i < num_logs; ++i) + { + auto log = batch_processor->MakeRecordable(); + log->SetBody("Log" + std::to_string(i)); + batch_processor->OnReceive(std::move(log)); + } + + EXPECT_TRUE(batch_processor->ForceFlush()); + + EXPECT_EQ(num_logs * 2, logs_received->size()); + for (int i = 0; i < num_logs * 2; ++i) + { + EXPECT_EQ("Log" + std::to_string(i % num_logs), logs_received->at(i)->GetBody()); + } +} + +TEST_F(AsyncBatchLogProcessorTest, TestManyLogsLoss) +{ + /* Test that when exporting more than max_queue_size logs, some are most likely lost*/ + + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr>> logs_received( + new std::vector>); + + const int max_queue_size = 4096; + + auto batch_processor = GetMockProcessor(logs_received, is_shutdown); + + // Create max_queue_size log records + for (int i = 0; i < max_queue_size; ++i) + { + auto log = batch_processor->MakeRecordable(); + log->SetBody("Log" + std::to_string(i)); + batch_processor->OnReceive(std::move(log)); + } + + EXPECT_TRUE(batch_processor->ForceFlush()); + + // Log should be exported by now + EXPECT_GE(max_queue_size, logs_received->size()); +} + +TEST_F(AsyncBatchLogProcessorTest, TestManyLogsLossLess) +{ + /* Test that no logs are lost when sending max_queue_size logs */ + + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr>> logs_received( + new std::vector>); + auto batch_processor = GetMockProcessor(logs_received, is_shutdown); + + const int num_logs = 2048; + + for (int i = 0; i < num_logs; ++i) + { + auto log = batch_processor->MakeRecordable(); + log->SetBody("Log" + std::to_string(i)); + batch_processor->OnReceive(std::move(log)); + } + + EXPECT_TRUE(batch_processor->ForceFlush()); + + EXPECT_EQ(num_logs, logs_received->size()); + for (int i = 0; i < num_logs; ++i) + { + EXPECT_EQ("Log" + std::to_string(i), logs_received->at(i)->GetBody()); + } +} + +TEST_F(AsyncBatchLogProcessorTest, TestScheduledDelayMillis) +{ + /* Test that max_export_batch_size logs are exported every scheduled_delay_millis + seconds */ + + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr> is_export_completed(new std::atomic(false)); + std::shared_ptr>> logs_received( + new std::vector>); + + const std::chrono::milliseconds export_delay(0); + const std::chrono::milliseconds scheduled_delay_millis(2000); + const size_t max_export_batch_size = 512; + + auto batch_processor = GetMockProcessor(logs_received, is_shutdown, is_export_completed, + export_delay, scheduled_delay_millis); + + for (std::size_t i = 0; i < max_export_batch_size; ++i) + { + auto log = batch_processor->MakeRecordable(); + log->SetBody("Log" + std::to_string(i)); + batch_processor->OnReceive(std::move(log)); + } + // Sleep for scheduled_delay_millis milliseconds + std::this_thread::sleep_for(scheduled_delay_millis); + + // small delay to give time to export, which is being performed + // asynchronously by the worker thread (this thread will not + // forcibly join() the main thread unless processor's shutdown() is called). + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + // Logs should be exported by now + EXPECT_TRUE(is_export_completed->load()); + EXPECT_EQ(max_export_batch_size, logs_received->size()); + for (size_t i = 0; i < max_export_batch_size; ++i) + { + EXPECT_EQ("Log" + std::to_string(i), logs_received->at(i)->GetBody()); + } +} +# endif +#endif diff --git a/sdk/test/logs/batch_log_processor_test.cc b/sdk/test/logs/batch_log_processor_test.cc index 63e44676c8..2379c11c0c 100644 --- a/sdk/test/logs/batch_log_processor_test.cc +++ b/sdk/test/logs/batch_log_processor_test.cc @@ -9,6 +9,8 @@ # include # include +# include +# include # include using namespace opentelemetry::sdk::logs; @@ -55,6 +57,17 @@ class MockLogExporter final : public LogExporter return ExportResult::kSuccess; } +# ifdef ENABLE_ASYNC_EXPORT + void Export(const opentelemetry::nostd::span> &records, + std::function + &&result_callback) noexcept override + { + // We should keep the order of test records + auto result = Export(records); + result_callback(result); + } +# endif + // toggles the boolean flag marking this exporter as shut down bool Shutdown( std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override diff --git a/sdk/test/logs/simple_log_processor_test.cc b/sdk/test/logs/simple_log_processor_test.cc index 32c62a5083..824bf62696 100644 --- a/sdk/test/logs/simple_log_processor_test.cc +++ b/sdk/test/logs/simple_log_processor_test.cc @@ -53,6 +53,17 @@ class TestExporter final : public LogExporter return ExportResult::kSuccess; } +# ifdef ENABLE_ASYNC_EXPORT + // Dummy Async Export implementation + void Export(const nostd::span> &records, + std::function + &&result_callback) noexcept override + { + auto result = Export(records); + result_callback(result); + } +# endif + // Increment the shutdown counter everytime this method is called bool Shutdown(std::chrono::microseconds timeout) noexcept override { @@ -137,6 +148,14 @@ class FailShutDownExporter final : public LogExporter return ExportResult::kSuccess; } +# ifdef ENABLE_ASYNC_EXPORT + void Export(const nostd::span> &records, + std::function + &&result_callback) noexcept override + { + result_callback(ExportResult::kSuccess); + } +# endif bool Shutdown(std::chrono::microseconds timeout) noexcept override { return false; } }; diff --git a/sdk/test/trace/CMakeLists.txt b/sdk/test/trace/CMakeLists.txt index b02ff705fa..8e9402625b 100644 --- a/sdk/test/trace/CMakeLists.txt +++ b/sdk/test/trace/CMakeLists.txt @@ -8,7 +8,8 @@ foreach( always_on_sampler_test parent_sampler_test trace_id_ratio_sampler_test - batch_span_processor_test) + batch_span_processor_test + async_batch_span_processor_test) add_executable(${testname} "${testname}.cc") target_link_libraries( ${testname} diff --git a/sdk/test/trace/async_batch_span_processor_test.cc b/sdk/test/trace/async_batch_span_processor_test.cc new file mode 100644 index 0000000000..1ac28c5bc9 --- /dev/null +++ b/sdk/test/trace/async_batch_span_processor_test.cc @@ -0,0 +1,375 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +#ifndef ENABLE_ASYNC_EXPORT +# include +TEST(AsyncBatchSpanProcessor, DummyTest) +{ + // For linking +} +#endif + +#ifdef ENABLE_ASYNC_EXPORT + +# include "opentelemetry/sdk/trace/async_batch_span_processor.h" +# include "opentelemetry/sdk/trace/span_data.h" +# include "opentelemetry/sdk/trace/tracer.h" + +# include +# include +# include +# include +# include + +OPENTELEMETRY_BEGIN_NAMESPACE + +/** + * Returns a mock span exporter meant exclusively for testing only + */ +class MockSpanExporter final : public sdk::trace::SpanExporter +{ +public: + MockSpanExporter( + std::shared_ptr>> spans_received, + std::shared_ptr> is_shutdown, + std::shared_ptr> is_export_completed = + std::shared_ptr>(new std::atomic(false)), + const std::chrono::milliseconds export_delay = std::chrono::milliseconds(0), + int callback_count = 1) noexcept + : spans_received_(spans_received), + is_shutdown_(is_shutdown), + is_export_completed_(is_export_completed), + export_delay_(export_delay), + callback_count_(callback_count) + {} + + std::unique_ptr MakeRecordable() noexcept override + { + return std::unique_ptr(new sdk::trace::SpanData); + } + + sdk::common::ExportResult Export( + const nostd::span> &recordables) noexcept override + { + *is_export_completed_ = false; + + std::this_thread::sleep_for(export_delay_); + + for (auto &recordable : recordables) + { + auto span = std::unique_ptr( + static_cast(recordable.release())); + + if (span != nullptr) + { + spans_received_->push_back(std::move(span)); + } + } + + *is_export_completed_ = true; + return sdk::common::ExportResult::kSuccess; + } + + void Export(const nostd::span> &records, + std::function + &&result_callback) noexcept override + { + // We should keep the order of test records + auto result = Export(records); + async_threads_.emplace_back(std::make_shared( + [this, + result](std::function &&result_callback) { + for (int i = 0; i < callback_count_; i++) + { + result_callback(result); + } + }, + std::move(result_callback))); + } + + bool Shutdown( + std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override + { + while (!async_threads_.empty()) + { + std::list> async_threads; + async_threads.swap(async_threads_); + for (auto &async_thread : async_threads) + { + if (async_thread && async_thread->joinable()) + { + async_thread->join(); + } + } + } + *is_shutdown_ = true; + return true; + } + + bool IsExportCompleted() { return is_export_completed_->load(); } + +private: + std::shared_ptr>> spans_received_; + std::shared_ptr> is_shutdown_; + std::shared_ptr> is_export_completed_; + // Meant exclusively to test force flush timeout + const std::chrono::milliseconds export_delay_; + std::list> async_threads_; + int callback_count_; +}; + +/** + * Fixture Class + */ +class AsyncBatchSpanProcessorTestPeer : public testing::Test +{ +public: + std::unique_ptr>> GetTestSpans( + std::shared_ptr processor, + const int num_spans) + { + std::unique_ptr>> test_spans( + new std::vector>); + + for (int i = 0; i < num_spans; ++i) + { + test_spans->push_back(processor->MakeRecordable()); + static_cast(test_spans->at(i).get()) + ->SetName("Span " + std::to_string(i)); + } + + return test_spans; + } +}; + +/* ################################## TESTS ############################################ */ + +TEST_F(AsyncBatchSpanProcessorTestPeer, TestAsyncShutdown) +{ + std::shared_ptr>> spans_received( + new std::vector>); + std::shared_ptr> is_shutdown(new std::atomic(false)); + + sdk::trace::AsyncBatchSpanProcessorOptions options{}; + options.max_export_async = 5; + + auto batch_processor = + std::shared_ptr(new sdk::trace::AsyncBatchSpanProcessor( + std::unique_ptr(new MockSpanExporter(spans_received, is_shutdown)), + options)); + const int num_spans = 2048; + + auto test_spans = GetTestSpans(batch_processor, num_spans); + + for (int i = 0; i < num_spans; ++i) + { + batch_processor->OnEnd(std::move(test_spans->at(i))); + } + + EXPECT_TRUE(batch_processor->Shutdown(std::chrono::milliseconds(5000))); + // It's safe to shutdown again + EXPECT_TRUE(batch_processor->Shutdown()); + + EXPECT_EQ(num_spans, spans_received->size()); + for (int i = 0; i < num_spans; ++i) + { + EXPECT_EQ("Span " + std::to_string(i), spans_received->at(i)->GetName()); + } + + EXPECT_TRUE(is_shutdown->load()); +} + +TEST_F(AsyncBatchSpanProcessorTestPeer, TestAsyncShutdownNoCallback) +{ + std::shared_ptr> is_export_completed(new std::atomic(false)); + std::shared_ptr>> spans_received( + new std::vector>); + const std::chrono::milliseconds export_delay(0); + std::shared_ptr> is_shutdown(new std::atomic(false)); + + sdk::trace::AsyncBatchSpanProcessorOptions options{}; + options.max_export_async = 8; + + auto batch_processor = + std::shared_ptr(new sdk::trace::AsyncBatchSpanProcessor( + std::unique_ptr(new MockSpanExporter( + spans_received, is_shutdown, is_export_completed, export_delay, 0)), + options)); + const int num_spans = 2048; + + auto test_spans = GetTestSpans(batch_processor, num_spans); + + for (int i = 0; i < num_spans; ++i) + { + batch_processor->OnEnd(std::move(test_spans->at(i))); + } + + // Shutdown should never block for ever and return on timeout + EXPECT_TRUE(batch_processor->Shutdown(std::chrono::milliseconds(5000))); + // It's safe to shutdown again + EXPECT_TRUE(batch_processor->Shutdown()); + + EXPECT_TRUE(is_shutdown->load()); +} + +TEST_F(AsyncBatchSpanProcessorTestPeer, TestAsyncForceFlush) +{ + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr>> spans_received( + new std::vector>); + + sdk::trace::AsyncBatchSpanProcessorOptions options{}; + + auto batch_processor = + std::shared_ptr(new sdk::trace::AsyncBatchSpanProcessor( + std::unique_ptr(new MockSpanExporter(spans_received, is_shutdown)), + options)); + const int num_spans = 2048; + + auto test_spans = GetTestSpans(batch_processor, num_spans); + + for (int i = 0; i < num_spans; ++i) + { + batch_processor->OnEnd(std::move(test_spans->at(i))); + } + + // Give some time to export + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + EXPECT_TRUE(batch_processor->ForceFlush()); + + EXPECT_EQ(num_spans, spans_received->size()); + for (int i = 0; i < num_spans; ++i) + { + EXPECT_EQ("Span " + std::to_string(i), spans_received->at(i)->GetName()); + } + + // Create some more spans to make sure that the processor still works + auto more_test_spans = GetTestSpans(batch_processor, num_spans); + for (int i = 0; i < num_spans; ++i) + { + batch_processor->OnEnd(std::move(more_test_spans->at(i))); + } + + // Give some time to export the spans + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + EXPECT_TRUE(batch_processor->ForceFlush()); + + EXPECT_EQ(num_spans * 2, spans_received->size()); + for (int i = 0; i < num_spans; ++i) + { + EXPECT_EQ("Span " + std::to_string(i % num_spans), + spans_received->at(num_spans + i)->GetName()); + } +} + +TEST_F(AsyncBatchSpanProcessorTestPeer, TestManySpansLoss) +{ + /* Test that when exporting more than max_queue_size spans, some are most likely lost*/ + + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr>> spans_received( + new std::vector>); + + const int max_queue_size = 4096; + + auto batch_processor = + std::shared_ptr(new sdk::trace::AsyncBatchSpanProcessor( + std::unique_ptr(new MockSpanExporter(spans_received, is_shutdown)), + sdk::trace::AsyncBatchSpanProcessorOptions())); + + auto test_spans = GetTestSpans(batch_processor, max_queue_size); + + for (int i = 0; i < max_queue_size; ++i) + { + batch_processor->OnEnd(std::move(test_spans->at(i))); + } + + // Give some time to export the spans + std::this_thread::sleep_for(std::chrono::milliseconds(700)); + + EXPECT_TRUE(batch_processor->ForceFlush()); + + // Span should be exported by now + EXPECT_GE(max_queue_size, spans_received->size()); +} + +TEST_F(AsyncBatchSpanProcessorTestPeer, TestManySpansLossLess) +{ + /* Test that no spans are lost when sending max_queue_size spans */ + + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr>> spans_received( + new std::vector>); + + const int num_spans = 2048; + + auto batch_processor = + std::shared_ptr(new sdk::trace::AsyncBatchSpanProcessor( + std::unique_ptr(new MockSpanExporter(spans_received, is_shutdown)), + sdk::trace::AsyncBatchSpanProcessorOptions())); + + auto test_spans = GetTestSpans(batch_processor, num_spans); + + for (int i = 0; i < num_spans; ++i) + { + batch_processor->OnEnd(std::move(test_spans->at(i))); + } + + // Give some time to export the spans + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + EXPECT_TRUE(batch_processor->ForceFlush()); + + EXPECT_EQ(num_spans, spans_received->size()); + for (int i = 0; i < num_spans; ++i) + { + EXPECT_EQ("Span " + std::to_string(i), spans_received->at(i)->GetName()); + } +} + +TEST_F(AsyncBatchSpanProcessorTestPeer, TestScheduleDelayMillis) +{ + /* Test that max_export_batch_size spans are exported every schedule_delay_millis + seconds */ + + std::shared_ptr> is_shutdown(new std::atomic(false)); + std::shared_ptr> is_export_completed(new std::atomic(false)); + std::shared_ptr>> spans_received( + new std::vector>); + const std::chrono::milliseconds export_delay(0); + const size_t max_export_batch_size = 512; + sdk::trace::AsyncBatchSpanProcessorOptions options{}; + options.schedule_delay_millis = std::chrono::milliseconds(2000); + + auto batch_processor = + std::shared_ptr(new sdk::trace::AsyncBatchSpanProcessor( + std::unique_ptr( + new MockSpanExporter(spans_received, is_shutdown, is_export_completed, export_delay)), + options)); + + auto test_spans = GetTestSpans(batch_processor, max_export_batch_size); + + for (size_t i = 0; i < max_export_batch_size; ++i) + { + batch_processor->OnEnd(std::move(test_spans->at(i))); + } + + // Sleep for schedule_delay_millis milliseconds + std::this_thread::sleep_for(options.schedule_delay_millis); + + // small delay to give time to export + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + // Spans should be exported by now + EXPECT_TRUE(is_export_completed->load()); + EXPECT_EQ(max_export_batch_size, spans_received->size()); + for (size_t i = 0; i < max_export_batch_size; ++i) + { + EXPECT_EQ("Span " + std::to_string(i), spans_received->at(i)->GetName()); + } +} + +OPENTELEMETRY_END_NAMESPACE + +#endif diff --git a/sdk/test/trace/batch_span_processor_test.cc b/sdk/test/trace/batch_span_processor_test.cc index 0e6f9c35aa..6f270b9766 100644 --- a/sdk/test/trace/batch_span_processor_test.cc +++ b/sdk/test/trace/batch_span_processor_test.cc @@ -7,6 +7,8 @@ #include #include +#include +#include #include OPENTELEMETRY_BEGIN_NAMESPACE @@ -56,6 +58,17 @@ class MockSpanExporter final : public sdk::trace::SpanExporter return sdk::common::ExportResult::kSuccess; } +#ifdef ENABLE_ASYNC_EXPORT + void Export(const nostd::span> &records, + std::function + &&result_callback) noexcept override + { + // This is just dummy implementation. + auto result = Export(records); + result_callback(result); + } +#endif + bool Shutdown( std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override { diff --git a/sdk/test/trace/simple_processor_test.cc b/sdk/test/trace/simple_processor_test.cc index 9398b922a5..aa59fa850c 100644 --- a/sdk/test/trace/simple_processor_test.cc +++ b/sdk/test/trace/simple_processor_test.cc @@ -51,6 +51,15 @@ class RecordShutdownExporter final : public SpanExporter return ExportResult::kSuccess; } +#ifdef ENABLE_ASYNC_EXPORT + void Export(const opentelemetry::nostd::span> &spans, + std::function + &&result_callback) noexcept override + { + result_callback(ExportResult::kSuccess); + } +#endif + bool Shutdown( std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override {