diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 19486afb76..dd464197a9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -28,6 +28,7 @@ add_subdirectory(metrics_simple) add_subdirectory(multithreaded) add_subdirectory(multi_processor) add_subdirectory(environment_carrier) +add_subdirectory(async) if(WITH_EXAMPLES_HTTP) add_subdirectory(http) diff --git a/examples/async/BUILD b/examples/async/BUILD new file mode 100644 index 0000000000..681b648aeb --- /dev/null +++ b/examples/async/BUILD @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") + +cc_binary( + name = "example_async", + srcs = [ + "main.cc", + ], + tags = ["ostream"], + deps = [ + "//api", + "//exporters/ostream:ostream_span_exporter", + "//sdk/src/trace", + ], +) diff --git a/examples/async/CMakeLists.txt b/examples/async/CMakeLists.txt new file mode 100644 index 0000000000..7c9d3ef86d --- /dev/null +++ b/examples/async/CMakeLists.txt @@ -0,0 +1,11 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +add_executable(example_async main.cc) +target_link_libraries( + example_async PRIVATE opentelemetry-cpp::trace + opentelemetry-cpp::ostream_span_exporter) + +if(BUILD_TESTING) + add_test(NAME examples.async COMMAND "$") +endif() diff --git a/examples/async/README.md b/examples/async/README.md new file mode 100644 index 0000000000..baa6138a82 --- /dev/null +++ b/examples/async/README.md @@ -0,0 +1,177 @@ +# OpenTelemetry C++ ASYNC Example + +This is a simple example that demonstrates tracing async requests between +generic clients and servers. It simulates context injections and extractions +and shows how to explicitly set parent span_id's in different situations, when +the scope is not available, or different threads are run in parallel. + +The proposed pattern may be used for any kind of asynchronous client/server +communication. + +## Running the example + +Build and Deploy this opentelemetry-cpp example as described in [INSTALL.md](../../INSTALL.md). + +## Example Flow + +* This example creates 2 asynchronous requests from a theoretical client. The +requests have distributed tracing context `injected` into a map, simulating +headers. + +* These requests then arrive to a server, which `extracts` the span information +and creates child spans sharing the same trace id's with the client request, +and marking it as parent. After that, other spans are `nested`, simulating +server work. + +* Answers contain again the context `injected`, and the client extracts them +without more context than the headers arriving from the + +* The Parent spans that originated the only 2 `trace-id`'s of this example are +kept alive during the whole example, and ended at the end. + +```text +[Client] [Server] + +request1 span ──────────→ server span + └─→ nested span +request2 span ──────────→ server span + └─→ nested + ←────────── reply +process_answer + ←────────── reply +process_answer +``` + +## Span Hierarchy + +Only 2 traces are generated in this example. Each one contains 4 spans in +total, created across both a server and a client. The Span hierarchy is +shown below: + +```text +request1 (trace: 29656cc8..., in client) +└─→ server (in server) + ├─→ nested (in server) + └─→ answer (in client) + +request2 (trace: 24a0afe3..., in client) +└─→ server + ├─→ nested (in server) + └─→ answer (in client) +``` + +## Auxiliary functions + +This example provides a small set of auxiliary functions that are able to +create child spans from a previous one, independently on the active span. + +It's worth noting that `shared_ptr`'s are used because it helps for keeping +spans alive and passing them across different lambda functions or execution +scopes for more complex use cases. These functions are: + +### Creating Child Spans + +An auxiliary function that returns a child span for a desired arbitrary parent. +In this example, the `SpanKind` and `name` are also passed from outside the +function. + +```cpp +nostd::shared_ptr create_child_span( + const std::string &name, + const nostd::shared_ptr &parent, + trace_api::SpanKind kind) +{ + trace_api::StartSpanOptions opts; + opts.kind = kind; + if (parent) + { + opts.parent = parent->GetContext(); + } + + auto span = get_tracer()->StartSpan(name, opts); + return span; +} +``` + +### Creating Child Spans from incoming requests + +An auxiliary function that returns a child span from incoming headers that +contain tracing information. In this example, the `SpanKind` and `name` are +also passed from outside the function. + +```cpp +nostd::shared_ptr create_child_span_from_remote(header_map &headers, + const std::string &name, + trace_api::SpanKind kind) +{ + HttpTextMapCarrier carrier(headers); + auto current_ctx = ctx::RuntimeContext::GetCurrent(); + auto new_context = ctx::propagation::GlobalTextMapPropagator::GetGlobalPropagator()->Extract( + carrier, current_ctx); + auto remote_span = opentelemetry::trace::GetSpan(new_context); + + return create_child_span(name, remote_span, kind); +} + +``` + +### Injecting arbitrary spans into carriers (i.e. headers) + +An auxiliary function that `injects` the tracing information to a given carrier +structure. In this example a simple implementation of the `TextMapCarrier` as +an `std::map` was used. + +It's important to note that the `trace_api::SetSpan(current_ctx, span)` +function is needed for the injection to explicitly use the desired span +context. + +```cpp +void inject_trace_context(const nostd::shared_ptr &span, header_map &headers) +{ + if (!span) + { + return; + } + + // First, we set the Span into the context explicitly + auto current_ctx = ctx::RuntimeContext::GetCurrent(); + auto ctx_with_span = trace_api::SetSpan(current_ctx, span); + + // Then we inject the span info into the headers + HttpTextMapCarrier carrier(headers); + ctx::propagation::GlobalTextMapPropagator::GetGlobalPropagator()->Inject(carrier, ctx_with_span); +} +``` + +The previous examples make use of the following aliases: + +```cpp +namespace trace_api = opentelemetry::trace; +namespace trace_sdk = opentelemetry::sdk::trace; +namespace nostd = opentelemetry::nostd; +namespace ctx = opentelemetry::context; +using header_map = std::map; +``` + +## Output + +The output will be a set of spans, in which you can check how the relationships +outlined in the previous diagrams are fulfilled. For example: + +### Trace 1 `29656cc8079bff4fe30bdb96b0f24bef` + +| name | parent_span_id | span_id | events | +|------|----------------|---------|--------| +| request1 | 0000000000000000 | f026e45ec526047e | _(none)_ | +| server | f026e45ec526047e | d701525271ff5d72 | Processing in server, Replying answer | +| nested | d701525271ff5d72 | c6aaee683b544ec3 | Nested did some work | +| process_answer | d701525271ff5d72 | fedcd1e2a22a1916 | Answer processed | + +### Trace 2 `24a0afe30c007794d43dac03c4b7c956` + +| name | parent_span_id | span_id | events | +|------|----------------|---------|--------| +| request2 | 0000000000000000 | a038787bb5eb6f1b | _(none)_ | +| server | a038787bb5eb6f1b | 529c7df6581d9279 | Processing in server, Replying answer | +| nested | 529c7df6581d9279 | d4946bb9738ae08b | Nested did some work | +| process_answer | 529c7df6581d9279 | 2b396d7d207addca | Answer processed | diff --git a/examples/async/main.cc b/examples/async/main.cc new file mode 100644 index 0000000000..a671bd39e8 --- /dev/null +++ b/examples/async/main.cc @@ -0,0 +1,232 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include +#include + +#include "opentelemetry/context/propagation/global_propagator.h" +#include "opentelemetry/context/propagation/text_map_propagator.h" +#include "opentelemetry/context/runtime_context.h" +#include "opentelemetry/exporters/ostream/span_exporter_factory.h" +#include "opentelemetry/nostd/shared_ptr.h" +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/sdk/resource/resource.h" +#include "opentelemetry/sdk/trace/exporter.h" +#include "opentelemetry/sdk/trace/processor.h" +#include "opentelemetry/sdk/trace/provider.h" +#include "opentelemetry/sdk/trace/simple_processor_factory.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/sdk/trace/tracer_provider_factory.h" +#include "opentelemetry/trace/context.h" +#include "opentelemetry/trace/propagation/http_trace_context.h" +#include "opentelemetry/trace/provider.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/tracer.h" +#include "opentelemetry/trace/tracer_provider.h" + +namespace trace_api = opentelemetry::trace; +namespace trace_sdk = opentelemetry::sdk::trace; +namespace nostd = opentelemetry::nostd; +namespace ctx = opentelemetry::context; + +using header_map = std::map; + +namespace +{ +void InitTracer() +{ + auto exporter = opentelemetry::exporter::trace::OStreamSpanExporterFactory::Create(); + auto processor = trace_sdk::SimpleSpanProcessorFactory::Create(std::move(exporter)); + std::shared_ptr provider = + trace_sdk::TracerProviderFactory::Create(std::move(processor), + opentelemetry::sdk::resource::Resource::Create({})); + // Set the global trace provider + trace_sdk::Provider::SetTracerProvider(provider); + + ctx::propagation::GlobalTextMapPropagator::SetGlobalPropagator( + nostd::shared_ptr( + new trace_api::propagation::HttpTraceContext())); +} + +void CleanupTracer() +{ + std::shared_ptr none; + trace_sdk::Provider::SetTracerProvider(none); +} + +nostd::shared_ptr get_tracer() +{ + auto provider = trace_api::Provider::GetTracerProvider(); + return provider->GetTracer("foo_library"); +} +} // namespace + +namespace utils +{ + +nostd::shared_ptr create_span(const std::string &name, trace_api::SpanKind kind) +{ + trace_api::StartSpanOptions opts; + opts.kind = kind; + + auto span = get_tracer()->StartSpan(name, opts); + return span; +} + +nostd::shared_ptr create_child_span( + const std::string &name, + const nostd::shared_ptr &parent, + trace_api::SpanKind kind) +{ + trace_api::StartSpanOptions opts; + opts.kind = kind; + if (parent) + { + opts.parent = parent->GetContext(); + } + + auto span = get_tracer()->StartSpan(name, opts); + return span; +} + +class HttpTextMapCarrier : public ctx::propagation::TextMapCarrier +{ +public: + HttpTextMapCarrier(header_map &headers) : headers_(headers) {} + + nostd::string_view Get(nostd::string_view key) const noexcept override + { + auto it = headers_.find(std::string(key.data(), key.size())); + if (it != headers_.end()) + { + return it->second; + } + return ""; + } + + void Set(nostd::string_view k, nostd::string_view v) noexcept override + { + headers_.emplace(std::string(k.data(), k.size()), std::string(v.data(), v.size())); + } + + header_map &headers_; +}; + +void inject_trace_context(const nostd::shared_ptr &span, header_map &headers) +{ + if (!span) + { + return; + } + + // First, we set the Span into the context explicitly + auto current_ctx = ctx::RuntimeContext::GetCurrent(); + auto ctx_with_span = trace_api::SetSpan(current_ctx, span); + + // Then we inject the span info into the headers + HttpTextMapCarrier carrier(headers); + ctx::propagation::GlobalTextMapPropagator::GetGlobalPropagator()->Inject(carrier, ctx_with_span); +} + +nostd::shared_ptr create_child_span_from_remote(header_map &headers, + const std::string &name, + const trace_api::SpanKind kind) +{ + HttpTextMapCarrier carrier(headers); + auto current_ctx = ctx::RuntimeContext::GetCurrent(); + auto new_context = ctx::propagation::GlobalTextMapPropagator::GetGlobalPropagator()->Extract( + carrier, current_ctx); + auto remote_span = opentelemetry::trace::GetSpan(new_context); + + return create_child_span(name, remote_span, kind); +} + +} // namespace utils + +namespace client +{ + +void process_answer(header_map &remote_headers) +{ + // The only context we have here is the headers + auto remote = utils::create_child_span_from_remote(remote_headers, "process_answer", + trace_api::SpanKind::kClient); + remote->AddEvent("Answer processed"); + remote->SetStatus(trace_api::StatusCode::kOk); + // this span is automatically ended on exit +} + +} // namespace client + +namespace server +{ + +header_map process_request(header_map &remote_headers) +{ + // Extract remote context and create server-side child span + auto server_span = + utils::create_child_span_from_remote(remote_headers, "server", trace_api::SpanKind::kServer); + + // Simulating work with nested spans + server_span->AddEvent("Processing in server"); + auto nested = utils::create_child_span("nested", server_span, trace_api::SpanKind::kServer); + nested->AddEvent("Nested did some work"); + nested->SetStatus(trace_api::StatusCode::kOk); + nested->End(); + + // Filling answer headers + header_map answer_headers; + utils::inject_trace_context(server_span, answer_headers); + + // server_span is automatically ended on exit + server_span->AddEvent("Replying answer"); + server_span->SetStatus(trace_api::StatusCode::kOk); + return answer_headers; +} + +} // namespace server + +int main(int /* argc */, char ** /* argv */) +{ + InitTracer(); + + // The main client thread is in charge of sending requests and + // processing when they are answered. + auto r1 = utils::create_span("request1", trace_api::SpanKind::kClient); + header_map h1; + utils::inject_trace_context(r1, h1); + + // The simulated "server" will handle the request asynchronously + auto server_future_1 = + std::async(std::launch::async, [&h1] { return server::process_request(h1); }); + + // The client thread is now free to send another request + auto r2 = utils::create_span("request2", trace_api::SpanKind::kClient); + header_map h2; + utils::inject_trace_context(r2, h2); + + // The simulated "server" will handle the request asynchronously + auto server_future_2 = + std::async(std::launch::async, [&h2] { return server::process_request(h2); }); + + // Order doesn't matter. Let's simulate that we get the second answer before the first one + header_map answer_headers_2 = server_future_2.get(); + client::process_answer(answer_headers_2); + + header_map answer_headers_1 = server_future_1.get(); + client::process_answer(answer_headers_1); + + // Both root spans are kept alive till the end, to prove they can both + // outlive other parallel and async processing without interfering each other + // when we explicitly set parent spans with the aux functions + r1->SetStatus(trace_api::StatusCode::kOk); + r1->End(); + + r2->SetStatus(trace_api::StatusCode::kOk); + r2->End(); + + CleanupTracer(); + return 0; +}