From f1062e23397a68ba4b52d707553989ed70de65dd Mon Sep 17 00:00:00 2001 From: Jose Gomez-Selles <14234281+jgomezselles@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:48:51 +0100 Subject: [PATCH] [EXAMPLE] Add async example This PR adds an 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. It defines auxiliary functions that can be easily adapted to other code bases together with a README documenting the flow and expected outcome. Apart from markdown lining and clang formatting, BUILD and relevant CMakeLists have been adapted. --- examples/CMakeLists.txt | 1 + examples/async/BUILD | 17 +++ examples/async/CMakeLists.txt | 11 ++ examples/async/README.md | 177 ++++++++++++++++++++++++++ examples/async/main.cc | 232 ++++++++++++++++++++++++++++++++++ 5 files changed, 438 insertions(+) create mode 100644 examples/async/BUILD create mode 100644 examples/async/CMakeLists.txt create mode 100644 examples/async/README.md create mode 100644 examples/async/main.cc 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; +}