      -----------------------------------------------------------------
       C wrapper for the C++ OpenTelemetry API, enabling C integration
      -----------------------------------------------------------------


Summary
------------------------------------------------------------------------------

   1. Introduction
   2. Build instructions
     2.1. Prerequisites for building the OTel C wrapper library
     2.2. Compiling and installing the OTel C++ client
     2.3. Compiling and installing the OTel C wrapper library
     2.4. Build options
   3. Testing the operation of the library
     3.1. Test programs
     3.2. Measurement and fuzzing tools
     3.3. Integration testing with a collector
   4. Basic concepts of OpenTelemetry
   5. Library API overview
     5.1. Objects and operations
     5.2. Lifecycle
     5.3. Contexts and ownership
     5.4. Return values and error reporting
     5.5. Utility types and functions
     5.6. Operation semantics
   6. YAML configuration
     6.1. Document structure
     6.2. Value conventions
     6.3. Background threads
     6.4. Exporters
     6.5. Samplers
     6.6. Processors
     6.7. Readers
     6.8. Providers
     6.9. Signals
     6.10. Environment variables
   7. Examples
     7.1. Tracing
     7.2. Metrics
     7.3. Logging
   8. Thread safety
   9. Known bugs and limitations
     9.1. Export drops and pipeline status
     9.2. Other limitations


1. Introduction
------------------------------------------------------------------------------

The OTel C wrapper library exposes the OpenTelemetry C++ client through a pure
C interface.  It covers all three telemetry signals -- traces, metrics and logs
-- and builds the pipeline behind each of them from a YAML configuration file,
so that a C program instruments its code against a small set of handles and
leaves the exporters, processors, samplers and resource attributes to that file.
The library was written primarily for the HAProxy OTel filter, to export the
telemetry that the analysis of software performance and behavior needs, but it
depends on nothing from HAProxy and serves any C program.  That filter has its
own repository at https://github.com/haproxytech/haproxy-opentelemetry .

The wrapper sits on the official OTel C++ client, whose source repository is at
https://github.com/open-telemetry/opentelemetry-cpp .  The build pins the client
to release 1.28.0, which the client project documents on its release page at
https://github.com/open-telemetry/opentelemetry-cpp/releases/tag/v1.28.0 ,
because the patch set that it applies to the client sources is prepared for
exactly that release; section 2.2 gives the reasons.  The wrapper itself also
compiles against an older client, with the parts that require the newer SDK
compiled out.

This document covers how to build the library, how to configure it and how to
call it.  It does not explain the internals of OpenTelemetry, nor the reasoning
that led to observability frameworks; section 4 summarizes only as much of the
terminology as the rest of the document relies on, and the official website
https://opentelemetry.io/ carries the full treatment.

Subjects that fall outside this document are covered by its companions:

  * README.md                - condensed overview for the repository front page
  * README-configuration     - compile-time macros of include/configuration.h
                               and the handle_map_shards key
  * README-naming_convention - naming scheme of the variadic and array-based
                               function variants
  * README-sharded_map       - design of the span handle maps and the
                               measurements behind it
  * README-meter_performance - meter locking, attribute handling and instrument
                               lookup measurements
  * MEMO                     - concurrency model of the OTel C++ API and the
                               internal design notes
  * ChangeLog                - release notes, grouped by package version
  * test/README-fuzz-yaml    - fuzzing harness for the configuration loader
  * test/README-speed_check  - throughput regression check

The library is distributed under the Apache License, Version 2.0, reproduced in
the LICENSE file; the copyright is held by HAProxy Technologies.


2. Build instructions
------------------------------------------------------------------------------

Building the wrapper takes three steps: install the development packages that
the build needs (section 2.1), build and install the OTel C++ client together
with its dependencies (section 2.2), and build the wrapper itself against that
client (section 2.3).  Section 2.4 lists the options that the wrapper build
accepts.

Note: the '%' prompt indicates that the command is being executed by a non-
      privileged user, whereas the '#' prompt indicates that the command is
      being executed by the root user.


2.1. Prerequisites for building the OTel C wrapper library
------------------------------------------------------------------------------

To simplify the process of compiling all the libraries required by the OTel C
wrapper, several shell scripts have been created and are available in the
scripts/build directory.  All scripts have been tested on the following Linux
distributions for the amd64 architecture:

  * debian 11 / 12 / 13
  * ubuntu 20.04.6 / 22.04.5 / 24.04.3 / 25.10
  * tuxedo 24.04.3
  * rhel 8.10 / 9.5 / 10.0
  * rocky 9.5
  * opensuse-leap 15.5 / 15.6

Linux distributions for other architectures, as well as other operating systems
supported by the OTel C++ client (such as BSD, macOS, and Windows), have not
been tested.

To install all the required packages for compiling and installing the OTel C
wrapper library, execute the linux-update.sh script located in the scripts/build
directory:

  # cd scripts/build
  # ./linux-update.sh

The script takes no arguments and works interactively, asking for confirmation
before each step it performs.

To summarize, the script installs the necessary packages for compiling and
setting up all required libraries, with the most important ones being:

  * GNU GCC Compiler and Development Environment
  * GNU autoconf / automake / libtool / make
  * Kitware CMake
  * various developer packages of the system libraries (libc, curl, ssl, zlib,
    lzma, systemd)
  * various utilities for downloading source code repositories (git, wget)


2.2. Compiling and installing the OTel C++ client
------------------------------------------------------------------------------

Installing all the prerequisite libraries can be quite demanding, which is why
several installation scripts have been created to simplify the process.  Each
prerequisite library has its own dedicated installation script, but it is not
recommended to run them separately.  Instead, it's advised to use one of the
following two scripts:

  * build.sh - each prerequisite library is compiled and installed individually,
    following a predefined order
  * build-bundle.sh - the SSL library and curl are compiled first, zlib too in
    a static build, and the cmake configuration of the OTel C++ client then
    downloads and compiles the remaining prerequisite libraries itself

It is strongly recommended to use the provided build scripts rather than relying
on system-installed dependency packages, which are likely outdated or compiled
with options incompatible with the OTel C wrapper.  The version of the OTel
C++ client used is set to 1.28.0 because the *-opentelemetry-cpp-1.28.0.patch
set in scripts/build/ is prepared for exactly that SDK release.  Besides build
adjustments, the patches extend the SDK exporters with methods that the wrapper
requires: MaybeSpawnBackgroundThread() pre-spawns the exporter's background
threads and connections, which the unpatched SDK creates lazily on the first
export, so that no thread creation or connection setup is deferred until then,
while SetBackgroundWaitFor() makes the idle timeout of the OTLP/HTTP exporter
background thread configurable.  Another SDK release cannot be used without
porting the patch set.

If none of the attached build-*.sh scripts is used, then the opentelemetry-cpp
patches in scripts/build/ must be applied to the OpenTelemetry C++ source tree
before the compilation and the same CMake configuration options found in the
scripts/build/opentelemetry-cpp-1.28.0-install.sh script must be used.

Whichever script is used, the result should be the same.  However, the use of
the build-bundle.sh script is recommended for this task.  It builds AWS-LC first
and compiles curl against it, so that the whole bundle shares one SSL library;
the curl of the system is left out on purpose, as it is tied to the SSL library
of the system, and two such libraries in one process clash.  OpenSSL can take
the place of AWS-LC when the comment marks of the two rows in build-bundle.sh
and build.sh are swapped.  In a dynamic build zlib comes from the system, while
a static build compiles it too, as an archive installed into the prefix ahead
of the SDK, which takes zlib only as an installed package, and the system ships
it as a shared object.

Example of how to run an installation script:

  # cd scripts/build
  # ./build-bundle.sh

Both scripts take their arguments positionally:

  # ./build.sh        [ prefix-dir [ install-dir ] ]
  # ./build-bundle.sh [ prefix-dir [ install-dir [ lib-type ] ] ]

The first argument names the directory that the packages are installed into and
defaults to /opt.  The second names the root directory of the installation, and
setting it is not recommended, as it serves debugging alone.  The third one,
which build-bundle.sh alone accepts, controls how the OTel C++ SDK libraries are
built: "dynamic", the default, or "static".  With "static" the SDK is compiled
as archives (.a) instead of shared objects (.so), and so are the SSL library,
zlib and curl; that is what linking the OTel C wrapper statically requires.  A
prefix should be emptied before it is reused for the other lib-type.

Finally, the installation script will verify that all library dependencies for
programs in the <prefix>/bin directory and libraries in the <prefix>/lib
directory are met, which is done by running the ldd utility.

Note: it is possible that some prerequisite libraries are already installed on
      the system (as part of the operating system).  This can lead to errors
      when compiling the OTel C++ client.  For this reason, it is recommended to
      use the provided script for installation and to install it in a non-system
      directory, such as /opt, /usr/local, or any other non-system directory of
      your choice.

A host without network access is served by the opentelemetry-cpp-monorepo.sh
script, which assembles a self-contained source tree: it clones the client and
the pinned dependency sources into the layout that the client build expects,
drops the origin remote from every clone, and leaves a .monorepo marker behind.

  % ./opentelemetry-cpp-monorepo.sh [ target-dir [ keep-remotes ] ]

