diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 00000000..537a71e9 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,2 @@ +build --incompatible_package_name_is_a_function=false +query --incompatible_package_name_is_a_function=false diff --git a/.circleci/config.yml b/.circleci/config.yml index c0e6def4..2b0eedbf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,95 +1,99 @@ version: 2 jobs: - asan: + cmake_no_grpc: docker: - image: ubuntu:18.04 steps: - checkout - run: ./ci/setup_build_environment.sh - - run: ./ci/install_grpc.sh - run: ./ci/install_opentracing.sh - - run: ./ci/do_ci.sh cmake.asan + - run: ./ci/install_protobuf.sh + - run: ./ci/do_ci.sh cmake.minimal - store_artifacts: path: /build/Testing/Temporary/LastTest.log destination: Test.log - tsan: + + cmake_with_grpc: docker: - image: ubuntu:18.04 steps: - checkout - run: ./ci/setup_build_environment.sh - run: ./ci/install_grpc.sh + - run: ./ci/install_libevent.sh + - run: ./ci/install_cares.sh - run: ./ci/install_opentracing.sh - - run: ./ci/do_ci.sh cmake.tsan + - run: ./ci/do_ci.sh cmake.full - store_artifacts: path: /build/Testing/Temporary/LastTest.log destination: Test.log - clang_tidy: + asan: docker: - image: ubuntu:18.04 steps: - checkout - run: ./ci/setup_build_environment.sh - - run: ./ci/install_grpc.sh - - run: ./ci/install_opentracing.sh - - run: ./ci/install_clang.sh - - run: ./ci/do_ci.sh cmake.clang-tidy - - store_artifacts: - path: /build/Testing/Temporary/LastTest.log - destination: Test.log - build_with_no_grpc: + - run: ./ci/install_bazel.sh + - run: ./ci/do_ci.sh bazel.asan + + tsan: docker: - image: ubuntu:18.04 steps: - checkout - run: ./ci/setup_build_environment.sh - - run: ./ci/install_opentracing.sh - - run: ./ci/install_protobuf.sh - - run: ./ci/do_ci.sh cmake.no_grpc - - store_artifacts: - path: /build/Testing/Temporary/LastTest.log - destination: Test.log - build_plugin: + - run: ./ci/install_bazel.sh + - run: ./ci/do_ci.sh bazel.tsan + + release: docker: - image: ubuntu:18.04 steps: + - run: apt-get -qq update; apt-get -y install git ssh - checkout - run: ./ci/setup_build_environment.sh - - run: ./ci/do_ci.sh plugin - - store_artifacts: - path: /liblightstep_tracer_plugin.so - destination: liblightstep_tracer_plugin.so - build_bazel: + - run: ./ci/do_ci.sh release + + benchmark: docker: - image: ubuntu:18.04 - resource_class: large steps: - checkout - run: ./ci/setup_build_environment.sh - run: ./ci/install_bazel.sh - - run: ./ci/do_ci.sh bazel.build - release: + - run: ./ci/do_ci.sh bazel.benchmark + - store_artifacts: + path: /benchmark + desination: benchmark + + clang_tidy: docker: - image: ubuntu:18.04 steps: - - run: apt-get -qq update; apt-get -y install git ssh - checkout - run: ./ci/setup_build_environment.sh - - run: ./ci/do_ci.sh release + - run: ./ci/install_clang.sh + - run: ./ci/install_bazel.sh + - run: + command: ./ci/do_ci.sh clang_tidy + no_output_timeout: 1800 + - store_artifacts: + path: /clang-tidy-result.txt + destination: /clang-tidy-result.txt + - store_artifacts: + path: /clang-tidy-errors.txt + destination: /clang-tidy-errors.txt - benchmark: + build_plugin: docker: - image: ubuntu:18.04 steps: - checkout - run: ./ci/setup_build_environment.sh - - run: ./ci/install_grpc.sh - - run: ./ci/install_benchmark.sh - - run: ./ci/install_opentracing.sh - - run: ./ci/do_ci.sh benchmark + - run: ./ci/do_ci.sh plugin - store_artifacts: - path: /benchmark-results - desination: benchmark-results + path: /liblightstep_tracer_plugin.so + destination: liblightstep_tracer_plugin.so coverage: docker: @@ -97,9 +101,8 @@ jobs: steps: - checkout - run: ./ci/setup_build_environment.sh - - run: ./ci/install_grpc.sh - - run: ./ci/install_opentracing.sh - - run: ./ci/do_ci.sh coverage + - run: ./ci/install_bazel.sh + - run: ./ci/do_ci.sh bazel.coverage - store_artifacts: path: /coverage desination: coverage @@ -114,11 +117,11 @@ workflows: ignore: /.*/ tags: only: /^v[0-9]+(\.[0-9]+)*$/ + - cmake_no_grpc + - cmake_with_grpc - asan - tsan - - clang_tidy - - build_with_no_grpc - - build_plugin - - build_bazel - - coverage - benchmark + - coverage + - build_plugin + - clang_tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 00000000..a50fccd3 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,2 @@ +Checks: '*' +WarningsAsErrors: '*' diff --git a/.gitignore b/.gitignore index 9bab4224..9a97bd05 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ linux # Ignore all bazel-* symlinks. There is no full list since this can change # based on the name of the directory bazel is cloned into.bazel-* /bazel-* + +# compilation database +compile_commands.json diff --git a/3rd_party/base64/BUILD b/3rd_party/base64/BUILD new file mode 100644 index 00000000..ea534a13 --- /dev/null +++ b/3rd_party/base64/BUILD @@ -0,0 +1,18 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "base64_lib", + private_hdrs = [ + "include/lightstep/base64/base64.h", + ], + srcs = [ + "src/base64.cpp", + ], + is_3rd_party = True, +) diff --git a/3rd_party/catch2/BUILD b/3rd_party/catch2/BUILD new file mode 100644 index 00000000..09b73d1c --- /dev/null +++ b/3rd_party/catch2/BUILD @@ -0,0 +1,27 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +# From commit a575536abe20b58c48863600f8a71e93b4052b81 +lightstep_cc_library( + name = "catch2", + hdrs = [ + "catch.hpp", + ], + is_3rd_party = True, +) + +lightstep_cc_library( + name = "main_lib", + srcs = [ + "catch_main.cpp", + ], + deps = [ + ":catch2", + ], + is_3rd_party = True, +) diff --git a/3rd_party/catch2/include/lightstep/catch2/catch.hpp b/3rd_party/catch2/catch.hpp similarity index 100% rename from 3rd_party/catch2/include/lightstep/catch2/catch.hpp rename to 3rd_party/catch2/catch.hpp diff --git a/3rd_party/catch2/catch_main.cpp b/3rd_party/catch2/catch_main.cpp new file mode 100644 index 00000000..c99c101b --- /dev/null +++ b/3rd_party/catch2/catch_main.cpp @@ -0,0 +1,31 @@ +#define CATCH_CONFIG_RUNNER +#include "catch.hpp" +#include +#include +#include +#include +#include + + +// Taken from https://stackoverflow.com/a/77336/4447365 +void handler(int sig) { + void *array[100]; + size_t size; + + // get void*'s for all entries on the stack + size = backtrace(array, 100); + + // print out all the frames to stderr + fprintf(stderr, "Error: signal %d:\n", sig); + backtrace_symbols_fd(array, size, STDERR_FILENO); + exit(1); +} + +int main( int argc, char* argv[] ) { + signal(SIGSEGV, handler); + signal(SIGTERM, handler); + + int result = Catch::Session().run( argc, argv ); + + return result; +} diff --git a/3rd_party/randutils/BUILD b/3rd_party/randutils/BUILD new file mode 100644 index 00000000..57201308 --- /dev/null +++ b/3rd_party/randutils/BUILD @@ -0,0 +1,15 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "randutils_lib", + private_hdrs = [ + "include/lightstep/randutils.h", + ], + is_3rd_party = True, +) diff --git a/3rd_party/randutils/include/lightstep/randutils.h b/3rd_party/randutils/include/lightstep/randutils.h index 92a808cc..90134bab 100644 --- a/3rd_party/randutils/include/lightstep/randutils.h +++ b/3rd_party/randutils/include/lightstep/randutils.h @@ -455,7 +455,7 @@ class auto_seeded : public SeedSeq { { // This is a constant that changes every time we compile the code constexpr uint32_t compile_stamp = - fnv(2166136261U, __DATE__ __TIME__ __FILE__); + fnv(2166136261U, "LKJLDSF23414lkjcas;liu23"); // Some people think you shouldn't use the random device much because // on some platforms it could be expensive to call or "use up" vital diff --git a/BUILD.bazel b/BUILD.bazel index 9bbb4661..8317dadb 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,27 +1,11 @@ -cc_library( - name = "lightstep_tracer", - srcs = glob([ - "src/*.cpp", - "src/*.h", - ]), - hdrs = glob(["include/lightstep/*.h"]) + [ - ":include/lightstep/config.h", - ":include/lightstep/version.h", - ], - strip_include_prefix = "include", - visibility = ["//visibility:public"], - deps = [ - "//lightstep-tracer-common:collector_proto_cc", - "//lightstep-tracer-configuration:tracer_configuration_proto_cc", - "//lightstep-tracer-common:lightstep_carrier_proto_cc", - ":3rd_party_base64", - ":3rd_party_randutils", - ":3rd_party_catch", - # https://github.com/opentracing/opentracing-cpp - "@io_opentracing_cpp//:opentracing", - ], +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_package", + "lightstep_cc_library", ) +lightstep_package() + genrule( name = "generate_version_h", srcs = glob([ @@ -32,13 +16,11 @@ genrule( "3rd_party/**/*", ]), outs = [ - "include/lightstep/config.h", - "include/lightstep/version.h", + "gen-config/lightstep/version.h", ], cmd = """ TEMP_DIR=$$(mktemp -d) - CONFIG_H_OUT=$${PWD}/$(location :include/lightstep/config.h) - VERSION_H_OUT=$${PWD}/$(location :include/lightstep/version.h) + VERSION_H_OUT=$${PWD}/$(location :gen-config/lightstep/version.h) LIGHTSTEP_ROOT=$$(dirname $${PWD}/$(location :CMakeLists.txt)) cd $$TEMP_DIR cmake \\ @@ -46,27 +28,41 @@ genrule( -DHEADERS_ONLY=ON \\ -L \\ $$LIGHTSTEP_ROOT - mv include/lightstep/config.h $$CONFIG_H_OUT mv include/lightstep/version.h $$VERSION_H_OUT rm -rf $$TEMP_DIR """, ) -cc_library( - name = "3rd_party_base64", - srcs = glob(["3rd_party/base64/src/*.cpp"]), - hdrs = glob(["3rd_party/base64/include/**/*.h"]), - strip_include_prefix = "3rd_party/base64/include", +lightstep_cc_library( + name = "config_lib", + hdrs = [ + ":gen-config/lightstep/version.h", + ], + strip_include_prefix = "gen-config", ) -cc_library( - name = "3rd_party_catch", - hdrs = glob(["3rd_party/catch/include/**/*.hpp"]), - strip_include_prefix = "3rd_party/catch/include", +lightstep_cc_library( + name = "manual_tracer_lib", + deps = [ + "//src/tracer:tracer_lib", + "//src/tracer:binary_carrier_lib", + "//src/recorder:transporter_lib", + "//src/tracer:no_default_ssl_roots_pem_lib", + "//src/recorder:no_grpc_transporter_lib", + "//src/recorder:no_stream_recorder_lib", + ], ) -cc_library( - name = "3rd_party_randutils", - hdrs = glob(["3rd_party/randutils/include/**/*.h"]), - strip_include_prefix = "3rd_party/randutils/include", +lightstep_cc_library( + name = "tracer_lib", + deps = [ + "//src/tracer:tracer_lib", + "//src/tracer:binary_carrier_lib", + "//src/tracer:dynamic_load_lib", + "//src/tracer:no_default_ssl_roots_pem_lib", + "//src/recorder:transporter_lib", + "//src/recorder/grpc_transporter:grpc_transporter_lib", + "//src/recorder/stream_recorder:stream_recorder_lib", + "//src/network/ares_dns_resolver:ares_dns_resolver_lib", + ], ) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b514301..0e17fd24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,10 +35,10 @@ include(CPack) include(GNUInstallDirs) option(WITH_GRPC "Build with support for gRPC." ON) +option(WITH_LIBEVENT "Build with support for libevent." OFF) +option(WITH_CARES "Build with support for dns resolution using c-ares." OFF) option(WITH_DYNAMIC_LOAD "Build support for dynamic loading." ON) -option(ENABLE_LINTING "Run clang-tidy on sources if available." OFF) -option(HEADERS_ONLY "Only generate config.h and version.h." OFF) -option(BUILD_BENCHMARKING "Build benchmarks." OFF) +option(HEADERS_ONLY "Only generate version.h." OFF) # Allow a user to specify an optional default roots.pem file to embed into the # library. @@ -54,7 +54,7 @@ set(DEFAULT_SSL_ROOTS_PEM "" CACHE STRING "Path to a default roots.pem file to e if (WITH_GRPC) set(LIGHTSTEP_USE_GRPC 1) -elseif(WITH_DYNAMIC_LOAD) +elseif(WITH_DYNAMIC_LOAD AND NOT WITH_LIBEVENT) message(WARNING "WITH_GRPC is not set; building without dynamic loading support.") set(WITH_DYNAMIC_LOAD 0) endif() @@ -70,7 +70,6 @@ endif() # Set up generated header files config.h and version.h configure_file(version.h.in include/lightstep/version.h) -configure_file(config.h.in include/lightstep/config.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}/include) install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/lightstep DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) @@ -84,25 +83,12 @@ endif() set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") - set(WARNING_CXX_FLAGS -Weverything - -Wno-c++98-compat - -Wno-c++98-compat-bind-to-temporary-copy - -Wno-weak-vtables - -Wno-exit-time-destructors - -Wno-global-constructors - -Wno-sign-conversion - -Wno-padded - -Wno-date-time - -Wno-switch-enum - -Wno-disabled-macro-expansion) -elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - set(WARNING_CXX_FLAGS -Wall -Wextra) -endif() # ============================================================================== # Find packages +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + find_package(Protobuf REQUIRED) if (NOT DEFINED OPENTRACING_INCLUDE_DIR) @@ -142,15 +128,23 @@ if (WITH_GRPC) endif() endif() +if (WITH_LIBEVENT) + find_package(Libevent REQUIRED) + list(APPEND LIGHTSTEP_LINK_LIBRARIES ${LIBEVENT_LIBRARIES}) + include_directories(SYSTEM ${LIBEVENT_INCLUDE_DIRS}) +endif() + +if (WITH_CARES) + find_package(CARES REQUIRED) + list(APPEND LIGHTSTEP_LINK_LIBRARIES ${CARES_LIBRARIES}) + include_directories(SYSTEM ${CARES_INCLUDE_DIR}) +endif() + set(CMAKE_THREAD_PREFER_PTHREAD TRUE) set(THREADS_PREFER_PTHREAD_FLAG TRUE) find_package(Threads REQUIRED) list(APPEND LIGHTSTEP_LINK_LIBRARIES Threads::Threads) -if (BUILD_BENCHMARKING) - find_package(benchmark REQUIRED) -endif() - # ============================================================================== # Build LightStep tracer library @@ -158,36 +152,83 @@ add_subdirectory(3rd_party) include_directories(SYSTEM ${LIGHTSTEP_THIRD_PARTY_INCLUDES}) include_directories(include) +include_directories(src) install(DIRECTORY include/lightstep DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules) include(LightStepTracerCommon) include(LightStepTracerConfiguration) -set(LIGHTSTEP_SRCS src/utility.cpp - src/in_memory_stream.cpp - src/logger.cpp - src/propagation.cpp - src/binary_carrier.cpp - src/grpc_transporter.cpp - src/report_builder.cpp - src/manual_recorder.cpp - src/auto_recorder.cpp - src/lightstep_span_context.cpp - src/lightstep_immutable_span_context.cpp - src/lightstep_span.cpp - src/lightstep_tracer_impl.cpp - src/lightstep_tracer_factory.cpp - src/transporter.cpp - src/tracer.cpp +set(LIGHTSTEP_SRCS src/common/utility.cpp + src/common/atomic_bit_set.cpp + src/common/bipart_memory_stream.cpp + src/common/circular_buffer.cpp + src/common/chunk_circular_buffer.cpp + src/common/protobuf.cpp + src/common/in_memory_stream.cpp + src/common/logger.cpp + src/common/random.cpp + src/common/random_traverser.cpp + src/recorder/report_builder.cpp + src/recorder/auto_recorder.cpp + src/recorder/manual_recorder.cpp + src/recorder/transporter.cpp + src/tracer/propagation.cpp + src/tracer/binary_carrier.cpp + src/tracer/lightstep_span_context.cpp + src/tracer/lightstep_immutable_span_context.cpp + src/tracer/lightstep_span.cpp + src/tracer/lightstep_tracer_impl.cpp + src/tracer/lightstep_tracer_factory.cpp + src/tracer/tracer.cpp ) +if (WITH_GRPC) + list(APPEND LIGHTSTEP_SRCS src/recorder/grpc_transporter/grpc_transporter.cpp) +else() + list(APPEND LIGHTSTEP_SRCS src/recorder/no_grpc_transporter.cpp) +endif() + +if (WITH_LIBEVENT) + list(APPEND LIGHTSTEP_SRCS src/recorder/stream_recorder/stream_recorder.cpp + src/recorder/stream_recorder/satellite_dns_resolution_manager.cpp + src/recorder/stream_recorder/satellite_endpoint_manager.cpp + src/recorder/stream_recorder/satellite_connection.cpp + src/recorder/stream_recorder/satellite_streamer.cpp + src/recorder/stream_recorder/span_stream.cpp + src/recorder/stream_recorder/stream_recorder_metrics.cpp + src/recorder/stream_recorder/connection_stream.cpp + src/recorder/stream_recorder/fragment_span_input_stream.cpp + src/recorder/stream_recorder/host_header.cpp + src/recorder/stream_recorder/embedded_metrics_message.cpp + src/recorder/stream_recorder/status_line_parser.cpp + src/recorder/stream_recorder/utility.cpp + src/network/event.cpp + src/network/event_base.cpp + src/network/fragment_input_stream.cpp + src/network/fragment_array_input_stream.cpp + src/network/timer_event.cpp + src/network/ip_address.cpp + src/network/socket.cpp + src/network/vector_write.cpp + ) + if (WITH_CARES) + list(APPEND LIGHTSTEP_SRCS src/network/ares_dns_resolver/ares_dns_resolver.cpp + src/network/ares_dns_resolver/ares_library_handle.cpp + ) + else() + list(APPEND LIGHTSTEP_SRCS src/network/no_dns_resolver.cpp) + endif() +else() + list(APPEND LIGHTSTEP_SRCS src/recorder/no_stream_recorder.cpp) +endif() + if (WITH_DYNAMIC_LOAD) - list(APPEND LIGHTSTEP_SRCS src/dynamic_load.cpp) + list(APPEND LIGHTSTEP_SRCS src/tracer/dynamic_load.cpp) endif() if (DEFAULT_SSL_ROOTS_PEM STREQUAL "") - list(APPEND LIGHTSTEP_SRCS src/no_default_ssl_roots_pem.cpp) + list(APPEND LIGHTSTEP_SRCS src/tracer/no_default_ssl_roots_pem.cpp) else() # Follows the approach described in https://stackoverflow.com/a/11814544/4447365 set(EMBED_SSL_ROOTS_PEM_CPP_FILE ${CMAKE_BINARY_DIR}/default_ssl_roots_pem.cpp) @@ -205,10 +246,6 @@ if (BUILD_SHARED_LIBS) $ ${LIGHTSTEP_SRCS}) target_compile_options(lightstep_tracer PUBLIC ${WARNING_CXX_FLAGS}) - if (ENABLE_LINTING) - include(LightStepClangTidy) - _apply_clang_tidy_if_available(lightstep_tracer) - endif() target_link_libraries(lightstep_tracer ${LIGHTSTEP_LINK_LIBRARIES}) set_target_properties(lightstep_tracer PROPERTIES SOVERSION ${LIGHTSTEP_VERSION_MAJOR}) install(TARGETS lightstep_tracer @@ -222,25 +259,15 @@ if (BUILD_STATIC_LIBS) ${LIGHTSTEP_SRCS}) set_target_properties(lightstep_tracer-static PROPERTIES OUTPUT_NAME lightstep_tracer) target_compile_options(lightstep_tracer-static PUBLIC ${WARNING_CXX_FLAGS}) - if (ENABLE_LINTING) - include(LightStepClangTidy) - _apply_clang_tidy_if_available(lightstep_tracer-static) - endif() target_link_libraries(lightstep_tracer-static ${LIGHTSTEP_LINK_LIBRARIES}) install(TARGETS lightstep_tracer-static ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) endif() - # ============================================================================== # Build tests and examples - include(CTest) if (BUILD_TESTING AND BUILD_SHARED_LIBS) - add_subdirectory(test) + add_subdirectory(test/cmake) add_subdirectory(example) endif() - -if (BUILD_BENCHMARKING) - add_subdirectory(benchmark) -endif() diff --git a/WORKSPACE b/WORKSPACE index 90c0ebaa..bc0d8846 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -9,21 +9,90 @@ git_repository( commit = "ac50154a7713877f877981c33c3375003b6ebfe1", ) +git_repository( + name = "com_github_googleapis_googleapis", + remote = "https://github.com/googleapis/googleapis", + commit = "41d72d444fbe445f4da89e13be02078734fb7875", +) + +http_archive( + name = "com_github_libevent_libevent", + urls = [ + "https://github.com/libevent/libevent/archive/release-2.1.8-stable.zip" + ], + sha256 = "70158101eab7ed44fd9cc34e7f247b3cae91a8e4490745d9d6eb7edc184e4d96", + strip_prefix = "libevent-release-2.1.8-stable", + build_file = "//bazel:libevent.BUILD", +) + +http_archive( + name = "com_github_cares_cares", + urls = [ + "https://github.com/c-ares/c-ares/releases/download/cares-1_15_0/c-ares-1.15.0.tar.gz", + ], + sha256 = "6cdb97871f2930530c97deb7cf5c8fa4be5a0b02c7cea6e7c7667672a39d6852", + strip_prefix = "c-ares-1.15.0", + build_file = "//bazel:cares.BUILD", +) + +git_repository( + name = "build_stack_rules_proto", + remote = "https://github.com/stackb/rules_proto", + commit = "4c2226458203a9653ae722245cc27e8b07c383f7", +) + +git_repository( + name = "com_github_grpc_grpc", + remote = "https://github.com/grpc/grpc", + commit = "e97c9457e2f4e6733873ea2975d3b90432fdfdc1", +) + +load("@build_stack_rules_proto//cpp:deps.bzl", "cpp_grpc_compile") + +cpp_grpc_compile() + +load("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_deps") + +grpc_deps() + +####################################### +# Testing dependencies +####################################### + +git_repository( + name = "com_google_benchmark", + commit = "e776aa0275e293707b6a0901e0e8d8a8a3679508", + remote = "https://github.com/google/benchmark", +) + http_archive( - name = "com_google_protobuf", - sha256 = "5d4551193416861cb81c3bc0a428f22a6878148c57c31fb6f8f2aa4cf27ff635", - strip_prefix = "protobuf-c4f59dcc5c13debc572154c8f636b8a9361aacde", - urls = ["https://github.com/google/protobuf/archive/c4f59dcc5c13debc572154c8f636b8a9361aacde.tar.gz"], + name = "io_bazel_rules_go", + urls = ["https://github.com/bazelbuild/rules_go/releases/download/0.17.0/rules_go-0.17.0.tar.gz"], + sha256 = "492c3ac68ed9dcf527a07e6a1b2dcbf199c6bf8b35517951467ac32e421c06c1", ) +load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains") +go_rules_dependencies() +go_register_toolchains() + http_archive( - name = "com_google_protobuf_cc", - sha256 = "5d4551193416861cb81c3bc0a428f22a6878148c57c31fb6f8f2aa4cf27ff635", - strip_prefix = "protobuf-c4f59dcc5c13debc572154c8f636b8a9361aacde", - urls = ["https://github.com/google/protobuf/archive/c4f59dcc5c13debc572154c8f636b8a9361aacde.tar.gz"], + name = "bazel_gazelle", + urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/0.16.0/bazel-gazelle-0.16.0.tar.gz"], + sha256 = "7949fc6cc17b5b191103e97481cf8889217263acf52e00b560683413af204fcb", ) -local_repository( - name = "lightstep_vendored_googleapis", - path = "lightstep-tracer-common/third_party/googleapis", +load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") +gazelle_dependencies() + +go_repository( + name = "com_github_miekg_dns", + importpath = "github.com/miekg/dns", + tag = "v1.1.4", ) + +go_repository( + name = "com_github_golang_protobuf", + importpath = "github.com/golang/protobuf", + tag = "v1.3.0", +) + diff --git a/bazel/BUILD b/bazel/BUILD new file mode 100644 index 00000000..e69de29b diff --git a/bazel/cares.BUILD b/bazel/cares.BUILD new file mode 100644 index 00000000..81c428cd --- /dev/null +++ b/bazel/cares.BUILD @@ -0,0 +1,62 @@ +package( + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) # MIT + +exports_files(["LICENSE.md"]) + +include_files = [ + "cares/include/ares.h", + "cares/include/ares_build.h", + "cares/include/ares_dns.h", + "cares/include/ares_rules.h", + "cares/include/ares_version.h", +] + +lib_files = [ + "cares/lib/libcares.a", +] + + +genrule( + name = "cares-srcs", + outs = include_files + lib_files, + srcs = glob(["**"]) + [ + "@local_config_cc//:toolchain", + ], + cmd = """ + set -e + export INSTALL_DIR=$$PWD/$(@D)/cares + export TMP_DIR=$$(mktemp -d -t cares.XXXXXX) + cp -R $$(dirname $(location :README.md))/* $$TMP_DIR + cd $$TMP_DIR + cmake \ + -DCMAKE_INSTALL_PREFIX="$$INSTALL_DIR" \ + -DCARES_STATIC=on \ + -DCARES_SHARED=off \ + -DCARES_STATIC_PIC=on \ + -DCARES_BUILD_TESTS=off \ + -DCARES_BUILD_TOOLS=off \ + -DCMAKE_BUILD_TYPE=Release \ + . + make + make install + rm -rf $$TMP_DIR + """, +) + +cc_library( + name = "ares", + srcs = [ + "cares/lib/libcares.a", + ], + hdrs = include_files, + includes = ["cares/include"], + linkstatic = 1, +) + +filegroup( + name = "cares-files", + srcs = include_files + lib_files, +) diff --git a/bazel/libevent.BUILD b/bazel/libevent.BUILD new file mode 100644 index 00000000..f5e7ccce --- /dev/null +++ b/bazel/libevent.BUILD @@ -0,0 +1,92 @@ +# libevent (libevent.org) library. +# from https://github.com/libevent/libevent +# +# Build taken from tensorflow with modifications +# https://github.com/tensorflow/serving/blob/a94542b30d56a0f6ff9d53c4b2f75643b076d6e5/third_party/libevent.BUILD + +package( + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) # BSD + +exports_files(["LICENSE"]) + +include_files = [ + "libevent/include/evdns.h", + "libevent/include/event.h", + "libevent/include/evhttp.h", + "libevent/include/evrpc.h", + "libevent/include/evutil.h", + "libevent/include/event2/buffer.h", + "libevent/include/event2/bufferevent_struct.h", + "libevent/include/event2/event.h", + "libevent/include/event2/http_struct.h", + "libevent/include/event2/rpc_struct.h", + "libevent/include/event2/buffer_compat.h", + "libevent/include/event2/dns.h", + "libevent/include/event2/event_compat.h", + "libevent/include/event2/keyvalq_struct.h", + "libevent/include/event2/tag.h", + "libevent/include/event2/bufferevent.h", + "libevent/include/event2/dns_compat.h", + "libevent/include/event2/event_struct.h", + "libevent/include/event2/listener.h", + "libevent/include/event2/tag_compat.h", + "libevent/include/event2/bufferevent_compat.h", + "libevent/include/event2/dns_struct.h", + "libevent/include/event2/http.h", + "libevent/include/event2/rpc.h", + "libevent/include/event2/thread.h", + "libevent/include/event2/event-config.h", + "libevent/include/event2/http_compat.h", + "libevent/include/event2/rpc_compat.h", + "libevent/include/event2/util.h", + "libevent/include/event2/visibility.h", +] + +lib_files = [ + "libevent/lib/libevent.a", + "libevent/lib/libevent_core.a", + "libevent/lib/libevent_extra.a", +] + +genrule( + name = "libevent-srcs", + outs = include_files + lib_files, + srcs = glob(["**"]) + [ + "@local_config_cc//:toolchain", + ], + cmd = """ + set -e + export INSTALL_DIR=$$PWD/$(@D)/libevent + export TMP_DIR=$$(mktemp -d -t libevent.XXXXXX) + cp -R $$(dirname $(location :README.md))/* $$TMP_DIR + cd $$TMP_DIR + cmake \ + -DCMAKE_INSTALL_PREFIX="$$INSTALL_DIR" \ + -DCMAKE_POSITION_INDEPENDENT_CODE=on \ + -DEVENT__DISABLE_OPENSSL:BOOL=on \ + -DEVENT__DISABLE_REGRESS:BOOL=on \ + -DCMAKE_BUILD_TYPE=Release \ + . + make + make install + rm -rf $$TMP_DIR + """, +) + +cc_library( + name = "libevent", + srcs = [ + "libevent/lib/libevent.a", + ], + hdrs = include_files, + includes = ["libevent/include"], + linkstatic = 1, +) + +filegroup( + name = "libevent-files", + srcs = include_files + lib_files, +) diff --git a/bazel/lightstep_build_system.bzl b/bazel/lightstep_build_system.bzl new file mode 100644 index 00000000..fa26d7e1 --- /dev/null +++ b/bazel/lightstep_build_system.bzl @@ -0,0 +1,182 @@ +def lightstep_package(): + native.package(default_visibility = ["//visibility:public"]) + +# Taken from https://github.com/bazelbuild/bazel/issues/2670#issuecomment-369674735 +# with modifications. +def lightstep_private_include_copts(includes, is_system=False): + copts = [] + prefix = '' + + # convert "@" to "external/" unless in the main workspace + repo_name = native.repository_name() + if repo_name != '@': + prefix = 'external/{}/'.format(repo_name[1:]) + + inc_flag = "-isystem " if is_system else "-I" + + for inc in includes: + copts.append("{}{}{}".format(inc_flag, prefix, inc)) + copts.append("{}$(GENDIR){}/{}".format(inc_flag, prefix, inc)) + + return copts + +def lightstep_include_copts(): + return lightstep_private_include_copts([ + "include", + "src", + "test", + ]) + lightstep_private_include_copts([ + "3rd_party/randutils/include", + "3rd_party/base64/include", + ], is_system=True) + +def lightstep_copts(is_3rd_party=False): + if is_3rd_party: + return [ + "-std=c++11", + ] + return [ + "-Wall", + "-Wextra", + "-Werror", + "-Wnon-virtual-dtor", + "-Woverloaded-virtual", + "-Wold-style-cast", + "-Wno-overloaded-virtual", + "-Wvla", + "-std=c++11", + ] + + +def lightstep_include_prefix(path): + if path.startswith('src/') or path.startswith('include/'): + return '/'.join(path.split('/')[1:]) + return None + +def lightstep_cc_library(name, + srcs = [], + hdrs = [], + private_hdrs = [], + copts = [], + linkopts = [], + includes = [], + visibility = None, + external_deps = [], + deps = [], + data = [], + is_3rd_party = False, + strip_include_prefix = None): + native.cc_library( + name = name, + srcs = srcs + private_hdrs, + hdrs = hdrs, + copts = lightstep_include_copts() + lightstep_copts(is_3rd_party) + copts, + linkopts = linkopts, + includes = includes, + deps = external_deps + deps, + data = data, + linkstatic = 1, + include_prefix = lightstep_include_prefix(native.package_name()), + visibility = visibility, + strip_include_prefix = strip_include_prefix, + ) + +def lightstep_cc_binary( + name, + args = [], + srcs = [], + data = [], + testonly = 0, + visibility = None, + external_deps = [], + deps = [], + linkopts = []): + native.cc_binary( + name = name, + args = args, + srcs = srcs, + data = data, + copts = lightstep_include_copts() + lightstep_copts(), + linkopts = linkopts, + testonly = testonly, + linkstatic = 1, + visibility = visibility, + stamp = 1, + deps = external_deps + deps, + ) + +def lightstep_cc_test( + name, + args = [], + srcs = [], + data = [], + testonly = 0, + visibility = None, + external_deps = [], + deps = [], + linkopts = []): + native.cc_test( + name = name, + args = args, + srcs = srcs, + data = data, + copts = lightstep_include_copts() + lightstep_copts(), + linkopts = linkopts, + testonly = testonly, + linkstatic = 1, + visibility = visibility, + stamp = 1, + deps = external_deps + deps, + ) + +def lightstep_catch_test( + name, + srcs = [], + data = [], + visibility = None, + external_deps = [], + deps = [], + linkopts = []): + lightstep_cc_test( + name = name, + srcs = srcs, + data = data, + linkopts = linkopts, + visibility = visibility, + deps = deps, + external_deps = external_deps + ["//3rd_party/catch2:main_lib"], + ) + +def lightstep_google_benchmark( + name, + srcs = [], + data = [], + visibility = None, + external_deps = [], + deps = [], + linkopts = []): + native.cc_binary( + name = name, + srcs = srcs, + data = data, + deps = deps + ["@com_google_benchmark//:benchmark"], + testonly = 1 + ) + native.cc_test( + name = name + "_test", + srcs = srcs, + args = ["--benchmark_min_time=0"], + data = data, + copts = lightstep_include_copts() + lightstep_copts(), + linkstatic = 1, + deps = deps + ["@com_google_benchmark//:benchmark"], + ) + native.genrule( + name = name + "_result", + outs = [ + name + "_result.txt", + ], + tools = [":" + name] + data, + testonly = 1, + cmd = "$(location :%s) --benchmark_color=false &> $@" % name, +) diff --git a/benchmark/BUILD b/benchmark/BUILD new file mode 100644 index 00000000..fde9389f --- /dev/null +++ b/benchmark/BUILD @@ -0,0 +1,16 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_google_benchmark", + "lightstep_package", +) + +lightstep_google_benchmark( + name = "span_operations_benchmark", + srcs = [ + "span_operations_benchmark.cpp", + ], + deps = [ + "//:manual_tracer_lib", + ], +) + diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt deleted file mode 100644 index 9e8889e0..00000000 --- a/benchmark/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_executable(span_operations_benchmark span_operations_benchmark.cpp) -target_link_libraries(span_operations_benchmark lightstep_tracer - ${LIGHTSTEP_LINK_LIBRARIES} - benchmark::benchmark) diff --git a/benchmark/span_operations_benchmark.cpp b/benchmark/span_operations_benchmark.cpp index fbd82da5..11fe410c 100644 --- a/benchmark/span_operations_benchmark.cpp +++ b/benchmark/span_operations_benchmark.cpp @@ -1,7 +1,9 @@ -#include -#include #include +#include "lightstep/tracer.h" + +#include "benchmark/benchmark.h" + namespace { class NullTransporter final : public lightstep::SyncTransporter { public: diff --git a/ci/Dockerfile b/ci/Dockerfile index 64c3e56b..d8b9d990 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -3,11 +3,9 @@ FROM ubuntu:18.04 WORKDIR /3rd_party ADD setup_build_environment.sh /3rd_party -ADD install_opentracing.sh /3rd_party -ADD install_grpc.sh /3rd_party +ADD install_bazel.sh /3rd_party ADD install_clang.sh /3rd_party RUN /3rd_party/setup_build_environment.sh \ - && /3rd_party/install_opentracing.sh \ - && /3rd_party/install_grpc.sh \ + && /3rd_party/install_bazel.sh \ && /3rd_party/install_clang.sh diff --git a/ci/build_plugin.sh b/ci/build_plugin.sh index 33ae2b35..f28068f2 100755 --- a/ci/build_plugin.sh +++ b/ci/build_plugin.sh @@ -4,18 +4,30 @@ set -e [ -z "${OPENTRACING_VERSION}" ] && export OPENTRACING_VERSION="v1.5.0" [ -z "${GRPC_VERSION}" ] && export GRPC_VERSION="v1.10.0" +[ -z "${LIBEVENT_VERSION}" ] && export LIBEVENT_VERSION="v2.1.8" +[ -z "${CARES_VERSION}" ] && export CARES_VERSION="1.15.0" # Compile for a portable cpu architecture export CFLAGS="-march=x86-64 -fPIC" export CXXFLAGS="-march=x86-64 -fPIC" export LDFLAGS="-fPIC" +### Build C-ares +cd "${BUILD_DIR}" +wget https://github.com/c-ares/c-ares/releases/download/cares-${CARES_VERSION//./_}/c-ares-${CARES_VERSION}.tar.gz +tar zxf c-ares-${CARES_VERSION}.tar.gz +cd c-ares-${CARES_VERSION} +./configure --with-pic=yes \ + --enable-shared=no \ + --enable-static=yes +make && make install + ### Build gRPC cd "${BUILD_DIR}" git clone -b ${GRPC_VERSION} https://github.com/grpc/grpc cd grpc git submodule update --init -make HAS_SYSTEM_PROTOBUF=false static +make HAS_SYSTEM_CARES=true HAS_SYSTEM_PROTOBUF=false static # According to https://github.com/grpc/grpc/issues/7917#issuecomment-243800503 # make install won't work with the static target, so manually copy in the files # needed. @@ -30,6 +42,19 @@ cp libs/opt/pkgconfig/*.pc /usr/local/lib/pkgconfig cd third_party/protobuf make install +# Build libevent +cd "${BUILD_DIR}" +git clone -b release-${LIBEVENT_VERSION/v/}-stable https://github.com/libevent/libevent +cd libevent +./autogen.sh +./configure \ + --enable-shared=no \ + --enable-static=yes \ + --with-pic \ + --disable-openssl +make +make install + # Build OpenTracing cd "${BUILD_DIR}" git clone -b ${OPENTRACING_VERSION} https://github.com/opentracing/opentracing-cpp.git @@ -55,6 +80,8 @@ EOF cmake -DCMAKE_BUILD_TYPE=Release \ -DBUILD_TESTING=OFF \ -DBUILD_STATIC_LIBS=OFF \ + -DWITH_LIBEVENT=ON \ + -DWITH_CARES=ON \ -DCMAKE_SHARED_LINKER_FLAGS="-static-libstdc++ -static-libgcc -Wl,--version-script=${PWD}/export.map" \ -DDEFAULT_SSL_ROOTS_PEM:STRING=${BUILD_DIR}/grpc/etc/roots.pem \ "${SRC_DIR}" diff --git a/ci/clang-tidy-wrapper.sh b/ci/clang-tidy-wrapper.sh new file mode 100755 index 00000000..76cac29a --- /dev/null +++ b/ci/clang-tidy-wrapper.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +clang-tidy-6.0 -warnings-as-errors='*' "$@" diff --git a/ci/do_ci.sh b/ci/do_ci.sh index c054ce50..a0c571f1 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -6,86 +6,102 @@ set -e [ -z "${BUILD_DIR}" ] && export BUILD_DIR=/build mkdir -p "${BUILD_DIR}" -if [[ "$1" == "cmake.debug" ]]; then +BAZEL_OPTIONS="--jobs 1" +BAZEL_TEST_OPTIONS="$BAZEL_OPTIONS --test_output=errors" + +function copy_benchmark_results() { + mkdir -p "${BENCHMARK_DST_DIR}" + cd "${BENCHMARK_SRC_DIR}" + find . -name '*_result.txt' -exec bash -c \ + 'echo "$@" && mkdir -p "${BENCHMARK_DST_DIR}"/$(dirname "$@") && \ + cp "$@" "${BENCHMARK_DST_DIR}"/"$@"' _ {} \; +} + +if [[ "$1" == "cmake.minimal" ]]; then cd "${BUILD_DIR}" cmake -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_FLAGS="-Werror" \ + -DWITH_GRPC=OFF \ + -DWITH_LIBEVENT=OFF \ "${SRC_DIR}" make - make test exit 0 -elif [[ "$1" == "cmake.no_grpc" ]]; then +elif [[ "$1" == "cmake.full" ]]; then cd "${BUILD_DIR}" cmake -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_FLAGS="-Werror" \ - -DWITH_GRPC=OFF \ + -DWITH_GRPC=ON \ + -DWITH_LIBEVENT=ON \ + -DWIH_CARES=ON \ "${SRC_DIR}" make make test exit 0 -elif [[ "$1" == "cmake.asan" ]]; then - cd "${BUILD_DIR}" - cmake -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_CXX_FLAGS="-Werror -fno-omit-frame-pointer -fsanitize=address" \ - -DCMAKE_SHARED_LINKER_FLAGS="-fno-omit-frame-pointer -fsanitize=address" \ - -DCMAKE_EXE_LINKER_FLAGS="-fno-omit-frame-pointer -fsanitize=address" \ - "${SRC_DIR}" - make - make test +elif [[ "$1" == "clang_tidy" ]]; then + export CC=/usr/bin/clang-6.0 + CC=/usr/bin/clang-6.0 bazel build \ + $BAZEL_OPTIONS \ + //src/... //test/... //benchmark/... //include/... + ./ci/gen_compilation_database.sh + ./ci/fix_compilation_database.py + ./ci/run_clang_tidy.sh |& tee /clang-tidy-result.txt + grep ": error:" /clang-tidy-result.txt | cat > /clang-tidy-errors.txt + num_errors=`wc -l /clang-tidy-errors.txt | awk '{print $1}'` + if [[ $num_errors -ne 0 ]]; then + exit 1 + fi exit 0 -elif [[ "$1" == "cmake.tsan" ]]; then - cd "${BUILD_DIR}" -# Testing with dynamic load seems to have some issues with TSAN so turn off -# dynamic loading in this test for now. - cmake -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_CXX_FLAGS="-Werror -fno-omit-frame-pointer -fsanitize=thread" \ - -DCMAKE_SHARED_LINKER_FLAGS="-fno-omit-frame-pointer -fsanitize=thread" \ - -DCMAKE_EXE_LINKER_FLAGS="-fno-omit-frame-pointer -fsanitize=thread" \ - -DWITH_DYNAMIC_LOAD=OFF \ - "${SRC_DIR}" - make VERBOSE=1 - make test +elif [[ "$1" == "bazel.asan" ]]; then + bazel build -c dbg \ + $BAZEL_OPTIONS \ + --copt=-fsanitize=address \ + --linkopt=-fsanitize=address \ + //... + bazel test -c dbg \ + $BAZEL_TEST_OPTIONS \ + --copt=-fsanitize=address \ + --linkopt=-fsanitize=address \ + //... exit 0 -elif [[ "$1" == "coverage" ]]; then - cd "${BUILD_DIR}" - cmake -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_CXX_FLAGS="-fprofile-arcs -ftest-coverage -fPIC -O0" \ - "$SRC_DIR" - make VERBOSE=1 - make test - cd CMakeFiles/lightstep_tracer.dir/src - gcovr -r "$SRC_DIR" . --html --html-details -o coverage.html - mkdir /coverage - cp *.html /coverage/ +elif [[ "$1" == "bazel.tsan" ]]; then + bazel build -c dbg \ + $BAZEL_OPTIONS \ + --copt=-fsanitize=thread \ + --linkopt=-fsanitize=thread \ + //... + bazel test -c dbg \ + $BAZEL_TEST_OPTIONS \ + --copt=-fsanitize=thread \ + --linkopt=-fsanitize=thread \ + //... exit 0 -elif [[ "$1" == "benchmark" ]]; then - cd "${BUILD_DIR}" - cmake -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_BENCHMARKING=ON \ - -DBUILD_TESTING=OFF \ - "${SRC_DIR}" - make VERBOSE=1 - mkdir /benchmark-results - ./benchmark/span_operations_benchmark --benchmark_color=false > /benchmark-results/span_operations_benchmark.out 2>&1 +elif [[ "$1" == "bazel.benchmark" ]]; then + export BENCHMARK_SRC_DIR=bazel-genfiles/benchmark + export BENCHMARK_DST_DIR=/benchmark + bazel build -c opt \ + $BAZEL_OPTIONS \ + //benchmark/... + copy_benchmark_results exit 0 -elif [[ "$1" == "cmake.clang-tidy" ]]; then - cd "${BUILD_DIR}" - cmake -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_C_COMPILER=/usr/bin/clang-6.0 \ - -DCMAKE_CXX_COMPILER=/usr/bin/clang++-6.0 \ - -DENABLE_LINTING=ON \ - -DCMAKE_CXX_FLAGS="-Werror" \ - "${SRC_DIR}" - make - make test +elif [[ "$1" == "bazel.coverage" ]]; then + mkdir -p /coverage + rm -rf /coverage/* + bazel coverage \ + $BAZEL_OPTIONS \ + --instrument_test_targets \ + --experimental_cc_coverage \ + --combined_report=lcov \ + --instrumentation_filter="-3rd_party,-benchmark,-test" \ + --coverage_report_generator=@bazel_tools//tools/test/CoverageOutputGenerator/java/com/google/devtools/coverageoutputgenerator:Main \ + //... + genhtml bazel-out/_coverage/_coverage_report.dat \ + --output-directory /coverage + tar czf /coverage.tgz /coverage exit 0 elif [[ "$1" == "plugin" ]]; then cd "${BUILD_DIR}" "${SRC_DIR}"/ci/build_plugin.sh exit 0 -elif [[ "$1" == "bazel.build" ]]; then - bazel build //... - exit 0 elif [[ "$1" == "release" ]]; then "${SRC_DIR}"/ci/build_plugin.sh "${SRC_DIR}"/ci/release.sh diff --git a/ci/fix_compilation_database.py b/ci/fix_compilation_database.py new file mode 100755 index 00000000..d68e0dd6 --- /dev/null +++ b/ci/fix_compilation_database.py @@ -0,0 +1,52 @@ +#!/usr/bin/python + +# Taken from https://github.com/envoyproxy/envoy/blob/964dd1de5d4d4cb388a0e6dd25dbff1f7ff56cea/tools/gen_compilation_database.py with modification. + +import json +import sys +import os + +def isHeader(filename): + for ext in (".h", ".hh", ".hpp", ".hxx"): + if filename.endswith(ext): + return True + return False + +def isCompileTarget(target): + filename = target["file"] + + if filename.startswith("3rd_party"): + return False + + if filename.startswith("bazel-out/"): + return False + + if filename.startswith("external/"): + return False + + return True + +def modifyCompileCommand(target): + cxx, options = target["command"].split(" ", 1) + + # Workaround for bazel added C++11 options, those doesn't affect build itself but + # clang-tidy will misinterpret them. + options = options.replace("-std=c++0x ", "") + options = options.replace("-std=c++11 ", "") + + if isHeader(target["file"]): + options += " -Wno-pragma-once-outside-header -Wno-unused-const-variable" + options += " -Wno-unused-function" + options += " -x c++-header" + + target["command"] = " ".join(["clang++", options]) + return target + +with open("compile_commands.json", "r") as db_file: + db = json.load(db_file) + +db = [modifyCompileCommand(target) for target in db if isCompileTarget(target)] +os.remove("compile_commands.json") + +with open("compile_commands.json", "w") as db_file: + json.dump(db, db_file, indent=2) diff --git a/ci/gen_compilation_database.sh b/ci/gen_compilation_database.sh new file mode 100755 index 00000000..11492a16 --- /dev/null +++ b/ci/gen_compilation_database.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +RELEASE_VERSION=0.3.1 + +if [[ ! -d bazel-compilation-database-${RELEASE_VERSION} ]]; then + curl -L https://github.com/grailbio/bazel-compilation-database/archive/${RELEASE_VERSION}.tar.gz | tar -xz +fi + +bazel-compilation-database-${RELEASE_VERSION}/generate.sh $@ diff --git a/ci/install_bazel.sh b/ci/install_bazel.sh index 6e29db92..d3326f00 100755 --- a/ci/install_bazel.sh +++ b/ci/install_bazel.sh @@ -2,15 +2,14 @@ set -e +[ -z "${BAZEL_VERSION}" ] && export BAZEL_VERSION="0.21.0" + apt-get update apt-get install --no-install-recommends --no-install-suggests -y \ - curl \ + wget \ + unzip \ ca-certificates \ openjdk-8-jdk -echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" \ - | tee /etc/apt/sources.list.d/bazel.list -curl https://bazel.build/bazel-release.pub.gpg | apt-key add - -apt-get update -apt-get install --no-install-recommends --no-install-suggests -y \ - bazel -apt-get upgrade -y bazel +wget https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-installer-linux-x86_64.sh +chmod +x bazel-${BAZEL_VERSION}-installer-linux-x86_64.sh +./bazel-${BAZEL_VERSION}-installer-linux-x86_64.sh diff --git a/ci/install_cares.sh b/ci/install_cares.sh new file mode 100755 index 00000000..d3c78b6c --- /dev/null +++ b/ci/install_cares.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +apt-get install --no-install-recommends --no-install-suggests -y \ + libc-ares-dev + diff --git a/ci/install_libevent.sh b/ci/install_libevent.sh new file mode 100755 index 00000000..da40ec61 --- /dev/null +++ b/ci/install_libevent.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +apt-get install --no-install-recommends --no-install-suggests -y \ + libevent-dev + diff --git a/ci/run_clang_tidy.sh b/ci/run_clang_tidy.sh new file mode 100755 index 00000000..041b7d8e --- /dev/null +++ b/ci/run_clang_tidy.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +set -e + +CHECKS="\ +-checks=*,\ +-bugprone-use-after-move,\ +-cert-err58-cpp,\ +-fuchsia-*,\ +-clang-analyzer-alpha.*,\ +-clang-diagnostic-deprecated,\ +-llvm-include-order,\ +-llvm-header-guard,\ +-misc-forwarding-reference-overload,\ +-google-runtime-references,\ +-google-build-using-namespace,\ +-google-runtime-int,\ +-google-explicit-constructor,\ +-modernize-make-unique,\ +-modernize-use-transparent-functors,\ +-performance-move-const-arg,\ +-hicpp-explicit-conversions,\ +-hicpp-member-init,\ +-hicpp-invalid-access-moved,\ +-hicpp-move-const-arg,\ +-hicpp-member-init,\ +-hicpp-vararg,\ +-hicpp-signed-bitwise,\ +-hicpp-no-array-decay,\ +-hicpp-special-member-functions,\ +-cppcoreguidelines-special-member-functions,\ +-cppcoreguidelines-pro-type-member-init,\ +-cppcoreguidelines-owning-memory,\ +-cppcoreguidelines-pro-type-reinterpret-cast,\ +-cppcoreguidelines-pro-type-const-cast,\ +-cppcoreguidelines-pro-bounds-array-to-pointer-decay,\ +-cppcoreguidelines-pro-bounds-constant-array-index,\ +-cppcoreguidelines-pro-bounds-pointer-arithmetic,\ +-cppcoreguidelines-pro-type-vararg" + +run-clang-tidy $CHECKS -clang-tidy-binary=ci/clang-tidy-wrapper.sh diff --git a/ci/setup_build_environment.sh b/ci/setup_build_environment.sh index 278829af..37332822 100755 --- a/ci/setup_build_environment.sh +++ b/ci/setup_build_environment.sh @@ -8,11 +8,15 @@ apt-get install --no-install-recommends --no-install-suggests -y \ pkg-config \ git \ ca-certificates \ + curl \ automake \ autogen \ autoconf \ libtool \ gnupg2 \ ssh \ + lcov \ + vim \ + gdb \ + wget \ python python-setuptools python-pip -pip install gcovr diff --git a/cmake/Modules/FindCARES.cmake b/cmake/Modules/FindCARES.cmake new file mode 100644 index 00000000..b2658871 --- /dev/null +++ b/cmake/Modules/FindCARES.cmake @@ -0,0 +1,43 @@ +# - Find c-ares +# Find the c-ares includes and library +# This module defines +# CARES_INCLUDE_DIR, where to find ares.h, etc. +# CARES_LIBRARIES, the libraries needed to use c-ares. +# CARES_FOUND, If false, do not try to use c-ares. +# also defined, but not for general use are +# CARES_LIBRARY, where to find the c-ares library. +# Taken from https://github.com/curl/curl/blob/d1207c07d0cc3c7870e50865052bb59850917ec9/CMake/FindCARES.cmake + +find_path(CARES_INCLUDE_DIR ares.h + /usr/local/include + /usr/include + ) + +set(CARES_NAMES ${CARES_NAMES} cares) +find_library(CARES_LIBRARY + NAMES ${CARES_NAMES} + PATHS /usr/lib /usr/local/lib + ) + +if(CARES_LIBRARY AND CARES_INCLUDE_DIR) + set(CARES_LIBRARIES ${CARES_LIBRARY}) + set(CARES_FOUND "YES") +else() + set(CARES_FOUND "NO") +endif() + + +if(CARES_FOUND) + if(NOT CARES_FIND_QUIETLY) + message(STATUS "Found c-ares: ${CARES_LIBRARIES}") + endif() +else() + if(CARES_FIND_REQUIRED) + message(FATAL_ERROR "Could not find c-ares library") + endif() +endif() + +mark_as_advanced( + CARES_LIBRARY + CARES_INCLUDE_DIR + ) diff --git a/cmake/Modules/FindLibevent.cmake b/cmake/Modules/FindLibevent.cmake new file mode 100644 index 00000000..91932b3b --- /dev/null +++ b/cmake/Modules/FindLibevent.cmake @@ -0,0 +1,48 @@ +# find LibEvent +# an event notification library (http://libevent.org/) +# +# Usage: +# LIBEVENT_INCLUDE_DIRS, where to find LibEvent headers +# LIBEVENT_LIBRARIES, LibEvent libraries +# Libevent_FOUND, If false, do not try to use libevent +# +# Taken from https://github.com/apache/thrift/blob/7edc8faefd391ce11eca3023a35cc54bcb2eb1af/build/cmake/FindLibevent.cmake +# with modification. + +set(LIBEVENT_ROOT CACHE PATH "Root directory of libevent installation") +set(LibEvent_EXTRA_PREFIXES /usr/local /opt/local "$ENV{HOME}" ${LIBEVENT_ROOT}) +foreach(prefix ${LibEvent_EXTRA_PREFIXES}) + list(APPEND LibEvent_INCLUDE_PATHS "${prefix}/include") + list(APPEND LibEvent_LIBRARIES_PATHS "${prefix}/lib") +endforeach() + +# Looking for "event.h" will find the Platform SDK include dir on windows +# so we also look for a peer header like evhttp.h to get the right path +find_path(LIBEVENT_INCLUDE_DIRS evhttp.h event.h PATHS ${LibEvent_INCLUDE_PATHS}) + +# "lib" prefix is needed on Windows in some cases +# newer versions of libevent use three libraries +find_library(LIBEVENT_LIBRARIES NAMES event_core PATHS ${LibEvent_LIBRARIES_PATHS}) + +if (LIBEVENT_LIBRARIES AND LIBEVENT_INCLUDE_DIRS) + set(Libevent_FOUND TRUE) + set(LIBEVENT_LIBRARIES ${LIBEVENT_LIBRARIES}) +else () + set(Libevent_FOUND FALSE) +endif () + +if (Libevent_FOUND) + if (NOT Libevent_FIND_QUIETLY) + message(STATUS "Found libevent: ${LIBEVENT_LIBRARIES}") + endif () +else () + if (LibEvent_FIND_REQUIRED) + message(FATAL_ERROR "Could NOT find libevent.") + endif () + message(STATUS "libevent NOT found.") +endif () + +mark_as_advanced( + LIBEVENT_LIBRARIES + LIBEVENT_INCLUDE_DIRS + ) diff --git a/cmake/Modules/LightStepClangTidy.cmake b/cmake/Modules/LightStepClangTidy.cmake deleted file mode 100644 index 2f28508b..00000000 --- a/cmake/Modules/LightStepClangTidy.cmake +++ /dev/null @@ -1,33 +0,0 @@ -find_program(CLANG_TIDY_EXE NAMES "clang-tidy" "clang-tidy-5.0" "clang-tidy-6.0" "clang-tidy-7.0" - DOC "Path to clang-tidy executable") -if(NOT CLANG_TIDY_EXE) - message(STATUS "clang-tidy not found.") -else() - message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}") - set(DO_CLANG_TIDY "${CLANG_TIDY_EXE}" -"-checks=*,\ --fuchsia-*,\ --clang-analyzer-alpha.*,\ --llvm-include-order,\ --google-runtime-references,\ --google-build-using-namespace,\ --modernize-make-unique,\ --hicpp-vararg,\ --hicpp-signed-bitwise,\ --hicpp-no-array-decay,\ --cppcoreguidelines-owning-memory,\ --cppcoreguidelines-pro-type-reinterpret-cast,\ --cppcoreguidelines-pro-type-const-cast,\ --cppcoreguidelines-pro-bounds-array-to-pointer-decay,\ --cppcoreguidelines-pro-bounds-constant-array-index,\ --cppcoreguidelines-pro-bounds-pointer-arithmetic,\ --cppcoreguidelines-pro-type-vararg;\ --warnings-as-errors=*") -endif() - -macro(_apply_clang_tidy_if_available TARGET) - if (CLANG_TIDY_EXE) - set_target_properties(${TARGET} PROPERTIES - CXX_CLANG_TIDY "${DO_CLANG_TIDY}") - endif() -endmacro() diff --git a/config.h.in b/config.h.in deleted file mode 100644 index 6e85ef8b..00000000 --- a/config.h.in +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -#cmakedefine LIGHTSTEP_USE_GRPC diff --git a/example/stream/BUILD b/example/stream/BUILD new file mode 100644 index 00000000..304f6a83 --- /dev/null +++ b/example/stream/BUILD @@ -0,0 +1,14 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_binary", +) + +lightstep_cc_binary( + name = "stream", + srcs = [ + "main.cpp", + ], + deps = [ + "//:tracer_lib", + ], +) diff --git a/example/stream/main.cpp b/example/stream/main.cpp new file mode 100644 index 00000000..2f923dc9 --- /dev/null +++ b/example/stream/main.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include // for std::getenv +#include +#include +#include + +static void MakeSpan(opentracing::Tracer& tracer, int span_index) { + std::cout << "Make Span: " << span_index << "\n"; + auto span = tracer.StartSpan("span_" + std::to_string(span_index)); + assert(span != nullptr); + for (int tag_index = 0; tag_index < 25; ++tag_index) { + span->SetTag("tag_" + std::to_string(tag_index), tag_index); + } +} + +int main() { + lightstep::LightStepTracerOptions options; + options.collector_plaintext = true; + options.satellite_endpoints = {{"collector.lightstep.com", 80}}; + options.use_stream_recorder = true; + options.verbose = true; + options.component_name = "Stream"; + if (const char* access_token = std::getenv("LIGHTSTEP_ACCESS_TOKEN")) { + options.access_token = access_token; + } else { + std::cerr << "You must set the environmental variable " + "`LIGHTSTEP_ACCESS_TOKEN` to your access token!\n"; + return -1; + } + + auto tracer = MakeLightStepTracer(std::move(options)); + assert(tracer != nullptr); + for (int i = 0; i < 1000; ++i) { + MakeSpan(*tracer, i); + } + std::cout << "Closing tracer\n"; + tracer->Close(); + return 0; +} diff --git a/example/tutorial/BUILD b/example/tutorial/BUILD new file mode 100644 index 00000000..ffd68794 --- /dev/null +++ b/example/tutorial/BUILD @@ -0,0 +1,16 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_binary", +) + +lightstep_cc_binary( + name = "tutorial", + srcs = [ + "text_map_carrier.h", + "tutorial.cpp", + ], + deps = [ + "//:tracer_lib", + ], +) + diff --git a/example/tutorial/text_map_carrier.h b/example/tutorial/text_map_carrier.h index 22829e8b..17dde4eb 100644 --- a/example/tutorial/text_map_carrier.h +++ b/example/tutorial/text_map_carrier.h @@ -5,27 +5,28 @@ #include #include -using opentracing::TextMapReader; -using opentracing::TextMapWriter; -using opentracing::expected; -using opentracing::string_view; - -class TextMapCarrier : public TextMapReader, public TextMapWriter { +class TextMapCarrier : public opentracing::TextMapReader, + public opentracing::TextMapWriter { public: TextMapCarrier(std::unordered_map& text_map) : text_map_(text_map) {} - expected Set(string_view key, string_view value) const override { + opentracing::expected Set( + opentracing::string_view key, + opentracing::string_view value) const override { text_map_[key] = value; return {}; } - expected ForeachKey( - std::function(string_view key, string_view value)> f) - const override { + opentracing::expected ForeachKey( + std::function(opentracing::string_view key, + opentracing::string_view value)> + f) const override { for (const auto& key_value : text_map_) { auto result = f(key_value.first, key_value.second); - if (!result) return result; + if (!result) { + return result; + } } return {}; } diff --git a/include/lightstep/BUILD b/include/lightstep/BUILD new file mode 100644 index 00000000..80a98eaf --- /dev/null +++ b/include/lightstep/BUILD @@ -0,0 +1,52 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "binary_carrier_interface", + hdrs = [ + "binary_carrier.h", + ], + deps = [ + "//lightstep-tracer-common:lightstep_carrier_proto_cc", + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + ], +) + +lightstep_cc_library( + name = "transporter_interface", + hdrs = [ + "transporter.h", + ], + external_deps = [ + "@com_google_protobuf//:protobuf", + "@io_opentracing_cpp//:opentracing", + ], +) + +lightstep_cc_library( + name = "metrics_observer_interface", + hdrs = [ + "metrics_observer.h", + ], +) + +lightstep_cc_library( + name = "tracer_interface", + hdrs = [ + "tracer.h", + ], + deps = [ + ":transporter_interface", + ":metrics_observer_interface", + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + ], +) diff --git a/include/lightstep/binary_carrier.h b/include/lightstep/binary_carrier.h index 9c56c1d4..415acc83 100644 --- a/include/lightstep/binary_carrier.h +++ b/include/lightstep/binary_carrier.h @@ -6,7 +6,7 @@ namespace lightstep { class LightStepBinaryReader : public opentracing::CustomCarrierReader { public: - LightStepBinaryReader(const BinaryCarrier* carrier) noexcept + explicit LightStepBinaryReader(const BinaryCarrier* carrier) noexcept : carrier_{carrier} {} opentracing::expected> Extract( diff --git a/include/lightstep/metrics_observer.h b/include/lightstep/metrics_observer.h index 75beef5e..14f6162e 100644 --- a/include/lightstep/metrics_observer.h +++ b/include/lightstep/metrics_observer.h @@ -4,15 +4,24 @@ namespace lightstep { // MetricsObserver can be used to track LightStep tracer events. class MetricsObserver { public: + MetricsObserver() noexcept = default; + + MetricsObserver(const MetricsObserver&) noexcept = default; + + MetricsObserver(MetricsObserver&&) noexcept = default; + virtual ~MetricsObserver() = default; + MetricsObserver& operator=(const MetricsObserver&) noexcept = default; + MetricsObserver& operator=(MetricsObserver&&) noexcept = default; + // OnSpansSent records spans transported. - virtual void OnSpansSent(int /*num_spans*/) {} + virtual void OnSpansSent(int /*num_spans*/) noexcept {} // OnSpansDropped records spans dropped. - virtual void OnSpansDropped(int /*num_spans*/) {} + virtual void OnSpansDropped(int /*num_spans*/) noexcept {} // OnFlush records flush events by the recorder. - virtual void OnFlush() {} + virtual void OnFlush() noexcept {} }; } // namespace lightstep diff --git a/include/lightstep/tracer.h b/include/lightstep/tracer.h index 256be52f..07932fe9 100644 --- a/include/lightstep/tracer.h +++ b/include/lightstep/tracer.h @@ -56,10 +56,20 @@ struct LightStepTracerOptions { // available on your account page at https://app.lightstep.com/account std::string access_token; - // The host, port, and whether or not to use plaintext communication for the - // collector. Ignored if a custom transporter is used. + // The host and port of collector. Ignored if a custom transporter is used. + // + // Note: Use `satellite_endpoints` when using the streaming recorder with + // `use_stream_recorder` == true. std::string collector_host = "collector-grpc.lightstep.com"; uint32_t collector_port = 443; + + // A list of satellite endpoints to upload spans to. + // + // Note: Only used when `use_stream_recorder` is true. + std::vector> satellite_endpoints = { + {"collector.lightstep.com", static_cast(80)}}; + + // Whether or not to use plaintext when communicating with the satellite. bool collector_plaintext = false; // `tags` are arbitrary key-value pairs that apply to all spans generated by @@ -97,6 +107,9 @@ struct LightStepTracerOptions { // concurrently, then the user is responsible for synchronization. bool use_thread = true; + // Enable streaming recorder for faster uploading of spans. + bool use_stream_recorder = false; + // `reporting_period` is the maximum duration of time between sending spans // to a collector. If zero, the default will be used; and ignored if // `use_thread` is false. @@ -137,9 +150,21 @@ class LightStepTracer : public opentracing::Tracer { noexcept; virtual bool Flush() noexcept = 0; + + virtual bool FlushWithTimeout( + std::chrono::system_clock::duration timeout) noexcept = 0; + + template + bool FlushWithTimeout(std::chrono::duration timeout) noexcept { + return this->FlushWithTimeout( + std::chrono::duration_cast( + timeout)); + } }; // Returns a std::shared_ptr to a LightStepTracer or nullptr on failure. std::shared_ptr MakeLightStepTracer( LightStepTracerOptions&& options) noexcept; + +std::shared_ptr MakeLightStepTracer(const char* config); } // namespace lightstep diff --git a/include/lightstep/transporter.h b/include/lightstep/transporter.h index c715ced8..17221870 100644 --- a/include/lightstep/transporter.h +++ b/include/lightstep/transporter.h @@ -9,8 +9,16 @@ namespace lightstep { // AsyncTransporter. class Transporter { public: + Transporter() noexcept = default; + + Transporter(const Transporter&) noexcept = default; + Transporter(Transporter&&) noexcept = default; + virtual ~Transporter() = default; + Transporter& operator=(const Transporter&) noexcept = default; + Transporter& operator=(Transporter&&) noexcept = default; + // Generates a valid artificial collector response. // // Can be used by testing code to to simulate the interaction with a LightStep @@ -34,8 +42,15 @@ class AsyncTransporter : public Transporter { // Callback interface used by Send. class Callback { public: + Callback() noexcept = default; + Callback(const Callback&) noexcept = default; + Callback(Callback&&) noexcept = default; + virtual ~Callback() = default; + Callback& operator=(const Callback&) noexcept = default; + Callback& operator=(Callback&&) noexcept = default; + virtual void OnSuccess() noexcept = 0; virtual void OnFailure(std::error_code error) noexcept = 0; diff --git a/lightstep-tracer-common/BUILD b/lightstep-tracer-common/BUILD index 03418c5c..f2e2cdf7 100644 --- a/lightstep-tracer-common/BUILD +++ b/lightstep-tracer-common/BUILD @@ -2,7 +2,7 @@ proto_library( name = "collector_proto", srcs = ["collector.proto"], deps = [ - "@lightstep_vendored_googleapis//:googleapis_proto", + "@com_github_googleapis_googleapis//google/api:annotations_proto", "@com_google_protobuf//:timestamp_proto", ], visibility = ["//visibility:public"], diff --git a/lightstep-tracer-common/third_party/googleapis/BUILD b/lightstep-tracer-common/third_party/googleapis/BUILD deleted file mode 100644 index 2adec340..00000000 --- a/lightstep-tracer-common/third_party/googleapis/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -proto_library( - name = "googleapis_proto", - srcs = [ - "google/api/annotations.proto", - "google/api/http.proto", - ], - deps = [ - "@com_google_protobuf//:descriptor_proto", - ], - visibility = ["//visibility:public"], -) diff --git a/lightstep-tracer-configuration/tracer_configuration.proto b/lightstep-tracer-configuration/tracer_configuration.proto index 2be05eed..b128d2c0 100644 --- a/lightstep-tracer-configuration/tracer_configuration.proto +++ b/lightstep-tracer-configuration/tracer_configuration.proto @@ -7,6 +7,12 @@ option objc_class_prefix = "LSPB"; option java_multiple_files = true; option java_package = "com.lightstep.tracer.grpc"; +message Endpoint { + // The Host and port of a network address + string host = 1; + uint32 port = 2; +} + message TracerConfiguration { // `component_name` is the human-readable identity of the instrumented // process. I.e., if one drew a block diagram of the distributed system, @@ -46,4 +52,10 @@ message TracerConfiguration { // // Note: `ssl_root_certificates` should follow the PEM format. string ssl_root_certificates = 10; + + // Enable streaming recorder for faster uploading of spans. + bool use_stream_recorder = 11; + + // A list of satellite endpoints to upload spans to. + repeated Endpoint satellite_endpoints = 12; } diff --git a/lightstep-tracer-configuration/tracer_configuration.schema.json b/lightstep-tracer-configuration/tracer_configuration.schema.json index bbc08d2a..c53ae746 100644 --- a/lightstep-tracer-configuration/tracer_configuration.schema.json +++ b/lightstep-tracer-configuration/tracer_configuration.schema.json @@ -56,6 +56,35 @@ "ssl_root_certificates": { "type": "string", "description": "`ssl_root_certificates` specifies the CA certificates to use when\ntransporting spans to the collector. If not set, LightStep will try to\nuse CA certificates located in standard system locations.\n\nNote: `ssl_root_certificates` should follow the PEM format." + }, + + "use_stream_recorder": { + "type": "boolean", + "description": "`use_stream_recorder` enables the streaming recorder for faster uploading of spans." + }, + + "satellite_endpoints": { + "type": "array", + "items": { "$ref": "#/definitions/endpoint" }, + "description": "A list of satellite endpoints to upload spans to." + } + }, + "definitions": { + "endpoint": { + "type": "object", + "required": [ "host", "port" ], + "properties": { + "host": { + "type": "string", + "description": "The host of an address." + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "description": "The port of an address." + } + } } } } diff --git a/scripts/run_clang_format.sh b/scripts/fix_format.sh similarity index 59% rename from scripts/run_clang_format.sh rename to scripts/fix_format.sh index 9a2d3972..caaa1c87 100755 --- a/scripts/run_clang_format.sh +++ b/scripts/fix_format.sh @@ -1,3 +1,5 @@ #!/bin/sh find . -path ./3rd_party -prune -o \( -name '*.h' -or -name '*.cpp' \) \ -exec clang-format -i {} \; +find . -path ./3rd_party -prune -o \( -name '*.go' \) \ + -exec go fmt {} \; diff --git a/src/common/BUILD b/src/common/BUILD new file mode 100644 index 00000000..0f9c05ec --- /dev/null +++ b/src/common/BUILD @@ -0,0 +1,162 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "atomic_bit_set_lib", + private_hdrs = [ + "atomic_bit_set.h", + ], + srcs = [ + "atomic_bit_set.cpp", + ], +) + +lightstep_cc_library( + name = "bipart_memory_stream_lib", + private_hdrs = [ + "bipart_memory_stream.h", + ], + srcs = [ + "bipart_memory_stream.cpp", + ], + external_deps = [ + "@com_google_protobuf//:protobuf", + ], +) + +lightstep_cc_library( + name = "circular_buffer_lib", + private_hdrs = [ + "circular_buffer.h", + ], + srcs = [ + "circular_buffer.cpp", + ], +) + +lightstep_cc_library( + name = "chunk_circular_buffer_lib", + private_hdrs = [ + "chunk_circular_buffer.h", + ], + srcs = [ + "chunk_circular_buffer.cpp", + ], + deps = [ + ":atomic_bit_set_lib", + ":bipart_memory_stream_lib", + ":circular_buffer_lib", + ":function_ref_lib", + ":utility_lib", + ], +) + +lightstep_cc_library( + name = "function_ref_lib", + private_hdrs = [ + "function_ref.h", + ], +) + +lightstep_cc_library( + name = "in_memory_stream_lib", + private_hdrs = [ + "in_memory_stream.h", + ], + srcs = [ + "in_memory_stream.cpp", + ], +) + +lightstep_cc_library( + name = "logger_lib", + private_hdrs = [ + "logger.h", + ], + srcs = [ + "logger.cpp", + ], + deps = [ + "//include/lightstep:tracer_interface", + ], +) + +lightstep_cc_library( + name = "noncopyable_lib", + private_hdrs = [ + "noncopyable.h", + ], +) + +lightstep_cc_library( + name = "random_lib", + private_hdrs = [ + "random.h", + ], + srcs = [ + "random.cpp", + ], + deps = [ + "//3rd_party/randutils:randutils_lib", + ], + linkopts = [ + "-pthread", + ], +) + +lightstep_cc_library( + name = "protobuf_lib", + private_hdrs = [ + "protobuf.h", + ], + srcs = [ + "protobuf.cpp", + ], + external_deps = [ + "@com_google_protobuf//:protobuf", + ], +) + +lightstep_cc_library( + name = "utility_lib", + private_hdrs = [ + "utility.h", + ], + srcs = [ + "utility.cpp", + ], + deps = [ + "//lightstep-tracer-common:collector_proto_cc", + ":logger_lib", + ], + external_deps = [ + "@com_google_protobuf//:protobuf", + "@io_opentracing_cpp//:opentracing", + ], +) + +lightstep_cc_library( + name = "condition_variable_wrapper_lib", + private_hdrs = [ + "condition_variable_wrapper.h", + ] +) + +lightstep_cc_library( + name = "random_traverser_lib", + private_hdrs = [ + "random_traverser.h", + ], + srcs = [ + "random_traverser.cpp", + ], + deps = [ + ":function_ref_lib", + ":random_lib", + ], +) diff --git a/src/common/atomic_bit_set.cpp b/src/common/atomic_bit_set.cpp new file mode 100644 index 00000000..b2911c4f --- /dev/null +++ b/src/common/atomic_bit_set.cpp @@ -0,0 +1,52 @@ +#include "common/atomic_bit_set.h" + +namespace lightstep { +static constexpr AtomicBitSet::BlockType one = 1; + +//------------------------------------------------------------------------------ +// constructor +//------------------------------------------------------------------------------ +AtomicBitSet::AtomicBitSet(size_t size) + : blocks_(size / bits_per_block + + static_cast(size % bits_per_block > 0)) {} + +//------------------------------------------------------------------------------ +// ComputeBlockIndex +//------------------------------------------------------------------------------ +int AtomicBitSet::ComputeBlockIndex(int bit_index) const noexcept { + return bit_index / bits_per_block; +} + +//------------------------------------------------------------------------------ +// ComputeBitOffSet +//------------------------------------------------------------------------------ +int AtomicBitSet::ComputeBitOffSet(int bit_index) const noexcept { + return bit_index % bits_per_block; +} + +//------------------------------------------------------------------------------ +// Test +//------------------------------------------------------------------------------ +bool AtomicBitSet::Test(int bit_index) const noexcept { + auto mask = one << ComputeBitOffSet(bit_index); + return static_cast(blocks_[ComputeBlockIndex(bit_index)].load() & mask); +} + +//------------------------------------------------------------------------------ +// Reset +//------------------------------------------------------------------------------ +bool AtomicBitSet::Reset(int bit_index) noexcept { + auto mask = one << ComputeBitOffSet(bit_index); + return static_cast( + blocks_[ComputeBlockIndex(bit_index)].fetch_and(~mask) & mask); +} + +//------------------------------------------------------------------------------ +// Set +//------------------------------------------------------------------------------ +bool AtomicBitSet::Set(int bit_index) noexcept { + auto mask = one << ComputeBitOffSet(bit_index); + return static_cast( + blocks_[ComputeBlockIndex(bit_index)].fetch_or(mask) & mask); +} +} // namespace lightstep diff --git a/src/common/atomic_bit_set.h b/src/common/atomic_bit_set.h new file mode 100644 index 00000000..db8a4e19 --- /dev/null +++ b/src/common/atomic_bit_set.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace lightstep { +/** + * An atomic bit set of dynamic size. + * + * Modeled after Folly's AtomicBitSet. See + * https://github.com/facebook/folly/blob/cd1bdc912603c0358ba733d379a74ae90ab3a437/folly/AtomicBitSet.h + */ +class AtomicBitSet { + public: + using BlockType = uint64_t; + + explicit AtomicBitSet(size_t size); + + /** + * Tests whether a given bit is set. + * @param bit_index identifies the bit to test. + * @return the value of the specified bit. + */ + bool Test(int bit_index) const noexcept; + + /** + * Resets a given bit to 0. + * @param bit_index identifies the bit to reset. + * @return the previous value of the given bit. + */ + bool Reset(int bit_index) noexcept; + + /** + * Sets a given bit to 1. + * @param bit_index identifies the bit to set. + * @return the previous value of the given bit. + */ + bool Set(int bit_index) noexcept; + + private: + static constexpr int bits_per_block = + static_cast(std::numeric_limits::digits); + + std::vector> blocks_; + + int ComputeBlockIndex(int bit_index) const noexcept; + + int ComputeBitOffSet(int bit_index) const noexcept; +}; +} // namespace lightstep diff --git a/src/common/bipart_memory_stream.cpp b/src/common/bipart_memory_stream.cpp new file mode 100644 index 00000000..efe2d864 --- /dev/null +++ b/src/common/bipart_memory_stream.cpp @@ -0,0 +1,65 @@ +#include "common/bipart_memory_stream.h" + +namespace lightstep { +//------------------------------------------------------------------------------ +// constructor +//------------------------------------------------------------------------------ +BipartMemoryOutputStream::BipartMemoryOutputStream(char* data1, size_t size1, + char* data2, + size_t size2) noexcept + : data1_{data1}, size1_{size1}, data2_{data2}, size2_{size2} {} + +BipartMemoryInputStream::BipartMemoryInputStream(const char* data1, + size_t size1, + const char* data2, + size_t size2) noexcept + : data1_{data1}, size1_{size1}, data2_{data2}, size2_{size2} {} + +//------------------------------------------------------------------------------ +// Next +//------------------------------------------------------------------------------ +bool BipartMemoryOutputStream::Next(void** data, int* size) { + if (num_bytes_written_ < size1_) { + *data = static_cast(data1_ + num_bytes_written_); + *size = static_cast(size1_ - num_bytes_written_); + num_bytes_written_ += *size; + return true; + } + auto num_bytes_written2 = num_bytes_written_ - size1_; + if (num_bytes_written2 < size2_) { + *data = static_cast(data2_ + num_bytes_written2); + *size = static_cast(size2_ - num_bytes_written2); + num_bytes_written_ += *size; + return true; + } + return false; +} + +bool BipartMemoryInputStream::Next(const void** data, int* size) { + if (num_bytes_read_ < size1_) { + *data = static_cast(data1_ + num_bytes_read_); + *size = static_cast(size1_ - num_bytes_read_); + num_bytes_read_ += *size; + return true; + } + auto num_bytes_read2 = num_bytes_read_ - size1_; + if (num_bytes_read2 < size2_) { + *data = static_cast(data2_ + num_bytes_read2); + *size = static_cast(size2_ - num_bytes_read2); + num_bytes_read_ += *size; + return true; + } + return false; +} + +//------------------------------------------------------------------------------ +// Skip +//------------------------------------------------------------------------------ +bool BipartMemoryInputStream::Skip(int count) { + if (count + num_bytes_read_ > size1_ + size2_) { + return false; + } + num_bytes_read_ += count; + return true; +} +} // namespace lightstep diff --git a/src/common/bipart_memory_stream.h b/src/common/bipart_memory_stream.h new file mode 100644 index 00000000..f3c1f4b2 --- /dev/null +++ b/src/common/bipart_memory_stream.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +namespace lightstep { +/** + * A ZeroCopyOutputStream that can be used to write to two separate blocks of + * memory. + * + * Note: this can be used with a circular buffer to write directly into the + * buffer if it wraps at the ends. + */ +class BipartMemoryOutputStream final + : public google::protobuf::io::ZeroCopyOutputStream { + public: + BipartMemoryOutputStream(char* data1, size_t size1, char* data2, + size_t size2) noexcept; + + // ZeroCopyOutputStream + bool Next(void** data, int* size) override; + + void BackUp(int count) override { num_bytes_written_ -= count; } + + google::protobuf::int64 ByteCount() const override { + return static_cast(num_bytes_written_); + } + + private: + char* data1_; + size_t size1_; + char* data2_; + size_t size2_; + size_t num_bytes_written_ = 0; +}; + +/** + * A ZeroCopyInputStream that can be used to read from two separate blocks of + * memory. + * + * See BipartMemoryOutputStream + */ +class BipartMemoryInputStream final + : public google::protobuf::io::ZeroCopyInputStream { + public: + BipartMemoryInputStream(const char* data1, size_t size1, const char* data2, + size_t size2) noexcept; + + // ZeroCopyInputStream + bool Next(const void** data, int* size) override; + + void BackUp(int count) final { num_bytes_read_ -= count; } + + google::protobuf::int64 ByteCount() const override { + return static_cast(num_bytes_read_); + } + + bool Skip(int count) override; + + private: + const char* data1_; + size_t size1_; + const char* data2_; + size_t size2_; + size_t num_bytes_read_ = 0; +}; +} // namespace lightstep diff --git a/src/common/chunk_circular_buffer.cpp b/src/common/chunk_circular_buffer.cpp new file mode 100644 index 00000000..f27420e9 --- /dev/null +++ b/src/common/chunk_circular_buffer.cpp @@ -0,0 +1,109 @@ +#include "common/chunk_circular_buffer.h" +#include "common/bipart_memory_stream.h" +#include "common/utility.h" + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +ChunkCircularBuffer::ChunkCircularBuffer(size_t max_bytes) + : ready_flags_{max_bytes + 1}, buffer_{max_bytes} {} + +//-------------------------------------------------------------------------------------------------- +// Add +//-------------------------------------------------------------------------------------------------- +bool ChunkCircularBuffer::Add(Serializer serializer, size_t size) noexcept { + assert(size > 0); + static const auto line_terminator = "\r\n"; + std::array chunk_buffer; // Note: We add space + // for an additional + // byte to account for + // snprintf's null + // terminator. + auto num_chunk_size_chars = + std::snprintf(chunk_buffer.data(), chunk_buffer.size(), "%llX", + static_cast(size)); + assert(num_chunk_size_chars > 0); + auto placement = + buffer_.Reserve(static_cast(num_chunk_size_chars) + 4 + size); + if (placement.data1 == nullptr) { + return false; + } + BipartMemoryOutputStream zero_copy_stream{placement.data1, placement.size1, + placement.data2, placement.size2}; + google::protobuf::io::CodedOutputStream output_stream{&zero_copy_stream}; + output_stream.WriteRaw(static_cast(chunk_buffer.data()), + num_chunk_size_chars); + output_stream.WriteRaw(static_cast(line_terminator), 2); + serializer(output_stream); + output_stream.WriteRaw(static_cast(line_terminator), 2); + auto prev_ready_state = + ready_flags_.Set(static_cast(placement.data1 - buffer_.data())); + assert(!prev_ready_state); + return true; +} + +//-------------------------------------------------------------------------------------------------- +// Allot +//-------------------------------------------------------------------------------------------------- +int ChunkCircularBuffer::Allot() noexcept { + int num_chunks_allotted = 0; + while (true) { + auto placement = buffer_.PeekFromPosition(num_bytes_allotted_); + if (placement.size1 == 0) { + return num_chunks_allotted; + } + if (!ready_flags_.Reset( + static_cast(placement.data1 - buffer_.data()))) { + return num_chunks_allotted; + } + BipartMemoryInputStream stream{placement.data1, placement.size1, + placement.data2, placement.size2}; + size_t chunk_size; + auto was_successful = ReadChunkHeader(stream, chunk_size); + assert(was_successful); + num_bytes_allotted_ += stream.ByteCount() + chunk_size + 2; + assert(stream.Skip(static_cast(chunk_size + 2))); + ++num_chunks_allotted; + } +} + +//-------------------------------------------------------------------------------------------------- +// Consume +//-------------------------------------------------------------------------------------------------- +void ChunkCircularBuffer::Consume(size_t num_bytes) noexcept { + assert(num_bytes <= num_bytes_allotted_); + num_bytes_allotted_ -= num_bytes; + buffer_.Consume(num_bytes); +} + +//-------------------------------------------------------------------------------------------------- +// FindChunk +//-------------------------------------------------------------------------------------------------- +std::tuple ChunkCircularBuffer::FindChunk( + const char* start, const char* ptr) const noexcept { + auto position = buffer_.ComputePosition(start); + auto index = buffer_.ComputePosition(ptr); + assert(position <= num_bytes_allotted_); + assert(index <= num_bytes_allotted_); + assert(position <= index); + int chunk_count = 0; + while (true) { + auto placement = buffer_.PeekFromPosition(position); + BipartMemoryInputStream stream{placement.data1, placement.size1, + placement.data2, placement.size2}; + size_t chunk_size; + auto was_successful = ReadChunkHeader(stream, chunk_size); + assert(was_successful); + auto total_size = stream.ByteCount() + chunk_size + 2; + if (index < position + total_size) { + return std::make_tuple(buffer_.Peek(position, total_size), chunk_count); + } + ++chunk_count; + position += total_size; + } +} +} // namespace lightstep diff --git a/src/common/chunk_circular_buffer.h b/src/common/chunk_circular_buffer.h new file mode 100644 index 00000000..41918cc9 --- /dev/null +++ b/src/common/chunk_circular_buffer.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +#include "common/atomic_bit_set.h" +#include "common/circular_buffer.h" +#include "common/function_ref.h" + +#include + +namespace lightstep { +/** + * Maintains a lock-free circular buffer of serialized segments. Each segment is + * framed for http/1.1 streaming as a chunk. + * + * See https://en.wikipedia.org/wiki/Chunked_transfer_encoding + */ +class ChunkCircularBuffer { + public: + using Serializer = + FunctionRef; + + explicit ChunkCircularBuffer(size_t max_bytes); + + /** + * Serializes data into the circular buffer as a chunk. + * @param serializer supplies a function that writes data into a + * CodedOutputStream that writes data into the circular buffer. + * @param size supplies the number of bytes of the serialization. + * @return true if the the serialization was added, or false if not enough + * space could be reserved. + */ + bool Add(Serializer serializer, size_t size) noexcept; + + /** + * Marks memory starting from the circular buffer's tail where serialization + * has finished. Once the memory is alloted is safe to read. + * @return the number of chunks allotted + * + * Note: Cannot be called concurrently with Consume. + */ + int Allot() noexcept; + + /** + * Frees serialized space in the circular buffer starting from the tail. + * @param num_bytes supplies the number of bytes to consume. + * + * Note: Only bytes that have been allotted can be freed. + */ + void Consume(size_t num_bytes) noexcept; + + /** + * Locates the chunk in the circular buffer that contains a given pointer. + * @param start a chunk boundary position to start searching from. + * @param ptr the pointer to search for. + * @return the chunk in the circular buffer that contains ptr and the number + * of preceding chunks. + * + * Note: both start and ptr must be within the allotment. + */ + std::tuple FindChunk(const char* start, + const char* ptr) const + noexcept; + + /** + * @return A placement referencing all data that has been allotted. + */ + CircularBufferConstPlacement allotment() const noexcept { + return buffer_.Peek(0, num_bytes_allotted_); + } + + /** + * @return the CircularBuffer where chunks are stored. + */ + const CircularBuffer& buffer() const noexcept { return buffer_; } + + /** + * @return the number of bytes that have been allotted. + */ + size_t num_bytes_allotted() const noexcept { return num_bytes_allotted_; } + + private: + AtomicBitSet ready_flags_; + CircularBuffer buffer_; + size_t num_bytes_allotted_{0}; +}; +} // namespace lightstep diff --git a/src/common/circular_buffer.cpp b/src/common/circular_buffer.cpp new file mode 100644 index 00000000..b3a10dfd --- /dev/null +++ b/src/common/circular_buffer.cpp @@ -0,0 +1,94 @@ +#include "common/circular_buffer.h" + +#include +#include + +namespace lightstep { +//------------------------------------------------------------------------------ +// constructor +//------------------------------------------------------------------------------ +CircularBuffer::CircularBuffer(size_t max_size) + : data_{new char[max_size + 1]}, capacity_{max_size + 1} {} + +//------------------------------------------------------------------------------ +// Reserve +//------------------------------------------------------------------------------ +CircularBufferPlacement CircularBuffer::Reserve(size_t num_bytes) noexcept { + uint64_t new_head; + uint64_t head = head_; + while (true) { + uint64_t tail = tail_; + + // Because we update head_ before tail_, it's possible that tail > head; if + // that's the case, then the compare_exchange_weak check below will fail and + // we'll reloop, but we use std::min here to ensure that we don't get some + // nonsensical result for the free_space because of overflow. + auto free_space = + max_size() - static_cast((head - std::min(head, tail))); + if (free_space < num_bytes) { + return {nullptr, 0, nullptr, 0}; + } + + new_head = head + num_bytes; + if (head_.compare_exchange_weak(head, new_head, std::memory_order_release, + std::memory_order_relaxed)) { + break; + } + } + return getPlacement(head, num_bytes); +} + +//------------------------------------------------------------------------------ +// Peek +//------------------------------------------------------------------------------ +CircularBufferConstPlacement CircularBuffer::Peek(size_t index, + size_t num_bytes) const + noexcept { + assert(index + num_bytes <= size()); + return getPlacement(tail_ + index, num_bytes); +} + +//------------------------------------------------------------------------------ +// Consume +//------------------------------------------------------------------------------ +void CircularBuffer::Consume(size_t num_bytes) noexcept { + assert(tail_ + num_bytes <= head_); + tail_ += num_bytes; +} + +//-------------------------------------------------------------------------------------------------- +// ComputePosition +//-------------------------------------------------------------------------------------------------- +size_t CircularBuffer::ComputePosition(const char* ptr) const noexcept { + auto index = static_cast(ptr - this->data()); + uint64_t tail = tail_; + tail = tail % capacity_; + if (index >= tail) { + return index - tail; + } + return capacity_ - tail + index; +} + +//-------------------------------------------------------------------------------------------------- +// size +//-------------------------------------------------------------------------------------------------- +size_t CircularBuffer::size() const noexcept { + uint64_t tail = tail_; + uint64_t head = head_; + assert(tail <= head); + return head - tail; +} + +//-------------------------------------------------------------------------------------------------- +// getPlacement +//-------------------------------------------------------------------------------------------------- +CircularBufferPlacement CircularBuffer::getPlacement( + size_t index, size_t num_bytes) noexcept { + index = index % capacity_; + auto data1 = data_.get() + index; + auto size1 = std::min(capacity_ - index, num_bytes); + auto data2 = data_.get(); + auto size2 = num_bytes - size1; + return {data1, size1, data2, size2}; +} +} // namespace lightstep diff --git a/src/common/circular_buffer.h b/src/common/circular_buffer.h new file mode 100644 index 00000000..6b01de70 --- /dev/null +++ b/src/common/circular_buffer.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include + +namespace lightstep { +/** + * Represents a block of data in a circular buffer. + * + * If the data is contiguous in memory, data1 points to the area its area in + * memory and size1 equals the total size. Otherwise, if the data wraps the ends + * around the ends of the circular buffer, data1, size1 represents the first + * block and data2, size2 represents the second block. + */ +struct CircularBufferPlacement { + char* data1; + size_t size1; + char* data2; + size_t size2; +}; + +/** + * Represents a const block of data in a circular buffer. + * + * See CircularBufferPlacement. + */ +struct CircularBufferConstPlacement { + CircularBufferConstPlacement() noexcept = default; + + CircularBufferConstPlacement(const char* data1_, size_t size1_, + const char* data2_, size_t size2_) noexcept + : data1{data1_}, size1{size1_}, data2{data2_}, size2{size2_} {} + + CircularBufferConstPlacement(CircularBufferPlacement placement) noexcept + : CircularBufferConstPlacement{placement.data1, placement.size1, + placement.data2, placement.size2} {} + + const char* data1; + size_t size1; + const char* data2; + size_t size2; +}; + +/* + * A lock-free circular buffer that supports multiple concurrent producers + * and a single consumer. + */ +class CircularBuffer { + public: + explicit CircularBuffer(size_t max_size); + + /** + * Atomically reserves space in the buffer. + * @param num_bytes supplies the number of bytes to reserve. + * @return a CircularBufferPlacement for the reserved space if successful; + * otherwise, a placement with data1 == nullptr if there is not enough space + * available. + */ + CircularBufferPlacement Reserve(size_t num_bytes) noexcept; + + /** + * Peeks at memory stored within the buffer. + * @param index supplies the position from the tail to take memory from. + * @param num_bytes supplies the number of bytes to peek at. + * @return a placement pointing to the requested memory. + * + * Note: This function cannot be called concurrently with Consume. + */ + CircularBufferConstPlacement Peek(size_t index, size_t num_bytes) const + noexcept; + + /** + * Peeks at all memory stored in the buffer starting at a given position. + * @param index supplies the position from the tail to take memory from. + * @return a placement pointing to the requested memory. + * + * Note: This function cannot be called concurrently with Consume. + */ + CircularBufferConstPlacement PeekFromPosition(size_t index) const noexcept { + return Peek(index, size() - index); + } + + /** + * Frees reserved space in the buffer starting from the tail. + * @param num_bytes supplies the number of bytes to consume. + */ + void Consume(size_t num_bytes) noexcept; + + /** + * Determine how far a pointer is from the tail of the circular buffer. + * @param ptr the pointer to compute the position of. + * @return the number of bytes ptr is from the tail of the circular buffer. + */ + size_t ComputePosition(const char* ptr) const noexcept; + + /** + * @return the maximum number of bytes that can be stored in the buffer. + */ + size_t max_size() const noexcept { return capacity_ - 1; } + + /** + * @return the number of bytes of free space available in the buffer. + */ + size_t free_space() const noexcept { return max_size() - size(); } + + /** + * @return true if the buffer is empty. + */ + bool empty() const noexcept { return head_ == tail_; } + + /** + * @return the number of bytes stored in the circular buffer. + */ + size_t size() const noexcept; + + /** + * @return a pointer to the buffer's memory. + */ + const char* data() const noexcept { return data_.get(); } + + /** + * @return the number of bytes consumed from the circular buffer. + */ + uint64_t num_bytes_consumed() const noexcept { return tail_; } + + /** + * @return the number of bytes added to the circular buffer. + */ + uint64_t num_bytes_produced() const noexcept { return head_; } + + private: + std::unique_ptr data_; + size_t capacity_; + std::atomic head_{0}; + std::atomic tail_{0}; + + CircularBufferPlacement getPlacement(size_t index, size_t num_bytes) noexcept; + + CircularBufferConstPlacement getPlacement(size_t index, + size_t num_bytes) const noexcept { + return const_cast(this)->getPlacement(index, num_bytes); + } +}; +} // namespace lightstep diff --git a/src/condition_variable_wrapper.h b/src/common/condition_variable_wrapper.h similarity index 83% rename from src/condition_variable_wrapper.h rename to src/common/condition_variable_wrapper.h index 4441ca59..b1f4a397 100644 --- a/src/condition_variable_wrapper.h +++ b/src/common/condition_variable_wrapper.h @@ -11,8 +11,16 @@ namespace lightstep { // run without blocking timeout. class ConditionVariableWrapper { public: + ConditionVariableWrapper() noexcept = default; + + ConditionVariableWrapper(ConditionVariableWrapper&&) = delete; + ConditionVariableWrapper(const ConditionVariableWrapper&) = delete; + virtual ~ConditionVariableWrapper() = default; + ConditionVariableWrapper& operator=(ConditionVariableWrapper&&) = delete; + ConditionVariableWrapper& operator=(const ConditionVariableWrapper&) = delete; + virtual std::chrono::steady_clock::time_point Now() const = 0; virtual bool WaitFor(std::unique_lock& lock, diff --git a/src/common/function_ref.h b/src/common/function_ref.h new file mode 100644 index 00000000..e1cc3ec0 --- /dev/null +++ b/src/common/function_ref.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + +namespace lightstep { +template +class FunctionRef; + +/** + * Non-owning function reference that can be used as a more performant + * replacement for std::function when ownership sematics aren't needed. + * + * See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0792r0.html + * + * Based off of https://stackoverflow.com/a/39087660/4447365 + */ +template +class FunctionRef { + void* callable_ = nullptr; + R (*invoker_)(void*, Args...) = nullptr; + + template + using FunctionPointer = decltype(std::addressof(std::declval())); + + template + void BindTo(F& f) noexcept { + callable_ = static_cast(std::addressof(f)); + invoker_ = [](void* callable_, Args... args) -> R { + return (*static_cast>(callable_))( + std::forward(args)...); + }; + } + + template + void BindTo(R_in (*f)(Args_in...)) noexcept { + using F = decltype(f); + if (f == nullptr) { + return BindTo(nullptr); + } + callable_ = reinterpret_cast(f); + invoker_ = [](void* callable_, Args... args) -> R { + return (F(callable_))(std::forward(args)...); + }; + } + + void BindTo(std::nullptr_t) noexcept { + callable_ = nullptr; + invoker_ = nullptr; + } + + public: + template < + class F, + typename std::enable_if< + !std::is_same::type>{}, + int>::type = 0, + typename std::enable_if< + std::is_convertible::type, R>{}, + int>::type = 0> + FunctionRef(F&& f) { + BindTo(f); // not forward + } + + FunctionRef(std::nullptr_t) {} + + FunctionRef(const FunctionRef&) noexcept = default; + FunctionRef(FunctionRef&&) noexcept = default; + + R operator()(Args... args) const { + return invoker_(callable_, std::forward(args)...); + } + + explicit operator bool() const { return invoker_; } +}; +} // namespace lightstep diff --git a/src/in_memory_stream.cpp b/src/common/in_memory_stream.cpp similarity index 95% rename from src/in_memory_stream.cpp rename to src/common/in_memory_stream.cpp index 657fdad7..e43934f1 100644 --- a/src/in_memory_stream.cpp +++ b/src/common/in_memory_stream.cpp @@ -1,4 +1,4 @@ -#include "in_memory_stream.h" +#include "common/in_memory_stream.h" namespace lightstep { //------------------------------------------------------------------------------ diff --git a/src/in_memory_stream.h b/src/common/in_memory_stream.h similarity index 100% rename from src/in_memory_stream.h rename to src/common/in_memory_stream.h diff --git a/src/logger.cpp b/src/common/logger.cpp similarity index 98% rename from src/logger.cpp rename to src/common/logger.cpp index 3f4628ce..ebb2444c 100644 --- a/src/logger.cpp +++ b/src/common/logger.cpp @@ -1,4 +1,4 @@ -#include "logger.h" +#include "common/logger.h" #include #include diff --git a/src/logger.h b/src/common/logger.h similarity index 100% rename from src/logger.h rename to src/common/logger.h diff --git a/src/common/noncopyable.h b/src/common/noncopyable.h new file mode 100644 index 00000000..89a9415c --- /dev/null +++ b/src/common/noncopyable.h @@ -0,0 +1,24 @@ +#pragma once + +namespace lightstep { +/** + * Helper class to ensure a class doesn't define unwanted copy methods. + * + * Modeled after boost::noncopyable + * See + * https://www.boost.org/doc/libs/1_68_0/libs/core/doc/html/core/noncopyable.html + */ +class Noncopyable { + public: + Noncopyable(const Noncopyable&) = delete; + Noncopyable(Noncopyable&&) = delete; + + ~Noncopyable() noexcept = default; + + Noncopyable& operator=(const Noncopyable&) = delete; + Noncopyable& operator=(Noncopyable&&) = delete; + + protected: + Noncopyable() noexcept = default; +}; +} // namespace lightstep diff --git a/src/common/protobuf.cpp b/src/common/protobuf.cpp new file mode 100644 index 00000000..11900459 --- /dev/null +++ b/src/common/protobuf.cpp @@ -0,0 +1,50 @@ +#include "common/protobuf.h" + +namespace lightstep { +static const uint32_t WireTypeLengthDelimited = 2; +static const int ProtoFieldShift = 3; + +//-------------------------------------------------------------------------------------------------- +// ComputeStreamMessageKey +//-------------------------------------------------------------------------------------------------- +static uint32_t ComputeStreamMessageKey(uint32_t field, uint32_t wire_type) { + // See description of encoding process at + // https://developers.google.com/protocol-buffers/docs/encoding + return (field << ProtoFieldShift) | wire_type; +} + +//-------------------------------------------------------------------------------------------------- +// WriteEmbeddedMessage +//-------------------------------------------------------------------------------------------------- +void WriteEmbeddedMessage(google::protobuf::io::CodedOutputStream& stream, + uint32_t field, + const google::protobuf::Message& message) { + WriteEmbeddedMessage(stream, field, message.ByteSizeLong(), message); +} + +void WriteEmbeddedMessage(google::protobuf::io::CodedOutputStream& stream, + uint32_t field, size_t message_size, + const google::protobuf::Message& message) { + stream.WriteVarint32(ComputeStreamMessageKey(field, WireTypeLengthDelimited)); + stream.WriteVarint64(message_size); + message.SerializeWithCachedSizes(&stream); +} + +//-------------------------------------------------------------------------------------------------- +// ComputeEmbeddedMessageSerializationSize +//-------------------------------------------------------------------------------------------------- +size_t ComputeEmbeddedMessageSerializationSize( + uint32_t field, const google::protobuf::Message& message) { + return ComputeEmbeddedMessageSerializationSize(field, message.ByteSizeLong()); +} + +size_t ComputeEmbeddedMessageSerializationSize(uint32_t field, + size_t message_size) { + size_t result = google::protobuf::io::CodedOutputStream::VarintSize32( + ComputeStreamMessageKey(field, WireTypeLengthDelimited)); + result += + google::protobuf::io::CodedOutputStream::VarintSize64(message_size) + + message_size; + return result; +} +} // namespace lightstep diff --git a/src/common/protobuf.h b/src/common/protobuf.h new file mode 100644 index 00000000..b4401de4 --- /dev/null +++ b/src/common/protobuf.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +namespace lightstep { +/** + * Serializes a protobuf message in the format that allows it to be embedded in + * to another message. + * @param stream the stream to serialize into. + * @param field the field number for the message. + * @param message the message to serialize. + */ +void WriteEmbeddedMessage(google::protobuf::io::CodedOutputStream& stream, + uint32_t field, + const google::protobuf::Message& message); + +/** + * Serializes a protobuf message in the format that allows it to be embedded in + * to another message. + * @param stream the stream to serialize into. + * @param field the field number for the message. + * @param size the serialization size of the message. + * @param message the message to serialize. + */ +void WriteEmbeddedMessage(google::protobuf::io::CodedOutputStream& stream, + uint32_t field, size_t message_size, + const google::protobuf::Message& message); + +/** + * Computes the size of an embedded protobuf message serialization. + * @param field the field number for the message. + * @param message the embedded message. + * @return the number of bytes it takes to serialize the message. + */ +size_t ComputeEmbeddedMessageSerializationSize( + uint32_t field, const google::protobuf::Message& message); + +/** + * Computes the size of an embedded protobuf message serialization. + * @param field the field number for the message. + * @param size the serialization size of the message. + * @return the number of bytes it takes to serialize the message. + */ +size_t ComputeEmbeddedMessageSerializationSize(uint32_t field, + size_t message_size); +} // namespace lightstep diff --git a/src/common/random.cpp b/src/common/random.cpp new file mode 100644 index 00000000..fb6bde84 --- /dev/null +++ b/src/common/random.cpp @@ -0,0 +1,58 @@ +#include "common/random.h" + +#include + +#include "lightstep/randutils.h" + +#include + +namespace lightstep { +//------------------------------------------------------------------------------ +// TlsRandomNumberGenerator +//------------------------------------------------------------------------------ +// Wraps a thread_local random number generator, but adds a fork handler so that +// the generator will be correctly seeded after forking. +// +// See https://stackoverflow.com/q/51882689/4447365 and +// https://github.com/opentracing-contrib/nginx-opentracing/issues/52 +namespace { +class TlsRandomNumberGenerator { + public: + TlsRandomNumberGenerator() { ::pthread_atfork(nullptr, nullptr, OnFork); } + + static std::mt19937_64& engine() { return base_generator_.engine(); } + + private: + static thread_local lightstep::randutils::mt19937_64_rng base_generator_; + + static void OnFork() { base_generator_.seed(); } +}; + +thread_local lightstep::randutils::mt19937_64_rng + TlsRandomNumberGenerator::base_generator_{}; +} // namespace + +//-------------------------------------------------------------------------------------------------- +// GetRandomNumberGenerator +//-------------------------------------------------------------------------------------------------- +std::mt19937_64& GetRandomNumberGenerator() { + static TlsRandomNumberGenerator random_number_generator; + return TlsRandomNumberGenerator::engine(); +} + +//-------------------------------------------------------------------------------------------------- +// GenerateId +//-------------------------------------------------------------------------------------------------- +uint64_t GenerateId() { return GetRandomNumberGenerator()(); } + +//-------------------------------------------------------------------------------------------------- +// GenerateRandomDuration +//-------------------------------------------------------------------------------------------------- +std::chrono::nanoseconds GenerateRandomDuration(std::chrono::nanoseconds a, + std::chrono::nanoseconds b) { + assert(a <= b); + std::uniform_int_distribution distribution{ + a.count(), b.count()}; + return std::chrono::nanoseconds{distribution(GetRandomNumberGenerator())}; +} +} // namespace lightstep diff --git a/src/common/random.h b/src/common/random.h new file mode 100644 index 00000000..786f4ce8 --- /dev/null +++ b/src/common/random.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace lightstep { +/** + * @return a seeded thread-local random number generator. + */ +std::mt19937_64& GetRandomNumberGenerator(); + +/** + * @return a random 64-bit number. + */ +uint64_t GenerateId(); + +/** + * Uniformily generates a random duration within a given range. + * @param a supplies the smallest duration to generate. + * @param b supplies the largest duration to generate. + * @return a random duration within [a,b] + */ +std::chrono::nanoseconds GenerateRandomDuration(std::chrono::nanoseconds a, + std::chrono::nanoseconds b); + +template +std::chrono::duration GenerateRandomDuration( + std::chrono::duration a, + std::chrono::duration b) { + return std::chrono::duration_cast>( + GenerateRandomDuration( + std::chrono::duration_cast(a), + std::chrono::duration_cast(b))); +} +} // namespace lightstep diff --git a/src/common/random_traverser.cpp b/src/common/random_traverser.cpp new file mode 100644 index 00000000..8a6e7be9 --- /dev/null +++ b/src/common/random_traverser.cpp @@ -0,0 +1,33 @@ +#include "common/random_traverser.h" + +#include +#include + +#include "common/random.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +RandomTraverser::RandomTraverser(int n) : indexes_(n) { + std::iota(indexes_.begin(), indexes_.end(), 0); +} + +//-------------------------------------------------------------------------------------------------- +// ForEachIndex +//-------------------------------------------------------------------------------------------------- +bool RandomTraverser::ForEachIndex(Callback callback) { + if (indexes_.empty()) { + return true; + } + auto n_minus_1 = static_cast(indexes_.size()) - 1; + for (int i = 0; i < n_minus_1; ++i) { + std::uniform_int_distribution distribution{i, n_minus_1}; + std::swap(indexes_[i], indexes_[distribution(GetRandomNumberGenerator())]); + if (!callback(indexes_[i])) { + return false; + } + } + return callback(indexes_.back()); +} +} // namespace lightstep diff --git a/src/common/random_traverser.h b/src/common/random_traverser.h new file mode 100644 index 00000000..d145f6c5 --- /dev/null +++ b/src/common/random_traverser.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "common/function_ref.h" + +namespace lightstep { +/** + * Iterate over indexes in random order. + */ +class RandomTraverser { + public: + using Callback = FunctionRef; + + explicit RandomTraverser(int n); + + /** + * Iterates over over indexes in random order. + * @param callback the callback to call with each index. Callback can return + * false to exit early. + * @return false if callback exited early; otherwise, true. + */ + bool ForEachIndex(Callback callback); + + private: + std::vector indexes_; +}; +} // namespace lightstep diff --git a/src/utility.cpp b/src/common/utility.cpp similarity index 88% rename from src/utility.cpp rename to src/common/utility.cpp index 017da90a..14a8bef9 100644 --- a/src/utility.cpp +++ b/src/common/utility.cpp @@ -1,7 +1,8 @@ -#include "utility.h" -#include +#include "common/utility.h" + #include #include + #include "lightstep-tracer-common/collector.pb.h" #include @@ -31,37 +32,15 @@ google::protobuf::Timestamp ToTimestamp( return ts; } -//------------------------------------------------------------------------------ -// TlsRandomNumberGenerator -//------------------------------------------------------------------------------ -// Wraps a thread_local random number generator, but adds a fork handler so that -// the generator will be correctly seeded after forking. -// -// See https://stackoverflow.com/q/51882689/4447365 and -// https://github.com/opentracing-contrib/nginx-opentracing/issues/52 -namespace { -class TlsRandomNumberGenerator { - public: - TlsRandomNumberGenerator() { pthread_atfork(nullptr, nullptr, OnFork); } - - static std::mt19937_64& engine() { return base_generator_.engine(); } - - private: - static thread_local lightstep::randutils::mt19937_64_rng base_generator_; - - static void OnFork() { base_generator_.seed(); } -}; - -thread_local lightstep::randutils::mt19937_64_rng - TlsRandomNumberGenerator::base_generator_{}; -} // namespace - -//------------------------------------------------------------------------------ -// GenerateId -//------------------------------------------------------------------------------ -uint64_t GenerateId() { - static TlsRandomNumberGenerator random_number_generator; - return TlsRandomNumberGenerator::engine()(); +timeval ToTimeval(std::chrono::microseconds microseconds) { + timeval result; + auto num_microseconds = microseconds.count(); + const size_t microseconds_in_second = 1000000; + result.tv_sec = + static_cast(num_microseconds / microseconds_in_second); + result.tv_usec = + static_cast(num_microseconds % microseconds_in_second); + return result; } //------------------------------------------------------------------------------ @@ -297,7 +276,7 @@ opentracing::string_view Uint64ToHex(uint64_t x, char* output) { output[i * 2 + 1] = digits[lookup_index + 1]; x >>= 8; } - return {output, 16}; + return {output, Num64BitHexDigits}; } //------------------------------------------------------------------------------ @@ -377,4 +356,37 @@ opentracing::expected HexToUint64(opentracing::string_view s) { return result; } + +//-------------------------------------------------------------------------------------------------- +// ReadChunkHeader +//-------------------------------------------------------------------------------------------------- +bool ReadChunkHeader(google::protobuf::io::ZeroCopyInputStream& stream, + size_t& chunk_size) { + std::array digits; + int digit_index = 0; + const char* data; + int size; + while (stream.Next(reinterpret_cast(&data), &size)) { + for (int i = 0; i < size; ++i) { + if (data[i] == '\r') { + auto chunk_size_maybe = + HexToUint64({digits.data(), static_cast(digit_index)}); + if (!chunk_size_maybe) { + return false; + } + stream.BackUp(size - i); + if (!stream.Skip(2)) { + return false; + } + chunk_size = *chunk_size_maybe; + return true; + } + if (digit_index >= static_cast(digits.size())) { + return false; + } + digits[digit_index++] = data[i]; + } + } + return false; +} } // namespace lightstep diff --git a/src/utility.h b/src/common/utility.h similarity index 60% rename from src/utility.h rename to src/common/utility.h index d89186d1..9f6532d4 100644 --- a/src/utility.h +++ b/src/common/utility.h @@ -3,18 +3,34 @@ #include #include #include +#include +#include #include +#include "common/logger.h" #include "lightstep-tracer-common/collector.pb.h" -#include "logger.h" + +#include + +#include namespace lightstep { +const size_t Num64BitHexDigits = std::numeric_limits::digits / 4; + // Convert a std::chrono::system_clock::time_point to the time value used // by protobuf. google::protobuf::Timestamp ToTimestamp( const std::chrono::system_clock::time_point& t); -// Generates a random uint64_t. -uint64_t GenerateId(); +/** + * + */ +timeval ToTimeval(std::chrono::microseconds microseconds); + +template +inline timeval toTimeval(std::chrono::duration duration) { + return toTimeval( + std::chrono::duration_cast(duration)); +} // Attempts to determine the name of the executable invoked. Returns // "c++-program" if unsuccessful. @@ -36,4 +52,13 @@ opentracing::string_view Uint64ToHex(uint64_t x, char* output); // Converts a hexidecimal number to a 64-bit integer. Either returns the number // or an error code. opentracing::expected HexToUint64(opentracing::string_view s); + +/** + * Reads the header for an http/1.1 streaming chunk. + * @param stream supplies the ZeroCopyInputStream to read from. + * @param chunk_size is where the chunk's size is written. + * @return true if the header was successfully read. + */ +bool ReadChunkHeader(google::protobuf::io::ZeroCopyInputStream& stream, + size_t& chunk_size); } // namespace lightstep diff --git a/src/network/BUILD b/src/network/BUILD new file mode 100644 index 00000000..4194dd05 --- /dev/null +++ b/src/network/BUILD @@ -0,0 +1,130 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "ip_address_lib", + private_hdrs = [ + "ip_address.h", + ], + srcs = [ + "ip_address.cpp", + ], +) + +lightstep_cc_library( + name = "event_lib", + private_hdrs = [ + "event.h", + "event_base.h", + ], + srcs = [ + "event.cpp", + "event_base.cpp", + ], + deps = [ + "//src/common:utility_lib", + ], + external_deps = [ + "@com_github_libevent_libevent//:libevent", + ], +) + +lightstep_cc_library( + name = "timer_event_lib", + private_hdrs = [ + "timer_event.h", + ], + srcs = [ + "timer_event.cpp", + ], + deps = [ + "//src/common:utility_lib", + ":event_lib", + ], + external_deps = [ + "@com_github_libevent_libevent//:libevent", + ], +) + +lightstep_cc_library( + name = "fragment_input_stream_lib", + private_hdrs = [ + "fragment_input_stream.h", + ], + srcs = [ + "fragment_input_stream.cpp", + ], + deps = [ + "//src/common:function_ref_lib", + ], +) + +lightstep_cc_library( + name = "fragment_array_input_stream_lib", + private_hdrs = [ + "fragment_array_input_stream.h", + ], + srcs = [ + "fragment_array_input_stream.cpp", + ], + deps = [ + ":fragment_input_stream_lib", + ], +) + +lightstep_cc_library( + name = "vector_write_lib", + private_hdrs = [ + "vector_write.h", + ], + srcs = [ + "vector_write.cpp", + ], + deps = [ + ":fragment_input_stream_lib", + ], +) + +lightstep_cc_library( + name = "dns_resolver_interface", + private_hdrs = [ + "dns_resolver.h", + ], + deps = [ + "//src/common:logger_lib", + "//src/common:function_ref_lib", + ":event_lib", + ":ip_address_lib", + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + ], +) + +lightstep_cc_library( + name = "no_dns_resolver_lib", + srcs = [ + "no_dns_resolver.cpp", + ], + deps = [ + ":dns_resolver_interface", + ], +) + +lightstep_cc_library( + name = "socket_lib", + private_hdrs = [ + "socket.h", + ], + srcs = [ + "socket.cpp", + ], + deps = [ + ":ip_address_lib", + ], +) diff --git a/src/network/ares_dns_resolver/BUILD b/src/network/ares_dns_resolver/BUILD new file mode 100644 index 00000000..28ca3602 --- /dev/null +++ b/src/network/ares_dns_resolver/BUILD @@ -0,0 +1,26 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "ares_dns_resolver_lib", + srcs = [ + "ares_dns_resolver.h", + "ares_dns_resolver.cpp", + "ares_library_handle.h", + "ares_library_handle.cpp", + ], + deps = [ + "//src/common:noncopyable_lib", + "//src/network:dns_resolver_interface", + "//src/network:event_lib", + "//src/network:timer_event_lib", + ], + external_deps = [ + "@com_github_cares_cares//:ares", + ], +) diff --git a/src/network/ares_dns_resolver/ares_dns_resolver.cpp b/src/network/ares_dns_resolver/ares_dns_resolver.cpp new file mode 100644 index 00000000..134b2047 --- /dev/null +++ b/src/network/ares_dns_resolver/ares_dns_resolver.cpp @@ -0,0 +1,192 @@ +#include "network/ares_dns_resolver/ares_dns_resolver.h" + +#include + +#include "network/ares_dns_resolver/ares_library_handle.h" + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// AresDnsResolution +//-------------------------------------------------------------------------------------------------- +namespace { +class AresDnsResolution final : public DnsResolution { + public: + explicit AresDnsResolution(const hostent& hosts) noexcept : hosts_{hosts} {} + + // DnsResolution + bool ForeachIpAddress( + const FunctionRef& f) const override { + for (int i = 0; hosts_.h_addr_list[i] != nullptr; ++i) { + IpAddress ip_address; + if (hosts_.h_addrtype == AF_INET) { + ip_address = + IpAddress{*reinterpret_cast(hosts_.h_addr_list[i])}; + } else if (hosts_.h_addrtype == AF_INET6) { + ip_address = + IpAddress{*reinterpret_cast(hosts_.h_addr_list[i])}; + } else { + continue; + } + if (!f(ip_address)) { + return false; + } + } + return true; + } + + private: + const hostent& hosts_; +}; +} // namespace + +//-------------------------------------------------------------------------------------------------- +// OnDnsResolution +//-------------------------------------------------------------------------------------------------- +static void OnDnsResolution(void* context, int status, int /*timeouts*/, + hostent* hosts) noexcept { + auto callback = static_cast(context); + if (status != ARES_SUCCESS) { + return callback->OnDnsResolution(DnsResolution{}, ares_strerror(status)); + } + if (hosts == nullptr) { + return callback->OnDnsResolution(DnsResolution{}, {}); + } + AresDnsResolution resolution{*hosts}; + callback->OnDnsResolution(resolution, {}); +} + +//-------------------------------------------------------------------------------------------------- +// SetAresOptions +//-------------------------------------------------------------------------------------------------- +static void SetAresOptions(const DnsResolverOptions& resolver_options, + ares_options& options, int& option_mask) { + options.udp_port = resolver_options.resolution_server_port; + option_mask |= ARES_OPT_UDP_PORT; + options.tcp_port = resolver_options.resolution_server_port; + option_mask |= ARES_OPT_TCP_PORT; + + if (resolver_options.timeout != std::chrono::milliseconds{0}) { + options.timeout = resolver_options.timeout.count(); + option_mask |= ARES_OPT_TIMEOUTMS; + } + + if (resolver_options.resolution_servers.empty()) { + return; + } + + options.servers = + const_cast(resolver_options.resolution_servers.data()); + options.nservers = + static_cast(resolver_options.resolution_servers.size()); + option_mask |= ARES_OPT_SERVERS; +} + +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +AresDnsResolver::AresDnsResolver(Logger& logger, EventBase& event_base, + const DnsResolverOptions& resolver_options) + : logger_{logger}, + event_base_{event_base}, + timer_{event_base, + MakeTimerCallback(), + static_cast(this)} { + if (ares_library_handle_ == nullptr) { + throw std::runtime_error{"ares failed to initialize"}; + } + struct ares_options options = {}; + options.sock_state_cb = [](void* context, int file_descriptor, int read, + int write) { + static_cast(context)->OnSocketStateChange(file_descriptor, + read, write); + }; + options.sock_state_cb_data = static_cast(this); + int option_mask = 0; + option_mask |= ARES_OPT_SOCK_STATE_CB; + SetAresOptions(resolver_options, options, option_mask); + auto status = ares_init_options(&channel_, &options, option_mask); + if (status != ARES_SUCCESS) { + logger_.Error("ares_init_options failed: ", ares_strerror(status)); + throw std::runtime_error{"ares_init_options failed"}; + } +} + +//-------------------------------------------------------------------------------------------------- +// destructor +//-------------------------------------------------------------------------------------------------- +AresDnsResolver::~AresDnsResolver() noexcept { ares_destroy(channel_); } + +//-------------------------------------------------------------------------------------------------- +// Resolve +//-------------------------------------------------------------------------------------------------- +void AresDnsResolver::Resolve(const char* name, int family, + DnsResolutionCallback& callback) noexcept { + ares_gethostbyname(channel_, name, family, OnDnsResolution, + static_cast(&callback)); +} + +//-------------------------------------------------------------------------------------------------- +// OnSocketStateChange +//-------------------------------------------------------------------------------------------------- +void AresDnsResolver::OnSocketStateChange(int file_descriptor, int read, + int write) noexcept try { + if (read == 0 && write == 0) { + socket_events_.erase(file_descriptor); + return; + } + short options = 0; + if (read == 1) { + options |= EV_READ; + } + if (write == 1) { + options |= EV_WRITE; + } + Event event{event_base_, file_descriptor, options, + MakeEventCallback(), + static_cast(this)}; + event.Add(nullptr); + socket_events_[file_descriptor] = std::move(event); +} catch (const std::exception& e) { + logger_.Error("failed to set up ares socket events: ", e.what()); +} + +//-------------------------------------------------------------------------------------------------- +// OnEvent +//-------------------------------------------------------------------------------------------------- +void AresDnsResolver::OnEvent(int file_descriptor, short what) noexcept try { + auto read_file_descriptor = + (what & EV_READ) != 0 ? file_descriptor : ARES_SOCKET_BAD; + auto write_file_descriptor = + (what & EV_WRITE) != 0 ? file_descriptor : ARES_SOCKET_BAD; + ares_process_fd(channel_, read_file_descriptor, write_file_descriptor); + UpdateTimer(); +} catch (const std::exception& e) { + logger_.Error("failed to process ares event: ", e.what()); +} + +//-------------------------------------------------------------------------------------------------- +// OnTimeout +//-------------------------------------------------------------------------------------------------- +void AresDnsResolver::OnTimeout() noexcept { OnEvent(ARES_SOCKET_BAD, 0); } + +//-------------------------------------------------------------------------------------------------- +// UpdateTimer +//-------------------------------------------------------------------------------------------------- +void AresDnsResolver::UpdateTimer() { + timeval timeout; + auto tv = ares_timeout(channel_, nullptr, &timeout); + timer_.Reset(tv); +} + +//-------------------------------------------------------------------------------------------------- +// MakeDnsResolver +//-------------------------------------------------------------------------------------------------- +std::unique_ptr MakeDnsResolver( + Logger& logger, EventBase& event_base, const DnsResolverOptions& options) { + return std::unique_ptr{ + new AresDnsResolver{logger, event_base, options}}; +} +} // namespace lightstep diff --git a/src/network/ares_dns_resolver/ares_dns_resolver.h b/src/network/ares_dns_resolver/ares_dns_resolver.h new file mode 100644 index 00000000..7d7a6c5f --- /dev/null +++ b/src/network/ares_dns_resolver/ares_dns_resolver.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "common/noncopyable.h" +#include "network/ares_dns_resolver/ares_library_handle.h" +#include "network/dns_resolver.h" +#include "network/event.h" +#include "network/timer_event.h" + +struct ares_channeldata; + +namespace lightstep { +/** + * A DnsResolver using the c-ares library. + */ +class AresDnsResolver final : public DnsResolver, private Noncopyable { + public: + AresDnsResolver(Logger& logger, EventBase& event_base, + const DnsResolverOptions& resolver_options); + + ~AresDnsResolver() noexcept override; + + // DnsResolver + void Resolve(const char* name, int family, + DnsResolutionCallback& callback) noexcept override; + + private: + std::shared_ptr ares_library_handle_{ + AresLibraryHandle::Instance}; + Logger& logger_; + EventBase& event_base_; + ares_channeldata* channel_; + + std::unordered_map socket_events_; + TimerEvent timer_; + + void OnSocketStateChange(int file_descriptor, int read, int write) noexcept; + + void OnEvent(int file_descriptor, short what) noexcept; + + void OnTimeout() noexcept; + + void UpdateTimer(); +}; +} // namespace lightstep diff --git a/src/network/ares_dns_resolver/ares_library_handle.cpp b/src/network/ares_dns_resolver/ares_library_handle.cpp new file mode 100644 index 00000000..654ab9cc --- /dev/null +++ b/src/network/ares_dns_resolver/ares_library_handle.cpp @@ -0,0 +1,37 @@ +#include "network/ares_dns_resolver/ares_library_handle.h" + +#include +#include +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +AresLibraryHandle::AresLibraryHandle() { + auto status = ares_library_init(ARES_LIB_INIT_ALL); + if (status != 0) { + throw std::runtime_error{std::string{"ares_library_init failed: "} + + ares_strerror(status)}; + } +} + +//-------------------------------------------------------------------------------------------------- +// destructor +//-------------------------------------------------------------------------------------------------- +AresLibraryHandle::~AresLibraryHandle() noexcept { ares_library_cleanup(); } + +//-------------------------------------------------------------------------------------------------- +// Instance +//-------------------------------------------------------------------------------------------------- +std::shared_ptr AresLibraryHandle::Instance = [] { + try { + return std::shared_ptr{new AresLibraryHandle{}}; + } catch (const std::exception& e) { + std::fprintf(stderr, "Failed to initialize AresLibraryHandle: %s\n", + e.what()); + return std::shared_ptr{nullptr}; + } +}(); +} // namespace lightstep diff --git a/src/network/ares_dns_resolver/ares_library_handle.h b/src/network/ares_dns_resolver/ares_library_handle.h new file mode 100644 index 00000000..6c9f5c50 --- /dev/null +++ b/src/network/ares_dns_resolver/ares_library_handle.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "common/noncopyable.h" + +namespace lightstep { +/** + * Manages initialization of the c-ares library. + */ +class AresLibraryHandle : private Noncopyable { + public: + ~AresLibraryHandle() noexcept; + + // We use a global variable to initialize the ares library because + // ares_library_init is not thread-safe and must be called before other + // threads are started. + // + // See https://c-ares.haxx.se/ares_library_init.html + static std::shared_ptr Instance; + + private: + AresLibraryHandle(); +}; +} // namespace lightstep diff --git a/src/network/dns_resolver.h b/src/network/dns_resolver.h new file mode 100644 index 00000000..b23e223b --- /dev/null +++ b/src/network/dns_resolver.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include + +#include "common/function_ref.h" +#include "common/logger.h" +#include "network/event_base.h" +#include "network/ip_address.h" + +#include + +namespace lightstep { +/** + * Internal options that can be used to control the behavior of dns resolvers. + */ +struct DnsResolverOptions { + std::vector resolution_servers; + uint16_t resolution_server_port{53}; + std::chrono::milliseconds timeout{2000}; +}; + +/** + * The result of a dns resolution. + */ +class DnsResolution { + public: + DnsResolution() noexcept = default; + DnsResolution(const DnsResolution&) noexcept = default; + DnsResolution(DnsResolution&&) noexcept = default; + + virtual ~DnsResolution() noexcept = default; + + DnsResolution& operator=(const DnsResolution&) noexcept = default; + DnsResolution& operator=(DnsResolution&&) noexcept = default; + + /** + * Iterate over the ip addresses of a dns resolution. + * @param f supplies a callback to invoke for each ip address. f can return + * false to bail out of the iteration. + * @return true if f returns true for each ip address; false, otherwise. + */ + virtual bool ForeachIpAddress( + const FunctionRef& /*f*/) const { + return true; + } +}; + +/** + * A callback to be invoked when an asynchronous dns resolution completes. + */ +class DnsResolutionCallback { + public: + DnsResolutionCallback() noexcept = default; + DnsResolutionCallback(const DnsResolutionCallback&) noexcept = default; + DnsResolutionCallback(DnsResolutionCallback&&) noexcept = default; + + virtual ~DnsResolutionCallback() noexcept = default; + + DnsResolutionCallback& operator=(const DnsResolutionCallback&) noexcept = + default; + DnsResolutionCallback& operator=(DnsResolutionCallback&&) noexcept = default; + + /** + * The function invoked when dns resolution completes. + * @param dns_resolution supplies the resolved ip addresses. + * @param error_message supplies an error message if dns resolution failed. + */ + virtual void OnDnsResolution( + const DnsResolution& dns_resolution, + opentracing::string_view error_message) noexcept = 0; +}; + +/** + * Interface for an asynchronous dns resolver. + */ +class DnsResolver { + public: + DnsResolver() noexcept = default; + DnsResolver(const DnsResolver&) = delete; + DnsResolver(DnsResolver&&) = delete; + + virtual ~DnsResolver() noexcept = default; + + DnsResolver& operator=(const DnsResolver&) = delete; + DnsResolver& operator=(DnsResolver&&) = delete; + + /** + * Resolves a name and invokes the provided callback when complete. + * @param name supplies the name to resolve. + * @param family identifies whether to query for ipv4 or ipv6 addresses. + * @param callback supplies the callback to invoke when resolution completes. + */ + virtual void Resolve(const char* name, int family, + DnsResolutionCallback& callback) noexcept = 0; +}; + +/** + * Interface to construct a DnsResolver. + * @param logger supplies the place to write logs. + * @param supplies the libevent dispatcher. + * @param options sets the behavior of the dns resolver. + * @return the dns resolver. + */ +std::unique_ptr MakeDnsResolver(Logger& logger, + EventBase& event_base, + const DnsResolverOptions& options); +} // namespace lightstep diff --git a/src/network/event.cpp b/src/network/event.cpp new file mode 100644 index 00000000..66cced7a --- /dev/null +++ b/src/network/event.cpp @@ -0,0 +1,79 @@ +#include "network/event.h" + +#include +#include +#include + +#include "common/utility.h" +#include "network/event_base.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +Event::Event(const EventBase& event_base, int file_descriptor, short options, + Callback callback, void* context) { + event_ = event_new(event_base.libevent_handle(), file_descriptor, options, + callback, context); + if (event_ == nullptr) { + throw std::runtime_error{"event_new failed"}; + } +} + +Event::Event(Event&& other) noexcept : event_{other.event_} { + other.event_ = nullptr; +} + +//-------------------------------------------------------------------------------------------------- +// destructor +//-------------------------------------------------------------------------------------------------- +Event::~Event() noexcept { FreeEvent(); } + +//-------------------------------------------------------------------------------------------------- +// operator= +//-------------------------------------------------------------------------------------------------- +Event& Event::operator=(Event&& other) noexcept { + assert(this != &other); + FreeEvent(); + event_ = other.event_; + other.event_ = nullptr; + return *this; +} + +//-------------------------------------------------------------------------------------------------- +// Add +//-------------------------------------------------------------------------------------------------- +void Event::Add(std::chrono::microseconds timeout) { + assert(event_ != nullptr); + auto tv = ToTimeval(timeout); + Add(&tv); +} + +void Event::Add(timeval* tv) { + assert(event_ != nullptr); + auto rcode = event_add(event_, tv); + if (rcode != 0) { + throw std::runtime_error{"event_add failed"}; + } +} + +//-------------------------------------------------------------------------------------------------- +// Remove +//-------------------------------------------------------------------------------------------------- +void Event::Remove() { + assert(event_ != nullptr); + auto rcode = event_del(event_); + if (rcode != 0) { + throw std::runtime_error{"event_del failed"}; + } +} + +//-------------------------------------------------------------------------------------------------- +// FreeEvent +//-------------------------------------------------------------------------------------------------- +void Event::FreeEvent() noexcept { + if (event_ != nullptr) { + event_free(event_); + } +} +} // namespace lightstep diff --git a/src/network/event.h b/src/network/event.h new file mode 100644 index 00000000..e92054bb --- /dev/null +++ b/src/network/event.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +#include + +struct event; + +namespace lightstep { +class EventBase; + +/** + * Wrapper for libevent's event struct. + */ +class Event { + public: + using Callback = void (*)(int socket, short what, void* context); + + Event() noexcept = default; + + Event(const EventBase& event_base, int file_descriptor, short options, + Callback callback, void* context); + + Event(const Event&) = delete; + + Event(Event&& other) noexcept; + + ~Event() noexcept; + + Event& operator=(const Event&) = delete; + Event& operator=(Event&& other) noexcept; + + /** + * @return the underlying event. + */ + event* libevent_handle() const noexcept { return event_; } + + /** + * Adds an event to dispatching. + * @param timeout supplies the timeout for the event. + */ + void Add(std::chrono::microseconds timeout); + + /** + * Adds an event to dispatching. + * @param timeout supplies the timeout for the event. Null can be used to set + * no timeout. + */ + void Add(timeval* tv); + + /** + * Removes an event from dispatching. + */ + void Remove(); + + private: + event* event_{nullptr}; + + void FreeEvent() noexcept; +}; + +/** + * For a given method, creates an event callback function that can be passed to + * libevent. + * @return a function pointer that can be passed to libevent. + */ +template +Event::Callback MakeEventCallback() { + return [](int socket, short what, void* context) { + (static_cast(context)->*MemberFunction)(socket, what); + }; +} +} // namespace lightstep diff --git a/src/network/event_base.cpp b/src/network/event_base.cpp new file mode 100644 index 00000000..5dc7b923 --- /dev/null +++ b/src/network/event_base.cpp @@ -0,0 +1,85 @@ +#include "event_base.h" + +#include +#include +#include + +#include "common/utility.h" + +#include + +namespace lightstep { +//------------------------------------------------------------------------------ +// constructor +//------------------------------------------------------------------------------ +EventBase::EventBase() { + event_base_ = event_base_new(); + if (event_base_ == nullptr) { + throw std::runtime_error{"event_base_new failed"}; + } +} + +EventBase::EventBase(EventBase&& other) noexcept { + event_base_ = other.event_base_; + other.event_base_ = nullptr; +} + +//------------------------------------------------------------------------------ +// destructor +//------------------------------------------------------------------------------ +EventBase::~EventBase() { + if (event_base_ != nullptr) { + event_base_free(event_base_); + } +} + +//------------------------------------------------------------------------------ +// operator= +//------------------------------------------------------------------------------ +EventBase& EventBase::operator=(EventBase&& other) noexcept { + assert(this != &other); + if (event_base_ != nullptr) { + event_base_free(event_base_); + } + event_base_ = other.event_base_; + other.event_base_ = nullptr; + return *this; +} + +//------------------------------------------------------------------------------ +// Dispatch +//------------------------------------------------------------------------------ +void EventBase::Dispatch() const { + assert(event_base_ != nullptr); + auto rcode = event_base_dispatch(event_base_); + if (rcode != 0) { + std::ostringstream oss; + oss << "event_base_dispatch faild with rcode = " << rcode; + throw std::runtime_error{oss.str()}; + } +} + +//------------------------------------------------------------------------------ +// LoopBreak +//------------------------------------------------------------------------------ +void EventBase::LoopBreak() const { + assert(event_base_ != nullptr); + auto rcode = event_base_loopbreak(event_base_); + if (rcode != 0) { + throw std::runtime_error{"event_base_loopbreak failed"}; + } +} + +//------------------------------------------------------------------------------ +// OnTimeout +//------------------------------------------------------------------------------ +void EventBase::OnTimeout(std::chrono::microseconds timeout, + Event::Callback callback, void* context) { + auto tv = ToTimeval(timeout); + auto rcode = + event_base_once(event_base_, -1, EV_TIMEOUT, callback, context, &tv); + if (rcode != 0) { + throw std::runtime_error{"OnTimeout failed"}; + } +} +} // namespace lightstep diff --git a/src/network/event_base.h b/src/network/event_base.h new file mode 100644 index 00000000..ca909bb5 --- /dev/null +++ b/src/network/event_base.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include "network/event.h" + +struct event_base; + +namespace lightstep { +/** + * Wrapper for libevent's event_base struct. + */ +class EventBase { + public: + EventBase(); + + EventBase(EventBase&& other) noexcept; + EventBase(const EventBase&) = delete; + + ~EventBase(); + + EventBase& operator=(EventBase&& other) noexcept; + EventBase& operator=(const EventBase&) = delete; + + /** + * @return the underlying event_base. + */ + event_base* libevent_handle() const noexcept { return event_base_; } + + /** + * Run the dispatch event loop. + */ + void Dispatch() const; + + /** + * Break out of the dispatch event loop. + */ + void LoopBreak() const; + + /** + * Schedule a callback to be invoked. + * @param timeout indicates when to invoke the callback. + * @param callback supplies the callback to invoke. + * @param context supplies an arbitrary argument to pass to the callback. + */ + void OnTimeout(std::chrono::microseconds timeout, Event::Callback callback, + void* context); + + /** + * Schedule a callback to be invoked. + * + * See other OnTimeout method. + */ + template + void OnTimeout(std::chrono::duration timeout, + Event::Callback callback, void* context) { + OnTimeout(std::chrono::duration_cast(timeout), + callback, context); + } + + private: + event_base* event_base_; +}; +} // namespace lightstep diff --git a/src/network/fragment_array_input_stream.cpp b/src/network/fragment_array_input_stream.cpp new file mode 100644 index 00000000..e473d771 --- /dev/null +++ b/src/network/fragment_array_input_stream.cpp @@ -0,0 +1,83 @@ +#include "network/fragment_array_input_stream.h" + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +FragmentArrayInputStream::FragmentArrayInputStream( + std::initializer_list fragments) + : fragments_{fragments} {} + +//-------------------------------------------------------------------------------------------------- +// operator= +//-------------------------------------------------------------------------------------------------- +FragmentArrayInputStream& FragmentArrayInputStream::operator=( + std::initializer_list fragments) { + fragments_ = fragments; + fragment_index_ = 0; + position_ = 0; + return *this; +} + +//-------------------------------------------------------------------------------------------------- +// num_fragments +//-------------------------------------------------------------------------------------------------- +int FragmentArrayInputStream::num_fragments() const noexcept { + return static_cast(fragments_.size()) - fragment_index_; +} + +//-------------------------------------------------------------------------------------------------- +// ForEachFragment +//-------------------------------------------------------------------------------------------------- +bool FragmentArrayInputStream::ForEachFragment(Callback callback) const + noexcept { + if (fragment_index_ >= static_cast(fragments_.size())) { + return true; + } + + auto current_fragment = fragments_[fragment_index_]; + auto result = + callback(static_cast(static_cast(current_fragment.first) + + position_), + current_fragment.second - position_); + if (!result) { + return false; + } + + for (int i = fragment_index_ + 1; i < static_cast(fragments_.size()); + ++i) { + if (!callback(fragments_[i].first, fragments_[i].second)) { + return false; + } + } + return true; +} + +//-------------------------------------------------------------------------------------------------- +// Clear +//-------------------------------------------------------------------------------------------------- +void FragmentArrayInputStream::Clear() noexcept { + fragment_index_ = 0; + position_ = 0; + fragments_.clear(); +} + +//-------------------------------------------------------------------------------------------------- +// Seek +//-------------------------------------------------------------------------------------------------- +void FragmentArrayInputStream::Seek(int fragment_index, int position) noexcept { + assert(fragment_index_ + fragment_index < + static_cast(fragments_.size())); + if (fragment_index == 0) { + position_ += position; + } else { + fragment_index_ += fragment_index; + position_ = position; + } + assert(fragment_index_ < static_cast(fragments_.size())); + assert(position_ < fragments_[fragment_index_].second); +} +} // namespace lightstep diff --git a/src/network/fragment_array_input_stream.h b/src/network/fragment_array_input_stream.h new file mode 100644 index 00000000..a0023002 --- /dev/null +++ b/src/network/fragment_array_input_stream.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include "network/fragment_input_stream.h" + +namespace lightstep { +/** + * A non-owning stream of fragments. + */ +class FragmentArrayInputStream final : public FragmentInputStream { + public: + FragmentArrayInputStream() noexcept = default; + + explicit FragmentArrayInputStream(std::initializer_list fragments); + + FragmentArrayInputStream& operator=( + std::initializer_list fragments); + + /** + * Reserve memory for fragments. + * @param size the number of fragments to reserve memory for. + */ + void Reserve(size_t size) { fragments_.reserve(size); } + + /** + * Add a fragment to the stream. + * @param fragment the fragment to add. + */ + void Add(Fragment fragment) { fragments_.emplace_back(fragment); } + + // FragmentInputStream + int num_fragments() const noexcept override; + + bool ForEachFragment(Callback callback) const noexcept override; + + void Clear() noexcept override; + + void Seek(int fragment_index, int position) noexcept override; + + private: + std::vector fragments_; + + int fragment_index_{0}; + int position_{0}; +}; +} // namespace lightstep diff --git a/src/network/fragment_input_stream.cpp b/src/network/fragment_input_stream.cpp new file mode 100644 index 00000000..1adac435 --- /dev/null +++ b/src/network/fragment_input_stream.cpp @@ -0,0 +1,37 @@ +#include "network/fragment_input_stream.h" + +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// MakeFragment +//-------------------------------------------------------------------------------------------------- +Fragment MakeFragment(const char* s) noexcept { + return {static_cast(const_cast(s)), + static_cast(std::strlen(s))}; +} + +//-------------------------------------------------------------------------------------------------- +// Consume +//-------------------------------------------------------------------------------------------------- +bool Consume(std::initializer_list fragment_input_streams, + int n) noexcept { + for (auto fragment_input_stream : fragment_input_streams) { + int fragment_index = 0; + auto f = [&n, &fragment_index](void* /*data*/, int size) { + if (n < size) { + return false; + } + ++fragment_index; + n -= size; + return true; + }; + if (!fragment_input_stream->ForEachFragment(f)) { + fragment_input_stream->Seek(fragment_index, n); + return false; + } + fragment_input_stream->Clear(); + } + return true; +} +} // namespace lightstep diff --git a/src/network/fragment_input_stream.h b/src/network/fragment_input_stream.h new file mode 100644 index 00000000..0c54f6f1 --- /dev/null +++ b/src/network/fragment_input_stream.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include + +#include "common/function_ref.h" + +namespace lightstep { +using Fragment = std::pair; + +/** + * Interface for iterating over sequences of fragments. + */ +class FragmentInputStream { + public: + using Callback = FunctionRef; + + FragmentInputStream() noexcept = default; + FragmentInputStream(const FragmentInputStream&) noexcept = default; + FragmentInputStream(FragmentInputStream&&) noexcept = default; + + virtual ~FragmentInputStream() noexcept = default; + + FragmentInputStream& operator=(const FragmentInputStream&) noexcept = default; + FragmentInputStream& operator=(FragmentInputStream&&) noexcept = default; + + /** + * @return the number of fragments in the set. + */ + virtual int num_fragments() const noexcept = 0; + + /** + * @return true if the set contains no fragments. + */ + bool empty() const noexcept { return num_fragments() == 0; } + + /** + * Iterate over each fragment in the set. + * @param callback a callback to call for each fragment. The callback can + * return false to break out of the iteration early. + * @return false if the iteration was interrupted early. + */ + virtual bool ForEachFragment(Callback callback) const noexcept = 0; + + /** + * Repositions the stream to the end of all the fragments. + */ + virtual void Clear() noexcept = 0; + + /** + * Adjust the position of the fragment stream. + * @param fragment_index the new fragment index to reposition to relative to + * the current fragment index. + * @param position the position within fragment_index to reposition to + * relative to the current position. + */ + virtual void Seek(int fragment_index, int position) noexcept = 0; +}; + +/** + * Converts a c-string to a fragment. + * @param s supplies the string to convert. + * @return a fragment for s. + */ +Fragment MakeFragment(const char* s) noexcept; + +/** + * Consumes n bytes from the provided inpute streams. + * @param fragment_input_stream the fragment streams to consume bytes from. + * @param n the number of bytes to consume. + * @return true if all bytes in the streams were consumed; false, othewise. + */ +bool Consume(std::initializer_list fragment_input_streams, + int n) noexcept; +} // namespace lightstep diff --git a/src/network/ip_address.cpp b/src/network/ip_address.cpp new file mode 100644 index 00000000..b0823734 --- /dev/null +++ b/src/network/ip_address.cpp @@ -0,0 +1,131 @@ +#include "network/ip_address.h" + +#include +#include +#include +#include +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +IpAddress::IpAddress() noexcept { + auto& ipv4_addr = reinterpret_cast(data_); + ipv4_addr.sin_family = AF_INET; +} + +IpAddress::IpAddress(const in_addr& addr) noexcept { + auto& ipv4_addr = reinterpret_cast(data_); + ipv4_addr.sin_family = AF_INET; + ipv4_addr.sin_addr = addr; +} + +IpAddress::IpAddress(const in6_addr& addr) noexcept { + auto& ipv6_addr = reinterpret_cast(data_); + ipv6_addr.sin6_family = AF_INET6; + ipv6_addr.sin6_addr = addr; +} + +IpAddress::IpAddress(const sockaddr& addr) noexcept { + if (addr.sa_family == AF_INET) { + reinterpret_cast(data_) = + reinterpret_cast(addr); + } else if (addr.sa_family == AF_INET6) { + reinterpret_cast(data_) = + reinterpret_cast(addr); + } else { + assert(0 && "Unknown address family"); + } +} + +IpAddress::IpAddress(const char* s) { + data_ = {}; + auto& ipv4_addr = reinterpret_cast(data_); + auto rcode = inet_pton(AF_INET, s, static_cast(&ipv4_addr.sin_addr)); + if (rcode == 1) { + ipv4_addr.sin_family = AF_INET; + return; + } + + auto& ipv6_addr = reinterpret_cast(data_); + rcode = inet_pton(AF_INET6, s, static_cast(&ipv6_addr.sin6_addr)); + if (rcode == 1) { + ipv6_addr.sin6_family = AF_INET6; + return; + } + + std::ostringstream oss; + oss << "failed to construct IpAddress from " << s; + throw std::runtime_error{oss.str()}; +} + +IpAddress::IpAddress(const char* s, uint16_t port) : IpAddress{s} { + set_port(port); +} + +//-------------------------------------------------------------------------------------------------- +// set_port +//-------------------------------------------------------------------------------------------------- +void IpAddress::set_port(uint16_t port) noexcept { + if (family() == AF_INET) { + reinterpret_cast(data_).sin_port = htons(port); + return; + } + if (family() == AF_INET6) { + reinterpret_cast(data_).sin6_port = htons(port); + return; + } + assert(0 && "Unknown address family"); +} + +//-------------------------------------------------------------------------------------------------- +// operator== +//-------------------------------------------------------------------------------------------------- +bool operator==(const IpAddress& lhs, const IpAddress& rhs) noexcept { + if (lhs.family() != rhs.family()) { + return false; + } + if (lhs.family() == AF_INET) { + return std::memcmp(static_cast(&lhs.ipv4_address()), + static_cast(&rhs.ipv4_address()), + sizeof(lhs.ipv4_address())) == 0; + } + if (lhs.family() == AF_INET6) { + return std::memcmp(static_cast(&lhs.ipv6_address()), + static_cast(&rhs.ipv6_address()), + sizeof(lhs.ipv6_address())) == 0; + } + assert(0 && "Unknown address family"); + return false; +} + +//-------------------------------------------------------------------------------------------------- +// operator<< +//-------------------------------------------------------------------------------------------------- +std::ostream& operator<<(std::ostream& out, const IpAddress& ip_address) { + std::array buffer = {}; + const char* s = nullptr; + if (ip_address.family() == AF_INET) { + s = inet_ntop(AF_INET, &ip_address.ipv4_address().sin_addr, buffer.data(), + sizeof(buffer)); + } else if (ip_address.family() == AF_INET6) { + s = inet_ntop(AF_INET6, &ip_address.ipv6_address().sin6_addr, buffer.data(), + sizeof(buffer)); + } else { + assert(0 && "Unknown address family"); + } + out << s; + return out; +} + +//-------------------------------------------------------------------------------------------------- +// ToString +//-------------------------------------------------------------------------------------------------- +std::string ToString(const IpAddress& ip_address) { + std::ostringstream oss; + oss << ip_address; + return oss.str(); +} +} // namespace lightstep diff --git a/src/network/ip_address.h b/src/network/ip_address.h new file mode 100644 index 00000000..2a6c7a35 --- /dev/null +++ b/src/network/ip_address.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include + +#include +#include + +namespace lightstep { +/** + * Wraps the system objects for an ip address. + */ +class IpAddress { + public: + IpAddress() noexcept; + + IpAddress(const in_addr& addr) noexcept; + + IpAddress(const in6_addr& addr) noexcept; + + IpAddress(const sockaddr& addr) noexcept; + + explicit IpAddress(const char* s); + + IpAddress(const char* s, uint16_t port); + + /** + * @return the system sockaddr object associated with this ip address. + */ + const sockaddr& addr() const noexcept { + return reinterpret_cast(data_); + } + + /** + * @return the family of the ip address. + */ + sa_family_t family() const noexcept { return data_.ss_family; } + + /** + * @returns for an ipv4 address, returns the system sockaddr_in object. + */ + const sockaddr_in& ipv4_address() const noexcept { + assert(family() == AF_INET); + return reinterpret_cast(data_); + } + + /** + * @returns for an ipv6 address, returns the system sockaddr_in6 object. + */ + const sockaddr_in6& ipv6_address() const noexcept { + assert(family() == AF_INET6); + return reinterpret_cast(data_); + } + + /** + * Sets the port for the ip address. + * @param port supplies the port to assign. + */ + void set_port(uint16_t port) noexcept; + + private: + sockaddr_storage data_{}; +}; + +bool operator==(const IpAddress& lhs, const IpAddress& rhs) noexcept; + +inline bool operator!=(const IpAddress& lhs, const IpAddress& rhs) noexcept { + return !(lhs == rhs); +} + +std::ostream& operator<<(std::ostream& out, const IpAddress& ip_address); + +/** + * Obtains a readable string for the given ip address. + * @param ip_address supplies the address to obtain a string for. + * @return a readable string for the ip address. + */ +std::string ToString(const IpAddress& ip_address); +} // namespace lightstep diff --git a/src/network/no_dns_resolver.cpp b/src/network/no_dns_resolver.cpp new file mode 100644 index 00000000..e4136abd --- /dev/null +++ b/src/network/no_dns_resolver.cpp @@ -0,0 +1,62 @@ +#include "network/dns_resolver.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// SingleIpDnsResolution +//-------------------------------------------------------------------------------------------------- +namespace { +class SingleIpDnsResolution final : public DnsResolution { + public: + explicit SingleIpDnsResolution(const IpAddress& ip_address) noexcept + : ip_address_{ip_address} {} + + bool ForeachIpAddress( + const FunctionRef& f) const override { + return f(ip_address_); + } + + private: + IpAddress ip_address_; +}; +} // namespace + +//-------------------------------------------------------------------------------------------------- +// IpOnlyDnsResolver +//-------------------------------------------------------------------------------------------------- +namespace { +class IpOnlyDnsResolver final : public DnsResolver { + public: + explicit IpOnlyDnsResolver(Logger& logger) noexcept : logger_{logger} {} + + void Resolve(const char* name, int family, + DnsResolutionCallback& callback) noexcept override { + IpAddress ip_address; + try { + ip_address = IpAddress{name}; + } catch (const std::exception& /*e*/) { + logger_.Error("Failed to resolve ", name, + " to an ip address: an explicit ip must be provided or the " + "tracer needs to be rebuilt with c-ares support."); + return callback.OnDnsResolution(DnsResolution{}, {}); + } + if (ip_address.family() == family) { + callback.OnDnsResolution(SingleIpDnsResolution{ip_address}, {}); + } else { + callback.OnDnsResolution(DnsResolution{}, {}); + } + } + + private: + Logger& logger_; +}; +} // namespace + +//-------------------------------------------------------------------------------------------------- +// MakeDnsResolver +//-------------------------------------------------------------------------------------------------- +std::unique_ptr MakeDnsResolver( + Logger& logger, EventBase& /*event_base*/, + const DnsResolverOptions& /*options*/) { + return std::unique_ptr{new IpOnlyDnsResolver{logger}}; +} +} // namespace lightstep diff --git a/src/network/socket.cpp b/src/network/socket.cpp new file mode 100644 index 00000000..2b805b7f --- /dev/null +++ b/src/network/socket.cpp @@ -0,0 +1,125 @@ +#include "network/socket.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace lightstep { +//------------------------------------------------------------------------------ +// constructor +//------------------------------------------------------------------------------ +Socket::Socket() : Socket{AF_INET, SOCK_STREAM} {} + +Socket::Socket(int family, int type) { + file_descriptor_ = ::socket(family, type, 0); + if (file_descriptor_ < 0) { + std::ostringstream oss; + oss << "failed to create socket: " << std::strerror(errno); + throw std::runtime_error{oss.str()}; + } +} + +Socket::Socket(int file_descriptor) noexcept + : file_descriptor_{file_descriptor} {} + +Socket::Socket(Socket&& other) noexcept { + file_descriptor_ = other.file_descriptor_; + other.file_descriptor_ = -1; +} + +//------------------------------------------------------------------------------ +// destructor +//------------------------------------------------------------------------------ +Socket::~Socket() noexcept { Free(); } + +//-------------------------------------------------------------------------------------------------- +// operator= +//-------------------------------------------------------------------------------------------------- +Socket& Socket::operator=(Socket&& other) noexcept { + assert(this != &other); + Free(); + file_descriptor_ = other.file_descriptor_; + other.file_descriptor_ = -1; + return *this; +} + +//------------------------------------------------------------------------------ +// SetNonblocking +//------------------------------------------------------------------------------ +void Socket::SetNonblocking() { + int rcode = ::fcntl(file_descriptor_, F_SETFL, + ::fcntl(file_descriptor_, F_GETFL, 0) | O_NONBLOCK); + if (rcode == -1) { + std::ostringstream oss; + oss << "failed to set the socket as non-blocking: " << std::strerror(errno); + throw std::runtime_error{oss.str()}; + } +} + +//-------------------------------------------------------------------------------------------------- +// Free +//-------------------------------------------------------------------------------------------------- +void Socket::Free() noexcept { + if (file_descriptor_ != -1) { + ::close(file_descriptor_); + } +} + +//------------------------------------------------------------------------------ +// SetReuseAddress +//------------------------------------------------------------------------------ +void Socket::SetReuseAddress() { + int optvalue = 1; + int rcode = ::setsockopt(file_descriptor_, SOL_SOCKET, SO_REUSEADDR, + static_cast(&optvalue), sizeof(int)); + if (rcode == -1) { + std::ostringstream oss; + oss << "failed to set the socket as reusable: : " << std::strerror(errno); + throw std::runtime_error{oss.str()}; + } +} + +//-------------------------------------------------------------------------------------------------- +// Connect +//-------------------------------------------------------------------------------------------------- +int Socket::Connect(const sockaddr& addr, size_t addrlen) noexcept { + assert(file_descriptor_ != -1); + return ::connect(file_descriptor_, &addr, static_cast(addrlen)); +} + +Socket Connect(const IpAddress& ip_address) { + Socket socket{ip_address.family(), SOCK_STREAM}; + socket.SetNonblocking(); + socket.SetReuseAddress(); + int rcode; + switch (ip_address.family()) { + case AF_INET: + rcode = + socket.Connect(ip_address.addr(), sizeof(ip_address.ipv4_address())); + break; + case AF_INET6: + rcode = + socket.Connect(ip_address.addr(), sizeof(ip_address.ipv6_address())); + break; + default: + throw std::runtime_error{"Unknown socket family."}; + } + if (rcode == 0) { + return socket; + } + assert(rcode == -1); + if (errno != EINPROGRESS) { + std::ostringstream oss; + oss << "connect failed: " << std::strerror(errno); + throw std::runtime_error{oss.str()}; + } + return socket; +} +} // namespace lightstep diff --git a/src/network/socket.h b/src/network/socket.h new file mode 100644 index 00000000..dec0364f --- /dev/null +++ b/src/network/socket.h @@ -0,0 +1,63 @@ +#pragma once + +#include "network/ip_address.h" + +namespace lightstep { +/** + * Wraps a system socket structure. + */ +class Socket { + public: + Socket(); + + Socket(int family, int type); + + explicit Socket(int file_descriptor) noexcept; + + Socket(const Socket&) = delete; + + Socket(Socket&& other) noexcept; + + ~Socket() noexcept; + + Socket& operator=(Socket&& other) noexcept; + + Socket& operator=(const Socket&) = delete; + + /** + * @return the file descriptor for this socket. + */ + int file_descriptor() const noexcept { return file_descriptor_; } + + /** + * Makes the socket non-blocking. + */ + void SetNonblocking(); + + /** + * Makes the socket's address be reusable. + */ + void SetReuseAddress(); + + /** + * Establishes a connection to a given address. + * @param addr supplies the sockaddr to connect to. + * @param addrlen supplies the length of the address. + * @return the return code of the system call ::connect. + */ + int Connect(const sockaddr& addr, size_t addrlen) noexcept; + + private: + int file_descriptor_{-1}; + + void Free() noexcept; +}; + +/** + * Establishes a connetion to a provided address. + * @param ip_address supplies the address to connect to. + * @param type supplies the type of connection. + * @return a non-blocking Socket for the connection. + */ +Socket Connect(const IpAddress& ip_address); +} // namespace lightstep diff --git a/src/network/timer_event.cpp b/src/network/timer_event.cpp new file mode 100644 index 00000000..5205f857 --- /dev/null +++ b/src/network/timer_event.cpp @@ -0,0 +1,42 @@ +#include "network/timer_event.h" + +#include +#include + +#include "common/utility.h" + +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +TimerEvent::TimerEvent(const EventBase& event_base, + std::chrono::microseconds interval, + Event::Callback callback, void* context) + : event_{event_base, -1, EV_PERSIST, callback, context}, + tv_{ToTimeval(interval)} { + event_.Add(&tv_); +} + +TimerEvent::TimerEvent(const EventBase& event_base, Event::Callback callback, + void* context) + : event_{event_base, -1, EV_PERSIST, callback, context} {} + +//-------------------------------------------------------------------------------------------------- +// Reset +//-------------------------------------------------------------------------------------------------- +void TimerEvent::Reset() { + event_.Remove(); + event_.Add(&tv_); +} + +void TimerEvent::Reset(const timeval* tv) { + event_.Remove(); + if (tv == nullptr) { + return; + } + tv_ = *tv; + event_.Add(&tv_); +} +} // namespace lightstep diff --git a/src/network/timer_event.h b/src/network/timer_event.h new file mode 100644 index 00000000..cd4a13a7 --- /dev/null +++ b/src/network/timer_event.h @@ -0,0 +1,56 @@ +#pragma once + +#include "network/event.h" +#include "network/event_base.h" + +#include + +struct event; + +namespace lightstep { +/** + * Manages a periodic timer event. + */ +class TimerEvent { + public: + TimerEvent() noexcept = default; + + template + TimerEvent(const EventBase& event_base, + std::chrono::duration interval, + Event::Callback callback, void* context) + : TimerEvent{ + event_base, + std::chrono::duration_cast(interval), + callback, context} {} + + TimerEvent(const EventBase& event_base, std::chrono::microseconds interval, + Event::Callback callback, void* context); + + TimerEvent(const EventBase& event_base, Event::Callback callback, + void* context); + + /** + * Resets the timer to start anew from now. + */ + void Reset(); + + void Reset(const timeval* tv); + + private: + Event event_; + timeval tv_{}; +}; + +/** + * For a given method, creates a timer callback function that can be passed to + * libevent. + * @return a function pointer that can be passed to libevent. + */ +template +Event::Callback MakeTimerCallback() { + return [](int /*socket*/, short /*what*/, void* context) { + (static_cast(context)->*MemberFunction)(); + }; +} +} // namespace lightstep diff --git a/src/network/vector_write.cpp b/src/network/vector_write.cpp new file mode 100644 index 00000000..f388fb76 --- /dev/null +++ b/src/network/vector_write.cpp @@ -0,0 +1,48 @@ +#include "network/vector_write.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// Write +//-------------------------------------------------------------------------------------------------- +bool Write(int socket, + std::initializer_list fragment_input_streams) { + int num_fragments = 0; + for (auto fragment_input_stream : fragment_input_streams) { + num_fragments += fragment_input_stream->num_fragments(); + } + if (num_fragments == 0) { + return true; + } + assert(num_fragments < IOV_MAX); + auto fragments = static_cast(alloca(sizeof(iovec) * num_fragments)); + auto fragment_iter = fragments; + for (auto fragment_input_stream : fragment_input_streams) { + fragment_input_stream->ForEachFragment( + [&fragment_iter](void* data, int size) { + *fragment_iter++ = iovec{data, static_cast(size)}; + return true; + }); + } + assert(fragment_iter == fragments + num_fragments); + auto rcode = ::writev(socket, fragments, num_fragments); + if (rcode == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return false; + } + std::ostringstream oss; + oss << "writev failed: " << std::strerror(errno); + throw std::runtime_error{oss.str()}; + } + return Consume(fragment_input_streams, static_cast(rcode)); +} +} // namespace lightstep diff --git a/src/network/vector_write.h b/src/network/vector_write.h new file mode 100644 index 00000000..df7a90d8 --- /dev/null +++ b/src/network/vector_write.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "network/fragment_input_stream.h" + +namespace lightstep { +/** + * Uses the writev system call to send fragments over a given socket. After + * writing, consumes the number of written bytes from the streams. + * @param socket the file descriptor of the socket. + * @param fragment_input_streams the list of fragments to send. + * @return true if everything in the streams was written; false, otherwise. + */ +bool Write(int socket, + std::initializer_list fragment_input_streams); +} // namespace lightstep diff --git a/src/recorder/BUILD b/src/recorder/BUILD new file mode 100644 index 00000000..a450d822 --- /dev/null +++ b/src/recorder/BUILD @@ -0,0 +1,127 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "grpc_transporter_interface", + private_hdrs = [ + "grpc_transporter.h", + ], + deps = [ + "//include/lightstep:tracer_interface", + ], +) + +lightstep_cc_library( + name = "no_grpc_transporter_lib", + srcs = [ + "no_grpc_transporter.cpp", + ], + deps = [ + "//include/lightstep:tracer_interface", + "//src/common:logger_lib", + ":grpc_transporter_interface", + "//:config_lib", + ], +) + +lightstep_cc_library( + name = "recorder_interface", + private_hdrs = [ + "recorder.h", + ], + deps = [ + "//lightstep-tracer-common:collector_proto_cc", + "//include/lightstep:tracer_interface", + ], +) + +lightstep_cc_library( + name = "report_builder_lib", + private_hdrs = [ + "report_builder.h", + ], + srcs = [ + "report_builder.cpp", + ], + deps = [ + "//lightstep-tracer-common:collector_proto_cc", + "//include/lightstep:tracer_interface", + "//src/common:random_lib", + "//src/common:utility_lib", + ], +) + +lightstep_cc_library( + name = "manual_recorder_lib", + private_hdrs = [ + "manual_recorder.h", + ], + srcs = [ + "manual_recorder.cpp", + ], + deps = [ + "//src/common:logger_lib", + "//src/common:noncopyable_lib", + "//src/common:utility_lib", + ":recorder_interface", + ":report_builder_lib", + ":transporter_lib", + ], +) + +lightstep_cc_library( + name = "auto_recorder_lib", + private_hdrs = [ + "auto_recorder.h", + ], + srcs = [ + "auto_recorder.cpp", + ], + deps = [ + "//src/common:logger_lib", + "//src/common:noncopyable_lib", + "//src/common:utility_lib", + "//src/common:condition_variable_wrapper_lib", + ":recorder_interface", + ":report_builder_lib", + ":transporter_lib", + ], +) + +lightstep_cc_library( + name = "transporter_lib", + srcs = [ + "transporter.cpp", + ], + deps = [ + "//include/lightstep:transporter_interface", + "//lightstep-tracer-common:collector_proto_cc", + ], +) + +lightstep_cc_library( + name = "stream_recorder_interface", + private_hdrs = [ + "stream_recorder.h", + ], + deps = [ + "//src/common:logger_lib", + "//src/recorder:recorder_interface", + "//include/lightstep:tracer_interface", + ], +) + +lightstep_cc_library( + name = "no_stream_recorder_lib", + srcs = [ + "no_stream_recorder.cpp", + ], + deps = [ + ":stream_recorder_interface", + ], +) diff --git a/src/auto_recorder.cpp b/src/recorder/auto_recorder.cpp similarity index 99% rename from src/auto_recorder.cpp rename to src/recorder/auto_recorder.cpp index 88df8170..d1bb5c19 100644 --- a/src/auto_recorder.cpp +++ b/src/recorder/auto_recorder.cpp @@ -1,6 +1,8 @@ -#include "auto_recorder.h" +#include "recorder/auto_recorder.h" + +#include "common/utility.h" + #include -#include "utility.h" namespace lightstep { //------------------------------------------------------------------------------ diff --git a/src/auto_recorder.h b/src/recorder/auto_recorder.h similarity index 83% rename from src/auto_recorder.h rename to src/recorder/auto_recorder.h index 372ed17e..a828da65 100644 --- a/src/auto_recorder.h +++ b/src/recorder/auto_recorder.h @@ -1,22 +1,24 @@ #pragma once -#include -#include #include #include #include #include -#include "condition_variable_wrapper.h" + +#include "common/condition_variable_wrapper.h" +#include "common/logger.h" +#include "common/noncopyable.h" #include "lightstep-tracer-common/collector.pb.h" -#include "logger.h" -#include "recorder.h" -#include "report_builder.h" +#include "lightstep/tracer.h" +#include "lightstep/transporter.h" +#include "recorder/recorder.h" +#include "recorder/report_builder.h" namespace lightstep { // AutoRecorder buffers spans finished by a tracer and sends them over to // the provided SyncTransporter. It uses an internal thread to regularly send // the reports according to the rate specified by LightStepTracerOptions. -class AutoRecorder final : public Recorder { +class AutoRecorder final : public Recorder, private Noncopyable { public: AutoRecorder(Logger& logger, LightStepTracerOptions&& options, std::unique_ptr&& transporter); @@ -25,11 +27,6 @@ class AutoRecorder final : public Recorder { std::unique_ptr&& transporter, std::unique_ptr&& write_cond); - AutoRecorder(const AutoRecorder&) = delete; - AutoRecorder(AutoRecorder&&) = delete; - AutoRecorder& operator=(const AutoRecorder&) = delete; - AutoRecorder& operator=(AutoRecorder&&) = delete; - ~AutoRecorder() override; void RecordSpan(const collector::Span& span) noexcept override; diff --git a/src/grpc_transporter.h b/src/recorder/grpc_transporter.h similarity index 75% rename from src/grpc_transporter.h rename to src/recorder/grpc_transporter.h index 592e9d08..d735d7b3 100644 --- a/src/grpc_transporter.h +++ b/src/recorder/grpc_transporter.h @@ -1,7 +1,7 @@ #pragma once -#include -#include "logger.h" +#include "common/logger.h" +#include "lightstep/tracer.h" namespace lightstep { std::unique_ptr MakeGrpcTransporter( diff --git a/src/recorder/grpc_transporter/BUILD b/src/recorder/grpc_transporter/BUILD new file mode 100644 index 00000000..b1f270d7 --- /dev/null +++ b/src/recorder/grpc_transporter/BUILD @@ -0,0 +1,45 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +load("@build_stack_rules_proto//:compile.bzl", "proto_compile") + +lightstep_package() + +proto_compile( + name = "collector_proto_grpc_files", + plugins = [ + "@build_stack_rules_proto//cpp:grpc_cpp", + ], + deps = [ + "//lightstep-tracer-common:collector_proto", + ], +) + +cc_library( + name = "collector_proto_grpc", + srcs = [ + "collector_proto_grpc_files", + ], + deps = [ + "//lightstep-tracer-common:collector_proto_cc", + "@com_github_grpc_grpc//:grpc++", + "@com_github_grpc_grpc//:grpc++_reflection", + ], + includes = ["collector_proto_grpc_files"], +) + +lightstep_cc_library( + name = "grpc_transporter_lib", + srcs = [ + "grpc_transporter.cpp", + ], + deps = [ + "//src/recorder:grpc_transporter_interface", + "//src/common:logger_lib", + "//include/lightstep:tracer_interface", + ":collector_proto_grpc", + ], +) diff --git a/src/grpc_transporter.cpp b/src/recorder/grpc_transporter/grpc_transporter.cpp similarity index 90% rename from src/grpc_transporter.cpp rename to src/recorder/grpc_transporter/grpc_transporter.cpp index 952688dc..0fe75e6f 100644 --- a/src/grpc_transporter.cpp +++ b/src/recorder/grpc_transporter/grpc_transporter.cpp @@ -1,10 +1,9 @@ -#include "grpc_transporter.h" -#include +#include "recorder/grpc_transporter.h" -#ifdef LIGHTSTEP_USE_GRPC #include #include #include + #include "lightstep-tracer-common/collector.grpc.pb.h" #include "lightstep-tracer-common/collector.pb.h" @@ -103,14 +102,3 @@ std::unique_ptr MakeGrpcTransporter( return std::unique_ptr{new GrpcTransporter{logger, options}}; } } // namespace lightstep -#else -#include -namespace lightstep { -std::unique_ptr MakeGrpcTransporter( - Logger& /*logger*/, const LightStepTracerOptions& /*options*/) { - throw std::runtime_error{ - "LightStep was not built with gRPC support, so a transporter must be " - "supplied."}; -} -} // namespace lightstep -#endif diff --git a/src/manual_recorder.cpp b/src/recorder/manual_recorder.cpp similarity index 98% rename from src/manual_recorder.cpp rename to src/recorder/manual_recorder.cpp index 2bc44650..485fe5bd 100644 --- a/src/manual_recorder.cpp +++ b/src/recorder/manual_recorder.cpp @@ -1,5 +1,5 @@ -#include "manual_recorder.h" -#include "utility.h" +#include "recorder/manual_recorder.h" +#include "common/utility.h" namespace lightstep { //------------------------------------------------------------------------------ diff --git a/src/manual_recorder.h b/src/recorder/manual_recorder.h similarity index 81% rename from src/manual_recorder.h rename to src/recorder/manual_recorder.h index 9daf4ede..2ea1c7b5 100644 --- a/src/manual_recorder.h +++ b/src/recorder/manual_recorder.h @@ -1,15 +1,17 @@ #pragma once -#include -#include "logger.h" -#include "recorder.h" -#include "report_builder.h" +#include "common/logger.h" +#include "common/noncopyable.h" +#include "lightstep/transporter.h" +#include "recorder/recorder.h" +#include "recorder/report_builder.h" namespace lightstep { // ManualRecorder buffers spans finished by a tracer and sends them over to // the provided AsyncTransporter when FlushWithTimeout is called. class ManualRecorder final : public Recorder, - private AsyncTransporter::Callback { + private AsyncTransporter::Callback, + private Noncopyable { public: ManualRecorder(Logger& logger, LightStepTracerOptions options, std::unique_ptr&& transporter); diff --git a/src/recorder/no_grpc_transporter.cpp b/src/recorder/no_grpc_transporter.cpp new file mode 100644 index 00000000..b3689b98 --- /dev/null +++ b/src/recorder/no_grpc_transporter.cpp @@ -0,0 +1,12 @@ +#include "recorder/grpc_transporter.h" + +#include + +namespace lightstep { +std::unique_ptr MakeGrpcTransporter( + Logger& /*logger*/, const LightStepTracerOptions& /*options*/) { + throw std::runtime_error{ + "LightStep was not built with gRPC support, so a transporter must be " + "supplied."}; +} +} // namespace lightstep diff --git a/src/recorder/no_stream_recorder.cpp b/src/recorder/no_stream_recorder.cpp new file mode 100644 index 00000000..8097a52a --- /dev/null +++ b/src/recorder/no_stream_recorder.cpp @@ -0,0 +1,12 @@ +#include "recorder/stream_recorder.h" + +#include + +namespace lightstep { +std::unique_ptr MakeStreamRecorder( + Logger& /*logger*/, LightStepTracerOptions&& /*tracer_options*/) { + throw std::runtime_error{ + "LightStep was not built with stream recorder support, so a different " + "recorder must be selected."}; +} +} // namespace lightstep diff --git a/src/recorder.h b/src/recorder/recorder.h similarity index 70% rename from src/recorder.h rename to src/recorder/recorder.h index 5e6b5882..b9aac4a4 100644 --- a/src/recorder.h +++ b/src/recorder/recorder.h @@ -8,8 +8,16 @@ namespace lightstep { // Abstract class that accepts spans from a Tracer once they are finished. class Recorder { public: + Recorder() noexcept = default; + + Recorder(Recorder&&) = delete; + Recorder(const Recorder&) = delete; + virtual ~Recorder() = default; + Recorder& operator=(Recorder&&) = delete; + Recorder& operator=(const Recorder&) = delete; + virtual void RecordSpan(const collector::Span& span) noexcept = 0; virtual bool FlushWithTimeout( diff --git a/src/report_builder.cpp b/src/recorder/report_builder.cpp similarity index 94% rename from src/report_builder.cpp rename to src/recorder/report_builder.cpp index 9f640958..286ab89f 100644 --- a/src/report_builder.cpp +++ b/src/recorder/report_builder.cpp @@ -1,5 +1,7 @@ -#include "report_builder.h" -#include "utility.h" +#include "recorder/report_builder.h" + +#include "common/random.h" +#include "common/utility.h" namespace lightstep { //------------------------------------------------------------------------------ diff --git a/src/report_builder.h b/src/recorder/report_builder.h similarity index 100% rename from src/report_builder.h rename to src/recorder/report_builder.h diff --git a/src/recorder/stream_recorder.h b/src/recorder/stream_recorder.h new file mode 100644 index 00000000..975c6de2 --- /dev/null +++ b/src/recorder/stream_recorder.h @@ -0,0 +1,17 @@ +#pragma once + +#include "common/logger.h" +#include "lightstep/tracer.h" +#include "recorder/recorder.h" + +namespace lightstep { +/** + * Constructs a StreamRecorder. + * @param logger supplies the place to write logs. + * @params tracer_options supplies the user-provided configuration options to + * apply. + * @return a StreamRecorder + */ +std::unique_ptr MakeStreamRecorder( + Logger& logger, LightStepTracerOptions&& tracer_options); +} // namespace lightstep diff --git a/src/recorder/stream_recorder/BUILD b/src/recorder/stream_recorder/BUILD new file mode 100644 index 00000000..4f3474a0 --- /dev/null +++ b/src/recorder/stream_recorder/BUILD @@ -0,0 +1,229 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "stream_recorder_options_lib", + private_hdrs = [ + "stream_recorder_options.h", + ], + deps = [ + "//src/network:dns_resolver_interface", + ], +) + +lightstep_cc_library( + name = "stream_recorder_metrics_lib", + private_hdrs = [ + "stream_recorder_metrics.h", + ], + srcs = [ + "stream_recorder_metrics.cpp", + ], + deps = [ + "//include/lightstep:metrics_observer_interface", + ], +) + +lightstep_cc_library( + name = "span_stream_lib", + private_hdrs = [ + "span_stream.h", + ], + srcs = [ + "span_stream.cpp", + ], + deps = [ + "//src/common:chunk_circular_buffer_lib", + "//src/network:fragment_array_input_stream_lib", + ":utility_lib", + ":fragment_span_input_stream_lib", + ":stream_recorder_metrics_lib", + ], +) + +lightstep_cc_library( + name = "fragment_span_input_stream_lib", + private_hdrs = [ + "fragment_span_input_stream.h", + ], + srcs = [ + "fragment_span_input_stream.cpp", + ], + deps = [ + "//src/common:circular_buffer_lib", + "//src/network:fragment_array_input_stream_lib", + ":utility_lib", + ], +) + +lightstep_cc_library( + name = "connection_stream_lib", + private_hdrs = [ + "connection_stream.h", + ], + srcs = [ + "connection_stream.cpp", + ], + deps = [ + "//src/common:function_ref_lib", + "//src/common:utility_lib", + "//src/network:fragment_input_stream_lib", + "//src/network:fragment_array_input_stream_lib", + ":embedded_metrics_message_lib", + ":span_stream_lib", + ":stream_recorder_metrics_lib", + ":fragment_span_input_stream_lib", + ], +) + +lightstep_cc_library( + name = "stream_recorder_lib", + private_hdrs = [ + "stream_recorder.h", + ], + srcs = [ + "stream_recorder.cpp", + ], + deps = [ + "//src/common:logger_lib", + "//src/common:noncopyable_lib", + "//src/common:protobuf_lib", + "//src/common:chunk_circular_buffer_lib", + "//src/network:event_lib", + "//src/network:timer_event_lib", + "//src/recorder:stream_recorder_interface", + ":stream_recorder_options_lib", + ":stream_recorder_metrics_lib", + ":satellite_streamer_lib", + ], +) + +lightstep_cc_library( + name = "satellite_dns_resolution_manager_lib", + private_hdrs = [ + "satellite_dns_resolution_manager.h", + ], + srcs = [ + "satellite_dns_resolution_manager.cpp", + ], + deps = [ + "//src/common:logger_lib", + "//src/common:random_lib", + "//src/common:noncopyable_lib", + "//src/network:event_lib", + "//src/network:timer_event_lib", + "//src/network:dns_resolver_interface", + ":stream_recorder_options_lib", + ], +) + +lightstep_cc_library( + name = "satellite_endpoint_manager_lib", + private_hdrs = [ + "satellite_endpoint_manager.h", + ], + srcs = [ + "satellite_endpoint_manager.cpp", + ], + deps = [ + ":satellite_dns_resolution_manager_lib", + ":utility_lib", + "//src/network:dns_resolver_interface", + "//src/common:noncopyable_lib", + ], +) + +lightstep_cc_library( + name = "satellite_streamer_lib", + private_hdrs = [ + "satellite_connection.h", + "satellite_streamer.h", + ], + srcs = [ + "satellite_connection.cpp", + "satellite_streamer.cpp", + ], + deps = [ + ":satellite_endpoint_manager_lib", + "//src/common:chunk_circular_buffer_lib", + "//src/common:noncopyable_lib", + "//src/common:random_lib", + "//src/common:random_traverser_lib", + "//src/network:socket_lib", + "//src/network:event_lib", + "//src/network:timer_event_lib", + "//src/network:vector_write_lib", + ":stream_recorder_metrics_lib", + ":embedded_metrics_message_lib", + ":utility_lib", + ":host_header_lib", + ":span_stream_lib", + ":connection_stream_lib", + ":status_line_parser_lib", + ], +) + +lightstep_cc_library( + name = "utility_lib", + private_hdrs = [ + "utility.h", + ], + srcs = [ + "utility.cpp", + ], + deps = [ + "//include/lightstep:tracer_interface", + "//lightstep-tracer-common:collector_proto_cc", + "//src/common:protobuf_lib", + "//src/common:utility_lib", + "//src/common:circular_buffer_lib", + "//src/network:fragment_array_input_stream_lib", + ], +) + +lightstep_cc_library( + name = "embedded_metrics_message_lib", + private_hdrs = [ + "embedded_metrics_message.h", + ], + srcs = [ + "embedded_metrics_message.cpp", + ], + deps = [ + "//lightstep-tracer-common:collector_proto_cc", + "//src/common:protobuf_lib", + ], +) + +lightstep_cc_library( + name = "host_header_lib", + private_hdrs = [ + "host_header.h", + ], + srcs = [ + "host_header.cpp", + ], + deps = [ + "//include/lightstep:tracer_interface", + ], +) + +lightstep_cc_library( + name = "status_line_parser_lib", + private_hdrs = [ + "status_line_parser.h", + ], + srcs = [ + "status_line_parser.cpp", + ], + deps = [ + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + ], +) diff --git a/src/recorder/stream_recorder/connection_stream.cpp b/src/recorder/stream_recorder/connection_stream.cpp new file mode 100644 index 00000000..ec504c4c --- /dev/null +++ b/src/recorder/stream_recorder/connection_stream.cpp @@ -0,0 +1,139 @@ +#include "recorder/stream_recorder/connection_stream.h" + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// TerminalFragment +//-------------------------------------------------------------------------------------------------- +static const Fragment TerminalFragment = MakeFragment("0\r\n\r\n"); + +//-------------------------------------------------------------------------------------------------- +// EndOfLineFragment +//-------------------------------------------------------------------------------------------------- +static const Fragment EndOfLineFragment = MakeFragment("\r\n"); + +//-------------------------------------------------------------------------------------------------- +// HttpRequestCommonFragment +//-------------------------------------------------------------------------------------------------- +static const Fragment HttpRequestCommonFragment = MakeFragment( + "POST /api/v2/reports HTTP/1.1\r\n" + "Connection:close\r\n" + "Content-Type:application/octet-stream\r\n" + "Transfer-Encoding:chunked\r\n"); + +//-------------------------------------------------------------------------------------------------- +// WriteChunkHeader +//-------------------------------------------------------------------------------------------------- +static Fragment WriteChunkHeader(char* buffer, size_t buffer_size, + int chunk_size) { + auto num_chunk_header_chars = + std::snprintf(buffer, buffer_size, "%llX\r\n", + static_cast(chunk_size)); + assert(num_chunk_header_chars > 0); + return {static_cast(buffer), static_cast(num_chunk_header_chars)}; +} + +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +ConnectionStream::ConnectionStream(Fragment host_header_fragment, + Fragment header_common_fragment, + SpanStream& span_stream) + : host_header_fragment_{std::move(host_header_fragment)}, + header_common_fragment_{std::move(header_common_fragment)}, + span_stream_{span_stream} { + InitializeStream(); +} + +//-------------------------------------------------------------------------------------------------- +// Reset +//-------------------------------------------------------------------------------------------------- +void ConnectionStream::Reset() { + if (!header_stream_.empty()) { + // We weren't able to upload the metrics so add the counters back + span_stream_.metrics().UnconsumeDroppedSpans( + embedded_metrics_message_.num_dropped_spans()); + } + if (!span_remnant_.empty()) { + span_stream_.metrics().OnSpansDropped(1); + span_stream_.RemoveSpanRemnant(span_remnant_.chunk_start()); + span_remnant_.Clear(); + } + InitializeStream(); +} + +//-------------------------------------------------------------------------------------------------- +// Shutdown +//-------------------------------------------------------------------------------------------------- +void ConnectionStream::Shutdown() noexcept { shutting_down_ = true; } + +//-------------------------------------------------------------------------------------------------- +// Flush +//-------------------------------------------------------------------------------------------------- +bool ConnectionStream::Flush(Writer writer) { + if (shutting_down_) { + return FlushShutdown(writer); + } + auto chunk_start = span_remnant_.chunk_start(); + span_stream_.Allot(); + auto result = writer({&header_stream_, &span_remnant_, &span_stream_}); + if (span_remnant_.empty()) { + if (chunk_start != nullptr) { + span_stream_.metrics().OnSpansSent(1); + span_stream_.RemoveSpanRemnant(chunk_start); + } + span_stream_.PopSpanRemnant(span_remnant_); + } + span_stream_.Consume(); + return result; +} + +//-------------------------------------------------------------------------------------------------- +// InitializeStream +//-------------------------------------------------------------------------------------------------- +void ConnectionStream::InitializeStream() { + shutting_down_ = false; + terminal_stream_ = {TerminalFragment}; + + embedded_metrics_message_.set_num_dropped_spans( + span_stream_.metrics().ConsumeDroppedSpans()); + auto metrics_fragment = embedded_metrics_message_.MakeFragment(); + + auto header_chunk_size = + header_common_fragment_.second + metrics_fragment.second; + + header_stream_ = { + HttpRequestCommonFragment, + host_header_fragment_, + EndOfLineFragment, + WriteChunkHeader(chunk_header_buffer_.data(), chunk_header_buffer_.size(), + header_chunk_size), + header_common_fragment_, + metrics_fragment, + EndOfLineFragment}; +} + +//-------------------------------------------------------------------------------------------------- +// first_chunk_position +//-------------------------------------------------------------------------------------------------- +int ConnectionStream::first_chunk_position() const noexcept { + return HttpRequestCommonFragment.second + host_header_fragment_.second + + EndOfLineFragment.second; +} + +//-------------------------------------------------------------------------------------------------- +// FlushShutdown +//-------------------------------------------------------------------------------------------------- +bool ConnectionStream::FlushShutdown(Writer writer) { + auto chunk_start = span_remnant_.chunk_start(); + auto result = writer({&header_stream_, &span_remnant_, &terminal_stream_}); + if (span_remnant_.empty() && chunk_start != nullptr) { + span_stream_.metrics().OnSpansSent(1); + span_stream_.RemoveSpanRemnant(chunk_start); + span_stream_.Consume(); + } + return result; +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/connection_stream.h b/src/recorder/stream_recorder/connection_stream.h new file mode 100644 index 00000000..abd9d79b --- /dev/null +++ b/src/recorder/stream_recorder/connection_stream.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +#include "common/function_ref.h" +#include "common/utility.h" +#include "network/fragment_array_input_stream.h" +#include "network/fragment_input_stream.h" +#include "recorder/stream_recorder/embedded_metrics_message.h" +#include "recorder/stream_recorder/fragment_span_input_stream.h" +#include "recorder/stream_recorder/span_stream.h" +#include "recorder/stream_recorder/stream_recorder_metrics.h" + +namespace lightstep { +/** + * Manages the data to send over a streaming satellite connection. + */ +class ConnectionStream { + // Account for the size encoded in hex plus the following \r\n sequence. + static const size_t MaxChunkHeaderSize = Num64BitHexDigits + 2; + + public: + using Writer = FunctionRef fragment_input_streams)>; + + ConnectionStream(Fragment host_header_fragment, + Fragment header_common_fragment, SpanStream& span_stream); + + /** + * Reset so as to begin a new streaming session. + */ + void Reset(); + + /** + * Set the current streaming session to end. + */ + void Shutdown() noexcept; + + /** + * @return true if the stream is in the process of shutting down. + */ + bool shutting_down() const noexcept { return shutting_down_; } + + /** + * Flushes data from the stream. + * @param writer the writer to flush data to. + * @return true if all the data in the stream was flushed; false, otherwise. + */ + bool Flush(Writer writer); + + /** + * @return true if the streaming session is complete. + */ + bool completed() const noexcept { return terminal_stream_.empty(); } + + /** + * @return the number of bytes in the stream until the first chunk. + */ + int first_chunk_position() const noexcept; + + private: + Fragment host_header_fragment_; + Fragment header_common_fragment_; + SpanStream& span_stream_; + std::array + chunk_header_buffer_; // Note: Use 1 greater than the actual size so that + // we can utilize the snprintf library functions + // that add a null terminator. + EmbeddedMetricsMessage embedded_metrics_message_; + + FragmentArrayInputStream header_stream_; + FragmentArrayInputStream terminal_stream_; + + FragmentSpanInputStream span_remnant_; + + bool shutting_down_; + + void InitializeStream(); + + bool FlushShutdown(Writer writer); +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/embedded_metrics_message.cpp b/src/recorder/stream_recorder/embedded_metrics_message.cpp new file mode 100644 index 00000000..bd7d97a8 --- /dev/null +++ b/src/recorder/stream_recorder/embedded_metrics_message.cpp @@ -0,0 +1,49 @@ +#include "recorder/stream_recorder/embedded_metrics_message.h" + +#include "common/protobuf.h" + +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +EmbeddedMetricsMessage::EmbeddedMetricsMessage() + : dropped_spans_count_{*message_.add_counts()} { + dropped_spans_count_.set_name("spans.dropped"); + dropped_spans_count_.set_int_value(0); +} + +//-------------------------------------------------------------------------------------------------- +// set_num_dropped_spans +//-------------------------------------------------------------------------------------------------- +void EmbeddedMetricsMessage::set_num_dropped_spans(int num_dropped_spans) const + noexcept { + dropped_spans_count_.set_int_value(num_dropped_spans); +} + +//-------------------------------------------------------------------------------------------------- +// num_dropped_spans +//-------------------------------------------------------------------------------------------------- +int EmbeddedMetricsMessage::num_dropped_spans() const noexcept { + return dropped_spans_count_.int_value(); +} + +//-------------------------------------------------------------------------------------------------- +// MakeFragment +//-------------------------------------------------------------------------------------------------- +std::pair EmbeddedMetricsMessage::MakeFragment() { + auto message_size = message_.ByteSizeLong(); + buffer_.resize(ComputeEmbeddedMessageSerializationSize( + collector::ReportRequest::kInternalMetricsFieldNumber, message_size)); + { + google::protobuf::io::ArrayOutputStream zero_copy_stream{ + static_cast(buffer_.data()), static_cast(buffer_.size())}; + google::protobuf::io::CodedOutputStream coded_stream{&zero_copy_stream}; + WriteEmbeddedMessage(coded_stream, + collector::ReportRequest::kInternalMetricsFieldNumber, + message_size, message_); + } + return {static_cast(buffer_.data()), static_cast(buffer_.size())}; +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/embedded_metrics_message.h b/src/recorder/stream_recorder/embedded_metrics_message.h new file mode 100644 index 00000000..39073d60 --- /dev/null +++ b/src/recorder/stream_recorder/embedded_metrics_message.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include "lightstep-tracer-common/collector.pb.h" + +namespace lightstep { +/** + * Manages the metrics message sent in a ReportRequest. + */ +class EmbeddedMetricsMessage { + public: + EmbeddedMetricsMessage(); + + /** + * Sets the number of dropped spans to send in the message. + * @param num_dropped_spans the number of dropped spans to set. + */ + void set_num_dropped_spans(int num_dropped_spans) const noexcept; + + /** + * @return the number of dropped spans in the message. + */ + int num_dropped_spans() const noexcept; + + /** + * @return a fragment for the serialized message. + */ + std::pair MakeFragment(); + + private: + collector::InternalMetrics message_; + collector::MetricsSample& dropped_spans_count_; + std::vector buffer_; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/fragment_span_input_stream.cpp b/src/recorder/stream_recorder/fragment_span_input_stream.cpp new file mode 100644 index 00000000..fc0a1ef3 --- /dev/null +++ b/src/recorder/stream_recorder/fragment_span_input_stream.cpp @@ -0,0 +1,43 @@ +#include "recorder/stream_recorder/fragment_span_input_stream.h" + +#include + +#include "recorder/stream_recorder/utility.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +FragmentSpanInputStream::FragmentSpanInputStream() { stream_.Reserve(2); } + +//-------------------------------------------------------------------------------------------------- +// Set +//-------------------------------------------------------------------------------------------------- +void FragmentSpanInputStream::Set(const CircularBufferConstPlacement& chunk, + const char* position) { + stream_.Clear(); + chunk_start_ = chunk.data1; + if (Contains(chunk.data1, chunk.size1, position)) { + stream_.Add(Fragment{ + static_cast(const_cast(position)), + static_cast(std::distance(position, chunk.data1 + chunk.size1))}); + if (chunk.size2 > 0) { + stream_.Add(Fragment{static_cast(const_cast(chunk.data2)), + static_cast(chunk.size2)}); + } + return; + } + assert(Contains(chunk.data2, chunk.size2, position)); + stream_.Add(Fragment{ + static_cast(const_cast(position)), + static_cast(std::distance(position, chunk.data2 + chunk.size2))}); +} + +//-------------------------------------------------------------------------------------------------- +// Clear +//-------------------------------------------------------------------------------------------------- +void FragmentSpanInputStream::Clear() noexcept { + chunk_start_ = nullptr; + return stream_.Clear(); +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/fragment_span_input_stream.h b/src/recorder/stream_recorder/fragment_span_input_stream.h new file mode 100644 index 00000000..12e4040d --- /dev/null +++ b/src/recorder/stream_recorder/fragment_span_input_stream.h @@ -0,0 +1,45 @@ +#pragma once + +#include "common/circular_buffer.h" +#include "network/fragment_array_input_stream.h" + +namespace lightstep { +/** + * Wraps a FragmentArrayInputStream for a span chunk. + */ +class FragmentSpanInputStream final : public FragmentInputStream { + public: + FragmentSpanInputStream(); + + /** + * Sets the span for the fragments. + * @param chunk the placement for the span in the circular buffer. + * @param position a pointer one past the last byte of the span sent. + */ + void Set(const CircularBufferConstPlacement& chunk, const char* position); + + /** + * @return the address of the span's chunk in the circular buffer. + */ + const char* chunk_start() const noexcept { return chunk_start_; } + + // FragmentInputStream + int num_fragments() const noexcept override { + return stream_.num_fragments(); + } + + bool ForEachFragment(Callback callback) const noexcept override { + return stream_.ForEachFragment(callback); + } + + void Clear() noexcept override; + + void Seek(int fragment_index, int position) noexcept override { + return stream_.Seek(fragment_index, position); + } + + private: + const char* chunk_start_{nullptr}; + FragmentArrayInputStream stream_; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/host_header.cpp b/src/recorder/stream_recorder/host_header.cpp new file mode 100644 index 00000000..92e01997 --- /dev/null +++ b/src/recorder/stream_recorder/host_header.cpp @@ -0,0 +1,40 @@ +#include "recorder/stream_recorder/host_header.h" + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +HostHeader::HostHeader(const LightStepTracerOptions& tracer_options) { + size_t max_host_length = 0; + for (auto& endpoint : tracer_options.satellite_endpoints) { + max_host_length = std::max(max_host_length, endpoint.first.size()); + } + format_ = "Host:%" + std::to_string(max_host_length) + "s\r\n"; + auto header_length = + 5 /*std::strlen("Host:")*/ + max_host_length + 2 /*std::strlen("\r\n")*/; + header_.resize( + header_length + + 1); // Note: Allow for an extra null terminator so we can use snprintf. +} + +//-------------------------------------------------------------------------------------------------- +// set_host +//-------------------------------------------------------------------------------------------------- +void HostHeader::set_host(const char* host) noexcept { + auto rcode = + std::snprintf(header_.data(), header_.size(), format_.c_str(), host); + (void)rcode; + assert(rcode > 0); +} + +//-------------------------------------------------------------------------------------------------- +// fragment +//-------------------------------------------------------------------------------------------------- +std::pair HostHeader::fragment() const noexcept { + return {static_cast(const_cast(header_.data())), + static_cast(header_.size()) - 1}; +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/host_header.h b/src/recorder/stream_recorder/host_header.h new file mode 100644 index 00000000..5ccf4a30 --- /dev/null +++ b/src/recorder/stream_recorder/host_header.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "lightstep/tracer.h" + +namespace lightstep { +/** + * Manages the host header in a streaming HTTP POST request. + */ +class HostHeader { + public: + explicit HostHeader(const LightStepTracerOptions& tracer_options); + + /** + * Sets the host to send in the header. + * @param host the host to set. + * + * Note: host must hvae been specified in LightStepTracerOptions. + */ + void set_host(const char* host) noexcept; + + /** + * @return the fragment for the host header. + * + * Note: The fragment remains the same even if set_host is called with a + * different host. + */ + std::pair fragment() const noexcept; + + private: + std::string format_; + std::vector header_; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_connection.cpp b/src/recorder/stream_recorder/satellite_connection.cpp new file mode 100644 index 00000000..8af6c7fc --- /dev/null +++ b/src/recorder/stream_recorder/satellite_connection.cpp @@ -0,0 +1,263 @@ +#include "recorder/stream_recorder/satellite_connection.h" + +#include +#include +#include +#include +#include + +#include "common/random.h" +#include "network/timer_event.h" +#include "network/vector_write.h" +#include "recorder/stream_recorder/satellite_streamer.h" + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +SatelliteConnection::SatelliteConnection(SatelliteStreamer& streamer) + : streamer_{streamer}, + host_header_{streamer.tracer_options()}, + connection_stream_{host_header_.fragment(), + streamer.header_common_fragment(), + streamer.span_stream()}, + reconnect_timer_{ + streamer_.event_base(), -1, 0, + MakeTimerCallback(), + static_cast(this)}, + graceful_shutdown_timeout_{ + streamer.event_base(), -1, 0, + MakeTimerCallback(), + static_cast(this)} {} + +//-------------------------------------------------------------------------------------------------- +// destructor +//-------------------------------------------------------------------------------------------------- +SatelliteConnection::~SatelliteConnection() noexcept { + // If there's a streaming session open, attempt to cleanly close it if we can + // do so without blocking. + if (writable_) { + connection_stream_.Shutdown(); + Flush(); + } +} + +//-------------------------------------------------------------------------------------------------- +// Start +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::Start() noexcept { Connect(); } + +//-------------------------------------------------------------------------------------------------- +// Flush +//-------------------------------------------------------------------------------------------------- +bool SatelliteConnection::Flush() noexcept try { + auto flushed_everything = connection_stream_.Flush( + [this](std::initializer_list fragment_streams) { + return Write(socket_.file_descriptor(), fragment_streams); + }); + if (flushed_everything) { + writable_ = true; + streamer_.logger().Info("Flushed everything to file_descriptor ", + socket_.file_descriptor()); + } else { + writable_ = false; + write_event_.Add(streamer_.recorder_options().satellite_write_timeout); + streamer_.logger().Info("Flushed partially to file_descriptor ", + socket_.file_descriptor()); + } + return flushed_everything; +} catch (const std::exception& e) { + streamer_.logger().Error("Flush failed: ", e.what()); + HandleFailure(); + return false; +} + +//-------------------------------------------------------------------------------------------------- +// ready +//-------------------------------------------------------------------------------------------------- +bool SatelliteConnection::ready() const noexcept { + return writable_ && !connection_stream_.shutting_down(); +} + +//-------------------------------------------------------------------------------------------------- +// Connect +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::Connect() noexcept try { + auto endpoint = streamer_.endpoint_manager().RequestEndpoint(); + streamer_.logger().Info("Connecting to satellite on ip ", endpoint.first); + socket_ = lightstep::Connect(endpoint.first); + host_header_.set_host(endpoint.second); + ScheduleReconnect(); + + read_event_ = + Event{streamer_.event_base(), socket_.file_descriptor(), EV_READ, + MakeEventCallback(), + static_cast(this)}; + read_event_.Add(nullptr); + + write_event_ = + Event{streamer_.event_base(), socket_.file_descriptor(), EV_WRITE, + MakeEventCallback(), + static_cast(this)}; + write_event_.Add(streamer_.recorder_options().satellite_write_timeout); +} catch (const std::exception& e) { + streamer_.logger().Error("Connect failed: ", e.what()); + HandleFailure(); +} + +//-------------------------------------------------------------------------------------------------- +// FreeSocket +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::FreeSocket() { + socket_ = Socket{-1}; + read_event_ = Event{}; + write_event_ = Event{}; + reconnect_timer_.Remove(); + graceful_shutdown_timeout_.Remove(); + writable_ = false; + connection_stream_.Reset(); + status_line_parser_.Reset(); +} + +//-------------------------------------------------------------------------------------------------- +// HandleFailure +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::HandleFailure() noexcept try { + FreeSocket(); + streamer_.event_base().OnTimeout( + streamer_.recorder_options().satellite_failure_retry_period, + MakeTimerCallback(), + static_cast(this)); +} catch (const std::exception& e) { + streamer_.logger().Error("HandleFailure failed: ", e.what()); +} + +//-------------------------------------------------------------------------------------------------- +// ScheduleReconnect +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::ScheduleReconnect() { + auto reconnect_period = GenerateRandomDuration( + streamer_.recorder_options().min_satellite_reconnect_period, + streamer_.recorder_options().max_satellite_reconnect_period); + reconnect_timer_.Add(reconnect_period); +} + +//-------------------------------------------------------------------------------------------------- +// InitiateReconnect +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::InitiateReconnect() noexcept try { + connection_stream_.Shutdown(); + if (writable_) { + Flush(); + } + graceful_shutdown_timeout_.Add( + streamer_.recorder_options().satellite_graceful_stream_shutdown_timeout); +} catch (const std::exception& e) { + streamer_.logger().Error("InitiateReconnect failed: ", e.what()); + return HandleFailure(); +} + +//-------------------------------------------------------------------------------------------------- +// Reconnect +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::Reconnect() noexcept try { + FreeSocket(); + Connect(); +} catch (const std::exception& e) { + streamer_.logger().Error("Reconnect failed: ", e.what()); +} + +//-------------------------------------------------------------------------------------------------- +// GracefulShutdownTimeout +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::GracefulShutdownTimeout() noexcept { + streamer_.logger().Error( + "Failed to shutdown satellite connection gracefully"); + Reconnect(); +} + +//-------------------------------------------------------------------------------------------------- +// OnReadable +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::OnReadable(int file_descriptor, + short /*what*/) noexcept try { + streamer_.logger().Info("Satellite file_descriptor ", file_descriptor, + " is readable"); + std::array buffer; + ssize_t rcode; + + // Ignore the contents of any response + while (true) { + rcode = ::read(file_descriptor, static_cast(buffer.data()), + buffer.size()); + if (rcode <= 0) { + break; + } + status_line_parser_.Parse( + opentracing::string_view{buffer.data(), static_cast(rcode)}); + } + + if (rcode == 0) { + if (!status_line_parser_.completed()) { + streamer_.logger().Error("No status line from satellite response"); + return OnSocketError(); + } + if (status_line_parser_.status_code() != 200) { + streamer_.logger().Error("Error from satellite ", + status_line_parser_.status_code(), ":", + status_line_parser_.reason()); + return OnSocketError(); + } + if (!connection_stream_.completed()) { + streamer_.logger().Warn("Socket closed prematurely by satellite"); + return OnSocketError(); + } + return Reconnect(); + } + assert(rcode == -1); + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return read_event_.Add(nullptr); + } + streamer_.logger().Error("Satellite socket error: ", std::strerror(errno)); + return OnSocketError(); +} catch (const std::exception& e) { + streamer_.logger().Error("OnReadable failed: ", e.what()); + return HandleFailure(); +} + +//-------------------------------------------------------------------------------------------------- +// OnWritable +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::OnWritable(int file_descriptor, + short what) noexcept try { + streamer_.logger().Info("Satellite file_descriptor ", file_descriptor, + " is writable"); + if ((what & EV_TIMEOUT) != 0) { + streamer_.logger().Error("Satellite connection timed out"); + return OnSocketError(); + } + assert((what & EV_WRITE) != 0); + Flush(); +} catch (const std::exception& e) { + streamer_.logger().Error("OnWritable failed: ", e.what()); + return HandleFailure(); +} + +//-------------------------------------------------------------------------------------------------- +// OnSocketError +//-------------------------------------------------------------------------------------------------- +void SatelliteConnection::OnSocketError() noexcept try { + FreeSocket(); + Connect(); +} catch (const std::exception& e) { + streamer_.logger().Error("OnSocketError failed: ", e.what()); + return HandleFailure(); +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_connection.h b/src/recorder/stream_recorder/satellite_connection.h new file mode 100644 index 00000000..eb8c6c1d --- /dev/null +++ b/src/recorder/stream_recorder/satellite_connection.h @@ -0,0 +1,68 @@ +#pragma once + +#include "common/noncopyable.h" +#include "network/event.h" +#include "network/socket.h" +#include "recorder/stream_recorder/connection_stream.h" +#include "recorder/stream_recorder/host_header.h" +#include "recorder/stream_recorder/status_line_parser.h" + +namespace lightstep { +class SatelliteStreamer; + +class SatelliteConnection : private Noncopyable { + public: + explicit SatelliteConnection(SatelliteStreamer& streamer); + + ~SatelliteConnection() noexcept; + + /** + * Start establishing a connection to a satellite. + */ + void Start() noexcept; + + /** + * Flushes through the streaming satellite connection. + * @return true if all data in th span buffer was flushed through the + * connection. + */ + bool Flush() noexcept; + + /** + * @return true if the satellite connection is available for streaming spans. + */ + bool ready() const noexcept; + + private: + SatelliteStreamer& streamer_; + HostHeader host_header_; + ConnectionStream connection_stream_; + StatusLineParser status_line_parser_; + Socket socket_{-1}; + bool writable_{false}; + Event read_event_; + Event write_event_; + Event reconnect_timer_; + Event graceful_shutdown_timeout_; + + void Connect() noexcept; + + void FreeSocket(); + + void HandleFailure() noexcept; + + void ScheduleReconnect(); + + void InitiateReconnect() noexcept; + + void Reconnect() noexcept; + + void GracefulShutdownTimeout() noexcept; + + void OnReadable(int file_descriptor, short what) noexcept; + + void OnWritable(int file_descriptor, short what) noexcept; + + void OnSocketError() noexcept; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_dns_resolution_manager.cpp b/src/recorder/stream_recorder/satellite_dns_resolution_manager.cpp new file mode 100644 index 00000000..5ab52032 --- /dev/null +++ b/src/recorder/stream_recorder/satellite_dns_resolution_manager.cpp @@ -0,0 +1,99 @@ +#include "recorder/stream_recorder/satellite_dns_resolution_manager.h" + +#include "common/random.h" +#include "network/timer_event.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +SatelliteDnsResolutionManager::SatelliteDnsResolutionManager( + Logger& logger, EventBase& event_base, + const StreamRecorderOptions& recorder_options, int family, const char* name, + std::function on_ready_callback) + : logger_{logger}, + event_base_{event_base}, + dns_resolver_{MakeDnsResolver(logger, event_base, + recorder_options.dns_resolver_options)}, + recorder_options_{recorder_options}, + family_{family}, + name_{name}, + on_ready_callback_{std::move(on_ready_callback)} {} + +//-------------------------------------------------------------------------------------------------- +// Start +//-------------------------------------------------------------------------------------------------- +void SatelliteDnsResolutionManager::Start() noexcept { + logger_.Info("Resolving ", name_, " for ", + family_ == AF_INET ? "ipv4" : "ipv6"); + dns_resolver_->Resolve(name_, family_, *this); +} + +//-------------------------------------------------------------------------------------------------- +// OnDnsResolution +//-------------------------------------------------------------------------------------------------- +void SatelliteDnsResolutionManager::OnDnsResolution( + const DnsResolution& dns_resolution, + opentracing::string_view error_message) noexcept try { + if (!error_message.empty()) { + logger_.Debug("Failed to resolve ", name_, ": ", error_message); + return OnFailure(); + } + std::vector ip_addresses; + ip_addresses.reserve(ip_addresses_.size()); + dns_resolution.ForeachIpAddress([&](const IpAddress& ip_address) { + logger_.Info("Resolved ", name_, " to ", ip_address); + ip_addresses.push_back(ip_address); + return true; + }); + if (ip_addresses.empty()) { + logger_.Debug("Dns resolution returned no addresses for ", name_); + return OnFailure(); + } + bool first_resolution = ip_addresses_.empty(); + ip_addresses_ = std::move(ip_addresses); + if (first_resolution) { + on_ready_callback_(); + } + ScheduleRefresh(); +} catch (const std::exception& e) { + logger_.Error("OnDnsResolution failed: ", e.what()); + OnFailure(); +} + +//-------------------------------------------------------------------------------------------------- +// OnRefresh +//-------------------------------------------------------------------------------------------------- +void SatelliteDnsResolutionManager::OnRefresh() noexcept { + dns_resolver_->Resolve(name_, family_, *this); +} + +//-------------------------------------------------------------------------------------------------- +// OnFailure +//-------------------------------------------------------------------------------------------------- +void SatelliteDnsResolutionManager::OnFailure() noexcept try { + event_base_.OnTimeout( + recorder_options_.dns_failure_retry_period, + MakeTimerCallback(), + static_cast(this)); +} catch (const std::exception& e) { + logger_.Error("OnFailure failed: ", e.what()); +} + +//-------------------------------------------------------------------------------------------------- +// SetupRefresh +//-------------------------------------------------------------------------------------------------- +void SatelliteDnsResolutionManager::ScheduleRefresh() noexcept try { + auto refresh_timeout = GenerateRandomDuration( + recorder_options_.min_dns_resolution_refresh_period, + recorder_options_.max_dns_resolution_refresh_period); + event_base_.OnTimeout( + refresh_timeout, + MakeTimerCallback(), + static_cast(this)); +} catch (const std::exception& e) { + logger_.Error("ScheduleRefresh failed: ", e.what()); +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_dns_resolution_manager.h b/src/recorder/stream_recorder/satellite_dns_resolution_manager.h new file mode 100644 index 00000000..1eb2f4d9 --- /dev/null +++ b/src/recorder/stream_recorder/satellite_dns_resolution_manager.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include + +#include "common/logger.h" +#include "common/noncopyable.h" +#include "network/dns_resolver.h" +#include "network/event_base.h" +#include "network/ip_address.h" +#include "network/timer_event.h" +#include "recorder/stream_recorder/stream_recorder_options.h" + +namespace lightstep { +/** + * Manages the DNS resolution of a host. + */ +class SatelliteDnsResolutionManager final : public DnsResolutionCallback, + private Noncopyable { + public: + SatelliteDnsResolutionManager(Logger& logger, EventBase& event_base, + const StreamRecorderOptions& recorder_options, + int family, const char* name, + std::function on_ready_callback); + + /** + * Start resolving the host an ip addresses. + */ + void Start() noexcept; + + /** + * @return the vector of IP addresses the host resolves to. + */ + const std::vector& ip_addresses() const noexcept { + return ip_addresses_; + } + + /** + * @return the name associated with this resolution + */ + const char* name() const noexcept { return name_; } + + // DnsResolutionCallback + void OnDnsResolution( + const DnsResolution& dns_resolution, + opentracing::string_view error_message) noexcept override; + + private: + Logger& logger_; + EventBase& event_base_; + std::unique_ptr dns_resolver_; + const StreamRecorderOptions& recorder_options_; + int family_; + const char* name_; + std::function on_ready_callback_; + + std::vector ip_addresses_; + + void OnRefresh() noexcept; + + void OnFailure() noexcept; + + void ScheduleRefresh() noexcept; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_endpoint_manager.cpp b/src/recorder/stream_recorder/satellite_endpoint_manager.cpp new file mode 100644 index 00000000..8628fba5 --- /dev/null +++ b/src/recorder/stream_recorder/satellite_endpoint_manager.cpp @@ -0,0 +1,87 @@ +#include "recorder/stream_recorder/satellite_endpoint_manager.h" + +#include + +#include "recorder/stream_recorder/utility.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +SatelliteEndpointManager::SatelliteEndpointManager( + Logger& logger, EventBase& event_base, + const LightStepTracerOptions& tracer_options, + const StreamRecorderOptions& recorder_options, + std::function on_ready_callback) + : on_ready_callback_{std::move(on_ready_callback)} { + if (tracer_options.satellite_endpoints.empty()) { + throw std::runtime_error{"no satellite endpoints provided"}; + } + std::vector hosts; + std::tie(hosts, endpoints_) = + SeparateEndpoints(tracer_options.satellite_endpoints); + + host_managers_.reserve(hosts.size()); + auto on_resolution_ready = [this] { this->OnResolutionReady(); }; + for (auto& name : hosts) { + SatelliteHostManager host_manager; + host_manager.ipv4_resolutions.reset( + new SatelliteDnsResolutionManager{logger, event_base, recorder_options, + AF_INET, name, on_resolution_ready}); + host_manager.ipv6_resolutions.reset( + new SatelliteDnsResolutionManager{logger, event_base, recorder_options, + AF_INET6, name, on_resolution_ready}); + host_managers_.emplace_back(std::move(host_manager)); + } +} + +//-------------------------------------------------------------------------------------------------- +// Start +//-------------------------------------------------------------------------------------------------- +void SatelliteEndpointManager::Start() noexcept { + for (auto& host_manager : host_managers_) { + host_manager.ipv4_resolutions->Start(); + host_manager.ipv6_resolutions->Start(); + } +} + +//-------------------------------------------------------------------------------------------------- +// RequestEndpoint +//-------------------------------------------------------------------------------------------------- +std::pair +SatelliteEndpointManager::RequestEndpoint() noexcept { + auto endpoint_index_start = endpoint_index_; + while (true) { + // RequestEndpoint shouldn't be called until at least one host name is + // resolved. + assert(endpoint_index_ != endpoint_index_start + endpoints_.size()); + + int host_index; + uint16_t port; + std::tie(host_index, port) = + endpoints_[endpoint_index_++ % endpoints_.size()]; + auto& host_manager = host_managers_[host_index]; + + auto& ip_addresses = !host_manager.ipv4_resolutions->ip_addresses().empty() + ? host_manager.ipv4_resolutions->ip_addresses() + : host_manager.ipv6_resolutions->ip_addresses(); + if (ip_addresses.empty()) { + continue; + } + auto result = + ip_addresses[host_manager.address_index++ % ip_addresses.size()]; + result.set_port(port); + return std::make_pair(result, host_manager.ipv4_resolutions->name()); + } +} + +//-------------------------------------------------------------------------------------------------- +// OnResolutionReady +//-------------------------------------------------------------------------------------------------- +void SatelliteEndpointManager::OnResolutionReady() noexcept { + ++num_resolutions_ready_; + if (num_resolutions_ready_ == 1) { + on_ready_callback_(); + } +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_endpoint_manager.h b/src/recorder/stream_recorder/satellite_endpoint_manager.h new file mode 100644 index 00000000..756d6628 --- /dev/null +++ b/src/recorder/stream_recorder/satellite_endpoint_manager.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include "common/noncopyable.h" +#include "lightstep/tracer.h" +#include "network/dns_resolver.h" +#include "recorder/stream_recorder/satellite_dns_resolution_manager.h" + +namespace lightstep { +/** + * Manages the resolution and assignment of satellite endpoints. + */ +class SatelliteEndpointManager : private Noncopyable { + struct SatelliteHostManager { + std::unique_ptr ipv4_resolutions; + std::unique_ptr ipv6_resolutions; + uint32_t address_index{0}; + }; + + public: + SatelliteEndpointManager(Logger& logger, EventBase& event_base, + const LightStepTracerOptions& tracer_options, + const StreamRecorderOptions& recorder_options, + std::function on_ready_callback); + + /** + * Start resolving hosts to ip addresses. + */ + void Start() noexcept; + + /** + * Assigns satellite endpoints using round robin. + * @return a satellite endpoint. + */ + std::pair RequestEndpoint() noexcept; + + private: + std::function on_ready_callback_; + std::vector host_managers_; + std::vector> endpoints_; + uint32_t endpoint_index_{0}; + int num_resolutions_ready_{0}; + + void OnResolutionReady() noexcept; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_streamer.cpp b/src/recorder/stream_recorder/satellite_streamer.cpp new file mode 100644 index 00000000..828b4b0c --- /dev/null +++ b/src/recorder/stream_recorder/satellite_streamer.cpp @@ -0,0 +1,61 @@ +#include "recorder/stream_recorder/satellite_streamer.h" + +#include +#include + +#include "common/random.h" +#include "recorder/stream_recorder/utility.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +SatelliteStreamer::SatelliteStreamer( + Logger& logger, EventBase& event_base, + const LightStepTracerOptions& tracer_options, + const StreamRecorderOptions& recorder_options, + StreamRecorderMetrics& metrics, ChunkCircularBuffer& span_buffer) + : logger_{logger}, + event_base_{event_base}, + tracer_options_{tracer_options}, + recorder_options_{recorder_options}, + header_common_fragment_{ + WriteStreamHeaderCommonFragment(tracer_options, GenerateId())}, + endpoint_manager_{logger, event_base, tracer_options, recorder_options, + [this] { this->OnEndpointManagerReady(); }}, + span_buffer_{span_buffer}, + span_stream_{span_buffer, metrics, + recorder_options_.num_satellite_connections}, + connection_traverser_{recorder_options_.num_satellite_connections} { + connections_.reserve(recorder_options.num_satellite_connections); + for (int i = 0; i < recorder_options.num_satellite_connections; ++i) { + connections_.emplace_back(new SatelliteConnection{*this}); + } + endpoint_manager_.Start(); +} + +//-------------------------------------------------------------------------------------------------- +// Flush +//-------------------------------------------------------------------------------------------------- +void SatelliteStreamer::Flush() noexcept { + if (span_buffer_.buffer().empty()) { + return; + } + connection_traverser_.ForEachIndex([this](int index) { + auto& connection = connections_[index]; + if (!connection->ready()) { + return true; + } + return !connection->Flush(); + }); +} + +//-------------------------------------------------------------------------------------------------- +// OnEndpointManagerReady +//-------------------------------------------------------------------------------------------------- +void SatelliteStreamer::OnEndpointManagerReady() noexcept { + for (auto& connection : connections_) { + connection->Start(); + } +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/satellite_streamer.h b/src/recorder/stream_recorder/satellite_streamer.h new file mode 100644 index 00000000..697a0e7b --- /dev/null +++ b/src/recorder/stream_recorder/satellite_streamer.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#include "common/chunk_circular_buffer.h" +#include "common/noncopyable.h" +#include "common/random_traverser.h" +#include "recorder/stream_recorder/satellite_connection.h" +#include "recorder/stream_recorder/satellite_endpoint_manager.h" +#include "recorder/stream_recorder/span_stream.h" +#include "recorder/stream_recorder/stream_recorder_metrics.h" + +namespace lightstep { +class SatelliteStreamer : private Noncopyable { + public: + SatelliteStreamer(Logger& logger, EventBase& event_base, + const LightStepTracerOptions& tracer_options, + const StreamRecorderOptions& recorder_options, + StreamRecorderMetrics& metrics, + ChunkCircularBuffer& span_buffer); + + /** + * @return the associated Logger. + */ + Logger& logger() const noexcept { return logger_; } + + /** + * @return the associated EventBase. + */ + EventBase& event_base() const noexcept { return event_base_; } + + /** + * @return the associated LightStepTracerOptions + */ + const LightStepTracerOptions& tracer_options() const noexcept { + return tracer_options_; + } + + /** + * @return the associated StreamRecorderOptions. + */ + const StreamRecorderOptions& recorder_options() const noexcept { + return recorder_options_; + } + + /** + * @return the SpanStream formed from the StreamRecorder's message buffer. + */ + SpanStream& span_stream() noexcept { return span_stream_; } + + /** + * @return the fragment with the common serialization data sent in + * ReportRequests. + */ + std::pair header_common_fragment() const noexcept { + return { + static_cast(const_cast(header_common_fragment_.data())), + static_cast(header_common_fragment_.size())}; + } + + /** + * @return an SatelliteEndpointManager for load balancing. + */ + SatelliteEndpointManager& endpoint_manager() noexcept { + return endpoint_manager_; + } + + /** + * Flush data to satellites if connections are available. + */ + void Flush() noexcept; + + private: + Logger& logger_; + EventBase& event_base_; + const LightStepTracerOptions& tracer_options_; + const StreamRecorderOptions& recorder_options_; + std::string header_common_fragment_; + SatelliteEndpointManager endpoint_manager_; + ChunkCircularBuffer& span_buffer_; + SpanStream span_stream_; + std::vector> connections_; + RandomTraverser connection_traverser_; + + void OnEndpointManagerReady() noexcept; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/span_stream.cpp b/src/recorder/stream_recorder/span_stream.cpp new file mode 100644 index 00000000..610d7685 --- /dev/null +++ b/src/recorder/stream_recorder/span_stream.cpp @@ -0,0 +1,164 @@ +#include "recorder/stream_recorder/span_stream.h" + +#include +#include +#include + +#include "recorder/stream_recorder/utility.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +SpanStream::SpanStream(ChunkCircularBuffer& span_buffer, + StreamRecorderMetrics& metrics, int max_connections) + : span_buffer_{span_buffer}, metrics_{metrics} { + span_remnants_.reserve(static_cast(max_connections)); +} + +//-------------------------------------------------------------------------------------------------- +// Allot +//-------------------------------------------------------------------------------------------------- +void SpanStream::Allot() noexcept { + num_spans_pending_ += span_buffer_.Allot(); + allotment_ = span_buffer_.allotment(); + if (stream_position_ == nullptr) { + stream_position_ = allotment_.data1; + } +} + +//-------------------------------------------------------------------------------------------------- +// num_fragments +//-------------------------------------------------------------------------------------------------- +int SpanStream::num_fragments() const noexcept { + if (Contains(allotment_.data1, allotment_.size1, stream_position_)) { + return 1 + static_cast(allotment_.size2 > 0); + } + return static_cast( + Contains(allotment_.data2, allotment_.size2, stream_position_)); +} + +//-------------------------------------------------------------------------------------------------- +// RemoveSpanRemnant +//-------------------------------------------------------------------------------------------------- +void SpanStream::RemoveSpanRemnant(const char* remnant) noexcept { + auto last = + std::remove(span_remnants_.begin(), span_remnants_.end(), remnant); + assert(last != span_remnants_.end()); + span_remnants_.erase(last, span_remnants_.end()); +} + +//-------------------------------------------------------------------------------------------------- +// PopSpanRemnant +//-------------------------------------------------------------------------------------------------- +void SpanStream::PopSpanRemnant(FragmentSpanInputStream& remnant) noexcept { + std::swap(remnant, span_remnant_); + span_remnant_.Clear(); +} + +//-------------------------------------------------------------------------------------------------- +// Consume +//-------------------------------------------------------------------------------------------------- +void SpanStream::Consume() noexcept { + auto num_bytes = span_buffer_.buffer().ComputePosition(stream_position_); + for (auto remnant : span_remnants_) { + num_bytes = + std::min(span_buffer_.buffer().ComputePosition(remnant), num_bytes); + } + span_buffer_.Consume(num_bytes); +} + +//-------------------------------------------------------------------------------------------------- +// ForEachFragment +//-------------------------------------------------------------------------------------------------- +bool SpanStream::ForEachFragment(Callback callback) const noexcept { + if (Contains(allotment_.data1, allotment_.size1, stream_position_)) { + auto result = + callback(static_cast(const_cast(stream_position_)), + static_cast(std::distance( + stream_position_, allotment_.data1 + allotment_.size1))); + if (!result) { + return false; + } + if (allotment_.size2 > 0) { + return callback(static_cast(const_cast(allotment_.data2)), + static_cast(allotment_.size2)); + } + return true; + } + if (Contains(allotment_.data2, allotment_.size2, stream_position_)) { + return callback( + static_cast(const_cast(stream_position_)), + static_cast(std::distance(stream_position_, + allotment_.data2 + allotment_.size2))); + } + return true; +} + +//-------------------------------------------------------------------------------------------------- +// Clear +//-------------------------------------------------------------------------------------------------- +void SpanStream::Clear() noexcept { + metrics_.OnSpansSent(num_spans_pending_); + num_spans_pending_ = 0; + SetPositionAfter(allotment_); + span_remnant_.Clear(); +} + +//-------------------------------------------------------------------------------------------------- +// Seek +//-------------------------------------------------------------------------------------------------- +void SpanStream::Seek(int fragment_index, int position) noexcept { + const char* last; + if (fragment_index == 0) { + last = stream_position_ + position; + } else { + assert(fragment_index == 1); + assert(Contains(allotment_.data1, allotment_.size1, stream_position_)); + last = allotment_.data2 + position; + assert(Contains(allotment_.data2, allotment_.size2, last)); + } + CircularBufferConstPlacement chunk; + int num_spans_sent; + std::tie(chunk, num_spans_sent) = + span_buffer_.FindChunk(stream_position_, last); + metrics_.OnSpansSent(num_spans_sent); + num_spans_pending_ -= num_spans_sent; + + span_remnant_.Clear(); + + // If last is at the start of the next chunk, we don't end up with any partial + // span that we need to track. + if (chunk.data1 == last) { + stream_position_ = last; + return; + } + + num_spans_pending_ -= 1; + span_remnants_.emplace_back(chunk.data1); + span_remnant_.Set(chunk, last); + SetPositionAfter(chunk); +} + +//-------------------------------------------------------------------------------------------------- +// SetPositionAfter +//-------------------------------------------------------------------------------------------------- +void SpanStream::SetPositionAfter( + const CircularBufferConstPlacement& placement) noexcept { + if (placement.size2 > 0) { + stream_position_ = placement.data2 + placement.size2; + } else { + stream_position_ = placement.data1 + placement.size1; + } + + // Handle the case when stream_position_ would wrap the end of the circular + // buffer. + // + // Note that max_size is one less than the actual amount of memory allocated + // for the circular buffer. + if (stream_position_ == + span_buffer_.buffer().data() + span_buffer_.buffer().max_size() + 1) { + stream_position_ = span_buffer_.buffer().data(); + } +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/span_stream.h b/src/recorder/stream_recorder/span_stream.h new file mode 100644 index 00000000..3f729a7c --- /dev/null +++ b/src/recorder/stream_recorder/span_stream.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include "common/chunk_circular_buffer.h" +#include "network/fragment_array_input_stream.h" +#include "recorder/stream_recorder/fragment_span_input_stream.h" +#include "recorder/stream_recorder/stream_recorder_metrics.h" + +namespace lightstep { +/** + * Manages the stream of data comming from the circular buffer of completed + * spans. + */ +class SpanStream final : public FragmentInputStream { + public: + SpanStream(ChunkCircularBuffer& span_buffer, StreamRecorderMetrics& metrics, + int max_connections); + + /** + * Allots spans from the ChunkCircularBuffer to stream to satellites. + */ + void Allot() noexcept; + + /** + * Removes the marker of the remnant of a span so that its bytes can be + * consumed. + * @param remnant the span remnant to remove. + */ + void RemoveSpanRemnant(const char* remnant) noexcept; + + /** + * Pops the span remnant left by the write. + * @param remnant where to output the last remnant. + */ + void PopSpanRemnant(FragmentSpanInputStream& remnant) noexcept; + + /** + * Consumes bytes no longer needed from the associated ChunkCircularBuffer. + */ + void Consume() noexcept; + + StreamRecorderMetrics& metrics() const noexcept { return metrics_; } + + // FragmentInputStream + int num_fragments() const noexcept override; + + bool ForEachFragment(Callback callback) const noexcept override; + + void Clear() noexcept override; + + void Seek(int fragment_index, int position) noexcept override; + + private: + ChunkCircularBuffer& span_buffer_; + StreamRecorderMetrics& metrics_; + CircularBufferConstPlacement allotment_; + int num_spans_pending_{0}; + const char* stream_position_{nullptr}; + FragmentSpanInputStream span_remnant_; + std::vector span_remnants_; + + void SetPositionAfter(const CircularBufferConstPlacement& placement) noexcept; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/status_line_parser.cpp b/src/recorder/stream_recorder/status_line_parser.cpp new file mode 100644 index 00000000..42428d2d --- /dev/null +++ b/src/recorder/stream_recorder/status_line_parser.cpp @@ -0,0 +1,54 @@ +#include "recorder/stream_recorder/status_line_parser.h" + +#include +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// Reset +//-------------------------------------------------------------------------------------------------- +void StatusLineParser::Reset() noexcept { + line_.clear(); + complete_ = false; +} + +//-------------------------------------------------------------------------------------------------- +// Parse +//-------------------------------------------------------------------------------------------------- +void StatusLineParser::Parse(opentracing::string_view s) { + if (complete_) { + return; + } + auto i = std::find(s.begin(), s.end(), '\r'); + line_.append(s.data(), static_cast(std::distance(s.begin(), i))); + complete_ = i != s.end(); + if (complete_) { + ParseFields(); + } +} + +//-------------------------------------------------------------------------------------------------- +// ParseFields +//-------------------------------------------------------------------------------------------------- +void StatusLineParser::ParseFields() try { + auto status_code_first = std::find(line_.begin(), line_.end(), ' '); + if (status_code_first == line_.end()) { + throw std::runtime_error{"Invalid http status line \"" + line_ + "\""}; + } + auto status_code_last = + std::find(std::next(status_code_first), line_.end(), ' '); + if (status_code_last == line_.end()) { + throw std::runtime_error{"Invalid http status line \"" + line_ + "\""}; + } + status_code_ = + static_cast(std::strtol(&*status_code_first, nullptr, 10)); + auto reason_first = std::next(status_code_last); + reason_ = opentracing::string_view{ + &*reason_first, + static_cast(std::distance(reason_first, line_.end()))}; +} catch (...) { + Reset(); + throw; +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/status_line_parser.h b/src/recorder/stream_recorder/status_line_parser.h new file mode 100644 index 00000000..30eca2a9 --- /dev/null +++ b/src/recorder/stream_recorder/status_line_parser.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +namespace lightstep { +class StatusLineParser { + public: + void Reset() noexcept; + + void Parse(opentracing::string_view s); + + bool completed() const noexcept { return complete_; } + + int status_code() const noexcept { return status_code_; } + + opentracing::string_view reason() const noexcept { return reason_; } + + private: + std::string line_; + bool complete_{false}; + int status_code_; + opentracing::string_view reason_; + + void ParseFields(); +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/stream_recorder.cpp b/src/recorder/stream_recorder/stream_recorder.cpp new file mode 100644 index 00000000..1a911a3e --- /dev/null +++ b/src/recorder/stream_recorder/stream_recorder.cpp @@ -0,0 +1,162 @@ +#include "stream_recorder.h" + +#include + +#include "common/protobuf.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// GetMetricsObserver +//-------------------------------------------------------------------------------------------------- +static MetricsObserver& GetMetricsObserver( + LightStepTracerOptions& tracer_options) { + if (tracer_options.metrics_observer == nullptr) { + tracer_options.metrics_observer.reset(new MetricsObserver{}); + } + return *tracer_options.metrics_observer; +} + +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +StreamRecorder::StreamRecorder(Logger& logger, + LightStepTracerOptions&& tracer_options, + StreamRecorderOptions&& recorder_options) + : logger_{logger}, + tracer_options_{std::move(tracer_options)}, + recorder_options_{std::move(recorder_options)}, + metrics_{GetMetricsObserver(tracer_options_)}, + span_buffer_{recorder_options_.max_span_buffer_bytes}, + early_flush_marker_{ + static_cast(recorder_options_.max_span_buffer_bytes * + recorder_options.early_flush_threshold)}, + poll_timer_{event_base_, recorder_options_.polling_period, + MakeTimerCallback(), + static_cast(this)}, + flush_timer_{event_base_, recorder_options.flushing_period, + MakeTimerCallback(), + static_cast(this)}, + streamer_{logger_, event_base_, tracer_options_, + recorder_options_, metrics_, span_buffer_} { + // If no MetricsObserver was provided, use a default one that does nothing. + if (tracer_options_.metrics_observer == nullptr) { + tracer_options_.metrics_observer.reset(new MetricsObserver{}); + } + thread_ = std::thread{&StreamRecorder::Run, this}; +} + +//-------------------------------------------------------------------------------------------------- +// destructor +//-------------------------------------------------------------------------------------------------- +StreamRecorder::~StreamRecorder() noexcept { + { + std::lock_guard lock_guard{flush_mutex_}; + exit_ = true; + } + flush_condition_variable_.notify_all(); + thread_.join(); +} + +//-------------------------------------------------------------------------------------------------- +// RecordSpan +//-------------------------------------------------------------------------------------------------- +void StreamRecorder::RecordSpan(const collector::Span& span) noexcept { + auto span_size = span.ByteSizeLong(); + auto serialization_callback = + [&span, span_size](google::protobuf::io::CodedOutputStream& stream) { + WriteEmbeddedMessage(stream, + collector::ReportRequest::kSpansFieldNumber, + span_size, span); + }; + auto was_added = span_buffer_.Add( + serialization_callback, + ComputeEmbeddedMessageSerializationSize( + collector::ReportRequest::kSpansFieldNumber, span_size)); + if (!was_added) { + logger_.Debug("Dropping span ", span.span_context().span_id()); + metrics_.OnSpansDropped(1); + } +} + +//-------------------------------------------------------------------------------------------------- +// FlushWithTimeout +//-------------------------------------------------------------------------------------------------- +bool StreamRecorder::FlushWithTimeout( + std::chrono::system_clock::duration timeout) noexcept try { + auto num_bytes_produced = span_buffer_.buffer().num_bytes_produced(); + std::unique_lock lock{flush_mutex_}; + if (num_bytes_consumed_ >= num_bytes_produced) { + return true; + } + ++pending_flush_counter_; + flush_condition_variable_.wait_for(lock, timeout, [this, num_bytes_produced] { + return exit_ || num_bytes_consumed_ >= num_bytes_produced; + }); + return num_bytes_consumed_ >= num_bytes_produced; +} catch (const std::exception& e) { + logger_.Error("StreamRecorder::FlushWithTimeout failed: ", e.what()); + return false; +} + +//-------------------------------------------------------------------------------------------------- +// Run +//-------------------------------------------------------------------------------------------------- +void StreamRecorder::Run() noexcept try { + event_base_.Dispatch(); +} catch (const std::exception& e) { + logger_.Error("StreamRecorder::Run failed: ", e.what()); +} + +//-------------------------------------------------------------------------------------------------- +// Poll +//-------------------------------------------------------------------------------------------------- +void StreamRecorder::Poll() noexcept { + if (exit_) { + try { + return event_base_.LoopBreak(); + } catch (const std::exception& e) { + logger_.Error("StreamRecorder: failed to break out of event loop: ", + e.what()); + std::terminate(); + } + } + + // Force an early flush if FlushWithTimeout was explicitly called or the + // buffer is filling up. + auto pending_flush_count = pending_flush_counter_.exchange(0); + if (pending_flush_count > 0 || + span_buffer_.buffer().size() > early_flush_marker_) { + Flush(); + } + + auto num_bytes_consumed = span_buffer_.buffer().num_bytes_consumed(); + if (num_bytes_consumed > num_bytes_consumed_) { + { + std::lock_guard lock_guard{flush_mutex_}; + num_bytes_consumed_ = num_bytes_consumed; + } + flush_condition_variable_.notify_all(); + } +} + +//-------------------------------------------------------------------------------------------------- +// Flush +//-------------------------------------------------------------------------------------------------- +void StreamRecorder::Flush() noexcept try { + streamer_.Flush(); + flush_timer_.Reset(); + metrics_.OnFlush(); +} catch (const std::exception& e) { + logger_.Error("StreamRecorder::Flush failed: ", e.what()); +} + +//-------------------------------------------------------------------------------------------------- +// MakeStreamRecorder +//-------------------------------------------------------------------------------------------------- +std::unique_ptr MakeStreamRecorder( + Logger& logger, LightStepTracerOptions&& tracer_options) { + std::unique_ptr result{ + new StreamRecorder{logger, std::move(tracer_options)}}; + return result; +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/stream_recorder.h b/src/recorder/stream_recorder/stream_recorder.h new file mode 100644 index 00000000..18f56325 --- /dev/null +++ b/src/recorder/stream_recorder/stream_recorder.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "common/chunk_circular_buffer.h" +#include "common/logger.h" +#include "common/noncopyable.h" +#include "lightstep/tracer.h" +#include "network/event_base.h" +#include "network/timer_event.h" +#include "recorder/stream_recorder.h" +#include "recorder/stream_recorder/satellite_streamer.h" +#include "recorder/stream_recorder/stream_recorder_metrics.h" +#include "recorder/stream_recorder/stream_recorder_options.h" + +namespace lightstep { +/** + * A Recorder that load balances and streams spans to multiple satellites. + */ +class StreamRecorder final : public Recorder, private Noncopyable { + public: + StreamRecorder( + Logger& logger, LightStepTracerOptions&& tracer_options, + StreamRecorderOptions&& recorder_options = StreamRecorderOptions{}); + + ~StreamRecorder() noexcept override; + + /** + * @return true if no spans are buffered in the recorder. + */ + bool empty() const noexcept { return span_buffer_.buffer().empty(); } + + // Recorder + void RecordSpan(const collector::Span& span) noexcept override; + + bool FlushWithTimeout( + std::chrono::system_clock::duration timeout) noexcept override; + + private: + Logger& logger_; + LightStepTracerOptions tracer_options_; + StreamRecorderOptions recorder_options_; + StreamRecorderMetrics metrics_; + ChunkCircularBuffer span_buffer_; + size_t early_flush_marker_; + + EventBase event_base_; + TimerEvent poll_timer_; + TimerEvent flush_timer_; + + SatelliteStreamer streamer_; + + std::thread thread_; + std::atomic exit_{false}; + + std::mutex flush_mutex_; + std::condition_variable flush_condition_variable_; + std::atomic pending_flush_counter_{0}; + uint64_t num_bytes_consumed_{0}; + + void Run() noexcept; + + void Poll() noexcept; + + void Flush() noexcept; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/stream_recorder_metrics.cpp b/src/recorder/stream_recorder/stream_recorder_metrics.cpp new file mode 100644 index 00000000..c06fa8fd --- /dev/null +++ b/src/recorder/stream_recorder/stream_recorder_metrics.cpp @@ -0,0 +1,44 @@ +#include "recorder/stream_recorder/stream_recorder_metrics.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +StreamRecorderMetrics::StreamRecorderMetrics( + MetricsObserver& metrics_observer) noexcept + : metrics_observer_{metrics_observer} {} + +//-------------------------------------------------------------------------------------------------- +// OnSpansDropped +//-------------------------------------------------------------------------------------------------- +void StreamRecorderMetrics::OnSpansDropped(int num_spans) noexcept { + metrics_observer_.OnSpansDropped(num_spans); + num_dropped_spans_ += num_spans; +} + +//-------------------------------------------------------------------------------------------------- +// OnSpansSent +//-------------------------------------------------------------------------------------------------- +void StreamRecorderMetrics::OnSpansSent(int num_spans) noexcept { + metrics_observer_.OnSpansSent(num_spans); +} + +//-------------------------------------------------------------------------------------------------- +// OnFlush +//-------------------------------------------------------------------------------------------------- +void StreamRecorderMetrics::OnFlush() noexcept { metrics_observer_.OnFlush(); } + +//-------------------------------------------------------------------------------------------------- +// ConsumeDroppedSpans +//-------------------------------------------------------------------------------------------------- +int StreamRecorderMetrics::ConsumeDroppedSpans() noexcept { + return num_dropped_spans_.exchange(0); +} + +//-------------------------------------------------------------------------------------------------- +// UnconsumeDroppedSpans +//-------------------------------------------------------------------------------------------------- +void StreamRecorderMetrics::UnconsumeDroppedSpans(int num_spans) noexcept { + num_dropped_spans_ += num_spans; +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/stream_recorder_metrics.h b/src/recorder/stream_recorder/stream_recorder_metrics.h new file mode 100644 index 00000000..dfa36b73 --- /dev/null +++ b/src/recorder/stream_recorder/stream_recorder_metrics.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include + +namespace lightstep { +/** + * Manages the metrics associated with a StreamRecorder. + */ +class StreamRecorderMetrics { + public: + explicit StreamRecorderMetrics(MetricsObserver& metrics_observer) noexcept; + + /** + * Record dropped spans. + * @param num_spans the number of spans dropped. + */ + void OnSpansDropped(int num_spans) noexcept; + + /** + * Record spans sent. + * @param num_spans the number of spans sent. + */ + void OnSpansSent(int num_spans) noexcept; + + /** + * Record flushes. + */ + void OnFlush() noexcept; + + /** + * Return then clear the dropped span counter. + * @return the dropped span count. + */ + int ConsumeDroppedSpans() noexcept; + + /** + * Add spans back to the dropped span counter. + * @param num_spans the number of dropped spans to add back. + */ + void UnconsumeDroppedSpans(int num_spans) noexcept; + + /** + * @return the dropped span count. + */ + int num_dropped_spans() const noexcept { return num_dropped_spans_; } + + private: + MetricsObserver& metrics_observer_; + std::atomic num_dropped_spans_{0}; +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/stream_recorder_options.h b/src/recorder/stream_recorder/stream_recorder_options.h new file mode 100644 index 00000000..b7b9de67 --- /dev/null +++ b/src/recorder/stream_recorder/stream_recorder_options.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include + +#include "network/dns_resolver.h" + +namespace lightstep { +/** + * Options to tweak the behavior of StreamRecorder. + * + * Note: This class isn't exposed to users. It's only used to change the + * behavior for testing. + */ +struct StreamRecorderOptions { + // The maximum number of bytes that will be buffered. + size_t max_span_buffer_bytes = 1048576; + + // The amount of time between executions of StreamRecorder's polling callback. + // + // Note: This should be a short duration; otherwise, the recorder can hang the + // process when exiting. + std::chrono::microseconds polling_period = + std::chrono::duration_cast( + std::chrono::milliseconds{10}); + + // The amount of time between flushes of StreamRecorder's span buffer. + std::chrono::microseconds flushing_period = + std::chrono::duration_cast( + std::chrono::seconds{5}); + + // If the span buffer fills past this fraction of its max size, then we do + // flush the span buffer early. + double early_flush_threshold = 0.5; + + // Options to use when resolving satellite host names. + DnsResolverOptions dns_resolver_options; + + // Dns resolutions will be cached and refreshed at a random point of time + // within the specified window from their last refresh. + std::chrono::microseconds min_dns_resolution_refresh_period = + std::chrono::duration_cast( + std::chrono::minutes{5}); + std::chrono::microseconds max_dns_resolution_refresh_period = + std::chrono::duration_cast( + std::chrono::minutes{6}); + + // The amount of time to wait until trying dns resolution again after a + // failure. + std::chrono::microseconds dns_failure_retry_period = + std::chrono::duration_cast( + std::chrono::seconds{5}); + + // The number of connections to make to satellites for streaming. + int num_satellite_connections = 8; + + // The amount of time to wait for a satellite connection to be writable before + // reconnecting. + std::chrono::microseconds satellite_write_timeout = + std::chrono::duration_cast( + std::chrono::seconds{5}); + + // The amount of time to wait until attempting to reconnect to a satellite + // after a failure. + std::chrono::microseconds satellite_failure_retry_period = + std::chrono::duration_cast( + std::chrono::seconds{1}); + + // Satellite connections will be reestablished at a random point of time + // within the specified window from when the connection was last established. + std::chrono::microseconds min_satellite_reconnect_period = + std::chrono::duration_cast( + std::chrono::seconds{5}); + std::chrono::microseconds max_satellite_reconnect_period = + std::chrono::duration_cast( + std::chrono::seconds{7}); + + // When we try to close a satellite stream, first attempt to close it + // gracefully by writing any pending data. If that can't be done within this + // time window, then we'll close the socket so that we have an incomplete + // stream. + std::chrono::microseconds satellite_graceful_stream_shutdown_timeout = + std::chrono::duration_cast( + std::chrono::seconds{5}); +}; +} // namespace lightstep diff --git a/src/recorder/stream_recorder/utility.cpp b/src/recorder/stream_recorder/utility.cpp new file mode 100644 index 00000000..53022730 --- /dev/null +++ b/src/recorder/stream_recorder/utility.cpp @@ -0,0 +1,95 @@ +#include "recorder/stream_recorder/utility.h" + +#include +#include +#include +#include + +#include "common/protobuf.h" +#include "common/utility.h" +#include "lightstep-tracer-common/collector.pb.h" + +#include +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// SeparateEndpoints +//-------------------------------------------------------------------------------------------------- +std::pair, std::vector>> +SeparateEndpoints( + const std::vector>& endpoints) { + std::vector hosts; + hosts.reserve(endpoints.size()); + for (auto& endpoint : endpoints) { + hosts.emplace_back(endpoint.first.c_str()); + } + auto less_than = [](const char* lhs, const char* rhs) { + return strcasecmp(lhs, rhs) < 0; + }; + auto equal_to = [](const char* lhs, const char* rhs) { + return strcasecmp(lhs, rhs) == 0; + }; + std::sort(hosts.begin(), hosts.end(), less_than); + hosts.erase(std::unique(hosts.begin(), hosts.end(), equal_to), hosts.end()); + + std::vector> indexed_endpoints; + indexed_endpoints.reserve(endpoints.size()); + for (auto& endpoint : endpoints) { + auto iter = std::lower_bound(hosts.begin(), hosts.end(), + endpoint.first.c_str(), less_than); + assert(iter != hosts.end()); + indexed_endpoints.emplace_back( + static_cast(std::distance(hosts.begin(), iter)), endpoint.second); + } + return {std::move(hosts), std::move(indexed_endpoints)}; +} + +//-------------------------------------------------------------------------------------------------- +// WriteStreamHeaderCommonFragment +//-------------------------------------------------------------------------------------------------- +std::string WriteStreamHeaderCommonFragment( + const LightStepTracerOptions& tracer_options, uint64_t reporter_id) { + collector::Reporter reporter; + reporter.set_reporter_id(reporter_id); + reporter.mutable_tags()->Reserve( + static_cast(tracer_options.tags.size())); + for (const auto& tag : tracer_options.tags) { + *reporter.mutable_tags()->Add() = ToKeyValue(tag.first, tag.second); + } + + collector::Auth auth; + auth.set_access_token(tracer_options.access_token); + + std::ostringstream oss; + { + google::protobuf::io::OstreamOutputStream zero_copy_stream{&oss}; + google::protobuf::io::CodedOutputStream coded_stream{&zero_copy_stream}; + + WriteEmbeddedMessage( + coded_stream, collector::ReportRequest::kReporterFieldNumber, reporter); + WriteEmbeddedMessage(coded_stream, + collector::ReportRequest::kAuthFieldNumber, auth); + } + + return oss.str(); +} + +//-------------------------------------------------------------------------------------------------- +// Contains +//-------------------------------------------------------------------------------------------------- +bool Contains(const char* data, size_t size, const char* ptr) noexcept { + if (data == nullptr) { + return false; + } + if (ptr == nullptr) { + return false; + } + auto delta = std::distance(data, ptr); + if (delta < 0) { + return false; + } + return static_cast(delta) < size; +} +} // namespace lightstep diff --git a/src/recorder/stream_recorder/utility.h b/src/recorder/stream_recorder/utility.h new file mode 100644 index 00000000..6231ef0a --- /dev/null +++ b/src/recorder/stream_recorder/utility.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +#include "common/circular_buffer.h" +#include "lightstep/tracer.h" +#include "network/fragment_array_input_stream.h" + +namespace lightstep { +/** + * Separates a vector of host-port pairs into a bector of unique hosts and + * indexed endpoints. + * @param endpoints a vector of host-port pairs. + * @return a vector of unique hosts and a vector of indexed endpoints. + */ +std::pair, std::vector>> +SeparateEndpoints( + const std::vector>& endpoints); + +/** + * Serializes the common parts of a ReportRequest. + * @param tracer_options the options used to construct the tracer. + * @param reporter_id a unique ID for the reporter. + * @return a string with the common parts of the ReportRequest serialization. + */ +std::string WriteStreamHeaderCommonFragment( + const LightStepTracerOptions& tracer_options, uint64_t reporter_id); + +/** + * Determines if a given range contains a pointer. + * @param data the start of the range + * @param size the size of the range + * @param ptr to pointer to check for + * @return true if ptr is contained in [data, data+size) + */ +bool Contains(const char* data, size_t size, const char* ptr) noexcept; +} // namespace lightstep diff --git a/src/transporter.cpp b/src/recorder/transporter.cpp similarity index 100% rename from src/transporter.cpp rename to src/recorder/transporter.cpp diff --git a/src/tracer/BUILD b/src/tracer/BUILD new file mode 100644 index 00000000..a9ad725c --- /dev/null +++ b/src/tracer/BUILD @@ -0,0 +1,152 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "propagation_lib", + private_hdrs = [ + "propagation.h", + ], + srcs = [ + "propagation.cpp", + ], + deps = [ + "//src/common:utility_lib", + "//src/common:in_memory_stream_lib", + "//3rd_party/base64:base64_lib", + "//lightstep-tracer-common:lightstep_carrier_proto_cc", + ], + external_deps = [ + "@com_google_protobuf//:protobuf", + "@io_opentracing_cpp//:opentracing", + ], +) + +lightstep_cc_library( + name = "binary_carrier_lib", + srcs = [ + "binary_carrier.cpp", + ], + deps = [ + "//include/lightstep:binary_carrier_interface", + "//include/lightstep:tracer_interface", + ":tracer_lib", + ], +) + +lightstep_cc_library( + name = "dynamic_load_lib", + srcs = [ + "dynamic_load.cpp", + ], + deps = [ + ":lightstep_tracer_factory_lib", + ], +) + +lightstep_cc_library( + name = "lightstep_span_context_interface", + private_hdrs = [ + "lightstep_span_context.h", + ], + srcs = [ + "lightstep_span_context.cpp", + ], + deps = [ + ":propagation_lib", + ], +) + +lightstep_cc_library( + name = "lightstep_immutable_span_context_lib", + private_hdrs = [ + "lightstep_immutable_span_context.h", + ], + srcs = [ + "lightstep_immutable_span_context.cpp", + ], + deps = [ + ":lightstep_span_context_interface", + ], +) + +lightstep_cc_library( + name = "lightstep_span_lib", + private_hdrs = [ + "lightstep_span.h", + ], + srcs = [ + "lightstep_span.cpp", + ], + deps = [ + "//src/common:logger_lib", + "//src/common:utility_lib", + "//src/common:random_lib", + "//src/recorder:recorder_interface", + ":lightstep_span_context_interface", + ], +) + +lightstep_cc_library( + name = "lightstep_tracer_impl_lib", + private_hdrs = [ + "lightstep_tracer_impl.h", + ], + srcs = [ + "lightstep_tracer_impl.cpp", + ], + deps = [ + "//src/common:logger_lib", + "//src/recorder:recorder_interface", + ":propagation_lib", + ":lightstep_span_lib", + ":lightstep_immutable_span_context_lib", + ], +) + +lightstep_cc_library( + name = "lightstep_tracer_factory_lib", + private_hdrs = [ + "lightstep_tracer_factory.h", + ], + srcs = [ + "lightstep_tracer_factory.cpp", + ], + deps = [ + "//lightstep-tracer-configuration:tracer_configuration_proto_cc", + "//include/lightstep:tracer_interface", + ], + external_deps = [ + "@com_google_protobuf//:protobuf", + ], +) + +lightstep_cc_library( + name = "tracer_lib", + srcs = [ + "tracer.cpp", + ], + deps = [ + "//src/recorder:auto_recorder_lib", + "//src/recorder:grpc_transporter_interface", + "//src/recorder:manual_recorder_lib", + "//src/recorder:stream_recorder_interface", + "//lightstep-tracer-common:collector_proto_cc", + "//:config_lib", + ":lightstep_tracer_impl_lib", + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + ], +) + +lightstep_cc_library( + name = "no_default_ssl_roots_pem_lib", + srcs = [ + "no_default_ssl_roots_pem.cpp", + ], +) diff --git a/src/binary_carrier.cpp b/src/tracer/binary_carrier.cpp similarity index 100% rename from src/binary_carrier.cpp rename to src/tracer/binary_carrier.cpp diff --git a/src/dynamic_load.cpp b/src/tracer/dynamic_load.cpp similarity index 97% rename from src/dynamic_load.cpp rename to src/tracer/dynamic_load.cpp index 7f740dc7..eab6c430 100644 --- a/src/dynamic_load.cpp +++ b/src/tracer/dynamic_load.cpp @@ -1,7 +1,7 @@ #include #include #include -#include "lightstep_tracer_factory.h" +#include "tracer/lightstep_tracer_factory.h" static int OpenTracingMakeTracerFactoryFunction( const char* opentracing_version, const char* opentracing_abi_version, diff --git a/src/lightstep_immutable_span_context.cpp b/src/tracer/lightstep_immutable_span_context.cpp similarity index 96% rename from src/lightstep_immutable_span_context.cpp rename to src/tracer/lightstep_immutable_span_context.cpp index c9d54ac2..7a59ce02 100644 --- a/src/lightstep_immutable_span_context.cpp +++ b/src/tracer/lightstep_immutable_span_context.cpp @@ -1,4 +1,4 @@ -#include "lightstep_immutable_span_context.h" +#include "tracer/lightstep_immutable_span_context.h" namespace lightstep { //------------------------------------------------------------------------------ diff --git a/src/lightstep_immutable_span_context.h b/src/tracer/lightstep_immutable_span_context.h similarity index 97% rename from src/lightstep_immutable_span_context.h rename to src/tracer/lightstep_immutable_span_context.h index 41768785..a58f3c14 100644 --- a/src/lightstep_immutable_span_context.h +++ b/src/tracer/lightstep_immutable_span_context.h @@ -1,6 +1,6 @@ #pragma once -#include "lightstep_span_context.h" +#include "tracer/lightstep_span_context.h" namespace lightstep { class LightStepImmutableSpanContext final : public LightStepSpanContext { diff --git a/src/lightstep_span.cpp b/src/tracer/lightstep_span.cpp similarity index 99% rename from src/lightstep_span.cpp rename to src/tracer/lightstep_span.cpp index b6fac63e..f111240e 100644 --- a/src/lightstep_span.cpp +++ b/src/tracer/lightstep_span.cpp @@ -1,11 +1,13 @@ -#include "lightstep_span.h" +#include "tracer/lightstep_span.h" #include -#include "utility.h" -using opentracing::SystemTime; -using opentracing::SystemClock; +#include "common/random.h" +#include "common/utility.h" + using opentracing::SteadyClock; using opentracing::SteadyTime; +using opentracing::SystemClock; +using opentracing::SystemTime; namespace lightstep { //------------------------------------------------------------------------------ diff --git a/src/lightstep_span.h b/src/tracer/lightstep_span.h similarity index 93% rename from src/lightstep_span.h rename to src/tracer/lightstep_span.h index 78f1c1dc..ffc4c02f 100644 --- a/src/lightstep_span.h +++ b/src/tracer/lightstep_span.h @@ -1,9 +1,9 @@ #pragma once +#include "common/logger.h" #include "lightstep-tracer-common/collector.pb.h" -#include "lightstep_span_context.h" -#include "logger.h" -#include "recorder.h" +#include "recorder/recorder.h" +#include "tracer/lightstep_span_context.h" #include @@ -66,19 +66,19 @@ class LightStepSpan final : public opentracing::Span, return span_.span_context().span_id(); } - virtual opentracing::expected Inject( + opentracing::expected Inject( const PropagationOptions& propagation_options, std::ostream& writer) const override { return this->InjectImpl(propagation_options, writer); } - virtual opentracing::expected Inject( + opentracing::expected Inject( const PropagationOptions& propagation_options, const opentracing::TextMapWriter& writer) const override { return this->InjectImpl(propagation_options, writer); } - virtual opentracing::expected Inject( + opentracing::expected Inject( const PropagationOptions& propagation_options, const opentracing::HTTPHeadersWriter& writer) const override { return this->InjectImpl(propagation_options, writer); diff --git a/src/lightstep_span_context.cpp b/src/tracer/lightstep_span_context.cpp similarity index 95% rename from src/lightstep_span_context.cpp rename to src/tracer/lightstep_span_context.cpp index 76e6a962..d3231556 100644 --- a/src/lightstep_span_context.cpp +++ b/src/tracer/lightstep_span_context.cpp @@ -1,4 +1,4 @@ -#include "lightstep_span_context.h" +#include "tracer/lightstep_span_context.h" namespace lightstep { //------------------------------------------------------------------------------ diff --git a/src/lightstep_span_context.h b/src/tracer/lightstep_span_context.h similarity index 97% rename from src/lightstep_span_context.h rename to src/tracer/lightstep_span_context.h index d0a44967..c3f4d890 100644 --- a/src/lightstep_span_context.h +++ b/src/tracer/lightstep_span_context.h @@ -5,7 +5,7 @@ #include #include #include -#include "propagation.h" +#include "tracer/propagation.h" namespace lightstep { class LightStepSpanContext : public opentracing::SpanContext { diff --git a/src/lightstep_tracer_factory.cpp b/src/tracer/lightstep_tracer_factory.cpp similarity index 60% rename from src/lightstep_tracer_factory.cpp rename to src/tracer/lightstep_tracer_factory.cpp index fcaeb6e6..9feb24c7 100644 --- a/src/lightstep_tracer_factory.cpp +++ b/src/tracer/lightstep_tracer_factory.cpp @@ -1,9 +1,36 @@ -#include "lightstep_tracer_factory.h" +#include "tracer/lightstep_tracer_factory.h" + +#include + #include #include #include "lightstep-tracer-configuration/tracer_configuration.pb.h" namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// GetSatelliteEndpoints +//-------------------------------------------------------------------------------------------------- +static std::vector> GetSatelliteEndpoints( + const tracer_configuration::TracerConfiguration& tracer_configuration) { + std::vector> result; + const auto& satellite_endpoints = tracer_configuration.satellite_endpoints(); + result.reserve(satellite_endpoints.size()); + for (auto& endpoint : satellite_endpoints) { + auto port = endpoint.port(); + if (port == 0) { + throw std::runtime_error{"endpoint port outside of range"}; + } + if (port > std::numeric_limits::max()) { + throw std::runtime_error{"endpoint port outside of range"}; + } + result.emplace_back(endpoint.host(), static_cast(port)); + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +// MakeTracer +//-------------------------------------------------------------------------------------------------- opentracing::expected> LightStepTracerFactory::MakeTracer(const char* configuration, std::string& error_message) const @@ -43,6 +70,10 @@ LightStepTracerFactory::MakeTracer(const char* configuration, std::chrono::microseconds{tracer_configuration.report_timeout()}; } + options.use_stream_recorder = tracer_configuration.use_stream_recorder(); + + options.satellite_endpoints = GetSatelliteEndpoints(tracer_configuration); + auto result = std::shared_ptr{ MakeLightStepTracer(std::move(options))}; if (result == nullptr) { diff --git a/src/lightstep_tracer_factory.h b/src/tracer/lightstep_tracer_factory.h similarity index 100% rename from src/lightstep_tracer_factory.h rename to src/tracer/lightstep_tracer_factory.h diff --git a/src/lightstep_tracer_impl.cpp b/src/tracer/lightstep_tracer_impl.cpp similarity index 89% rename from src/lightstep_tracer_impl.cpp rename to src/tracer/lightstep_tracer_impl.cpp index 76700e2c..a1d0356b 100644 --- a/src/lightstep_tracer_impl.cpp +++ b/src/tracer/lightstep_tracer_impl.cpp @@ -1,8 +1,9 @@ -#include "lightstep_tracer_impl.h" -#include "lightstep_immutable_span_context.h" -#include "lightstep_span.h" +#include "tracer/lightstep_tracer_impl.h" +#include "tracer/lightstep_immutable_span_context.h" +#include "tracer/lightstep_span.h" namespace lightstep { +const auto DefaultFlushTimeout = std::chrono::seconds{10}; //------------------------------------------------------------------------------ // InjectImpl @@ -120,7 +121,15 @@ LightStepTracerImpl::Extract( // Flush //------------------------------------------------------------------------------ bool LightStepTracerImpl::Flush() noexcept { - return recorder_->FlushWithTimeout(std::chrono::hours(24)); + return recorder_->FlushWithTimeout(DefaultFlushTimeout); +} + +//-------------------------------------------------------------------------------------------------- +// FlushWithTimeout +//-------------------------------------------------------------------------------------------------- +bool LightStepTracerImpl::FlushWithTimeout( + std::chrono::system_clock::duration timeout) noexcept { + return recorder_->FlushWithTimeout(timeout); } //------------------------------------------------------------------------------ diff --git a/src/lightstep_tracer_impl.h b/src/tracer/lightstep_tracer_impl.h similarity index 90% rename from src/lightstep_tracer_impl.h rename to src/tracer/lightstep_tracer_impl.h index 4dfa4d83..04941d9b 100644 --- a/src/lightstep_tracer_impl.h +++ b/src/tracer/lightstep_tracer_impl.h @@ -1,9 +1,9 @@ #pragma once #include -#include "logger.h" -#include "propagation.h" -#include "recorder.h" +#include "common/logger.h" +#include "recorder/recorder.h" +#include "tracer/propagation.h" namespace lightstep { class LightStepTracerImpl final @@ -44,6 +44,8 @@ class LightStepTracerImpl final bool Flush() noexcept override; + bool FlushWithTimeout(std::chrono::system_clock::duration timeout) noexcept override; + void Close() noexcept override; private: diff --git a/src/no_default_ssl_roots_pem.cpp b/src/tracer/no_default_ssl_roots_pem.cpp similarity index 100% rename from src/no_default_ssl_roots_pem.cpp rename to src/tracer/no_default_ssl_roots_pem.cpp diff --git a/src/propagation.cpp b/src/tracer/propagation.cpp similarity index 98% rename from src/propagation.cpp rename to src/tracer/propagation.cpp index e11f9978..e66c9928 100644 --- a/src/propagation.cpp +++ b/src/tracer/propagation.cpp @@ -1,15 +1,16 @@ -#include "propagation.h" +#include "tracer/propagation.h" #include #include +#include #include #include #include #include #include #include -#include "in_memory_stream.h" +#include "common/in_memory_stream.h" +#include "common/utility.h" #include "lightstep-tracer-common/lightstep_carrier.pb.h" -#include "utility.h" namespace lightstep { #define PREFIX_TRACER_STATE "ot-tracer-" @@ -93,12 +94,13 @@ static opentracing::expected InjectSpanContextBaggage( static opentracing::expected InjectSpanContextMultiKey( const opentracing::TextMapWriter& carrier, uint64_t trace_id, uint64_t span_id, bool sampled, const BaggageMap& baggage) { - char data[16]; - auto result = carrier.Set(FieldNameTraceID, Uint64ToHex(trace_id, data)); + std::array data; + auto result = + carrier.Set(FieldNameTraceID, Uint64ToHex(trace_id, data.data())); if (!result) { return result; } - result = carrier.Set(FieldNameSpanID, Uint64ToHex(span_id, data)); + result = carrier.Set(FieldNameSpanID, Uint64ToHex(span_id, data.data())); if (!result) { return result; } diff --git a/src/propagation.h b/src/tracer/propagation.h similarity index 100% rename from src/propagation.h rename to src/tracer/propagation.h diff --git a/src/tracer.cpp b/src/tracer/tracer.cpp similarity index 82% rename from src/tracer.cpp rename to src/tracer/tracer.cpp index 6122fef5..33658d76 100644 --- a/src/tracer.cpp +++ b/src/tracer/tracer.cpp @@ -1,22 +1,26 @@ -#include -#include -#include -#include -#include +#include "lightstep/tracer.h" + #include #include #include #include #include #include -#include "auto_recorder.h" -#include "grpc_transporter.h" + +#include "common/logger.h" +#include "common/utility.h" #include "lightstep-tracer-common/collector.pb.h" -#include "lightstep_immutable_span_context.h" -#include "lightstep_tracer_impl.h" -#include "logger.h" -#include "manual_recorder.h" -#include "utility.h" +#include "lightstep/version.h" +#include "recorder/auto_recorder.h" +#include "recorder/grpc_transporter.h" +#include "recorder/manual_recorder.h" +#include "recorder/stream_recorder.h" +#include "tracer/lightstep_immutable_span_context.h" +#include "tracer/lightstep_tracer_impl.h" + +#include "opentracing/string_view.h" +#include "opentracing/value.h" +#include "opentracing/version.h" namespace lightstep { const opentracing::string_view component_name_key = "lightstep.component_name"; @@ -117,6 +121,18 @@ static std::shared_ptr MakeThreadedTracer( std::move(logger), propagation_options, std::move(recorder)}}; } +//-------------------------------------------------------------------------------------------------- +// MakeStreamTracer +//-------------------------------------------------------------------------------------------------- +static std::shared_ptr MakeStreamTracer( + std::shared_ptr logger, LightStepTracerOptions&& options) { + PropagationOptions propagation_options{}; + propagation_options.use_single_key = options.use_single_key_propagation; + auto recorder = MakeStreamRecorder(*logger, std::move(options)); + return std::shared_ptr{new LightStepTracerImpl{ + std::move(logger), propagation_options, std::move(recorder)}}; +} + //------------------------------------------------------------------------------ // MakeSingleThreadedTracer //------------------------------------------------------------------------------ @@ -189,6 +205,21 @@ std::shared_ptr MakeLightStepTracer( if (!options.use_thread) { return MakeSingleThreadedTracer(logger, std::move(options)); } + if (options.use_stream_recorder) { + if (!options.collector_plaintext) { + logger->Error("Encrypted streaming not supported yet"); + return nullptr; + } + if (!options.use_thread) { + logger->Error("Stream recorder only supports threaded mode"); + return nullptr; + } + if (options.transporter != nullptr) { + logger->Error("Stream recorder doesn't support custom transports"); + return nullptr; + } + return MakeStreamTracer(logger, std::move(options)); + } return MakeThreadedTracer(logger, std::move(options)); } catch (const std::exception& e) { logger->Error("Failed to construct LightStep Tracer: ", e.what()); diff --git a/test/BUILD b/test/BUILD new file mode 100644 index 00000000..5ee5acd5 --- /dev/null +++ b/test/BUILD @@ -0,0 +1,138 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "utility_lib", + private_hdrs = [ + "utility.h", + ], + srcs = [ + "utility.cpp", + ], + deps = [ + "//lightstep-tracer-common:collector_proto_cc", + "//src/common:utility_lib", + "//src/common:chunk_circular_buffer_lib", + "//src/network:socket_lib", + "//src/network:fragment_input_stream_lib", + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + ], +) + +lightstep_cc_library( + name = "zero_copy_connection_input_stream_lib", + private_hdrs = [ + "zero_copy_connection_input_stream.h", + ], + srcs = [ + "zero_copy_connection_input_stream.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:connection_stream_lib", + ], + external_deps = [ + "@com_google_protobuf//:protobuf", + ], +) + + +lightstep_cc_library( + name = "number_simulation_lib", + private_hdrs = [ + "number_simulation.h", + ], + srcs = [ + "number_simulation.cpp", + ], + deps = [ + "//src/common:chunk_circular_buffer_lib", + "//src/common:bipart_memory_stream_lib", + "//src/common:utility_lib", + ":utility_lib", + ":zero_copy_connection_input_stream_lib", + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + "@com_google_protobuf//:protobuf", + ], +) + + +lightstep_cc_library( + name = "testing_condition_variable_wrapper_lib", + private_hdrs = [ + "testing_condition_variable_wrapper.h", + ], + srcs = [ + "testing_condition_variable_wrapper.cpp", + ], + deps = [ + "//src/common:condition_variable_wrapper_lib", + ], +) + +lightstep_cc_library( + name = "http_connection_lib", + private_hdrs = [ + "http_connection.h" + ], + srcs = [ + "http_connection.cpp" + ], + deps = [ + "//src/common:noncopyable_lib", + "//src/network:event_lib", + ], + external_deps = [ + "@com_github_libevent_libevent//:libevent", + "@com_google_protobuf//:protobuf", + ], +) + +lightstep_cc_library( + name = "counting_metrics_observer_lib", + private_hdrs = [ + "counting_metrics_observer.h", + ], + deps = [ + "//include/lightstep:metrics_observer_interface", + ], +) + +lightstep_cc_library( + name = "child_process_handle_lib", + private_hdrs = [ + "child_process_handle.h", + ], + srcs = [ + "child_process_handle.cpp", + ], +) + +lightstep_cc_library( + name = "ports_lib", + private_hdrs = [ + "ports.h", + ], +) + +lightstep_cc_library( + name = "mock_dns_resolution_callback_lib", + private_hdrs = [ + "mock_dns_resolution_callback.h", + ], + srcs = [ + "mock_dns_resolution_callback.cpp", + ], + deps = [ + "//src/network:dns_resolver_interface", + "//src/network:event_lib", + ], +) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index f27c99a6..00000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -macro(_lightstep_test TEST_NAME) - add_executable(${TEST_NAME} ${ARGN}) - target_link_libraries(${TEST_NAME} lightstep_tracer ${LIGHTSTEP_LINK_LIBRARIES}) - target_compile_options(${TEST_NAME} PUBLIC "-g") - add_test(${TEST_NAME} ${TEST_NAME} ${${TEST_NAME}_opts}) -endmacro() - -_lightstep_test(tracer_test tracer_test.cpp - utility.cpp) -_lightstep_test(utility_test utility_test.cpp) -_lightstep_test(logger_test logger_test.cpp) -_lightstep_test(fork_id_test fork_id_test.cpp) -_lightstep_test(propagation_test propagation_test.cpp) -_lightstep_test(auto_recorder_test auto_recorder_test.cpp - utility.cpp - in_memory_sync_transporter.cpp - testing_condition_variable_wrapper.cpp) -_lightstep_test(manual_recorder_test manual_recorder_test.cpp - utility.cpp - in_memory_async_transporter.cpp - testing_condition_variable_wrapper.cpp) -if (WITH_DYNAMIC_LOAD AND BUILD_SHARED_LIBS) - set(dynamic_load_test_opts --lightstep_library - ${CMAKE_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}lightstep_tracer${CMAKE_SHARED_LIBRARY_SUFFIX}) - _lightstep_test(dynamic_load_test dynamic_load_test.cpp - in_memory_collector.cpp) -endif() diff --git a/test/child_process_handle.cpp b/test/child_process_handle.cpp new file mode 100644 index 00000000..8057bf3f --- /dev/null +++ b/test/child_process_handle.cpp @@ -0,0 +1,96 @@ +#include "test/child_process_handle.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// StartChild +//-------------------------------------------------------------------------------------------------- +static int StartChild(const char* command, + const std::vector& arguments) { + std::vector argv; + argv.reserve(arguments.size() + 2); + argv.push_back(command); + std::transform(arguments.begin(), arguments.end(), std::back_inserter(argv), + [](const std::string& s) { return s.data(); }); + argv.push_back(nullptr); + auto rcode = ::fork(); + if (rcode == -1) { + std::cerr << "fork failed: " << std::strerror(errno) << "\n"; + std::terminate(); + } + if (rcode != 0) { + return rcode; + } + ::execv(command, const_cast(argv.data())); + + // Only run this code if execv failed + std::cerr << "execv failed: " << std::strerror(errno) << "\n"; + std::terminate(); +} + +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +ChildProcessHandle::ChildProcessHandle( + const char* command, const std::vector& arguments) { + pid_ = StartChild(command, arguments); +} + +ChildProcessHandle::ChildProcessHandle(ChildProcessHandle&& other) noexcept { + pid_ = other.pid_; + other.pid_ = -1; +} + +//-------------------------------------------------------------------------------------------------- +// destructor +//-------------------------------------------------------------------------------------------------- +ChildProcessHandle::~ChildProcessHandle() noexcept { KillChild(); } + +//-------------------------------------------------------------------------------------------------- +// operator= +//-------------------------------------------------------------------------------------------------- +ChildProcessHandle& ChildProcessHandle::operator=( + ChildProcessHandle&& other) noexcept { + KillChild(); + pid_ = other.pid_; + other.pid_ = -1; + return *this; +} + +//-------------------------------------------------------------------------------------------------- +// KillChild +//-------------------------------------------------------------------------------------------------- +void ChildProcessHandle::KillChild() noexcept { + if (pid_ == -1) { + return; + } + // Note: We do a hard kill here because TSAN does signal handling interception + // that prevents the child process from receiving an ordinary SIGTERM. + // + // See https://github.com/google/sanitizers/issues/838 + auto rcode = kill(pid_, SIGKILL); + if (rcode != 0) { + std::cerr << "failed to kill child process: " << std::strerror(errno) + << "\n"; + std::terminate(); + } + + int status; + rcode = ::waitpid(pid_, &status, 0); + if (rcode != pid_) { + std::cerr << "failed to kill child: waitpid returned " << rcode << "\n"; + std::terminate(); + } + pid_ = -1; +} +} // namespace lightstep diff --git a/test/child_process_handle.h b/test/child_process_handle.h new file mode 100644 index 00000000..24ae453e --- /dev/null +++ b/test/child_process_handle.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace lightstep { +/** + * Manages a child process. + */ +class ChildProcessHandle { + public: + ChildProcessHandle() noexcept = default; + + ChildProcessHandle(const char* command, + const std::vector& arguments); + + ChildProcessHandle(const ChildProcessHandle&) = delete; + ChildProcessHandle(ChildProcessHandle&& other) noexcept; + + ~ChildProcessHandle() noexcept; + + ChildProcessHandle& operator=(const ChildProcessHandle&) = delete; + ChildProcessHandle& operator=(ChildProcessHandle&& other) noexcept; + + private: + int pid_{-1}; + + void KillChild() noexcept; +}; +} // namespace lightstep diff --git a/test/cmake/CMakeLists.txt b/test/cmake/CMakeLists.txt new file mode 100644 index 00000000..4a757277 --- /dev/null +++ b/test/cmake/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(load_tracer_test load_tracer_test.cpp) +target_link_libraries(load_tracer_test ${OPENTRACING_LIBRARY}) +if (WITH_DYNAMIC_LOAD AND BUILD_SHARED_LIBS) + add_test(load_tracer_test load_tracer_test + ${CMAKE_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}lightstep_tracer${CMAKE_SHARED_LIBRARY_SUFFIX}) +endif() diff --git a/test/cmake/load_tracer_test.cpp b/test/cmake/load_tracer_test.cpp new file mode 100644 index 00000000..2e8a7d45 --- /dev/null +++ b/test/cmake/load_tracer_test.cpp @@ -0,0 +1,21 @@ +#include + +#include + +int main(int argc, char* argv[]) { + if (argc != 2) { + std::cerr << "Usage: \n"; + return -1; + } + + // Verify we can load the tracer library. + std::string error_message; + auto handle_maybe = + opentracing::DynamicallyLoadTracingLibrary(argv[1], error_message); + if (!handle_maybe) { + std::cerr << "Failed to load tracer library " << error_message << "\n"; + return -1; + } + + return 0; +} diff --git a/test/common/BUILD b/test/common/BUILD new file mode 100644 index 00000000..d3764ecb --- /dev/null +++ b/test/common/BUILD @@ -0,0 +1,123 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_catch_test", + "lightstep_cc_test", + "lightstep_package", +) + +lightstep_package() + +lightstep_catch_test( + name = "atomic_bit_set_test", + srcs = [ + "atomic_bit_set_test.cpp", + ], + deps = [ + "//src/common:atomic_bit_set_lib", + ], +) + +lightstep_catch_test( + name = "bipart_memory_stream_test", + srcs = [ + "bipart_memory_stream_test.cpp", + ], + deps = [ + "//src/common:bipart_memory_stream_lib", + ], +) + +lightstep_catch_test( + name = "circular_buffer_test", + srcs = [ + "circular_buffer_test.cpp", + ], + deps = [ + "//src/common:circular_buffer_lib", + ], +) + +lightstep_catch_test( + name = "chunk_circular_buffer_test", + srcs = [ + "chunk_circular_buffer_test.cpp", + ], + deps = [ + "//src/common:chunk_circular_buffer_lib", + "//src/common:utility_lib", + "//test:utility_lib", + "//test:number_simulation_lib", + ], +) + +lightstep_catch_test( + name = "function_ref_test", + srcs = [ + "function_ref_test.cpp", + ], + deps = [ + "//src/common:function_ref_lib", + ], +) + +lightstep_catch_test( + name = "utility_test", + srcs = [ + "utility_test.cpp", + ], + deps = [ + "//src/common:utility_lib", + "//src/common:bipart_memory_stream_lib", + ], +) + +lightstep_catch_test( + name = "logger_test", + srcs = [ + "logger_test.cpp", + ], + deps = [ + "//src/common:logger_lib", + ], +) + +lightstep_catch_test( + name = "random_test", + srcs = [ + "random_test.cpp", + ], + deps = [ + "//src/common:random_lib", + ], +) + +lightstep_cc_test( + name = "fork_id_test", + srcs = [ + "fork_id_test.cpp", + ], + deps = [ + "//src/common:random_lib", + ], +) + +lightstep_catch_test( + name = "protobuf_test", + srcs = [ + "protobuf_test.cpp", + ], + deps = [ + "//src/common:protobuf_lib", + "//lightstep-tracer-common:collector_proto_cc", + ], +) + +lightstep_catch_test( + name = "random_traverser_test", + srcs = [ + "random_traverser_test.cpp", + ], + deps = [ + "//src/common:random_traverser_lib", + ], +) diff --git a/test/common/atomic_bit_set_test.cpp b/test/common/atomic_bit_set_test.cpp new file mode 100644 index 00000000..63b096da --- /dev/null +++ b/test/common/atomic_bit_set_test.cpp @@ -0,0 +1,47 @@ +#include "common/atomic_bit_set.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("AtomicBitSet") { + size_t size = 100; + AtomicBitSet bit_set{size}; + + SECTION("A bit set is initialized with everything set to false.") { + for (int bit_index = 0; bit_index < static_cast(size); ++bit_index) { + REQUIRE(!bit_set.Test(bit_index)); + } + } + + SECTION("After calling set a bit tests as true.") { + bit_set.Set(63); + for (int bit_index = 0; bit_index < static_cast(size); ++bit_index) { + if (bit_index == 63) { + REQUIRE(bit_set.Test(bit_index)); + } else { + REQUIRE(!bit_set.Test(bit_index)); + } + } + } + + SECTION("Setting a bit in the second block works as expected") { + CHECK(!bit_set.Set(64)); + CHECK(bit_set.Set(64)); + for (int bit_index = 0; bit_index < static_cast(size); ++bit_index) { + if (bit_index == 64) { + REQUIRE(bit_set.Test(bit_index)); + } else { + REQUIRE(!bit_set.Test(bit_index)); + } + } + } + + SECTION("Set, then Reset returns everything to zero") { + bit_set.Set(63); + CHECK(bit_set.Reset(63)); + CHECK(!bit_set.Reset(63)); + for (int bit_index = 0; bit_index < static_cast(size); ++bit_index) { + REQUIRE(!bit_set.Test(bit_index)); + } + } +} diff --git a/test/common/bipart_memory_stream_test.cpp b/test/common/bipart_memory_stream_test.cpp new file mode 100644 index 00000000..ec7f2ad7 --- /dev/null +++ b/test/common/bipart_memory_stream_test.cpp @@ -0,0 +1,154 @@ +#include "common/bipart_memory_stream.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("BipartMemoryOutputStream") { + SECTION("Next returns false when there's no data attached.") { + BipartMemoryOutputStream buffer{nullptr, 0, nullptr, 0}; + void* data; + int size; + CHECK(!buffer.Next(&data, &size)); + } + + SECTION("Next returns the first memory buffer provided") { + char data1[10]; + BipartMemoryOutputStream buffer{data1, 10, nullptr, 0}; + void* data; + int size; + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data1); + CHECK(size == 10); + CHECK(buffer.ByteCount() == 10); + CHECK(!buffer.Next(&data, &size)); + } + + SECTION("Next returns the first, then second memory buffer if provided") { + char data1[10]; + char data2[5]; + BipartMemoryOutputStream buffer{data1, 10, data2, 5}; + void* data; + int size; + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data1); + CHECK(size == 10); + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data2); + CHECK(size == 5); + CHECK(buffer.ByteCount() == 15); + CHECK(!buffer.Next(&data, &size)); + } + + SECTION("BackUp to the first buffer returns thre correct partial buffer") { + char data1[10]; + char data2[5]; + BipartMemoryOutputStream buffer{data1, 10, data2, 5}; + void* data; + int size; + CHECK(buffer.Next(&data, &size)); + buffer.BackUp(5); + CHECK(buffer.ByteCount() == 5); + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data1 + 5); + CHECK(size == 5); + } + + SECTION("BackUp to the second buffer returns the correct partial buffer") { + char data1[10]; + char data2[5]; + BipartMemoryOutputStream buffer{data1, 10, data2, 5}; + void* data; + int size; + CHECK(buffer.Next(&data, &size)); + CHECK(buffer.Next(&data, &size)); + buffer.BackUp(3); + CHECK(buffer.ByteCount() == 12); + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data2 + 2); + CHECK(size == 3); + } +} + +TEST_CASE("BipartMemoryInputStream") { + SECTION("Next returns false when there's no data attached.") { + BipartMemoryInputStream buffer{nullptr, 0, nullptr, 0}; + const void* data; + int size; + CHECK(!buffer.Next(&data, &size)); + } + + SECTION("Next returns the first memory buffer provided") { + char data1[10]; + BipartMemoryInputStream buffer{data1, 10, nullptr, 0}; + const void* data; + int size; + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data1); + CHECK(size == 10); + CHECK(buffer.ByteCount() == 10); + CHECK(!buffer.Next(&data, &size)); + } + + SECTION("Next returns the first, then second memory buffer if provided") { + char data1[10]; + char data2[5]; + BipartMemoryInputStream buffer{data1, 10, data2, 5}; + const void* data; + int size; + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data1); + CHECK(size == 10); + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data2); + CHECK(size == 5); + CHECK(buffer.ByteCount() == 15); + CHECK(!buffer.Next(&data, &size)); + } + + SECTION("BackUp to the first buffer returns thre correct partial buffer") { + char data1[10]; + char data2[5]; + BipartMemoryInputStream buffer{data1, 10, data2, 5}; + const void* data; + int size; + CHECK(buffer.Next(&data, &size)); + buffer.BackUp(5); + CHECK(buffer.ByteCount() == 5); + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data1 + 5); + CHECK(size == 5); + } + + SECTION("BackUp to the second buffer returns the correct partial buffer") { + char data1[10]; + char data2[5]; + BipartMemoryInputStream buffer{data1, 10, data2, 5}; + const void* data; + int size; + CHECK(buffer.Next(&data, &size)); + CHECK(buffer.Next(&data, &size)); + buffer.BackUp(3); + CHECK(buffer.ByteCount() == 12); + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data2 + 2); + CHECK(size == 3); + } + + SECTION("Skip acts as if bytes are read") { + char data1[10]; + BipartMemoryInputStream buffer{data1, 10, nullptr, 0}; + CHECK(buffer.Skip(2)); + const void* data; + int size; + CHECK(buffer.ByteCount() == 2); + CHECK(buffer.Next(&data, &size)); + CHECK(static_cast(data) == data1 + 2); + CHECK(size == 8); + } + + SECTION("Skipping more bytes than are in the buffer returns false") { + char data1[10]; + BipartMemoryInputStream buffer{data1, 10, nullptr, 0}; + CHECK(!buffer.Skip(11)); + } +} diff --git a/test/common/chunk_circular_buffer_test.cpp b/test/common/chunk_circular_buffer_test.cpp new file mode 100644 index 00000000..6d04aea6 --- /dev/null +++ b/test/common/chunk_circular_buffer_test.cpp @@ -0,0 +1,105 @@ +#include "common/chunk_circular_buffer.h" +#include "common/utility.h" +#include "test/utility.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "3rd_party/catch2/catch.hpp" +#include "test/number_simulation.h" +using namespace lightstep; + +TEST_CASE( + "Verify through simulation that ChunkCircularBuffer behaves correctly.") { + const size_t num_producer_threads = 4; + const size_t n = 25000; + for (size_t max_size : {10, 50, 100, 1000}) { + ChunkCircularBuffer buffer{max_size}; + std::vector producer_numbers; + std::vector consumer_numbers; + auto producer = + std::thread{RunBinaryNumberProducer, std::ref(buffer), + std::ref(producer_numbers), num_producer_threads, n}; + std::atomic exit{false}; + auto consumer = std::thread{RunBinaryNumberConsumer, std::ref(buffer), + std::ref(exit), std::ref(consumer_numbers)}; + producer.join(); + exit = true; + consumer.join(); + std::sort(producer_numbers.begin(), producer_numbers.end()); + std::sort(consumer_numbers.begin(), consumer_numbers.end()); + REQUIRE(producer_numbers == consumer_numbers); + } +} + +TEST_CASE( + "ChunkCircularBuffer stores serialized messages surrounded by http/1.1 " + "chunk framing.") { + ChunkCircularBuffer buffer{13}; + REQUIRE(buffer.num_bytes_allotted() == 0); + REQUIRE(buffer.allotment().size1 == 0); + + SECTION("Allot does nothing if the buffer is empty.") { + REQUIRE(buffer.Allot() == 0); + REQUIRE(buffer.num_bytes_allotted() == 0); + REQUIRE(buffer.allotment().size1 == 0); + } + + SECTION( + "Messages added to the buffer are surrounded by http/1.1 chunk " + "framing.") { + REQUIRE(AddString(buffer, "abc")); + REQUIRE(buffer.Allot() == 1); + REQUIRE(ToString(buffer.allotment()) == "3\r\nabc\r\n"); + } + + SECTION("Adding a message fails if it would exceed the size of the buffer.") { + REQUIRE(AddString(buffer, "abc")); + REQUIRE(!AddString(buffer, "abc")); + } + + SECTION("Successive messages get serialized one after the other together.") { + REQUIRE(AddString(buffer, "ab")); + REQUIRE(AddString(buffer, "c")); + REQUIRE(buffer.Allot() == 2); + REQUIRE(buffer.num_bytes_allotted() == 13); + REQUIRE(ToString(buffer.allotment()) == "2\r\nab\r\n1\r\nc\r\n"); + } + + SECTION("Allot picks up where the last allotment left.") { + REQUIRE(AddString(buffer, "ab")); + REQUIRE(buffer.Allot() == 1); + REQUIRE(AddString(buffer, "c")); + REQUIRE(buffer.Allot() == 1); + REQUIRE(buffer.num_bytes_allotted() == 13); + REQUIRE(ToString(buffer.allotment()) == "2\r\nab\r\n1\r\nc\r\n"); + } + + SECTION("Once space has been allotted, it can be consumed.") { + REQUIRE(AddString(buffer, "ab")); + REQUIRE(AddString(buffer, "c")); + REQUIRE(buffer.Allot() == 2); + buffer.Consume(7); + REQUIRE(buffer.num_bytes_allotted() == 6); + REQUIRE(ToString(buffer.allotment()) == "1\r\nc\r\n"); + } + + SECTION("FindChunk locates the chunk that contains a given pointer.") { + REQUIRE(AddString(buffer, "ab")); + REQUIRE(AddString(buffer, "c")); + REQUIRE(buffer.Allot() == 2); + CircularBufferConstPlacement chunk; + int num_preceding_chunks; + std::tie(chunk, num_preceding_chunks) = + buffer.FindChunk(buffer.buffer().data(), buffer.buffer().data() + 9); + REQUIRE(ToString(chunk) == "1\r\nc\r\n"); + REQUIRE(num_preceding_chunks == 1); + } +} diff --git a/test/common/circular_buffer_test.cpp b/test/common/circular_buffer_test.cpp new file mode 100644 index 00000000..6824de7f --- /dev/null +++ b/test/common/circular_buffer_test.cpp @@ -0,0 +1,86 @@ +#include "common/circular_buffer.h" + +#include +#include + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("CircularBuffer") { + CircularBuffer circular_buffer{5}; + REQUIRE(circular_buffer.max_size() == 5); + REQUIRE(circular_buffer.empty()); + + SECTION( + "We can make a single reservation that doesn't exceed the capacity.") { + auto reservation = circular_buffer.Reserve(2); + REQUIRE(reservation.data1 != nullptr); + REQUIRE(reservation.size1 == 2); + REQUIRE(reservation.size2 == 0); + + auto placement = circular_buffer.Peek(0, 2); + REQUIRE(placement.data1 == reservation.data1); + } + + SECTION( + "Sequential reservations that don't exceed the capacity are placed " + "sequentially.") { + auto reservation1 = circular_buffer.Reserve(1); + REQUIRE(reservation1.data1 != nullptr); + REQUIRE(reservation1.size1 == 1); + reservation1.data1[0] = 'a'; + + auto reservation2 = circular_buffer.Reserve(2); + REQUIRE(reservation2.data1 != nullptr); + REQUIRE(reservation2.size1 == 2); + + reservation2.data1[0] = 'b'; + reservation2.data1[1] = 'c'; + + REQUIRE(circular_buffer.size() == 3); + + auto placement = circular_buffer.Peek(0, 3); + REQUIRE(placement.size1 == 3); + CHECK(placement.data1[0] == 'a'); + CHECK(placement.data1[1] == 'b'); + CHECK(placement.data1[2] == 'c'); + } + + SECTION( + "Reserve fails if the amount of space requested is more than what's " + "available in the circular buffer.") { + REQUIRE(circular_buffer.Reserve(6).data1 == nullptr); + + REQUIRE(circular_buffer.Reserve(2).data2 != nullptr); + REQUIRE(circular_buffer.size() == 2); + + REQUIRE(circular_buffer.Reserve(3).data2 != nullptr); + REQUIRE(circular_buffer.size() == 5); + + REQUIRE(circular_buffer.Reserve(1).data2 == nullptr); + REQUIRE(circular_buffer.size() == 5); + } + + SECTION("Reservations can wrap around the end of a circular buffer.") { + REQUIRE(circular_buffer.Reserve(3).data2 != nullptr); + REQUIRE(circular_buffer.size() == 3); + circular_buffer.Consume(3); + REQUIRE(circular_buffer.empty()); + + auto placement = circular_buffer.Reserve(4); + REQUIRE(placement.size1 == 3); + REQUIRE(placement.size2 == 1); + REQUIRE(placement.data1 == placement.data2 + 3); + } + + SECTION( + "ComputePosition determines how far a pointer is from the tail of the " + "circular buffer.") { + auto reservation1 = circular_buffer.Reserve(2); + REQUIRE(reservation1.data1 != nullptr); + REQUIRE(reservation1.size1 == 2); + reservation1.data1[0] = 'a'; + reservation1.data1[1] = 'a'; + REQUIRE(circular_buffer.ComputePosition(reservation1.data1 + 1) == 1); + } +} diff --git a/test/fork_id_test.cpp b/test/common/fork_id_test.cpp similarity index 94% rename from test/fork_id_test.cpp rename to test/common/fork_id_test.cpp index 517fd441..187ca079 100644 --- a/test/fork_id_test.cpp +++ b/test/common/fork_id_test.cpp @@ -1,14 +1,14 @@ // Verifies that IDs don't clash after forking the process. // // See https://github.com/opentracing-contrib/nginx-opentracing/issues/52 -#include "../src/utility.h" +#include "common/random.h" -#include -#include #include #include #include #include +#include +#include #include using namespace lightstep; diff --git a/test/common/function_ref_test.cpp b/test/common/function_ref_test.cpp new file mode 100644 index 00000000..03e793fe --- /dev/null +++ b/test/common/function_ref_test.cpp @@ -0,0 +1,29 @@ +#include "common/function_ref.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +int Call(FunctionRef f) { return f(); } + +int Return3() { return 3; } + +TEST_CASE("FunctionRef") { + int x = 9; + + auto f = [&] { return x; }; + + SECTION("FunctionRef can reference a lambda functor.") { + REQUIRE(Call(f) == 9); + } + + SECTION("FunctionRef can reference a function pointer.") { + REQUIRE(Call(Return3) == 3); + } + + SECTION("FunctionRef can be converted to bool") { + FunctionRef fref1{nullptr}; + FunctionRef fref2{f}; + REQUIRE(!static_cast(fref1)); + REQUIRE(static_cast(fref2)); + } +} diff --git a/test/logger_test.cpp b/test/common/logger_test.cpp similarity index 91% rename from test/logger_test.cpp rename to test/common/logger_test.cpp index 6eee3d78..bb496d38 100644 --- a/test/logger_test.cpp +++ b/test/common/logger_test.cpp @@ -1,8 +1,6 @@ -#include "../src/logger.h" - -#define CATCH_CONFIG_MAIN -#include +#include "common/logger.h" +#include "3rd_party/catch2/catch.hpp" using namespace lightstep; TEST_CASE("logger") { diff --git a/test/common/protobuf_test.cpp b/test/common/protobuf_test.cpp new file mode 100644 index 00000000..01a8d9b4 --- /dev/null +++ b/test/common/protobuf_test.cpp @@ -0,0 +1,41 @@ +#include "common/protobuf.h" + +#include + +#include "lightstep-tracer-common/collector.pb.h" + +#include +#include +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("Protobuf") { + collector::ReportRequest report; + report.mutable_auth()->set_access_token("abc"); + std::ostringstream oss; + { + google::protobuf::io::OstreamOutputStream zero_copy_stream{&oss}; + google::protobuf::io::CodedOutputStream coded_stream{&zero_copy_stream}; + WriteEmbeddedMessage(coded_stream, + collector::ReportRequest::kAuthFieldNumber, + report.auth()); + } + auto serialization = oss.str(); + + SECTION( + "WriteEmbeddedMessage writes messages that can be read out by " + "protobuf.") { + collector::ReportRequest deserialized_report; + REQUIRE(deserialized_report.ParseFromString(serialization)); + REQUIRE(google::protobuf::util::MessageDifferencer::Equals( + report, deserialized_report)); + } + + SECTION( + "ComputeEmbeddedMessageSerializationSize can be used to compute the " + "serialization size of an embedded message.") { + REQUIRE(serialization.size() == + ComputeEmbeddedMessageSerializationSize( + collector::ReportRequest::kAuthFieldNumber, report.auth())); + } +} diff --git a/test/common/random_test.cpp b/test/common/random_test.cpp new file mode 100644 index 00000000..3ffcbeff --- /dev/null +++ b/test/common/random_test.cpp @@ -0,0 +1,21 @@ +#include "common/random.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("Random") { + SECTION("GenerateId returns a random 64-bit number.") { + auto x = GenerateId(); + auto y = GenerateId(); + REQUIRE(x != y); + } + + SECTION( + "GeneateRandomDuration returns a random duration within a given range.") { + auto a = std::chrono::microseconds{5}; + auto b = std::chrono::microseconds{10}; + auto t = GenerateRandomDuration(a, b); + REQUIRE(a <= t); + REQUIRE(t <= b); + } +} diff --git a/test/common/random_traverser_test.cpp b/test/common/random_traverser_test.cpp new file mode 100644 index 00000000..46fe849e --- /dev/null +++ b/test/common/random_traverser_test.cpp @@ -0,0 +1,42 @@ +#include "common/random_traverser.h" + +#include +#include + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("RandomTraverser") { + std::vector v(5, 0); + RandomTraverser traverser{static_cast(v.size())}; + + SECTION( + "RandomTraverser allows iteration over sequential indexes in random " + "order.") { + auto completed = traverser.ForEachIndex([&v](int i) { + ++v[i]; + return true; + }); + REQUIRE(completed); + REQUIRE(std::all_of(v.begin(), v.end(), [](int x) { return x == 1; })); + } + + SECTION("Iteration can be exited early by returning false.") { + auto completed = traverser.ForEachIndex([&v](int i) { + ++v[i]; + return false; + }); + REQUIRE(!completed); + std::sort(v.begin(), v.end()); + REQUIRE(v == std::vector{0, 0, 0, 0, 1}); + } + + SECTION("The callback is never called if the sequence is empty.") { + traverser = RandomTraverser{0}; + auto completed = traverser.ForEachIndex([](int /*i*/) { + REQUIRE(false); + return true; + }); + REQUIRE(completed); + } +} diff --git a/test/utility_test.cpp b/test/common/utility_test.cpp similarity index 61% rename from test/utility_test.cpp rename to test/common/utility_test.cpp index 79c29da4..3770a0dc 100644 --- a/test/utility_test.cpp +++ b/test/common/utility_test.cpp @@ -1,9 +1,10 @@ -#include "../src/utility.h" +#include "common/utility.h" +#include "common/bipart_memory_stream.h" + #include #include -#define CATCH_CONFIG_MAIN -#include +#include "3rd_party/catch2/catch.hpp" using namespace lightstep; using namespace opentracing; @@ -111,3 +112,68 @@ TEST_CASE("hex-integer conversions") { CHECK(!HexToUint64("abcHef")); } } + +TEST_CASE( + "ReadChunkHeader reads the header from an http/1.1 streaming chunk.") { + SECTION("We can read the chunk header from contiguous memory.") { + BipartMemoryInputStream stream{"A\r\n", 3, nullptr, 0}; + size_t chunk_size; + REQUIRE(ReadChunkHeader(stream, chunk_size)); + REQUIRE(chunk_size == 10); + REQUIRE(stream.ByteCount() == 3); + } + + SECTION( + "We can read the chunk header from contiguous memory that spans multiple " + "digits.") { + BipartMemoryInputStream stream{"A1\r\n", 4, nullptr, 0}; + size_t chunk_size; + REQUIRE(ReadChunkHeader(stream, chunk_size)); + REQUIRE(chunk_size == 161); + REQUIRE(stream.ByteCount() == 4); + } + + SECTION("Only the chunk header is read from the stream.") { + BipartMemoryInputStream stream{"A1\r\nabc", 7, nullptr, 0}; + size_t chunk_size; + REQUIRE(ReadChunkHeader(stream, chunk_size)); + REQUIRE(chunk_size == 161); + REQUIRE(stream.ByteCount() == 4); + } + + SECTION( + "We can read a header that's split across separate parts of contiguous " + "memory.") { + BipartMemoryInputStream stream{"A", 1, "1\r\n", 3}; + size_t chunk_size; + REQUIRE(ReadChunkHeader(stream, chunk_size)); + REQUIRE(chunk_size == 161); + REQUIRE(stream.ByteCount() == 4); + } + + SECTION("ReadChunkHeader fails if there's no carriage return.") { + BipartMemoryInputStream stream{"A", 1, "123", 3}; + size_t chunk_size; + REQUIRE(!ReadChunkHeader(stream, chunk_size)); + } + + SECTION("ReadChunkHeader fails if there are invalid characters.") { + BipartMemoryInputStream stream{"AX\r\n", 4, nullptr, 0}; + size_t chunk_size; + REQUIRE(!ReadChunkHeader(stream, chunk_size)); + } + + SECTION( + "ReadChunkHeader fails if the number overflows an unsigned 64-bit " + "integer.") { + BipartMemoryInputStream stream{"0123456789ABCDEF0\r\n", 19, nullptr, 0}; + size_t chunk_size; + REQUIRE(!ReadChunkHeader(stream, chunk_size)); + } + + SECTION("ReadChunkHeader fails if two bytes don't follow the hex number.") { + BipartMemoryInputStream stream{"A0\r", 3, nullptr, 0}; + size_t chunk_size; + REQUIRE(!ReadChunkHeader(stream, chunk_size)); + } +} diff --git a/test/counting_metrics_observer.h b/test/counting_metrics_observer.h index 63939fac..f73d1acf 100644 --- a/test/counting_metrics_observer.h +++ b/test/counting_metrics_observer.h @@ -1,17 +1,21 @@ #pragma once -#include #include +#include "lightstep/metrics_observer.h" + namespace lightstep { struct CountingMetricsObserver : MetricsObserver { - void OnSpansSent(int num_spans) override { num_spans_sent += num_spans; } + // MetricsObserver + void OnSpansSent(int num_spans) noexcept override { + num_spans_sent += num_spans; + } - void OnSpansDropped(int num_spans) override { + void OnSpansDropped(int num_spans) noexcept override { num_spans_dropped += num_spans; } - void OnFlush() override { ++num_flushes; } + void OnFlush() noexcept override { ++num_flushes; } std::atomic num_flushes{0}; std::atomic num_spans_sent{0}; diff --git a/test/echo_server/BUILD b/test/echo_server/BUILD new file mode 100644 index 00000000..01a76ab2 --- /dev/null +++ b/test/echo_server/BUILD @@ -0,0 +1,35 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_package", + "lightstep_cc_library", +) + +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +lightstep_package() + +go_binary( + name = "echo_server", + srcs = [ + "main.go", + ], + out = "echo_server", +) + +lightstep_cc_library( + name = "echo_server_lib", + private_hdrs = [ + "echo_server_handle.h", + ], + srcs = [ + "echo_server_handle.cpp", + ], + deps = [ + "//test:child_process_handle_lib", + "//test:utility_lib", + "//test:http_connection_lib", + ], + data = [ + ":echo_server", + ], +) diff --git a/test/echo_server/echo_server_handle.cpp b/test/echo_server/echo_server_handle.cpp new file mode 100644 index 00000000..88465b0c --- /dev/null +++ b/test/echo_server/echo_server_handle.cpp @@ -0,0 +1,27 @@ +#include "test/echo_server/echo_server_handle.h" + +#include + +#include "test/utility.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +EchoServerHandle::EchoServerHandle(uint16_t http_port, uint16_t tcp_port) + : handle_{"test/echo_server/echo_server", + {std::to_string(http_port), std::to_string(tcp_port)}}, + http_connection_{"127.0.0.1", http_port} { + if (!IsEventuallyTrue([http_port, tcp_port] { + return CanConnect(http_port) && CanConnect(tcp_port); + })) { + std::cerr << "Failed to connect to echo server\n"; + std::terminate(); + } +} + +//-------------------------------------------------------------------------------------------------- +// data +//-------------------------------------------------------------------------------------------------- +std::string EchoServerHandle::data() { return http_connection_.Get("/data"); } +} // namespace lightstep diff --git a/test/echo_server/echo_server_handle.h b/test/echo_server/echo_server_handle.h new file mode 100644 index 00000000..5cbfe513 --- /dev/null +++ b/test/echo_server/echo_server_handle.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "test/child_process_handle.h" +#include "test/http_connection.h" + +namespace lightstep { +/** + * Manages a server that stores and returns any data sent. + */ +class EchoServerHandle { + public: + EchoServerHandle(uint16_t http_port, uint16_t tcp_port); + + /** + * @return the data sent to the server. + */ + std::string data(); + + private: + ChildProcessHandle handle_; + HttpConnection http_connection_; +}; +} // namespace lightstep diff --git a/test/echo_server/main.go b/test/echo_server/main.go new file mode 100644 index 00000000..af09ddfc --- /dev/null +++ b/test/echo_server/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "net" + "net/http" + "os" + "strconv" + "sync" +) + +type server struct { + mutex sync.Mutex + buffer bytes.Buffer +} + +func (server *server) handleTcpConnection(connection net.Conn) { + server.mutex.Lock() + defer server.mutex.Unlock() + _, err := server.buffer.ReadFrom(connection) + if err != nil { + log.Fatalf("Read failed: %s\n", err.Error()) + } +} + +func (server *server) serveTcp(port int) { + listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) + if err != nil { + log.Fatalf("Listen failed: %s\n", err.Error()) + } + defer listener.Close() + for { + connection, err := listener.Accept() + if err != nil { + log.Fatalf("Accept failed: %s\n", err.Error()) + } + go server.handleTcpConnection(connection) + } +} + +func (server *server) serveHttp(port int) { + http.Handle("/data", server) + log.Fatal(http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), nil)) +} + +func (server *server) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { + server.mutex.Lock() + defer server.mutex.Unlock() + _, err := responseWriter.Write(server.buffer.Bytes()) + if err != nil { + log.Fatalf("Write failed: %s\n", err.Error()) + } +} + +func main() { + if len(os.Args) != 3 { + log.Fatal("Must provide port") + } + httpPort, err := strconv.Atoi(os.Args[1]) + if err != nil { + log.Fatalf("Failed to parse httpPort: %s\n", err.Error()) + } + tcpPort, err := strconv.Atoi(os.Args[2]) + if err != nil { + log.Fatalf("Failed to parse tcpPort: %s\n", err.Error()) + } + server := &server{} + go server.serveHttp(httpPort) + server.serveTcp(tcpPort) +} diff --git a/test/http_connection.cpp b/test/http_connection.cpp new file mode 100644 index 00000000..e0c4ca9b --- /dev/null +++ b/test/http_connection.cpp @@ -0,0 +1,134 @@ +#include "test/http_connection.h" + +#include +#include +#include +#include + +#include +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +HttpConnection::HttpConnection(const char* address, uint16_t port) { + connection_ = evhttp_connection_base_new(event_base_.libevent_handle(), + nullptr, address, port); + if (connection_ == nullptr) { + std::cerr << "evhttp_connection_base_new failed\n"; + std::terminate(); + } +} + +//-------------------------------------------------------------------------------------------------- +// destructor +//-------------------------------------------------------------------------------------------------- +HttpConnection::~HttpConnection() noexcept { + evhttp_connection_free(connection_); +} + +//-------------------------------------------------------------------------------------------------- +// Get +//-------------------------------------------------------------------------------------------------- +void HttpConnection::Get(const char* uri, google::protobuf::Message& response) { + auto response_content = Get(uri); + auto rcode = response.ParseFromString(response_content); + if (!rcode) { + std::cerr << "ParseFromString failed\n"; + std::terminate(); + } +} + +std::string HttpConnection::Get(const char* uri) { + MakeRequest(EVHTTP_REQ_POST, uri); + event_base_.Dispatch(); + if (error_) { + std::cerr << "Get failed\n"; + std::terminate(); + } + return result_; +} + +//-------------------------------------------------------------------------------------------------- +// Post +//-------------------------------------------------------------------------------------------------- +void HttpConnection::Post(const char* uri, + const google::protobuf::Message& request, + google::protobuf::Message& response) { + std::string content; + request.SerializeToString(&content); + Post(uri, content, response); +} + +void HttpConnection::Post(const char* uri, const std::string& content, + google::protobuf::Message& response) { + MakeRequest(EVHTTP_REQ_POST, uri, content); + response_message_ = &response; + event_base_.Dispatch(); + if (error_) { + std::cerr << "Post failed\n"; + std::terminate(); + } + auto rcode = response.ParseFromString(result_); + if (!rcode) { + std::cerr << "ParseFromString failed\n"; + std::terminate(); + } +} + +//-------------------------------------------------------------------------------------------------- +// MakeRequest +//-------------------------------------------------------------------------------------------------- +evhttp_request* HttpConnection::MakeRequest(evhttp_cmd_type command, + const char* uri, + const std::string& content) { + auto request = evhttp_request_new(HttpConnection::OnCompleteRequest, + static_cast(this)); + if (request == nullptr) { + std::cerr << "evhttp_request_new failed\n"; + std::terminate(); + } + auto headers = evhttp_request_get_output_headers(request); + auto rcode = evhttp_add_header(headers, "Host", "localhost"); + if (rcode != 0) { + std::cerr << "evhttp_add_header failed\n"; + std::terminate(); + } + auto output_buffer = evhttp_request_get_output_buffer(request); + rcode = evbuffer_add(output_buffer, static_cast(content.data()), + content.size()); + if (rcode != 0) { + std::cerr << "evbuffer_add failure\n"; + std::terminate(); + } + rcode = evhttp_make_request(connection_, request, command, uri); + if (rcode != 0) { + std::cerr << "evhttp_make_request failed\n"; + std::terminate(); + } + return request; +} + +//-------------------------------------------------------------------------------------------------- +// OnCompleteRequest +//-------------------------------------------------------------------------------------------------- +void HttpConnection::OnCompleteRequest(evhttp_request* request, + void* context) noexcept { + auto self = static_cast(context); + self->error_ = request == nullptr || request->response_code != 200; + self->result_.clear(); + auto buffer = evhttp_request_get_input_buffer(request); + if (buffer == nullptr) { + return; + } + self->result_.resize(evbuffer_get_length(buffer)); + auto rcode = evbuffer_copyout(buffer, static_cast(&self->result_[0]), + self->result_.size()); + if (rcode == -1) { + std::cerr << "evbuffer_copyout failed\n"; + std::terminate(); + } + self->event_base_.LoopBreak(); +} +} // namespace lightstep diff --git a/test/http_connection.h b/test/http_connection.h new file mode 100644 index 00000000..16ed30e5 --- /dev/null +++ b/test/http_connection.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include + +#include "common/noncopyable.h" +#include "network/event_base.h" + +#include +#include + +struct evhttp_connection; +struct evhttp_request; + +namespace lightstep { +/** + * Manages an http connection. + */ +class HttpConnection : private Noncopyable { + public: + HttpConnection(const char* address, uint16_t port); + + ~HttpConnection() noexcept; + + /** + * Sends an http get request. + * @param uri the uri to use for the request. + * @return the contents of the response. + */ + std::string Get(const char* uri); + + /** + * Sends an http request and parses the response into a protobuf message. + * @param uri the uri to use for the request. + * @param response the protobuf to parse the response into. + */ + void Get(const char* uri, google::protobuf::Message& response); + + /** + * Sends an http post request and parses the response into a protobuf message. + * @param uri the uri to use for the request. + * @param request a protobuf message to serialize into the request body. + * @param response the protobuf to parse the response into. + */ + void Post(const char* uri, const google::protobuf::Message& request, + google::protobuf::Message& response); + + /** + * Sends an http post request and parses the response into a protobuf message. + * @param uri the uri to use for the request. + * @param request a string to use in the request body. + * @param response the protobuf to parse the response into. + */ + void Post(const char* uri, const std::string& content, + google::protobuf::Message& response); + + private: + EventBase event_base_; + evhttp_connection* connection_; + std::string result_; + google::protobuf::Message* response_message_{nullptr}; + bool error_{false}; + + evhttp_request* MakeRequest(evhttp_cmd_type command, const char* uri, + const std::string& content = {}); + + static void OnCompleteRequest(evhttp_request* request, + void* context) noexcept; +}; +} // namespace lightstep diff --git a/test/mock_dns_resolution_callback.cpp b/test/mock_dns_resolution_callback.cpp new file mode 100644 index 00000000..c7595693 --- /dev/null +++ b/test/mock_dns_resolution_callback.cpp @@ -0,0 +1,14 @@ +#include "test/mock_dns_resolution_callback.h" + +namespace lightstep { +void MockDnsResolutionCallback::OnDnsResolution( + const DnsResolution& resolution, + opentracing::string_view error_message) noexcept { + resolution.ForeachIpAddress([&](const IpAddress& ip_address) { + ip_addresses_.push_back(ip_address); + return true; + }); + error_message_ = error_message; + event_base_.LoopBreak(); +} +} // namespace lightstep diff --git a/test/mock_dns_resolution_callback.h b/test/mock_dns_resolution_callback.h new file mode 100644 index 00000000..982c415f --- /dev/null +++ b/test/mock_dns_resolution_callback.h @@ -0,0 +1,37 @@ +#pragma once + +#include "network/dns_resolver.h" +#include "network/event_base.h" + +namespace lightstep { +/** + * A dns resolution callback that captures everything provided. + */ +class MockDnsResolutionCallback final : public DnsResolutionCallback { + public: + explicit MockDnsResolutionCallback(EventBase& event_base) + : event_base_{event_base} {} + + /** + * @return the ip addresses provided by the dns resolution. + */ + const std::vector ip_addresses() const noexcept { + return ip_addresses_; + } + + /** + * @return the error messages provied by the dns resolution. + */ + const std::string& error_message() const noexcept { return error_message_; } + + // DnsResolutionCallback + void OnDnsResolution( + const DnsResolution& resolution, + opentracing::string_view error_message) noexcept override; + + private: + EventBase& event_base_; + std::string error_message_; + std::vector ip_addresses_; +}; +} // namespace lightstep diff --git a/test/mock_dns_server/BUILD b/test/mock_dns_server/BUILD new file mode 100644 index 00000000..6e942909 --- /dev/null +++ b/test/mock_dns_server/BUILD @@ -0,0 +1,39 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_package", + "lightstep_cc_library", +) + +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +lightstep_package() + +go_binary( + name = "mock_dns_server", + srcs = [ + "main.go", + ], + out = "mock_dns_server", + deps = [ + "@com_github_miekg_dns//:go_default_library", + ], +) + +lightstep_cc_library( + name = "mock_dns_server_lib", + private_hdrs = [ + "mock_dns_server_handle.h", + ], + srcs = [ + "mock_dns_server_handle.cpp", + ], + deps = [ + "//test:child_process_handle_lib", + "//test:utility_lib", + "//test:mock_dns_resolution_callback_lib", + "//src/network/ares_dns_resolver:ares_dns_resolver_lib", + ], + data = [ + ":mock_dns_server", + ], +) diff --git a/test/mock_dns_server/main.go b/test/mock_dns_server/main.go new file mode 100644 index 00000000..f4142009 --- /dev/null +++ b/test/mock_dns_server/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "fmt" + "log" + "os" + "strconv" + "sync/atomic" + + "github.com/miekg/dns" +) + +var counter = uint64(0) + +func addIpv4Address(m *dns.Msg, name string, ip string) { + rr, err := dns.NewRR(fmt.Sprintf("%s A %s", name, ip)) + if err != nil { + log.Fatalf("addIpv4Address failed: %s\n ", err.Error()) + } + m.Answer = append(m.Answer, rr) +} + +func handleIpv4Query(m *dns.Msg, name string) { + switch name { + case "test.service.": + addIpv4Address(m, name, "192.168.0.2") + case "flip.service.": + count := atomic.LoadUint64(&counter) + atomic.AddUint64(&counter, 1) + if count%2 == 0 { + addIpv4Address(m, name, "192.168.0.2") + } else { + addIpv4Address(m, name, "192.168.0.3") + } + case "flaky.service.": + count := atomic.LoadUint64(&counter) + atomic.AddUint64(&counter, 1) + if count%2 == 0 { + return + } else { + addIpv4Address(m, name, "192.168.0.1") + } + case "satellites.service.": + addIpv4Address(m, name, "192.168.0.1") + addIpv4Address(m, name, "192.168.0.2") + } +} + +func parseQuery(m *dns.Msg) { + for _, q := range m.Question { + switch q.Qtype { + case dns.TypeA: + log.Printf("Query for %s\n", q.Name) + handleIpv4Query(m, q.Name) + case dns.TypeAAAA: + log.Printf("Query for %s\n", q.Name) + if q.Name != "ipv6.service." { + return + } + rr, err := dns.NewRR(fmt.Sprintf("%s AAAA %s", q.Name, "2001:0db8:85a3:0000:0000:8a2e:0370:7334")) + if err == nil { + m.Answer = append(m.Answer, rr) + } + } + } +} + +func handleDnsRequest(w dns.ResponseWriter, r *dns.Msg) { + m := new(dns.Msg) + m.SetReply(r) + m.Compress = false + + switch r.Opcode { + case dns.OpcodeQuery: + parseQuery(m) + } + + w.WriteMsg(m) +} + +func main() { + if len(os.Args) != 2 { + log.Fatal("Must provide port") + } + port, err := strconv.Atoi(os.Args[1]) + if err != nil { + log.Fatalf("Failed to parse port: %s\n", err.Error()) + } + + // attach request handler func + dns.HandleFunc("service.", handleDnsRequest) + + // start server + server := &dns.Server{Addr: ":" + strconv.Itoa(port), Net: "udp"} + log.Printf("Starting at %d\n", port) + err = server.ListenAndServe() + defer server.Shutdown() + if err != nil { + log.Fatalf("Failed to start server: %s\n ", err.Error()) + } +} diff --git a/test/mock_dns_server/mock_dns_server_handle.cpp b/test/mock_dns_server/mock_dns_server_handle.cpp new file mode 100644 index 00000000..b95a2036 --- /dev/null +++ b/test/mock_dns_server/mock_dns_server_handle.cpp @@ -0,0 +1,41 @@ +#include "test/mock_dns_server/mock_dns_server_handle.h" + +#include "common/logger.h" +#include "network/ares_dns_resolver/ares_dns_resolver.h" +#include "test/mock_dns_resolution_callback.h" +#include "test/utility.h" + +#include +#include +#include + +#include + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// IsMockServerUp +//-------------------------------------------------------------------------------------------------- +static bool IsMockServerUp(uint16_t port) { + EventBase event_base; + MockDnsResolutionCallback callback{event_base}; + DnsResolverOptions options; + Logger logger; + options.resolution_server_port = port; + options.resolution_servers = {IpAddress{"127.0.0.1"}.ipv4_address().sin_addr}; + AresDnsResolver resolver{logger, event_base, std::move(options)}; + resolver.Resolve("test.service", AF_INET, callback); + event_base.Dispatch(); + return callback.ip_addresses().size() == 1; +} + +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +MockDnsServerHandle::MockDnsServerHandle(uint16_t port) + : handle_{"test/mock_dns_server/mock_dns_server", {std::to_string(port)}} { + if (!IsEventuallyTrue([port] { return IsMockServerUp(port); })) { + std::cerr << "Failed to connect to mock dns server\n"; + std::terminate(); + } +} +} // namespace lightstep diff --git a/test/mock_dns_server/mock_dns_server_handle.h b/test/mock_dns_server/mock_dns_server_handle.h new file mode 100644 index 00000000..c1271287 --- /dev/null +++ b/test/mock_dns_server/mock_dns_server_handle.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "test/child_process_handle.h" + +namespace lightstep { +/** + * Manages a mock dns server started as a child process. + */ +class MockDnsServerHandle { + public: + MockDnsServerHandle() noexcept = default; + + explicit MockDnsServerHandle(uint16_t port); + + private: + ChildProcessHandle handle_; +}; +} // namespace lightstep diff --git a/test/mock_satellite/BUILD b/test/mock_satellite/BUILD new file mode 100644 index 00000000..9268069d --- /dev/null +++ b/test/mock_satellite/BUILD @@ -0,0 +1,95 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_package", + "lightstep_cc_library", + "lightstep_catch_test", +) + +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") + +lightstep_package() + +go_proto_library( + name = "collector_proto_go", + proto = "//lightstep-tracer-common:collector_proto", + importpath = "github.com/lightstep/lightstep-tracer-cpp/lightstep-tracer-common", + deps = [ + "@com_github_googleapis_googleapis//google/api:annotations_go_proto", + ], +) + +proto_library( + name = "mock_satellite_proto", + srcs = [ + "mock_satellite.proto", + ], + deps = [ + "//lightstep-tracer-common:collector_proto", + ], +) + +cc_proto_library( + name = "mock_satellite_proto_cc", + deps = [":mock_satellite_proto"], + visibility = ["//visibility:public"], +) + +go_proto_library( + name = "mock_satellite_proto_go", + proto = ":mock_satellite_proto", + importpath = "github.com/lightstep/lightstep-tracer-cpp/test/mock_satellite", + deps = [ + ":collector_proto_go", + ], +) + +go_binary( + name = "mock_satellite", + srcs = [ + "main.go", + "proto.go", + "satellitehandler.go", + "reportprocessor.go", + ], + out = "mock_satellite", + deps = [ + ":collector_proto_go", + ":mock_satellite_proto_go", + "@com_github_golang_protobuf//proto:go_default_library", + "@com_github_golang_protobuf//ptypes:go_default_library", + "@com_github_golang_protobuf//ptypes/timestamp:go_default_library", + ], +) + +lightstep_cc_library( + name = "mock_satellite_lib", + private_hdrs = [ + "mock_satellite_handle.h", + ], + srcs = [ + "mock_satellite_handle.cpp", + ], + deps = [ + "//src/common:protobuf_lib", + "//test:child_process_handle_lib", + "//test:utility_lib", + "//test:http_connection_lib", + ":mock_satellite_proto_cc", + ], + data = [ + ":mock_satellite", + ], +) + +lightstep_catch_test( + name = "mock_satellite_test", + srcs = [ + "mock_satellite_test.cpp", + ], + deps = [ + ":mock_satellite_lib", + "//test:http_connection_lib", + "//test:ports_lib", + ], +) diff --git a/test/mock_satellite/main.go b/test/mock_satellite/main.go new file mode 100644 index 00000000..957b0fe5 --- /dev/null +++ b/test/mock_satellite/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "github.com/golang/protobuf/proto" + "github.com/lightstep/lightstep-tracer-cpp/lightstep-tracer-common" + "log" + "net/http" + "os" + "strconv" +) + +const ( + maxBufferedReports = 5 +) + +func main() { + if len(os.Args) != 2 { + log.Fatal("Must provide port") + } + port, err := strconv.Atoi(os.Args[1]) + if err != nil { + log.Fatalf("Failed to parse port: %s\n", err.Error()) + } + reportChannel := make(chan *collectorpb.ReportRequest, maxBufferedReports) + satelliteHandler := NewSatelliteHandler(reportChannel) + reportProcessor := NewReportProcessor(reportChannel) + defer reportProcessor.Close() + defer close(reportChannel) + + http.Handle("/api/v2/reports", satelliteHandler) + http.Handle("/report-mock-streaming", satelliteHandler) + + http.HandleFunc("/spans", func(responseWriter http.ResponseWriter, request *http.Request) { + data, err := proto.Marshal(reportProcessor.Spans()) + if err != nil { + log.Fatalf("Failed to marshal Spans: %s\n", err.Error()) + } + responseWriter.Write(data) + }) + + http.HandleFunc("/reports", func(responseWriter http.ResponseWriter, request *http.Request) { + data, err := proto.Marshal(reportProcessor.Reports()) + if err != nil { + log.Fatalf("Failed to marshal Reports: %s\n", err.Error()) + } + responseWriter.Write(data) + }) + + log.Fatal(http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), nil)) +} diff --git a/test/mock_satellite/mock_satellite.proto b/test/mock_satellite/mock_satellite.proto new file mode 100644 index 00000000..9da12896 --- /dev/null +++ b/test/mock_satellite/mock_satellite.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package lightstep.mock_satellite; + +option go_package = "mocksatellitepb"; + +import "lightstep-tracer-common/collector.proto"; + +message Spans { + repeated lightstep.collector.Span spans = 1; +} + +message Reports { + repeated lightstep.collector.ReportRequest reports = 1; +} diff --git a/test/mock_satellite/mock_satellite_handle.cpp b/test/mock_satellite/mock_satellite_handle.cpp new file mode 100644 index 00000000..e526b81c --- /dev/null +++ b/test/mock_satellite/mock_satellite_handle.cpp @@ -0,0 +1,49 @@ +#include "test/mock_satellite/mock_satellite_handle.h" + +#include +#include +#include + +#include "test/utility.h" + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +MockSatelliteHandle::MockSatelliteHandle(uint16_t port) + : handle_{"test/mock_satellite/mock_satellite", {std::to_string(port)}}, + connection_{"127.0.0.1", port} { + if (!IsEventuallyTrue([port] { return CanConnect(port); })) { + std::cerr << "Failed to connect to mock satellite\n"; + std::terminate(); + } +} + +//-------------------------------------------------------------------------------------------------- +// spans +//-------------------------------------------------------------------------------------------------- +std::vector MockSatelliteHandle::spans() { + mock_satellite::Spans spans; + connection_.Get("/spans", spans); + std::vector result; + result.reserve(spans.spans().size()); + for (auto& span : spans.spans()) { + result.emplace_back(span); + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +// reports +//-------------------------------------------------------------------------------------------------- +std::vector MockSatelliteHandle::reports() { + mock_satellite::Reports reports; + connection_.Get("/reports", reports); + std::vector result; + result.reserve(reports.reports().size()); + for (auto& report : reports.reports()) { + result.emplace_back(report); + } + return result; +} +} // namespace lightstep diff --git a/test/mock_satellite/mock_satellite_handle.h b/test/mock_satellite/mock_satellite_handle.h new file mode 100644 index 00000000..418c21b8 --- /dev/null +++ b/test/mock_satellite/mock_satellite_handle.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "test/child_process_handle.h" +#include "test/http_connection.h" +#include "test/mock_satellite/mock_satellite.pb.h" + +namespace lightstep { +/** + * Manages a server that acts like a satellite. + */ +class MockSatelliteHandle { + public: + explicit MockSatelliteHandle(uint16_t port); + + /** + * @return the spans recorded by the satellite. + */ + std::vector spans(); + + /** + * @return the reports recorded by the satellite. + */ + std::vector reports(); + + private: + ChildProcessHandle handle_; + HttpConnection connection_; +}; +} // namespace lightstep diff --git a/test/mock_satellite/mock_satellite_test.cpp b/test/mock_satellite/mock_satellite_test.cpp new file mode 100644 index 00000000..b8232e44 --- /dev/null +++ b/test/mock_satellite/mock_satellite_test.cpp @@ -0,0 +1,74 @@ +#include +#include + +#include "3rd_party/catch2/catch.hpp" +#include "common/protobuf.h" +#include "test/http_connection.h" +#include "test/mock_satellite/mock_satellite_handle.h" +#include "test/ports.h" + +#include +#include +using namespace lightstep; + +static std::string WriteStreamingReport( + const collector::ReportRequest& report) { + std::ostringstream oss; + { + google::protobuf::io::OstreamOutputStream zero_copy_stream{&oss}; + google::protobuf::io::CodedOutputStream coded_stream{&zero_copy_stream}; + WriteEmbeddedMessage(coded_stream, + collector::ReportRequest::kReporterFieldNumber, + report.reporter()); + WriteEmbeddedMessage(coded_stream, + collector::ReportRequest::kAuthFieldNumber, + report.auth()); + WriteEmbeddedMessage(coded_stream, + collector::ReportRequest::kInternalMetricsFieldNumber, + report.internal_metrics()); + for (auto& span : report.spans()) { + WriteEmbeddedMessage(coded_stream, + collector::ReportRequest::kSpansFieldNumber, span); + } + } + return oss.str(); +} + +TEST_CASE("MockSatellite") { + MockSatelliteHandle mock_satellite{ + static_cast(PortAssignments::MockSatelliteTest)}; + HttpConnection connection{ + "127.0.0.1", static_cast(PortAssignments::MockSatelliteTest)}; + + SECTION("ReportRequests can be posted to a MockSatellite") { + collector::ReportRequest report; + collector::Span span1; + span1.mutable_span_context()->set_span_id(1); + *report.mutable_spans()->Add() = span1; + collector::ReportResponse response; + connection.Post("/api/v2/reports", report, response); + REQUIRE(mock_satellite.spans().size() == 1); + } + + SECTION("ReportRequests can be streamed to a MockSatellite") { + collector::ReportRequest report; + report.mutable_reporter()->set_reporter_id(123); + report.mutable_auth()->set_access_token("abc"); + report.mutable_internal_metrics()->set_duration_micros(321); + + collector::Span span1; + span1.mutable_span_context()->set_span_id(1); + *report.mutable_spans()->Add() = span1; + + collector::Span span2; + span1.mutable_span_context()->set_span_id(2); + *report.mutable_spans()->Add() = span2; + + auto s = WriteStreamingReport(report); + + collector::ReportResponse response; + connection.Post("/report-mock-streaming", s, response); + REQUIRE(mock_satellite.spans().size() == 2); + REQUIRE(mock_satellite.reports().size() == 3); + } +} diff --git a/test/mock_satellite/proto.go b/test/mock_satellite/proto.go new file mode 100644 index 00000000..1bb70a35 --- /dev/null +++ b/test/mock_satellite/proto.go @@ -0,0 +1,77 @@ +package main + +import ( + "bufio" + "encoding/binary" + "errors" + "fmt" + "github.com/golang/protobuf/proto" + "github.com/lightstep/lightstep-tracer-cpp/lightstep-tracer-common" + "io" +) + +func computeFieldTypeNumber(field uint64, wireType uint64) uint64 { + return (field << 3) | wireType +} + +func readEmbeddedMessage(reader *bufio.Reader, field uint64, message proto.Message) error { + fieldType, err := binary.ReadUvarint(reader) + if err != nil { + return err + } + if expectedFieldType := computeFieldTypeNumber(field, proto.WireBytes); fieldType != expectedFieldType { + return errors.New(fmt.Sprintf("Unexpected fieldType %d number for Reporter: expected %d", fieldType, + expectedFieldType)) + } + length, err := binary.ReadUvarint(reader) + if err != nil { + return err + } + buffer := make([]byte, length) + _, err = io.ReadFull(reader, buffer) + if err != nil { + return err + } + return proto.Unmarshal(buffer, message) +} + +func readReporter(reader *bufio.Reader) (*collectorpb.Reporter, error) { + reporter := &collectorpb.Reporter{} + return reporter, readEmbeddedMessage(reader, 1, reporter) +} + +func readAuth(reader *bufio.Reader) (*collectorpb.Auth, error) { + auth := &collectorpb.Auth{} + return auth, readEmbeddedMessage(reader, 2, auth) +} + +func readInternalMetrics(reader *bufio.Reader) (*collectorpb.InternalMetrics, error) { + internalMetrics := &collectorpb.InternalMetrics{} + return internalMetrics, readEmbeddedMessage(reader, 6, internalMetrics) +} + +func ReadStreamHeader(reader *bufio.Reader) (*collectorpb.ReportRequest, error) { + reporter, err := readReporter(reader) + if err != nil { + return nil, err + } + auth, err := readAuth(reader) + if err != nil { + return nil, err + } + internalMetrics, err := readInternalMetrics(reader) + if err != nil { + return nil, err + } + result := &collectorpb.ReportRequest{ + Reporter: reporter, + Auth: auth, + InternalMetrics: internalMetrics, + } + return result, nil +} + +func ReadSpan(reader *bufio.Reader) (*collectorpb.Span, error) { + span := &collectorpb.Span{} + return span, readEmbeddedMessage(reader, 3, span) +} diff --git a/test/mock_satellite/reportprocessor.go b/test/mock_satellite/reportprocessor.go new file mode 100644 index 00000000..bc2a0ab5 --- /dev/null +++ b/test/mock_satellite/reportprocessor.go @@ -0,0 +1,66 @@ +package main + +import ( + "github.com/lightstep/lightstep-tracer-cpp/lightstep-tracer-common" + "github.com/lightstep/lightstep-tracer-cpp/test/mock_satellite" + "sync" +) + +type ReportProcessor struct { + reportChannel chan *collectorpb.ReportRequest + done chan bool + mutex sync.Mutex + reports []*collectorpb.ReportRequest +} + +func NewReportProcessor(reportChannel chan *collectorpb.ReportRequest) *ReportProcessor { + processor := &ReportProcessor{ + reportChannel: reportChannel, + done: make(chan bool), + } + go processor.processReports() + return processor +} + +func (processor *ReportProcessor) processReports() { + for report := range processor.reportChannel { + processor.addReport(report) + } + processor.done <- true +} + +func (processor *ReportProcessor) addReport(report *collectorpb.ReportRequest) { + processor.mutex.Lock() + defer processor.mutex.Unlock() + processor.reports = append(processor.reports, report) +} + +func (processor *ReportProcessor) Reports() *mocksatellitepb.Reports { + result := &mocksatellitepb.Reports{} + processor.mutex.Lock() + defer processor.mutex.Unlock() + result.Reports = make([]*collectorpb.ReportRequest, len(processor.reports)) + copy(result.Reports, processor.reports) + return result +} + +func (processor *ReportProcessor) Spans() *mocksatellitepb.Spans { + result := &mocksatellitepb.Spans{} + processor.mutex.Lock() + defer processor.mutex.Unlock() + numSpans := 0 + for _, report := range processor.reports { + numSpans += len(report.Spans) + } + result.Spans = make([]*collectorpb.Span, numSpans) + spanCount := 0 + for _, report := range processor.reports { + copy(result.Spans[spanCount:], report.Spans) + spanCount += len(report.Spans) + } + return result +} + +func (processor *ReportProcessor) Close() { + <-processor.done +} diff --git a/test/mock_satellite/satellitehandler.go b/test/mock_satellite/satellitehandler.go new file mode 100644 index 00000000..922a8650 --- /dev/null +++ b/test/mock_satellite/satellitehandler.go @@ -0,0 +1,119 @@ +package main + +import ( + "bufio" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" + tspb "github.com/golang/protobuf/ptypes/timestamp" + "github.com/lightstep/lightstep-tracer-cpp/lightstep-tracer-common" + "io" + "log" + "net/http" + "time" +) + +func getCurrentTime() *tspb.Timestamp { + timestamp, err := ptypes.TimestampProto(time.Now()) + if err != nil { + log.Fatalf("TimestampProto failed: %s\n", err.Error()) + } + return timestamp +} + +type SatelliteHandler struct { + reportChannel chan *collectorpb.ReportRequest +} + +func NewSatelliteHandler(reportChannel chan *collectorpb.ReportRequest) *SatelliteHandler { + return &SatelliteHandler{ + reportChannel: reportChannel, + } +} + +func sendResponse(responseWriter http.ResponseWriter, response *collectorpb.ReportResponse) { + response.TransmitTimestamp = getCurrentTime() + serializedResponse, err := proto.Marshal(response) + if err != nil { + log.Fatalf("Failed to marshal ReportResponse: %s\n", err.Error()) + } + responseWriter.Write(serializedResponse) +} + +func isStreamedRequest(request *http.Request) bool { + if request.URL.String() == "/report-mock-streaming" { + return true + } + if len(request.TransferEncoding) == 0 { + return false + } + return request.TransferEncoding[0] == "chunked" +} + +func (handler *SatelliteHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { + if isStreamedRequest(request) { + handler.serveStreamingHTTP(responseWriter, request) + return + } + handler.serveFixedHTTP(responseWriter, request) +} + +func (handler *SatelliteHandler) serveFixedHTTP(responseWriter http.ResponseWriter, request *http.Request) { + response := &collectorpb.ReportResponse{ + ReceiveTimestamp: getCurrentTime(), + } + if request.ContentLength < 0 { + log.Fatalf("Request has unexpected content length %s\n", request.ContentLength) + } + report := &collectorpb.ReportRequest{} + buffer := make([]byte, request.ContentLength) + _, err := io.ReadFull(request.Body, buffer) + if err != nil { + log.Fatalf("Failed to read request body: %s\n", err.Error()) + } + err = proto.Unmarshal(buffer, report) + if err != nil { + log.Fatalf("Failed to unmarshal ReportRequest: %s\n", err.Error()) + } + sendResponse(responseWriter, response) + handler.reportChannel <- report +} + +func (handler *SatelliteHandler) serveStreamingHTTP(responseWriter http.ResponseWriter, request *http.Request) { + log.Println("serving streaming http") + response := &collectorpb.ReportResponse{ + ReceiveTimestamp: getCurrentTime(), + } + reader := bufio.NewReader(request.Body) + reportHeader, err := ReadStreamHeader(reader) + if err != nil { + log.Fatalf("ReadStreamHeader failed: %s\n", err.Error()) + } + handler.reportChannel <- reportHeader + for { + _, err := reader.ReadByte() + if err == io.EOF { + break + } + if err != nil { + log.Fatalf("ReadByte failed: %s\n", err.Error()) + break + } + err = reader.UnreadByte() + if err != nil { + log.Fatalf("UnreadByte failed: %s\n", err.Error()) + break + } + span, err := ReadSpan(reader) + if err != nil { + log.Fatalf("ReadSpan failed: %s\n", err.Error()) + } + report := &collectorpb.ReportRequest{ + Reporter: reportHeader.Reporter, + Auth: reportHeader.Auth, + Spans: make([]*collectorpb.Span, 1), + } + report.Spans[0] = span + handler.reportChannel <- report + } + sendResponse(responseWriter, response) +} diff --git a/test/network/BUILD b/test/network/BUILD new file mode 100644 index 00000000..b54ebb05 --- /dev/null +++ b/test/network/BUILD @@ -0,0 +1,107 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_cc_library", + "lightstep_catch_test", + "lightstep_package", +) + +lightstep_package() + +lightstep_catch_test( + name = "event_test", + srcs = [ + "event_test.cpp", + ], + deps = [ + "//src/network:event_lib", + ], +) + +lightstep_catch_test( + name = "event_base_test", + srcs = [ + "event_base_test.cpp", + ], + deps = [ + "//src/network:event_lib", + ], +) + +lightstep_catch_test( + name = "fragment_input_stream_test", + srcs = [ + "fragment_input_stream_test.cpp", + ], + deps = [ + "//src/network:fragment_array_input_stream_lib", + "//test:utility_lib", + ], +) + +lightstep_catch_test( + name = "fragment_array_input_stream_test", + srcs = [ + "fragment_array_input_stream_test.cpp", + ], + deps = [ + "//src/network:fragment_array_input_stream_lib", + "//test:utility_lib", + ], +) + + +lightstep_catch_test( + name = "vector_write_test", + srcs = [ + "vector_write_test.cpp", + ], + deps = [ + "//src/network:socket_lib", + "//src/network:vector_write_lib", + "//src/network:fragment_array_input_stream_lib", + "//test/echo_server:echo_server_lib", + "//test:ports_lib", + "//test:utility_lib", + ], +) + +lightstep_catch_test( + name = "timer_event_test", + srcs = [ + "timer_event_test.cpp", + ], + deps = [ + "//src/network:timer_event_lib", + ], +) + +lightstep_catch_test( + name = "ip_address_test", + srcs = [ + "ip_address_test.cpp", + ], + deps = [ + "//src/network:ip_address_lib", + ], +) + +lightstep_catch_test( + name = "socket_test", + srcs = [ + "socket_test.cpp", + ], + deps = [ + "//src/network:socket_lib", + ], +) + +lightstep_catch_test( + name = "no_dns_resolver_test", + srcs = [ + "no_dns_resolver_test.cpp", + ], + deps = [ + "//test:mock_dns_resolution_callback_lib", + "//src/network:no_dns_resolver_lib", + ], +) diff --git a/test/network/ares_dns_resolver/BUILD b/test/network/ares_dns_resolver/BUILD new file mode 100644 index 00000000..a1d19ca3 --- /dev/null +++ b/test/network/ares_dns_resolver/BUILD @@ -0,0 +1,20 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_catch_test", + "lightstep_package", +) + +lightstep_package() + +lightstep_catch_test( + name = "ares_dns_resolver_test", + srcs = [ + "ares_dns_resolver_test.cpp", + ], + deps = [ + "//src/network/ares_dns_resolver:ares_dns_resolver_lib", + "//test/mock_dns_server:mock_dns_server_lib", + "//test:ports_lib", + ], +) + diff --git a/test/network/ares_dns_resolver/ares_dns_resolver_test.cpp b/test/network/ares_dns_resolver/ares_dns_resolver_test.cpp new file mode 100644 index 00000000..acf853f8 --- /dev/null +++ b/test/network/ares_dns_resolver/ares_dns_resolver_test.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include "network/ares_dns_resolver/ares_dns_resolver.h" +#include "network/event_base.h" +#include "test/mock_dns_resolution_callback.h" +#include "test/mock_dns_server/mock_dns_server_handle.h" +#include "test/ports.h" + +#include "3rd_party/catch2/catch.hpp" + +using namespace lightstep; + +const auto ResolutionTimeout = std::chrono::milliseconds{100}; + +TEST_CASE("AresDnsResolver") { + Logger logger; + EventBase event_base; + DnsResolverOptions options; + options.timeout = ResolutionTimeout; + MockDnsServerHandle dns_server{ + static_cast(PortAssignments::AresDnsResolverTest)}; + options.resolution_server_port = + static_cast(PortAssignments::AresDnsResolverTest); + options.resolution_servers = {IpAddress{"127.0.0.1"}.ipv4_address().sin_addr}; + auto resolver = MakeDnsResolver(logger, event_base, options); + MockDnsResolutionCallback callback{event_base}; + + SECTION("We can resolve ipv4 addresses.") { + resolver->Resolve("test.service", AF_INET, callback); + event_base.Dispatch(); + REQUIRE(callback.ip_addresses() == + std::vector{IpAddress{"192.168.0.2"}}); + } + + SECTION("We can resolve ipv6 addresses.") { + resolver->Resolve("ipv6.service", AF_INET6, callback); + event_base.Dispatch(); + REQUIRE(callback.ip_addresses() == + std::vector{ + IpAddress{"2001:0db8:85a3:0000:0000:8a2e:0370:7334"}}); + } + + SECTION("If no answer is received, then the query times out.") { + resolver->Resolve("timeout.service", AF_INET, callback); + event_base.Dispatch(); + REQUIRE(callback.ip_addresses().empty()); + REQUIRE(!callback.error_message().empty()); + } + + SECTION("The dns resolver doesn't block the process from exiting.") { + event_base.OnTimeout( + ResolutionTimeout / 4, + [](int /*file_descriptor*/, short /*what*/, void* context) { + static_cast(context)->LoopBreak(); + }, + static_cast(&event_base)); + resolver->Resolve("timeout.service", AF_INET, callback); + auto t1 = std::chrono::system_clock::now(); + event_base.Dispatch(); + auto t2 = std::chrono::system_clock::now(); + REQUIRE((t2 - t1) < ResolutionTimeout / 2); + resolver.reset(); + } +} diff --git a/test/network/event_base_test.cpp b/test/network/event_base_test.cpp new file mode 100644 index 00000000..0a76184b --- /dev/null +++ b/test/network/event_base_test.cpp @@ -0,0 +1,41 @@ +#include + +#include "network/event_base.h" + +#include "3rd_party/catch2/catch.hpp" + +using namespace lightstep; + +TEST_CASE("EventBase") { + EventBase event_base; + + SECTION("EventBase can be move constructed.") { + auto libevent_handle = event_base.libevent_handle(); + REQUIRE(libevent_handle != nullptr); + EventBase event_base2{std::move(event_base)}; + REQUIRE(event_base2.libevent_handle() == libevent_handle); + } + + SECTION("EventBase can be move assigned.") { + auto libevent_handle = event_base.libevent_handle(); + REQUIRE(libevent_handle != nullptr); + EventBase event_base2; + event_base2 = std::move(event_base); + REQUIRE(event_base2.libevent_handle() == libevent_handle); + } + + SECTION("OnTimeout can be used to schedule a 1-off event.") { + bool was_called = false; + auto callback = [](int /*socket*/, short /*what*/, void* context) { + *static_cast(context) = true; + }; + auto exit_callback = [](int /*socket*/, short /*what*/, void* context) { + static_cast(context)->LoopBreak(); + }; + event_base.OnTimeout(std::chrono::milliseconds{5}, callback, + static_cast(&was_called)); + event_base.OnTimeout(std::chrono::milliseconds{10}, exit_callback, + static_cast(&event_base)); + event_base.Dispatch(); + } +} diff --git a/test/network/event_test.cpp b/test/network/event_test.cpp new file mode 100644 index 00000000..dc10029b --- /dev/null +++ b/test/network/event_test.cpp @@ -0,0 +1,35 @@ +#include "network/event.h" +#include "network/event_base.h" + +#include "3rd_party/catch2/catch.hpp" + +using namespace lightstep; + +TEST_CASE("Event") { + auto callback = [](int /*file_descriptor*/, short /*what*/, + void* /*context*/) {}; + + EventBase event_base; + Event event{event_base, -1, 0, callback, nullptr}; + REQUIRE(event.libevent_handle() != nullptr); + + SECTION("Event can be default constructed.") { + Event event; + REQUIRE(event.libevent_handle() == nullptr); + } + + SECTION("Event can be move constructed.") { + auto handle = event.libevent_handle(); + Event event2{std::move(event)}; + REQUIRE(event2.libevent_handle() == handle); + REQUIRE(event.libevent_handle() == nullptr); + } + + SECTION("Event can be move assigned.") { + Event event2{event_base, -1, 0, callback, nullptr}; + auto handle = event.libevent_handle(); + event2 = std::move(event); + REQUIRE(event2.libevent_handle() == handle); + REQUIRE(event.libevent_handle() == nullptr); + } +} diff --git a/test/network/fragment_array_input_stream_test.cpp b/test/network/fragment_array_input_stream_test.cpp new file mode 100644 index 00000000..ff3bf855 --- /dev/null +++ b/test/network/fragment_array_input_stream_test.cpp @@ -0,0 +1,68 @@ +#include "network/fragment_array_input_stream.h" + +#include +#include + +#include "3rd_party/catch2/catch.hpp" +#include "test/utility.h" +using namespace lightstep; + +TEST_CASE("FragmentArrayInputStream") { + FragmentArrayInputStream input_stream{MakeFragment("abc"), + MakeFragment("123")}; + REQUIRE(input_stream.num_fragments() == 2); + REQUIRE(ToString(input_stream) == "abc123"); + + SECTION("FragmentArrayInputStream default constructs to an empty stream.") { + FragmentArrayInputStream input_stream2; + REQUIRE(input_stream2.empty()); + } + + SECTION("We can add fragments to a FragmentArrayInputStream.") { + input_stream.Add(MakeFragment("456")); + REQUIRE(ToString(input_stream) == "abc123456"); + } + + SECTION( + "FragmentArrayInputStream can be reassigned to a new sequence of " + "fragments.") { + input_stream.Seek(0, 1); + input_stream = {MakeFragment("qrz")}; + REQUIRE(ToString(input_stream) == "qrz"); + } + + SECTION("We can seek past characters in the fragment stream.") { + input_stream.Seek(0, 1); + REQUIRE(input_stream.num_fragments() == 2); + REQUIRE(ToString(input_stream) == "bc123"); + } + + SECTION("We can seek past a fragment.") { + input_stream.Seek(1, 0); + REQUIRE(input_stream.num_fragments() == 1); + REQUIRE(ToString(input_stream) == "123"); + } + + SECTION("Seeks are done relative to the current position.") { + input_stream.Seek(0, 1); + input_stream.Seek(0, 1); + REQUIRE(input_stream.num_fragments() == 2); + REQUIRE(ToString(input_stream) == "c123"); + input_stream.Seek(1, 0); + input_stream.Seek(0, 1); + REQUIRE(input_stream.num_fragments() == 1); + REQUIRE(ToString(input_stream) == "23"); + } + + SECTION("When we Clear a stream, there's nothing in it.") { + input_stream.Clear(); + REQUIRE(input_stream.num_fragments() == 0); + int fragment_count = 0; + input_stream.ForEachFragment( + [&fragment_count](void* /*data*/, int /*size*/) { + ++fragment_count; + return true; + }); + REQUIRE(fragment_count == 0); + } +} diff --git a/test/network/fragment_input_stream_test.cpp b/test/network/fragment_input_stream_test.cpp new file mode 100644 index 00000000..fa888408 --- /dev/null +++ b/test/network/fragment_input_stream_test.cpp @@ -0,0 +1,104 @@ +#include + +#include "3rd_party/catch2/catch.hpp" +#include "network/fragment_array_input_stream.h" +#include "test/utility.h" +using namespace lightstep; + +TEST_CASE("FragmentInputStream") { + FragmentArrayInputStream fragment_input_stream1{MakeFragment("abc"), + MakeFragment("123")}; + FragmentArrayInputStream fragment_input_stream2{ + MakeFragment("alpha"), MakeFragment("beta"), MakeFragment("gamma")}; + std::initializer_list fragment_input_streams = { + &fragment_input_stream1, &fragment_input_stream2}; + + SECTION("Consuming nothing leaves the streams unmodified.") { + REQUIRE(!Consume(fragment_input_streams, 0)); + + REQUIRE((ToString(fragment_input_stream1) + + ToString(fragment_input_stream2)) == "abc123alphabetagamma"); + } + + SECTION("We can consume bytes from the first fragment.") { + REQUIRE(!Consume(fragment_input_streams, 1)); + + REQUIRE((ToString(fragment_input_stream1) + + ToString(fragment_input_stream2)) == "bc123alphabetagamma"); + } + + SECTION("We can consume the first fragment.") { + REQUIRE(!Consume(fragment_input_streams, 3)); + + REQUIRE((ToString(fragment_input_stream1) + + ToString(fragment_input_stream2)) == "123alphabetagamma"); + } + + SECTION("We can consume into the second fragment.") { + REQUIRE(!Consume(fragment_input_streams, 4)); + + REQUIRE((ToString(fragment_input_stream1) + + ToString(fragment_input_stream2)) == "23alphabetagamma"); + } + + SECTION("We can consume first fragment set.") { + REQUIRE(!Consume(fragment_input_streams, 6)); + + REQUIRE((ToString(fragment_input_stream1) + + ToString(fragment_input_stream2)) == "alphabetagamma"); + } + + SECTION("We can consume into the second fragment set.") { + REQUIRE(!Consume(fragment_input_streams, 11)); + + REQUIRE((ToString(fragment_input_stream1) + + ToString(fragment_input_stream2)) == "betagamma"); + + REQUIRE(!Consume(fragment_input_streams, 1)); + + REQUIRE((ToString(fragment_input_stream1) + + ToString(fragment_input_stream2)) == "etagamma"); + } + + SECTION("We can consume all the data.") { + REQUIRE(Consume(fragment_input_streams, 20)); + REQUIRE( + (ToString(fragment_input_stream1) + ToString(fragment_input_stream2)) + .empty()); + } + + SECTION("We can consume the data one byte at a time.") { + std::string s; + while (true) { + s += (ToString(fragment_input_stream1) + + ToString(fragment_input_stream2))[0]; + auto result = Consume(fragment_input_streams, 1); + if (result) { + break; + } + } + REQUIRE(s == "abc123alphabetagamma"); + } + + SECTION("We can consume data of random lengths.") { + std::mt19937 random_number_generator{std::random_device{}()}; + for (int i = 0; i < 100; ++i) { + fragment_input_stream1 = {MakeFragment("abc"), MakeFragment("123")}; + fragment_input_stream2 = {MakeFragment("alpha"), MakeFragment("beta"), + MakeFragment("gamma")}; + std::string s; + while (true) { + auto contents = + ToString(fragment_input_stream1) + ToString(fragment_input_stream2); + std::uniform_int_distribution distribution{0, contents.size()}; + auto n = distribution(random_number_generator); + s += contents.substr(0, n); + auto result = Consume(fragment_input_streams, static_cast(n)); + if (result) { + break; + } + } + REQUIRE(s == "abc123alphabetagamma"); + } + } +} diff --git a/test/network/ip_address_test.cpp b/test/network/ip_address_test.cpp new file mode 100644 index 00000000..5ea0d338 --- /dev/null +++ b/test/network/ip_address_test.cpp @@ -0,0 +1,37 @@ +#include "network/ip_address.h" + +#include "3rd_party/catch2/catch.hpp" + +using namespace lightstep; + +TEST_CASE("IpAddress") { + IpAddress addr1{"192.168.0.1"}, addr2{"192.168.1.1"}, + addr3{"FE80:CD00:0000:03DE:1257:0000:211E:729C"}, + addr4{"FE80:CD00:0000:0CDE:1257:0000:211E:729C"}; + + SECTION("Addresses can be converted to strings.") { + REQUIRE(ToString(addr1) == "192.168.0.1"); + REQUIRE(ToString(addr3) == "fe80:cd00:0:3de:1257:0:211e:729c"); + } + + SECTION("Addresses can be compared.") { + REQUIRE(addr1 == addr1); + REQUIRE(addr1 != addr2); + REQUIRE(addr1 != addr3); + REQUIRE(addr3 == addr3); + REQUIRE(addr3 != addr4); + } + + SECTION("Addresses can be constructed from sockaddr.") { + IpAddress addr5{addr1.addr()}, addr6{addr3.addr()}; + REQUIRE(addr5 == addr1); + REQUIRE(addr6 == addr3); + } + + SECTION("The port can be set.") { + addr1.set_port(8080); + REQUIRE(addr1.ipv4_address().sin_port == htons(8080)); + addr3.set_port(8080); + REQUIRE(addr3.ipv6_address().sin6_port == htons(8080)); + } +} diff --git a/test/network/no_dns_resolver_test.cpp b/test/network/no_dns_resolver_test.cpp new file mode 100644 index 00000000..60e85192 --- /dev/null +++ b/test/network/no_dns_resolver_test.cpp @@ -0,0 +1,34 @@ +#include "network/dns_resolver.h" + +#include "common/logger.h" +#include "network/event_base.h" +#include "test/mock_dns_resolution_callback.h" + +#include "3rd_party/catch2/catch.hpp" + +using namespace lightstep; + +TEST_CASE("NoDnsResolver") { + Logger logger; + EventBase event_base; + DnsResolverOptions resolver_options; + auto resolver = MakeDnsResolver(logger, event_base, resolver_options); + MockDnsResolutionCallback callback{event_base}; + + SECTION("We can resolve raw ip addresses.") { + resolver->Resolve("192.168.0.2", AF_INET, callback); + REQUIRE(callback.ip_addresses() == + std::vector{IpAddress{"192.168.0.2"}}); + REQUIRE(callback.error_message().empty()); + } + + SECTION("Resolving an ipv4 address as ipv6 fails.") { + resolver->Resolve("192.168.0.2", AF_INET6, callback); + REQUIRE(callback.ip_addresses().empty()); + } + + SECTION("Resolving a non-ip address fails.") { + resolver->Resolve("www.google.com", AF_INET, callback); + REQUIRE(callback.ip_addresses().empty()); + } +} diff --git a/test/network/socket_test.cpp b/test/network/socket_test.cpp new file mode 100644 index 00000000..a850850f --- /dev/null +++ b/test/network/socket_test.cpp @@ -0,0 +1,29 @@ +#include "network/socket.h" + +#include "3rd_party/catch2/catch.hpp" + +using namespace lightstep; + +TEST_CASE("Socket") { + SECTION("Socket can be move constructed.") { + Socket s1; + auto file_descriptor = s1.file_descriptor(); + Socket s2{std::move(s1)}; + CHECK(s2.file_descriptor() == file_descriptor); + CHECK(s1.file_descriptor() == -1); + } + + SECTION("Socket can be move assigned.") { + Socket s1, s2; + auto file_descriptor = s1.file_descriptor(); + s2 = std::move(s1); + REQUIRE(s2.file_descriptor() == file_descriptor); + REQUIRE(s1.file_descriptor() == -1); + } + + SECTION("We can change options on a socket.") { + Socket s1; + s1.SetNonblocking(); + s1.SetReuseAddress(); + } +} diff --git a/test/network/timer_event_test.cpp b/test/network/timer_event_test.cpp new file mode 100644 index 00000000..5d977c7d --- /dev/null +++ b/test/network/timer_event_test.cpp @@ -0,0 +1,63 @@ +#include + +#include "network/timer_event.h" + +#include "3rd_party/catch2/catch.hpp" + +using namespace lightstep; + +static const auto TestingCallbackInterval = std::chrono::milliseconds{600}; +static const auto TestingDuration = std::chrono::milliseconds{2000}; +static const auto TestingEpsilon = std::chrono::milliseconds{100}; + +namespace { +struct CallbackContext { + EventBase* event_base{nullptr}; + std::vector time_points; +}; +} // namespace + +static void TimerCallback(int /*socket*/, short /*what*/, void* context) { + auto& callback_context = *static_cast(context); + callback_context.time_points.push_back(std::chrono::steady_clock::now()); + auto duration = callback_context.time_points.back() - + callback_context.time_points.front(); + if (duration >= TestingDuration) { + callback_context.event_base->LoopBreak(); + } +} + +TEST_CASE("TimerEvent") { + EventBase event_base; + CallbackContext callback_context; + callback_context.event_base = &event_base; + TimerEvent timer_event{event_base, TestingCallbackInterval, TimerCallback, + static_cast(&callback_context)}; + SECTION("TimerEvent schedules callbacks at a given interval.") { + event_base.Dispatch(); + REQUIRE(callback_context.time_points.size() == 5); + for (int i = 1; i < 5; ++i) { + auto duration = + callback_context.time_points[i] - callback_context.time_points[i - 1]; + REQUIRE(duration > TestingCallbackInterval - TestingEpsilon); + REQUIRE(duration < TestingCallbackInterval + TestingEpsilon); + } + } + + SECTION( + "Reset can be used to restart the timer callbacks from at the current " + "time point.") { + event_base.OnTimeout( + 5 * TestingCallbackInterval - TestingEpsilon, + [](int /*socket*/, short /*what*/, void* context) { + static_cast(context)->Reset(); + }, + static_cast(&timer_event)); + event_base.Dispatch(); + auto duration = + callback_context.time_points.at(4) - callback_context.time_points.at(3); + auto expected_duration = 2 * TestingCallbackInterval - TestingEpsilon; + REQUIRE(duration > expected_duration - TestingEpsilon); + REQUIRE(duration < expected_duration + TestingEpsilon); + } +} diff --git a/test/network/vector_write_test.cpp b/test/network/vector_write_test.cpp new file mode 100644 index 00000000..00578fae --- /dev/null +++ b/test/network/vector_write_test.cpp @@ -0,0 +1,68 @@ +#include "network/vector_write.h" + +#include + +#include "3rd_party/catch2/catch.hpp" +#include "network/fragment_array_input_stream.h" +#include "network/socket.h" +#include "test/echo_server/echo_server_handle.h" +#include "test/ports.h" +#include "test/utility.h" +using namespace lightstep; + +const auto TcpPort = static_cast(PortAssignments::VectorWriteTestTcp); + +TEST_CASE("VectorWrite") { + EchoServerHandle echo_server{ + static_cast(PortAssignments::VectorWriteTestHttp), TcpPort}; + + FragmentArrayInputStream fragments1{MakeFragment("abc"), MakeFragment("123")}; + FragmentArrayInputStream fragments2{MakeFragment("xyz"), MakeFragment("")}; + Socket socket; + socket.SetReuseAddress(); + IpAddress server_address{"127.0.0.1", TcpPort}; + REQUIRE(socket.Connect(server_address.addr(), + sizeof(server_address.ipv4_address())) == 0); + + SECTION("Calling write with no fragments returns true.") { + REQUIRE(Write(socket.file_descriptor(), {})); + } + + SECTION("Write can be used to send fragments over a socket.") { + auto result = Write(socket.file_descriptor(), {&fragments1, &fragments2}); + socket = Socket{}; + REQUIRE(result); + REQUIRE(echo_server.data() == "abc123xyz"); + } + + SECTION( + "On a non-blocking socket, the vectorized write returns immediately with " + "false if the socket would block.") { + socket.SetNonblocking(); + std::vector big_array(100000, ' '); + int i = 0; + int max_iterations = 1000; + for (; i < max_iterations; ++i) { + FragmentArrayInputStream fragments{{static_cast(big_array.data()), + static_cast(big_array.size())}, + {static_cast(big_array.data()), + static_cast(big_array.size())}, + {static_cast(big_array.data()), + static_cast(big_array.size())}, + {static_cast(big_array.data()), + static_cast(big_array.size())}}; + auto result = Write(socket.file_descriptor(), {&fragments}); + if (!result) { + break; + } + } + REQUIRE(i < max_iterations); + } + + SECTION("Write throws an exception when there's an error.") { + socket.SetNonblocking(); + auto file_descriptor = socket.file_descriptor(); + socket = Socket{}; + REQUIRE_THROWS(Write(file_descriptor, {&fragments1, &fragments2})); + } +} diff --git a/test/number_simulation.cpp b/test/number_simulation.cpp new file mode 100644 index 00000000..73092cf7 --- /dev/null +++ b/test/number_simulation.cpp @@ -0,0 +1,240 @@ +#include "test/number_simulation.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/bipart_memory_stream.h" +#include "common/utility.h" +#include "test/utility.h" +#include "test/zero_copy_connection_input_stream.h" + +#include + +static thread_local std::mt19937 RandomNumberGenerator{std::random_device{}()}; + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// GenerateRandomBinaryNumber +//-------------------------------------------------------------------------------------------------- +static std::tuple +GenerateRandomBinaryNumber(size_t max_digits) { + assert(max_digits <= 32); + static thread_local std::array number_buffer; + std::uniform_int_distribution num_digits_distribution{ + 1, static_cast(max_digits)}; + std::bernoulli_distribution digit_distribution{0.5}; + auto num_digits = num_digits_distribution(RandomNumberGenerator); + uint32_t value = 0; + for (int i = 0; i < num_digits; ++i) { + value *= 2; + if (digit_distribution(RandomNumberGenerator)) { + number_buffer[i] = '1'; + value += 1; + } else { + number_buffer[i] = '0'; + } + } + return {value, {number_buffer.data(), static_cast(num_digits)}}; +} + +//-------------------------------------------------------------------------------------------------- +// GenerateRandomBinaryNumbers +//-------------------------------------------------------------------------------------------------- +static void GenerateRandomBinaryNumbers(ChunkCircularBuffer& buffer, + std::vector& numbers, + size_t n) { + assert(buffer.buffer().max_size() > 5); + size_t max_digits = std::min(buffer.buffer().max_size() - 5, 32); + while (n-- != 0) { + uint32_t x; + opentracing::string_view s; + std::tie(x, s) = GenerateRandomBinaryNumber(max_digits); + if (AddString(buffer, s)) { + numbers.push_back(x); + } + } +} + +//-------------------------------------------------------------------------------------------------- +// ReadStreamHeader +//-------------------------------------------------------------------------------------------------- +static void ReadStreamHeader( + google::protobuf::io::ZeroCopyInputStream& stream) { + size_t chunk_size; + if (!ReadChunkHeader(stream, chunk_size)) { + std::cerr << "ReadChunkHeader failed\n"; + std::terminate(); + } + { + google::protobuf::io::LimitingInputStream limit_stream{ + &stream, static_cast(chunk_size)}; + collector::ReportRequest report; + if (!report.ParseFromZeroCopyStream(&limit_stream)) { + std::cerr << "Failed to parse stream header\n"; + std::terminate(); + } + } + stream.Skip(2); +} + +//-------------------------------------------------------------------------------------------------- +// ReadBinaryNumber +//-------------------------------------------------------------------------------------------------- +static uint32_t ReadBinaryNumber( + google::protobuf::io::ZeroCopyInputStream& stream, size_t num_digits) { + const char* data; + int size; + uint32_t result = 0; + size_t digit_index = 0; + while (stream.Next(reinterpret_cast(&data), &size)) { + for (int i = 0; i < size; ++i) { + if (digit_index++ == num_digits) { + stream.BackUp(size - i); + return result; + } + result = 2 * result + static_cast(data[i] == '1'); + } + } + if (digit_index != num_digits) { + std::cerr << "unexpected number of digits\n"; + std::terminate(); + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +// ReadBinaryNumberChunk +//-------------------------------------------------------------------------------------------------- +static bool ReadBinaryNumberChunk( + google::protobuf::io::ZeroCopyInputStream& stream, uint32_t& x) { + size_t num_digits; + if (!ReadChunkHeader(stream, num_digits)) { + return false; + } + + if (num_digits == 0) { + // We reached the terminal chunk + stream.Skip(2); + return false; + } + + x = ReadBinaryNumber(stream, num_digits); + stream.Skip(2); + return true; +} + +//-------------------------------------------------------------------------------------------------- +// HasPendingData +//-------------------------------------------------------------------------------------------------- +static bool HasPendingData(ConnectionStream& connection_stream) { + bool result = false; + connection_stream.Flush( + [&result](std::initializer_list streams) { + for (auto stream : streams) { + if (stream->num_fragments() > 0) { + result = true; + return false; + } + } + return true; + }); + return result; +} + +//-------------------------------------------------------------------------------------------------- +// RunBinaryNumberProducer +//-------------------------------------------------------------------------------------------------- +void RunBinaryNumberProducer(ChunkCircularBuffer& buffer, + std::vector& numbers, size_t num_threads, + size_t n) { + std::vector> thread_numbers(num_threads); + std::vector threads(num_threads); + for (size_t thread_index = 0; thread_index < num_threads; ++thread_index) { + threads[thread_index] = + std::thread{GenerateRandomBinaryNumbers, std::ref(buffer), + std::ref(thread_numbers[thread_index]), n}; + } + for (auto& thread : threads) { + thread.join(); + } + for (size_t thread_index = 0; thread_index < num_threads; ++thread_index) { + numbers.insert(numbers.end(), thread_numbers[thread_index].begin(), + thread_numbers[thread_index].end()); + } +} + +//-------------------------------------------------------------------------------------------------- +// RunBinaryNumberConsumer +//-------------------------------------------------------------------------------------------------- +void RunBinaryNumberConsumer(ChunkCircularBuffer& buffer, + std::atomic& exit, + std::vector& numbers) { + while (!exit || !buffer.buffer().empty()) { + buffer.Allot(); + auto allotment = buffer.allotment(); + BipartMemoryInputStream stream{allotment.data1, allotment.size1, + allotment.data2, allotment.size2}; + uint32_t x; + while (ReadBinaryNumberChunk(stream, x)) { + numbers.push_back(x); + } + buffer.Consume(buffer.num_bytes_allotted()); + } +} + +//-------------------------------------------------------------------------------------------------- +// RunBinaryNumberConnectionConsumer +//-------------------------------------------------------------------------------------------------- +void RunBinaryNumberConnectionConsumer( + SpanStream& span_stream, std::vector& connection_streams, + std::atomic& exit, std::vector& numbers) { + std::vector> zero_copy_streams; + zero_copy_streams.reserve(connection_streams.size()); + for (auto& connection_stream : connection_streams) { + zero_copy_streams.emplace_back( + new ZeroCopyConnectionInputStream{connection_stream}); + zero_copy_streams.back()->Skip(connection_stream.first_chunk_position()); + ReadStreamHeader(*zero_copy_streams.back()); + } + + std::cout << "reading numbers" << std::endl; + std::uniform_int_distribution distribution{ + 0, static_cast(connection_streams.size()) - 1}; + while (true) { + if (exit) { + span_stream.Allot(); + if (span_stream.empty()) { + break; + } + } + auto connection_index = distribution(RandomNumberGenerator); + auto& connection_stream = connection_streams[connection_index]; + if (!HasPendingData(connection_stream)) { + continue; + } + auto& stream = *zero_copy_streams[connection_index]; + uint32_t x; + if (!ReadBinaryNumberChunk(stream, x)) { + std::cerr << "ReadBinaryNumberChunk failed\n"; + std::terminate(); + } + numbers.push_back(x); + } + std::cout << "shutting down" << std::endl; + for (auto& connection_stream : connection_streams) { + connection_stream.Shutdown(); + } + for (auto& stream : zero_copy_streams) { + uint32_t x; + while (ReadBinaryNumberChunk(*stream, x)) { + numbers.push_back(x); + } + } +} +} // namespace lightstep diff --git a/test/number_simulation.h b/test/number_simulation.h new file mode 100644 index 00000000..a7330cc9 --- /dev/null +++ b/test/number_simulation.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "common/chunk_circular_buffer.h" +#include "recorder/stream_recorder/connection_stream.h" + +#include +#include + +namespace lightstep { +/** + * Randomly writes binary numbers into a given ChunkCircularBuffer. + * @param buffer the buffer to write numbers into. + * @param numbers outputs the numbers written. + * @param num_threads the number of threads to write numbers on. + * @param n the number of numbers to write. + */ +void RunBinaryNumberProducer(ChunkCircularBuffer& buffer, + std::vector& numbers, size_t num_threads, + size_t n); + +/** + * Reads numbers out of the given ChunkCircularBuffer. + * @param buffer the buffer to read numbers out of. + * @param exit indicates that the producer has finished. + * @param numbers outputs the numbers read. + */ +void RunBinaryNumberConsumer(ChunkCircularBuffer& buffer, + std::atomic& exit, + std::vector& numbers); + +/** + * Reads numbers out of the given ChunkCircularBuffer through the given + * ConnectionStreams. + * @param span_stream the SpanStream connected the ChunkCircularBuffer. + * @param connection_streams the ConnectionStreams to read numbers out of. + * @param exit indicates that the producer has finished. + * @param numbers outputs the numbers read. + */ +void RunBinaryNumberConnectionConsumer( + SpanStream& span_stream, std::vector& connection_streams, + std::atomic& exit, std::vector& numbers); +} // namespace lightstep diff --git a/test/ports.h b/test/ports.h new file mode 100644 index 00000000..624dc08d --- /dev/null +++ b/test/ports.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace lightstep { +enum class PortAssignments : uint16_t { + AresDnsResolverTest = 9000, + SatelliteDnsResolutionManagerTest, + SatelliteEndpointManagerTest, + StreamRecorderTest, + MockSatelliteTest, + VectorWriteTestHttp, + VectorWriteTestTcp +}; +} // namespace lightstep diff --git a/test/recorder/BUILD b/test/recorder/BUILD new file mode 100644 index 00000000..21e65405 --- /dev/null +++ b/test/recorder/BUILD @@ -0,0 +1,72 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_catch_test", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_cc_library( + name = "in_memory_recorder_lib", + private_hdrs = [ + "in_memory_recorder.h", + ], + deps = [ + "//src/recorder:recorder_interface", + ], +) + +lightstep_cc_library( + name = "in_memory_async_transporter_lib", + private_hdrs = [ + "in_memory_async_transporter.h", + ], + srcs = [ + "in_memory_async_transporter.cpp", + ], + deps = [ + "//src/recorder:transporter_lib", + ], +) + +lightstep_cc_library( + name = "in_memory_sync_transporter_lib", + private_hdrs = [ + "in_memory_sync_transporter.h", + ], + srcs = [ + "in_memory_sync_transporter.cpp", + ], + deps = [ + "//src/recorder:transporter_lib", + ], +) + +lightstep_catch_test( + name = "manual_recorder_test", + srcs = [ + "manual_recorder_test.cpp", + ], + deps = [ + "//:manual_tracer_lib", + "//test:utility_lib", + "//test:testing_condition_variable_wrapper_lib", + "//test:counting_metrics_observer_lib", + ":in_memory_async_transporter_lib", + ], +) + +lightstep_catch_test( + name = "auto_recorder_test", + srcs = [ + "auto_recorder_test.cpp", + ], + deps = [ + "//:manual_tracer_lib", + "//test:utility_lib", + "//test:testing_condition_variable_wrapper_lib", + "//test:counting_metrics_observer_lib", + ":in_memory_sync_transporter_lib", + ], +) diff --git a/test/auto_recorder_test.cpp b/test/recorder/auto_recorder_test.cpp similarity index 77% rename from test/auto_recorder_test.cpp rename to test/recorder/auto_recorder_test.cpp index 85cbe8a5..8d6cc5f0 100644 --- a/test/auto_recorder_test.cpp +++ b/test/recorder/auto_recorder_test.cpp @@ -1,14 +1,13 @@ -#include "../src/auto_recorder.h" -#include #include -#include "../src/lightstep_tracer_impl.h" -#include "counting_metrics_observer.h" -#include "in_memory_sync_transporter.h" -#include "testing_condition_variable_wrapper.h" -#include "utility.h" -#define CATCH_CONFIG_MAIN -#include +#include "3rd_party/catch2/catch.hpp" +#include "lightstep/tracer.h" +#include "recorder/auto_recorder.h" +#include "test/counting_metrics_observer.h" +#include "test/recorder/in_memory_sync_transporter.h" +#include "test/testing_condition_variable_wrapper.h" +#include "test/utility.h" +#include "tracer/lightstep_tracer_impl.h" using namespace lightstep; using namespace opentracing; @@ -34,13 +33,13 @@ TEST_CASE("auto_recorder") { CHECK(tracer); // Ensure that the writer thread is waiting. - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); SECTION( "The writer thread waits until `now() + reporting_period` to " "send a report") { auto now = condition_variable->Now(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); auto event = condition_variable->next_event(); CHECK(dynamic_cast( event) != nullptr); @@ -51,12 +50,12 @@ TEST_CASE("auto_recorder") { "If the writer thread takes longer than `reporting_period` to run, then " "it runs again immediately upon finishing.") { auto now = condition_variable->Now() + 2 * reporting_period; - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); auto span = tracer->StartSpan("abc"); span->Finish(); condition_variable->set_now(now); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); auto event = condition_variable->next_event(); CHECK(dynamic_cast( event) != nullptr); @@ -68,12 +67,12 @@ TEST_CASE("auto_recorder") { "If the writer thread takes time between 0 and `reporting_period` to " "run, then it subtracts the elapse time from the next timeout") { auto now = condition_variable->Now() + 3 * reporting_period / 2; - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); auto span = tracer->StartSpan("abc"); span->Finish(); condition_variable->set_now(now); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); auto event = condition_variable->next_event(); CHECK(dynamic_cast( event) != nullptr); @@ -90,25 +89,10 @@ TEST_CASE("auto_recorder") { auto span = tracer->StartSpan("abc"); span->Finish(); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); condition_variable->set_block_notify_all(false); - condition_variable->Step(); - CHECK(!recorder->is_writer_running()); - } - - SECTION( - "If a collector sends back a disable command, then the tracer stops " - "sending reports") { - in_memory_transporter->set_should_disable(true); - condition_variable->set_block_notify_all(true); - - auto span = tracer->StartSpan("abc"); - span->Finish(); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); - condition_variable->set_block_notify_all(false); - condition_variable->Step(); + REQUIRE_NOTHROW(condition_variable->Step()); CHECK(!recorder->is_writer_running()); } @@ -126,17 +110,17 @@ TEST_CASE("auto_recorder") { condition_variable->next_event()) != nullptr); // Wait until the first report gets sent. - condition_variable->Step(); - condition_variable->Step(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->Step()); condition_variable->set_block_notify_all(false); auto span = tracer->StartSpan("xyz"); CHECK(span); span->Finish(); // Ensure that the second report gets sent. - condition_variable->Step(); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); auto reports = in_memory_transporter->reports(); CHECK(reports.size() == 2); @@ -151,8 +135,8 @@ TEST_CASE("auto_recorder") { "successfully flushed.") { auto span = tracer->StartSpan("abc"); span->Finish(); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); CHECK(metrics_observer->num_flushes == 1); } @@ -163,8 +147,8 @@ TEST_CASE("auto_recorder") { span1->Finish(); auto span2 = tracer->StartSpan("abc"); span2->Finish(); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); CHECK(metrics_observer->num_spans_sent == 2); } @@ -177,8 +161,8 @@ TEST_CASE("auto_recorder") { span->Finish(); } condition_variable->set_block_notify_all(false); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); CHECK(metrics_observer->num_spans_sent == max_buffered_spans); CHECK(metrics_observer->num_spans_dropped == 1); } @@ -200,8 +184,8 @@ TEST_CASE("auto_recorder") { CHECK(dynamic_cast( condition_variable->next_event()) == nullptr); condition_variable->set_block_notify_all(false); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); condition_variable->set_block_notify_all(true); for (size_t i = 0; i < max_buffered_spans_new; ++i) { @@ -214,7 +198,7 @@ TEST_CASE("auto_recorder") { CHECK(dynamic_cast( condition_variable->next_event()) != nullptr); condition_variable->set_block_notify_all(false); - condition_variable->Step(); - condition_variable->WaitTillNextEvent(); + REQUIRE_NOTHROW(condition_variable->Step()); + REQUIRE_NOTHROW(condition_variable->WaitTillNextEvent()); } } diff --git a/test/in_memory_async_transporter.cpp b/test/recorder/in_memory_async_transporter.cpp similarity index 97% rename from test/in_memory_async_transporter.cpp rename to test/recorder/in_memory_async_transporter.cpp index 010dcb62..32e3b492 100644 --- a/test/in_memory_async_transporter.cpp +++ b/test/recorder/in_memory_async_transporter.cpp @@ -21,7 +21,7 @@ void InMemoryAsyncTransporter::Write() { std::cerr << "No context, success callback, or request\n"; std::terminate(); } - const collector::ReportRequest& report = + const auto& report = dynamic_cast(*active_request_); reports_.push_back(report); diff --git a/test/in_memory_async_transporter.h b/test/recorder/in_memory_async_transporter.h similarity index 96% rename from test/in_memory_async_transporter.h rename to test/recorder/in_memory_async_transporter.h index da01bb3d..5c4aafb6 100644 --- a/test/in_memory_async_transporter.h +++ b/test/recorder/in_memory_async_transporter.h @@ -1,11 +1,12 @@ #pragma once -#include #include #include #include #include + #include "lightstep-tracer-common/collector.pb.h" +#include "lightstep/transporter.h" namespace lightstep { class InMemoryAsyncTransporter : public AsyncTransporter { diff --git a/test/in_memory_recorder.h b/test/recorder/in_memory_recorder.h similarity index 88% rename from test/in_memory_recorder.h rename to test/recorder/in_memory_recorder.h index 5fd4f778..e0f39483 100644 --- a/test/in_memory_recorder.h +++ b/test/recorder/in_memory_recorder.h @@ -1,9 +1,10 @@ #pragma once +#include "recorder/recorder.h" + #include #include #include -#include "../src/recorder.h" namespace lightstep { // InMemoryRecorder is used for testing only. @@ -26,7 +27,9 @@ class InMemoryRecorder final : public Recorder { collector::Span top() const { std::lock_guard lock_guard{mutex_}; - if (spans_.empty()) throw std::runtime_error("no spans"); + if (spans_.empty()) { + throw std::runtime_error("no spans"); + } return spans_.back(); } diff --git a/test/in_memory_sync_transporter.cpp b/test/recorder/in_memory_sync_transporter.cpp similarity index 86% rename from test/in_memory_sync_transporter.cpp rename to test/recorder/in_memory_sync_transporter.cpp index 13e8e241..fab3d4ed 100644 --- a/test/in_memory_sync_transporter.cpp +++ b/test/recorder/in_memory_sync_transporter.cpp @@ -1,4 +1,4 @@ -#include "in_memory_sync_transporter.h" +#include "test/recorder/in_memory_sync_transporter.h" namespace lightstep { //------------------------------------------------------------------------------ @@ -10,8 +10,7 @@ opentracing::expected InMemorySyncTransporter::Send( if (should_throw_) { throw std::runtime_error{"should_throw_ == true"}; } - const collector::ReportRequest& report = - dynamic_cast(request); + const auto& report = dynamic_cast(request); reports_.push_back(report); spans_.reserve(spans_.size() + report.spans_size()); diff --git a/test/in_memory_sync_transporter.h b/test/recorder/in_memory_sync_transporter.h similarity index 96% rename from test/in_memory_sync_transporter.h rename to test/recorder/in_memory_sync_transporter.h index cc58ec8e..78f91f04 100644 --- a/test/in_memory_sync_transporter.h +++ b/test/recorder/in_memory_sync_transporter.h @@ -1,11 +1,12 @@ #pragma once -#include #include #include #include #include + #include "lightstep-tracer-common/collector.pb.h" +#include "lightstep/transporter.h" namespace lightstep { class InMemorySyncTransporter : public SyncTransporter { diff --git a/test/manual_recorder_test.cpp b/test/recorder/manual_recorder_test.cpp similarity index 91% rename from test/manual_recorder_test.cpp rename to test/recorder/manual_recorder_test.cpp index e8df4e5c..33b5daf8 100644 --- a/test/manual_recorder_test.cpp +++ b/test/recorder/manual_recorder_test.cpp @@ -1,14 +1,13 @@ -#include "../src/manual_recorder.h" -#include #include -#include "../src/lightstep_tracer_impl.h" -#include "counting_metrics_observer.h" -#include "in_memory_async_transporter.h" -#include "testing_condition_variable_wrapper.h" -#include "utility.h" -#define CATCH_CONFIG_MAIN -#include +#include "3rd_party/catch2/catch.hpp" +#include "lightstep/tracer.h" +#include "recorder/manual_recorder.h" +#include "test/counting_metrics_observer.h" +#include "test/recorder/in_memory_async_transporter.h" +#include "test/testing_condition_variable_wrapper.h" +#include "test/utility.h" +#include "tracer/lightstep_tracer_impl.h" using namespace lightstep; using namespace opentracing; @@ -33,7 +32,7 @@ TEST_CASE("manual_recorder") { auto span = tracer->StartSpan("abc"); CHECK(span); span->Finish(); - CHECK(in_memory_transporter->reports().size() == 0); + CHECK(in_memory_transporter->reports().empty()); CHECK(tracer->Flush()); in_memory_transporter->Write(); CHECK(in_memory_transporter->reports().size() == 1); diff --git a/test/recorder/stream_recorder/BUILD b/test/recorder/stream_recorder/BUILD new file mode 100644 index 00000000..0f24f8a2 --- /dev/null +++ b/test/recorder/stream_recorder/BUILD @@ -0,0 +1,136 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_catch_test", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_catch_test( + name = "stream_recorder_test", + srcs = [ + "stream_recorder_test.cpp", + ], + deps = [ + "//src/tracer:lightstep_tracer_impl_lib", + "//src/recorder/stream_recorder:stream_recorder_lib", + "//src/network/ares_dns_resolver:ares_dns_resolver_lib", + "//test/mock_satellite:mock_satellite_lib", + "//test:ports_lib", + "//test:utility_lib", + "//test:counting_metrics_observer_lib", + ], +) + +lightstep_catch_test( + name = "fragment_span_input_stream_test", + srcs = [ + "fragment_span_input_stream_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:fragment_span_input_stream_lib", + "//test:utility_lib", + ], +) + +lightstep_catch_test( + name = "utility_test", + srcs = [ + "utility_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:utility_lib", + "//lightstep-tracer-common:collector_proto_cc", + ], +) + +lightstep_catch_test( + name = "satellite_streamer_test", + srcs = [ + "satellite_streamer_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:satellite_streamer_lib", + ], +) + +lightstep_catch_test( + name = "satellite_endpoint_manager_test", + srcs = [ + "satellite_endpoint_manager_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:satellite_endpoint_manager_lib", + "//src/network/ares_dns_resolver:ares_dns_resolver_lib", + "//test/mock_dns_server:mock_dns_server_lib", + "//test:ports_lib", + ], +) + +lightstep_catch_test( + name = "satellite_dns_resolution_manager_test", + srcs = [ + "satellite_dns_resolution_manager_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:satellite_dns_resolution_manager_lib", + "//src/network/ares_dns_resolver:ares_dns_resolver_lib", + "//test/mock_dns_server:mock_dns_server_lib", + "//test:ports_lib", + ], +) + +lightstep_catch_test( + name = "embedded_metrics_message_test", + srcs = [ + "embedded_metrics_message_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:embedded_metrics_message_lib", + ], +) + +lightstep_catch_test( + name = "span_stream_test", + srcs = [ + "span_stream_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:span_stream_lib", + "//test:utility_lib", + ], +) + +lightstep_catch_test( + name = "connection_stream_test", + srcs = [ + "connection_stream_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:connection_stream_lib", + "//src/recorder/stream_recorder:utility_lib", + "//test:number_simulation_lib", + "//test:utility_lib", + ], +) + +lightstep_catch_test( + name = "host_header_test", + srcs = [ + "host_header_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:host_header_lib", + ], +) + +lightstep_catch_test( + name = "status_line_parser_test", + srcs = [ + "status_line_parser_test.cpp", + ], + deps = [ + "//src/recorder/stream_recorder:status_line_parser_lib", + ], +) diff --git a/test/recorder/stream_recorder/connection_stream_test.cpp b/test/recorder/stream_recorder/connection_stream_test.cpp new file mode 100644 index 00000000..d1a0bfbc --- /dev/null +++ b/test/recorder/stream_recorder/connection_stream_test.cpp @@ -0,0 +1,290 @@ +#include "recorder/stream_recorder/connection_stream.h" + +#include +#include + +#include "3rd_party/catch2/catch.hpp" +#include "recorder/stream_recorder/span_stream.h" +#include "recorder/stream_recorder/utility.h" +#include "test/number_simulation.h" +#include "test/utility.h" +using namespace lightstep; + +static std::string ToString( + std::initializer_list fragment_streams) { + std::string result; + for (auto fragment_stream : fragment_streams) { + result += ToString(*fragment_stream); + } + return result; +} + +static collector::ReportRequest ParseStreamHeader(std::string& s) { + collector::ReportRequest result; + auto body_start = s.find("\r\n\r\n"); + REQUIRE(body_start != std::string::npos); + body_start += 4; + auto chunk_start = s.find("\r\n", body_start); + REQUIRE(chunk_start != std::string::npos); + auto chunk_size = + std::stoi(s.substr(body_start, body_start - chunk_start), nullptr, 16); + chunk_start += 2; + REQUIRE(result.ParseFromString(s.substr(chunk_start, chunk_size))); + s = s.substr(chunk_start + chunk_size + 2); + return result; +} + +TEST_CASE("ConnectionStream") { + LightStepTracerOptions tracer_options; + ChunkCircularBuffer span_buffer{1000}; + MetricsObserver metrics_observer; + StreamRecorderMetrics metrics{metrics_observer}; + SpanStream span_stream{span_buffer, metrics, 1}; + std::string header_common_fragment = + WriteStreamHeaderCommonFragment(tracer_options, 123); + auto host_header_fragment = MakeFragment("Host:abc\r\n"); + ConnectionStream connection_stream{ + host_header_fragment, + Fragment{static_cast(&header_common_fragment[0]), + static_cast(header_common_fragment.size())}, + span_stream}; + std::string contents; + REQUIRE(connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + })); + connection_stream.Reset(); + + SECTION( + "The ConnectionStream writes the same header if we flush in smaller " + "pieces.") { + std::string contents2; + while (true) { + auto result = connection_stream.Flush( + [&contents2]( + std::initializer_list fragment_streams) { + contents2 += ToString(fragment_streams)[0]; + return Consume(fragment_streams, 1); + }); + if (result) { + break; + } + } + REQUIRE(contents == contents2); + } + + SECTION( + "The ConnectionStream is only completed with the terminal fragment is " + "written.") { + REQUIRE(!connection_stream.completed()); + connection_stream.Flush( + [](std::initializer_list fragment_streams) { + auto n = ToString(fragment_streams).size(); + return Consume(fragment_streams, static_cast(n)); + }); + REQUIRE(!connection_stream.completed()); + connection_stream.Shutdown(); + REQUIRE(!connection_stream.completed()); + connection_stream.Flush( + [](std::initializer_list fragment_streams) { + auto n = ToString(fragment_streams).size(); + return Consume(fragment_streams, static_cast(n) - 1); + }); + REQUIRE(!connection_stream.completed()); + connection_stream.Flush( + [](std::initializer_list fragment_streams) { + return Consume(fragment_streams, 1); + }); + REQUIRE(connection_stream.completed()); + } + + SECTION( + "The ConnectionStream consumes metrics and sends them in the stream " + "header.") { + metrics.OnSpansDropped(3); + connection_stream.Reset(); + REQUIRE(metrics.num_dropped_spans() == 0); + REQUIRE(connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + })); + auto report_request = ParseStreamHeader(contents); + auto& counts = report_request.internal_metrics().counts(); + REQUIRE(counts.size() == 1); + REQUIRE(counts[0].int_value() == 3); + } + + SECTION( + "ConnectionStream adds metrics back if it fails to send the stream " + "header.") { + metrics.OnSpansDropped(3); + connection_stream.Reset(); + REQUIRE(metrics.num_dropped_spans() == 0); + connection_stream.Flush( + [](std::initializer_list fragment_streams) { + auto contents = ToString(fragment_streams); + return Consume(fragment_streams, + static_cast(contents.size()) - 1); + }); + connection_stream.Reset(); + REQUIRE(connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + })); + auto report_request = ParseStreamHeader(contents); + auto& counts = report_request.internal_metrics().counts(); + REQUIRE(counts.size() == 1); + REQUIRE(counts[0].int_value() == 3); + REQUIRE(metrics.num_dropped_spans() == 0); + } + + SECTION( + "After writing the header, ConnectionStream writes the contents of the " + "span buffer.") { + AddString(span_buffer, "abc"); + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + }); + ParseStreamHeader(contents); + REQUIRE(contents == "3\r\nabc\r\n"); + } + + SECTION("If a remnant is left, it gets picked up on the next flush.") { + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + }); + AddString(span_buffer, "abc"); + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, 4); + }); + AddString(span_buffer, "123"); + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, 4); + }); + REQUIRE(contents == "bc\r\n3\r\n123\r\n"); + } + + SECTION( + "If a remnant is left when the stream is shutdown, it still gets " + "written.") { + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + }); + AddString(span_buffer, "abc"); + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, 4); + }); + connection_stream.Shutdown(); + AddString(span_buffer, "123"); + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, 4); + }); + REQUIRE(contents == "bc\r\n0\r\n\r\n"); + } + + SECTION( + "If ConnectionStream is reset when there's a remnant left, it clears it " + "and records a dropped span.") { + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + }); + AddString(span_buffer, "abc"); + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, 4); + }); + connection_stream.Reset(); + AddString(span_buffer, "123"); + connection_stream.Flush( + [&contents]( + std::initializer_list fragment_streams) { + contents = ToString(fragment_streams); + return Consume(fragment_streams, static_cast(contents.size())); + }); + REQUIRE(span_buffer.buffer().empty()); + auto report_request = ParseStreamHeader(contents); + auto& counts = report_request.internal_metrics().counts(); + REQUIRE(counts.size() == 1); + REQUIRE(counts[0].int_value() == 1); + } +} + +TEST_CASE( + "Verify through simulation that ConnectionStream behaves correctly.") { + LightStepTracerOptions tracer_options; + std::string header_common_fragment = + WriteStreamHeaderCommonFragment(tracer_options, 123); + MetricsObserver metrics_observer; + StreamRecorderMetrics metrics{metrics_observer}; + auto host_header_fragment = MakeFragment("Host:abc\r\n"); + const size_t num_producer_threads = 4; + const size_t num_connections = 10; + const size_t n = 25000; + for (size_t max_size : {10, 50, 100, 1000}) { + ChunkCircularBuffer buffer{max_size}; + SpanStream span_stream{buffer, metrics, num_connections}; + std::vector connection_streams; + connection_streams.reserve(num_connections); + for (int i = 0; i < static_cast(num_connections); ++i) { + connection_streams.emplace_back( + host_header_fragment, + Fragment{static_cast(&header_common_fragment[0]), + static_cast(header_common_fragment.size())}, + span_stream); + } + std::vector producer_numbers; + std::vector consumer_numbers; + auto producer = + std::thread{RunBinaryNumberProducer, std::ref(buffer), + std::ref(producer_numbers), num_producer_threads, n}; + std::atomic exit{false}; + auto consumer = + std::thread{RunBinaryNumberConnectionConsumer, std::ref(span_stream), + std::ref(connection_streams), std::ref(exit), + std::ref(consumer_numbers)}; + producer.join(); + exit = true; + consumer.join(); + REQUIRE(span_stream.empty()); + for (auto& connection_stream : connection_streams) { + REQUIRE(connection_stream.completed()); + } + REQUIRE(buffer.buffer().empty()); + std::sort(producer_numbers.begin(), producer_numbers.end()); + std::sort(consumer_numbers.begin(), consumer_numbers.end()); + REQUIRE(producer_numbers.size() == consumer_numbers.size()); + REQUIRE(producer_numbers == consumer_numbers); + } +} diff --git a/test/recorder/stream_recorder/embedded_metrics_message_test.cpp b/test/recorder/stream_recorder/embedded_metrics_message_test.cpp new file mode 100644 index 00000000..8a974197 --- /dev/null +++ b/test/recorder/stream_recorder/embedded_metrics_message_test.cpp @@ -0,0 +1,21 @@ +#include "recorder/stream_recorder/embedded_metrics_message.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("EmbeddedMetricsMessage") { + EmbeddedMetricsMessage metrics_message; + metrics_message.set_num_dropped_spans(3); + REQUIRE(metrics_message.num_dropped_spans() == 3); + auto fragment = metrics_message.MakeFragment(); + + SECTION("The metrics fragment can be deserialized by protobuf.") { + collector::ReportRequest report; + REQUIRE(report.ParseFromArray(fragment.first, + static_cast(fragment.second))); + REQUIRE(report.internal_metrics().counts().size() == 1); + auto& dropped_span_count = report.internal_metrics().counts()[0]; + REQUIRE(dropped_span_count.name() == "spans.dropped"); + REQUIRE(dropped_span_count.int_value() == 3); + } +} diff --git a/test/recorder/stream_recorder/fragment_span_input_stream_test.cpp b/test/recorder/stream_recorder/fragment_span_input_stream_test.cpp new file mode 100644 index 00000000..2ee67a62 --- /dev/null +++ b/test/recorder/stream_recorder/fragment_span_input_stream_test.cpp @@ -0,0 +1,22 @@ +#include "recorder/stream_recorder/fragment_span_input_stream.h" + +#include + +#include "3rd_party/catch2/catch.hpp" +#include "test/utility.h" +using namespace lightstep; + +TEST_CASE("FragmentSpanInputStream") { + FragmentSpanInputStream stream; + const char* s1 = "abc"; + const char* s2 = "123"; + + REQUIRE(stream.empty()); + REQUIRE(stream.chunk_start() == nullptr); + + SECTION("Set adds fragments for a span.") { + CircularBufferConstPlacement placement{s1, 2, s2, 3}; + stream.Set(placement, s1 + 1); + REQUIRE(ToString(stream) == "b123"); + } +} diff --git a/test/recorder/stream_recorder/host_header_test.cpp b/test/recorder/stream_recorder/host_header_test.cpp new file mode 100644 index 00000000..60dbc7a2 --- /dev/null +++ b/test/recorder/stream_recorder/host_header_test.cpp @@ -0,0 +1,17 @@ +#include "recorder/stream_recorder/host_header.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("HostHeader") { + LightStepTracerOptions options; + options.satellite_endpoints = {{"abc", 123}, {"abc123", 456}}; + HostHeader host_header{options}; + const char* header = static_cast(host_header.fragment().first); + + host_header.set_host("abc"); + REQUIRE(std::string{header} == "Host: abc\r\n"); + + host_header.set_host("abc123"); + REQUIRE(std::string{header} == "Host:abc123\r\n"); +} diff --git a/test/recorder/stream_recorder/satellite_dns_resolution_manager_test.cpp b/test/recorder/stream_recorder/satellite_dns_resolution_manager_test.cpp new file mode 100644 index 00000000..c93a5ff1 --- /dev/null +++ b/test/recorder/stream_recorder/satellite_dns_resolution_manager_test.cpp @@ -0,0 +1,76 @@ +#include "recorder/stream_recorder/satellite_dns_resolution_manager.h" + +#include "3rd_party/catch2/catch.hpp" +#include "network/dns_resolver.h" +#include "test/mock_dns_server/mock_dns_server_handle.h" +#include "test/ports.h" +using namespace lightstep; + +const auto DnsRefreshPeriod = + std::chrono::duration_cast( + std::chrono::milliseconds{100}); + +const auto DnsFailureRetryPeriod = + std::chrono::duration_cast( + std::chrono::milliseconds{100}); + +const auto ResolutionTimeout = std::chrono::milliseconds{100}; + +TEST_CASE("SatelliteDnsResolutionManager") { + MockDnsServerHandle dns_server{static_cast( + PortAssignments::SatelliteDnsResolutionManagerTest)}; + + StreamRecorderOptions recorder_options; + recorder_options.min_dns_resolution_refresh_period = DnsRefreshPeriod; + recorder_options.max_dns_resolution_refresh_period = DnsRefreshPeriod; + recorder_options.dns_failure_retry_period = DnsFailureRetryPeriod; + auto& resolver_options = recorder_options.dns_resolver_options; + resolver_options.resolution_server_port = + static_cast(PortAssignments::SatelliteDnsResolutionManagerTest); + resolver_options.resolution_servers = { + IpAddress{"127.0.0.1"}.ipv4_address().sin_addr}; + resolver_options.timeout = ResolutionTimeout; + Logger logger; + EventBase event_base; + + std::function on_ready_callback = [&] { event_base.LoopBreak(); }; + + SECTION("Hosts get resolved to ip addresses.") { + SatelliteDnsResolutionManager resolution_manager{ + logger, event_base, recorder_options, + AF_INET, "test.service", on_ready_callback}; + resolution_manager.Start(); + event_base.Dispatch(); + REQUIRE(resolution_manager.ip_addresses() == + std::vector{IpAddress{"192.168.0.2"}}); + } + + SECTION("Dns resolutions are periodically refreshed.") { + SatelliteDnsResolutionManager resolution_manager{ + logger, event_base, recorder_options, + AF_INET, "flip.service", on_ready_callback}; + resolution_manager.Start(); + event_base.Dispatch(); + REQUIRE(resolution_manager.ip_addresses() == + std::vector{IpAddress{"192.168.0.2"}}); + event_base.OnTimeout( + 1.5 * DnsRefreshPeriod, + [](int /*socket*/, short /*what*/, void* context) { + static_cast(context)->LoopBreak(); + }, + static_cast(&event_base)); + event_base.Dispatch(); + REQUIRE(resolution_manager.ip_addresses() == + std::vector{IpAddress{"192.168.0.3"}}); + } + + SECTION("Dns resolutions are retried when if there's an error.") { + SatelliteDnsResolutionManager resolution_manager{ + logger, event_base, recorder_options, + AF_INET, "flaky.service", on_ready_callback}; + resolution_manager.Start(); + event_base.Dispatch(); + REQUIRE(resolution_manager.ip_addresses() == + std::vector{IpAddress{"192.168.0.1"}}); + } +} diff --git a/test/recorder/stream_recorder/satellite_endpoint_manager_test.cpp b/test/recorder/stream_recorder/satellite_endpoint_manager_test.cpp new file mode 100644 index 00000000..5252b99e --- /dev/null +++ b/test/recorder/stream_recorder/satellite_endpoint_manager_test.cpp @@ -0,0 +1,59 @@ +#include "recorder/stream_recorder/satellite_endpoint_manager.h" + +#include "3rd_party/catch2/catch.hpp" +#include "network/dns_resolver.h" +#include "test/mock_dns_server/mock_dns_server_handle.h" +#include "test/ports.h" +using namespace lightstep; + +const auto ResolutionTimeout = std::chrono::milliseconds{100}; + +TEST_CASE("SatelliteEndpointManager") { + MockDnsServerHandle dns_server{ + static_cast(PortAssignments::SatelliteEndpointManagerTest)}; + + StreamRecorderOptions recorder_options; + auto& resolver_options = recorder_options.dns_resolver_options; + resolver_options.resolution_server_port = + static_cast(PortAssignments::SatelliteEndpointManagerTest); + resolver_options.resolution_servers = { + IpAddress{"127.0.0.1"}.ipv4_address().sin_addr}; + resolver_options.timeout = ResolutionTimeout; + LightStepTracerOptions tracer_options; + Logger logger; + EventBase event_base; + std::function on_ready_callback = [&] { event_base.LoopBreak(); }; + + SECTION( + "Round robin is used when assigning endpoints if multiple IPs are " + "available for a port.") { + tracer_options.satellite_endpoints = {{"satellites.service", 1234}}; + auto name = tracer_options.satellite_endpoints[0].first.c_str(); + SatelliteEndpointManager endpoint_manager{logger, event_base, + tracer_options, recorder_options, + on_ready_callback}; + endpoint_manager.Start(); + event_base.Dispatch(); + + REQUIRE(endpoint_manager.RequestEndpoint() == + std::make_pair(IpAddress{"192.168.0.1", 1234}, name)); + REQUIRE(endpoint_manager.RequestEndpoint() == + std::make_pair(IpAddress{"192.168.0.2", 1234}, name)); + } + + SECTION( + "Round robin is used when assigning endpoints if multiple ports are " + "listed for a host.") { + tracer_options.satellite_endpoints = {{"satellites.service", 1234}, + {"satellites.service", 2345}}; + SatelliteEndpointManager endpoint_manager{logger, event_base, + tracer_options, recorder_options, + on_ready_callback}; + endpoint_manager.Start(); + event_base.Dispatch(); + REQUIRE(endpoint_manager.RequestEndpoint().first == + IpAddress{"192.168.0.1", 1234}); + REQUIRE(endpoint_manager.RequestEndpoint().first == + IpAddress{"192.168.0.2", 2345}); + } +} diff --git a/test/recorder/stream_recorder/satellite_streamer_test.cpp b/test/recorder/stream_recorder/satellite_streamer_test.cpp new file mode 100644 index 00000000..c242cb78 --- /dev/null +++ b/test/recorder/stream_recorder/satellite_streamer_test.cpp @@ -0,0 +1,6 @@ +#include "recorder/stream_recorder/satellite_connection.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("SatelliteStreamer") {} diff --git a/test/recorder/stream_recorder/span_stream_test.cpp b/test/recorder/stream_recorder/span_stream_test.cpp new file mode 100644 index 00000000..e0457d42 --- /dev/null +++ b/test/recorder/stream_recorder/span_stream_test.cpp @@ -0,0 +1,90 @@ +#include "recorder/stream_recorder/span_stream.h" + +#include "test/utility.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("SpanStream") { + const int max_connections = 10; + const int max_buffer_bytes = 20; + ChunkCircularBuffer buffer{max_buffer_bytes}; + MetricsObserver metrics_observer; + StreamRecorderMetrics metrics{metrics_observer}; + SpanStream span_stream{buffer, metrics, 10}; + + SECTION("When the attached buffer is empty, SpanStream has no fragments.") { + REQUIRE(span_stream.empty()); + } + + SECTION("SpanStream contains a single fragment if the buffer doesn't wrap.") { + AddString(buffer, "abc"); + span_stream.Allot(); + REQUIRE(span_stream.num_fragments() == 1); + REQUIRE(ToString(span_stream) == "3\r\nabc\r\n"); + } + + SECTION("SpanStream's position is correctly updated if the buffer fills.") { + AddString(buffer, "X"); + span_stream.Allot(); + Consume({&span_stream}, 6); + span_stream.Consume(); + REQUIRE(buffer.buffer().empty()); + + AddString(buffer, std::string(10, 'X')); + span_stream.Allot(); + Consume({&span_stream}, 15); + span_stream.Consume(); + REQUIRE(buffer.buffer().empty()); + + AddString(buffer, "abc"); + span_stream.Allot(); + REQUIRE(ToString(span_stream) == "3\r\nabc\r\n"); + } + + SECTION( + "If a chunk is partially consumed, we advance to the end of the chunk " + "and store a remnant.") { + AddString(buffer, "abc"); + AddString(buffer, "123"); + span_stream.Allot(); + Consume({&span_stream}, 4); + REQUIRE(ToString(span_stream) == "3\r\n123\r\n"); + FragmentSpanInputStream span_remnant; + span_stream.PopSpanRemnant(span_remnant); + REQUIRE(ToString(span_remnant) == "bc\r\n"); + } + + SECTION("If chunks are fully consumed, there's no remnant.") { + AddString(buffer, "abc"); + AddString(buffer, "123"); + span_stream.Allot(); + Consume({&span_stream}, 8); + REQUIRE(ToString(span_stream) == "3\r\n123\r\n"); + FragmentSpanInputStream span_remnant; + span_stream.PopSpanRemnant(span_remnant); + REQUIRE(span_remnant.empty()); + } + + SECTION("SpanStream won't consume past a remnant until it's been cleared.") { + AddString(buffer, "abc"); + span_stream.Allot(); + Consume({&span_stream}, 4); + FragmentSpanInputStream span_remnant1; + span_stream.PopSpanRemnant(span_remnant1); + span_stream.Consume(); + + AddString(buffer, "123"); + span_stream.Allot(); + Consume({&span_stream}, 8); + FragmentSpanInputStream span_remnant2; + span_stream.PopSpanRemnant(span_remnant2); + REQUIRE(span_remnant2.empty()); + span_stream.Consume(); + + REQUIRE(ToString(buffer.allotment()) == "3\r\nabc\r\n3\r\n123\r\n"); + span_stream.RemoveSpanRemnant(span_remnant1.chunk_start()); + span_stream.Consume(); + REQUIRE(ToString(buffer.allotment()).empty()); + } +} diff --git a/test/recorder/stream_recorder/status_line_parser_test.cpp b/test/recorder/stream_recorder/status_line_parser_test.cpp new file mode 100644 index 00000000..5166d808 --- /dev/null +++ b/test/recorder/stream_recorder/status_line_parser_test.cpp @@ -0,0 +1,43 @@ +#include "recorder/stream_recorder/status_line_parser.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("StatusLineParser") { + StatusLineParser parser; + REQUIRE(!parser.completed()); + + SECTION("The status line is correctly parsed out of an http response.") { + parser.Parse("HTTP/1.1 200 OK\r\n"); + REQUIRE(parser.completed()); + REQUIRE(parser.status_code() == 200); + REQUIRE(parser.reason() == "OK"); + } + + SECTION("The status line can be parsed in multiple pieces.") { + parser.Parse("HTTP/"); + REQUIRE(!parser.completed()); + parser.Parse("1.1 200 OK\r\n"); + REQUIRE(parser.completed()); + parser.Parse("ignored"); + REQUIRE(parser.completed()); + REQUIRE(parser.status_code() == 200); + REQUIRE(parser.reason() == "OK"); + } + + SECTION("The parser can be reset.") { + parser.Parse("HTTP/1.1 200 OK\r\n"); + REQUIRE(parser.completed()); + parser.Reset(); + REQUIRE(!parser.completed()); + parser.Parse("HTTP/1.1 404 Deadly failure\r\n"); + REQUIRE(parser.completed()); + REQUIRE(parser.status_code() == 404); + REQUIRE(parser.reason() == "Deadly failure"); + } + + SECTION("The parser throws exceptions when given an invalid status line.") { + REQUIRE_THROWS(parser.Parse("abc\r\n")); + REQUIRE_THROWS(parser.Parse(" abc\r\n")); + } +} diff --git a/test/recorder/stream_recorder/stream_recorder_test.cpp b/test/recorder/stream_recorder/stream_recorder_test.cpp new file mode 100644 index 00000000..978b990f --- /dev/null +++ b/test/recorder/stream_recorder/stream_recorder_test.cpp @@ -0,0 +1,94 @@ +#include "recorder/stream_recorder/stream_recorder.h" + +#include + +#include "test/counting_metrics_observer.h" +#include "test/mock_satellite/mock_satellite_handle.h" +#include "test/ports.h" +#include "test/utility.h" +#include "tracer/lightstep_tracer_impl.h" + +#include "3rd_party/catch2/catch.hpp" +using namespace lightstep; + +TEST_CASE("StreamRecorder") { + std::unique_ptr mock_satellite{new MockSatelliteHandle{ + static_cast(PortAssignments::StreamRecorderTest)}}; + + // Testing stub. More will be added when StreamRecorder is filled out. + auto logger = std::make_shared(); + LightStepTracerOptions tracer_options; + tracer_options.satellite_endpoints = { + {"localhost", + static_cast(PortAssignments::StreamRecorderTest)}}; + auto metrics_observer = new CountingMetricsObserver{}; + tracer_options.metrics_observer.reset(metrics_observer); + + StreamRecorderOptions recorder_options; + recorder_options.min_satellite_reconnect_period = + std::chrono::duration_cast( + std::chrono::milliseconds{100}); + recorder_options.max_satellite_reconnect_period = + std::chrono::duration_cast( + std::chrono::milliseconds{150}); + + recorder_options.flushing_period = + std::chrono::duration_cast( + std::chrono::milliseconds{100}); + recorder_options.num_satellite_connections = 1; + auto stream_recorder = new StreamRecorder{*logger, std::move(tracer_options), + std::move(recorder_options)}; + std::unique_ptr recorder{stream_recorder}; + PropagationOptions propagation_options; + auto tracer = std::make_shared( + logger, propagation_options, std::move(recorder)); + + SECTION("Spans are consumed from the buffer and sent to the satellite.") { + auto span = tracer->StartSpan("abc"); + span->Finish(); + REQUIRE(!stream_recorder->empty()); + REQUIRE(IsEventuallyTrue( + [&stream_recorder] { return stream_recorder->empty(); })); + std::vector spans; + REQUIRE(IsEventuallyTrue([&] { + spans = mock_satellite->spans(); + return !spans.empty(); + })); + REQUIRE(spans.size() == 1); + REQUIRE(metrics_observer->num_spans_sent == 1); + } + + SECTION("Reports are sent to the satellite.") { + std::vector reports; + REQUIRE(IsEventuallyTrue([&] { + reports = mock_satellite->reports(); + return !reports.empty(); + })); + } + + SECTION("Closing a tracer forces any pending spans to be flushed.") { + auto span = tracer->StartSpan("abc"); + span->Finish(); + REQUIRE(!stream_recorder->empty()); + tracer->Close(); + REQUIRE(stream_recorder->empty()); + std::vector spans; + REQUIRE(IsEventuallyTrue([&] { + spans = mock_satellite->spans(); + return !spans.empty(); + })); + REQUIRE(spans.size() == 1); + REQUIRE(metrics_observer->num_spans_sent == 1); + } + + SECTION("Flush returns with false if the timeout is reached.") { + mock_satellite.reset(nullptr); + auto span = tracer->StartSpan("abc"); + span->Finish(); + REQUIRE(!stream_recorder->empty()); + REQUIRE(!stream_recorder->FlushWithTimeout( + std::chrono::duration_cast( + std::chrono::milliseconds{10}))); + REQUIRE(!stream_recorder->empty()); + } +} diff --git a/test/recorder/stream_recorder/utility_test.cpp b/test/recorder/stream_recorder/utility_test.cpp new file mode 100644 index 00000000..8184b639 --- /dev/null +++ b/test/recorder/stream_recorder/utility_test.cpp @@ -0,0 +1,50 @@ +#include "recorder/stream_recorder/utility.h" + +#include "3rd_party/catch2/catch.hpp" +#include "lightstep-tracer-common/collector.pb.h" +using namespace lightstep; + +TEST_CASE("SeparateEndpoints") { + std::vector hosts; + std::vector> indexed_endpoints; + + SECTION("Endpoints with the same host are separated out.") { + std::vector> endpoints = { + {"abc", 123}, {"xyz", 456}, {"abc", 789}}; + + std::tie(hosts, indexed_endpoints) = SeparateEndpoints(endpoints); + REQUIRE(hosts.size() == 2); + REQUIRE(hosts[0] == std::string{"abc"}); + REQUIRE(hosts[1] == std::string{"xyz"}); + REQUIRE(indexed_endpoints == std::vector>{ + {0, 123}, {1, 456}, {0, 789}}); + } + + SECTION("Case is ignored when comparing hosts.") { + std::vector> endpoints = {{"abc", 123}, + {"ABC", 456}}; + std::tie(hosts, indexed_endpoints) = SeparateEndpoints(endpoints); + REQUIRE(hosts.size() == 1); + } +} + +TEST_CASE("WriteStreamHeaderCommonFragment") { + LightStepTracerOptions tracer_options; + tracer_options.access_token = "abc123"; + tracer_options.tags = {{"xyz", 456}}; + auto serialization = WriteStreamHeaderCommonFragment(tracer_options, 789); + collector::ReportRequest report; + REQUIRE(report.ParseFromString(serialization)); + REQUIRE(report.auth().access_token() == "abc123"); + REQUIRE(report.reporter().reporter_id() == 789); + auto& tags = report.reporter().tags(); + REQUIRE(tags.size() == 1); + REQUIRE(tags[0].key() == "xyz"); + REQUIRE(tags[0].int_value() == 456); +} + +TEST_CASE("Contains") { + const char* s = "abc123"; + REQUIRE(Contains(s, 3, s + 1)); + REQUIRE(!Contains(s, 3, s + 3)); +} diff --git a/test/testing_condition_variable_wrapper.cpp b/test/testing_condition_variable_wrapper.cpp index 4012c2b0..9252b534 100644 --- a/test/testing_condition_variable_wrapper.cpp +++ b/test/testing_condition_variable_wrapper.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace lightstep { @@ -77,7 +78,7 @@ bool TestingConditionVariableWrapper::WaitUntil( if (predicate()) { return true; } - while (1) { + while (true) { lock.unlock(); AddEvent(&event); while (!event.DecrementTicker()) { @@ -111,7 +112,7 @@ void TestingConditionVariableWrapper::NotifyAll() { //------------------------------------------------------------------------------ void TestingConditionVariableWrapper::WaitTillNextEvent() { auto start_timestamp = std::chrono::system_clock::now(); - while (1) { + while (true) { { std::lock_guard lock_guard{mutex_}; if (!events_.empty()) { @@ -120,8 +121,7 @@ void TestingConditionVariableWrapper::WaitTillNextEvent() { } std::this_thread::yield(); if (std::chrono::system_clock::now() - start_timestamp > Timeout) { - std::cerr << "WaitTilNextEvent timed out\n"; - std::terminate(); + throw std::runtime_error{"WaitTilNext event timed out"}; } } } @@ -131,7 +131,7 @@ void TestingConditionVariableWrapper::WaitTillNextEvent() { //------------------------------------------------------------------------------ void TestingConditionVariableWrapper::Step() { auto start_timestamp = std::chrono::system_clock::now(); - while (1) { + while (true) { { std::lock_guard lock_guard{mutex_}; auto iter = std::min_element(events_.begin(), events_.end(), @@ -148,8 +148,7 @@ void TestingConditionVariableWrapper::Step() { } std::this_thread::yield(); if (std::chrono::system_clock::now() - start_timestamp > Timeout) { - std::cerr << "Step timed out\n"; - std::terminate(); + throw std::runtime_error{"Step timed out"}; } } } diff --git a/test/testing_condition_variable_wrapper.h b/test/testing_condition_variable_wrapper.h index 5ad304be..c5467346 100644 --- a/test/testing_condition_variable_wrapper.h +++ b/test/testing_condition_variable_wrapper.h @@ -3,7 +3,8 @@ #include #include #include -#include "../src/condition_variable_wrapper.h" + +#include "common/condition_variable_wrapper.h" namespace lightstep { @@ -12,8 +13,19 @@ namespace lightstep { // support testing. class TestingConditionVariableWrapper : public ConditionVariableWrapper { public: + TestingConditionVariableWrapper() = default; + + TestingConditionVariableWrapper(TestingConditionVariableWrapper&&) = delete; + TestingConditionVariableWrapper(const TestingConditionVariableWrapper&) = + delete; + ~TestingConditionVariableWrapper() override; + TestingConditionVariableWrapper& operator=( + TestingConditionVariableWrapper&&) = delete; + TestingConditionVariableWrapper& operator=( + const TestingConditionVariableWrapper&) = delete; + std::chrono::steady_clock::time_point Now() const override; void set_now(const std::chrono::steady_clock::time_point& time_point); @@ -36,11 +48,17 @@ class TestingConditionVariableWrapper : public ConditionVariableWrapper { class Event { public: - Event(const std::chrono::steady_clock::time_point& timeout) + explicit Event(const std::chrono::steady_clock::time_point& timeout) : timeout_{timeout} {} + Event(Event&&) = delete; + Event(const Event&) = delete; + virtual ~Event() = default; + Event& operator=(Event&&) = delete; + Event& operator=(const Event&) = delete; + virtual void Process( TestingConditionVariableWrapper& condition_variable) = 0; diff --git a/test/tracer/BUILD b/test/tracer/BUILD new file mode 100644 index 00000000..deced9af --- /dev/null +++ b/test/tracer/BUILD @@ -0,0 +1,69 @@ +load( + "//bazel:lightstep_build_system.bzl", + "lightstep_catch_test", + "lightstep_cc_test", + "lightstep_cc_library", + "lightstep_package", +) + +lightstep_package() + +lightstep_catch_test( + name = "propagation_test", + srcs = [ + "propagation_test.cpp", + ], + deps = [ + "//:manual_tracer_lib", + "//test/recorder:in_memory_recorder_lib", + ], +) + +lightstep_catch_test( + name = "tracer_test", + srcs = [ + "tracer_test.cpp", + ], + deps = [ + "//:manual_tracer_lib", + "//test/recorder:in_memory_recorder_lib", + "//test:utility_lib", + ], +) + +lightstep_cc_library( + name = "in_memory_collector_lib", + private_hdrs = [ + "in_memory_collector.h", + ], + srcs = [ + "in_memory_collector.cpp", + ], + deps = [ + "//src/recorder/grpc_transporter:collector_proto_grpc", + ], +) + +cc_binary( + name = "lightstep_plugin.so", + linkshared = 1, + deps = [ + "//:tracer_lib", + ], +) + +lightstep_catch_test( + name = "dynamic_load_test", + srcs = [ + "dynamic_load_test.cpp", + ], + data = [ + ":lightstep_plugin.so", + ], + deps = [ + ":in_memory_collector_lib", + ], + external_deps = [ + "@io_opentracing_cpp//:opentracing", + ], +) diff --git a/test/dynamic_load_test.cpp b/test/tracer/dynamic_load_test.cpp similarity index 68% rename from test/dynamic_load_test.cpp rename to test/tracer/dynamic_load_test.cpp index 95814a2d..7c266a16 100644 --- a/test/dynamic_load_test.cpp +++ b/test/tracer/dynamic_load_test.cpp @@ -1,23 +1,23 @@ -#include -#include "in_memory_collector.h" +#include "opentracing/dynamic_load.h" #include #include #include -#define CATCH_CONFIG_RUNNER -#include +#include "test/tracer/in_memory_collector.h" + +#include "3rd_party/catch2/catch.hpp" + using namespace lightstep; -static std::string lightstep_library; const char* const server_address = "0.0.0.0:50051"; TEST_CASE("dynamic_load") { std::string error_message; auto handle_maybe = opentracing::DynamicallyLoadTracingLibrary( - lightstep_library.c_str(), error_message); - REQUIRE(error_message == ""); + "test/tracer/lightstep_plugin.so", error_message); + REQUIRE(error_message.empty()); REQUIRE(handle_maybe); auto& tracer_factory = handle_maybe->tracer_factory(); @@ -52,7 +52,7 @@ TEST_CASE("dynamic_load") { "collector_plaintext": true })"; auto tracer_maybe = tracer_factory.MakeTracer(config, error_message); - REQUIRE(error_message == ""); + REQUIRE(error_message.empty()); REQUIRE(tracer_maybe); auto& tracer = *tracer_maybe; @@ -63,24 +63,3 @@ TEST_CASE("dynamic_load") { CHECK(spans.size() == 1); } } - -int main(int argc, char* argv[]) { - Catch::Session session; - - using namespace Catch::clara; - auto cli = session.cli() | - Opt(lightstep_library, "lightstep_library")["--lightstep_library"]; - - session.cli(cli); - int rcode = session.applyCommandLine(argc, argv); - if (rcode != 0) { - return rcode; - } - - if (lightstep_library.empty()) { - std::cerr << "Must provide lightstep_library!\n"; - return -1; - } - - return session.run(); -} diff --git a/test/in_memory_collector.cpp b/test/tracer/in_memory_collector.cpp similarity index 92% rename from test/in_memory_collector.cpp rename to test/tracer/in_memory_collector.cpp index 5febf5a0..cb706bfc 100644 --- a/test/in_memory_collector.cpp +++ b/test/tracer/in_memory_collector.cpp @@ -1,4 +1,4 @@ -#include "in_memory_collector.h" +#include "test/tracer/in_memory_collector.h" namespace lightstep { grpc::Status InMemoryCollector::Report( diff --git a/test/in_memory_collector.h b/test/tracer/in_memory_collector.h similarity index 99% rename from test/in_memory_collector.h rename to test/tracer/in_memory_collector.h index e544e9b0..3b886561 100644 --- a/test/in_memory_collector.h +++ b/test/tracer/in_memory_collector.h @@ -2,6 +2,7 @@ #include #include + #include "lightstep-tracer-common/collector.grpc.pb.h" namespace lightstep { diff --git a/test/propagation_test.cpp b/test/tracer/propagation_test.cpp similarity index 95% rename from test/propagation_test.cpp rename to test/tracer/propagation_test.cpp index 8f629ede..08df4af6 100644 --- a/test/propagation_test.cpp +++ b/test/tracer/propagation_test.cpp @@ -1,19 +1,18 @@ #include -#include -#include #include #include #include #include #include #include -#include "../src/lightstep_immutable_span_context.h" -#include "../src/lightstep_tracer_impl.h" -#include "../src/utility.h" -#include "in_memory_recorder.h" -#define CATCH_CONFIG_MAIN -#include +#include "3rd_party/catch2/catch.hpp" +#include "common/utility.h" +#include "lightstep/binary_carrier.h" +#include "lightstep/tracer.h" +#include "test/recorder/in_memory_recorder.h" +#include "tracer/lightstep_immutable_span_context.h" +#include "tracer/lightstep_tracer_impl.h" using namespace lightstep; @@ -21,7 +20,8 @@ using namespace lightstep; // TextMapCarrier //------------------------------------------------------------------------------ struct TextMapCarrier : opentracing::TextMapReader, opentracing::TextMapWriter { - TextMapCarrier(std::unordered_map& text_map_) + explicit TextMapCarrier( + std::unordered_map& text_map_) : text_map(text_map_) {} opentracing::expected Set( @@ -40,9 +40,8 @@ struct TextMapCarrier : opentracing::TextMapReader, opentracing::TextMapWriter { auto iter = text_map.find(key); if (iter != text_map.end()) { return opentracing::string_view{iter->second}; - } else { - return opentracing::make_unexpected(opentracing::key_not_found_error); } + return opentracing::make_unexpected(opentracing::key_not_found_error); } opentracing::expected ForeachKey( @@ -52,7 +51,9 @@ struct TextMapCarrier : opentracing::TextMapReader, opentracing::TextMapWriter { ++foreach_key_call_count; for (const auto& key_value : text_map) { auto result = f(key_value.first, key_value.second); - if (!result) return result; + if (!result) { + return result; + } } return {}; } @@ -67,7 +68,8 @@ struct TextMapCarrier : opentracing::TextMapReader, opentracing::TextMapWriter { //------------------------------------------------------------------------------ struct HTTPHeadersCarrier : opentracing::HTTPHeadersReader, opentracing::HTTPHeadersWriter { - HTTPHeadersCarrier(std::unordered_map& text_map_) + explicit HTTPHeadersCarrier( + std::unordered_map& text_map_) : text_map(text_map_) {} opentracing::expected Set( @@ -83,7 +85,9 @@ struct HTTPHeadersCarrier : opentracing::HTTPHeadersReader, f) const override { for (const auto& key_value : text_map) { auto result = f(key_value.first, key_value.second); - if (!result) return result; + if (!result) { + return result; + } } return {}; } diff --git a/test/tracer_test.cpp b/test/tracer/tracer_test.cpp similarity index 80% rename from test/tracer_test.cpp rename to test/tracer/tracer_test.cpp index 13143797..d277b975 100644 --- a/test/tracer_test.cpp +++ b/test/tracer/tracer_test.cpp @@ -1,13 +1,13 @@ -#include #include #include -#include "../src/lightstep_tracer_impl.h" -#include "../src/utility.h" -#include "in_memory_recorder.h" -#include "utility.h" -#define CATCH_CONFIG_MAIN -#include +#include "3rd_party/catch2/catch.hpp" +#include "common/utility.h" +#include "lightstep/tracer.h" +#include "test/recorder/in_memory_recorder.h" +#include "test/utility.h" +#include "tracer/lightstep_tracer_impl.h" + using namespace lightstep; using namespace opentracing; @@ -132,9 +132,9 @@ TEST_CASE("tracer") { auto noop_span = noop_tracer->StartSpan("noop"); CHECK(noop_span); StartSpanOptions options; - options.references.push_back( + options.references.emplace_back( std::make_pair(SpanReferenceType::ChildOfRef, &noop_span->context())); - options.references.push_back( + options.references.emplace_back( std::make_pair(SpanReferenceType::ChildOfRef, nullptr)); auto span = tracer->StartSpanWithOptions("a", options); CHECK(span); @@ -184,3 +184,34 @@ TEST_CASE("tracer") { CHECK(recorder->top().logs().size() == 1); } } + +TEST_CASE("Configuration validation") { + LightStepTracerOptions options; + + // Don't make connections to the outside network; there's nothing listening at + // these addresses but that's ok since we don't need to send spans with this + // test. + options.collector_host = "localhost"; + options.collector_port = 1234; + options.satellite_endpoints = {{"localhost", 1234}}; + + SECTION( + "Constructing the streaming tracer errors if plaintext isn't " + "specified.") { + options.collector_plaintext = false; + options.use_stream_recorder = true; + REQUIRE(MakeLightStepTracer(std::move(options)) == nullptr); + } + + SECTION("The streaming recorder only supports a threaded mode.") { + options.use_stream_recorder = true; + options.use_thread = false; + REQUIRE(MakeLightStepTracer(std::move(options)) == nullptr); + } + + SECTION("The streaming recorder doesn't support custom transporters.") { + options.use_stream_recorder = true; + options.transporter.reset(new Transporter{}); + REQUIRE(MakeLightStepTracer(std::move(options)) == nullptr); + } +} diff --git a/test/utility.cpp b/test/utility.cpp index 8165b6b0..8884e38f 100644 --- a/test/utility.cpp +++ b/test/utility.cpp @@ -1,9 +1,13 @@ -#include "../src/utility.h" +#include "test/utility.h" + #include #include +#include #include #include -#include "utility.h" + +#include "common/utility.h" +#include "network/socket.h" namespace lightstep { //------------------------------------------------------------------------------ @@ -65,4 +69,47 @@ bool HasRelationship(opentracing::SpanReferenceType relationship, other); }); } + +//-------------------------------------------------------------------------------------------------- +// CanConnect +//-------------------------------------------------------------------------------------------------- +bool CanConnect(uint16_t port) noexcept try { + Socket socket{}; + socket.SetReuseAddress(); + IpAddress ip_address{"127.0.0.1", port}; + return socket.Connect(ip_address.addr(), sizeof(ip_address.ipv4_address())) == + 0; +} catch (...) { + return false; +} + +//-------------------------------------------------------------------------------------------------- +// ToString +//-------------------------------------------------------------------------------------------------- +std::string ToString(const FragmentInputStream& fragment_input_stream) { + std::string result; + fragment_input_stream.ForEachFragment([&result](void* data, int size) { + result.append(static_cast(data), static_cast(size)); + return true; + }); + return result; +} + +std::string ToString(const CircularBufferConstPlacement& placement) { + std::string result; + result.append(placement.data1, placement.size1); + result.append(placement.data2, placement.size2); + return result; +} + +//-------------------------------------------------------------------------------------------------- +// AddString +//-------------------------------------------------------------------------------------------------- +bool AddString(ChunkCircularBuffer& buffer, opentracing::string_view s) { + return buffer.Add( + [s](google::protobuf::io::CodedOutputStream& stream) { + stream.WriteRaw(s.data(), s.size()); + }, + s.size()); +} } // namespace lightstep diff --git a/test/utility.h b/test/utility.h index 204612e5..e9913fc0 100644 --- a/test/utility.h +++ b/test/utility.h @@ -1,5 +1,11 @@ #pragma once +#include +#include + +#include "common/chunk_circular_buffer.h" +#include "network/fragment_input_stream.h" + #include #include "lightstep-tracer-common/collector.pb.h" @@ -12,4 +18,59 @@ bool HasTag(const collector::Span& span, opentracing::string_view key, bool HasRelationship(opentracing::SpanReferenceType relationship, const collector::Span& span_a, const collector::Span& span_b); + +/** + * Determines whether we can connect to a given localhost port. + * @param port supplies the port to test for connectivity. + * @return true if we can connect to localhost:port. + */ +bool CanConnect(uint16_t port) noexcept; + +/** + * Continually polls a provied functor until either it returns true or a timeout + * is reached. + * @param f supplies the functor to poll. + * @param polling_interval supplies the amount of time between polls. + * @param timeout supplies the timeout. + * @return true if the functor returned true within the timeout; false, + * otherwise. + */ +template +inline bool IsEventuallyTrue( + F f, Duration1 polling_interval = std::chrono::milliseconds{50}, + Duration2 timeout = std::chrono::minutes{2}) { + auto start = std::chrono::system_clock::now(); + while (true) { + if (f()) { + return true; + } + std::this_thread::sleep_for(polling_interval); + if (std::chrono::system_clock::now() - start > timeout) { + return false; + } + } +} + +/** + * Builds a string concatenating all the pieces of a fragment set. + * @param fragment_input_stream supplies the fragments to concatenate. + * @return a string concatenating all the fragments. + */ +std::string ToString(const FragmentInputStream& fragment_input_stream); + +/** + * Converts a CircularBufferConstPlacement to a string. + * @param placement the placement to convert. + * @return a string with the contents of placement. + */ +std::string ToString(const CircularBufferConstPlacement& placement); + +/** + * Adds a string to a ChunkCircularBuffer. + * @param buffer the buffer to add the string to. + * @param s the string to add. + * @return true if the string was succesfully added. + */ +bool AddString(ChunkCircularBuffer& buffer, opentracing::string_view s); } // namespace lightstep diff --git a/test/zero_copy_connection_input_stream.cpp b/test/zero_copy_connection_input_stream.cpp new file mode 100644 index 00000000..1aca1ea0 --- /dev/null +++ b/test/zero_copy_connection_input_stream.cpp @@ -0,0 +1,117 @@ +#include "test/zero_copy_connection_input_stream.h" + +#include +#include + +static thread_local std::mt19937 RandomNumberGenerator{std::random_device{}()}; + +namespace lightstep { +//-------------------------------------------------------------------------------------------------- +// CountBytes +//-------------------------------------------------------------------------------------------------- +static int CountBytes(std::initializer_list streams) { + int result = 0; + for (auto stream : streams) { + stream->ForEachFragment([&result](void* /*data*/, int size) { + result += size; + return true; + }); + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +// CopyN +//-------------------------------------------------------------------------------------------------- +static void CopyN(std::initializer_list streams, int n, + std::string& s) { + for (auto stream : streams) { + auto should_continue = + stream->ForEachFragment([&n, &s](void* data, int size) { + if (n <= size) { + s.append(static_cast(data), n); + return false; + } + s.append(static_cast(data), size); + n -= size; + return true; + }); + if (!should_continue) { + return; + } + } +} + +//-------------------------------------------------------------------------------------------------- +// constructor +//-------------------------------------------------------------------------------------------------- +ZeroCopyConnectionInputStream::ZeroCopyConnectionInputStream( + ConnectionStream& stream) + : stream_{stream} {} + +//-------------------------------------------------------------------------------------------------- +// Next +//-------------------------------------------------------------------------------------------------- +bool ZeroCopyConnectionInputStream::Next(const void** data, int* size) { + if (position_ == static_cast(buffer_.size())) { + SetBuffer(); + } + if (stream_.completed() && buffer_.empty()) { + return false; + } + *data = static_cast(buffer_.data() + position_); + *size = static_cast(buffer_.size()) - position_; + position_ = static_cast(buffer_.size()); + byte_count_ += *size; + return true; +} + +//-------------------------------------------------------------------------------------------------- +// BackUp +//-------------------------------------------------------------------------------------------------- +void ZeroCopyConnectionInputStream::BackUp(int count) { + assert(position_ >= count); + byte_count_ -= count; + position_ -= count; +} + +//-------------------------------------------------------------------------------------------------- +// Skip +//-------------------------------------------------------------------------------------------------- +bool ZeroCopyConnectionInputStream::Skip(int count) { + while (true) { + if (stream_.completed() && buffer_.empty()) { + return false; + } + if (count == 0) { + return true; + } + if (position_ == static_cast(buffer_.size())) { + SetBuffer(); + } + auto delta = static_cast(buffer_.size()) - position_; + auto n = std::min(count, delta); + position_ += n; + byte_count_ += n; + count -= n; + } +} + +//-------------------------------------------------------------------------------------------------- +// SetBuffer +//-------------------------------------------------------------------------------------------------- +void ZeroCopyConnectionInputStream::SetBuffer() { + buffer_.clear(); + position_ = 0; + stream_.Flush([this](std::initializer_list streams) { + auto num_bytes = CountBytes(streams); + if (num_bytes == 0) { + return true; + } + std::uniform_int_distribution distribution{0, num_bytes}; + auto n = distribution(RandomNumberGenerator); + CopyN(streams, n, buffer_); + return Consume(streams, n); + }); +} +} // namespace lightstep diff --git a/test/zero_copy_connection_input_stream.h b/test/zero_copy_connection_input_stream.h new file mode 100644 index 00000000..2f8c0bbb --- /dev/null +++ b/test/zero_copy_connection_input_stream.h @@ -0,0 +1,36 @@ +#pragma once + +#include "recorder/stream_recorder/connection_stream.h" + +#include + +namespace lightstep { +/** + * Wraps a ConnectionStream to expose a ZeroCopyInputStream interface. + * + * Reads bytes from the ConnectionStream in random amounts so as to simulate + * different scenarios for testing. + */ +class ZeroCopyConnectionInputStream + : public google::protobuf::io::ZeroCopyInputStream { + public: + explicit ZeroCopyConnectionInputStream(ConnectionStream& stream); + + // google::protobuf::io::ZeroCopyInputStream + bool Next(const void** data, int* size) override; + + void BackUp(int count) override; + + bool Skip(int count) override; + + google::protobuf::int64 ByteCount() const override { return byte_count_; } + + private: + ConnectionStream& stream_; + std::string buffer_; + google::protobuf::int64 byte_count_{0}; + int position_{0}; + + void SetBuffer(); +}; +} // namespace lightstep