A C wrapper for the C++ OpenTelemetry API, enabling C integration with the OpenTelemetry observability framework.
The OpenTelemetry (OTel) C wrapper library provides a pure C API on top of the official OpenTelemetry C++ client. It was developed by HAProxy Technologies for use in the HAProxy OTel filter, but is suitable for any C application that needs to export telemetry data.
The library supports three OTel signals:
- Traces -- span creation, context propagation, baggage, sampling, events, links, and exceptions.
- Metrics -- synchronous and observable instruments (counters, histograms, gauges, up-down counters) with views.
- Logs -- structured log records with severity filtering and span correlation.
All SDK components are configured through a single YAML file.
Install required system packages using the provided script (run as root):
cd scripts/build
./linux-update.shTested Linux distributions (amd64): Debian 11/12/13, Ubuntu 20.04/22.04/24.04, Tuxedo 24.04, RHEL 8.10/9.5/10.0, Rocky 9.5, and openSUSE Leap 15.5/15.6.
Use the bundled build script to compile and install all prerequisite libraries
into /opt (or another prefix of your choice):
cd scripts/build
./build-bundle.sh [prefix-dir]By default, libraries are installed under /opt. A sequential alternative
(build.sh) is also available.
Autotools (recommended):
./scripts/bootstrap
./configure --prefix=/opt --with-opentelemetry=/opt
make -j8
make installTo build the debug variant (enables detailed internal logging):
./scripts/distclean
./scripts/bootstrap
./configure --prefix=/opt --with-opentelemetry=/opt --enable-debug
make -j8
make installCMake (alternative):
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/opt -DOPENTELEMETRY_DIR=/opt ..
make -j8
make installAdd -DENABLE_DEBUG=ON for the debug variant.
The library requires a YAML parser. By default it uses
rapidyaml, which is already built as
part of the OTel C++ SDK dependencies. Alternatively,
libfyaml can be selected with
--with-libfyaml (autotools) or -DWITH_LIBFYAML=ON (CMake). Both parsers
cannot be used at the same time.
Compile and run the test suite:
make test
cd test
./otel-c-wrapper-test --help
./otel-c-wrapper-test --runcount=10 --threads=8The test program uses otel-cfg.yml as its library configuration file.
Signal-specific test programs (test-tracer, test-meter, test-logger,
test-yaml) are also built by make test.
For integration testing with an OTel Collector and a backend such as
Elasticsearch/Kibana, use the Docker Compose setup in test/elastic-apm/.
A reference OpenTelemetry Collector
configuration is provided in test/otelcol/. It collects all three signals
(traces, metrics, and logs) over OTLP/gRPC and OTLP/HTTP, and exports traces
to a Jaeger instance reachable at a local IP address via OTLP/HTTP.
#include <stdio.h>
#include <stdlib.h>
#include <opentelemetry-c-wrapper/include.h>
int main(void)
{
struct otelc_tracer *tracer;
struct otelc_span *span;
char *err = NULL;
/* Initialize the library */
if (otelc_init("otel-cfg.yml", &err) != OTELC_RET_OK) {
fprintf(stderr, "Failed to init: %s\n", err);
free(err);
return 1;
}
/* Create and start a tracer */
tracer = otelc_tracer_create(&err);
if (tracer == NULL) {
fprintf(stderr, "Failed to create tracer: %s\n", err);
free(err);
otelc_deinit(NULL, NULL, NULL);
return 1;
}
tracer->ops->start(tracer);
/* Start a span, do work, end the span */
span = tracer->ops->start_span(tracer, "my-operation");
/* ... */
span->ops->end(&span);
/* Clean up */
otelc_deinit(&tracer, NULL, NULL);
return 0;
}#include <stdio.h>
#include <stdlib.h>
#include <opentelemetry-c-wrapper/include.h>
int main(void)
{
struct otelc_meter *meter;
struct otelc_value value;
char *err = NULL;
int64_t counter;
otelc_init("otel-cfg.yml", &err);
meter = otelc_meter_create(&err);
meter->ops->start(meter);
counter = meter->ops->create_instrument(meter, "requests", "Total request count", "1", OTELC_METRIC_INSTRUMENT_COUNTER_UINT64, NULL);
value.u_type = OTELC_VALUE_UINT64;
value.u.value_uint64 = 1;
meter->ops->update_instrument(meter, counter, &value);
otelc_deinit(NULL, &meter, NULL);
return 0;
}#include <stdio.h>
#include <stdlib.h>
#include <opentelemetry-c-wrapper/include.h>
int main(void)
{
struct otelc_logger *logger;
char *err = NULL;
otelc_init("otel-cfg.yml", &err);
logger = otelc_logger_create(&err);
logger->ops->start(logger);
logger->ops->log_span(logger, OTELC_LOG_SEVERITY_INFO, 0, NULL, NULL, NULL, NULL, 0, "Application started successfully");
otelc_deinit(NULL, NULL, &logger);
return 0;
}The API is organized around instance structs that each carry a pointer to an operations vtable, one pair per signal:
struct otelc_tracer-- creates trace spans and propagates context.struct otelc_meter-- creates and records metric instruments.struct otelc_logger-- emits structured log records.
Operations are invoked through the ops pointer:
tracer->ops->start_span(tracer, "name");Convenience macros OTELC_OPS() and OTELC_OPSR() (the latter passes &ptr
so the callee can NULL the pointer on destroy/end) are provided in
<opentelemetry-c-wrapper/define.h>:
OTELC_OPS(tracer, start_span, "name");
OTELC_OPSR(span, end);otelc_init(cfgfile, &err)-- parse the YAML configuration.otelc_*_create(&err)-- allocate a signal instance.instance->ops->start(instance)-- start the pipeline.- (use the signal) -- create spans, record metrics, emit logs.
otelc_deinit(&tracer, &meter, &logger)-- shut down and free.
Functions that can fail return OTELC_RET_OK (0) on success or
OTELC_RET_ERROR (-1) on failure. Functions that create resources return a
pointer on success or NULL on failure.
struct otelc_value-- tagged union (bool, integer, double, string).struct otelc_kv-- key-value pair (key string + otelc_value).struct otelc_text_map-- dynamic array of key-value string pairs.
Including <opentelemetry-c-wrapper/include.h> pulls in all public headers.
The YAML file passed to otelc_init() contains these top-level sections:
| Section | Purpose |
|---|---|
exporters |
Where telemetry is sent (OTLP, ostream, etc.) |
readers |
Periodic metric collection intervals |
samplers |
Trace sampling strategy |
processors |
Batching before export (batch or single) |
providers |
Resource attributes (service name, etc.) |
signals |
Binds the above components per signal type |
Minimal configuration exporting traces to stdout:
exporters:
my_exporter:
type: ostream
filename: /dev/stdout
processors:
my_processor:
type: single
samplers:
my_sampler:
type: always_on
providers:
my_provider:
resources:
- service.name: "my-service"
signals:
traces:
scope_name: "my-application"
exporters: my_exporter
samplers: my_sampler
processors: my_processor
providers: my_providerA complete example covering all three signals is in test/otel-cfg.yml.
| Exporter | Traces | Metrics | Logs |
|---|---|---|---|
| OTLP/gRPC | yes | yes | yes |
| OTLP/HTTP | yes | yes | yes |
| OTLP/File | yes | yes | yes |
| OStream | yes | yes | yes |
| In-Memory | yes | yes | -- |
| Zipkin | yes | -- | -- |
| Elasticsearch | -- | -- | yes |
All data-plane operations (creating spans, recording metrics, emitting logs) are thread-safe and can be called concurrently. Spans are stored in a 64-shard map with independent locks to distribute contention.
Lifecycle operations (otelc_init, otelc_*_create, start, destroy,
otelc_deinit) must be called from a single thread, typically during program
startup and shutdown.
Applications can provide a custom thread-ID function via otelc_ext_init()
before calling otelc_init().
README-- build instructions, API overview, configuration reference, and usage examples.MEMO-- in-depth design notes on multithreading, span lifecycle, context propagation, and memory management.README-sharded_map-- analysis of span handle management strategies.README-naming_convention-- function naming patterns for variadic, key-value, and array-based argument styles.README-configuration-- compile-time configuration macros and their effects on threading, context propagation, and provider architecture.TODO-- implemented features and planned enhancements.
This project is licensed under the Apache License 2.0.
Copyright (C) 2026 HAProxy Technologies.