A non-empty second argument keeps the upstream remotes attached.  The install
script recognizes the marker and builds such a tree as it stands: the bundled
sources are used unchanged instead of being fetched again, and the dependencies
that the tree carries are never taken from an installed package, so that a stale
installation cannot hijack the build.  The curl sources, and in a static build
the zlib sources as well, are taken from the assembled tree when it carries
them.  The SSL library is not part of that tree; its release tarball is used as
it is when placed next to the scripts.

List of (almost) all dependencies for the OTel C++ client package:

  * AWS libcrypto (AWS-LC)
    https://github.com/aws/aws-lc
  * OpenSSL - a TLS/SSL and crypto library, usable in place of AWS-LC
    https://github.com/openssl/openssl
  * curl - a command-line tool for transferring data from or to a server using
    URLs
    https://github.com/curl/curl
  * zlib - a general purpose data compression library
    https://github.com/madler/zlib
  * Abseil - C++ Common Libraries
    https://github.com/abseil/abseil-cpp
  * c-ares - a modern DNS (stub) resolver library
    https://github.com/c-ares/c-ares
  * RE2, a regular expression library
    https://github.com/google/re2
  * Protocol Buffers - Google's data interchange format
    https://github.com/protocolbuffers/protobuf
  * JSON for Modern C++
    https://github.com/nlohmann/json
  * GoogleTest - Google's C++ test framework
    https://github.com/google/googletest
  * Benchmark - a library to benchmark code snippets, similar to unit tests
    https://github.com/google/benchmark
  * gRPC - an RPC library and framework
    https://github.com/grpc/grpc
  * Rapid YAML - a C++ library to parse and emit YAML
    https://github.com/biojppm/rapidyaml
  * OpenTelemetry C++ - the C++ OpenTelemetry client
    https://github.com/open-telemetry/opentelemetry-cpp

This is not a complete list of dependencies; other libraries present on the
operating system, such as libidn, libpsl, libunistring, zlib, and libzstd, are
also required.

Additional information on this topic can be found at:
https://github.com/open-telemetry/opentelemetry-cpp/blob/main/docs/dependencies.md


2.3. Compiling and installing the OTel C wrapper library
------------------------------------------------------------------------------

Along with the OTel C++ client, the OTel C wrapper library depends on a YAML
parsing library.  By default, rapidyaml (ryml) is used, and this is the
recommended configuration.  Since rapidyaml is already built and installed as
part of the OTel C++ client dependencies, no additional steps are required.

Alternatively, libfyaml can be used instead, through the --with-libfyaml option
(autotools) or with -DWITH_LIBFYAML=ON (CMake), either of which overrides the
rapidyaml default.  Requesting both parsers explicitly, with --with-libfyaml
and --with-rapidyaml together, fails at configuration time.

  * Rapid YAML - a C++ library to parse and emit YAML
    https://github.com/biojppm/rapidyaml
  * libfyaml - a fully-featured YAML 1.2 and JSON parser/writer
    https://github.com/pantoniou/libfyaml

The OTel C wrapper requires the OTel C++ client to be built with ABI version 2
enabled (CMake option -DWITH_ABI_VERSION_2=ON, which the provided installation
scripts already set).  Both the autotools and CMake builds check the ABI version
at configuration time and fail if it is not 2.

Once the OTel C++ client is installed, the OTel C wrapper library can be
compiled and installed.  In this example, we will install two versions of the
library: the release version first, followed by the debug version.  Both
versions will be installed in the /opt directory.

  % git clone https://github.com/haproxytech/opentelemetry-c-wrapper.git
  % cd opentelemetry-c-wrapper
  % ./scripts/bootstrap
  % ./configure --prefix=/opt --with-opentelemetry=/opt
  % make
  # make install

  % ./scripts/distclean
  % ./scripts/bootstrap
  % ./configure --prefix=/opt --with-opentelemetry=/opt --enable-debug
  % make
  # make install

Alternatively, the library can be compiled using CMake:

  % mkdir build && cd build
  % cmake -DCMAKE_INSTALL_PREFIX=/opt -DOPENTELEMETRY_DIR=/opt ..
  % make
  # make install

To build the debug version, add the -DENABLE_DEBUG=ON option to the cmake
command above.  The two versions live side by side under one prefix, because
the debug library and its pkg-config file both carry a _dbg suffix.

The wrapper library can also be built as a static archive (.a) instead of a
shared library (.so).  This requires the OTel C++ SDK to be compiled as static
libraries as well (see section 2.2, lib-type argument).

With autotools, both static and shared libraries are built by default.  To build
only a static library, pass --disable-shared:

  % ./configure --prefix=/opt --with-opentelemetry=/opt \
        --disable-shared

With CMake, use the BUILD_STATIC option:

  % cmake -DCMAKE_INSTALL_PREFIX=/opt -DOPENTELEMETRY_DIR=/opt \
        -DBUILD_STATIC=ON ..


2.4. Build options
------------------------------------------------------------------------------

Both build systems accept the same set of options, each in its own spelling.
The autotools form is given first below, the CMake form second.

  --enable-debug, -DENABLE_DEBUG=ON
      Build the debug version of the library, which logs the internal operation
      in detail, down to the individual function calls, and links the tracking
      allocator.  The result installs under the _dbg suffix.

  --enable-warnings, -DENABLE_WARNINGS=ON
      Add the extended compiler warning set to CFLAGS and CXXFLAGS.

  --enable-gprof, -DENABLE_GPROF=ON
      Build with the gprof profiling instrumentation.

  --enable-asan, -DENABLE_ASAN=ON
      Build with the address sanitizer.

  --enable-tsan, -DENABLE_TSAN=ON
      Build with the thread sanitizer.  The two sanitizers are mutually
      exclusive, and asking for both fails at configuration time.

  --enable-ubsan, -DENABLE_UBSAN=ON
      Build with the undefined behavior sanitizer, which combines with
      either of the other two.

  --enable-hardening, -DENABLE_HARDENING=ON
      Add the toolchain hardening flags: the fortified source define, the
      stack protector, the stack clash and control flow protections, and
      the read-only relocation, immediate binding and non-executable
      stack link flags.  Each flag is used only if the toolchain accepts
      it, and the fortified source define is skipped in a debug build,
      whose missing optimization it requires.

  --disable-threads, -DENABLE_THREADS=OFF
      Drop the pthread support that is otherwise compiled and linked in.

  --disable-shared, -DBUILD_STATIC=ON
      Build the static archive alone; autotools builds both the shared object
      and the archive by default.

  --with-opentelemetry[=DIR], -DOPENTELEMETRY_DIR=DIR
      Install prefix of the OTel C++ client.

  --with-rapidyaml[=DIR], -DWITH_RAPIDYAML=ON -DRAPIDYAML_DIR=DIR
      Use the rapidyaml parser, which is what both builds do by default.

  --with-libfyaml[=DIR], -DWITH_LIBFYAML=ON -DLIBFYAML_DIR=DIR
      Use the libfyaml parser in place of rapidyaml.

Running the test programs of a thread sanitizer build needs the suppression file
test/tsan.supp, which silences the races that the sanitizer reports inside the
uninstrumented SDK; the file describes its own use in its header.


3. Testing the operation of the library
------------------------------------------------------------------------------

The test/ directory holds the test programs, the configuration they read and
the tooling built around them.  Section 3.1 covers the programs, section 3.2
the measurement and fuzzing tools, and section 3.3 the integration tests against
a collector and a backend.


3.1. Test programs
------------------------------------------------------------------------------

The test programs are not compiled during the regular build.  The 'make test'
target, which stands for 'make check', builds every one of them:

  % make test
  % cd test
  % ./otel-c-wrapper-test --help
  % ./otel-c-wrapper-test --runcount=10 --threads=8

The programs are:

  * otel-c-wrapper-test - simulates a worker process that generates traces,
                          metrics and logs from several threads at once
  * test-tracer         - exercises the tracer, span and context propagation
                          operations
  * test-meter          - exercises the meter, instrument and view operations
  * test-logger         - exercises the logger operations, including the
                          severity threshold
  * test-yaml           - exercises the configuration loader, its failure
                          paths included
  * test-multi          - exercises several contexts and signal instances
                          side by side
  * test-memory         - exercises the debug memory allocator, the rejected
                          double frees included (debug build only)

Every program reads its configuration from otel-cfg.yml in the current directory
and looks up the signal entry named 'default'; the --config and --name options
override both.  The main program takes the run length, the thread and instance
counts, a delay and a random seed on top of that, and --help lists them all.
Each program prints a PASS or FAIL line per test case and leaves a non-zero
exit status behind when a case fails.

A debug build appends _dbg to every program name.  Only the main program is
installed, into the <prefix>/bin directory, and only when it was built before
'make install' ran.

The names in the test directory are libtool wrapper scripts.  The real binaries
sit in test/.libs and carry an rpath to the installed library, so running one
of them directly picks up the installed copy rather than the one just built.
Run the wrapper scripts instead, and when a debugger or valgrind needs the real
binary, put the src/.libs directory of the build tree first in LD_LIBRARY_PATH,
the way the gdb.sh script does.


3.2. Measurement and fuzzing tools
------------------------------------------------------------------------------

