C++ Instrumentation with OTeL

Praneet SharmaPraneet Sharma
4 min read

Often times, when practitioners think of generating spans/traces, we think of languages like Python, Javascript, Java, etc. Even GoLang has become indexed in the minds of the observability community as a defacto language. However, C++ is still a mainstay in many systems, and according to the TIOBE Index , is #2 (ranked behind Python).

Luckily, OTeL has a robust C++ SDK. An opinionated way to instrument is to be minimalistic on library components and rely on a host collector to access the wealth of processors and exporters. With that in mind, one can simply use the OTLP GRPC exporter to send to a local collector, creating a decoupled architecture with minimal potential impact.

A particularly valuable use case for the C++ OTeL SDK is tracing, judiciously, intensive computing jobs. We can treat spans as “structured logs” in this case and methodically add them in sections. Here is an example “epidemic simulation” :

#include <iostream>
#include <vector>
#include <random>
#include <memory>
#include <thread>
#include <chrono>
#include <algorithm>  // for std::count

// OpenTelemetry headers
#include "opentelemetry/exporters/otlp/otlp_grpc_exporter_factory.h"
#include "opentelemetry/sdk/trace/simple_processor_factory.h"
#include "opentelemetry/sdk/trace/tracer_provider_factory.h"
#include "opentelemetry/trace/provider.h"

namespace trace     = opentelemetry::trace;
namespace trace_sdk = opentelemetry::sdk::trace;
namespace otlp      = opentelemetry::exporter::otlp;

namespace
{
opentelemetry::exporter::otlp::OtlpGrpcExporterOptions opts;
std::shared_ptr<opentelemetry::sdk::trace::TracerProvider> provider;

void InitTracer()
{
    // Create OTLP exporter instance
    auto exporter  = otlp::OtlpGrpcExporterFactory::Create(opts);
    auto processor = trace_sdk::SimpleSpanProcessorFactory::Create(std::move(exporter));
    provider       = trace_sdk::TracerProviderFactory::Create(std::move(processor));

    // Set the global trace provider
    std::shared_ptr<opentelemetry::trace::TracerProvider> api_provider = provider;
    trace::Provider::SetTracerProvider(api_provider);
}

void CleanupTracer()
{
    // We call ForceFlush to prevent canceling running exports, but it's optional.
    if (provider)
    {
        provider->ForceFlush();
    }

    provider.reset();
    std::shared_ptr<opentelemetry::trace::TracerProvider> none;
    trace::Provider::SetTracerProvider(none);
}
}  // namespace

int main()
{
    opts.endpoint = "localhost:4317";  // Default OTLP gRPC endpoint
    opts.use_ssl_credentials = false;  // Disable SSL/TLS for local communication

    // Initialize the tracer
    InitTracer();

    auto tracer = trace::Provider::GetTracerProvider()->GetTracer("epidemic_simulation");

    // Start the main simulation span
    auto simulation_span = tracer->StartSpan("Simulation");
    auto scoped_simulation = trace::Scope(simulation_span);

    // Simulation parameters
    const int population_size = 1000;
    const int initial_infected = 10;
    const double infection_rate = 0.05;
    const double recovery_rate = 0.01;
    const double mortality_rate = 0.005;

    // States: false = susceptible, true = infected, 2 = recovered, 3 = dead
    std::vector<int> population(population_size, 0);
    std::fill(population.begin(), population.begin() + initial_infected, 1);

    std::default_random_engine generator;
    std::uniform_real_distribution<double> infection_dist(0.0, 1.0);
    std::uniform_real_distribution<double> recovery_dist(0.0, 1.0);
    std::uniform_real_distribution<double> mortality_dist(0.0, 1.0);

    int day = 1;
    while (true)  // Infinite loop, change condition to stop based on your needs
    {
        auto day_span = tracer->StartSpan("Day " + std::to_string(day));
        auto scoped_day = trace::Scope(day_span);

        int new_infections = 0;
        int recoveries = 0;
        int deaths = 0;

        // Copy of the population to avoid modifying while iterating
        std::vector<int> new_population = population;

        for (int i = 0; i < population_size; ++i)
        {
            if (population[i] == 1)  // Infected
            {
                // Chance to recover or die
                if (recovery_dist(generator) < recovery_rate)
                {
                    new_population[i] = 2;  // Recovered
                    recoveries++;
                }
                else if (mortality_dist(generator) < mortality_rate)
                {
                    new_population[i] = 3;  // Dead
                    deaths++;
                }
                else
                {
                    // Try to infect others
                    for (int j = 0; j < population_size; ++j)
                    {
                        if (population[j] == 0 && infection_dist(generator) < infection_rate)
                        {
                            new_population[j] = 1;  // Newly infected
                            new_infections++;
                        }
                    }
                }
            }
        }

        population = new_population;

        int total_infected = std::count(population.begin(), population.end(), 1);
        int total_recovered = std::count(population.begin(), population.end(), 2);
        int total_dead = std::count(population.begin(), population.end(), 3);

        // Log the day's results
        std::cout << "Day " << day << ": " << new_infections << " new infections, " 
                  << recoveries << " recoveries, " << deaths << " deaths.\n";

        // Add events and attributes to the span
        day_span->AddEvent("Day summary", {
            {"new_infections", new_infections},
            {"recoveries", recoveries},
            {"deaths", deaths},
        });
        day_span->SetAttribute("day_number", day);
        day_span->SetAttribute("total_infected", total_infected);
        day_span->SetAttribute("total_recovered", total_recovered);
        day_span->SetAttribute("total_dead", total_dead);

        day_span->End();
        day++;

        // Sleep to slow down the simulation
        std::this_thread::sleep_for(std::chrono::seconds(1));  // Sleep for 1 second between days
    }

    simulation_span->End();

    // Clean up and flush tracer
    CleanupTracer();

    return 0;
}

Key points to note:

  1. InitTracer : where we create our tracer instance and pass options (such as OTLP endpoint)

  2. tracer->StartSpan declarations to create spans

  3. AddEvent and SetAttribute to add crucial metadata to the spans. This can be valuable to store counts, facets for later processing in storage.

  4. Ending and Cleaning up to flush

Leveraging spans as a rich data structure provides a versatile way to instrument C++ applications and get a detailed inspection of behavior and performance.

0
Subscribe to my newsletter

Read articles from Praneet Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Praneet Sharma
Praneet Sharma