diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e8aeed756..26f331ea48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -200,9 +200,11 @@ endif() include(CMakePackageConfigHelpers) include_directories(api/include) -add_subdirectory(api) include_directories(sdk/include) include_directories(sdk) +include_directories(ext/include) + +add_subdirectory(api) add_subdirectory(sdk) add_subdirectory(exporters) if(WITH_EXAMPLES) diff --git a/ext/test/CMakeLists.txt b/ext/test/CMakeLists.txt index d12e56796b..7170e8c0c6 100644 --- a/ext/test/CMakeLists.txt +++ b/ext/test/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(zpages) add_subdirectory(http) +add_subdirectory(w3c_tracecontext_test) diff --git a/ext/test/w3c_tracecontext_test/BUILD b/ext/test/w3c_tracecontext_test/BUILD new file mode 100644 index 0000000000..916eb0d65c --- /dev/null +++ b/ext/test/w3c_tracecontext_test/BUILD @@ -0,0 +1,26 @@ +cc_binary( + name = "w3c_tracecontext_test", + srcs = [ + "main.cc", + ], + # TODO: Move copts/linkopts for static CURL usage into shared bzl file. + copts = [ + "-DCURL_STATICLIB", + ], + linkopts = select({ + "//bazel:windows": [ + "-DEFAULTLIB:advapi32.lib", + "-DEFAULTLIB:crypt32.lib", + "-DEFAULTLIB:Normaliz.lib", + ], + "//conditions:default": [], + }), + deps = [ + "//api", + "//exporters/ostream:ostream_span_exporter", + "//ext:headers", + "//sdk/src/trace", + "@curl", + "@github_nlohmann_json//:json", + ], +) diff --git a/ext/test/w3c_tracecontext_test/CMakeLists.txt b/ext/test/w3c_tracecontext_test/CMakeLists.txt new file mode 100644 index 0000000000..124a6beb60 --- /dev/null +++ b/ext/test/w3c_tracecontext_test/CMakeLists.txt @@ -0,0 +1,16 @@ +include_directories(${CMAKE_SOURCE_DIR}/exporters/ostream/include) + +find_package(CURL) +find_package(nlohmann_json) + +if(NOT CURL_FOUND) + message(WARNING "Skipping example_w3c_tracecontext_test: CURL not found") +elseif(NOT nlohmann_json_FOUND) + message( + WARNING "Skipping example_w3c_tracecontext_test: nlohmann_json not found") +else() + add_executable(w3c_tracecontext_test main.cc) + target_link_libraries( + w3c_tracecontext_test ${CMAKE_THREAD_LIBS_INIT} opentelemetry_trace + opentelemetry_exporter_ostream_span ${CURL_LIBRARIES}) +endif() diff --git a/ext/test/w3c_tracecontext_test/Dockerfile b/ext/test/w3c_tracecontext_test/Dockerfile new file mode 100644 index 0000000000..1deac123c7 --- /dev/null +++ b/ext/test/w3c_tracecontext_test/Dockerfile @@ -0,0 +1,8 @@ +FROM python + +RUN pip install aiohttp +RUN git clone https://github.com/w3c/trace-context + +WORKDIR ./trace-context/test + +ENTRYPOINT [ "python", "test.py" ] diff --git a/ext/test/w3c_tracecontext_test/README.md b/ext/test/w3c_tracecontext_test/README.md new file mode 100644 index 0000000000..0a4abd51a6 --- /dev/null +++ b/ext/test/w3c_tracecontext_test/README.md @@ -0,0 +1,32 @@ +# Test service endpoint for W3C validation + +This test application is intended to be used as a test service for the [W3C Distributed Tracing Validation Service](https://github.com/w3c/trace-context/tree/master/test). +It is implemented according to [this instructions](https://github.com/w3c/trace-context/tree/master/test#implement-test-service). + +## Usage + +1. Build and start the test service endpoint: + ```sh + $ ./w3c_tracecontext_test + Listening to http://localhost:30000/test + ``` + A custom port number for the test service to listen to can be specified: + ```sh + $ ./w3c_tracecontext_test 31339 + Listening to http://localhost:31339/test + ``` + The test service will print the full URI that the validation service can connect to. +2. In a different terminal, set up and start the validation service according to + the [instructions](https://github.com/w3c/trace-context/tree/master/test#run-test-cases), + giving the address of the test service endpoint as argument: + ```sh + $ python test.py http://localhost:31339/test + ``` + One can also use the `Dockerfile` provided in this folder to conveniently + run the validation service: + ```sh + $ docker build --tag w3c_driver . + $ docker run --network host w3c_driver http://localhost:31339/test + ``` +3. The validation service will run the test suite and print detailed test results. +4. Stop the test service by pressing enter. diff --git a/ext/test/w3c_tracecontext_test/main.cc b/ext/test/w3c_tracecontext_test/main.cc new file mode 100644 index 0000000000..722d20acae --- /dev/null +++ b/ext/test/w3c_tracecontext_test/main.cc @@ -0,0 +1,187 @@ +#include "opentelemetry/context/runtime_context.h" +#include "opentelemetry/exporters/ostream/span_exporter.h" +#include "opentelemetry/ext/http/client/curl/http_client_curl.h" +#include "opentelemetry/ext/http/server/http_server.h" +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/trace/propagation/http_trace_context.h" +#include "opentelemetry/trace/provider.h" +#include "opentelemetry/trace/scope.h" + +#include +#include "nlohmann/json.hpp" + +namespace +{ +static opentelemetry::trace::propagation::HttpTraceContext> + propagator_format; + +void Setter(std::map &carrier, + nostd::string_view trace_type = "traceparent", + nostd::string_view trace_description = "") +{ + carrier[std::string(trace_type)] = std::string(trace_description); +} + +nostd::string_view Getter(const std::map &carrier, + nostd::string_view trace_type = "traceparent") +{ + auto it = carrier.find(std::string(trace_type)); + if (it != carrier.end()) + { + return nostd::string_view(it->second); + } + return ""; +} + +void initTracer() +{ + auto exporter = std::unique_ptr( + new opentelemetry::exporter::trace::OStreamSpanExporter); + auto processor = std::shared_ptr( + new sdktrace::SimpleSpanProcessor(std::move(exporter))); + auto provider = nostd::shared_ptr( + new sdktrace::TracerProvider(processor)); + // Set the global trace provider + opentelemetry::trace::Provider::SetTracerProvider(provider); +} + +nostd::shared_ptr get_tracer() +{ + auto provider = opentelemetry::trace::Provider::GetTracerProvider(); + return provider->GetTracer("w3c_tracecontext_test"); +} + +struct Uri +{ + std::string host; + uint16_t port; + std::string path; + + Uri(std::string uri) + { + size_t host_end = uri.substr(7, std::string::npos).find(":"); + size_t port_end = uri.substr(host_end + 1, std::string::npos).find("/"); + + host = uri.substr(0, host_end + 7); + port = std::stoi(uri.substr(7 + host_end + 1, port_end)); + path = uri.substr(host_end + port_end + 2, std::string::npos); + } +}; + +// A noop event handler for making HTTP requests. We don't care about response bodies and error +// messages. +class NoopEventHandler : public opentelemetry::ext::http::client::EventHandler +{ +public: + void OnEvent(opentelemetry::ext::http::client::SessionState state, + opentelemetry::nostd::string_view reason) noexcept override + {} + + void OnConnecting(const opentelemetry::ext::http::client::SSLCertificate &) noexcept override {} + + void OnResponse(opentelemetry::ext::http::client::Response &response) noexcept override {} +}; +} // namespace + +// Sends an HTTP POST request to the given url, with the given body. +void send_request(opentelemetry::ext::http::client::curl::SessionManager &client, + const std::string &url, + const std::string &body) +{ + static std::unique_ptr handler( + new NoopEventHandler()); + + auto request_span = get_tracer()->StartSpan(__func__); + opentelemetry::trace::Scope scope(request_span); + + Uri uri{url}; + + auto session = client.CreateSession(uri.host, uri.port); + auto request = session->CreateRequest(); + + request->SetMethod(opentelemetry::ext::http::client::Method::Post); + request->SetUri(uri.path); + opentelemetry::ext::http::client::Body b = {body.c_str(), body.c_str() + body.size()}; + request->SetBody(b); + request->AddHeader("Content-Type", "application/json"); + request->AddHeader("Content-Length", std::to_string(body.size())); + + std::map headers; + propagator_format.Inject(Setter, headers, opentelemetry::context::RuntimeContext::GetCurrent()); + + for (auto const &hdr : headers) + { + request->AddHeader(hdr.first, hdr.second); + } + + session->SendRequest(*handler); + session->FinishSession(); +} + +// This application receives requests from the W3C test service. Each request has a JSON body which +// consists of an array of objects, each containing an URL to which to post to, and arguments which +// need to be used as body when posting to the given URL. +int main(int argc, char *argv[]) +{ + initTracer(); + + constexpr char default_host[] = "localhost"; + constexpr uint16_t default_port = 30000; + uint16_t port; + + // The port the validation service listens to can be specified via the command line. + if (argc > 1) + { + port = atoi(argv[1]); + } + else + { + port = default_port; + } + + auto root_span = get_tracer()->StartSpan(__func__); + opentelemetry::trace::Scope scope(root_span); + + testing::HttpServer server(default_host, port); + opentelemetry::ext::http::client::curl::SessionManager client; + + testing::HttpRequestCallback test_cb{ + [&](testing::HttpRequest const &req, testing::HttpResponse &resp) { + auto body = nlohmann::json::parse(req.content); + + std::cout << "Received request with body :\n" << req.content << "\n"; + + for (auto &part : body) + { + auto current_ctx = opentelemetry::context::RuntimeContext::GetCurrent(); + auto ctx = propagator_format.Extract(Getter, req.headers, current_ctx); + auto token = opentelemetry::context::RuntimeContext::Attach(ctx); + + auto url = part["url"].get(); + auto arguments = part["arguments"].dump(); + + std::cout << " Sending request to " << url << "\n"; + + send_request(client, url, arguments); + } + + std::cout << "\n"; + + resp.code = 200; + return 0; + }}; + + server["/test"] = test_cb; + + // Start server + server.start(); + + std::cout << "Listening at http://" << default_host << ":" << port << "/test\n"; + + // Wait for console input + std::cin.get(); + + // Stop server + server.stop(); +}