-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathREADME-implementation
More file actions
1935 lines (1593 loc) · 96.9 KB
/
Copy pathREADME-implementation
File metadata and controls
1935 lines (1593 loc) · 96.9 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
OpenTelemetry Filter Implementation Review
======================================================================
1 Overview
----------------------------------------------------------------------
The OpenTelemetry (OTel) filter for HAProxy creates, propagates and exports the
spans, metric instruments and log records that the OpenTelemetry specification
defines. The filter hooks into the HAProxy stream processing pipeline through
the filter API and maps channel analyzer events to span lifecycle operations,
metric recordings and log-record emissions.
The implementation lives at the top of this addon checkout: header files, C
source files, a Makefile.mk fragment, and test configurations with their runner
scripts.
2 Directory Structure
----------------------------------------------------------------------
haproxy-opentelemetry/
|-- Makefile.mk Build integration (loaded via EXTRA_MAKE)
|-- include/
| |-- include.h Master include (pulls all headers)
| |-- config.h Build-time tunables (pool sizes, limits)
| |-- define.h Utility macros (memory, strings, lists)
| |-- debug.h Debug/logging infrastructure
| |-- filter.h Filter return codes, alert macros
| |-- parser.h Configuration keyword definitions
| |-- conf.h Configuration data structures
| |-- conf_funcs.h Generated init/free function macros
| |-- event.h Event enumeration and data table
| |-- scope.h Runtime span/context structures
| |-- pool.h Memory pool helpers
| |-- http.h HTTP header manipulation
| |-- otelc.h Span context inject/extract wrappers
| |-- vars.h HAProxy variable integration
| |-- sample.h Sample fetch names (otel.context, otel.bytes_*)
| |-- util.h String conversion, sample helpers
| |-- group.h Group action (HAProxy rule integration)
| `-- cli.h CLI command interface
|-- src/
| |-- filter.c Filter lifecycle and channel callbacks
| |-- parser.c Configuration file parser
| |-- conf.c Configuration structure init/free
| |-- event.c Scope/span execution engine
| |-- scope.c Runtime context and span management
| |-- http.c HTTP header get/set/remove
| |-- otelc.c C wrapper inject/extract bridge
| |-- vars.c HAProxy variable read/write
| |-- sample.c Sample fetch for span-context ACL checks
| |-- pool.c Pool alloc/free, trash buffers
| |-- util.c Argument handling, sample conversion
| |-- group.c Group action parsing and execution
| `-- cli.c CLI command handlers
|-- dummy/ Build-only wrapper stand-in (see dummy/README)
`-- test/
|-- copy-yml.sh YAML configuration transformer
|-- test-speed.sh Performance benchmarking runner
|-- run-test-config.sh Single-instance runner (real file)
|-- run-sa.sh Standalone test runner (symlink)
|-- run-full.sh Full event-coverage test runner (symlink)
|-- run-fe-be.sh Frontend-backend chain runner
|-- run-ctx.sh Context propagation test runner (symlink)
|-- run-cmp.sh Comparison test runner (symlink)
|-- run-tcp.sh TCP-mode test runner (symlink)
|-- run-updown.sh Up-down counter test runner (symlink)
|-- run-err.sh Error-logging test runner (symlink)
|-- run-empty.sh Empty configuration test runner (symlink)
|-- run-parser.sh Configuration parser test driver (real file)
|-- otlp_http-recorder.py OTLP/HTTP traffic recorder
|-- otlp_http-replay.py OTLP/HTTP traffic replayer
|-- haproxy-common.cfg Shared HAProxy configuration part
|-- index.html Static page served by the origin server
|-- README.md Test overview, coverage and speed results
|-- README-parser Configuration parser test description
|-- sa/ Standalone test configs
|-- full/ Full event-coverage test configs
|-- fe/ Frontend-only test configs
|-- be/ Backend-only test configs
|-- ctx/ Context propagation test configs
|-- cmp/ Comparison test configs
|-- tcp/ TCP-mode test configs
|-- updown/ Up-down counter test configs
|-- err/ Error-logging test configs
|-- empty/ Minimal/empty configuration test
`-- parser/ Parser test cases, tables and generators
3 Build System
----------------------------------------------------------------------
The Makefile.mk fragment is pulled in by the HAProxy top-level Makefile via the
EXTRA_MAKE variable, which lists addon directories whose Makefile.mk fragments
are included unconditionally. The fragment detects the opentelemetry-c-wrapper
library via pkg-config or manual OTEL_INC/OTEL_LIB paths.
Build options:
EXTRA_MAKE=<path> Addon directory containing Makefile.mk. Listing this
directory is what enables the filter. Multiple directories
may be passed, space-separated.
OTEL_DEBUG=1 Compile with DEBUG_OTEL; links the _dbg variant of the
wrapper library and enables the debug-only flt_ops callbacks
deinit_per_thread, http_payload and http_reset in filter.c.
OTEL_USE_VARS=1 Define USE_OTEL_VARS (and USE_OTEL_VARS_NAME when struct
var has a 'name' member), which allows span context
propagation via HAProxy transaction variables in addition
to HTTP headers; vars.c itself is always compiled.
OTEL_INC=<path> Manual include path for the C wrapper.
OTEL_LIB=<path> Manual library path for the C wrapper.
OTEL_RUNPATH=1 Embed RPATH to the wrapper library.
OTEL_STATIC=1 Pass --static to pkg-config so OTEL_LDFLAGS picks up the
full Libs.private chain; needed for static linking.
Compiled objects (vars.o is built unconditionally):
cli.o conf.o event.o filter.o group.o http.o otelc.o parser.o
pool.o sample.o scope.o util.o vars.o
The dummy/ subtree stands in for the wrapper API so the filter builds and links
with neither the wrapper nor the OpenTelemetry C++ SDK installed. Its Makefile
produces a static archive named the way Makefile.mk expects, with OTEL_INC and
OTEL_LIB pointed at it instead of pkg-config, and Makefile.mk builds the archive
in the same pass, handing OTEL_DEBUG on because DEBUG_OTEL and OTELC_DBG_MEM
change the shape of the debug macros and of the external allocator types. The
resulting executable runs the whole of the filter's own logic but produces no
telemetry, and reports the C++ version as "none" so that it cannot be mistaken
for a real build. See dummy/README.
4 Configuration Parsing
----------------------------------------------------------------------
Configuration parsing is driven by parser.c. The filter is declared in the
HAProxy configuration with:
filter opentelemetry [id <id>] config <file> [<name>]
The flt_otel_parse() function (parser.c) handles the "filter" line, creates an
flt_otel_conf structure, and delegates to parse_cfg() which loads the referenced
YAML/CFG file. That file is parsed using temporary section registrations for
the section types:
otel-instrumentation -> flt_otel_parse_cfg_instr()
otel-group -> flt_otel_parse_cfg_group()
otel-scope -> flt_otel_parse_cfg_scope()
After each section is fully parsed, a post-parse function validates the section
(e.g., flt_otel_post_parse_cfg_scope() checks that context injection is only
used on events that support it).
An optional trailing name on the filter line selects the [<name>] section of
the file: flt_otel_parse_check_scope() compares it, or the filter id when it
is absent, against HAProxy's cfg_scope while the file is parsed. That same
function also rejects a keyword line that stands outside of any [<name>] scope.
The uniqueness of the scope name, the other top-level rule of section 4.5, is
checked before the file is parsed, by flt_otel_parse_check_scope_names() over
the loaded content: a section parser is called for the lines of a scope but
never for the declaration itself, so two scopes of the same name that follow
each other cannot be told apart from a single one there.
Before flt_otel_parse_cfg() returns, the instrumentation's YAML configuration
is validated by the wrapper's otelc_cfg_validate(), which parses the document
and probes the context-name resolution per signal without creating a library
context. The library itself is initialized in the init callback that check
mode never runs, so this validation is what makes 'haproxy -c' reject a broken
YAML file.
4.1 Instrumentation Section
otel-instrumentation <name>
config <file> [context]
log <target>
debug-level <value>
rate-limit <value>
option { disabled | dontlog-normal | hard-errors | noflush | require-context }
groups <name> ...
scopes <name> ...
acl <aclname> <criterion> ...
The instrumentation block defines global filter parameters: the YAML exporter
configuration file, logging, rate limiting, and references to groups and scopes.
Exactly one instrumentation block is allowed per filter instance.
4.2 Group Section
otel-group <name>
scopes <name> ...
Groups bundle multiple scopes under a single name for use with HAProxy
http-request/http-response rules via the "otel-group" action. The group action
(group.c) parses the rule, resolves the scope references at check time, and
executes all referenced scopes when the rule fires.
4.3 Scope Section
otel-scope <name>
otel-event <event-name> [{ if | unless } <condition>]
idle-timeout <time>
extract <name-prefix> [use-vars | use-headers]
span <name> [parent <ref>] [link <ref>] [root] [kind <kind>]
link { <ref> ... | <ref> attr <key> <sample> ... } [{ if | unless } <condition>]
attribute <key> <sample> ... [{ if | unless } <condition>]
event <name> [time [<unit>] <sample>] <key> <sample> ... [{ if | unless } <condition>]
baggage <key> <sample> ... [{ if | unless } <condition>]
status <code> [<sample> ...] [{ if | unless } <condition>]
exception <type> [message <sample> ...] [attr <key> <sample> ...] [{ if | unless } <condition>]
inject <name-prefix> [use-vars] [use-headers]
finish <name> ...
instrument { <type> <name> ... | update <name> ... } [{ if | unless } <condition>]
log-record <severity> [id <integer> event <name>] [time [<unit>] <sample>] [span <ref>] [attr <key> <sample>] ... <sample> ... [{ if | unless } <condition>]
set-var <var-name> <sample> ... [{ if | unless } <condition>]
set-var-ctx <var-name> <ref> <field> [{ if | unless } <condition>]
unset-var <var-name> ... [{ if | unless } <condition>]
acl <aclname> <criterion> ...
otel-stop [{ if | unless } <condition>]
Each scope ties to a single HAProxy analyzer event (or none, if used only
through groups). Scopes contain context extraction directives, span
definitions, metric instruments, log records, finish and otel-stop directives.
A span may specify:
- A parent reference (another span or extracted context).
- One or more links to other spans/contexts. Inline link syntax allows one
link on the span line; the standalone "link" keyword allows multiple.
- The "root" mark, which declares the root span and changes nothing at run
time.
- Attributes, events, baggages and status evaluated from HAProxy sample
expressions at runtime.
- An inject directive to propagate the span context via HTTP headers and/or
HAProxy variables.
4.4 Configuration Structure Initialization
All configuration structures are allocated and freed using macro-generated
functions from conf_funcs.h:
FLT_OTEL_CONF_FUNC_INIT(type, id_field, max_len, extra_init)
FLT_OTEL_CONF_FUNC_FREE(type, id_field, extra_free)
These macros produce flt_otel_conf_<type>_init() and _free() functions.
The init function:
- Checks the identifier length against the limit of the type, which is
FLT_OTEL_ID_MAXLEN (128) for a name and FLT_OTEL_LEN_UNLIMITED for the text
of a sample expression.
- Checks for duplicate identifiers in the target list.
- Allocates the structure with OTELC_CALLOC.
- Copies the identifier with OTELC_STRDUP.
- Appends to the head list.
- Executes any extra initialization (e.g., LIST_INIT for sub-lists in the
span structure).
The free function:
- Executes any extra cleanup (e.g., destroying sub-lists).
- Frees the identifier string.
- Removes the node from its list.
- Frees the structure.
The full init/free chain for all structures:
flt_otel_conf flt_otel_conf_init() / flt_otel_conf_free()
flt_otel_conf_instr generated via macro
flt_otel_conf_ph generated (for ph_groups, ph_scopes)
flt_otel_conf_group generated
flt_otel_conf_ph generated (for ph_scopes)
flt_otel_conf_scope generated
flt_otel_conf_context generated
flt_otel_conf_span generated
flt_otel_conf_link generated
flt_otel_conf_sample generated + _init_ex()
flt_otel_conf_sample_expr generated
flt_otel_conf_exception generated
flt_otel_conf_sample generated (message, attributes)
flt_otel_conf_str generated (for spans_to_finish)
flt_otel_conf_stop generated (for the otel-stop lines)
flt_otel_conf_instrument generated
flt_otel_conf_log_record generated
flt_otel_conf_sample generated + _init_ex()
flt_otel_conf_sample_expr generated
flt_otel_conf_sample generated (for set_vars)
flt_otel_conf_set_var_ctx generated
flt_otel_conf_unset_var generated
flt_otel_conf_str generated (for the vars sublist)
The conf_funcs.h macros also generate the leaf flt_otel_conf_str and
flt_otel_conf_hdr structures (a plain string node and a header name/value
pair, respectively), reused wherever a scope needs such a list -- for
instance spans_to_finish and the unset-var directive's variable names.
4.5 Keyword Definition Rules
This section states the rules of the keywords of the OTel configuration file.
Breaking a rule stated here is an error unless the section says otherwise.
The paragraphs here hold for every keyword; each entry below adds what belongs
to that keyword alone. An entry indented under 'span' works on the active span
and needs a 'span' line before it. A section keyword opens its section and the
next section header ends it; every other keyword needs its own section open.
A keyword takes a trailing condition only when its entry names a scheme below,
or says so in words as 'otel-event' does. Such a keyword repeats, one line per
condition, and the scheme says how the lines combine; 'otel-event' takes its
condition without repeating. A keyword that takes no condition may repeat too,
when its entry says so. Section 18.7 records the operation behavior behind the
schemes. The parser cannot enforce this for 'acl', where everything after the
criterion is pattern text, so a condition written there is read as one more
pattern value.
first-match - the filter applies the first line of a key whose condition holds
and skips the rest. The line without a condition is the default
and comes last.
apply-all - the filter applies every line whose condition holds, in the
order written. The lines are separate items: a line without a
condition always applies, and none of them has to come last.
Lines compete for the first match inside one otel-scope only, inside one span
for a keyword written under a span, and only when they name the same key or the
same name; a keyword that names neither lets them all compete. A span named
again in another otel-scope starts over: the scopes run one after the other, so
that line is a later operation and not a repetition.
Whatever its scheme allows across the lines, a keyword may not name the same
thing twice on one line: a name, a reference or an attribute key written twice
in the same list is an error, and so is a repeated clause of the keyword, 'root'
or 'link' or 'unit' or 'time' among them. The same name may still stand in two
lists of one line: a span may name one span as its 'parent' and as its inline
'link'. A clause that opens one item of a list is written per item instead:
the 'attr' of a 'log-record' and of an 'exception' stands before every key, and
the same clause of a 'link' and of an 'instrument' update may be left out after
the first. Several sample expressions after a key make one value together,
except after the key of an 'attr' clause and after a 'time' clause, each of
which takes one and reads the next word as the next argument of the line; the
'value' of an 'instrument' refuses a second, and a 'bounds' clause a repeated
boundary value.
Nothing may be called 'if' or 'unless', the filter id, the top-level scope name
and the context name of the 'config' line included, whether the line defines or
references the name. The ban is on the two words as written: another case never
opens a condition and stays a plain name. An ACL may not be called 'or' either,
whatever its case, since that word joins the terms of a condition. A standalone
'link' reads 'attr' as its clause anywhere after the first reference and an
'instrument' update anywhere after its name, so a later link target and a key of
either may not be called 'attr'; an 'event' reads 'time' right after its name as
that clause, so its key may not be called 'time'.
A name is at most 127 characters long, every key and variable name included,
a variable counted with its scope prefix and the key inside a 'set-var-ctx'
field left aside; a longer one is refused with 'name too long'. No length rule
reaches an ACL name, an 'event' name, an exception type, the description of an
'instrument', the text of a sample expression, the filter id, the context name
of the 'config' line or the top-level scope name, on the 'filter' line as in its
'[<name>]' declaration.
Some names carry a character rule too. An 'inject' or an 'extract' prefix and a
'baggage' key take letters, digits, '_', '.' and '-' alone; an 'instrument' name
and the key inside a 'set-var-ctx' field follow the rules their entries state;
an ACL name, a section name, a top-level scope name and a variable name follow
HAProxy's own. The other names take any character.
A reference names a span or an extract context that some otel-scope defines.
The 'parent' and the inline 'link' of a 'span', the standalone 'link', 'finish'
and 'set-var-ctx' take either, the 'span' of a 'log-record' a span alone. The
'finish' wildcards '*', '*req*' and '*res*' stand for whatever the stream holds
and resolve on their own.
A condition looks each ACL name up in the otel-scope that holds the line, then
in the otel-instrumentation section, then in the HAProxy configuration, so a
scope ACL hides an instrumentation ACL of the same name and one condition may
mix the lists. The ACL has to be defined by the time the line is read, so an
otel-instrumentation section below the otel-scope, or a HAProxy ACL below the
'filter' line, comes too late.
The parser reports most of these rules as it reads the file, naming the file and
the line. A rule that pairs two lines of one section is reported on the second
of them: an 'idle-timeout' pairs with the scope event it needs, and so does a
'use-headers' written on an 'inject' or on an 'extract'. A line that never
comes is reported on the section header or on the span once the section has
been read: the 'config' line of an otel-instrumentation, the 'scopes' line of
an otel-group, the 'otel-event' of a scope carrying an 'idle-timeout' or an
'inject use-headers', the 'idle-timeout' of an 'on-idle-timeout' scope, and
the name of an 'inject' written as '-', whose scope event may stand below the
line. A top-level scope with no otel-instrumentation section is reported on the
'filter' line once the whole file has been read. The rules that need the whole
configuration are checked once every section is read, with no line number in the
alert: the references to names defined elsewhere in the file, the 'groups' line
that every otel-group needs, the 'root' rules, the resolved 'inject' names, the
'extract' prefix no span may carry and the agreement of the create lines of one
'instrument'. Rules this section does not state are checked there too: the
filter id that no other OTel filter of any proxy carries, the 'extract' context
'require-context' needs, the event a scope may carry while that option is set,
the HTTP-mode proxy that a header operation needs, and the filter, the group and
the instrumentation an 'otel-group' action names.
Some checks only warn. Beside the ones that the 'span' entry names, they break
no rule stated here: an otel-scope no 'scopes' line names, an HTTP event on a
proxy that carries no HTTP message, a frontend-phase or the stream-stop event
for a filter of a backend section, a backend-phase request event on a listen
proxy, a condition or a sample expression the event of its scope, or the action
running it, cannot serve, a 'rate-limit' below 100.0, a 'filter' line with no
id, a 'debug-level' value that a build lacking the debug code ignores, and a
'use-vars' context whose name the HAProxy variable it generates cannot keep as
written.
A scope runs when a 'scopes' line of the instrumentation or of an otel-group
names it: at its event when it carries one, and from the 'otel-group' action
when a group holds it; the rules below mean this by a scope that runs.
A rule that depends on which scope runs first can fail only at execution. A
'parent' whose span no scope has created yet, or whose extract context found
nothing, is a runtime error and the span carrying it is not created; the other
references do nothing when they do not resolve, and a 'log-record' whose 'span'
is not found is emitted without it.
The rules read one top-level scope, the one the 'filter' line selects; the other
scopes stay unread until a filter line selects them, and only the scope names
are checked over the whole file. The skipping needs an open section: from the
first section header of the file on, an unread scope may hold anything, a broken
section header included, while a line above every header is an error HAProxy
itself reports.
A frontend's and a backend's filter meet on the streams routed between them,
which the configuration cannot foresee, so keeping their 'inject' names apart is
left to the writer.
Top-level OTel scope:
[<name>] - the scope name is unique. The line opens the OTel scope and the
next [<name>] line ends the previous one. A line outside of any
scope is an error.
Section "otel-instrumentation":
otel-instrumentation - the section is required and stands once in a top-level
scope, whatever its name.
acl - <aclname> is unique in the instrumentation; several
lines are allowed, and the name may be used in every
otel-scope.
log - allowed only once, a prefix included. The filter reads
the prefix too and leaves its own logging off for 'no',
on for 'default'.
config - required, and allowed only once.
groups - <name> is unique in the instrumentation and must be a
defined otel-group section; several lines are allowed.
scopes - <name> is unique in the instrumentation and must be a
defined otel-scope section; several lines are allowed.
rate-limit - allowed only once.
option - each option is allowed only once, and a prefix does not
lift the limit. The 'no' and 'default' prefixes keep
the meaning HAProxy gives them.
debug-level - allowed only once.
Section "otel-group":
otel-group - <name> is unique; another top-level scope may reuse the name.
The section must hold at least one 'scopes' line, and a 'groups'
line of the otel-instrumentation must name the group.
scopes - <name> is unique in the group; several lines are allowed. A name
must be a defined otel-scope section.
Section "otel-scope":
otel-scope - <name> is unique; another top-level scope may reuse the name.
span - the first line of a <name> in a scope creates the span, with or
without creation arguments, and every line of the name makes
it the active span for the operations that follow. A later
line of the name in that scope must be bare: one with creation
arguments, a defining line, is a repeated definition. Another
otel-scope may carry a defining line of the same name too: at
execution the first line to reach the span creates it, bare
or defining, a later bare line re-activates it and another
defining line is a runtime error. The creation line carries
one inline 'link' at most, and the standalone keyword adds
the rest. The 'parent' may not name the span itself, which
is looked up before it exists. The 'root' mark declares the
root span and changes nothing at execution: a span without
a 'parent' starts a trace of its own, marked or not. Only
one span name may carry the mark, the same name marked root
in another otel-scope being the usual alternative creation.
Leaving every span unmarked is not an error, only a warning,
and only the spans of the scopes that run count towards it. A
scope that a 'scopes' line names but that never runs draws a
warning of its own. The 'parent' of a marked span may name an
extract context, the remote parent of the trace, never a span.
A span may not be called by one of the 'finish' wildcards.
link - apply-all scheme.
attribute - first-match scheme per <key>.
event - the lines are gathered by name: each name makes one span event
whose attributes are the keys of its lines. The (name, key)
pairs are independent items under the apply-all scheme, while
within one pair the value follows the first-match scheme. The
event timestamp comes from the first applied line whose 'time'
clause evaluates, and the later lines keep it.
baggage - first-match scheme per <key>.
inject - no other 'inject' of the configuration, or of the other OTel
filters of the proxy, may resolve to the name this one does,
one differing only in case included: both carriers keep the
name in lower case. Several lines are allowed, but at most
one per span of one otel-scope, so a span that another scope
re-activates may carry a second context there. A '-' line
follows the name rule with its resolved name, and at most one
context whose name opens with '-', written or resolved, may
inject headers on the proxy: the nameless headers would collide
whatever the names are. The resolved name is measured like a
written one, the mark counted with it, and the span name it
takes when the scope has no event is checked like a written
prefix. Injecting headers, which a line with no storage word
does, needs an event that can still change them: any other
event is an error, and so is a scope with no event.
status - first-match scheme for the span's single status.
exception - apply-all scheme, one exception event per applied line.
extract - <name-prefix> is unique within its scope; several lines are
allowed, and another otel-scope may reuse the name. No span of
the configuration may carry the name: a reference resolves to
the span first. 'use-headers' needs a channel to read, so it
is an error on an event that carries none; a scope with no
event is exempt, the 'otel-group' action that runs it carrying
one.
finish - <name> is unique within its scope; several lines are allowed,
and another otel-scope may finish the same span.
otel-stop - first-match scheme.
instrument - the create form follows the first-match scheme per its <name>
within one otel-scope. The update form is apply-all for every
instrument type, and records the value of the create lines of
its own otel-scope, of the scope that created the instrument
where its own carries none, and until the instrument exists of
the first scope of the file that runs and defines the name.
An update needs a create line of its name in some scope of
the file, and an update in a scope that runs needs one in a
scope that runs, its own or another one. The lines of one
name are one instrument, across the scopes too and with the
case of the name folded, so they must agree on the type, the
aggregation, the description, the unit and the bounds, while
the value and the condition are what the repeated lines vary;
only one scope ever creates the instrument, and a create line
of another scope that finds it created is a runtime error. The
name begins with a letter and carries only letters, digits
and the punctuation '_', '.', '-' and '/', while the unit is
at most 63 ASCII characters, both rules of the metric SDK,
which records nothing for an instrument it refuses. A 'bounds'
clause stands on a hist_int instrument alone.
log-record - apply-all scheme, one record per applied line. The 'id' and
'event' clauses are given together or both omitted.
idle-timeout - allowed only once, and it belongs to the 'on-idle-timeout'
event: a scope bound to that event must carry it, and one bound
to another event, or to no event at all, may not.
acl - <aclname> is unique within its scope and may be used in that
scope alone; several lines are allowed, and another otel-scope
may reuse the name.
otel-event - the keyword may be given only once in a scope, whatever its
name, optionally with a condition.
set-var - first-match scheme per <var-name>.
set-var-ctx - first-match scheme per <var-name>. The W3C rules reach the
key inside a 'baggage' field, an HTTP token of at most 4096
characters, and the one inside a 'tracestate' field: at most
256 lower-case letters, digits, '_', '-', '*' and '/' opening
with a letter, or a tenant id and a system id of at most 241
and 14 around one '@', the tenant opening with a letter or a
digit and the system id with a letter. The other fields take
no key.
unset-var - first-match scheme per <var-name>.
5 Filter Lifecycle
----------------------------------------------------------------------
The filter registers its operations in the flt_otel_ops structure (filter.c)
and the keyword parser via INITCALL1 (parser.c).
5.1 Proxy-Level Initialization
flt_otel_ops_init():
- Registers CLI commands via flt_otel_cli_init().
- Initializes the OpenTelemetry library via flt_otel_lib_init(): verifies
the C wrapper version, resolves the absolute path of the YAML
configuration file, calls otelc_init() to set up exporters, creates the
tracer, meter and logger objects, and registers custom memory allocation
and thread-id callbacks with the wrapper via otelc_ext_init().
- Sets the FLT_CFG_FL_HTX capability flag only for an HTTP-mode proxy, so
HAProxy attaches the filter to HTX streams; a TCP-mode proxy leaves it
unset and the filter runs on the raw stream.
flt_otel_ops_check():
- Resolves the sample fetch arguments set aside while the OTel file was
parsed, with the frontend and the backend capabilities combined so that a
backend-only fetch passes on a frontend proxy; an argument that does not
resolve is rejected.
- Validates that filter IDs are unique across all proxies.
- Resolves group->scope and instrumentation->scope/group placeholder
references to actual configuration structures (setting the ptr field
and flag_used).
- Rejects an otel-group that no 'groups' line of the instrumentation names.
- Warns about unused scopes, a scope that neither an event nor a group runs,
and a missing root span; rejects a second root span name and a root span
parented under a span.
- Validates metric instruments: binds the lines of one name to the create
line that owns its single creation, taken from a scope that runs, and
places the instrument in that scope until one creates it; rejects a
create-form name that repeats with another instrument type or an otherwise
differing definition, and an update whose create forms all stand in scopes
that never run.
- Rejects a resolved inject context name that another span already carries,
in this instance or in another OTel filter of the proxy, and a second
nameless-header context anywhere on the proxy.
- Rejects an 'extract' context name that a span of the configuration already
carries.
- Rejects a 'parent', 'link', 'finish', 'set-var-ctx' or 'log-record' name
that no otel-scope defines as a span, or as an extracted context where
one may stand.
- Rejects a used scope whose event runs before the request context can be
read while 'require-context' is set, and a configuration where no used
scope extracts one at all.
- Rejects an 'inject' or an 'extract' that carries the context in HTTP
headers on a proxy that is not in HTTP mode.
- Warns about a used scope bound to an event the proxy keeps from firing: an
HTTP-phase event on a proxy that is not in HTTP mode, on-stream-start or a
frontend-phase event for a filter of a backend section, the body event
among them unless that backend asks for the body itself, on-stream-stop
for such a filter, which HAProxy releases before that callback, and the
backend-phase request events on a listen proxy, which fire there only for
a stream switched to another backend or routed in from another frontend.
- Computes the aggregated analyzer bitmask from all used scopes.
flt_otel_ops_init_per_thread():
- Starts the tracer, meter and logger background threads on first call.
- Uses an atomic claim on instr->flag_started to guarantee that start runs
on a single thread only; the FLT_CFG_FL_HTX flag is set elsewhere, in
flt_otel_ops_init(), not here.
flt_otel_ops_deinit():
- Force-flushes the tracer, meter and logger within one shared
budget, or sets a zero flush budget instead under 'option noflush'.
- Destroys the tracer, meter and logger.
- Frees the entire configuration tree.
- Calls otelc_deinit() to shut down the wrapper library.
5.2 Stream-Level Callbacks
flt_otel_ops_attach():
- Checks if the filter is globally disabled; returns IGNORE.
- Applies rate limiting via ha_random32(); returns IGNORE if the random
value exceeds the configured rate_limit.
- Creates the runtime context (flt_otel_runtime_context_init) with a
generated UUID and initialized span/context lists.
- Sets pre_analyzers and post_analyzers bitmasks from the instrumentation's
aggregated analyzer flags. AN_REQ_WAIT_HTTP and AN_RES_WAIT_HTTP are
placed in post_analyzers because those analyzers can only be used in the
post_analyze callback. AN_REQ_HTTP_TARPIT stays in pre_analyzers; it is
left out only when channel_start_analyze force-injects the pre_analyzers
into the channel, so the tarpit event fires only when a tarpit rule has
armed that analyzer itself.
- Arms the idle timer from the precomputed minimum idle_timeout and sets
the first wake-up on the stream task. The stream-start callback would
miss a filter attached at backend selection, as HAProxy runs it for the
frontend filters alone.
flt_otel_ops_detach():
- Frees the runtime context, which finishes all remaining active spans and
destroys all remaining contexts.
flt_otel_ops_check_timeouts():
- Disarms the idle timer when the filter is disabled for the stream.
- Checks whether the idle-timeout timer has expired; if so, fires the
on-idle-timeout event and reschedules the timer for the next interval
(disarming instead when the event itself disabled the filter).
- Sets STRM_EVT_MSG on the stream's pending_events to ensure the filter is
re-evaluated after a timeout.
- Re-asserts the idle wake-up on the stream task expiry on every pass.
5.3 Error Handling
Helper functions manage errors:
flt_otel_return_int() / flt_otel_return_void():
- If the result indicates an error or an error string is set: in hard-error
mode, the filter is disabled for the current stream (flag_disabled = 1)
and the disabled counter is incremented atomically. In soft-error mode,
the error is merely logged.
- The error string is always freed.
- For int returns, FLT_OTEL_RET_OK is returned regardless, so the stream
continues processing even after an error.
6 Event Processing (Channel Analyzers)
----------------------------------------------------------------------
The filter maps HAProxy channel analyzer callbacks to a table of named events
defined in event.h (FLT_OTEL_EVENT_DEFINES).
6.1 Event Table
Each event entry carries:
- an_bit: the HAProxy analyzer bit (AN_REQ_*, AN_RES_*)
- an_name: the analyzer bit name (e.g. "AN_REQ_HTTP_PROCESS_FE")
- smp_opt_dir: sample fetch direction (REQ or RES); an event that runs
on no channel carries neither, and its samples are fetched
on the request side
- smp_val_fe/be: SMP_VAL_FE_*/SMP_VAL_BE_* fetch-location masks for the
event's processing point, paired per event as the SPOE
filter pairs its own; flt_otel_ops_check() ORs the two
into the location a bound scope's fetches and conditions
are validated against. The values are not everywhere the
same as SPOE's: on-client-session-start carries the HTTP
request header location, where SPOE carries the connection
accept one, because the HTTP fetches do resolve at that
point on an HTTP-mode proxy
- flag_http_inject: whether span context can be injected into HTTP headers
at this point
- flag_http_extract: whether span context can be extracted from HTTP headers
at this point, which needs a channel to read
- flag_http_only: whether the event fires only on an HTTP-mode proxy
- flag_context: whether the incoming trace context can be read at this
point, which 'require-context' needs
- name: configuration event name (e.g. "on-frontend-http-request")
Events with an_bit == 0 are pseudo-events not tied to any channel
analyzer. The stream lifecycle callbacks fire:
- on-stream-start (flt_otel_ops_stream_start, before channel processing)
- on-stream-stop (flt_otel_ops_stream_stop, after channel processing)
The check_timeouts callback fires periodically:
- on-idle-timeout (flt_otel_ops_check_timeouts, when stream is idle)
The stream_set_backend callback fires:
- on-backend-set (flt_otel_ops_stream_set_backend, when backend is assigned)
The HTTP lifecycle callbacks fire:
- on-http-headers-request / on-http-headers-response (flt_otel_ops_http_headers)
- on-http-end-request / on-http-end-response (flt_otel_ops_http_end)
- on-http-reply (flt_otel_ops_http_reply)
But on-http-reply never fires on current HAProxy; section 6.2 says why, and the
full explanation is in README and README-configuration.
The remaining pseudo-events fire from channel start/end callbacks:
- on-client-session-start / on-client-session-end
- on-server-session-start / on-server-session-end
- on-server-unavailable
The events on-stream-start and on-stream-stop pass a NULL channel argument, so
neither context injection nor extraction via HTTP headers can be used there,
while on-idle-timeout and on-backend-set are handed the request channel to read,
which allows extraction but not injection. These events fetch their samples
with a direction that is neither the request nor the response one, so that the
'finish' wildcards leave their spans alone and a sample fetched there reads the
request side.
6.2 Callback Flow
attach(s, f):
- Fires no event; see 5.2. Runs for a filter of a backend section as well,
at backend selection.
stream_start(s, f):
- Fires on-stream-start with chn=NULL.
- Called when a new stream begins, before any channel processing, for the
filters of the stream's frontend alone: a filter of a backend section
never sees it.
stream_set_backend(s, f, be):
- Fires on-backend-set with chn=&s->req.
- Called when a backend is assigned, the frontend itself included.
stream_stop(s, f):
- Fires on-stream-stop with chn=NULL.
- Called when a stream is destroyed, after all channel processing, over the
filters still attached: a filter of a backend section is released at the
end of the channel analysis and never sees it.
check_timeouts(s, f):
- Fires on-idle-timeout with chn=&s->req when the idle timer expires, and
reschedules it; see 5.2.
channel_start_analyze(chn):
- Records on the response channel that its analysis started, the sign that
a server was reached.
- Enables the per-channel analyzers from pre_analyzers (except
AN_REQ_HTTP_TARPIT, which only an armed tarpit rule may enable), on an HTX
stream only, and for a filter attached at backend selection only those
past the backend start, so the frontend rules do not run a second time.
- Fires on-client-session-start (request) or on-server-session-start
(response).
channel_pre_analyze(chn, an_bit):
- Looks up the event by an_bit in the event table.
- Calls flt_otel_event_run() for the matching event.
channel_post_analyze(chn, an_bit):
- Same as pre_analyze but for post-analyzers (AN_REQ_WAIT_HTTP,
AN_RES_WAIT_HTTP).
channel_end_analyze(chn):
- Fires on-client-session-end (request) or on-server-session-end (response).
- On the channel whose analysis ends last: if the response channel never
began its analysis (no server was reached), fires on-server-unavailable.
http_headers(s, f, msg):
- Fires on-http-headers-request or on-http-headers-response depending on
msg->chn direction.
http_end(s, f, msg):
- Fires on-http-end-request or on-http-end-response depending on
msg->chn direction.
http_reply(s, f, status, msg):
- Fires on-http-reply with chn=&s->res.
- Never reached on current HAProxy: HAProxy dropped the flt_http_reply()
call in 2.2-dev8 (commit 8dfeccf6d, 2020), so this callback has no caller.
6.3 Scope Execution
flt_otel_event_run() (event.c):
- Captures timestamps (CLOCK_MONOTONIC + CLOCK_REALTIME).
- Iterates all scopes matching the event; calls flt_otel_scope_run() for
each used scope.
flt_otel_scope_run() (event.c):
1. Evaluates the scope's ACL condition and, if it does not hold, returns
without processing.
2. Extracts contexts: for each configured extract directive, reads
the span context from HTTP headers or HAProxy variables via
flt_otel_scope_context_init().
3. Runs "set-var" directives via flt_otel_scope_run_set_var(), setting
HAProxy variables from sample expressions (before any span runs).
4. Processes spans: for each configured span:
a. Calls flt_otel_scope_span_init() which either returns an existing
scope_span (by name) or creates a new one with resolved parent
reference.
b. Calls flt_otel_scope_span_start() which creates the OTel span via
tracer->start_span_with_options(), unless an earlier scope created
it already, and sets flag_norec when the sampler left the new span
out.
c. Resolves span links against the runtime context -- first searching
active spans, then extracted contexts. Unresolved links are skipped
(a debug build traces them; a release build stays silent).
d. Evaluates the attribute, event and status samples through
flt_otel_scope_span_samples(), and the baggage samples in place, both
of them via flt_otel_sample_add().
e. Calls flt_otel_scope_run_span() which:
- Adds all resolved links via span->add_link().
- Sets baggage, attributes, events, and status.
- Records the configured exceptions.
- Optionally injects the span context into HTTP headers and/or
HAProxy variables.
A span whose flag_norec is set skips the links of step c, the samples
of step d except the baggages, and the exceptions of step e, all of
which the SDK would discard. The span itself is created and injected
all the same, so the trace still reaches the downstream services with
the sampled flag cleared.
5. Runs "set-var-ctx" directives via flt_otel_scope_run_set_var_ctx(),
storing a field of a referenced span or context into a HAProxy
variable (after the spans have run, so their contexts are available).
6. Processes metric instruments via flt_otel_scope_run_instrument(), which
runs two passes: the first lazily creates the create-form instruments
whose condition passes, using HA_ATOMIC_CAS for thread-safe one-time
creation; the second records measurements for update-form instruments,
creating on the spot one not created yet (UNSET), waiting for one that
another thread is creating (PENDING) and skipping one given up after
repeated creation failures (FAILED).
7. Emits log records via flt_otel_scope_run_log_record(), which iterates
the scope's log-record list, skips entries below the logger's severity
threshold, evaluates sample expressions into a body string, resolves
the optional span reference, and emits the record via the logger.
8. Runs "unset-var" directives, removing the named HAProxy variables via
flt_otel_var_unset_byname() (each guarded by its optional condition); a
variable that an earlier passing line already names is skipped per the
first-match rule, checked via flt_otel_unset_var_taken().
9. Marks spans listed in "finish" directives; a fired "otel-stop" marks
every open span and context for completion.
10. Calls flt_otel_scope_finish_marked() to end marked spans/contexts.
11. Calls flt_otel_scope_free_unused() to remove finished and destroyed
scope_span/scope_context entries from the runtime lists.
12. If "otel-stop" fired, sets rt_ctx->flag_disabled = 1; the guard at the
top of flt_otel_scope_run() then skips the stream's remaining scopes.
7 Runtime Data Structures
----------------------------------------------------------------------
7.1 Runtime Context (per stream)
flt_otel_runtime_context:
stream Owning stream pointer.
filter Owning filter pointer.
uuid[40] Generated UUID v4 for the session.
flag_harderr Copied from instrumentation config.
flag_disabled Set when the filter encounters a hard error or an 'otel-stop'
directive fires, and by 'require-context' when no extract
of a scope yields a valid context.
flag_ctx_valid Set once an extract yields a valid upstream span context;
read with 'require-context' only.
flag_res_started Set when the response channel starts its analysis, that is
once a server connection is established.
logging Logging flags.
idle_timeout Idle timeout interval in milliseconds (0 = off).
idle_exp Tick at which the next idle timeout fires.
bytes_in Raw payload bytes seen on the request channel (TCP mode).
bytes_out Raw payload bytes seen on the response channel (TCP mode).
spans Linked list of flt_otel_scope_span.
contexts Linked list of flt_otel_scope_context.
7.2 Scope Span
flt_otel_scope_span:
id / id_len Span operation name (borrowed from config).
smp_opt_dir Direction in which the span was created.
flag_finish Set by finish directives, cleared after ending.
flag_norec Set when the created span reports that it does not record,
which keeps its samples from being evaluated.
span The OTel span object (NULL before start, NULL after
end_with_options).
ref_span Parent span pointer (resolved at init).
ref_ctx Parent context pointer (resolved at init).
list Chain in runtime_context.spans.
flt_otel_scope_span_init() performs memoization: if a span with the same name
already exists in rt_ctx->spans, it returns the existing entry. This allows
multiple scopes to contribute attributes/events to the same logical span.
7.3 Scope Context
flt_otel_scope_context:
id / id_len Context name (borrowed from config).
smp_opt_dir Direction in which the context was extracted.
flag_finish Marks the context for destruction.
context The OTel span_context object.
baggage The inbound baggage carrier, or NULL (used by set-var-ctx).
list Chain in runtime_context.contexts.
Similarly memoized: duplicate extraction of the same context name returns the
existing entry.
7.4 Scope Data (per span per scope run, stack-allocated)
flt_otel_scope_data:
baggage Key-value array for baggage items.
attributes Key-value array for span attributes.
events Linked list of flt_otel_scope_data_event (each with name,
an optional per-event timestamp in ts / ts_set, and a
key-value array).
links Linked list of flt_otel_scope_data_link (each with span
and/or context pointer).
status Status code and description string.
Initialized at the start of each span processing block and freed at the end.
The link entries hold borrowed pointers to the OTel span/context objects
owned by the runtime context, so those are not freed; each link's own
attribute key-value array is owned by the link, however, and is destroyed
via otelc_kv_destroy() before the link node itself is freed.
7.5 Span Finishing
finish <name> / finish * / finish *req* / finish *res*
The "finish" directive marks spans and contexts for completion:
- "*" marks all.
- "*req*" / "*res*" marks those created in the request/response direction
respectively.
- Otherwise, marks by exact name.
flt_otel_scope_finish_marked() iterates all marked entries:
- Spans are ended via span->end_with_options() which NULLs the span pointer;
an entry whose span creation failed carries none and is skipped.
- Contexts are destroyed via context->destroy() which NULLs the context
pointer.
flt_otel_scope_free_unused() then removes entries with NULL span/context
pointers from the runtime lists. For contexts, associated HTTP headers
and variables are also cleaned up.
On stream detach (flt_otel_runtime_context_free), any remaining active spans
are force-ended and all entries are freed.
8 Span Links
----------------------------------------------------------------------
Span links associate a span with other spans or contexts without establishing
a parent-child relationship.
8.1 Configuration
Two syntaxes are supported:
Inline (one link per span declaration):
span <name> [parent <ref>] link <ref> [root]
Standalone (multiple links, or one link with attributes):
link { <ref> [<ref> ...] | <ref> attr <key> <sample> ... } [{ if | unless } <condition>]
The flt_otel_conf_link structure stores each link target name and, for the
single-link form, a list of attribute samples. Each line is parsed on its own
private list to bypass the duplicate-name check, so the same target may be named
on several lines, while a repeated target within one line is still rejected.
The links list is initialized in flt_otel_conf_span_init() and destroyed in
flt_otel_conf_span_free(). The optional 'if'/'unless' condition is stored on
each conf_link; on a multi-name line the parser builds it into every created
link, so each of them is guarded independently.
8.2 Runtime Resolution