Several shell scripts in the test directory drive the programs beyond the plain
test runs:

  * speed.sh       - runs the throughput measurement against the speed_test
                     signal entries, whose exporters write to /dev/null so
                     that the measured cost is the API path itself
  * speed-check.sh - compares a run against a machine-local baseline and
                     fails when the throughput or the scaling between the
                     worker groups regresses
  * speed-show.sh  - groups the worker counts of a run and reports the rate
                     range of each group
  * fuzz-yaml.sh   - builds the library with clang into an out-of-tree
                     directory and runs the libFuzzer harness over the
                     configuration loader
  * gdb.sh         - starts the main program under gdb against the freshly
                     built library

The documents test/README-speed_check and test/README-fuzz-yaml describe the
throughput check and the fuzzing harness in detail.


3.3. Integration testing with a collector
------------------------------------------------------------------------------

The test/otelcol directory carries a reference configuration for running an
OpenTelemetry Collector beside the test programs; its sources live at
https://github.com/open-telemetry/opentelemetry-collector .  It receives all
three signals over OTLP/gRPC and OTLP/HTTP, and it takes traces over the Jaeger
and Zipkin protocols as well.  Traces leave through the debug exporter and over
OTLP/HTTP to an endpoint on the local network, while metrics and logs reach the
debug exporter alone.  The README in that directory walks through the whole
configuration.

For a complete backend, the test/elastic-apm directory holds a Docker Compose
stack of Elasticsearch, Kibana and APM Server:

  % cd test/elastic-apm
  % docker compose up -d

The APM Server takes OTLP over HTTP on port 8200 and Kibana answers on port
5601; the README in that directory lists the endpoints and the credentials.
Whichever of the two is running, the otel-cfg.yml that the test programs read
has to point its exporters at that endpoint.


4. Basic concepts of OpenTelemetry
------------------------------------------------------------------------------

OpenTelemetry (OTel) is an observability framework for cloud-native software.
It provides a standardized, vendor-neutral way to create and collect telemetry
data.  The terms collected here are the ones that the rest of this document
relies on; the official concepts page, https://opentelemetry.io/docs/concepts/ ,
treats them at length.

The components of OTel are:

  * Signal: a category of telemetry data.  The three signals are traces,
    metrics and logs, and this library implements all of them.

  * API: the set of interfaces that instrumented code calls to obtain a tracer,
    start a span or record a measurement.  It is decoupled from the
    implementation behind it.

  * SDK: the official implementation of the API.  It holds the configuration
    and the logic that processes telemetry data, and it is what the exporters,
    processors and samplers plug into.

  * Exporter: the component that hands telemetry data to a particular backend
    or collector.  The wrapper carries OTLP exporters over files, gRPC and
    HTTP, a Zipkin exporter for traces and an Elasticsearch exporter for logs,
    among others; section 6.4 lists them.

  * Collector: a standalone service that receives, processes and exports
    telemetry data.  It acts as a flexible pipeline in which the data can be
    transformed and filtered before it reaches the observability backend.

  * Resource: the set of attributes that identify the process producing the
    telemetry, such as service.name and service.version.  Every span, metric
    data point and log record carries them; section 6.8 configures them.

  * Instrumentation scope: the name identifying the instrumented component
    inside the process, given per signal as scope_name.  The wrapper reports a
    fixed scope version and schema URL alongside that name.

Concepts of the traces signal:

  * Trace: the journey of a request as it moves through all the services of a
    distributed system.  A single trace is composed of one or more spans.

  * Span: a single unit of work within a trace, such as an HTTP request, a
    database query or a function call.  A span has a start time, an end time,
    attributes, events, links to related spans in other traces, and a status.

  * Context propagation: the mechanism that correlates spans across services.
    The caller injects the current trace and span identifiers into the request,
    typically as HTTP headers, and the callee extracts them and opens its own
    span as a child of the one that made the call.

  * Sampling: the decision whether a span is recorded and exported, taken by a
    sampler as the span starts.  It is what keeps the volume of trace data
    under control, and the parent-based samplers carry the decision taken for
    the root span down the whole trace; section 6.5 configures them.

Concepts of the metrics signal:

  * Instrument: the handle that a measurement is recorded through.  Counters
    and up-down counters accumulate, gauges hold the last value written and
    histograms distribute the values over buckets.  A synchronous instrument
    is written by the instrumented code, while an observable one is read
    through a callback when the collection happens.

  * View: a rule that renames an instrument or changes the way its
    measurements are aggregated, the bucket bounds of a histogram included.

  * Reader: the component that collects the instruments at a fixed interval
    and hands the result to an exporter; section 6.7 configures the readers.

Concepts of the logs signal:

  * Log record: a timestamped message with a severity, a body and attributes,
    which the emitting code can tie to a span so that the record and the trace
    it belongs to can be read together.

  * Severity: the level of a record, running from TRACE to FATAL.  Records
    below the configured threshold are dropped before reaching the processor.

For traces and logs a processor sits between the API and the exporter, either
batching the records or passing them on one at a time; the metrics signal uses
a reader in its place.  Section 6 describes how a configuration assembles these
parts into the pipelines of a signal.


5. Library API overview
------------------------------------------------------------------------------

The library provides a pure C API on top of the OpenTelemetry C++ SDK.  All
public headers reside under include/opentelemetry-c-wrapper/, and including
<opentelemetry-c-wrapper/include.h> pulls in every one of them.  Section 5.1
introduces the objects the API is built from, section 5.2 the order in which
they are created and destroyed, section 5.3 the ownership rules that govern
their lifetimes, section 5.4 the return values, section 5.5 the utility types
and functions that surround them, and section 5.6 what a repeated invocation
of an operation does.


5.1. Objects and operations
------------------------------------------------------------------------------

The API is organized around instance structs that each carry a pointer to an
operations vtable, a struct of function pointers.  One such pair exists per
telemetry 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

A tracer additionally hands out two more handle types, which follow the same
convention.  Neither of them holds telemetry state: each is an index into an
internal handle map, and a span records the tracer it belongs to as well.

  * struct otelc_span         - one started, not yet ended, trace span
  * struct otelc_span_context - a span identity, built from raw IDs or
                                extracted from a carrier

The three signal instances share a common layout.  Every one of them carries
an 'err' member holding the text of the last error the instance recorded, a
'scope_name' member with the instrumentation scope name read from the YAML
configuration, an 'enabled' flag that suppresses the work of the instance while
it is false, a 'flush_timeout' budget in milliseconds for the provider flush
performed by the destroy operation (a zero budget, or a flush that runs out of
it, shuts the exporters down instead, dropping the queued telemetry), a 'ctx'
pointer to the owning context, and the 'ops' pointer to the vtable.  A logger
carries a 'min_severity' member as well.  The remaining members are internal,
and the library context itself, struct otelc_ctx, stays opaque.

Operations are invoked through the ops pointer, with the instance repeated as
the first argument:

  tracer->ops->start_span(tracer, "name")

The header <opentelemetry-c-wrapper/define.h> provides the convenience macros
OTELC_OPS() and OTELC_OPSR(), which supply that first argument themselves.  The
"R" variant passes the address of the handle, so that the callee can set it to
NULL in the destroy and end operations:

  OTELC_OPS(tracer, start_span, "name")
  OTELC_OPSR(span, end)

Both macros are written as statement expressions over __typeof__ and therefore
need GCC or Clang; the plain call through the ops pointer stays available to
every compiler.


5.2. Lifecycle
------------------------------------------------------------------------------

The typical usage follows this lifecycle:

  1. ctx = otelc_init(cfgfile, name, &err) - parse the YAML configuration file
                                             and create a library context
  2. otelc_*_create(ctx, &err)             - allocate a signal instance bound
                                             to the context
  3. instance->ops->start(instance)        - start the signal pipeline
  4. (use the signal)                      - create spans, record metrics,
                                             emit logs
  5. otelc_deinit(...)                     - shut down all signals, release
                                             the context, free memory
  6. otelc_lib_shutdown()                  - reset process-wide hooks; call once
                                             after the final context

The 'name' argument to otelc_init() selects which named entry to read from each
subtree of the 'signals' section in the YAML file.  When no entry with that name
exists, the entry called 'default' is used as a fallback; when that is also
absent, a configuration written in the legacy layout, with the settings placed
directly under the signal subtree, is accepted as a last resort.  The outcome
of the lookup is recorded per signal section at otelc_init() time and can be
read back with otelc_ctx_nstate_get().  If no variant is present, creating the
corresponding signal fails; see section 6.9 for the details.

The otelc_deinit() function accepts pointers to the context and to all three
signal types and destroys whichever ones are non-NULL:

  otelc_deinit(&ctx, &tracer, &meter, &logger);

otelc_deinit() first flushes each provider within its instance's flush_timeout
budget, and shuts the exporters down instead, dropping the queued telemetry,
when that budget is zero or runs out; see section 5.1.

It only touches per-context state.  The SDK internal log handler installed via
otelc_log_set_handler() and the malloc/free/thread-id callbacks registered via
otelc_ext_init() are process-wide and are left untouched, so deinitializing one
context does not disturb the hooks that other live contexts rely on.  Once the
final context has been destroyed, call otelc_lib_shutdown() to reset those hooks
to their library defaults; this is required before unloading caller code that
owns any of the registered callbacks, and is otherwise optional.


