Instrument your C++ application with OpenTelemetry
This walkthrough shows how to add observability to your C++ application using the OpenTelemetry C++ libraries and tools.
Feature | Supported |
---|---|
Automatic Instrumentation | No |
Automatic OneAgent Ingestion | No |
Prerequisites
- Dynatrace version 1.222+
- A supported C++ compiler (C++ 11 and later)
- The Protocol Buffers library
- The OpenTelemetry library
- For tracing, W3C Trace Context is enabled
- From the Dynatrace menu, go to Settings > Preferences > OneAgent features.
- Turn on Send W3C Trace Context HTTP headers.
Get the Dynatrace access details
Determine the API base URL
For details on how to assemble the base OTLP endpoint URL, see Export with OTLP. The URL should end in /api/v2/otlp
.
Get API access token
The access token for ingesting traces, logs, and metrics can be generated in your Dynatrace menu under Access tokens.
Export with OTLP has more details on the format and the necessary access scopes.
Set up OpenTelemetry
-
Add the following directives to your CMake build configuration in
CMakeLists.txt
:find_package(CURL REQUIRED) find_package(Protobuf REQUIRED) find_package(opentelemetry-cpp CONFIG REQUIRED) include_directories("${OPENTELEMETRY_CPP_INCLUDE_DIRS}") target_link_libraries( <YOUR_EXE_NAME> ${OPENTELEMETRY_CPP_LIBRARIES} opentelemetry_trace opentelemetry_common opentelemetry_http_client_curl opentelemetry_exporter_otlp_http opentelemetry_exporter_otlp_http_client opentelemetry_otlp_recordable opentelemetry_resources opentelemetry_metrics opentelemetry_exporter_otlp_http_metric )
-
Create a file named
otel.h
in your application directory and save the following content:// tracer_common.h #pragma once #include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h" #include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h" #include "opentelemetry/exporters/ostream/span_exporter_factory.h" #include "opentelemetry/context/propagation/global_propagator.h" #include "opentelemetry/context/propagation/text_map_propagator.h" #include "opentelemetry/nostd/shared_ptr.h" #include "opentelemetry/sdk/trace/simple_processor_factory.h" #include "opentelemetry/sdk/trace/tracer_context.h" #include "opentelemetry/sdk/trace/tracer_context_factory.h" #include "opentelemetry/sdk/trace/tracer_provider_factory.h" #include "opentelemetry/trace/propagation/http_trace_context.h" #include "opentelemetry/trace/provider.h" #include <cstring> #include <iostream> #include <vector> #include <fstream> #include <list> using namespace std; namespace nostd = opentelemetry::nostd; namespace otlp = opentelemetry::exporter::otlp; namespace sdktrace = opentelemetry::sdk::trace; namespace resource = opentelemetry::sdk::resource; namespace { // Class definition for context propagation template <typename T> class HttpTextMapCarrier : public opentelemetry::context::propagation::TextMapCarrier { public: HttpTextMapCarrier<T>(T &headers) : headers_(headers) {} HttpTextMapCarrier() = default; virtual nostd::string_view Get(nostd::string_view key) const noexcept override { std::string key_to_compare = key.data(); if (key == opentelemetry::trace::propagation::kTraceParent) { key_to_compare = "Traceparent"; } else if (key == opentelemetry::trace::propagation::kTraceState) { key_to_compare = "Tracestate"; } auto it = headers_.find(key_to_compare); if (it != headers_.end()) { return it->second; } return ""; } virtual void Set(nostd::string_view key, nostd::string_view value) noexcept override { headers_.insert(std::pair<std::string, std::string>(std::string(key), std::string(value))); } T headers_; }; void initOpenTelemetry() { // ===== GENERAL SETUP ===== std::string DT_API_URL = ""; std::string DT_API_TOKEN = ""; resource::ResourceAttributes resource_attributes = { {"service.name", "cpp-quickstart"}, //TODO Replace with the name of your application {"service.version", "1.0.1"} //TODO Replace with the version of your application }; resource::ResourceAttributes dt_resource_attributes; try { for (string name : {"dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties", "/var/lib/dynatrace/enrichment/dt_metadata.properties"}) { string file_path; ifstream dt_file; dt_file.open(name); if (dt_file.is_open()) { string dt_metadata; ifstream dt_properties; while (getline(dt_file, file_path)) { dt_properties.open(file_path); if (dt_properties.is_open()) { while (getline(dt_properties, dt_metadata)) { dt_resource_attributes.SetAttribute( dt_metadata.substr(0, dt_metadata.find("=")), dt_metadata.substr(dt_metadata.find("=") + 1) ); } dt_properties.close(); } } dt_file.close(); } } } catch (...) {} // ===== TRACING SETUP ===== otlp::OtlpHttpExporterOptions traceOptions; traceOptions.url = DT_API_URL + "/v1/traces"; traceOptions.content_type = otlp::HttpRequestContentType::kBinary; traceOptions.http_headers.insert( std::make_pair<const std::string, std::string>("Authorization", "Api-Token " + DT_API_TOKEN) ); auto dt_resource = resource::Resource::Create(dt_resource_attributes); auto resource = resource::Resource::Create(resource_attributes); auto merged_resource = dt_resource.Merge(resource); auto exporter = otlp::OtlpHttpExporterFactory::Create(traceOptions); auto processor = sdktrace::SimpleSpanProcessorFactory::Create(std::move(exporter)); std::vector<std::unique_ptr<sdktrace::SpanProcessor>> processors; processors.push_back(std::move(processor)); auto context = sdktrace::TracerContextFactory::Create(std::move(processors), merged_resource); std::shared_ptr<opentelemetry::trace::TracerProvider> provider = sdktrace::TracerProviderFactory::Create(std::move(context)); opentelemetry::trace::Provider::SetTracerProvider(provider); opentelemetry::context::propagation::GlobalTextMapPropagator::SetGlobalPropagator( opentelemetry::nostd::shared_ptr<opentelemetry::context::propagation::TextMapPropagator>( new opentelemetry::trace::propagation::HttpTraceContext() ) ); // ===== METRIC SETUP ===== otlp_exporter::OtlpHttpMetricExporterOptions otlpOptions; otlpOptions.url = DT_API_URL + "/v1/metrics"; otlpOptions.aggregation_temporality = metric_sdk::AggregationTemporality::kDelta; otlpOptions.content_type = otlp_exporter::HttpRequestContentType::kBinary; otlpOptions.http_headers.insert( std::make_pair<const std::string, std::string>("Authorization", "Api-Token " + DT_API_TOKEN) ); //This creates the exporter with the options we have defined above. auto exporter = otlp_exporter::OtlpHttpMetricExporterFactory::Create(otlpOptions); //Build MeterProvider and Reader metric_sdk::PeriodicExportingMetricReaderOptions options; options.export_interval_millis = std::chrono::milliseconds(1000); options.export_timeout_millis = std::chrono::milliseconds(500); std::unique_ptr<metric_sdk::MetricReader> reader{ new metric_sdk::PeriodicExportingMetricReader(std::move(exporter), options) }; auto provider = std::shared_ptr<metrics_api::MeterProvider>(new metric_sdk::MeterProvider()); auto p = std::static_pointer_cast<metric_sdk::MeterProvider>(provider); p->AddMetricReader(std::move(reader)); metrics_api::Provider::SetMeterProvider(provider); } }
-
Configure
DT_API_URL
andDT_API_TOKEN
for the Dynatrace URL and access token inotel.h
.
Instrument your application
To use OpenTelemetry, you first need to complete these two steps:
-
Add the necessary header files to your code.
To add the header files, include
otel.h
wherever you want to make use of OpenTelemetry.#include "otel.h"
-
Initialize OpenTelemetry.
For the initialization, use the
initOpenTelemetry
function inotel.h
and call it early on in the startup code of your application.
Add tracing
-
Get a reference to the tracer provider.
auto provider = opentelemetry::trace::Provider::GetTracerProvider();
-
Obtain a tracer object.
auto tracer = provider->GetTracer("my-tracer");
-
With
tracer
, we can now start new spans and set them for the current execution scope.StartSpanOptions options; options.kind = SpanKind::kServer; auto span = tracer->StartSpan("Call to /myendpoint", { { "http.method", "GET" }, { "net.protocol.version", "1.1" } }, options); auto scope = tracer->WithActiveSpan(span); // TODO: Your code goes here span->End();
In the above code, we:
- Create a new span and name it "Call to /myendpoint"
- Add two attributes, following the semantic naming convention, specific to the action of this span: information on the HTTP method and version
- Add a
TODO
in place of the eventual business logic - Call the span's
End()
method to complete the span
Collect metrics
-
Get a reference to the meter provider.
auto provider = metrics_api::Provider::GetMeterProvider();
-
Obtain a meter object.
nostd::shared_ptr<metrics_api::Meter> meter = provider->GetMeter("my-meter", "1.0.1");
-
With
meter
, we can now create individual instruments, such as a counter.auto request_counter = meter->CreateUInt64Counter("request_counter");
-
We can now invoke the
Add()
method ofrequest_counter
to record new values with the counter and save additional attributes.std::map<std::string, std::string> labels = { {"ip", "an ip address here"} }; auto labelkv = opentelemetry::common::KeyValueIterableView<decltype(labels)>{ labels }; request_counter.Add(1, labelkv);
Connect logs
OpenTelemetry logging is currently not yet available for C++ and is still under development.
Ensure context propagation optional
Context propagation is particularly important when network calls (for example, REST) are involved.
In the following examples, we assume that we are handling context propagation using the standard W3C trace context headers, and we receive and set HTTP headers with the OpenTelemetry http_client::Headers
object.
For that purpose, we use an instance of the class HttpTextMapCarrier
, which we defined during the setup, and which is based on the OpenTelemetry class TextMapCarrier
.
Extracting the context when receiving a request
To extract information on an existing context, we call the Extract
method of the global propagator singleton and pass it the HttpTextMapCarrier
instance, as well as the current context. This returns a new context object (new_context
), which we allows us to continue the previous trace with our spans.
#include "opentelemetry/trace/context.h"
#include "otel.h"
using namespace opentelemetry::trace;
namespace context = opentelemetry::context;
// your method
{
StartSpanOptions options;
options.kind = SpanKind::kServer;
// extract context from http header
HttpTextMapCarrier<http_client::Headers> carrier;
auto prop = context::propagation::GlobalTextMapPropagator::GetGlobalPropagator();
auto current_ctx = context::RuntimeContext::GetCurrent();
auto new_context = prop->Extract(carrier, current_ctx);
options.parent = GetSpan(new_context)->GetContext();
// start scoped span with parent context extracted from http header
auto span = get_tracer("manual-server")
->StartSpan("my-server-span", { //TODO Replace with the name of your span
{"my-server-key-1", "my-server-value-1"} //TODO Add attributes
}, options);
auto scope = get_tracer("http_server")->WithActiveSpan(span);
// your code goes here
span->End();
}
Injecting the context when sending requests
For injecting current context information into an outbound request, we call the Inject
method of the global propagator singleton and pass it the HttpTextMapCarrier
instance, as well as the current context. This adds the applicable headers to the carrier
instance, which we then use in the text step with our HTTP request.
#include "opentelemetry/ext/http/client/http_client_factory.h"
#include "opentelemetry/ext/http/common/url_parser.h"
#include "opentelemetry/trace/context.h"
#include "otel.h"
using namespace opentelemetry::trace;
namespace context = opentelemetry::context;
namespace http_client = opentelemetry::ext::http::client;
// your method
{
auto http_client = http_client::HttpClientFactory::CreateSync();
std::string url = "<HTTP_URL>";
// start scoped active span
StartSpanOptions options;
options.kind = SpanKind::kClient;
opentelemetry::ext::http::common::UrlParser url_parser(url);
std::string span_name = url_parser.path_; // Fetch the URL path to be used as span name
auto span = get_tracer("http-client")
->StartSpan(span_name, {
{"my-client-key-1", "my-client-value-1"} //TODO Add attributes
}, options);
auto scope = get_tracer("http-client")->WithActiveSpan(span);
// inject current context into http header
auto current_ctx = context::RuntimeContext::GetCurrent();
HttpTextMapCarrier<http_client::Headers> carrier;
auto prop = context::propagation::GlobalTextMapPropagator::GetGlobalPropagator();
prop->Inject(carrier, current_ctx);
// send http request
http_client::Result result = http_client->Get(url, carrier.headers_);
//your code goes here
span->End();
}
Configure data capture to meet privacy requirements optional
While Dynatrace automatically captures all OpenTelemetry resource and span attributes, only attribute values specified in the allowlist are stored and displayed in the Dynatrace web UI. This prevents accidental storage of personal data, so you can meet your privacy requirements and control the amount of monitoring data stored.
To view your custom span attributes, you need to allow them in the Dynatrace web UI first.
- Span attributes: In the Dynatrace menu, go to Settings and select Server-side service monitoring > Span attributes.
- Resource attributes: In the Dynatrace menu, go to Settings and select Server-side service monitoring > Resource attributes.
Verify data ingestion into Dynatrace
Once you have finished the instrumentation of your application, perform a couple of test actions to create and send demo traces, metrics, and logs and verify that they were correctly ingested into Dynatrace.
To do that for traces, in the Dynatrace menu, go to Distributed traces and select the Ingested traces tab. If you use OneAgent, select PurePaths instead.
Metrics and logs can be found under their respective entries at Observe and explore.