-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathREADME
More file actions
2002 lines (1541 loc) · 86 KB
/
Copy pathREADME
File metadata and controls
2002 lines (1541 loc) · 86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-----------------------------------------------------------------
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: "")