5.3. Contexts and ownership
------------------------------------------------------------------------------

All configuration and provider state is per-context, so multiple contexts may
coexist in the same process, each loaded from a distinct configuration file
or selecting a distinct named signal entry.  The helper otelc_close_cfg(ctx)
releases the parsed YAML document attached to a context independently of the
providers, once every signal instance has been created and started.  The create
functions and the start operation read the document, so after the call no signal
instance can be created against the context and no existing instance can be
started, for the first time or again; the name states recorded by otelc_init()
stay readable.

Tracers, meters and loggers each fully own their own SDK provider, exporters,
processors and (for tracers) text-map propagator; meters also own their own
distinct instrument and view registries.  The shared span and the span-context
handle maps are reference-counted by the tracers and torn down only when the
last tracer is destroyed, so destroying any one tracer does not invalidate the
spans owned by another tracer that is still in use.  A span or span context
that outlives the last tracer, and with it the handle maps, supports only the
end, end_with_options and destroy operations; any other operation on such a
leftover instance fails and, in the default build, records no error message.
A span whose own tracer is gone while other tracers keep the maps alive stays
usable at the SDK level, but its error reporting and its inject operation still
reach into the freed tracer structure, so the spans of a tracer must be ended
before the tracer is destroyed.

Strings handed to the caller through an err argument are allocated by the API
and are released with OTELC_SFREE(), the macro that tolerates a null pointer.
The err member of an instance is different: it belongs to that instance, is
replaced whenever a newer error is recorded, and is freed when the instance is
destroyed, so a caller reads it but never frees it.


5.4. Return values and error reporting
------------------------------------------------------------------------------

Functions that create resources return a pointer on success, or NULL on failure,
and report the reason through their err argument.  Functions that merely succeed
or fail return OTELC_RET_OK (0) or OTELC_RET_ERROR (-1) and, once an instance
exists, leave the reason in the err member of that instance.

Two families depart from the plain pair, and both are recognized by testing
for OTELC_RET_ERROR rather than for OTELC_RET_OK, since a false answer and a
success share the value zero:

  * the enabled predicates return true or false, or OTELC_RET_ERROR
  * create_instrument() returns a non-negative instrument ID and
    otelc_ctx_nstate_get() an otelc_ctx_name_t value, or OTELC_RET_ERROR


5.5. Utility types and functions
------------------------------------------------------------------------------

The library provides several utility types for passing structured data to the
API:

  * struct otelc_value    - a tagged union carrying a bool, a signed or
                            unsigned 32-bit or 64-bit integer, a double, a
                            string, or a block of binary data, with a null
                            variant for the absent value
  * struct otelc_kv       - a key-value pair (key string + otelc_value)
  * struct otelc_text_map - a dynamic array of key-value string pairs, each
                            pair carrying flags that say whether the map
                            duplicates or adopts the key and the value

Additional utility functions are also available; the most notable are:

  * otelc_ext_init()            - register custom malloc/free/thread-ID
  * otelc_log_set_handler()     - install an SDK diagnostic log callback
  * otelc_log_set_level()       - set the SDK internal log level
  * otelc_lib_shutdown()        - reset process-wide library hooks
  * otelc_ctx_nstate_get()      - read how the context name resolved
  * otelc_pipeline_status_get() - read the export-pipeline status
  * otelc_span_context_create() - construct a span context from raw IDs


5.6. Operation semantics
------------------------------------------------------------------------------

What a repeated invocation of an operation does depends on the operation, and
every mutating operation falls into one of the classes below.  The class is
decided partly by the wrapper itself and partly by the OpenTelemetry C++ SDK
underneath it; the _var, _kv_var and _kv_n variants of one base name always
share the class of that name.

  * additive          - every call appends a new item
  * overwrite per key - the last call wins for the named key, the others stay
  * last wins         - a single slot that every call replaces
  * once              - the first call decides; a repeat fails or is ignored
  * per call          - no shared state, so every call acts independently or
                        returns a fresh result

Common to the tracer, the meter and the logger:

  * start             - last wins: every start flushes the previous provider
                        within the flush budget, builds the provider anew and
                        replaces the previous one, whose exporters are shut
                        down first; exactly as for destroy, every concurrent
                        operation must be drained before a restart and, for a
                        tracer, all its spans must have been ended; a span still
                        open then belongs to the pipeline that start shuts down
                        and is dropped when it ends
  * set_enabled       - last wins
  * set_flush_timeout - last wins
  * force_flush       - per call
  * shutdown          - once: the SDK provider shuts down on the first call; a
                        repeat does no work and returns without an error
  * destroy           - once: the operation consumes the handle and sets the
                        caller's pointer to NULL

The tracer adds:

  * start_span            - per call: every call starts one new, independent
                            span; the with-options variant fixes the parent,
                            the kind, the timestamps and creation-time links,
                            which no later operation can amend
  * extract_text_map      - per call: every call builds one new span context
                            from the carrier
  * extract_http_headers  - per call, exactly like extract_text_map, over the
                            HTTP header carrier

The span:

  * set_attribute       - overwrite per key with the ostream exporter, whose
                          span data keeps the attributes in a map keyed by the
                          attribute name; the OTLP exporters append every call,
                          so a repeated key is exported twice
  * add_event           - additive: every call appends one more span event
  * add_link            - additive: every single call appends one more link
  * record_exception    - additive: every call appends one more event named
                          'exception' that carries the type, the message, the
                          stacktrace and the attributes
  * set_status          - last wins: the SDK stores the code and description
                          with no precedence at all, so a later call replaces
                          an earlier error
  * set_operation_name  - last wins
  * set_baggage         - overwrite per key: a repeated key replaces its value,
                          per the W3C baggage rules
  * inject_text_map     - last wins at the carrier: every invocation releases
                          the entries a previous one stored in the writer's map
                          and writes the current set
  * inject_http_headers - last wins, exactly like inject_text_map, over the
                          HTTP header carrier
  * end                 - once: the very first call hands the span data over
                          to the processor and consumes the handle, as destroy
                          does, so no later operation can reach the span
  * end_with_options    - once, exactly like end; the status argument is applied
                          first, under the set_status semantics
  * destroy             - once: consumes the handle; a span that was never ended
                          is ended implicitly

The span context:

  * trace_state_set     - per call: the modified W3C header is returned with
                          the key replaced in the result; the stored context
                          itself never changes
  * trace_state_delete  - per call, exactly like trace_state_set, with the key
                          removed from the result
  * destroy             - once: consumes the handle

The meter adds:

  * create_instrument       - once per name and type pair: a repeated create
                              returns the existing instrument ID, and the first
                              creator's description and unit stay in effect
  * update_instrument       - additive for the counters and the histograms,
                              which accumulate every measurement; last wins
                              for a gauge, which keeps the latest value per
                              attribute set within a collection cycle; no-op
                              for an observable instrument, whose callback
                              supplies the value
  * add_view                - once per view name: a repeated name returns the
                              existing view ID
  * add_instrument_callback - additive: every call registers one more observable
                              callback on the instrument, and the operation
                              remove_instrument_callback removes one of them
                              per call

The logger adds:

  * log              - additive: every single call emits one more log record;
                       log_span, log_body and log_body_span behave the same
                       and differ only in how the body and the span correlation
                       are supplied
  * set_min_severity - last wins

Section 5.3 describes what remains callable on a span or a span context that
outlives its tracer.


6. YAML configuration
------------------------------------------------------------------------------

The library reads its configuration from a YAML file whose path is passed to
otelc_init().  The named pipeline components -- exporters, samplers, processors,
readers and providers -- are defined in top-level sections, and the 'signals'
section binds them together per signal type, under named entries.  Each call to
otelc_init() also takes a context name that selects the named entry to load,
with a fallback to 'default'; section 6.9 describes the lookup.

The three sections that follow cover the material shared by the whole document:
section 6.1 the layout, section 6.2 the value syntax common to every key, and
section 6.3 the thread settings common to every component that runs a background
thread.  The remaining sections document one top-level section each.


6.1. Document structure
------------------------------------------------------------------------------

The YAML file contains the following top-level sections:

  * exporters  - define where telemetry data is sent
  * samplers   - control trace sampling strategy (traces only)
  * processors - define how telemetry is batched before export (traces and logs
                 only)
  * readers    - configure periodic metric collection intervals (metrics only)
  * providers  - set resource attributes attached to all telemetry
  * signals    - bind the above components together per signal type

Each section except 'signals' contains named configuration blocks.  The
'signals' section groups its 'traces', 'metrics' and 'logs' subtrees by name,
and each named entry references the other top-level blocks by name, either as a
single name or as a YAML list of names.

Besides these sections the document accepts the optional top-level scalar key
'handle_map_shards', which sets the shard count of the span and span context
handle maps.  Its value must be a power of two in the range 1..65536 and takes
effect on the first otelc_init() call; README-configuration describes the key
and its limitations in full.

Here is a minimal configuration that exports traces to stdout:

  exporters:
    my_exporter:
      type:     ostream
      filename: stdout

  processors:
    my_processor:
      type: single

  samplers:
    my_sampler:
      type: always_on

  providers:
    my_provider:
      resources:
        - service.name: "my-service"

  signals:
    traces:
      default:
        scope_name: "my-application"
        exporters:  my_exporter
        samplers:   my_sampler
        processors: my_processor
        providers:  my_provider

