This walkthrough shows how to add observability to your C++ application using the OpenTelemetry C++ libraries and tools.
For details on how to assemble the base OTLP endpoint URL, see Export with OTLP. The URL should end in /api/v2/otlp
.
The access token for ingesting traces, logs, and metrics can be generated under Access Tokens.
Export with OTLP has more details on the format and the necessary access scopes.
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_traceopentelemetry_commonopentelemetry_http_client_curlopentelemetry_exporter_otlp_httpopentelemetry_exporter_otlp_http_clientopentelemetry_otlp_recordableopentelemetry_resourcesopentelemetry_metricsopentelemetry_exporter_otlp_http_metric)
Create a file named otel.h
in your application directory and save the following content:
#include "opentelemetry/trace/provider.h"#include "opentelemetry/trace/propagation/http_trace_context.h"#include "opentelemetry/context/propagation/global_propagator.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/exporters/ostream/span_exporter_factory.h"#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h"#include "opentelemetry/metrics/provider.h"#include "opentelemetry/sdk/metrics/export/periodic_exporting_metric_reader.h"#include "opentelemetry/sdk/metrics/view/view_registry_factory.h"#include "opentelemetry/sdk/metrics/meter_context_factory.h"#include "opentelemetry/sdk/metrics/meter_provider_factory.h"#include "opentelemetry/exporters/ostream/metric_exporter_factory.h"#include "opentelemetry/exporters/otlp/otlp_http_metric_exporter_factory.h"#include "opentelemetry/logs/provider.h"#include "opentelemetry/sdk/logs/logger.h"#include "opentelemetry/sdk/logs/logger_provider_factory.h"#include "opentelemetry/sdk/logs/simple_log_record_processor_factory.h"#include "opentelemetry/sdk/logs/logger_context_factory.h"#include "opentelemetry/exporters/ostream/log_record_exporter.h"#include "opentelemetry/exporters/otlp/otlp_http_log_record_exporter_factory.h"#include <cstring>#include <iostream>#include <vector>#include <fstream>#include <list>#include <memory>#include <thread>#include <iostream>#include <string>using namespace std;namespace nostd = opentelemetry::nostd;namespace otlp = opentelemetry::exporter::otlp;namespace resource = opentelemetry::sdk::resource;namespace trace_api = opentelemetry::trace;namespace trace_sdk = opentelemetry::sdk::trace;namespace metrics_api = opentelemetry::metrics;namespace metrics_sdk = opentelemetry::sdk::metrics;namespace logs_api = opentelemetry::logs;namespace logs_sdk = opentelemetry::sdk::logs;namespace{// Class definition for context propagationotlp::OtlpHttpMetricExporterOptions options;std::string version{ "1.0.1" };std::string name{ "app_cpp" };std::string schema{ "https://opentelemetry.io/schemas/1.2.0" };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();// Header's first letter seems to be automatically capitaliazed by our test http-server, so// compare accordingly.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_;};// ===== GENERAL SETUP =====void initTracer(){otlp::OtlpHttpExporterOptions traceOptions;traceOptions.url = std::string(std::getenv("DT_API_URL")) + "/v1/traces";traceOptions.content_type = otlp::HttpRequestContentType::kBinary;traceOptions.http_headers.insert(std::make_pair<const std::string, std::string>("Authorization", std::getenv("DT_API_TOKEN")));resource::ResourceAttributes resource_attributes = {{"service.name", name},{"service.version", version}};resource::ResourceAttributes dt_resource_attributes;try{for (string name : {"dt_metadata_e617c525669e072eebe3d0f08212e8f2.properties","/var/lib/dynatrace/enrichment/dt_metadata.properties","/var/lib/dynatrace/enrichment/dt_host_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 (...) {}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 = trace_sdk::SimpleSpanProcessorFactory::Create(std::move(exporter));std::vector<std::unique_ptr<trace_sdk::SpanProcessor>> processors;processors.push_back(std::move(processor));auto context = trace_sdk::TracerContextFactory::Create(std::move(processors), merged_resource);std::shared_ptr<opentelemetry::trace::TracerProvider> provider = opentelemetry::sdk::trace::TracerProviderFactory::Create(std::move(context));// Set the global trace provideropentelemetry::trace::Provider::SetTracerProvider(provider);// set global propagatoropentelemetry::context::propagation::GlobalTextMapPropagator::SetGlobalPropagator(opentelemetry::nostd::shared_ptr<opentelemetry::context::propagation::TextMapPropagator>(new opentelemetry::trace::propagation::HttpTraceContext()));}// ===== METRIC SETUP =====void initMeter() {resource::ResourceAttributes resource_attributes = {{"service.name", name},{"service.version", version}};otlp::OtlpHttpMetricExporterOptions otlpOptions;auto resource = resource::Resource::Create(resource_attributes);otlpOptions.url = std::string(std::getenv("DT_API_URL")) + "/v1/metrics";otlpOptions.aggregation_temporality = otlp::PreferredAggregationTemporality::kDelta;otlpOptions.content_type = otlp::HttpRequestContentType::kBinary;otlpOptions.http_headers.insert(std::make_pair<const std::string, std::string>("Authorization", std::getenv("DT_API_TOKEN")));//This creates the exporter with the options we have defined above.auto exporter = otlp::OtlpHttpMetricExporterFactory::Create(otlpOptions);metrics_sdk::PeriodicExportingMetricReaderOptions options;options.export_interval_millis = std::chrono::milliseconds(1000);options.export_timeout_millis = std::chrono::milliseconds(500);std::unique_ptr<metrics_sdk::MetricReader> reader{new metrics_sdk::PeriodicExportingMetricReader(std::move(exporter), options) };auto context = metrics_sdk::MeterContextFactory::Create(opentelemetry::sdk::metrics::ViewRegistryFactory::Create(), resource);context->AddMetricReader(std::move(reader));auto u_provider = metrics_sdk::MeterProviderFactory::Create(std::move(context));std::shared_ptr<opentelemetry::metrics::MeterProvider> provider(std::move(u_provider));metrics_api::Provider::SetMeterProvider(provider);}// ===== LOG SETUP =====void initLogger() {resource::ResourceAttributes resource_attributes = {{"service.name", name},{"service.version", version}};auto resource = resource::Resource::Create(resource_attributes);otlp::OtlpHttpLogRecordExporterOptions loggerOptions;loggerOptions.url = std::string(std::getenv("DT_API_URL")) + "/v1/logs";loggerOptions.http_headers.insert(std::make_pair<const std::string, std::string>("Authorization", std::getenv("DT_API_TOKEN")));loggerOptions.content_type = opentelemetry::exporter::otlp::HttpRequestContentType::kBinary;auto exporter = otlp::OtlpHttpLogRecordExporterFactory::Create(loggerOptions);auto processor = logs_sdk::SimpleLogRecordProcessorFactory::Create(std::move(exporter));std::vector<std::unique_ptr<logs_sdk::LogRecordProcessor>> processors;processors.push_back(std::move(processor));auto context = logs_sdk::LoggerContextFactory::Create(std::move(processors), resource);std::shared_ptr<logs_api::LoggerProvider> provider = logs_sdk::LoggerProviderFactory::Create(std::move(context));opentelemetry::logs::Provider::SetLoggerProvider(provider);}nostd::shared_ptr<opentelemetry::logs::Logger> get_logger(std::string scope){// TODO: add your log provider herereturn logger;}opentelemetry::nostd::shared_ptr<opentelemetry::trace::Tracer> get_tracer(std::string tracer_name){// TODO: add your trace provider herereturn tracer;}nostd::unique_ptr<opentelemetry::metrics::Counter<uint64_t>> initIntCounter(){// TODO: add your custom metrics herereturn request_counter;}}
The file read operations, parsing the dt_metadata
files in the example code, attempt to read the OneAgent data files to enrich the OTLP request and ensure that all relevant topology information is available within Dynatrace.
Configure DT_API_URL
and DT_API_TOKEN
for the Dynatrace URL and access token in otel.h
.
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 in otel.h
and call it early on in the startup code of your application.
Get a reference to the tracer provider.
auto provider = opentelemetry::trace::Provider::GetTracerProvider();
Obtain a tracer object.
// In our case the GetTraces method takes the tracer name and returns the tracer providerauto tracer = provider->GetTracer(tracer_name);
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 herespan->End();
In the above code, we:
TODO
in place of the eventual business logicEnd()
method to complete the spanGet 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 of request_counter
to record new values with the counter and save additional attributes (for example, action.type
).
std::map<std::string, std::string> labels = { {"action.type", "create"} };auto labelkv = opentelemetry::common::KeyValueIterableView<decltype(labels)>{ labels };request_counter->Add(1, labelkv);
Get a reference to the logger provider.
auto provider = logs_api::Provider::GetLoggerProvider();
Call the provider's GetLogger()
method to obtain a logger instance.
auto logger = provider->GetLogger("scope_name", "", OPENTELEMETRY_SDK_VERSION);
Call any of the available logging methods to record a log statement. The following example logs a debug statement.
logger->Debug("My debug statement here");
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
.
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.
StartSpanOptions options;options.kind = SpanKind::kServer;std::string span_name = request.uri;// extract context from http headerstd::map<std::string, std::string> &request_headers =const_cast<std::map<std::string, std::string> &>(request.headers);const HttpTextMapCarrier<std::map<std::string, std::string>> carrier(request_headers);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();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);for (auto &kv : request.headers){span->SetAttribute("http.header." + std::string(kv.first.data()), kv.second);}span->AddEvent("Processing request");response.headers[HTTP_SERVER_NS::CONTENT_TYPE] = HTTP_SERVER_NS::CONTENT_TYPE_TEXT;response.body = doCall();span->End();
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.
auto http_client = http_client::HttpClientFactory::CreateSync();std::string url = std::getenv("URL"); // TODO set URL you want to callCustomCounter(); // remove// start active spanStartSpanOptions options;options.kind = SpanKind::kClient;opentelemetry::ext::http::common::UrlParser url_parser(url);std::string span_name = url_parser.path_;auto span = get_tracer("http-client")->StartSpan(span_name,{{SemanticConventions::kUrlFull, url_parser.url_},{SemanticConventions::kUrlScheme, url_parser.scheme_},{SemanticConventions::kHttpRequestMethod, "GET"}},options);auto scope = get_tracer("http-client")->WithActiveSpan(span);// inject current context into http headerauto current_ctx = context::RuntimeContext::GetCurrent();HttpTextMapCarrier<http_client::Headers> carrier;auto prop = context::propagation::GlobalTextMapPropagator::GetGlobalPropagator();prop->Inject(carrier, current_ctx);// send http requesthttp_client::Result result = http_client->GetNoSsl(url, carrier.headers_);//your code goes here//then end span
While Dynatrace automatically captures all OpenTelemetry 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 attributes, you need to allow them in the Dynatrace web UI first. To learn how to configure attribute storage and masking, see Attribute redaction.
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, go to Distributed Traces or Distributed Traces Classic (latest Dynatrace) and select the Ingested traces tab. If you use OneAgent, select PurePaths instead.
For metrics and logs, go to Metrics or Logs or Logs & Events (latest Dynatrace).