This guide shows you how to instrument your C++ application with OpenTelemetry and send traces to SigNoz. You build the opentelemetry-cpp SDK into your project with CMake or Bazel, then initialize a tracer that exports over OTLP/HTTP.
Prerequisites
- A C++ compiler with C++14 or later support
- CMake 3.16 or later, or Bazel, to build your project
- An instance of SigNoz (either Cloud or Self-Hosted)
- Your application code
Send traces to SigNoz
Step 1. Set environment variables
The OTLP exporter reads its destination from the environment, so your source code holds no endpoint. Set these variables before you run your application:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
export OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
export OTEL_SERVICE_NAME="<service-name>"
export OTEL_RESOURCE_ATTRIBUTES="service.version=<service-version>"Verify these values:
<region>: Your SigNoz Cloud region<your-ingestion-key>: Your SigNoz ingestion key<service-name>: A descriptive name for your service (for example,payment-service)<service-version>(optional): Your release version, image tag, or git SHA (for example,1.4.2ora01dbef8)
Step 1. Set environment variables in your Dockerfile
Add the environment variables to the image that runs your compiled binary:
ENV OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
ENV OTEL_SERVICE_NAME="<service-name>"
ENV OTEL_RESOURCE_ATTRIBUTES="service.version=<service-version>"Verify these values:
<region>: Your SigNoz Cloud region<service-name>: A descriptive name for your service (for example,payment-service)<service-version>(optional): Your release version, image tag, or git SHA (for example,1.4.2ora01dbef8)
Step 1. Set environment variables
Create a Secret that holds the ingestion header:
kubectl create secret generic signoz-ingestion \
--from-literal=otlp-headers="signoz-ingestion-key=<your-ingestion-key>"Then add these environment variables to your deployment manifest:
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: 'https://ingest.<region>.signoz.cloud:443'
- name: OTEL_EXPORTER_OTLP_HEADERS
valueFrom:
secretKeyRef:
name: signoz-ingestion
key: otlp-headers
- name: OTEL_SERVICE_NAME
value: '<service-name>'
- name: OTEL_RESOURCE_ATTRIBUTES
value: 'service.version=<service-version>'Verify these values:
<region>: Your SigNoz Cloud region<your-ingestion-key>: Your SigNoz ingestion key<service-name>: A descriptive name for your service (for example,payment-service)<service-version>(optional): Your release version, image tag, or git SHA (for example,1.4.2ora01dbef8)
The manifest carries no credential, so you can commit it. Only the Secret holds the key.
Step 1. Set environment variables (PowerShell)
$env:OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.<region>.signoz.cloud:443"
$env:OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>"
$env:OTEL_SERVICE_NAME="<service-name>"
$env:OTEL_RESOURCE_ATTRIBUTES="service.version=<service-version>"Verify these values:
<region>: Your SigNoz Cloud region<your-ingestion-key>: Your SigNoz ingestion key<service-name>: A descriptive name for your service (for example,payment-service)<service-version>(optional): Your release version, image tag, or git SHA (for example,1.4.2ora01dbef8)
These variables apply to the current PowerShell session. Use [Environment]::SetEnvironmentVariable or your service manager to keep them after the session ends.
Step 2. Add the opentelemetry-cpp dependency
opentelemetry-cpp ships the OTLP HTTP exporter as an opt-in build target. Enable it when you build the SDK.
The OTLP HTTP exporter links against Protobuf and libcurl. Install both from your package manager before you build the SDK. On Debian or Ubuntu:
sudo apt-get update
sudo apt-get install -y libprotobuf-dev protobuf-compiler libabsl-dev libcurl4-openssl-devBuild and install the SDK with the OTLP HTTP exporter enabled:
git clone --depth 1 --branch v1.28.0 https://github.com/open-telemetry/opentelemetry-cpp.git
cd opentelemetry-cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release -DWITH_OTLP_HTTP=ON -DBUILD_TESTING=OFF -DWITH_BENCHMARK=OFF
cmake --build build --target all --config Release
cmake --install build --config Release --prefix <install-root>Set <install-root> to the directory that receives the headers and libraries, such as /usr/local.
Then link the SDK into your own target:
cmake_minimum_required(VERSION 3.16)
project(my-cpp-app CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(opentelemetry-cpp CONFIG REQUIRED COMPONENTS api sdk exporters_otlp_http)
add_executable(my-cpp-app main.cc)
target_link_libraries(my-cpp-app
PRIVATE
opentelemetry-cpp::api
opentelemetry-cpp::trace
opentelemetry-cpp::otlp_http_exporter
)If you installed to a non-standard prefix, point CMake at it when you configure your project:
cmake -B build -DCMAKE_PREFIX_PATH=<install-root>Declare both dependencies in MODULE.bazel. The repo_name value becomes the label prefix for the SDK targets, and the OpenTelemetry examples use io_opentelemetry_cpp:
bazel_dep(name = "rules_cc", version = "0.2.22")
bazel_dep(
name = "opentelemetry-cpp",
version = "1.28.0",
repo_name = "io_opentelemetry_cpp",
)Then load cc_binary from rules_cc, and depend on the API, the trace SDK, and the OTLP HTTP exporter:
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
cc_binary(
name = "my-cpp-app",
srcs = ["main.cc"],
deps = [
"@io_opentelemetry_cpp//api",
"@io_opentelemetry_cpp//exporters/otlp:otlp_http_exporter",
"@io_opentelemetry_cpp//sdk/src/trace",
],
)Step 3. Initialize the tracer
Create main.cc. InitTracer connects the OTLP HTTP exporter to a batch span processor, then registers the result as the global tracer provider. No SigNoz values appear in the code. The exporter and the resource read them from the environment variables you set in Step 1:
#include <memory>
#include <utility>
#include "opentelemetry/exporters/otlp/otlp_http_exporter_factory.h"
#include "opentelemetry/exporters/otlp/otlp_http_exporter_options.h"
#include "opentelemetry/sdk/resource/resource.h"
#include "opentelemetry/sdk/trace/batch_span_processor_factory.h"
#include "opentelemetry/sdk/trace/batch_span_processor_options.h"
#include "opentelemetry/sdk/trace/provider.h"
#include "opentelemetry/sdk/trace/tracer_provider.h"
#include "opentelemetry/sdk/trace/tracer_provider_factory.h"
#include "opentelemetry/trace/provider.h"
#include "opentelemetry/trace/scope.h"
#include "opentelemetry/trace/tracer_provider.h"
namespace trace = opentelemetry::trace;
namespace trace_sdk = opentelemetry::sdk::trace;
namespace otlp = opentelemetry::exporter::otlp;
namespace resource_sdk = opentelemetry::sdk::resource;
namespace
{
std::shared_ptr<trace_sdk::TracerProvider> provider;
void InitTracer()
{
// Reads OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS.
// With OTEL_EXPORTER_OTLP_ENDPOINT set, the exporter appends /v1/traces.
otlp::OtlpHttpExporterOptions exporter_options;
auto exporter = otlp::OtlpHttpExporterFactory::Create(exporter_options);
// Batch spans instead of sending one HTTP request per span.
trace_sdk::BatchSpanProcessorOptions processor_options;
auto processor =
trace_sdk::BatchSpanProcessorFactory::Create(std::move(exporter), processor_options);
// Reads OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES.
auto resource = resource_sdk::Resource::Create({});
provider = trace_sdk::TracerProviderFactory::Create(std::move(processor), resource);
std::shared_ptr<trace::TracerProvider> api_provider = provider;
trace_sdk::Provider::SetTracerProvider(api_provider);
}
void CleanupTracer()
{
// Flush spans still queued in the batch processor before the process exits.
if (provider)
{
provider->ForceFlush();
}
provider.reset();
std::shared_ptr<trace::TracerProvider> none;
trace_sdk::Provider::SetTracerProvider(none);
}
} // namespace
int main()
{
InitTracer();
auto tracer = trace::Provider::GetTracerProvider()->GetTracer("my-cpp-app");
{
// Scope ends the span when the block exits.
auto scope = trace::Scope(tracer->StartSpan("startup"));
// Call your application code here. Any span started while this scope is
// active becomes a child of "startup".
}
CleanupTracer();
return 0;
}Call InitTracer once at startup, and call CleanupTracer before main returns. If you skip the cleanup, the SDK discards the spans still in the batch queue.
Step 4. Run your application
Run it where you set the variables in Step 1.
cmake -B build
cmake --build build
./build/my-cpp-appbazel run //:my-cpp-appReplace my-cpp-app with the name of the cc_binary target you defined in your BUILD file.
Build the image, then pass the ingestion key at runtime:
docker build -t my-cpp-app .
docker run --rm -e OTEL_EXPORTER_OTLP_HEADERS="signoz-ingestion-key=<your-ingestion-key>" my-cpp-appVerify these values:
<your-ingestion-key>: Your SigNoz ingestion key
Your Dockerfile needs a CMD that starts the compiled binary, such as CMD ["./build/my-cpp-app"].
Apply the manifest, then restart the workload so the new environment reaches your pods:
kubectl apply -f <your-manifest>.yaml
kubectl rollout restart deployment/<deployment-name>
kubectl rollout status deployment/<deployment-name>Verify these values:
<your-manifest>: The manifest you edited in Step 1<deployment-name>: The Deployment that runs your C++ binary
Pass --config Release so the build and the executable path agree:
cmake -B build
cmake --build build --config Release
.\build\Release\my-cpp-app.exebazel run //:my-cpp-appValidate
Run your instrumented application, then verify that traces reach SigNoz:
- Run your application so it executes the code path that starts a span.
- Open SigNoz and navigate to Traces.
- Click Refresh and filter by the
service.nameyou set inOTEL_SERVICE_NAME. - Click any trace to see its spans, timing, and attributes.

Spans leave in batches, so wait a few seconds after the run before you look for them.
Troubleshooting
Why don't traces appear in SigNoz?
Verify that the environment variables reached the process:
echo $OTEL_EXPORTER_OTLP_ENDPOINT
echo $OTEL_SERVICE_NAMEWhen OTEL_EXPORTER_OTLP_ENDPOINT is empty, the exporter uses its default of http://localhost:4318/v1/traces. Spans then go to a local Collector that can be absent.
Then test the connection to the endpoint:
curl -v https://ingest.<region>.signoz.cloud:443/v1/tracesFor a full walkthrough covering SDK diagnostics, Collector connectivity, and common ingestion errors (also for logs and metrics), see Debug missing traces, logs, and metrics in SigNoz.
Why does find_package fail on utf8_range::utf8_validity?
The SDK was built against a Protobuf that CMake fetched and built itself, so the installed CMake config points at targets that were never installed. Install Protobuf from your package manager, then rebuild and reinstall the SDK.
Why does the CMake build fail on benchmark::benchmark?
WITH_BENCHMARK is on by default and does not follow BUILD_TESTING. With only -DBUILD_TESTING=OFF, CMake still creates the benchmark targets but never imports Google Benchmark. Pass -DWITH_BENCHMARK=OFF as well, as shown in Step 2.
Why does the build fail on the OTLP exporter headers?
opentelemetry-cpp does not build the OTLP HTTP exporter by default. For CMake, rebuild the SDK with -DWITH_OTLP_HTTP=ON. For Bazel, verify that your target depends on @io_opentelemetry_cpp//exporters/otlp:otlp_http_exporter.
Why does Bazel report that cc_binary was removed?
Bazel 9 removed the built-in C++ rules. Add rules_cc to MODULE.bazel, then load cc_binary at the top of your BUILD file, as shown in Step 2.
Why does Bazel report that //api does not exist?
A label that begins with // points at your own workspace. Prefix every SDK label with the repository name, as in @io_opentelemetry_cpp//api.
Why do spans go missing when the process exits?
The batch span processor queues spans and exports them on a timer. Call provider->ForceFlush() through CleanupTracer before main returns, as shown in Step 3.
Why does the service show as unknown_service?
The process started without OTEL_SERVICE_NAME. Resource::Create then uses unknown_service as the service name, or unknown_service:<binary> when it can read the executable name.
Setup OpenTelemetry Collector (Optional)
What is the OpenTelemetry Collector?
The Collector sits between your application and SigNoz. Your application sends spans to the Collector, and the Collector forwards them to SigNoz.
Why use it?
- Cleaning up data: Filter out noisy spans, or remove sensitive attributes before they leave your servers.
- Keeping your app lightweight: Move batching, retries, and compression out of your process.
- Adding context: Tag spans with host, Kubernetes, or cloud metadata that the application does not know.
- Future flexibility: Fan out to more than one backend without a rebuild of your binary.
To send this setup through a Collector, point OTEL_EXPORTER_OTLP_ENDPOINT at the Collector's OTLP/HTTP address. The default address is http://localhost:4318. Remove OTEL_EXPORTER_OTLP_HEADERS, because the Collector holds the ingestion key.
See Switch from direct export to Collector for step-by-step instructions to convert your setup.
For more details, see Why use the OpenTelemetry Collector? and the Collector configuration guide.
Next steps
- Correlate traces with logs to accelerate triage across signals
- Set up alerts for your C++ application
- Create dashboards to visualize application health
- Add spans, attributes, and error records with the OpenTelemetry C++ instrumentation reference
Get Help
If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack. If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.