A complete configuration covering all three signals, multiple exporter types,
and multiple named entries can be found in the file test/otel-cfg.yml.


6.2. Value conventions
------------------------------------------------------------------------------

The rules described here apply to every key documented in this section, and the
per-key descriptions that follow state only the deviations from them.

String values are limited to 4095 characters and longer values are truncated.
A key whose value is empty is treated as if the key were absent, so the default
documented for that key applies.  Keys that the addressed component does not
read are ignored, which means a misspelled key is silently left out instead of
being reported.

Boolean values accept "true" and "1" for true, and "false" and "0" for false.
Integer values are converted with base detection, so a decimal, a 0x-prefixed
hexadecimal and a leading-zero octal form are all accepted.  Both integer and
floating-point values are validated against the inclusive range stated with each
key, and a value outside that range fails the load.

Type names and enumerated values, such as the exporter 'type' or the sampler
'delegate', are matched without regard to case.  A value matching none of the
accepted spellings is reported as an error rather than silently replaced by
the default.

Duplicate keys within a mapping are rejected at load time by rapidyaml, the
default parser; the libfyaml parser does not detect them.


6.3. Background threads
------------------------------------------------------------------------------

Batch processors, the OTLP File and OTLP HTTP exporters and periodic metric
readers each run a background thread.  Every one of these components accepts
the same two optional keys, which control the operating system properties of
that thread:

  thread_name (string, default: "")
      Name assigned to the background thread of the component.  It is set with
      pthread_setname_np() and truncated to 15 characters, and is useful for
      identifying SDK threads in debuggers and monitoring tools.

  cpu_id (integer, default: -1)
      Binds the thread to a single CPU core through pthread_setaffinity_np().
      Accepts a core number from 0 to OTEL_MAX_CPU_ID (4095), or -1 to leave
      the affinity unset.  The setting requires sched.h and is therefore only
      effective on Linux.

Both keys are set per component, alongside the other keys of that component:

  processors:
    my_processor:
      type:        batch
      thread_name: "proc/batch trac"
      cpu_id:      2

The two keys reach the thread through the SDK thread instrumentation interface,
which the SDK build must enable with ENABLE_THREAD_INSTRUMENTATION_PREVIEW.
Without that flag the values are still read and validated, but never applied.
A processor of type 'single' accepts them as well and ignores them, because it
exports on the calling thread.

The OTLP gRPC exporter accepts both keys in the YAML file for configuration
consistency, but does not apply them, because the OTel C++ SDK does not provide
runtime options for gRPC exporter threads.  The Elasticsearch log exporter also
spawns a background thread, yet accepts neither key; its thread runs with the
process defaults.


6.4. Exporters
------------------------------------------------------------------------------

Exporters define where telemetry data is sent.  Each named exporter block must
include a 'type' key that selects the exporter backend.  The available exporter
types and their signal support are:

  Type              Traces  Metrics  Logs
  ----              ------  -------  ----
  otlp_file         yes     yes      yes
  otlp_grpc         yes     yes      yes
  otlp_http         yes     yes      yes
  ostream           yes     yes      yes
  memory            yes     yes      no
  zipkin            yes     no       no
  elasticsearch     no      no       yes

Exporter availability depends on the build configuration; unsupported types
produce an error at startup, as does a type used with a signal that the table
above marks as unsupported.

What the build found is published in the generated header otel_cpp.h as one
OTELC_HAVE_EXPORTER_<NAME> macro per exporter type, set to 1 when the exporter
is present and to 0 when it is not, so a program or a test can pick its exporter
at compile time instead of failing at start().  The <NAME> part is the type of
the table above in upper case, except for 'memory', which is spelled IN_MEMORY.

The configuration keys for each exporter type are listed below.  All keys are
optional unless noted otherwise.  The thread_name and cpu_id keys described in
section 6.3 are omitted from the lists.


OTLP File exporter (otlp_file)

Writes telemetry data to rotating files in OTLP format.

  type (string, mandatory)
      Must be "otlp_file".

  file_pattern (string, default: "otel-logfile-%F-%N.txt")
      Output file naming pattern.

  alias_pattern (string, default: "")
      Symbolic link or alias pattern for the current output file.

  flush_interval (integer, default: 30000000, range: 100000-60000000)
      Flush interval in microseconds.

  flush_count (integer, default: 256, range: 16-8192)
      Number of records buffered before flushing to disk.

  file_size (integer, default: 20971520, range: 65536-2147483648)
      Maximum file size in bytes (64 KB to 2 GB).

  rotate_size (integer, default: 3, range: 1-256)
      Number of rotated files to retain.


OTLP gRPC exporter (otlp_grpc)

Sends telemetry data to a collector over gRPC.

  type (string, mandatory)
      Must be "otlp_grpc".

  endpoint (string)
      Server endpoint URL.  Signal-specific defaults:
        traces:  "http://localhost:4317/v1/traces"
        metrics: "http://localhost:4317/v1/metrics"
        logs:    "http://localhost:4317/v1/logs"

  use_ssl_credentials (boolean, default: false)
      Enable TLS for the gRPC connection.

  ssl_credentials_cacert_path (string, default: "")
      Path to a CA certificate file for server verification.

  ssl_credentials_cacert_as_string (string, default: "")
      CA certificate content provided as an inline string.

  ssl_client_key_path (string, default: "")
      Path to the client private key.  Requires the SDK MTLS build flag
      (ENABLE_OTLP_GRPC_SSL_MTLS_PREVIEW).

  ssl_client_key_string (string, default: "")
      Client private key as an inline string.  Requires the SDK MTLS build flag.

  ssl_client_cert_path (string, default: "")
      Path to the client certificate.  Requires the SDK MTLS build flag.

  ssl_client_cert_string (string, default: "")
      Client certificate as an inline string.  Requires the SDK MTLS build flag.

  timeout (integer, default: 10, range: 1-60)
      Request timeout in seconds.

  user_agent (string, default: "")
      Custom user-agent string sent with each request.

  max_threads (integer, default: 0, range: 1-1024)
      Maximum number of concurrent gRPC threads.  The default of 0 cannot be
      written into the file, and leaves the SDK thread quota untouched.

  compression (string, default: "")
      Compression algorithm (e.g., "gzip").

  max_concurrent_requests (integer, default: 0, range: 1-1024)
      Maximum number of concurrent export requests.  The default of 0 cannot be
      written into the file, and leaves the SDK default in place.  Requires the
      SDK async-export build flag (ENABLE_ASYNC_EXPORT).


OTLP HTTP exporter (otlp_http)

Sends telemetry data to a collector over HTTP using the curl library.

  type (string, mandatory)
      Must be "otlp_http".

  endpoint (string)
      Server endpoint URL.  Signal-specific defaults:
        traces:  "http://localhost:4318/v1/traces"
        metrics: "http://localhost:4318/v1/metrics"
        logs:    "http://localhost:4318/v1/logs"

  content_type (string, default: "json")
      HTTP request body encoding.  Accepted values: "json", "binary".

  json_bytes_mapping (string, default: "hexid")
      How binary data is encoded in JSON output.  Accepted values: "hexid",
      "hex", "base64".

  use_json_name (boolean, default: false)
      Use JSON field names instead of protobuf field names.

  debug (boolean, default: false)
      Enable debug logging for HTTP requests.  When enabled, the SDK internal
      log level is also raised to Debug.

  timeout (integer, default: 10, range: 1-60)
      Request timeout in seconds.

  http_headers (map, default: none)
      Custom HTTP headers sent with each request.  Specified as a YAML sequence
      of key-value pairs:

        http_headers:
          - X-Custom-Header: "value"
          - Authorization:   "Bearer token"

  max_concurrent_requests (integer, default: 64, range: 1-1024)
      Maximum number of concurrent HTTP requests.  Requires the SDK async-export
      build flag (ENABLE_ASYNC_EXPORT).

  max_requests_per_connection (integer, default: 8, range: 1-1024)
      Maximum number of requests sent over a single HTTP connection before the
      connection is re-established.  Requires the SDK async-export build flag
      (ENABLE_ASYNC_EXPORT).

  background_thread_wait_for (integer, default: 0, range: 0-3600000)
      How long the curl background thread waits (in milliseconds) after its last
      request before exiting.  A value of 0 keeps the thread alive indefinitely,
      which prevents crashes caused by thread respawn failures in the SDK's
      noexcept MaybeSpawnBackgroundThread() call.  Requires the corresponding
      SDK patch (see scripts/build/).

  ssl_insecure_skip_verify (boolean, default: false)
      Skip TLS certificate verification.

  ssl_ca_cert_path (string, default: "")
      Path to a CA certificate file for server verification.

  ssl_ca_cert_string (string, default: "")
      CA certificate content provided as an inline string.

  ssl_client_key_path (string, default: "")
      Path to the client private key.

  ssl_client_key_string (string, default: "")
      Client private key as an inline string.

  ssl_client_cert_path (string, default: "")
      Path to the client certificate.

  ssl_client_cert_string (string, default: "")
      Client certificate as an inline string.

  ssl_min_tls (string, default: "")
      Minimum TLS protocol version (e.g., "1.2").

  ssl_max_tls (string, default: "")
      Maximum TLS protocol version (e.g., "1.3").

  ssl_cipher (string, default: "")
      TLS cipher list for TLS 1.2 and earlier.

  ssl_cipher_suite (string, default: "")
      TLS ciphersuites for TLS 1.3.

  compression (string, default: "")
      Compression algorithm (e.g., "gzip").


OStream exporter (ostream)

Writes human-readable telemetry data to a C++ output stream (stdout, stderr,
or a file).

  type (string, mandatory)
      Must be "ostream".

  filename (string, default: "stdout")
      Output destination.  Accepted values: "stdout", "stderr", or a file path.
      Every file-backed ostream exporter owns its stream, so an instance may
      write to several files and may reopen a file on a repeated start.  The
      file is emptied when the new pipeline is installed, so a repeated start
      empties it as well; a start that fails before that last step leaves it
      untouched, and only a failure in the step itself leaves an emptied file.
      The previous pipeline is flushed within a positive flush budget before
      the switch and its exporters are shut down before the file is emptied,
      so nothing of it lands in the emptied file.  Every export batch is pushed
      to the file at once.

Unlike the otlp_file exporter, this one writes without any size limit and never
rotates, so a file destination grows for as long as telemetry is produced and is
bounded only by the free space of its filesystem.  Traffic alone can therefore
fill that filesystem, which is why a file destination suits development rather
than production.


In-Memory exporter (memory)

Stores telemetry data in a circular memory buffer.  Supported for traces and
metrics only.

  type (string, mandatory)
      Must be "memory".

  buffer_size (integer, default: 100, range: 16-65536)
      Maximum number of records in the circular buffer.  The default is the
      MAX_BUFFER_SIZE value of the SDK.


Zipkin exporter (zipkin)

Sends trace data to a Zipkin backend.  Supported for traces only.

  type (string, mandatory)
      Must be "zipkin".

  endpoint (string, default: "http://localhost:9411/api/v2/spans")
      Zipkin collector endpoint URL.

  format (string, default: "")
      Transport format.  Accepted values: "json", "protobuf".  When empty, the
      SDK default is used.

  service_name (string, default: "default-service")
      Service name reported to Zipkin.

  ipv4 (string, default: "")
      IPv4 address of the service endpoint.

  ipv6 (string, default: "")
      IPv6 address of the service endpoint.


Elasticsearch exporter (elasticsearch)

Sends log records to an Elasticsearch cluster.  Supported for logs only.

  type (string, mandatory)
      Must be "elasticsearch".

  host (string, default: "localhost")
      Elasticsearch server hostname.

  port (integer, default: 9200, range: 1-65535)
      Elasticsearch server port.

  index (string, default: "logs")
      Index name where log records are stored.

  response_timeout (integer, default: 30, range: 1-3600)
      Response timeout in seconds.

  debug (boolean, default: false)
      Enable debug logging for Elasticsearch requests.

  http_headers (map, default: none)
      Custom HTTP headers.  Uses the same YAML format as the OTLP HTTP
      exporter's http_headers option.


6.5. Samplers
------------------------------------------------------------------------------

Samplers control which traces are recorded and exported.  They are configured
in the top-level 'samplers' section and referenced from the traces signal.  Only
one sampler is active per tracer.  When the traces signal references no sampler
at all, a trace_id_ratio_based sampler with a ratio of 1.0 is used.

  type (string, mandatory)
      Sampler type.  Accepted values: "always_on", "always_off",
      "trace_id_ratio_based", "parent_based", "composite".

Type "always_on" samples every trace unconditionally.  No additional
configuration keys.

Type "always_off" rejects every trace unconditionally.  No additional
configuration keys.

Type "trace_id_ratio_based" samples traces based on a configurable
probability.  Additional key:

  ratio (double, mandatory, range: 0.0-1.0)
      Probability that a trace is sampled.  A value of 0.0 means no traces are
      sampled; 1.0 means all traces are sampled.

Type "parent_based" delegates the sampling decision based on the parent span's
context.  When no parent span exists, the delegate sampler is used.  Additional
keys:

  delegate (string, default: "always_on")
      Root sampler used when there is no parent span.  Accepted values:
      "always_on", "always_off", "trace_id_ratio_based".

  ratio (double, range: 0.0-1.0)
      Sampling ratio for the delegate.  Mandatory when the delegate is set to
      "trace_id_ratio_based"; otherwise not read.

  remote_sampled (string, default: "always_on")
      Sampler applied when the remote parent has the sampled flag set.
      Accepted values: "always_on", "always_off".

  remote_not_sampled (string, default: "always_off")
      Sampler applied when the remote parent does not have the sampled flag.
      Accepted values: "always_on", "always_off".

  local_sampled (string, default: "always_on")
      Sampler applied when the local parent has the sampled flag set.
      Accepted values: "always_on", "always_off".

  local_not_sampled (string, default: "always_off")
      Sampler applied when the local parent does not have the sampled flag.
      Accepted values: "always_on", "always_off".

Type "composite" wraps a composable sampler to perform consistent-probability
sampling.  Unlike "trace_id_ratio_based", the resulting sampling threshold is
written to the "ot" entry of the W3C tracestate, so that downstream services
and the collector can reconstruct the adjusted span count from the propagated
decision.  This type requires an OpenTelemetry C++ SDK of version 1.28.0 or
newer; when the wrapper is built against an older SDK the type is rejected as
invalid.  Additional keys:

  composable (string, default: "probability")
      Composable sampler that the composite drives.  Accepted values:
      "always_on", "always_off", "probability", "parent_threshold".

  ratio (double, range: 0.0-1.0)
      Sampling probability.  Mandatory when composable is set to "probability"
      or "parent_threshold"; otherwise not read.  For "parent_threshold" it is
      the ratio of the probability sampler that decides the fate of root spans.


6.6. Processors
------------------------------------------------------------------------------

Processors control how telemetry records are batched and delivered to exporters.
They are used by the traces and logs signals.  Metrics use readers instead of
processors (see section 6.7).

  type (string, mandatory)
      Processor type.  Accepted values: "batch", "single".

Type "batch" buffers records in a bounded circular queue and exports them in
batches at regular intervals from its own thread.  Additional keys:

  max_queue_size (integer, default: 2048, range: 64-65536)
      Maximum number of records the queue can hold.  Must be greater than or
      equal to max_export_batch_size.

  schedule_delay (integer, range: 1-60000)
      Time in milliseconds between consecutive export cycles.
      Default: 5000 for traces, 1000 for logs.

  export_timeout (integer, default: 30000, range: 1-60000)
      Maximum time in milliseconds allowed for a single export operation.  Read
      and forwarded for configuration consistency, but the OTel C++ SDK batch
      processors do not currently use this value.

  max_export_batch_size (integer, default: 512, range: 1-65536)
      Maximum number of records per export batch.

When the queue fills up, new records are silently dropped.  The library reports
the drop count through otelc_pipeline_status_get(); section 9.1 has the details.

Type "single" exports each record immediately, on the thread that produced it.
The batch keys are still read and range-checked in its block, but not used.


6.7. Readers
------------------------------------------------------------------------------

Readers configure the periodic metric collection and export.  They are used
exclusively by the metrics signal, and pair with exporters to form the metric
pipelines.  Unlike the other components, a reader block has no 'type' key, and
a metrics signal that references no reader gets one built from the defaults
below.

  export_interval (integer, default: 60000, range: 100-3600000)
      Time in milliseconds between consecutive metric exports.  Must be greater
      than export_timeout.

  export_timeout (integer, default: 30000, range: 100-3600000)
      Maximum time in milliseconds allowed for a single metric export operation.


6.8. Providers
------------------------------------------------------------------------------

Providers define resource attributes that are attached to all telemetry emitted
by a signal.  These attributes identify the service producing the data and are
carried in every span, metric data point, and log record.

  resources (map)
      A YAML sequence of key-value pairs specifying resource attributes:

        providers:
          my_provider:
            resources:
              - service.name:        "my-service"
              - service.version:     "1.0.0"
              - service.namespace:   "production"
              - service.instance.id: "host-1"

      Any string key-value pair is accepted.  Common attributes follow
      the OpenTelemetry semantic conventions and include service.name,
      service.version, service.namespace, and service.instance.id.

Resource attributes from the YAML file are merged with attributes detected from
environment variables (see section 6.10).  YAML attributes take precedence over
environment attributes for duplicate keys.

The OTel C++ SDK itself adds identification attributes to every resource it
creates, so telemetry.sdk.language, telemetry.sdk.name and telemetry.sdk.version
are always present, with the values "cpp", "opentelemetry" and the version of
the OTel C++ client the wrapper is built with ("1.28.0").  Listing those keys
in the resources sequence overrides the SDK values like any other attribute:

  providers:
    my_provider:
      resources:
        - telemetry.sdk.language: "my_lang"
        - telemetry.sdk.name:     "my_name"
        - telemetry.sdk.version:  "my_version"

When neither the YAML file nor the environment supplies service.name, the SDK
falls back to "unknown_service", extended with the executable name when the
process.executable.name attribute is present among the resource attributes.


6.9. Signals
------------------------------------------------------------------------------

The 'signals' section is the central binding point that ties pipeline components
together.  Each signal type -- traces, metrics, or logs -- groups all of its
definitions by name, so the same configuration file can hold several independent
signal definitions:

  signals:
    traces:
      default:
        scope_name: "my-application"
        exporters:  my_exporter
        ...
      debug:
        scope_name: "my-application/debug"
        ...
    metrics:
      default:
        ...
    logs:
      default:
        ...

The 'name' argument passed to otelc_init() selects which named entry is used.
For each signal type the library probes signals/<sig>/<name> first; if that
entry is absent it falls back to signals/<sig>/default.  When neither entry
exists, the subtree itself is probed for the legacy layout that predates the
naming level and keeps the settings directly under signals/<sig>; the layout is
recognized through the mandatory scope_name key at that level and, when found,
serves any requested name.  When no variant exists for a signal that is later
created via otelc_*_create(), the call fails with an error.  A signal section
missing from the document altogether is recorded as absent by the name-state
probe, so a caller can skip such a signal instead of treating it as an error.

Configurations in the legacy layout therefore keep working unchanged:

  signals:
    traces:
      scope_name: "my-application"
      exporters:  my_exporter
      samplers:   my_sampler
      processors: my_processor
      providers:  my_provider

Within each named entry, the keys reference the other top-level sections.  The
keys that may carry a list decide how many pipelines the signal builds.  For
traces and logs, one pipeline is created per processor, pairing processor[i]
with exporter[i]; for metrics, one pipeline is created per exporter, pairing
exporter[i] with reader[i].  When the paired list is shorter, its last entry is
reused for the remaining pipelines, and surplus entries in the longer list are
ignored.

A single scalar name is accepted in place of a list, but the two notations may
not be combined freely.  For traces and logs the 'exporters' and 'processors'
keys must both be scalars or both be lists, because a scalar paired with a list
fails at startup.  For metrics the 'exporters' key alone decides: a scalar
'readers' value is applied to every pipeline, while a 'readers' list combined
with a scalar 'exporters' value is ignored, which leaves the reader defaults
of section 6.7 in place.


Traces signal

  scope_name (string, mandatory)
      Instrumentation scope name for the tracer.

  exporters (string or list of strings, mandatory)
      Exporter name(s) from the top-level 'exporters' section.

  samplers (string)
      Sampler name from the top-level 'samplers' section.

  processors (string or list of strings, mandatory)
      Processor name(s) from the top-level 'processors' section.

  providers (string)
      Provider name from the top-level 'providers' section.

  flush_timeout (integer, default: 5000, range: 0-600000)
      Destroy-time provider flush budget in milliseconds.  Zero, or a flush that
      runs out of the budget, shuts the exporters down instead, dropping the
      telemetry still queued.


Metrics signal

  scope_name (string, mandatory)
      Instrumentation scope name for the meter.

  exporters (string or list of strings, mandatory)
      Exporter name(s) from the top-level 'exporters' section.

  readers (string or list of strings)
      Reader name(s) from the top-level 'readers' section.

  providers (string)
      Provider name from the top-level 'providers' section.

  flush_timeout (integer, default: 5000, range: 0-600000)
      Destroy-time provider flush budget in milliseconds.  Zero, or a flush that
      runs out of the budget, shuts the exporters down instead, dropping the
      telemetry still queued.


Logs signal

  scope_name (string, mandatory)
      Instrumentation scope name for the logger.

  exporters (string or list of strings, mandatory)
      Exporter name(s) from the top-level 'exporters' section.

  processors (string or list of strings, mandatory)
      Processor name(s) from the top-level 'processors' section.

  providers (string)
      Provider name from the top-level 'providers' section.

  min_severity (string, default: "TRACE")
      Minimum log severity level.  Log records below this severity are discarded
      before reaching the processor.  Accepted values (case-insensitive): TRACE,
      TRACE2, TRACE3, TRACE4, DEBUG, DEBUG2, DEBUG3, DEBUG4, INFO, INFO2, INFO3,
      INFO4, WARN, WARN2, WARN3, WARN4, ERROR, ERROR2, ERROR3, ERROR4, FATAL,
      FATAL2, FATAL3, FATAL4.

  flush_timeout (integer, default: 5000, range: 0-600000)
      Destroy-time provider flush budget in milliseconds.  Zero, or a flush that
      runs out of the budget, shuts the exporters down instead, dropping the
      telemetry still queued.


6.10. Environment variables
------------------------------------------------------------------------------

Resource attributes can also be set through environment variables, independently
of the YAML file.  Attributes from environment variables are detected at startup
and merged with those specified in the YAML providers section.  YAML values take
precedence for duplicate keys.

  OTEL_RESOURCE_ATTRIBUTES
      Comma-separated list of key=value pairs to use as resource attributes.

      Example:
        export OTEL_RESOURCE_ATTRIBUTES="service.name=test,version=1.0"

  OTEL_SERVICE_NAME
      Sets the service.name resource attribute.  Takes precedence over a
      service.name value in OTEL_RESOURCE_ATTRIBUTES.


7. Examples
------------------------------------------------------------------------------

The examples in this section are complete programs, one per signal type.  Each
of them follows the lifecycle of section 5.2 -- initialize the library, create
the signal instance, start it, use it, and release everything in the reverse
order -- and they differ only in the middle part, where the signal is put to
work.

Every example reads its configuration from the file otel-cfg.yml in the current
directory and asks for the named entry 'default'.  The minimal document shown in
section 6.1 serves the tracing example as it stands; the other two need the same
document with a metrics or a logs signal in place of the traces one.

Against the installed library, an example is built with a single command:

  cc -o example example.c $(pkg-config --cflags --libs opentelemetry-c-wrapper)

Two error conventions appear throughout.  The otelc_init() and otelc_*_create()
functions report through their err argument, and the caller releases that string
with OTELC_SFREE().  Once an instance exists, its operations record the failure
text in the err member of that instance, which is released together with it.

The closing otelc_lib_shutdown() call resets the process-wide hooks.  It is the
last lifecycle step, it is optional for a program that is about to exit anyway,
and it is therefore left out of the error paths below.


7.1. Tracing
------------------------------------------------------------------------------

The program below creates a tracer, starts a span, and leaves the configured
exporter to write the finished span out.

```c
#include <stdio.h>
#include <stdlib.h>
#include <opentelemetry-c-wrapper/include.h>

int main(void)
{
    struct otelc_ctx    *ctx;
    struct otelc_tracer *tracer;
    struct otelc_span   *span;
    char                *err = NULL;

    /* Initialize the library; "default" selects the named signal entry */
    ctx = otelc_init("otel-cfg.yml", "default", &err);
    if (ctx == NULL) {
        fprintf(stderr, "Failed to init: %s\n", err);
        OTELC_SFREE(err);
        return 1;
    }

    /* Create a tracer bound to the context */
    tracer = otelc_tracer_create(ctx, &err);
    if (tracer == NULL) {
        fprintf(stderr, "Failed to create tracer: %s\n", err);
        OTELC_SFREE(err);
        otelc_deinit(&ctx, NULL, NULL, NULL);
        return 1;
    }

    /* Build the trace pipeline described by the configuration */
    if (tracer->ops->start(tracer) != OTELC_RET_OK) {
        fprintf(stderr, "Failed to start tracer: %s\n", tracer->err);
        otelc_deinit(&ctx, &tracer, NULL, NULL);
        return 1;
    }

    /* Start a new span */
    span = tracer->ops->start_span(tracer, "my-operation");
    if (span == NULL) {
        fprintf(stderr, "Failed to start span: %s\n", tracer->err);
        otelc_deinit(&ctx, &tracer, NULL, NULL);
        return 1;
    }

    /* ... perform work ... */

    /* End the span; the operation also clears the pointer */
    span->ops->end(&span);

    /* Clean up the tracer and the context, then the process-wide hooks */
    otelc_deinit(&ctx, &tracer, NULL, NULL);
    otelc_lib_shutdown();

    return 0;
}
```


7.2. Metrics
------------------------------------------------------------------------------

The program below creates a counter instrument and records a measurement into
it.  The value leaves the process when the periodic reader fires, and once more
when that reader is shut down along with the context, so a short-lived program
like this one still exports what it recorded.

```c
#include <stdio.h>
#include <stdlib.h>
#include <opentelemetry-c-wrapper/include.h>

int main(void)
{
    struct otelc_ctx   *ctx;
    struct otelc_meter *meter;
    struct otelc_value  value;
    char               *err = NULL;
    int64_t             counter_id;

    /* Initialize the library; "default" selects the named signal entry */
    ctx = otelc_init("otel-cfg.yml", "default", &err);
    if (ctx == NULL) {
        fprintf(stderr, "Failed to init: %s\n", err);
        OTELC_SFREE(err);
        return 1;
    }

    /* Create a meter bound to the context */
    meter = otelc_meter_create(ctx, &err);
    if (meter == NULL) {
        fprintf(stderr, "Failed to create meter: %s\n", err);
        OTELC_SFREE(err);
        otelc_deinit(&ctx, NULL, NULL, NULL);
        return 1;
    }

    /* Build the metric pipeline described by the configuration */
    if (meter->ops->start(meter) != OTELC_RET_OK) {
        fprintf(stderr, "Failed to start meter: %s\n", meter->err);
        otelc_deinit(&ctx, NULL, &meter, NULL);
        return 1;
    }

    /* Create a counter instrument; the returned id addresses it later */
    counter_id = meter->ops->create_instrument(meter,
        "requests", "Total request count", "1",
        OTELC_METRIC_INSTRUMENT_COUNTER_UINT64, NULL);
    if (counter_id == OTELC_RET_ERROR) {
        fprintf(stderr, "Failed to create instrument: %s\n", meter->err);
        otelc_deinit(&ctx, NULL, &meter, NULL);
        return 1;
    }

    /* Record a measurement */
    value.u_type         = OTELC_VALUE_UINT64;
    value.u.value_uint64 = 1;
    (void)meter->ops->update_instrument(meter, counter_id, &value);

    /* Clean up the meter and the context, then the process-wide hooks */
    otelc_deinit(&ctx, NULL, &meter, NULL);
    otelc_lib_shutdown();

    return 0;
}
```


7.3. Logging
------------------------------------------------------------------------------

The program below emits a single log record.  The log_span() operation takes an
optional span whose trace and span identifiers are copied into the record; the
example passes NULL, which leaves both identifiers zero and so the record is not
correlated with any trace.  Its body is built from a printf-style format string
and the arguments that follow it.

```c
#include <stdio.h>
#include <stdlib.h>
#include <opentelemetry-c-wrapper/include.h>

int main(void)
{
    struct otelc_ctx    *ctx;
    struct otelc_logger *logger;
    char                *err = NULL;

    /* Initialize the library; "default" selects the named signal entry */
    ctx = otelc_init("otel-cfg.yml", "default", &err);
    if (ctx == NULL) {
        fprintf(stderr, "Failed to init: %s\n", err);
        OTELC_SFREE(err);
        return 1;
    }

    /* Create a logger bound to the context */
    logger = otelc_logger_create(ctx, &err);
    if (logger == NULL) {
        fprintf(stderr, "Failed to create logger: %s\n", err);
        OTELC_SFREE(err);
        otelc_deinit(&ctx, NULL, NULL, NULL);
        return 1;
    }

    /* Build the log pipeline described by the configuration */
    if (logger->ops->start(logger) != OTELC_RET_OK) {
        fprintf(stderr, "Failed to start logger: %s\n", logger->err);
        otelc_deinit(&ctx, NULL, NULL, &logger);
        return 1;
    }

    /* Emit a record; the NULL span leaves it uncorrelated with any trace */
    (void)logger->ops->log_span(logger, OTELC_LOG_SEVERITY_INFO,
        0, NULL, NULL, NULL, NULL, NULL, 0,
        "Application started successfully");

    /* Clean up the logger and the context, then the process-wide hooks */
    otelc_deinit(&ctx, NULL, NULL, &logger);
    otelc_lib_shutdown();

    return 0;
}
```


8. Thread safety
------------------------------------------------------------------------------

The library is designed for multi-threaded use.  The data-plane operations --
creating spans, recording measurements, emitting log records -- are thread-safe,
and any number of threads may call them concurrently on the same tracer, meter
or logger instance.  The error text a failing operation leaves in the instance
is replaced under a lock, so concurrent failures cannot corrupt it.

Span and span context handles live in a sharded map whose shards are locked
independently and spread the contention across the threads; the shard count
comes from the handle_map_shards key of section 6.1, and a compile-time default
applies without it.  The shared-handle model is what the default build selects,
and it is what allows a span started on one thread to be ended on another.  The
alternative model, chosen at compile time, keeps the handles thread-local: it
removes the locking altogether but confines each span to the thread that made
it.  README-configuration describes both models.

Metric instrument lookups and updates take a shared lock and run in parallel,
while instrument and view creation is serialized, the instrument set being small
and rarely touched after the startup.

The operations that replace or release the state of an instance fall outside
that guarantee.  Before start() or destroy() runs on a tracer, meter or logger,
the caller has to drain every concurrent operation on that same instance and
end the spans it produced; a call still in flight when either of them runs reads
state that is being replaced or freed.  The instances are independent of each
other, so the ordering is required per instance rather than across the process,
and the same holds for the contexts that own them.

Two calls are process-wide rather than per-instance.  otelc_ext_init() installs
the allocation and thread-id callbacks and has to run before anything that uses
them, and otelc_lib_shutdown() clears those callbacks again, so it has to run
only after every context and every instance has been destroyed.

Each thread is identified internally by a numeric ID.  An application supplies
its own thread-ID function through otelc_ext_init(); without one, the library
assigns the identifiers itself.

The background threads of the SDK -- the batch processor export threads, the
OTLP File and HTTP exporter threads and the periodic metric reader threads --
are named and bound to a CPU core through the thread_name and cpu_id keys that
section 6.3 describes.


9. Known bugs and limitations
------------------------------------------------------------------------------

The library implements the three telemetry signals of OTel -- traces, metrics
and logs -- together with the span events and the baggage that ride along with
the traces.  Profiles are left out, that signal being still experimental in the
OTel specification.

Section 9.1 describes the one behaviour that surprises callers most often, the
silent dropping of telemetry under load, and the interface that reports it.
Section 9.2 collects the remaining limitations.


9.1. Export drops and pipeline status
------------------------------------------------------------------------------

The OTel C++ SDK batch processors (BatchSpanProcessor for traces and
BatchLogRecordProcessor for logs) use bounded, lock-free circular buffers to
queue telemetry data for export.  When the application produces spans or log
records faster than the export thread can drain them, the buffer fills up and
subsequent items are silently dropped.  Several factors influence whether drops
occur:

  * Production rate vs. export throughput: High-concurrency workloads with
    many threads generating telemetry can overwhelm the export thread,
    especially when the exporter involves network I/O (OTLP/gRPC, OTLP/HTTP)
    rather than a local sink.

  * Batch processor configuration: The max_queue_size parameter controls the
    circular buffer capacity (default 2048).  Larger values absorb longer
    bursts but consume more memory.  The schedule_delay parameter controls
    how frequently the export thread wakes up to drain the buffer; shorter
    intervals reduce the chance of overflow but increase CPU overhead.

  * Export latency: Slow or unreachable collectors stall the export thread,
    preventing it from freeing buffer slots.  Under sustained load this
    quickly leads to drops.

Drops are an expected part of the SDK's back-pressure mechanism -- they keep the
application from blocking or running out of memory.  The wrapper library exposes
the full export-pipeline state through otelc_pipeline_status_get(), which fills
a struct otelc_pipeline_status with one struct otelc_export_status per signal
(traces, logs, and metrics) so that callers can monitor drops and adjust batch
processor settings or collector capacity accordingly.

Each otelc_export_status carries the drop count (dropped), how full the batch
queue is (queue_depth and queue_capacity), the successful and failed export call
counts (export_ok and export_fail), the records those calls carried (records_ok
and records_fail), and the age in milliseconds of the most recent successful
export (last_export_ms, or -1 before the first one).  A rising export_fail count
is the network-side counterpart to a queue-full drop: the queue drains, but the
collector rejects or never receives the batches.  The export counts are Export()
calls (batches); the record counts are the spans or log records those calls
moved, so one export may carry many records.

The counters are process-wide: every instance of a signal adds to the same
totals.  Queue depth and capacity describe only the most recently created batch
processor, and trace or log pipelines built with the single processor type skip
the counting layer entirely, so their exports and drops do not appear in the
status at all.

Metrics are driven by a periodic reader rather than a batch processor and have
no queue, so the dropped, queue_depth, and queue_capacity fields of the metrics
member are always -1, as are records_ok and records_fail; only the export-call
counts (export_ok and export_fail) are meaningful.  The reader exports on its
configured interval whether or not any instrument has recorded data, so the
metric export count tracks those cycles rather than data volume and climbs even
when no metric is produced.  Note that asynchronous exporters (OTLP/HTTP and
OTLP/gRPC) count an export as successful the moment the batch is handed off for
delivery, so transport failures surface later through the diagnostic log handler
installed with otelc_log_set_handler(), not in export_fail.


9.2. Other limitations
------------------------------------------------------------------------------

  * Span lifecycle: ending every span before the tracer that produced it is
    destroyed is the caller's responsibility.  In the default build the span
    maps are shared by all tracers, so the spans left open are ended
    implicitly only when the last tracer goes; section 8 describes the
    thread-local alternative, in which a span never leaves its own thread.

  * ABI compatibility: the library has to be built against an OTel C++ client
    compiled with ABI version 2, and both build systems verify this at
    configuration time and reject any other ABI version.  AddLink, an ABI
    version 2 feature, is therefore always available.  The
    start_span_with_options operation additionally establishes span links at
    creation time, which the OTel API supports on every ABI version.

  * Drop reporting asymmetry in the C++ SDK: the BatchSpanProcessor logs a
    warning to stderr ("BatchSpanProcessor queue is full - dropping span.")
    when a span is dropped, while the BatchLogRecordProcessor discards log
    records without any diagnostic output.  The wrapper reports the drop
    counts of both signals uniformly through otelc_pipeline_status_get().
