Compare commits

...

149 commits

Author SHA1 Message Date
Willy Tarreau
90e8ccd9c2 MINOR: sample: add new sample fetch functions reporting current CPU usage
Some checks are pending
Contrib / dev/flags/ (push) Waiting to run
Contrib / dev/haring/ (push) Waiting to run
Contrib / dev/hpack/ (push) Waiting to run
Contrib / dev/poll/ (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
VTest / Alpine+musl, gcc (push) Waiting to run
Windows / Windows, gcc, all features (push) Waiting to run
Some features can automatically turn on or off depending on CPU usage,
but it's not easy to measure it. Let's provide 3 new sample fetch functions
reporting the CPU usage as measured inside haproxy during the previous
polling loop, and reported in "idle" stats header / "show info", or used
by tune.glitches.kill.cpu-usage, or maxcompcpuusage:

  - cpu_usage_thr: CPU usage between 0 and 100 of the current thread, used
    by functions above
  - cpu_usage_grp: CPU usage between 0 and 100, averaged over all threads of
    the same group as the current one.
  - cpu_usage_proc: CPU usage between 0 and 100, averaged over all threads
    of the current process

Note that the value will fluctuate since it only covers a few tens to
hundreds of requests of the last polling loop, but it reports what is
being used to take decisions.

It could also be used to disable some non-essential debugging/processing
under too high loads for example.
2026-04-14 17:47:18 +02:00
Willy Tarreau
630ef96f92 MINOR: sample: return the number of the current thread group
Just like we have a sample fetch function that returns the number of the
current thread, let's have the same with the thread group number. This
can be useful for troubleshooting, given that certain things are currently
per thread-group (e.g. idle backend connections, certain LB algos etc).
2026-04-14 17:05:34 +02:00
Willy Tarreau
b943d2a7eb CLEANUP: sample: fix the comment regarding the range of the thread sample fetch
The comment says "between 1 and nbthread" while it's in fact between 0 and
nbthread-1 and this is also documented like this in the config manual. No
backport needed though it cannot hurt.
2026-04-14 16:59:56 +02:00
Willy Tarreau
9c6e07c43f MINOR: stats: report the number of thread groups in "show info"
Since thread groups were enabled by default in 3.3, it has become an
important element of diagnostic that we're missing in "show info". Let's
add it under "NbThreadGroups".
2026-04-14 16:48:16 +02:00
Willy Tarreau
e9fcf994ca BUILD: Makefile: don't forget to also delete haterm on make clean
haterm depends on the same source files as haproxy, yet it wasn't deleted
on "make clean", resulting in confusion when rebuilding and believing to
run the freshly built one. Let's just add it to the "clean" target. No
backport is needed since haterm is 3.4-only.
2026-04-14 16:48:16 +02:00
William Lallemand
788f57448a EXAMPLES: ssl: keylog entries are greater than 1024
Adjust the log size to 2048, the default 1024 bytes of a log line are
too small since f28dd15 ("MINOR: ssl: add TLS 1.2 values in
HAPROXY_KEYLOG_XX_LOG_FMT")
2026-04-14 16:24:28 +02:00
William Lallemand
f28dd158ed MINOR: ssl: add TLS 1.2 values in HAPROXY_KEYLOG_XX_LOG_FMT
Add the CLIENT_RANDOM line for TLS1.2 in HAPROXY_KEYLOG_FC_LOG_FMT and
HAPROXY_KEY_LOG_BC_FMT. These are useful to produce a keylog file
compatible with both TLS1.3 and TLS1.2.
2026-04-14 16:03:25 +02:00
Christopher Faulet
7270bfcff5 BUG/MEDIUM: htx: Don't count delta twice when block value is replaced
A regression was introduced by the commit a8887e55a ("BUG/MEDIUM: htx: Fix
function used to change part of a block value when defrag").

When a block value was replaced and a defragmentation was performed, the
delta between the old value and the new one was counted twice. htx_defrag()
already is responsible to set the new size for the HTX message. So it must
not be performed in htx_replace_blk_value().

This patch must be backported with the commit above. So theorically to all
stable versions.
2026-04-14 14:07:21 +02:00
Christopher Faulet
d899f23017 BUG/MEDIUM: htx: Properly handle block modification during defragmentation
A regression was introcuded by the commit 0c6f2207f ("MEDIUM: htx: Refactor
htx defragmentation to merge data blocks").

When a defragmentation is performed, it is possible to alter a block
size. The main usage is to prepare a block value replacement. However, since
the commit above, the change is no longer handled. The block info are
changed but the size of the message is not modified accordingly.

This patch depends on the commit "MINOR: htx: Add helper function to get
type and size from the block info field"

No backport needed.
2026-04-14 14:07:21 +02:00
Christopher Faulet
ebaa88a23a MINOR: htx: Add helper function to get type and size from the block info field
__htx_blkinfo_type() and __htx_blkinfo_size() function was added to return,
respectively, the type and the size from the block info field. The main
usage for these functions is internal to the htx code.
2026-04-14 14:07:21 +02:00
Tim Duesterhus
6ff659db06 CI: Build dev/haring/ as part of contrib.yml
This functionally is quite similar to `dev/flags/` and thus it makes sense to
include it.
2026-04-14 11:16:17 +02:00
Tim Duesterhus
c9067a6ed7 CI: Use matrix: in contrib.yml
This makes it much easier to add additional "smoke-tests" to contrib.yml. The
previous set-up also didn't allow to easily see all failures when a single
build fails, because it would abort after any failed step.
2026-04-14 11:16:17 +02:00
Tim Duesterhus
291d265aeb CI: Run contrib.yml on ubuntu-slim
This is sufficient for this simple "does it compile" smoke-test.
2026-04-14 11:16:17 +02:00
Tim Duesterhus
2ccd2827d4 CI: Generate vtest.yml matrix on ubuntu-slim
This runner image is more lightweight by running inside of a container rather
than a full VM. This is sufficient to run some Python.
2026-04-14 11:16:17 +02:00
Tim Duesterhus
578d971d7d CI: Use case() function
GitHub Actions introduced a new `case()` function in January that works just
like HAProxy’s `iif()` converter (just with an arbitrary number of
expressions). It is more robust than chaining strings with `&&` and `||`,
because it includes proper type-checking.

see https://github.blog/changelog/2026-01-29-github-actions-smarter-editing-clearer-debugging-and-a-new-case-function/#write-more-expressive-expressions-with-a-case-function
2026-04-14 11:16:17 +02:00
Tim Duesterhus
7640d7949f CI: Integrate Musl build into vtest.yml
With the previous sync, these two workflows perform almost the same steps and
both logically belong to "Run VTest tests". Integrate musl.yml into vtest.yml,
which will hopefully encourage future changes to consistently apply to all jobs
in that workflow.
2026-04-14 11:16:17 +02:00
Tim Duesterhus
76d1dbfb61 CI: Sync musl.yml with vtest.yml
This syncs up musl.yml with vtest.yml as much as possible by:

- Aligning indentation.
- Reordering steps.
- Aligning step names.
- Adding missing functionality to musl.yml.
2026-04-14 11:16:17 +02:00
Tim Duesterhus
edf72ac1a4 CI: Use sh in actions/setup-vtest/action.yml
Bash might not always be preinstalled and we don't make use of any
bash-specific features either. Switch to POSIX sh for simplicity.

This partly reverts the fix in 073240044e, which
installed `bash` for the musl job.
2026-04-14 11:16:17 +02:00
Tim Duesterhus
716218f723 CI: Remove obsolete steps from musl.yml
These have become obsolete with the use of `./.github/actions/setup-vtest` in
6e67b59aca.
2026-04-14 11:16:17 +02:00
William Lallemand
3415abe56d MINOR: mjson: reintroduce mjson_next()
The lack of mjson_next() prevents to iterate easily and need to hack by
iterating on a loop of snprintf + $.field[XXX] combined with
mjson_find().

This reintroduce mjson_next() so we could iterate without having to
build the string.

The patch does not reintroduce MJSON_ENABLE_NEXT so it could be used
without having to define it.
2026-04-14 10:57:21 +02:00
William Lallemand
cf72132f22 MINOR: acme: display the type of challenge in ACME_INITIAL_DELAY
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
The ACME_INITIAL_DELAY state displays a message about 'dns-01', but this
state is also used for 'dns-persist-01'.

This patch displays the challenge that was configured instead of dns-01
2026-04-14 10:16:11 +02:00
Tim Duesterhus
ed0c51d2c0 MINOR: http_fetch: Add support for checks to unique-id fetch
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
This allows to use the `unique-id` fetch within `tcp-check` or `http-check`
ruleset. The format is taken from the checked server's backend (which is
naturally inherited from the corresponding `defaults` section).

This is particularly useful with

    http-check send ... hdr request-id %[unique-id]

to ensure all requests sent by HAProxy have a unique ID header attached.

This resolves GitHub Issue #3307.

Reviewed-by: Volker Dusch <github@wallbash.com>
2026-04-13 20:02:21 +02:00
Tim Duesterhus
2c748125f5 MINOR: check: Support generating a unique_id for checks
This implementation is directly modeled after `stream_generate_unique_id()` and
the corresponding `unique_id` field on `struct stream`.

It will be used in a future commit to enable the use of the `%[unique-id]`
fetch in check rules.
2026-04-13 20:01:42 +02:00
Tim Duesterhus
7ff2627112 CLEANUP: log: Stop touching struct stream internals for %ID
Use the return value of `stream_generate_unique_id()` instead of relying on the
`unique_id` field of `struct stream` when handling the `%ID` log placeholder.
This also allowed to unify the "stream available" and "stream not available"
paths.

Reviewed-by: Volker Dusch <github@wallbash.com>
2026-04-13 20:01:42 +02:00
Tim Duesterhus
38796d4c06 MINOR: Allow inlining of stream_generate_unique_id()
With the introduction of the `generate_unique_id()` helper, the actual
complicated logic is sitting in a different file. Allow inlining of
`stream_generate_unique_id()`, so that callers can benefit from an abstraction
without hiding away the access of `strm->unique_id` behind a function call.
2026-04-13 20:01:42 +02:00
Tim Duesterhus
73040e3a8e MINOR: Add generate_unique_id() helper
This new function will handle the actual generation of the unique ID according
to a format. The caller is responsible to check that no unique ID is stored
yet.
2026-04-13 20:01:02 +02:00
Tim Duesterhus
4cf06a7d23 CLEANUP: Make lf_expr parameter of sess_build_logline_orig() const
Since this is safely possible without making any changes, we can provide this
hint to the compiler.
2026-04-13 19:59:12 +02:00
Willy Tarreau
9a5db56a36 BUG/MINOR: haterm: don't apply the default pipe size margin twice
Commit 6d16b11022 ("BUG/MINOR: haterm: preserve the pipe size margin
for splicing") solved the issue of pipe size being sufficient for the
vmsplice() call, but as Christopher pointed out, the ratio was applied
to the default size of 64k, so now it's applied twice, giving 100k
instead of 80k. Let's drop it from there.

No backport needed.
2026-04-13 19:38:48 +02:00
Egor Shestakov
79c54d28b0 BUG/MINOR: acme: don't pass NULL into format string
Printing a "(null)" when NULL passed with the %s format specifier is a
GNU extension, so it must be avoided for portability reasons.

Must be backported as far as 3.2
2026-04-13 18:56:13 +02:00
William Lallemand
53679fe5f6 BUG/MINOR: acme: read the wildcard flag from the authorization response
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
The wildcard field was declared and used when building the dns-persist-01
TXT record value (policy=wildcard suffix), but was never populated from
the server's authorization response. Add the missing mjson_get_bool() call
to read $.wildcard before saving auth->dns.
2026-04-13 18:49:53 +02:00
William Lallemand
a5e732ed1d DOC: configuration: document dns-persist-01 challenge type and options
Document the dns-persist-01 challenge type under the challenge keyword,
the challenge-ready dns option (existence-only TXT check for dns-persist-01),
and the default challenge-ready value when challenge is dns-persist-01.
2026-04-13 18:45:08 +02:00
William Lallemand
39476040ec MINOR: acme: extend resolver-based DNS pre-check to dns-persist-01
Add challenge_type parameter to acme_rslv_start() to select the correct
DNS lookup prefix: _validation-persist.<domain> for dns-persist-01 and
_acme-challenge.<domain> for dns-01.

Default cond_ready to ACME_RDY_DNS|ACME_RDY_DELAY for dns-persist-01.
Extend ACME_CLI_WAIT to cover dns-persist-01 alongside dns-01.

In ACME_RSLV_READY, check only TXT record existence for dns-persist-01
since the resolver cannot parse multiple strings within a single TXT entry.
2026-04-13 18:45:08 +02:00
Mia Kanashi
0d3689959d MEDIUM: acme: implement dns-persist-01 challenge
Implements draft DNS-PERSIST-01 challenge based on
https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-persist

Blog post: https://letsencrypt.org/2026/02/18/dns-persist-01

This challenge is designed to use preprovisioned DNS records,
unlike DNS-01 challenge it doesn't need per provider API integration.

In short instead of validating order by crafting a custom response
based on input recieved from ACME server, like other challenges do
in particular DNS-01, HTTP-01, TLS-ALPN-01, in this challenge you
authorize domain statically, ACME account key functions similar to
a private key and accounturi in the record functions like a public key,
ACME server verifies that account uri matches account key and authorizes
based on that. You only need to write DNS record one time,
accounturi binds to an account key, and will only change if new account
key is created, although it is possible to rotate account key without
changing account uri.

Main benefits of this challenge in contrast to DNS-01:
1. Security, no need to give reverse proxy write access to the DNS.
2. Simplicity, no complex per provider integrations like Lego needed.
3. Robustness, no worrying about DNS record cache each renewal.

It would be used like this:
1. generate an account key ahead of time
2. add required DNS record manually or automatically using IaC tools
3. start HAProxy with the same account key used

Intended way to use this challenge is with a code that will print
and maybe sets DNS records ahead of time. For example that could
be integrated into the IaC provisioning step. This challenge type
is extremely recent though, so those integrations are yet to be written.

It is possible to do this challenge without extra tools too,
with pebble / challtestsrv steps would be as following:

After starting HAProxy it will print required records in the logs.

With challtestsrv you can then set those records like this:

curl -d '{
  "host":"_validation-persist.localhost.",
  "value": "pebble.letsencrypt.org; accounturi=...; policy=wildcard"}
' http://localhost:8055/set-txt

After setting the records run renew with the name of the certificate:

echo "acme renew @cert/localhost.pem" \
  | socat stdio tcp4-connect:127.0.0.1:9999

Or just restart HAProxy.

Unlike with DNS-01 you don't have to worry about DNS records changing,
if there is any problem with DNS records you can just retry.
2026-04-13 18:45:08 +02:00
Willy Tarreau
6d16b11022 BUG/MINOR: haterm: preserve the pipe size margin for splicing
Originally in httpterm we used to allocate 5/4 of the size of a pipe to
permit to use vmsplice because there's some fragmentation or overhead
internally that requires to use a bit of margin. While this was initially
applied to haterm as well, it was accidentally lost with commit fb82dece47
("BUG/MEDIUM: haterm: Properly initialize the splicing support for haterm"),
resulting in errors about vmsplice() whenever tune.pipesize is set. Let's
enforce the ratio again.

No backport is needed.
2026-04-13 18:37:04 +02:00
Egor Shestakov
61f04d1951 MINOR: errors: remove excessive errmsg checks
I noticed some strange checks for presence of errmsg. Called functions
generate non-empty error message in case of failure, so a non-NULL address
of the error message is enough.

No backport needed.
2026-04-13 15:39:05 +02:00
Christopher Faulet
5331093195 REGTESTS: Never reuse server connection in reg-tests/jwt/jwt_decrypt.vtc
A "Connection: close" header is added to responses to avoid any connection
reuse. This should avoid errors on the client side.
2026-04-13 15:18:47 +02:00
Christopher Faulet
ebb801d7c8 BUG/MEDIUM: cli: Properly handle too big payload on a command line
When command line is parsed, when the payload was too big the error was not
properly handled. Instead of leaving the parsing function to print the
error, we looped infinitly trying to parse remaining data.

When the command line is too big, we must exit the parsing function in
CLI_ST_PRINT_ERR state. Instead of exiting the function, we only left the
while loop, setting this way the cli applet in CLI_ST_PROMPT state.

This patch must be backported as far as 3.2.
2026-04-13 15:18:47 +02:00
Egor Shestakov
c82b10b8d2 MINOR: tools: memvprintf(): remove <out> check that always true
memvprintf() exits early if the <out> is NULL, so the further NULL check is
redundant.

No backport needed.
2026-04-13 14:36:52 +02:00
Ilia Shipitsin
76f74d7a55 CI: build WolfSSL job with asan enabled
Reference: https://github.com/haproxy/haproxy/issues/3317

this allows to distribute memory checking to WolfSSL code as well

Only applies on the WolfSSL weekly job which build the wolfssl git
version.
2026-04-13 14:03:30 +02:00
Tim Duesterhus
801d028790 CLEANUP: http_fetch: Use local unique_id variable in smp_fetch_uniqueid()
Instead of relying on the implementation detail that
`stream_generate_unique_id()` will store the unique ID in `strm->unique_id` we
should use the returned value, especially since that one is already checked in
the `isttest()`.

Reviewed-by: Volker Dusch <github@wallbash.com>
2026-04-13 14:02:29 +02:00
Tim Duesterhus
44a461a19f CLEANUP: stream: Reduce duplication in stream_generate_unique_id()
The return value of the `if()` and `else` branch is identical. We can just move
it out of conditional paths.

Reviewed-by: Volker Dusch <github@wallbash.com>
2026-04-13 14:02:29 +02:00
Tim Duesterhus
f778e6feb0 CLEANUP: stream: Explain the two-step initialization in stream_generate_unique_id()
This two-step initialization of `strm->unique_id` looks like a refactoring
target. Add a comment to prevent regressions of the fix in
fb7b5c8a53.
2026-04-13 14:02:29 +02:00
Tim Duesterhus
756ad19f04 CLEANUP: log: Return size_t from sess_build_logline_orig()
`sess_build_logline_orig()` takes a `size_t maxsize` as input and accordingly
should also return `size_t` instead of `int` as the resulting length. In
practice most of the callers already stored the result in a `size_t` anyways.
The few places that used an `int` were adjusted.

This Coccinelle patch was used to check for completeness:

    @@
    type T != size_t;
    T var;
    @@

    (
    * var = build_logline(...)
    |
    * var = build_logline_orig(...)
    |
    * var = sess_build_logline(...)
    |
    * var = sess_build_logline_orig(...)
    )

Reviewed-by: Volker Dusch <github@wallbash.com>
2026-04-13 14:02:29 +02:00
Tim Duesterhus
34c17608e7 BUG/MINOR: log: Fix error message when using unavailable fetch in logfmt
The following configuration:

    defaults
    	unique-id-format TEST-%[srv_name]

    frontend fe_http
    	mode http

    	bind :::8080 v4v6

Emitted the following error:

    [ALERT]    (219835) : Parsing [./patch.cfg:2]: failed to parse unique-id : sample fetch <srv_name]> may not be reliably used here because it needs 'server' which is not available here.

The `]` in the name of the sample fetch should not be there.

This bug exists since at least HAProxy 2.4, which is the oldest supported
version. The fix should be backported there.

Reviewed-by: Volker Dusch <github@wallbash.com>
2026-04-13 14:02:29 +02:00
Amaury Denoyelle
34c9ded340 BUG/MINOR: quic: do not use hardcoded values in QMux TP frame builder
Reuse QUIC transport parameters value set in xprt_qstrm layer in frame
builder function. Prior to this patch, mux_quic would use different
values from the advertised ones.

No need to backport.
2026-04-13 13:38:11 +02:00
William Lallemand
073240044e CI: github: add bash to the musl job
Previous commit 6e67b59 ("CI: Consistently set up VTest with
./.github/actions/setup-vtest") requires bash to use the github action.

This commit adds bash to the list of installed package in alpine.
2026-04-13 11:28:51 +02:00
Amaury Denoyelle
175717f5be MINOR: mux_quic: remove duplicate QMux local transport params
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
When QMux was first implemented, values used for emitted transport
parameters in xprt_qstrm and local flow control in mux_quic were
initialized separately. This is error prone in particular if a value is
change in one layer but not the other.

This patch fixes this by using xprt_qstrm_lparams() in QMux init
function. Mux flow control is then loaded with these values. Thus all
values are now initialized in a single place which is xprt_qstrm_init().
2026-04-13 09:38:46 +02:00
Tim Duesterhus
6e67b59aca CI: Consistently set up VTest with ./.github/actions/setup-vtest
Two jobs still used `scripts/build-vtest.sh` directly, which seems like an
oversight.
2026-04-13 09:34:07 +02:00
Tim Duesterhus
a7c2cf9274 CI: Merge aws-lc-template.yml into aws-lc.yml
There is no need to have an entirely separate workflow, when we can just use a
build matrix for the variation.
2026-04-13 09:34:07 +02:00
Tim Duesterhus
5ea919fa7c CI: Merge aws-lc.yml and aws-lc-fips.yml into aws-lc.yml
These two jobs run on exactly the same triggers and are effectively variations
of each other. There is no need to have two separate workflows for them.
2026-04-13 09:34:07 +02:00
Tim Duesterhus
86430ab5a4 CI: Simplify version extraction with haproxy -vq
Instead of running `awk` on the output of `haproxy -v` to extract the bare
version number, we can use `haproxy -vq`.
2026-04-13 09:34:07 +02:00
Tim Duesterhus
c6b9ba80ae CI: Update to actions/checkout@v6
No functional change, but we should keep this current.

see 5f4ddb54b0
see 5c923f1869
see b81a7f428b
2026-04-13 09:34:07 +02:00
Tim Duesterhus
abcf2d757d CI: Fix regular expression escaping in matrix.py
This fixes:

    .github/matrix.py:72: SyntaxWarning: "\." is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\."? A raw string is also an option.
      return re.match('^v[0-9]+(\.[0-9]+)*$', version_string)
    .github/matrix.py:89: SyntaxWarning: "\." is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\."? A raw string is also an option.
      return re.match('^AWS-LC-FIPS-[0-9]+(\.[0-9]+)*$', version_string)
    .github/matrix.py:106: SyntaxWarning: "\." is an invalid escape sequence. Such sequences will not work in the future. Did you mean "\\."? A raw string is also an option.
      return re.match('^v[0-9]+(\.[0-9]+)*-stable$', version_string)
2026-04-13 09:34:07 +02:00
Tim Duesterhus
5d6a09580a CI: Wrap all if: conditions in ${{ }}
While `if:` also works with a bare condition, it is a best practice to always
wrap "dynamic placeholders" in `${{ }}`.

See: https://github.blog/changelog/2026-01-29-github-actions-smarter-editing-clearer-debugging-and-a-new-case-function/#better-if-condition-handling
2026-04-13 09:34:07 +02:00
Tim Duesterhus
a4737cca08 CI: Consistently add a top-level permissions definition to GHA workflows
This makes it easy to verify the permissions and to apply them to all jobs
within a given workflow.
2026-04-13 09:34:07 +02:00
Tim Duesterhus
991d5dabe0 CI: Drop obsolete packages: write permission from quic-interop-*.yml
This is no longer necessary since dfe1de4335.
2026-04-13 09:34:07 +02:00
Miroslav Zagorac
40d4fa29fd MINOR: ot: renamed the variable dbg_indent_level to flt_ot_dbg_indent_level
The thread-local variable dbg_indent_level used a generic name that could
collide with identifiers in other compilation units.  Renamed it to
flt_ot_dbg_indent_level so that it carried the flt_ot_ prefix consistent
with the rest of the OpenTracing filter namespace.  The rename covered the
declaration, definition, and all macro references in debug.h, parser.c and
util.c.
2026-04-13 09:23:30 +02:00
Miroslav Zagorac
f2629342b2 BUILD: ot: removed explicit include path when building opentracing filter
The -Iaddons/ot/include flag in OT_CFLAGS allowed source files to use a
bare #include "include.h", which was fragile because it depended on the
compiler search path.  Removed that flag from the Makefile and changed
every source file under addons/ot/src/ to use the relative include path
../include/include.h instead.  This made header resolution explicit and
consistent with standard addon conventions.
2026-04-13 09:23:30 +02:00
Miroslav Zagorac
124e32ad78 BUG/MINOR: ot: fixed wrong NULL check in flt_ot_parse_cfg_group()
After calling flt_ot_conf_group_init() and storing the result in
flt_ot_current_group, the code incorrectly checked flt_ot_current_config
for NULL instead of the newly assigned flt_ot_current_group.  This meant
a failed group init was never detected and the error path was never taken.
2026-04-13 09:23:30 +02:00
Miroslav Zagorac
9fa34e592a BUG/MINOR: ot: removed dead code in flt_ot_parse_cfg_str()
The local variable str was declared but never assigned a value other than
NULL.  The error-handling block that called flt_ot_conf_str_free(&str) on
it was therefore a no-op.  Removed both the unused variable and the dead
cleanup path.
2026-04-13 09:23:30 +02:00
Miroslav Zagorac
de41121c0c CLEANUP: ot: use the item API for the variables trees
In flt_ot_vars_scope_dump(), switched from cebu64_first()/cebu64_next() to
cebu64_imm_first()/cebu64_imm_next() for iterating the variable name trees.
Since this function only reads variables under a read lock, the immutable
traversal API is the correct choice.  Also updated the container_of()
member from 'node' to 'name_node' to match the current struct var layout.
2026-04-13 09:23:30 +02:00
Miroslav Zagorac
dae6fa9c6f DOC: otel: added README.md overview document
Added a Markdown-formatted overview covering features, build instructions,
configuration, scope keywords, CLI commands, performance benchmarks and
test configurations.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
7c66bb5497 MINOR: otel: changed instrument attr to use sample expressions
Replaced the static key-value attribute storage in update-form instruments
with sample-evaluated attributes, matching the log-record attr change.
The 'attr' keyword now accepts a key and a HAProxy sample expression
evaluated at runtime.

The struct (conf.h) changed from otelc_kv/attr_len to a list of
flt_otel_conf_sample entries.  The parser (parser.c) calls
flt_otel_parse_cfg_sample() with n=1 per attr keyword.  At runtime
(event.c) each attribute is evaluated via flt_otel_sample_eval() and
added via flt_otel_sample_add_kv() to a bare flt_otel_scope_data_kv,
which is passed to the meter.

Updated documentation, debug macro and test configurations.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
651e9fd8a7 MINOR: otel: changed log-record attr to use sample expressions
Replaced the static key-value attribute storage in log-record with
sample-evaluated attributes.  The 'attr' keyword now accepts a key and a
HAProxy sample expression evaluated at runtime, instead of a static string
value.

The struct (conf.h) changed from otelc_kv/attr_len to a list of
flt_otel_conf_sample entries.  The parser (parser.c) calls
flt_otel_parse_cfg_sample() with n=1 per attr keyword.  At runtime
(event.c) each attribute is evaluated via flt_otel_sample_eval() and added
via flt_otel_sample_add_kv() to a bare flt_otel_scope_data_kv, which is
passed to logger->log_span().

Updated documentation, debug macro and test configurations.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
d96ce16cef MINOR: otel: added flt_otel_sample_eval and exposed flt_otel_sample_add_kv
Factored the sample evaluation logic out of flt_otel_sample_add() into a
new flt_otel_sample_eval() function that evaluates a sample definition
into an otelc_value.  Both the log-format path and the bare sample
expression path are handled, with a flag_native parameter controlling
native type preservation for single-expression samples.
flt_otel_sample_add() now calls flt_otel_sample_eval() and dispatches the
result.

Made flt_otel_sample_add_kv() non-static so callers outside util.c can
add key-value pairs directly to a bare flt_otel_scope_data_kv without
requiring the full flt_otel_scope_data structure.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
20c740db8c DOC: otel: added cross-cutting design patterns document
Added README-design covering the internal design patterns that span
multiple source files: X-macro code generation, type-safe generic macros,
error handling architecture, thread safety model, sample evaluation
pipeline, rate limiting, memory management, context propagation, debug
infrastructure, idle timeout mechanism, group action integration and CLI
runtime control.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
bb2c512d29 DOC: otel: test: added speed test guide and benchmark results
The test directory gained a speed test guide (README-test-speed)
explaining how to run performance benchmarks at various rate-limit levels,
together with benchmark result files for the standalone, composite,
context-propagation, and frontend-backend test configurations.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
081a8fd5cf DOC: otel: test: added test README-* files
Added README documentation for each test configuration (sa, cmp, ctx,
fe-be, empty, full) describing event coverage, signal usage, instrument
tables, span hierarchies and run instructions.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
8d8d7e4602 DOC: otel: added documentation
Added the full documentation set for the OpenTelemetry filter.

The main README served as the user-facing guide covering build
instructions, core OpenTelemetry concepts, the complete filter
configuration reference, usage examples with worked scenarios,
CLI commands, and known limitations.

Supplementary documents provided a detailed configuration guide with
worked examples (README-configuration), an internal C structure reference
for developers (README-conf), a function reference organized by source
file (README-func), an architecture and implementation review
(README-implementation), and miscellaneous notes (README-misc).
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
579d1247f2 MINOR: otel: test: added full-event test config
Added the 'full' test configuration that exercises all 29 supported OTel
filter events with all three signal types (traces, metrics, logs).  Every
instrument definition has a corresponding update.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
6f177cd01e MINOR: otel: added log-record signal support
Added "log-record" as the third OpenTelemetry signal alongside traces
(span) and metrics (instrument).  This includes the
flt_otel_conf_log_record structure definition, parser keyword defines,
the otel-scope section parser with optional "id", "event", "span", and
"attr" keywords followed by sample fetch expressions or a log-format
string, init/free lifecycle, scope list wiring, log-format evaluation
in flt_otel_scope_run_instrument_record(), a test configuration example,
log-record span reference validation in flt_otel_check(), and logger
handle creation, startup, and teardown in the filter lifecycle.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
bf05a014db MINOR: otel: added metrics instrument support
Added the "instrument" keyword to otel-scope sections for recording metric
measurements alongside traces.

Introduced flt_otel_conf_instrument holding instrument type, description,
unit, sample expressions, and optional key-value attributes.  The
supported synchronous integer-precision instrument types were counters,
histograms, up-down counters, and gauges.

Instruments followed a two-form design: a "create" form defined a new
instrument with its type and value expression, while an "update" form
recorded measurements against an existing instrument with per-scope
attributes.

Instrument creation was performed lazily at first use with HA_ATOMIC_CAS
to guarantee thread-safe one-time initialization.  The configuration
check phase validated that every update-form had a matching create-form
definition and that create-form names were unique across all scopes.

The meter lifecycle was integrated into filter init and deinit, starting
the meter alongside the tracer and shutting it down during cleanup.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
eaa05d2af3 MINOR: otel: added span link support
Added span link support, allowing a span to reference other spans or
extracted contexts without establishing a parent relationship.

Introduced the flt_otel_conf_link structure and added a links list to
flt_otel_conf_span.  The parser accepted both an inline syntax on the span
declaration line ("span <name> link <target>") and a standalone multi-
argument form ("link <span> ..."), each creating a conf_link entry
appended to the span's link list.

At runtime, each configured link name was resolved against the active
spans and extracted contexts in the runtime context.  Resolved references
were collected into flt_otel_scope_data_link entries and passed to the C
wrapper add_link API during span creation.

Initialization, cleanup, and debug dump routines were added for the link
data structures at both configuration and runtime levels.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
d8e69f07dc MINOR: otel: test: added test and benchmark suite for the OTel filter
Added a test suite under addons/otel/test/ for the OpenTelemetry filter.
Five scenarios exercise different filter capabilities: standalone (sa)
covers all hook points including idle-timeout heartbeats, metrics and log
records; compact (cmp) covers the full request/response lifecycle with
ACL-based error handling; context (ctx) tests explicit inject/extract
propagation through numbered context variables; frontend/backend (fe/be)
tests distributed tracing across two HAProxy instances; and empty tests
bare filter initialisation with no active scopes.

A performance benchmarking script (test-speed.sh) uses wrk to measure
throughput and latency at different rate-limit settings (100% through 0%,
disabled, and filter-off).  Each scenario includes comprehensive YAML
exporter definitions covering OTLP file/gRPC/HTTP, ostream, memory,
Zipkin, and Elasticsearch backends.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
bc6402f609 MINOR: otel: added log-format support to the sample parser and runtime
Extended flt_otel_parse_cfg_sample() to accept log-format strings in
addition to bare sample expressions.  Added lf_expr and lf_used fields
to flt_otel_conf_sample.

Extended flt_otel_sample_add() to evaluate log-format expressions when
lf_used was set.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
40ac0fd932 MEDIUM: otel: added group action for rule-based scope execution
Added the "otel-group" action keyword that allows executing a named group
of OTel scopes from HAProxy TCP and HTTP action rule contexts.

The new group.c module registers the "otel-group" keyword for all four
action contexts (tcp-request, tcp-response, http-request, http-response)
and implements the action lifecycle callbacks.

The parser flt_otel_group_parse() accepts a filter ID and group ID as
arguments, duplicates them into the action rule's argument slots, and
wires up the check, action, and release callbacks.

The post-parse validator flt_otel_group_check() resolves the filter ID and
group ID string references into direct configuration pointers by searching
the proxy's filter list for a matching OTel filter and then looking up the
named group within that filter's configuration.

The action handler flt_otel_group_action() retrieves the filter and group
configuration from the resolved rule arguments, verifies the filter is
attached to the stream and not disabled, then iterates through all scopes
in the group and executes each via flt_otel_scope_run() with a shared
timestamp pair.  This allows operators to trigger OTel instrumentation
conditionally from HAProxy rules, for example applying different tracing
scopes based on ACL conditions or request properties.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
cb4cb1065c MEDIUM: otel: added CLI commands for runtime filter management
Added HAProxy CLI commands that allow runtime inspection and modification
of OTel filter settings without requiring a configuration reload.

The new cli.c module registers CLI keywords under the "otel" prefix and
implements the following commands: flt_otel_cli_parse_status() displays a
comprehensive status report of all OTel filter instances including filter
ID, proxy, disabled state, hard-error mode, logging state, rate limit,
analyzer bits, and SDK diagnostic message count;
flt_otel_cli_parse_disabled() enables or disables filtering across all
instances; flt_otel_cli_parse_option() toggles the hard-error mode that
controls whether errors disable the filter for a stream or are silently
ignored; flt_otel_cli_parse_logging() manages the logging state with
support for off, on, and dontlog-normal modes; flt_otel_cli_parse_rate()
adjusts the sampling rate limit as a floating-point percentage; and
flt_otel_cli_parse_debug() sets the debug verbosity level in debug builds.
All modifications are applied atomically across every OTel filter instance
in every proxy.

The CLI initialization is called from flt_otel_ops_init() during filter
startup via flt_otel_cli_init(), which registers the keyword table through
cli_register_kw().

Supporting changes include the FLT_OTEL_U32_FLOAT macro for converting the
internal uint32_t rate representation to a human-readable percentage, the
FLT_OTEL_PROXIES_LIST_START/END iteration macros for traversing all OTel
filter instances across the proxy list, and flt_otel_filters_dump() for
debug logging of filter instances.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
b1498910ec MINOR: otel: added prefix-based variable scanning
Introduced an alternative variable scanning strategy that directly walked
the CEB tree of HAProxy's variable store instead of maintaining a separate
tracking buffer.

The Makefile auto-detected whether struct var carried a "name" member
by inspecting include/haproxy/vars-t.h and conditionally defined
USE_OTEL_VARS_NAME.  When enabled, the tracking buffer (flt_otel_ctx) and
its callback type were compiled out and replaced by direct tree walks.

flt_otel_vars_unset() walked the CEB tree for the resolved scope, removed
every variable whose normalized name matched the given prefix followed by
a dot, and adjusted the variable accounting.  flt_otel_vars_get()
performed the same prefix scan under a read lock, denormalized each
matching variable name back to its original OTel form, and assembled the
results into an otelc_text_map.

A helper flt_otel_vars_get_scope() was added to resolve scope name strings
("txn", "sess", "proc", "req", "res") to the corresponding HAProxy
variable store.  The set path skipped the tracking buffer update when
prefix scanning was available.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
2d6a15ac00 MEDIUM: otel: added HAProxy variable storage for context propagation
Added support for storing OTel span context in HAProxy transaction
variables as an alternative to HTTP headers, enabled by the OTEL_USE_VARS
compile flag.

The new vars.c module implements variable-based context propagation
through the HAProxy variable subsystem.  Variable names are constructed
from a configurable prefix and the OTel propagation key, with dots
normalized to underscores for HAProxy variable name compatibility
and denormalized back during retrieval.  The module provides
flt_otel_var_register() to pre-register variables at parse time,
flt_otel_var_set() and flt_otel_vars_unset() to store and clear context
key-value pairs in the txn scope, flt_otel_vars_get() to collect all
variables matching a prefix into an otelc_text_map for context extraction,
and flt_otel_vars_dump() for debug logging of all OTel variables.

The inject/extract keywords in the scope parser now accept an optional
"use-vars" argument alongside "use-headers", controlled by the new
FLT_OTEL_CTX_USE_VARS flag.  Both storage types can be used simultaneously
on the same span context, allowing context to be propagated through both
HTTP headers and variables.

The scope runner in event.c was extended to handle variable-based
context in parallel with headers: during extraction, it reads matching
variables via flt_otel_vars_get() when FLT_OTEL_CTX_USE_VARS is set;
during injection, it stores each propagation key as a variable via
flt_otel_var_set().  The unused resource cleanup now also unsets context
variables when removing failed extraction contexts.

The filter attach callback registers and sets the sess.otel.uuid variable
with the generated session UUID, making the trace identifier available to
HAProxy log formats and ACL expressions.

The feature is conditionally compiled: the OTEL_USE_VARS flag controls
whether vars.c is included in the build and whether the "use-vars" keyword
is available in the configuration parser.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
8effcac6f5 MEDIUM: otel: added HTTP header operations for context propagation
Added the HTTP header manipulation layer that enables span context
injection into and extraction from HAProxy's HTX message buffers,
completing the end-to-end context propagation path.

The new http.c module implements three public functions:
flt_otel_http_headers_get() extracts HTTP headers matching a name prefix
from the channel's HTX buffer into an otelc_text_map structure, stripping
the prefix and separator dash from header names before storage;
flt_otel_http_header_set() constructs a full header name from a prefix and
suffix joined by a dash, removes all existing occurrences, and optionally
adds the header with a new value; and flt_otel_http_headers_remove()
removes all headers matching a given prefix.  A debug-only
flt_otel_http_headers_dump() logs all HTTP headers from a channel at
NOTICE level.

The scope runner in event.c now extracts propagation contexts from HTTP
headers before processing spans: for each configured extract context, it
calls flt_otel_http_headers_get() to read matching headers into a text
map, then passes the text map to flt_otel_scope_context_init() which
extracts the OTel span context from the carrier.  After span execution,
the span runner injects the span context back into HTTP headers via
flt_otel_inject_http_headers() followed by flt_otel_http_header_set()
for each propagation key.

The unused resource cleanup in flt_otel_scope_free_unused() now also
removes contexts that failed extraction by deleting their associated
HTTP headers via flt_otel_http_headers_remove() before freeing the scope
context structure.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
ea9d05de02 MEDIUM: otel: added context propagation via carrier interfaces
Added the span context injection and extraction layer that bridges the
OTel C wrapper's propagation API with HAProxy's HTTP headers and text map
carriers.

The new otelc.c module implements four public functions that wrap the
OTel C wrapper's context propagation methods: flt_otel_inject_text_map()
and flt_otel_inject_http_headers() serialize a span's context into
a text map or HTTP headers carrier for outbound propagation, while
flt_otel_extract_text_map() and flt_otel_extract_http_headers()
deserialize an inbound carrier into an otelc_span_context for parent
linking.

Each direction uses a pair of callbacks registered on the carrier
structure.  The injection writers (flt_otel_text_map_writer_set_cb and
flt_otel_http_headers_writer_set_cb) store key-value pairs emitted by the
SDK into the carrier's text map via OTELC_TEXT_MAP_ADD().  The extraction
readers (flt_otel_text_map_reader_foreach_key_cb and
flt_otel_http_headers_reader_foreach_key_cb) iterate the carrier's text
map entries and pass each pair to the SDK's handler callback.

The scope context initialization in flt_otel_scope_context_init() now
calls flt_otel_extract_http_headers() to extract the span context from the
provided text map carrier and stores it in the scope context structure,
making extracted contexts available for parent linking in subsequent span
creation.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
bab0ea7b77 MEDIUM: otel: implemented scope execution and span management
Implemented the scope execution engine that creates OTel spans, evaluates
sample expressions to collect telemetry data, and manages span lifecycle
during request and response processing.

The scope runner flt_otel_scope_run() was expanded from a stub into a
complete implementation that evaluates ACL conditions on the scope,
extracts span contexts from HTTP headers when configured, iterates over
the scope's span definitions calling flt_otel_scope_run_span() for each,
marks and finishes completed spans, and cleans up unused runtime
resources.

The span runner flt_otel_scope_run_span() creates OTel spans via the
tracer with optional parent references (from other spans or extracted
contexts), collects telemetry by calling flt_otel_sample_add() for each
configured attribute, event, baggage and status entry, then applies the
collected data to the span (attributes, events with their own key-value
arrays, baggage items, and status code with description) and injects the
span context into HTTP headers when configured.

The sample evaluation layer converts HAProxy sample expressions into OTel
telemetry data.  flt_otel_sample_add() evaluates each sample expression
against the stream, converts the result via flt_otel_sample_to_value()
which preserves native types (booleans as OTELC_VALUE_BOOL, integers as
OTELC_VALUE_INT64, all others as strings), and routes the key-value pair
to the appropriate collector based on the sample type (attribute, event,
baggage, or status).  The key-value arrays grow dynamically using the
FLT_OTEL_ATTR_INIT_SIZE and FLT_OTEL_ATTR_INC_SIZE constants.

Span finishing is handled in two phases: flt_otel_scope_finish_mark()
marks spans and contexts for completion using exact name matching or
wildcards ("*" for all, "*req*" for request-direction, "*res*" for
response-direction), and flt_otel_scope_finish_marked() ends all marked
spans with a common monotonic timestamp and destroys their contexts.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
3184470339 MEDIUM: otel: wired OTel C wrapper library integration
Connected the OpenTelemetry C wrapper library to the filter lifecycle by
implementing the library initialization, tracer creation, memory and
thread callbacks, shutdown sequence, and span completion.

The flt_otel_lib_init() function now verifies the C wrapper library
version against the compiled headers, calls otelc_init() with the absolute
configuration file path, and creates the tracer via otelc_tracer_create().
On success, it registers HAProxy pool-based memory callbacks
(flt_otel_mem_malloc, flt_otel_mem_free) and a thread ID callback
(flt_otel_thread_id) through otelc_ext_init(), so the C++ SDK allocates
span and context objects from pool_head_otel_span_context.  A custom log
handler (flt_otel_log_handler_cb) is registered via otelc_log_set_handler()
to count OTel SDK internal diagnostic messages in the flt_otel_drop_cnt
counter.

The per-thread init callback now starts the tracer thread via
OTELC_OPS(tracer, start) instead of unconditionally returning success.

The deinit callback saves the tracer handle before freeing the
configuration, then shuts down the library via otelc_deinit() after the
pool is destroyed, ensuring the ext callbacks remain valid while the
configuration structures are still being freed.  In debug builds, it logs
wrapper statistics, attach counters, and per-event HTX usage counters
before shutdown.

The runtime context cleanup in flt_otel_runtime_context_free() now ends
all active spans with a common monotonic timestamp via
OTELC_OPSR(span, end_with_options) before freeing them.  The scope context
cleanup in flt_otel_scope_context_free() now destroys the underlying OTel
span context via OTELC_OPSR(context, destroy).

The parser gained static storage for the debug memory tracker
(OTELC_DBG_MEM) and its initialization in the parse entry point, used when
compiled with the OTELC_DBG_MEM flag.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
2e962a5443 MEDIUM: otel: implemented filter callbacks and event dispatcher
Replaced the stub filter callbacks with full implementations that dispatch
OTel events through the scope execution engine, and added the supporting
debug, error handling and utility infrastructure.

The filter lifecycle callbacks (init, deinit, init_per_thread) now
initialize the OpenTelemetry C wrapper library, create the tracer from the
instrumentation configuration file, enable HTX stream filtering, and clean
up the configuration and memory pools on shutdown.

The stream callbacks (attach, stream_start, stream_set_backend,
stream_stop, detach, check_timeouts) create the per-stream runtime context
on attach with rate-limit based sampling, fire the corresponding OTel
events (on-stream-start, on-backend-set, on-stream-stop), manage the
idle timeout timer with reschedule logic in detach, and free the runtime
context in check_timeouts.  The attach callback also registers the
required pre and post channel analyzers from the instrumentation
configuration.

The channel callbacks (start_analyze, pre_analyze, post_analyze,
end_analyze) register per-channel analyzers, map analyzer bits to event
indices via flt_otel_get_event(), and dispatch the matching events.
The end_analyze callback also fires the on-server-unavailable event
when response analyzers were configured but never executed.

The HTTP callbacks (http_headers, http_end, http_reply, and the debug-only
http_payload and http_reset) dispatch their respective request/response
events based on the channel direction.

The event dispatcher flt_otel_event_run() in event.c iterates over all
scopes matching a given event index and calls flt_otel_scope_run() for
each, sharing a common monotonic and wall-clock timestamp across all spans
within a single event.

Error handling is centralized in flt_otel_return_int() and
flt_otel_return_void(), which implement the hard-error/soft-error policy:
hard errors disable the filter for the stream, soft errors are silently
cleared.

The new debug.h header provides conditional debug macros
(FLT_OTEL_DBG_ARGS, FLT_OTEL_DBG_BUF) and the FLT_OTEL_LOG macro for
structured logging through the instrumentation's log server list.  The
utility layer gained debug-only label functions for channel direction,
proxy mode, stream position, filter type, and analyzer bit name lookups.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
f05a6735b1 MEDIUM: otel: added memory pool and runtime scope layer
Added the memory pool management and the runtime scope layer that track
per-stream OTel spans and contexts during request processing.

The pool layer in pool.c manages HAProxy memory pools for the runtime
structures used by the filter: scope spans, scope contexts, runtime
contexts, and span contexts.  Each pool is conditionally compiled via
USE_POOL_OTEL_* macros defined in config.h and registered with
REGISTER_POOL().  The allocation functions (flt_otel_pool_alloc,
flt_otel_pool_strndup, flt_otel_pool_free) transparently fall back to
heap allocation when the corresponding pool is not enabled.  Trash buffer
helpers (flt_otel_trash_alloc, flt_otel_trash_free) provide scratch space
using either HAProxy's trash chunk pool or direct heap allocation.

The scope layer in scope.c implements the per-stream runtime state.  The
flt_otel_runtime_context structure is allocated when a stream starts and
holds the stream and filter references, hard-error/disabled/logging flags
copied from the instrumentation configuration, idle timeout state, a
generated UUID, and lists of active scope spans and extracted scope
contexts.  Scope spans (flt_otel_scope_span) carry the operation name,
fetch direction, the OTel span handle, and optional parent references
resolved from other spans or extracted contexts.  Scope contexts
(flt_otel_scope_context) hold an extracted span context obtained from
a carrier text map via the tracer.  The scope data structures
(flt_otel_scope_data) aggregate growable key-value arrays for attributes
and baggage, a linked list of named events with their own attribute
arrays, and a span status code with description, representing the
telemetry collected during a single event execution.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
c0fd39457f MEDIUM: otel: added post-parse configuration check
Implemented the flt_otel_ops_check() callback that validates the parsed
OTel filter configuration after all HAProxy configuration sections have
been processed.

The check callback performs the following validations: resolves deferred
sample fetch arguments under full frontend and backend capabilities,
verifies uniqueness of filter IDs across all proxies, ensures the
instrumentation section and its configuration file are present, checks
for duplicate group and scope section names, verifies that groups are not
empty, resolves group-to-scope and instrumentation-to-group/scope
cross-references by linking placeholder entries to their definitions,
detects unused scopes, counts root spans and warns when the count differs
from one, and accumulates the required channel analyzer bits from all used
scopes into the instrumentation configuration.

The commit also added the flt_otel_counters structure to track per-event
diagnostic counters in debug builds, the FLT_OTEL_ALERT macro for
filter-scoped error messages, and the FLT_OTEL_DBG_LIST macro for
iterating and dumping named configuration lists.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
2d56399b0c MEDIUM: otel: added configuration parser and event model
Added the full configuration parser that reads the OTel filter's external
configuration file and the event model that maps filter events to HAProxy
channel analyzers.

The event model in event.h defines an X-macro table
(FLT_OTEL_EVENT_DEFINES) that maps each filter event to its HAProxy
channel analyzer bit, sample fetch direction, and event name.  Events
cover stream lifecycle (start, stop, backend-set, idle-timeout), client
and server sessions, request analyzers (frontend and backend TCP and
HTTP inspection, switching rules, sticking rules, RDP cookie), response
analyzers (TCP inspection, HTTP response processing), and HTTP headers,
end, and reply callbacks.  The event names are partially compatible with
the SPOE filter.  The flt_otel_event_data[] table in event.c is generated
from the same X-macro and provides per-event metadata at runtime.

The parser in parser.c implements section parsers for the three OTel
configuration blocks: otel-instrumentation (tracer identity, log server,
config file path, groups, scopes, ACLs, rate-limit, options for
disabled/hard-errors/nolognorm, and debug-level), otel-group (group
identity and scope list), and otel-scope (scope identity, span definitions
with optional root/parent modifiers, attributes, events, baggages, status
codes, inject/extract context operations, finish lists, idle-timeout,
ACLs, and otel-event binding with optional if/unless ACL conditions).
Each section has a post-parse callback that validates the parsed state.

The top-level flt_otel_parse_cfg() temporarily registers these section
parsers, loads the external configuration file via parse_cfg(), and
handles deferred resolution of sample fetch arguments by saving them in
conf->smp_args for later resolution in flt_otel_check() when full frontend
and backend capabilities are available.  The main flt_otel_parse() entry
point was extended to parse the filter ID and config file keywords, verify
that insecure-fork-wanted is enabled, and wire the parsed configuration
into the flt_conf structure.

The utility layer gained flt_otel_strtod() and flt_otel_strtoll() for
validated string-to-number conversion used by rate-limit and debug-level
parsing.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
8126fd569b MEDIUM: otel: added configuration and utility layer
Added the configuration structures that model the OTel filter's
instrumentation hierarchy and the utility functions that support the
configuration parser.

The configuration is organized as a tree rooted at flt_otel_conf, which
holds the proxy reference, filter identity, and lists of groups and
scopes.  Below it, flt_otel_conf_instr carries the instrumentation
settings: tracer handle, rate limiting, hard-error mode, logging state,
channel analyzers, and placeholder references to groups and scopes.
Groups (flt_otel_conf_group) aggregate scopes by name.  Scopes
(flt_otel_conf_scope) bind an event to its ACL condition, span context
declarations, span definitions and a list of spans scheduled for
finishing.  Spans (flt_otel_conf_span) carry attributes, events,
baggages and status entries, each represented as flt_otel_conf_sample
structures that pair a key with concatenated sample-expression arguments.

All configuration types share a common header macro (FLT_OTEL_CONF_HDR)
that embeds an identifier string, its length, a configuration line number,
and a list link.  Their init and free functions are generated by the
FLT_OTEL_CONF_FUNC_INIT and FLT_OTEL_CONF_FUNC_FREE macros in
conf_funcs.h, with per-type custom initialization and cleanup bodies.

The utility layer in util.c provides argument counting and concatenation
for the configuration parser, sample data to string conversion covering
boolean, integer, IPv4, IPv6, string and HTTP method types, and debug
helpers for dumping argument arrays and linked list state.
2026-04-13 09:23:26 +02:00
Miroslav Zagorac
cd14abf9f3 MEDIUM: otel: added OpenTelemetry filter skeleton
The OpenTelemetry (OTel) filter enables distributed tracing of requests
across service boundaries, export of metrics such as request rates,
latencies and error counts, and structured logging tied to trace context,
giving operators a unified view of HAProxy traffic through any
OpenTelemetry-compatible backend.

The OTel filter is implemented using the standard HAProxy stream filter
API.  Stream filters attach to proxies and intercept traffic at each stage
of processing: they receive callbacks on stream creation and destruction,
channel analyzer events, HTTP header and payload processing, and TCP data
forwarding.  This allows the filter to collect telemetry data at every
stage of the request/response lifecycle without modifying the core proxy
logic.

This commit added the minimum set of files required for the filter to
compile: the addon Makefile with pkg-config-based detection of the
opentelemetry-c-wrapper library, header files with configuration
constants, utility macros and type definitions, and the source files
containing stub filter operation callbacks registered through
flt_otel_ops and the "opentelemetry" keyword parser entry point.

The filter uses the opentelemetry-c-wrapper library from HAProxy
Technologies, which provides a C interface to the OpenTelemetry C++ SDK.
This wrapper allows HAProxy, a C codebase, to leverage the full
OpenTelemetry observability pipeline without direct C++ dependencies
in the HAProxy source tree.

  https://github.com/haproxytech/opentelemetry-c-wrapper
  https://github.com/open-telemetry/opentelemetry-cpp

Build options:

  USE_OTEL     - enable the OpenTelemetry filter
  OTEL_DEBUG   - compile the filter in debug mode
  OTEL_INC     - force the include path to the C wrapper
  OTEL_LIB     - force the library path to the C wrapper
  OTEL_RUNPATH - add the C wrapper RUNPATH to the executable

Example build with OTel and debug enabled:

  make -j8 USE_OTEL=1 OTEL_DEBUG=1 TARGET=linux-glibc
2026-04-13 09:23:26 +02:00
Amaury Denoyelle
b8145fa5d4 BUG/MINOR: xprt_qstrm: do not parse record length on read again
conn_recv_qstrm() may be called several times per connection if the read
data is too short and a truncated record is received.

Previously, record length was parsed every time the function is invoked.
However, this must only be performed if record length varint is
incomplete. Once read and parsed, data are removed from the buffer via
b_quic_dec_int(). Thus, next conn_recv_qstrm() run will reread an
invalid record length this time.

This patch fixes this by only parsing record length if <rxrlen> member
is null. Prior to it, parsing of QMux transport parameters would fail in
case of a first truncated read, which would prevent the connection
initialization.

No need to backport.
2026-04-13 09:11:08 +02:00
Amaury Denoyelle
b5624a6365 BUG/MINOR: mux_quic: prevent QMux crash on qcc_io_send() error path
A QCC connection may be flagged with QC_CF_ERRL to trigger a
CONNECTION_CLOSE emission. However, for now error reporting is not
functional with QMux, as it relies on quic_conn layer access.

To prevent a crash in qcc_io_send() when using QMux, add a
conn_is_quic() check when QC_CF_ERRL is set to ensure no access will be
performed on quic_conn layer. In the future, this should be extended so
that QMux is also able to emit CONNECTION_CLOSE for connection closure.

No need to backport.
2026-04-13 09:11:08 +02:00
Christopher Faulet
fb82dece47 BUG/MEDIUM: haterm: Properly initialize the splicing support for haterm
Some checks failed
Contrib / build (push) Has been cancelled
alpine/musl / gcc (push) Has been cancelled
VTest / Generate Build Matrix (push) Has been cancelled
Windows / Windows, gcc, all features (push) Has been cancelled
VTest / (push) Has been cancelled
First, we must not emit any warning if splicing is not configured and the
global maxpipes value is 0. Then we must not remove GTUNE_USE_SPLICE flag
when we fail to allocate the haterm master pipe. Instead, we test it when we
negociate with the opposite side, to properly exclude the splicing if it is
not usable.

No backport needed.
2026-04-10 16:32:29 +02:00
Christopher Faulet
313121639e Revert "BUG/MEDIUM: haterm: Move all init functions of haterm in haterm_init.c"
This reverts commit 8056117e98.

Moving haterm init from haproxy is not the right way to fix the issue
because it should be possible to use a haterm configuration in haproxy.

So let's revert the commit above.
2026-04-10 16:32:29 +02:00
Amaury Denoyelle
63febbace7 BUG/MINOR: do not crash on QMux reception of BLOCKED frames
Some checks failed
Contrib / build (push) Has been cancelled
alpine/musl / gcc (push) Has been cancelled
VTest / Generate Build Matrix (push) Has been cancelled
Windows / Windows, gcc, all features (push) Has been cancelled
VTest / (push) Has been cancelled
Add QUIC BLOCKED frames in the list of supported types in
qstrm_parse_frm(). Nothing is really implemented for them as for QUIC,
but this prevents a crash when receiving one of them via QMux.

No need to backport.
2026-04-10 10:30:49 +02:00
Amaury Denoyelle
ec552b0cc2 DOC: update draft link for QMux protocol
QMux draft 01 support is mostly achieved thanks to the recent
implementation of the Record layer. This patch thus updates the link in
the documentation to the validated draft version.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
a993f0c503 MEDIUM: mux-quic/xprt_qstrm: implement QMux record emission
This patch implements emission of the new Record layer for QMux frames.
This handles mux-quic and xprt_qstrm layers as this is performed
similarly in both cases.

Currently, the simplest approach has been prefered : each frame is
encoded in its own record. This is not the most efficient in size but it
is extremely simple to implement for a first interop testing.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
792e055c7c MEDIUM: xprt_qstrm: implement QMux record parsing
This patch implements the new QMux record layer parsing for xprt_qstrm.
This is mostly similar to the MUX code from the previous patch.

Along with this change, a new xprt_qstrm layer accessor exposes the
possible remaining record length after Transport parameters parsing.
This can only occur when xprt_qstrm Rx buffer is not completely emptied
due to other following frames. If stored in the same record, MUX layer
has to know the remaining record length.

Thus, xprt_qstrm_rxrlen() is now used in qmux_init() to preinitialize
<rx.rlen> QCC field.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
5271cdaca3 MEDIUM: mux-quic: implement QMux record parsing
This is the first patch of a serie which aims to support the new Record
layer defined by the draft 01 of QMux protocol.

  https://www.ietf.org/archive/id/draft-ietf-quic-qmux-01.html#name-qmux-records

This patch deals with QMux reception at the MUX layer. The function
qcc_qstrm_recv() is adapted to read record headers before frame parsing.
This requires to keep the last record length read in a new QCC field
named <rx.rlen>.

Frames are only parsed once a full record is received. One of the
advantage of the record layer is that it can only contains whole frame
without truncation.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
10f2867dc2 MINOR: xprt_qstrm: handle connection errors
This patch implements proper connection error handling for xprt_qstrm
layer. Basically, processing is interrupted if CO_FL_ERROR is
encountered after either rcv_buf or snd_buf operations. Connectionn
error is set to the newly defined value CO_ER_QSTRM.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
47199ce895 MINOR: xprt_qstrm: implement Tx buffering
This commit adds buffering on transmission for xprt_qstrm layer. This is
necessary in the rare case where send syscall only emits partial data.

A new <txbuf> member is defined in xprt_qstrm context. On first send
invokation, buffer is allocated and then the QMux transport parameters
frame is encoded. Then emission is performed via snd_buf and each time
the send function is invoked.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
fb3b268747 MINOR: xprt_qstrm/mux-quic: handle extra QMux frames after params
Layer xprt_qstrm is responsible to read the initial QMux transport
parameters frame. However, it could receive more data if some other
frames follow it. This extra content can only be handled by the MUX
layer once initialized.

Theorically, it could have been implemented via MSG_PEEK. However, this
flag is currently ignored by SSL layer. Besides, it is tedious to
implement safely. A new approach has been prefered where the MUX layer
is responsible to retrieve remaining data via xprt_qstrm_rxbuf()
accessor function during its initialization.

Thus, qmux_init() now may retrieve the buffer from xprt_qstrm layer.
This is performed via b_xfer() which will result in a zero copy
transfer. If this happens, tasklet is immediately scheduled to start
demuxing.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
890831f292 MINOR: xprt_qstrm: implement Rx buffering
Implement buffering for reception on xprt_qstrm layer. This is necessary
to handle reception of a truncated QMux transport parameters frame.

This is performed via a new dedicated <rxbuf> member in xprt_qstrm
context. Read is performed by reusing the buffer until a whole frame can
be read.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
c63e6ecd4b BUG/MINOR: quic: increment pos pointer on QMux transport params parsing
QUIC frame parsers functions take a <pos> pointer as input argument for
the data to be parsed. If parsing is successful, <pos> must be
incremented to point to the next data.

Increment was not performed when parsing QMux transport parameters
frame. This commit fixes this. Note that for now there is no real issue
as xprt_qstrm does not check the QMux frame length.

No need to backport.
2026-04-10 10:20:52 +02:00
Amaury Denoyelle
90d0e8a948 BUG/MINOR: mux-quic: fix potential NULL deref on qcc_release()
In qcc_release(), <conn> may be NULL. Thus every access on it must be
tested.

With recent QMux introduction, a call to conn_is_quic() has been added
prior to registration of the stream rejection callback. It could lead to
NULL deref as <conn> is not tested there. Fix this by adding an extra
check on the pointer validity.

No need to backport.
2026-04-10 10:20:52 +02:00
Greg Kroah-Hartman
4ad200f276 BUG/MINOR: hlua: fix use-after-free of HTTP reason string
hlua_applet_http_status() stored the result of luaL_optlstring()
directly in http_ctx->reason. The pointer references Lua-managed
string storage which is only guaranteed valid until the C function
returns to Lua. If the GC runs between applet:set_status(200, str)
and applet:start_response(), the pointer dangles.

hlua_applet_http_send_response() then calls ist(http_ctx->reason)
which does strlen() on freed memory, followed by memcpy into the
HTX status line. The freed-and-reallocated chunk contents are sent
verbatim to the HTTP client.

Trigger:

    applet:set_status(200, table.concat({"Reason ", str:rep(50)}))
    collectgarbage("collect"); collectgarbage("collect")
    applet:start_response()

With heap grooming, adjacent allocation contents (session data, TLS
material from the same thread) leak into the response status line.

Anchor the Lua string in the registry keyed by the http_ctx field
address so it survives until the applet is done with it. The
registry entry is overwritten on each call (handles repeated
set_status) and naturally cleaned up when the lua_State is closed.

This patch should be backported to all stable versions.
2026-04-10 10:18:27 +02:00
Greg Kroah-Hartman
0aeae23056 BUG/MEDIUM: mux-fcgi: prevent record-length truncation with large bufsize
FCGI content_length is a 16-bit field but fcgi_set_record_size()
is called with size_t/uint32_t arguments. With tune.bufsize >= 65544
(legal; cfgparse-global.c only enforces <= INT_MAX-16), a single
HTX DATA block or accumulated outbuf can exceed 65535 bytes. The
implicit conversion to uint16_t silently truncates the length field
while b_add(mbuf, outbuf.data) writes the full body.

A client posting ~99000 bytes can craft the body so that bytes
after the truncated length are parsed by PHP-FPM as fresh FCGI
records on the connection: a smuggled BEGIN_REQUEST + PARAMS with
arbitrary SCRIPT_FILENAME / PHP_VALUE bypasses all haproxy ACLs.

Fix the zero-copy path by refusing it when the block exceeds 65535
bytes (falls through to copy). Fix the copy path by capping
outbuf.size to 65535 + header so the data-fill loop naturally
stops at the FCGI maximum and emits the rest in a subsequent record.

The PARAMS path at line 2084 is similarly affected but harder to
trigger (requires combined header+param size > 65535) and is
covered by the same outbuf.size cap pattern if applied there.

This patch must be backported to all stable versions.
2026-04-10 09:40:16 +02:00
Greg Kroah-Hartman
e6c3660327 BUG/MINOR: sample: fix info leak in regsub when exp_replace fails
exp_replace() returns int and returns -1 when the back-reference
expansion overflows the output buffer (regex.c:51). output->data is
size_t, so -1 becomes SIZE_MAX. There was no error check.

The subsequent comparisons interpret SIZE_MAX as a huge length:
"output->data > b_room(trash)" tries to grow trash, then
"max > output->data" is false so max stays at trash->size, and
memcpy(trash, output->area, trash->size) copies the full chunk.
output->area is a pool_alloc()'d chunk that is NOT zeroed; the
bytes after the partial exp_replace output are stale data from a
prior pool user (request headers, response bodies from the same
worker thread).

Trigger with a backreference whose expansion exceeds bufsize:

    http-request set-header X %[req.hdr(In),regsub('(.+)','\1\1')]

and a request with In: of ~9000 bytes. The X header sent to the
backend then contains ~9KB of stale heap data.

With tune.bufsize.large set, get_larger_trash_chunk() upgrades trash
and the memcpy reads up to ~50KB past the (smaller) output->area
allocation.

http_ana.c:2728 and http_act.c:551 already check exp_replace() for
-1; this call site was missed when backreferences were added.

This patch must be backported to all stable versions.
2026-04-10 09:33:37 +02:00
Christopher Faulet
b0a9216ca5 BUG/MEDIUM: samples: Fix handling of SMP_T_METH samples
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
Samples of type SMP_T_METH were not properly handled in smp_dup(),
smp_is_safe() and smp_is_rw(). For "other" methods, for instance PATCH, a
fallback was performed on the SMP_T_STR type. Only the buffer considered
changed. "smp->data.u.meth.str" should be used for the SMP_T_METH samples
while smp->data.u.str should be used for SMP_T_STR samples. However, in
smp_dup(), the result was stored in wrong buffer, the string one instead of
the method one. In smp_is_safe() and smp_is_rw(), the method buffer was not
used at all.

We now take care to use the right buffer.

This patch must be backported to all stable versions.
2026-04-09 22:05:12 +02:00
Christopher Faulet
265be7e8cb BUG/MINOR: haterm: Return the good start-line for 100-continue interim message
When "Expect" header was found in request headers, "HTTP/1.1 100-continue"
was returned instead of "HTTP/1.1 100 continue". Let's fix it.

No backport needed.
2026-04-09 22:04:42 +02:00
Greg Kroah-Hartman
0cde3cd4df BUG/MINOR: http-act: validate decoded lengths in *-headers-bin
http_action_set_headers_bin() decodes varint name and value lengths
from a binary sample but never validates that the decoded length
fits in the remaining sample data before constructing the ist.

If the value's varint decodes to a large number with only a few
bytes following, v.len exceeds the buffer and http_add_header()
memcpys past the sample, copying adjacent heap data into a header
sent to the backend (or client, with http-response).

The intended source for this action is the hdrs_bin sample fetch
which produces well-formed output, but nothing prevents an admin
from feeding it req.body or another untrusted source. With:

    http-request set-var(txn.h) req.body
    http-request add-headers-bin var(txn.h)

a POST body of [05]"X-Foo"[c8]"AB" produces v = {ptr="AB", len=200}
and 198 bytes of adjacent heap data go into X-Foo.

http_action_del_headers_bin() was fixed too.

Compare spoe_decode_buffer() which has the equivalent check.
Validate both name and value lengths against remaining data.

No backport needed.
2026-04-09 17:10:56 +02:00
Greg Kroah-Hartman
bd03f05007 BUG/MINOR: spoe: fix pointer arithmetic overflow in spoe_decode_buffer()
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
decode_varint() has no iteration cap and accepts varints decoding to
any uint64_t value. When sz is large enough that p + sz wraps modulo
2^64, the check "p + sz > end" passes, *buf is set to the wrapped
pointer, and the caller's parsing loop continues from an arbitrary
relative offset before the demux buffer.

A malicious SPOE agent sending an AGENT_HELLO frame with a key-name
length varint of 0xfffffffffffff000 causes spop_conn_handle_hello()
to dereference memory ~64KB before the dbuf allocation, resulting in
SIGSEGV (DoS) or, if the read lands on live heap data, parser
confusion. The relative offset is fully attacker-controlled and
ASLR-independent.

Compare against the remaining length instead of computing p + sz.
Since p <= end is guaranteed after a successful decode_varint(),
end - p is non-negative.

This patch must be backport to all stable versions.
2026-04-09 16:47:19 +02:00
Greg Kroah-Hartman
b63cae7f9b BUG/MINOR: resolvers: fix memory leak on AAAA additional records
Commit c84c15d393 ("BUG/MINOR: resolvers: Apply dns-accept-family
setting on additional records") converted a switch statement to an
if/else chain but left the break; in the AAAA branch. In the new
form, break exits the surrounding for loop instead of a switch case.

For every AAAA additional record in an SRV response:
  - answer_record allocated at line 1460 is never freed and never
    inserted into answer_tree -> ~580 bytes leaked per response
  - all subsequent additional records in the response are silently
    discarded

A DNS server controlling SRV responses for haproxy service discovery
can leak memory at MB/min rates given default resolution intervals.
Also breaks IPv6 SRV target resolution outright since the AAAA record
is leaked rather than attached to its SRV entry.
2026-04-09 16:31:05 +02:00
William Lallemand
0e18e1cc77 REGTESTS: lua: add tune.lua.openlibs to all Lua reg-tests
Ensure that all Lua regression tests exercise the restricted library
mode by setting "tune.lua.openlibs none" in their global section.

Only txn_get_priv-thread.vtc requires "string,table"
2026-04-09 14:32:12 +02:00
William Lallemand
591a85e29e MINOR: lua: add tune.lua.openlibs to restrict loaded Lua standard libraries
HAProxy has always called luaL_openlibs() unconditionally, which opens
all standard Lua libraries including io, os, package and debug. This
makes it impossible to prevent Lua scripts from executing binaries
(os.execute, io.popen), loading native C modules (package/require), or
bypassing any Lua-level sandbox via the debug library.

Add a new global directive tune.lua.openlibs that accepts a comma-separated
list of library names to load:

  tune.lua.openlibs none                   # only base + coroutine
  tune.lua.openlibs string,math,table,utf8 # safe libs only
  tune.lua.openlibs all                    # default, same as before

The base and coroutine libraries are always loaded regardless: base provides
core Lua functions that HAProxy relies on, and coroutine is required because
HAProxy overrides coroutine.create() with its own safe implementation.

When all libraries are enabled (the default), the fast path still calls
luaL_openlibs() directly with no overhead. A parse error is returned if
the directive appears after lua-load or lua-load-per-thread (the Lua state
is already initialised at that point), or if 'none' is combined with other
library names. Note that fork() and new thread creation are already blocked
by default regardless of this setting (see "insecure-fork-wanted").
2026-04-09 14:31:10 +02:00
Willy Tarreau
3020fde525 BUG/MAJOR: slz: always make sure to limit fixed output to less than worst case literals
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
Literals are sent in two ways:
  - in EOB state, unencoded and prefixed with their length
  - in FIXED state, huffman-encoded

And references are only sent in FIXED state.

The API promises that the amount of data will not grow by more than
5 bytes every 65535 input bytes (the comment was adjusted to remind
this last point). This is guaranteed by the literal encoding in EOB
state (BT, LEN, NLEN + bytes), which is supposed to be the worst
case by design.

However, as reported by Greg KH, this is currently not true: the test
that decides whether or not to switch to FIXED state to send references
doesn't properly account for the number of bytes needed to roll back
to the *exact* same state in EOB, which means sending EOB, BT,
alignment, LEN and NLEN in addition to the referenced bytes, versus
sending the encoding for the reference. By not taking into account the
cost of returning to the initial state (BT+LEN+NLEN), it was possible
to stay too long in the FIXED state and to consume the extra bytes that
are needed to return to the EOB state, resulting in producing much more
data in case of multiple switchovers (up to 6.25% increase was measured
in tests, or 1/16, which matches worst case estimates based on the code).

And this check is only valid when starting from EOB (in order to restore
the same state that offers this guarantee). When already in FIXED state,
the encoded reference is always smaller than or same size as the data.
The smallest match length we support is 4 bytes, and when encoded this
is no more than 28 bits, so it is safe to stay in FIXED state as long
as needed while checking the possibility of switching back to EOB.

This very slightly reduces the compression ratio (-0.17% on a linux
kernel source) but makes sure we respect the API promise of no more
than 5 extra bytes per 65535 of input. A side effect of the slightly
simpler check is an ~7.5% performance increase in compression speed.

Many thanks to Greg for the detailed report allowing to reproduce
the issue.

This is libslz upstream commit 002e838935bf298d967f670036efa95822b6c84e.

Note: in haproxy's default configuration (tune.bufsize 16384,
tune.maxrewrite 1024), this problem cannot be triggered, because the
reserve limits input to 15360 bytes, and the overflow is maximum
960 bytes resulting in 16320 bytes total, which still fits into the
buffer. However, reducing tune.maxrewrite below 964, or tune.bufsize
above 17408 can result in overflows for specially crafted patterns.

A workaround for larger buffers consists in always setting tune.bufsize
to at least 1/16 of tune.bufsize.

Reported-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Link: https://www.mail-archive.com/haproxy@formilux.org/msg46837.html
2026-04-08 19:14:25 +02:00
Olivier Houchard
d759e60a32 MEDIUM: check: Revamp the way the protocol and xprt are determined
Storing the protocol directly into the check was not a good idea,
because the protocol may not be determined until after a DNS resolution
on the server, and may even change at runtime, if the DNS changes.
What we can, however, figure out at start up, is the net_addr_type,
which will contain all that we need to find out which protocol to use
later.
Also revert the changes made by commit 07edaed191
that would not reuse the server xprt if a different alpn is set for
checks. The alpn is just a string, and should not influence the choice
of the xprt.
We'll now make sure to use the server xprt, unless an address is
provided, in which case we'll use whatever xprt matches that address, or
a port, in which case we'll assume we want TCP, and use check_ssl to
know whetver we want the SSL xprt or not.
Now that the check contains all that is needed to know which protocol to
look up, always just use that when creating a new check connection if it
is the default check connection, and for now, always use TCP when a
tcp-check or http-check connect rule is used (which means those can't be
used for QUIC so far).

This should hopefully fix github issue #3324.
2026-04-08 18:41:48 +02:00
Olivier Houchard
2140249c18 MINOR: tools: Implement net_addr_type_is_quic()
Implement net_addr_type_is_quic(), that returns 1 if the provided
net_addr_type looks like it is using QUIC, and 0 otherwise.
2026-04-08 18:41:48 +02:00
Olivier Houchard
2eefd489c2 MEDIUM: connections: Really enforce mux protocol requirements
Commit 1b0dfff552 attempted to make it so
the mux would expect a QUIC-like protocol or not, however it only made
that we would not instantiate a non-QUIC mux on a QUIC protocol, but not
that we tried to instance a QUIC mux on a non-QUIC protocol, so fix
that.
2026-04-08 18:41:48 +02:00
William Lallemand
052feec33f CI: github: add the architecture to the cache key for vtest2
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
ARM runners can't use the same build as the other x86_64 ones, add the
architecture to the cache key so it caches and gets the right one.
2026-04-08 11:16:59 +02:00
William Lallemand
8745d2cf8e CI: github: fix vtest path to allow correct caching
The vtest binary does not seem to be cached correctly by actions/cache,
the cause of the problem seems to be the binary is installed outside the
github workspace. This patch installs the binary in ~/vtest/ to fix the
issue.
2026-04-08 11:05:38 +02:00
William Lallemand
923b4c3a19 Revert "BUG: hlua: fix stack overflow in httpclient headers conversion"
This reverts commit a03120e228.

A WIP version of the patch was applied before the actual patch by
accident. The correct patch is 2db801c ("BUG/MINOR: hlua: fix stack
overflow in httpclient headers conversion")
2026-04-08 11:05:38 +02:00
William Lallemand
4111cf3e0e CI: github: update to cache@v5
github complains about cache@v4:

Node.js 20 actions are deprecated. The following actions are running on
Node.js 20 and may not work as expected: actions/cache@v4. Actions will
be forced to run with Node.js 24 by default starting June 2nd, 2026.
Node.js 20 will be removed from the runner on September 16th, 2026.
Please check if updated versions of these actions are available that
support Node.js 24. To opt into Node.js 24 now, set the
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true environment variable on the
runner or in your workflow file. Once Node.js 24 becomes the default,
you can temporarily opt out by setting
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. For more information see:
https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/
2026-04-08 10:15:18 +02:00
Christopher Faulet
b7add82f92 BUG/MEDIUM: connection: Wake the stconn on error when failing to create mux
When the app_ops were removed, direct calls to the SC wake callback function
were replaced by tasklet wakeups. However, in conn_create_mux(), it was
replaced by a direct call to sc_conn_process(). However, sc_conn_process()
is only usable when the SC is attach to a stream. A backend mux can be
created for a healcheck. In this context, sc_conn_process() cannot be
called.

Because of this bug, crashes can be experienced when an error is triggered
during a SSL connection attempt from a healthcheck.

To fix the issue, the call to sc_conn_process() was replaced by a tasklet
wakeup.

This patch should fix the issue #3326. No backport needed.
2026-04-08 08:20:59 +02:00
William Lallemand
accc9003e8 CI: VTest build with git clone + cache
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
The VTest2 tarball URL at code.vinyl-cache.org/vtest/VTest2/archive/main.tar.gz
no longer works. Switch scripts/build-vtest.sh to use a git clone of the
repository instead.

Add a cache step in the setup-vtest CI action so VTest is only rebuilt
when its HEAD commit changes, keyed on the runner OS and the VTest2 HEAD
SHA.
2026-04-07 18:35:23 +02:00
Greg Kroah-Hartman
06673291d7 BUG/MINOR: peers: fix OOB heap write in dictionary cache update
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
When a peer sends a dictionary entry update with a value (the else
branch at line 2109), the entry id decoded from the wire was never
validated against dc->max_entries before being used as an array index
into dc->rx[].

A malicious peer can send id=N where N > 128 (PEER_STKT_CACHE_MAX_ENTRIES)
to:
  - dc->rx[id-1].de at line 2123: OOB read followed by atomic decrement
    and potential free of an attacker-controlled pointer via
    dict_entry_unref()
  - dc->rx[id-1].de = de at line 2124: OOB write of a heap pointer at
    an attacker-controlled offset (16-byte stride, ~64 GiB range)

The bounds check was added to the key-only branch in commit f9e51beec
("BUG/MINOR: peers: Do not ignore a protocol error for dictionary
entries.") but was never added to the with-value branch. The bug has
been present since dictionary support was introduced in commit
8d78fa7def ("MINOR: peers: Make peers protocol support new
"server_name" data type.").

Reachable from any TCP client that knows the configured peer name
(no cryptographic authentication on the peers protocol). Requires a
stick-table with "store server_key" in the configuration.

Fix by hoisting the bounds check above the branch so it covers both
paths.

Must be backported as far as 2.6.
2026-04-07 14:41:46 +02:00
Greg Kroah-Hartman
782a1b5888 BUG/MEDIUM: chunk: fix infinite loop in get_larger_trash_chunk()
When the input chunk is already the large buffer (chk->size ==
large_trash_size), the <= comparison still matched and returned
another large buffer of the same size. Callers that retry on a
non-NULL return value (sample.c:4567 in json_query) loop forever.

The json_query infinite loop is trivially triggered: mjson_unescape()
returns -1 not only when the output buffer is too small but also for
any \uXXYY escape where XX != "00" (mjson.c:305) and for invalid
escapes like \q. The retry loop assumes -1 always means "grow the
buffer", so a 14-byte JSON body of {"k":"\u0100"} hangs the worker
thread permanently. Send N such requests to exhaust all worker
threads.

Use < instead of <= so a chunk that is already large yields NULL.
This also fixes the json converter overflow at sample.c:2869 where
no recheck happens after the "growth" returned a same-size buffer.

Introduced in commit ce912271db ("MEDIUM: chunk: Add support for
large chunks"). No backport needed.
2026-04-07 14:20:38 +02:00
Greg Kroah-Hartman
f712841cf0 BUG/MEDIUM: chunk: fix typo allocating small trash with bufsize_large
A copy-paste error in alloc_trash_buffers_per_thread() passes
global.tune.bufsize_large to alloc_small_trash_buffers() instead of
global.tune.bufsize_small. This sets small_trash_size = bufsize_large.

When tune.bufsize.large is configured, get_larger_trash_chunk() then
incorrectly matches a large buffer against small_trash_size at line
169 and "grows" it to a regular (smaller) buffer. b_xfer() at line
179 attempts to copy the large buffer's contents into the smaller one:

  - Default builds (DEBUG_STRICT=1): BUG_ON in __b_putblk() aborts
    the process -> remote DoS
  - DEBUG_STRICT=0 builds: BUG_ON becomes ASSUME() and the compiler
    elides the check -> heap overflow with attacker-controlled bytes

Reachable via the json converter (sample.c:2862) when escaping
~bufsize_large/6 control characters in attacker-supplied data such
as a request header or body.

Introduced in commit 92a24a4e87 ("MEDIUM: chunk: Add support for
small chunks"). No backport needed.
2026-04-07 14:20:38 +02:00
Greg Kroah-Hartman
d6284470e4 BUG/MINOR: hlua: fix format-string vulnerability in Patref error path
hlua_error() is a printf-family function (calls vsnprintf), but
hlua_patref_set, hlua_patref_add, and _hlua_patref_add_bulk pass
errmsg directly as the format string. errmsg is built by pattern.c
helpers that embed the user-supplied key or value verbatim, e.g.
pat_ref_set_elt() generates "unable to parse '<value>'".

A Lua script calling:

    ref:set("key", "%p.%p.%p.%p.%p.%p.%p.%p")

against a map with an integer output type (where the parse fails)
gets stack/register contents formatted into the (nil, err) return
value -> ASLR/canary leak. With %n and no _FORTIFY_SOURCE this
becomes an arbitrary write primitive.

This must be backported as far as the Patref Lua API exists.
2026-04-07 14:18:13 +02:00
Greg Kroah-Hartman
2db801c635 BUG/MINOR: hlua: fix stack overflow in httpclient headers conversion
hlua_httpclient_table_to_hdrs() declares a VLA of size
global.tune.max_http_hdr (default 101) on the stack but never checks
hdr_num against that bound. A Lua script that supplies a header table
with more than 101 values writes struct http_hdr entries (two ist =
two heap pointers + two lengths) past the end of the VLA, smashing
the stack frame.

Trigger from any Lua action/task/service:

    local hc = core.httpclient()
    local v = {}
    for i = 1, 300 do v[i] = "x" end
    hc:get{ url = "http://127.0.0.1/", headers = { ["X"] = v } }

Each out-of-bounds entry writes a heap pointer (controllable
allocation contents via istdup) plus an attacker-chosen length onto
the stack, overwriting the saved return address.

[wla: this is only reachable if the Lua script passes more than
max_http_hdr header values, which requires access to the script itself]

This must be backported as far as the httpclient Lua API exists.

Signed-off-by: William Lallemand <wlallemand@haproxy.com>
2026-04-07 13:31:39 +02:00
Greg Kroah-Hartman
a03120e228 BUG: hlua: fix stack overflow in httpclient headers conversion
hlua_httpclient_table_to_hdrs() declares a VLA of size
global.tune.max_http_hdr (default 101) on the stack but never checks
hdr_num against that bound. A Lua script that supplies a header table
with more than 101 values writes struct http_hdr entries (two ist =
two heap pointers + two lengths) past the end of the VLA, smashing
the stack frame.

Trigger from any Lua action/task/service:

    local hc = core.httpclient()
    local v = {}
    for i = 1, 300 do v[i] = "x" end
    hc:get{ url = "http://127.0.0.1/", headers = { ["X"] = v } }

Each out-of-bounds entry writes a heap pointer (controllable
allocation contents via istdup) plus an attacker-chosen length onto
the stack, overwriting the saved return address. With no stack
canary, this is direct RCE; with a canary, it requires a leak first.

Reachable from any deployment that loads Lua scripts. While Lua
scripts are nominally trusted, this turns "can edit Lua" into "can
execute arbitrary native code", which is a meaningful boundary in
many setups (Lua sandbox escape).

This must be backported as far as the httpclient Lua API exists.
2026-04-07 11:23:40 +02:00
Greg Kroah-Hartman
5161415653 BUG/MEDIUM: jwe: fix memory leak in jwt_decrypt_secret with var argument
When the secret argument to jwt_decrypt_secret is a variable
(ARGT_VAR) rather than a literal string, alloc_trash_chunk() is
called to hold the base64-decoded secret but the buffer is never
released. The end: label frees input, decrypted_cek, out, and the
decoded_items array but not secret.

Each request leaks one trash chunk (~tune.bufsize, default 16KB).
At ~65000 requests per GiB this allows slow memory exhaustion DoS
against any config of the form:

    http-request set-var(txn.x) req.hdr(...),jwt_decrypt_secret(txn.key)

This must be backported as far as JWE support exists.
2026-04-07 11:17:30 +02:00
Greg Kroah-Hartman
648b0e7bea BUG/MEDIUM: jwt: fix heap overflow in ECDSA signature DER conversion
convert_ecdsa_sig() calls i2d_ECDSA_SIG(ecdsa_sig, &p) where p
points into signature->area, a trash chunk of tune.bufsize bytes
(default 16384). i2d writes with no output bound.

The raw R||S input can be up to bufsize bytes (filled by
base64urldec at jwt.c:520-527), giving bignum_len up to 8192. The
DER encoding adds a SEQUENCE header (2-4 bytes), two INTEGER headers
(2-4 bytes each), and up to two leading-zero sign-padding bytes when
the bignum high bit is set. With two 8192-byte bignums having the
high bit set, the encoding is ~16398 bytes, overflowing the 16384-
byte buffer by ~14 bytes.

Triggered by any JWT with alg=ES256/384/512 and a ~21830-character
base64url signature. The signature does not need to verify
successfully; the overflow happens before verification. Reachable
from any config using jwt_verify with an EC algorithm.

Also fixes the existing wrong check: i2d returns -1 on error which
became SIZE_MAX in the size_t signature->data, defeating the
"== 0" test.

This must be backported as far as JWT support exists.
2026-04-07 11:11:42 +02:00
Greg Kroah-Hartman
717e9aec5f BUG/MEDIUM: jwe: fix NULL deref crash with empty CEK and non-dir alg
In sample_conv_jwt_decrypt_secret(), when a JWE token has an empty
encrypted-key section but the algorithm is not "dir" (e.g. A128KW),
neither branch initializes decrypted_cek. The NULL pointer is then
passed to decrypt_ciphertext() which dereferences it:

  - For GCM encodings: aes_process() calls b_orig(NULL) -> SIGSEGV
  - For CBC encodings: b_data(NULL) at jwe.c:463 -> SIGSEGV

A single HTTP request with a crafted Authorization header crashes the
worker process. Trigger token (JOSE header {"alg":"A128KW","enc":"A128GCM"},
empty CEK section between the two dots):

  eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIn0..AAAAAAAAAAAAAAAA.AA.AA

Reachable in any configuration using the jwt_decrypt_secret converter.
The other two decrypt converters (jwt_decrypt_jwk, jwt_decrypt_cert)
already have the check.

This must be backported as far as JWE support exists.
2026-04-07 10:57:47 +02:00
Greg Kroah-Hartman
ed267f9bc5 BUG/MEDIUM: payload: validate SNI name_len in req.ssl_sni
The 16-bit name_len field is read directly from the ClientHello and
stored as the sample length without any validation against srv_len,
ext_len, or the channel buffer size. A 65-byte ClientHello with
name_len=0xffff produces a sample claiming 65535 bytes of data when
only ~4 bytes are actually present in the buffer.

Downstream consumers then read tens of kilobytes past the channel
buffer:
  - pattern.c:741 XXH3() hashes 65535 bytes -> ~50KB OOB heap read
  - sample.c smp_dup memcpy if large trash configured
  - log-format %[req.ssl_sni] leaks heap contents to logs/headers

Reachable pre-authentication on any TCP frontend using req.ssl_sni
(req_ssl_sni), which is the documented way to do SNI-based content
switching in TCP mode. No SSL handshake is required; the parser
runs on raw buffer contents in tcp-request content rules.

Bug introduced in commit d4c33c8889 (2013). The ALPN parser in
the same file at line 1044 has the equivalent check; SNI never did.

This must be backported to all supported versions.
2026-04-07 10:50:04 +02:00
Christopher Faulet
41bded8952 BUG/MEDIUM: tcpcheck: Properly retrieve tcpcheck type to install the best mux
Some checks failed
Contrib / build (push) Has been cancelled
alpine/musl / gcc (push) Has been cancelled
VTest / Generate Build Matrix (push) Has been cancelled
Windows / Windows, gcc, all features (push) Has been cancelled
VTest / (push) Has been cancelled
When the healthcheck section support was added, the tcpcheck type was moved
into the tcpcheck ruleset. However, conn_install_mux_chk() function was not
updated accordingly. So the TCP mode was always returned.

No backport needed. This patch is related to #3324 but it is not the root
cause of the issue.
2026-04-05 09:26:46 +02:00
Aurelien DARRAGON
ca53ee17b6 BUG/MINOR: counters: fix unexpected 127 char GUID truncation for shm-stats-file objects
Some checks failed
Contrib / build (push) Has been cancelled
alpine/musl / gcc (push) Has been cancelled
VTest / Generate Build Matrix (push) Has been cancelled
Windows / Windows, gcc, all features (push) Has been cancelled
VTest / (push) Has been cancelled
As reported by GH @phihos on GH #3320, using the shm-stats-file feature
with objects exceeding 127 chars would result in object name being
unexpectedly truncated, while GUID API supports up to 128 chars.

Indeed, with the config below, and shm-stats-file enabled:
   server s1  127.0.0.1:1 guid srv:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:SRV_1 disabled
    server s10 127.0.0.1:1 guid srv:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:SRV_10 disabled

haproxy would store the second server object with the same id as the first
one, but upon reload, only the first one would be restored, which would
eventually cause shm-stats-file slot exhaustion with repetitive reloads.

@phihos, found out the underlying issue, in counters.c we used snprintf()
with sizeof(shm_obj->guid) - 1 as <size> parameter, while we should have
use sizeof(shm_obj->guid) instead since shm_obj->guid already takes the
terminating NULL byte into account.

So we simply apply the fix suggested by @phihos, and hopefully this should
solve the shm-stats-file slot leak that was observed.

Unfortunately, for now, we cannot warn the user that a duplicate
shm-stats-file object was found, because we accept duplicate objects
by design for 2 reasons. The first one is for a new process to be able
to change the object type for a previously known GUID while allowing
previous processes to use the old object as long as they are alive.
The second reason is that upon startup we cannot afford to scan the
whole object list, as soon as we find a match (type + GUID), we bind
the object, and this way we avoid unnecessary lookup time.

Perhaps we have room for improvement in the future, but for now let's
keep it this way.

It should be backported to 3.3

Big thanks to @phihos for the bug description, analysis and
suggestions.
2026-04-04 02:14:50 +02:00
Christopher Faulet
774d0dfe14 BUG/MEDIUM: tcpcheck/server: Fix parsing of healthcheck param for dynamic servers
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
The parsing of the "healthcheck" parameter for dynamic servers was not
finished. The post-config was missing, leading to a crash because the
ruleset pointer was NULL.

To fix the issue, check_server_tcpcheck() function is called in
cli_parse_add_server().

No backport needed.
2026-04-03 16:31:18 +02:00
Christopher Faulet
09c37fb6bd MINOR: tcpcheck: Reject unknown keyword during parsing of healthcheck section
unknown keyword was just ignored. it is not really handy to detect
error. Now an error is reported and the parsing is aborted.
2026-04-03 16:31:17 +02:00
Willy Tarreau
6ed656d691 BUG/MINOR: http-act: fix a typo in the "pause" action error message
It was saying "mause" instead of "pause" :-)
This should be backported to 3.2.
2026-04-03 16:25:49 +02:00
Christopher Faulet
063dfd9ff3 DOC: config: Fix two typos in the server param "healthcheck" description
There was 2 typos here. First, the 'k' was missing on the parameter name.
Then "sectino" was used in the description instead of "section". Let's fix
them.
2026-04-03 15:56:57 +02:00
Olivier Houchard
2147e8e368 MINOR: servers: The right parameter for idle-pool.shared is "full"
In documentation, and in an error message, provide the right new keyword for
"idle-pool.shared", it is "full", not "auto".
2026-04-03 15:45:26 +02:00
Christopher Faulet
c010c3924a BUG/MEDIUM: mux-h1: Disable 0-copy forwarding when draining the request
When an early response is sent to the client and the H1 connection is
switched to the draining state, we must take care to disable the 0-copy data
forwarding because the backend side is no longer here. It is an issue
because this prevent any regular receive to be performed.

This patch should fix the issue #3316. It must be backported as far as 3.0.
2026-04-03 15:12:55 +02:00
Christopher Faulet
8056117e98 BUG/MEDIUM: haterm: Move all init functions of haterm in haterm_init.c
Functions used to initialize haterm (the splicing and the response buffers)
were defined and registered in haterm.c. The problem is that this file in
compiled with haproxy. So it may be an issue. And for the splicing part,
warnings may be emitted when haproxy is started.

To avoid any issue during haproxy startup and to avoid to initialize some
part of haterm, all init functions were moved into haterm_init.c file.

No backport needed.
2026-04-03 15:09:44 +02:00
William Lallemand
ed208b7e0f MINOR: tcpcheck: reintroduce proxy_parse_tcpcheck() symbol
Remove the proxy_parse_tcpcheck() static keyword.
2026-04-03 14:37:27 +02:00
William Lallemand
910a182f05 Revert "CLEANUP: tcpcheck: Don't needlessly expose proxy_parse_tcpcheck()"
This reverts commit 51e1562a0d.
2026-04-03 14:15:11 +02:00
Aurelien DARRAGON
3eebd5f67d REGTESTS: add a test for "filter-sequence" directive
Some checks are pending
Contrib / build (push) Waiting to run
alpine/musl / gcc (push) Waiting to run
VTest / Generate Build Matrix (push) Waiting to run
VTest / (push) Blocked by required conditions
Windows / Windows, gcc, all features (push) Waiting to run
We add a reg-test, filter_sequence.vtc, with associated lua file
dummy_filters.lua to cover the "filter-sequence" directive and
ensure it is working as expected, both for request and responses
paths.

This regtest will only be effective starting with 3.4-dev0
2026-04-03 12:10:32 +02:00
Aurelien DARRAGON
8d28c0e37b MEDIUM: filters: add "filter-sequence" directive
This is another pre-requisite work for upcoming decompression filter.

In this patch we implement the "filter-sequence" directive which can be
used in proxy section (frontend,backend,listen) and takes 2 parameters

The first one is the direction (request or response), the second one
is a comma separated list of filter names previously declared on the
proxy using the "filter" keyword.

The main goal of this directive is to be able to instruct haproxy in which
order the filters should be executed on request and response paths,
especially if the ordering between request and response handling must
differ, and without relying on the filter declaration ordering (within
the proxy) which is used by default by haproxy.

Another benefit of this feature is that it becomes possible to "ignore"
a previously declared filter on the proxy. Indeed, when filter-sequence
is defined for a given direction (request/response), then it will be used
over the implicit filter ordering, but if a filter which was previously
declared is not specified in the related filter-sequence, it will not be
executed on purpose. This can be used as a way to temporarily disable a
filter without completely removing its configuration.

Documentation was updated (check examples for more info)
2026-04-03 12:10:27 +02:00
Aurelien DARRAGON
629a5ae531 MINOR: filters: add filter name to flt_conf struct
flt_conf struct stores the filter id, which is used internally to check
match the filter against static pointer identifier, and also used as
descriptive text to describe the filter. But the id is not consistent
with the public name as used in the configuration (for instance when
selecting filter through the 'filter' directive).

What we do in this patch is that we add flt_conf->name member, which
stores the real filter name as seen in the configuration. This will
allow to select filters by their name from other directives in the
configuration.
2026-04-03 12:10:20 +02:00
Aurelien DARRAGON
882176a602 DOC: config: fix ambiguous info in log-steps directive description
log-steps takes <steps> as parameter. <steps> is made of individual
log origins separated by commas, as shown in the examples, but the
directive's description says it should be separated by spaces, which
is wrong.

Let's fix that

It should be backported up to 3.2
2026-04-03 12:10:14 +02:00
194 changed files with 24127 additions and 656 deletions

View file

@ -7,26 +7,40 @@ runs:
- name: Setup coredumps
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
shell: bash
shell: sh
run: |
sudo sysctl -w fs.suid_dumpable=1
sudo sysctl kernel.core_pattern=/tmp/core.%h.%e.%t
- name: Setup ulimit for core dumps
shell: bash
shell: sh
run: |
# This is required for macOS which does not actually allow to increase
# the '-n' soft limit to the hard limit, thus failing to run.
ulimit -n 65536
ulimit -c unlimited
- name: Install VTest
shell: bash
- name: Get VTest latest commit SHA
id: vtest-sha
shell: sh
run: |
scripts/build-vtest.sh
echo "sha=$(git ls-remote https://code.vinyl-cache.org/vtest/VTest2 HEAD | cut -f1)" >> $GITHUB_OUTPUT
- name: Cache VTest
id: cache-vtest
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/vtest
key: vtest-${{ runner.os }}-${{ runner.arch }}-${{ steps.vtest-sha.outputs.sha }}
- name: Install VTest
if: ${{ steps.cache-vtest.outputs.cache-hit != 'true' }}
shell: sh
run: |
DESTDIR=${{ github.workspace }}/vtest scripts/build-vtest.sh
- name: Install problem matcher for VTest
shell: bash
shell: sh
# This allows one to more easily see which tests fail.
run: echo "::add-matcher::.github/vtest.json"

6
.github/matrix.py vendored
View file

@ -69,7 +69,7 @@ def aws_lc_version_string_to_num(version_string):
return tuple(map(int, version_string[1:].split('.')))
def aws_lc_version_valid(version_string):
return re.match('^v[0-9]+(\.[0-9]+)*$', version_string)
return re.match(r'^v[0-9]+(\.[0-9]+)*$', version_string)
@functools.lru_cache(5)
def determine_latest_aws_lc(ssl):
@ -86,7 +86,7 @@ def aws_lc_fips_version_string_to_num(version_string):
return tuple(map(int, version_string[12:].split('.')))
def aws_lc_fips_version_valid(version_string):
return re.match('^AWS-LC-FIPS-[0-9]+(\.[0-9]+)*$', version_string)
return re.match(r'^AWS-LC-FIPS-[0-9]+(\.[0-9]+)*$', version_string)
@functools.lru_cache(5)
def determine_latest_aws_lc_fips(ssl):
@ -103,7 +103,7 @@ def wolfssl_version_string_to_num(version_string):
return tuple(map(int, version_string[1:].removesuffix('-stable').split('.')))
def wolfssl_version_valid(version_string):
return re.match('^v[0-9]+(\.[0-9]+)*-stable$', version_string)
return re.match(r'^v[0-9]+(\.[0-9]+)*-stable$', version_string)
@functools.lru_cache(5)
def determine_latest_wolfssl(ssl):

View file

@ -1,12 +0,0 @@
name: AWS-LC-FIPS
on:
schedule:
- cron: "0 0 * * 4"
workflow_dispatch:
jobs:
test:
uses: ./.github/workflows/aws-lc-template.yml
with:
command: "from matrix import determine_latest_aws_lc_fips; print(determine_latest_aws_lc_fips(''))"

View file

@ -1,94 +0,0 @@
name: AWS-LC template
on:
workflow_call:
inputs:
command:
required: true
type: string
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- name: Determine latest AWS-LC release
id: get_aws_lc_release
run: |
result=$(cd .github && python3 -c "${{ inputs.command }}")
echo $result
echo "result=$result" >> $GITHUB_OUTPUT
- name: Cache AWS-LC
id: cache_aws_lc
uses: actions/cache@v4
with:
path: '~/opt/'
key: ssl-${{ steps.get_aws_lc_release.outputs.result }}-Ubuntu-latest-gcc
- name: Install apt dependencies
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none
sudo apt-get --no-install-recommends -y install socat gdb jose
- name: Install AWS-LC
if: ${{ steps.cache_ssl.outputs.cache-hit != 'true' }}
run: env ${{ steps.get_aws_lc_release.outputs.result }} scripts/build-ssl.sh
- name: Compile HAProxy
run: |
make -j$(nproc) ERR=1 CC=gcc TARGET=linux-glibc \
USE_OPENSSL_AWSLC=1 USE_QUIC=1 \
SSL_LIB=${HOME}/opt/lib SSL_INC=${HOME}/opt/include \
DEBUG="-DDEBUG_POOL_INTEGRITY -DDEBUG_UNIT" \
ADDLIB="-Wl,-rpath,/usr/local/lib/ -Wl,-rpath,$HOME/opt/lib/"
sudo make install
- name: Show HAProxy version
id: show-version
run: |
ldd $(which haproxy)
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
- uses: ./.github/actions/setup-vtest
- name: Run VTest for HAProxy
id: vtest
run: |
make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Run Unit tests
id: unittests
run: |
make unit-tests
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
for folder in ${TMPDIR:-/tmp}/haregtests-*/vtc.*; do
printf "::group::"
cat $folder/INFO
cat $folder/LOG
echo "::endgroup::"
done
exit 1
- name: Show coredumps
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
failed=false
shopt -s nullglob
for file in /tmp/core.*; do
failed=true
printf "::group::"
gdb -ex 'thread apply all bt full' ./haproxy $file
echo "::endgroup::"
done
if [ "$failed" = true ]; then
exit 1;
fi
- name: Show Unit-Tests results
if: ${{ failure() && steps.unittests.outcome == 'failure' }}
run: |
for result in ${TMPDIR:-/tmp}/ha-unittests-*/results/res.*; do
printf "::group::"
cat $result
echo "::endgroup::"
done
exit 1

View file

@ -5,8 +5,95 @@ on:
- cron: "0 0 * * 4"
workflow_dispatch:
permissions:
contents: read
jobs:
test:
uses: ./.github/workflows/aws-lc-template.yml
with:
command: "from matrix import determine_latest_aws_lc; print(determine_latest_aws_lc(''))"
Test:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: AWS-LC
command: "from matrix import determine_latest_aws_lc; print(determine_latest_aws_lc(''))"
- name: AWS-LC (FIPS)
command: "from matrix import determine_latest_aws_lc_fips; print(determine_latest_aws_lc_fips(''))"
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v6
- name: Determine latest AWS-LC release
id: get_aws_lc_release
run: |
result=$(cd .github && python3 -c "${{ matrix.command }}")
echo $result
echo "result=$result" >> $GITHUB_OUTPUT
- name: Cache AWS-LC
id: cache_aws_lc
uses: actions/cache@v5
with:
path: '~/opt/'
key: ssl-${{ steps.get_aws_lc_release.outputs.result }}-Ubuntu-latest-gcc
- name: Install apt dependencies
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none
sudo apt-get --no-install-recommends -y install socat gdb jose
- name: Install AWS-LC
if: ${{ steps.cache_ssl.outputs.cache-hit != 'true' }}
run: env ${{ steps.get_aws_lc_release.outputs.result }} scripts/build-ssl.sh
- name: Compile HAProxy
run: |
make -j$(nproc) ERR=1 CC=gcc TARGET=linux-glibc \
USE_OPENSSL_AWSLC=1 USE_QUIC=1 \
SSL_LIB=${HOME}/opt/lib SSL_INC=${HOME}/opt/include \
DEBUG="-DDEBUG_POOL_INTEGRITY -DDEBUG_UNIT" \
ADDLIB="-Wl,-rpath,/usr/local/lib/ -Wl,-rpath,$HOME/opt/lib/"
sudo make install
- name: Show HAProxy version
id: show-version
run: |
ldd $(which haproxy)
haproxy -vv
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- uses: ./.github/actions/setup-vtest
- name: Run VTest for HAProxy
id: vtest
run: |
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Run Unit tests
id: unittests
run: |
make unit-tests
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
for folder in ${TMPDIR:-/tmp}/haregtests-*/vtc.*; do
printf "::group::"
cat $folder/INFO
cat $folder/LOG
echo "::endgroup::"
done
exit 1
- name: Show coredumps
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
failed=false
shopt -s nullglob
for file in /tmp/core.*; do
failed=true
printf "::group::"
gdb -ex 'thread apply all bt full' ./haproxy $file
echo "::endgroup::"
done
if [ "$failed" = true ]; then
exit 1;
fi
- name: Show Unit-Tests results
if: ${{ failure() && steps.unittests.outcome == 'failure' }}
run: |
for result in ${TMPDIR:-/tmp}/ha-unittests-*/results/res.*; do
printf "::group::"
cat $result
echo "::endgroup::"
done
exit 1

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: codespell-project/codespell-problem-matcher@v1.2.0
- uses: codespell-project/actions-codespell@master
with:

View file

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install h2spec
id: install-h2spec
run: |
@ -45,7 +45,7 @@ jobs:
fi
echo "::endgroup::"
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- name: Launch HAProxy ${{ steps.show-version.outputs.version }}
run: haproxy -f .github/h2spec.config -D
- name: Run h2spec ${{ steps.install-h2spec.outputs.version }}

View file

@ -7,16 +7,27 @@ permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
compile:
name: ${{ matrix.name }}
runs-on: ubuntu-slim
strategy:
matrix:
include:
- name: dev/flags/
targets:
- dev/flags/flags
- name: dev/haring/
targets:
- dev/haring/haring
- name: dev/hpack/
targets:
- dev/hpack/decode
- dev/hpack/gen-enc
- dev/hpack/gen-rht
- name: dev/poll/
targets:
- dev/poll/poll
fail-fast: false
steps:
- uses: actions/checkout@v5
- name: Compile dev/flags/flags
run: |
make dev/flags/flags
- name: Compile dev/poll/poll
run: |
make dev/poll/poll
- name: Compile dev/hpack
run: |
make dev/hpack/decode dev/hpack/gen-enc dev/hpack/gen-rht
- uses: actions/checkout@v6
- run: make ${{ join(matrix.targets, ' ') }}

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install apt dependencies
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none

View file

@ -99,7 +99,7 @@ jobs:
sudo apt-get -yq --force-yes install \
gcc-${{ matrix.platform.arch }} \
${{ matrix.platform.libs }}
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: install quictls

View file

@ -25,7 +25,7 @@ jobs:
container:
image: fedora:rawhide
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install dependencies
run: |
dnf -y install awk diffutils git pcre-devel zlib-devel pcre2-devel 'perl(FindBin)' perl-IPC-Cmd 'perl(File::Copy)' 'perl(File::Compare)' lua-devel socat findutils systemd-devel clang openssl-devel.x86_64
@ -48,7 +48,7 @@ jobs:
ldd $(command -v haproxy)
echo "::endgroup::"
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
#
# TODO: review this workaround later
- name: relax crypto policies
@ -59,7 +59,7 @@ jobs:
- name: Run VTest for HAProxy ${{ steps.show-version.outputs.version }}
id: vtest
run: |
make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |

View file

@ -5,15 +5,16 @@ on:
- cron: "0 0 25 * *"
workflow_dispatch:
permissions:
contents: read
jobs:
gcc:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
permissions:
contents: read
steps:
- name: "Checkout repository"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: "Build on VM"
uses: vmactions/solaris-vm@v1

View file

@ -1,76 +0,0 @@
name: alpine/musl
on:
push:
permissions:
contents: read
jobs:
musl:
name: gcc
runs-on: ubuntu-latest
container:
image: alpine:latest
options: --privileged --ulimit core=-1 --security-opt seccomp=unconfined
volumes:
- /tmp/core:/tmp/core
steps:
- name: Setup coredumps
run: |
ulimit -c unlimited
echo '/tmp/core/core.%h.%e.%t' > /proc/sys/kernel/core_pattern
- uses: actions/checkout@v5
- name: Install dependencies
run: apk add gcc gdb make tar git python3 libc-dev linux-headers pcre-dev pcre2-dev openssl-dev lua5.3-dev grep socat curl musl-dbg lua5.3-dbg jose
- name: Install VTest
run: scripts/build-vtest.sh
- name: Build
run: make -j$(nproc) TARGET=linux-musl DEBUG="-DDEBUG_POOL_INTEGRITY -DDEBUG_UNIT" ARCH_FLAGS='-ggdb3' CC=cc V=1 USE_LUA=1 LUA_INC=/usr/include/lua5.3 LUA_LIB=/usr/lib/lua5.3 USE_OPENSSL=1 USE_PCRE2=1 USE_PCRE2_JIT=1 USE_PROMEX=1
- name: Show version
run: ./haproxy -vv
- name: Show linked libraries
run: ldd haproxy
- name: Install problem matcher for VTest
# This allows one to more easily see which tests fail.
run: echo "::add-matcher::.github/vtest.json"
- name: Run VTest
id: vtest
run: make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Run Unit tests
id: unittests
run: |
make unit-tests
- name: Show coredumps
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
failed=false
ls /tmp/core/
for file in /tmp/core/core.*; do
failed=true
printf "::group::"
gdb -ex 'thread apply all bt full' ./haproxy $file
echo "::endgroup::"
done
if [ "$failed" = true ]; then
exit 1;
fi
- name: Show results
if: ${{ failure() }}
run: |
for folder in /tmp/haregtests-*/vtc.*; do
printf "::group::"
cat $folder/INFO
cat $folder/LOG
echo "::endgroup::"
done
- name: Show Unit-Tests results
if: ${{ failure() && steps.unittests.outcome == 'failure' }}
run: |
for result in ${TMPDIR:-/tmp}/ha-unittests-*/results/res.*; do
printf "::group::"
cat $result
echo "::endgroup::"
done
exit 1

View file

@ -5,15 +5,16 @@ on:
- cron: "0 0 25 * *"
workflow_dispatch:
permissions:
contents: read
jobs:
gcc:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
permissions:
contents: read
steps:
- name: "Checkout repository"
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: "Build on VM"
uses: vmactions/netbsd-vm@v1

View file

@ -13,15 +13,13 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- name: Install VTest
run: |
scripts/build-vtest.sh
- uses: actions/checkout@v6
- name: Install apt dependencies
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none
sudo apt-get --no-install-recommends -y install socat gdb
sudo apt-get --no-install-recommends -y install libpsl-dev
- uses: ./.github/actions/setup-vtest
- name: Install OpenSSL+ECH
run: env OPENSSL_VERSION="git-feature/ech" GIT_TYPE="branch" scripts/build-ssl.sh
- name: Install curl+ECH
@ -40,7 +38,7 @@ jobs:
run: |
ldd $(which haproxy)
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- name: Install problem matcher for VTest
run: echo "::add-matcher::.github/vtest.json"
- name: Run VTest for HAProxy
@ -51,7 +49,7 @@ jobs:
ulimit -n 65536
# allow to catch coredumps
ulimit -c unlimited
make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install apt dependencies
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none
@ -35,7 +35,7 @@ jobs:
run: |
ldd $(which haproxy)
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- name: Install problem matcher for VTest
run: echo "::add-matcher::.github/vtest.json"
- name: Run VTest for HAProxy
@ -46,7 +46,7 @@ jobs:
ulimit -n 65536
# allow to catch coredumps
ulimit -c unlimited
make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |

View file

@ -9,17 +9,16 @@ on:
schedule:
- cron: "0 0 * * 2"
permissions:
contents: read
jobs:
combined-build-and-run:
runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Update Docker to the latest
uses: docker/setup-docker-action@v4
@ -50,7 +49,7 @@ jobs:
python run.py -j result.json -l logs-ngtcp2 -r haproxy=local:aws-lc -t "handshake,transfer,longrtt,chacha20,multiplexing,retry,resumption,zerortt,http3,blackhole,keyupdate,ecn,amplificationlimit,handshakeloss,transferloss,handshakecorruption,transfercorruption,ipv6,v2" -c ngtcp2 -s haproxy
- name: Delete succeeded logs
if: failure()
if: ${{ failure() }}
run: |
for client in chrome picoquic quic-go ngtcp2; do
pushd quic-interop-runner/logs-${client}/haproxy_${client}
@ -59,7 +58,7 @@ jobs:
done
- name: Logs upload
if: failure()
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: logs

View file

@ -9,17 +9,16 @@ on:
schedule:
- cron: "0 0 * * 2"
permissions:
contents: read
jobs:
combined-build-and-run:
runs-on: ubuntu-24.04
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Update Docker to the latest
uses: docker/setup-docker-action@v4
@ -48,7 +47,7 @@ jobs:
python run.py -j result.json -l logs-quic-go -r haproxy=local:libressl -t "handshake,transfer,longrtt,chacha20,multiplexing,retry,http3,blackhole,amplificationlimit,transferloss,transfercorruption,v2" -c quic-go -s haproxy
- name: Delete succeeded logs
if: failure()
if: ${{ failure() }}
run: |
for client in picoquic quic-go; do
pushd quic-interop-runner/logs-${client}/haproxy_${client}
@ -57,7 +56,7 @@ jobs:
done
- name: Logs upload
if: failure()
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: logs

View file

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install apt dependencies
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none
@ -38,12 +38,12 @@ jobs:
run: |
ldd $(which haproxy)
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- uses: ./.github/actions/setup-vtest
- name: Run VTest for HAProxy
id: vtest
run: |
make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |

View file

@ -19,11 +19,11 @@ jobs:
# generated by .github/matrix.py.
generate-matrix:
name: Generate Build Matrix
runs-on: ubuntu-latest
runs-on: ubuntu-slim
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Generate Build Matrix
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -44,7 +44,7 @@ jobs:
TMPDIR: /tmp
OT_CPP_VERSION: 1.6.0
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 100
@ -59,7 +59,7 @@ jobs:
- name: Cache SSL libs
if: ${{ matrix.ssl && matrix.ssl != 'stock' && matrix.ssl != 'BORINGSSL=yes' && !contains(matrix.ssl, 'QUICTLS') }}
id: cache_ssl
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: '~/opt/'
key: ssl-${{ steps.generate-cache-key.outputs.key }}
@ -67,7 +67,7 @@ jobs:
- name: Cache OpenTracing
if: ${{ contains(matrix.FLAGS, 'USE_OT=1') }}
id: cache_ot
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: '~/opt-ot/'
key: ${{ matrix.os }}-ot-${{ matrix.CC }}-${{ env.OT_CPP_VERSION }}-${{ contains(matrix.name, 'ASAN') }}
@ -76,9 +76,9 @@ jobs:
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none
sudo apt-get --no-install-recommends -y install \
${{ contains(matrix.FLAGS, 'USE_LUA=1') && 'liblua5.4-dev' || '' }} \
${{ contains(matrix.FLAGS, 'USE_PCRE2=1') && 'libpcre2-dev' || '' }} \
${{ contains(matrix.ssl, 'BORINGSSL=yes') && 'ninja-build' || '' }} \
${{ case(contains(matrix.FLAGS, 'USE_LUA=1'), 'liblua5.4-dev', '') }} \
${{ case(contains(matrix.FLAGS, 'USE_PCRE2=1'), 'libpcre2-dev', '') }} \
${{ case(contains(matrix.ssl, 'BORINGSSL=yes'), 'ninja-build', '') }} \
socat \
gdb \
jose
@ -136,11 +136,11 @@ jobs:
fi
echo "::endgroup::"
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- name: Run VTest for HAProxy ${{ steps.show-version.outputs.version }}
id: vtest
run: |
make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
@ -178,3 +178,105 @@ jobs:
if [ "$failed" = true ]; then
exit 1;
fi
Test-musl:
name: Alpine+musl, ${{ matrix.name }}
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: gcc
CC: gcc
FLAGS:
- ARCH_FLAGS='-ggdb3'
- USE_LUA=1
- LUA_INC=/usr/include/lua5.3
- LUA_LIB=/usr/lib/lua5.3
- USE_OPENSSL=1
- USE_PCRE2=1
- USE_PCRE2_JIT=1
- USE_PROMEX=1
fail-fast: false
container:
image: alpine:latest
options: --privileged --ulimit core=-1 --security-opt seccomp=unconfined
volumes:
- /tmp/core:/tmp/core
steps:
- name: Setup coredumps
run: |
echo '/tmp/core/core.%h.%e.%t' > /proc/sys/kernel/core_pattern
- uses: actions/checkout@v6
- name: Install dependencies
run: apk add gcc gdb make tar git python3 libc-dev linux-headers pcre-dev pcre2-dev openssl-dev lua5.3-dev grep socat curl musl-dbg lua5.3-dbg jose
- uses: ./.github/actions/setup-vtest
- name: Compile HAProxy with ${{ matrix.CC }}
run: |
echo "::group::Show compiler's version"
echo | ${{ matrix.CC }} -v
echo "::endgroup::"
echo "::group::Show platform specific defines"
echo | ${{ matrix.CC }} -dM -xc -E -
echo "::endgroup::"
make -j$(nproc) all \
ERR=1 \
TARGET=linux-musl \
CC=${{ matrix.CC }} \
DEBUG="-DDEBUG_POOL_INTEGRITY -DDEBUG_UNIT" \
${{ join(matrix.FLAGS, ' ') }} \
ADDLIB="-Wl,-rpath,/usr/local/lib/ -Wl,-rpath,$HOME/opt/lib/"
make install-bin
- name: Show HAProxy version
id: show-version
run: |
echo "::group::Show dynamic libraries."
if command -v ldd > /dev/null; then
# Linux
ldd $(which haproxy)
else
# macOS
otool -L $(which haproxy)
fi
echo "::endgroup::"
haproxy -vv
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- name: Run VTest for HAProxy ${{ steps.show-version.outputs.version }}
id: vtest
run: |
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Show VTest results
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
for folder in ${TMPDIR:-/tmp}/haregtests-*/vtc.*; do
printf "::group::"
cat $folder/INFO
cat $folder/LOG
echo "::endgroup::"
done
exit 1
- name: Run Unit tests
id: unittests
run: |
make unit-tests
- name: Show Unit-Tests results
if: ${{ failure() && steps.unittests.outcome == 'failure' }}
run: |
for result in ${TMPDIR:-/tmp}/ha-unittests-*/results/res.*; do
printf "::group::"
cat $result
echo "::endgroup::"
done
exit 1
- name: Show coredumps
if: ${{ failure() && steps.vtest.outcome == 'failure' }}
run: |
failed=false
ls /tmp/core/
for file in /tmp/core/core.*; do
failed=true
printf "::group::"
gdb -ex 'thread apply all bt full' ./haproxy $file
echo "::endgroup::"
done
if [ "$failed" = true ]; then
exit 1;
fi

View file

@ -36,7 +36,7 @@ jobs:
- USE_THREAD=1
- USE_ZLIB=1
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: msys2/setup-msys2@v2
with:
install: >-

View file

@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'haproxy' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install apt dependencies
run: |
sudo apt-get update -o Acquire::Languages=none -o Acquire::Translation=none
sudo apt-get --no-install-recommends -y install socat gdb jose
- name: Install WolfSSL
run: env WOLFSSL_VERSION=git-master WOLFSSL_DEBUG=1 scripts/build-ssl.sh
run: env WOLFSSL_VERSION=git-master WOLFSSL_DEBUG=1 CFLAGS="-fsanitize=address -g" scripts/build-ssl.sh
- name: Compile HAProxy
run: |
make -j$(nproc) ERR=1 CC=gcc TARGET=linux-glibc \
@ -34,12 +34,12 @@ jobs:
run: |
ldd $(which haproxy)
haproxy -vv
echo "version=$(haproxy -v |awk 'NR==1{print $3}')" >> $GITHUB_OUTPUT
echo "version=$(haproxy -vq)" >> $GITHUB_OUTPUT
- uses: ./.github/actions/setup-vtest
- name: Run VTest for HAProxy
id: vtest
run: |
make reg-tests VTEST_PROGRAM=../vtest/vtest REGTESTS_TYPES=default,bug,devel
make reg-tests VTEST_PROGRAM=${{ github.workspace }}/vtest/vtest REGTESTS_TYPES=default,bug,devel
- name: Run Unit tests
id: unittests
run: |

View file

@ -60,6 +60,7 @@
# USE_OBSOLETE_LINKER : use when the linker fails to emit __start_init/__stop_init
# USE_THREAD_DUMP : use the more advanced thread state dump system. Automatic.
# USE_OT : enable the OpenTracing filter
# USE_OTEL : enable the OpenTelemetry filter
# USE_MEMORY_PROFILING : enable the memory profiler. Linux-glibc only.
# USE_LIBATOMIC : force to link with/without libatomic. Automatic.
# USE_PTHREAD_EMULATION : replace pthread's rwlocks with ours
@ -128,6 +129,11 @@
# OT_LIB : force the lib path to libopentracing-c-wrapper
# OT_RUNPATH : add RUNPATH for libopentracing-c-wrapper to haproxy executable
# OT_USE_VARS : allows the use of variables for the OpenTracing context
# OTEL_DEBUG : compile the OpenTelemetry filter in debug mode
# OTEL_INC : force the include path to libopentelemetry-c-wrapper
# OTEL_LIB : force the lib path to libopentelemetry-c-wrapper
# OTEL_RUNPATH : add RUNPATH for libopentelemetry-c-wrapper to haproxy executable
# OTEL_USE_VARS : allows the use of variables for the OpenTelemetry context
# IGNOREGIT : ignore GIT commit versions if set.
# VERSION : force haproxy version reporting.
# SUBVERS : add a sub-version (eg: platform, model, ...).
@ -347,7 +353,7 @@ use_opts = USE_EPOLL USE_KQUEUE USE_NETFILTER USE_POLL \
USE_CPU_AFFINITY USE_TFO USE_NS USE_DL USE_RT USE_LIBATOMIC \
USE_MATH USE_DEVICEATLAS USE_51DEGREES \
USE_WURFL USE_OBSOLETE_LINKER USE_PRCTL USE_PROCCTL \
USE_THREAD_DUMP USE_EVPORTS USE_OT USE_QUIC USE_PROMEX \
USE_THREAD_DUMP USE_EVPORTS USE_OT USE_OTEL USE_QUIC USE_PROMEX \
USE_MEMORY_PROFILING USE_SHM_OPEN \
USE_STATIC_PCRE USE_STATIC_PCRE2 \
USE_PCRE USE_PCRE_JIT USE_PCRE2 USE_PCRE2_JIT \
@ -862,6 +868,10 @@ ifneq ($(USE_OT:0=),)
include addons/ot/Makefile
endif
ifneq ($(USE_OTEL:0=),)
include addons/otel/Makefile
endif
# better keep this one close to the end, as several libs above may need it
ifneq ($(USE_DL:0=),)
DL_LDFLAGS = -ldl
@ -1162,7 +1172,7 @@ uninstall:
$(Q)rm -f "$(DESTDIR)$(SBINDIR)"/haproxy
clean:
$(Q)rm -f *.[oas] src/*.[oas] haproxy test .build_opts .build_opts.new
$(Q)rm -f *.[oas] src/*.[oas] haproxy haterm test .build_opts .build_opts.new
$(Q)for dir in . src dev/* admin/* addons/* include/* doc; do rm -f $$dir/*~ $$dir/*.rej $$dir/core; done
$(Q)rm -f haproxy-$(VERSION).tar.gz haproxy-$(VERSION)$(SUBVERS)$(EXTRAVERSION).tar.gz
$(Q)rm -f haproxy-$(VERSION) haproxy-$(VERSION)$(SUBVERS)$(EXTRAVERSION) nohup.out gmon.out
@ -1170,6 +1180,7 @@ clean:
$(Q)rm -f addons/51degrees/*.[oas] addons/51degrees/dummy/*.[oas] addons/51degrees/dummy/*/*.[oas]
$(Q)rm -f addons/deviceatlas/*.[oas] addons/deviceatlas/dummy/*.[oas] addons/deviceatlas/dummy/*.o
$(Q)rm -f addons/deviceatlas/dummy/Os/*.o
$(Q)rm -f addons/otel/src/*.[oas]
$(Q)rm -f addons/ot/src/*.[oas]
$(Q)rm -f addons/wurfl/*.[oas] addons/wurfl/dummy/*.[oas]
$(Q)rm -f admin/*/*.[oas] admin/*/*/*.[oas]

View file

@ -70,4 +70,4 @@ OPTIONS_OBJS += \
addons/ot/src/vars.o
endif
OT_CFLAGS := $(OT_CFLAGS) -Iaddons/ot/include $(OT_DEFINE)
OT_CFLAGS := $(OT_CFLAGS) $(OT_DEFINE)

View file

@ -35,11 +35,11 @@
do { \
if (!(l) || (flt_ot_debug.level & (1 << (l)))) \
(void)fprintf(stderr, FLT_OT_DBG_FMT("%.*s" f "\n"), \
dbg_indent_level, FLT_OT_DBG_INDENT, ##__VA_ARGS__); \
flt_ot_dbg_indent_level, FLT_OT_DBG_INDENT, ##__VA_ARGS__); \
} while (0)
# define FLT_OT_FUNC(f, ...) do { FLT_OT_DBG(1, "%s(" f ") {", __func__, ##__VA_ARGS__); dbg_indent_level += 3; } while (0)
# define FLT_OT_RETURN(a) do { dbg_indent_level -= 3; FLT_OT_DBG(1, "}"); return a; } while (0)
# define FLT_OT_RETURN_EX(a,t,f) do { dbg_indent_level -= 3; { t _r = (a); FLT_OT_DBG(1, "} = " f, _r); return _r; } } while (0)
# define FLT_OT_FUNC(f, ...) do { FLT_OT_DBG(1, "%s(" f ") {", __func__, ##__VA_ARGS__); flt_ot_dbg_indent_level += 3; } while (0)
# define FLT_OT_RETURN(a) do { flt_ot_dbg_indent_level -= 3; FLT_OT_DBG(1, "}"); return a; } while (0)
# define FLT_OT_RETURN_EX(a,t,f) do { flt_ot_dbg_indent_level -= 3; { t _r = (a); FLT_OT_DBG(1, "} = " f, _r); return _r; } } while (0)
# define FLT_OT_RETURN_INT(a) FLT_OT_RETURN_EX((a), int, "%d")
# define FLT_OT_RETURN_PTR(a) FLT_OT_RETURN_EX((a), void *, "%p")
# define FLT_OT_DBG_IFDEF(a,b) a
@ -54,7 +54,7 @@ struct flt_ot_debug {
};
extern THREAD_LOCAL int dbg_indent_level;
extern THREAD_LOCAL int flt_ot_dbg_indent_level;
extern struct flt_ot_debug flt_ot_debug;
#else

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
/***

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
/***

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
#define FLT_OT_EVENT_DEF(a,b,c,d,e,f) { AN_##b##_##a, SMP_OPT_DIR_##b, SMP_VAL_FE_##c, SMP_VAL_BE_##d, e, f },

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
/*

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
#define FLT_OT_GROUP_DEF(a,b,c) { a, b, c },

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
#ifdef DEBUG_OT

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
static struct pool_head *pool_head_ot_span_context __read_mostly = NULL;

View file

@ -17,12 +17,12 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
#ifdef DEBUG_OT
struct flt_ot_debug flt_ot_debug;
THREAD_LOCAL int dbg_indent_level = 0;
THREAD_LOCAL int flt_ot_dbg_indent_level = 0;
#endif
#ifdef OTC_DBG_MEM
@ -359,8 +359,7 @@ static int flt_ot_parse_cfg_sample(const char *file, int linenum, char **args, s
*/
static int flt_ot_parse_cfg_str(const char *file, int linenum, char **args, struct list *head, char **err)
{
struct flt_ot_conf_str *str = NULL;
int i, retval = ERR_NONE;
int i, retval = ERR_NONE;
FLT_OT_FUNC("\"%s\", %d, %p, %p, %p:%p", file, linenum, args, head, FLT_OT_DPTR_ARGS(err));
@ -368,9 +367,6 @@ static int flt_ot_parse_cfg_str(const char *file, int linenum, char **args, stru
if (flt_ot_conf_str_init(args[i], linenum, head, err) == NULL)
retval |= ERR_ABORT | ERR_ALERT;
if (retval & ERR_CODE)
flt_ot_conf_str_free(&str);
FLT_OT_RETURN_INT(retval);
}
@ -644,7 +640,7 @@ static int flt_ot_parse_cfg_group(const char *file, int linenum, char **args, in
if (pdata->keyword == FLT_OT_PARSE_GROUP_ID) {
flt_ot_current_group = flt_ot_conf_group_init(args[1], linenum, &(flt_ot_current_config->groups), &err);
if (flt_ot_current_config == NULL)
if (flt_ot_current_group == NULL)
retval |= ERR_ABORT | ERR_ALERT;
}
else if (pdata->keyword == FLT_OT_PARSE_GROUP_SCOPES) {

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
/***

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
static struct pool_head *pool_head_ot_scope_span __read_mostly = NULL;

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
#ifdef DEBUG_OT
@ -41,7 +41,7 @@ void flt_ot_args_dump(char **args)
argc = flt_ot_args_count(args);
(void)fprintf(stderr, FLT_OT_DBG_FMT("%.*sargs[%d]: { '%s' "), dbg_indent_level, FLT_OT_DBG_INDENT, argc, args[0]);
(void)fprintf(stderr, FLT_OT_DBG_FMT("%.*sargs[%d]: { '%s' "), flt_ot_dbg_indent_level, FLT_OT_DBG_INDENT, argc, args[0]);
for (i = 1; i < argc; i++)
(void)fprintf(stderr, "'%s' ", args[i]);

View file

@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "include.h"
#include "../include/include.h"
#ifdef DEBUG_OT
@ -46,10 +46,10 @@ static void flt_ot_vars_scope_dump(struct vars *vars, const char *scope)
vars_rdlock(vars);
for (i = 0; i < VAR_NAME_ROOTS; i++) {
struct ceb_node *node = cebu64_first(&(vars->name_root[i]));
struct ceb_node *node = cebu64_imm_first(&(vars->name_root[i]));
for ( ; node != NULL; node = cebu64_next(&(vars->name_root[i]), node)) {
struct var *var = container_of(node, struct var, node);
for ( ; node != NULL; node = cebu64_imm_next(&(vars->name_root[i]), node)) {
struct var *var = container_of(node, struct var, name_node);
FLT_OT_DBG(2, "'%s.%016" PRIx64 "' -> '%.*s'", scope, var->name_hash, (int)b_data(&(var->data.u.str)), b_orig(&(var->data.u.str)));
}

1
addons/otel/AUTHORS Normal file
View file

@ -0,0 +1 @@
Miroslav Zagorac <mzagorac@haproxy.com>

1
addons/otel/MAINTAINERS Normal file
View file

@ -0,0 +1 @@
Miroslav Zagorac <mzagorac@haproxy.com>

80
addons/otel/Makefile Normal file
View file

@ -0,0 +1,80 @@
# USE_OTEL : enable the OpenTelemetry filter
# OTEL_DEBUG : compile the OpenTelemetry filter in debug mode
# OTEL_INC : force the include path to libopentelemetry-c-wrapper
# OTEL_LIB : force the lib path to libopentelemetry-c-wrapper
# OTEL_RUNPATH : add libopentelemetry-c-wrapper RUNPATH to haproxy executable
# OTEL_USE_VARS : allows the use of variables for the OpenTelemetry context
OTEL_DEFINE =
OTEL_CFLAGS =
OTEL_LDFLAGS =
OTEL_DEBUG_EXT =
OTEL_PKGSTAT =
OTELC_WRAPPER = opentelemetry-c-wrapper
ifneq ($(OTEL_DEBUG:0=),)
OTEL_DEBUG_EXT = _dbg
OTEL_DEFINE = -DDEBUG_OTEL
endif
ifeq ($(OTEL_INC),)
OTEL_PKGSTAT = $(shell pkg-config --exists $(OTELC_WRAPPER)$(OTEL_DEBUG_EXT); echo $$?)
OTEL_CFLAGS = $(shell pkg-config --silence-errors --cflags $(OTELC_WRAPPER)$(OTEL_DEBUG_EXT))
else
ifneq ($(wildcard $(OTEL_INC)/$(OTELC_WRAPPER)/.*),)
OTEL_CFLAGS = -I$(OTEL_INC) $(if $(OTEL_DEBUG),-DOTELC_DBG_MEM)
endif
endif
ifeq ($(OTEL_PKGSTAT),)
ifeq ($(OTEL_CFLAGS),)
$(error OpenTelemetry C wrapper : can't find headers)
endif
else
ifneq ($(OTEL_PKGSTAT),0)
$(error OpenTelemetry C wrapper : can't find package)
endif
endif
ifeq ($(OTEL_LIB),)
OTEL_LDFLAGS = $(shell pkg-config --silence-errors --libs $(OTELC_WRAPPER)$(OTEL_DEBUG_EXT))
else
ifneq ($(wildcard $(OTEL_LIB)/lib$(OTELC_WRAPPER).*),)
OTEL_LDFLAGS = -L$(OTEL_LIB) -l$(OTELC_WRAPPER)$(OTEL_DEBUG_EXT)
ifneq ($(OTEL_RUNPATH),)
OTEL_LDFLAGS += -Wl,--rpath,$(OTEL_LIB)
endif
endif
endif
ifeq ($(OTEL_LDFLAGS),)
$(error OpenTelemetry C wrapper : can't find library)
endif
OPTIONS_OBJS += \
addons/otel/src/cli.o \
addons/otel/src/conf.o \
addons/otel/src/event.o \
addons/otel/src/filter.o \
addons/otel/src/group.o \
addons/otel/src/http.o \
addons/otel/src/otelc.o \
addons/otel/src/parser.o \
addons/otel/src/pool.o \
addons/otel/src/scope.o \
addons/otel/src/util.o
ifneq ($(OTEL_USE_VARS:0=),)
OTEL_DEFINE += -DUSE_OTEL_VARS
OPTIONS_OBJS += addons/otel/src/vars.o
# Auto-detect whether struct var has a 'name' member. When present,
# prefix-based variable scanning can be used instead of the tracking
# buffer approach.
OTEL_VAR_HAS_NAME := $(shell awk '/^struct var \{/,/^\}/' include/haproxy/vars-t.h 2>/dev/null | grep -q '[*]name;' && echo 1)
ifneq ($(OTEL_VAR_HAS_NAME),)
OTEL_DEFINE += -DUSE_OTEL_VARS_NAME
endif
endif
OTEL_CFLAGS := $(OTEL_CFLAGS) -Iaddons/otel/include $(OTEL_DEFINE)

1182
addons/otel/README Normal file

File diff suppressed because it is too large Load diff

456
addons/otel/README-conf Normal file
View file

@ -0,0 +1,456 @@
OpenTelemetry Filter Configuration Structures
==============================================================================
1 Overview
------------------------------------------------------------------------------
The OpenTelemetry filter configuration is a tree of C structures that mirrors
the hierarchical layout of the filter's configuration file. Each structure type
carries a common header macro, and its allocation and deallocation are performed
by macro-generated init/free function pairs defined in conf_funcs.h.
The root of the tree is flt_otel_conf, which owns the instrumentation settings,
groups, and scopes. Scopes contain the actual tracing and metrics definitions:
contexts, spans, instruments, and their sample expressions.
Source files:
include/conf.h Structure definitions and debug macros.
include/conf_funcs.h Init/free macro templates and declarations.
src/conf.c Init/free implementations for all types.
2 Common Macros
------------------------------------------------------------------------------
Two macros provide the building blocks embedded in every configuration
structure.
2.1 FLT_OTEL_CONF_STR(p)
Expands to an anonymous struct containing a string pointer and its cached
length:
struct {
char *p;
size_t p_len;
};
Used for auxiliary string fields that do not need list linkage (e.g. ref_id and
ctx_id in flt_otel_conf_span).
2.2 FLT_OTEL_CONF_HDR(p)
Expands to an anonymous struct that extends FLT_OTEL_CONF_STR with a
configuration file line number and an intrusive list node:
struct {
char *p;
size_t p_len;
int cfg_line;
struct list list;
};
Every configuration structure embeds FLT_OTEL_CONF_HDR as its first member.
The <p> parameter names the identifier field (e.g. "id", "key", "str", "span",
"fmt_expr"). The list node chains the structure into its parent's list.
The cfg_line records the source line for error reporting.
3 Structure Hierarchy
------------------------------------------------------------------------------
The complete ownership tree, from root to leaves:
flt_otel_conf
+-- flt_otel_conf_instr (one, via pointer)
| +-- flt_otel_conf_ph (ph_groups list)
| +-- flt_otel_conf_ph (ph_scopes list)
| +-- struct acl (acls list, HAProxy-owned type)
| +-- struct logger (proxy_log.loggers, HAProxy type)
+-- flt_otel_conf_group (groups list)
| +-- flt_otel_conf_ph (ph_scopes list)
+-- flt_otel_conf_scope (scopes list)
+-- flt_otel_conf_context (contexts list)
+-- flt_otel_conf_span (spans list)
| +-- flt_otel_conf_link (links list)
| +-- flt_otel_conf_sample (attributes list)
| | +-- flt_otel_conf_sample_expr (exprs list)
| +-- flt_otel_conf_sample (events list)
| | +-- flt_otel_conf_sample_expr (exprs list)
| +-- flt_otel_conf_sample (baggages list)
| | +-- flt_otel_conf_sample_expr (exprs list)
| +-- flt_otel_conf_sample (statuses list)
| +-- flt_otel_conf_sample_expr (exprs list)
+-- flt_otel_conf_str (spans_to_finish list)
+-- flt_otel_conf_instrument (instruments list)
| +-- flt_otel_conf_sample (samples list)
| +-- flt_otel_conf_sample_expr (exprs list)
+-- flt_otel_conf_log_record (log_records list)
+-- flt_otel_conf_sample (attributes list)
| +-- flt_otel_conf_sample_expr (exprs list)
+-- flt_otel_conf_sample (samples list)
+-- flt_otel_conf_sample_expr (exprs list)
All child lists use HAProxy's intrusive doubly-linked list (struct list)
threaded through the FLT_OTEL_CONF_HDR embedded in each child structure.
3.1 Placeholder Structures
The flt_otel_conf_ph structure serves as an indirection node. During parsing,
placeholder entries record names of groups and scopes. At check time
(flt_otel_check), these names are resolved to pointers to the actual
flt_otel_conf_group or flt_otel_conf_scope structures via the ptr field.
Two type aliases exist for clarity:
#define flt_otel_conf_ph_group flt_otel_conf_ph
#define flt_otel_conf_ph_scope flt_otel_conf_ph
Corresponding free aliases ensure the FLT_OTEL_LIST_DESTROY macro can locate
the correct free function:
#define flt_otel_conf_ph_group_free flt_otel_conf_ph_free
#define flt_otel_conf_ph_scope_free flt_otel_conf_ph_free
4 Structure Definitions
------------------------------------------------------------------------------
4.1 flt_otel_conf (root)
proxy Proxy owning the filter.
id The OpenTelemetry filter id.
cfg_file The OpenTelemetry filter configuration file name.
instr The OpenTelemetry instrumentation settings (pointer).
groups List of all available groups.
scopes List of all available scopes.
cnt Various counters related to filter operation.
smp_args Deferred sample fetch arguments to resolve at check time.
This structure does not use FLT_OTEL_CONF_HDR because it is not part of any
list -- it is the unique root, owned by the filter instance.
4.2 flt_otel_conf_instr (instrumentation)
FLT_OTEL_CONF_HDR(id) The OpenTelemetry instrumentation name.
config The OpenTelemetry configuration file name.
tracer The OpenTelemetry tracer handle.
meter The OpenTelemetry meter handle.
logger The OpenTelemetry logger handle.
rate_limit Rate limit as uint32 ([0..2^32-1] maps [0..100]%).
flag_harderr Hard-error mode flag.
flag_disabled Disabled flag.
logging Logging mode (0, 1, or 3).
proxy_log The log server list (HAProxy proxy structure).
analyzers Defined channel analyzers bitmask.
idle_timeout Minimum idle timeout across scopes (ms, 0 = off).
acls ACLs declared on this tracer.
ph_groups List of all used groups (placeholders).
ph_scopes List of all used scopes (placeholders).
Exactly one instrumentation block is allowed per filter instance. The parser
stores a pointer to it in flt_otel_conf.instr.
4.3 flt_otel_conf_group
FLT_OTEL_CONF_HDR(id) The group name.
flag_used The indication that the group is being used.
ph_scopes List of all used scopes (placeholders).
Groups bundle scopes for use with the "otel-group" HAProxy action.
4.4 flt_otel_conf_scope
FLT_OTEL_CONF_HDR(id) The scope name.
flag_used The indication that the scope is being used.
event FLT_OTEL_EVENT_* identifier.
idle_timeout Idle timeout interval in milliseconds (0 = off).
acls ACLs declared on this scope.
cond ACL condition to meet.
contexts Declared contexts.
spans Declared spans.
spans_to_finish The list of spans scheduled for finishing.
instruments The list of metric instruments.
log_records The list of log records.
Each scope binds to a single HAProxy analyzer event (or none, if used only
through groups).
4.5 flt_otel_conf_span
FLT_OTEL_CONF_HDR(id) The name of the span.
FLT_OTEL_CONF_STR(ref_id) The reference name, if used.
FLT_OTEL_CONF_STR(ctx_id) The span context name, if used.
ctx_flags The type of storage used for the span context.
flag_root Whether this is a root span.
links The set of linked span names.
attributes The set of key:value attributes.
events The set of events with key-value attributes.
baggages The set of key:value baggage items.
statuses Span status code and description.
The ref_id and ctx_id fields use FLT_OTEL_CONF_STR because they are simple name
strings without list linkage.
4.6 flt_otel_conf_instrument
FLT_OTEL_CONF_HDR(id) The name of the instrument.
idx Meter instrument index: UNSET (-1) before creation,
PENDING (-2) while another thread is creating, or >= 0
for the actual meter index.
type Instrument type (or UPDATE).
aggr_type Aggregation type for the view (create only).
description Instrument description (create only).
unit Instrument unit (create only).
samples Sample expressions for the value.
bounds Histogram bucket boundaries (create only).
bounds_num Number of histogram bucket boundaries.
attributes Instrument attributes (update only, flt_otel_conf_sample).
ref Resolved create-form instrument (update only).
Instruments come in two forms: create-form (defines a new metric with type,
description, unit, and optional histogram bounds) and update-form (references
an existing instrument via the ref pointer). Update-form attributes are stored
as flt_otel_conf_sample entries and evaluated at runtime.
4.7 flt_otel_conf_log_record
FLT_OTEL_CONF_HDR(id) Required by macro; member <id> is not used directly.
severity The severity level.
event_id Optional event identifier.
event_name Optional event name.
span Optional span reference.
attributes Log record attributes (flt_otel_conf_sample list).
samples Sample expressions for the body.
Log records are emitted via the OTel logger at the configured severity. The
optional span reference associates the log record with an open span at runtime.
Attributes are stored as flt_otel_conf_sample entries added via the 'attr'
keyword, which can be repeated. Attribute values are HAProxy sample expressions
evaluated at runtime.
4.8 flt_otel_conf_context
FLT_OTEL_CONF_HDR(id) The name of the context.
flags Storage type from which the span context is extracted.
4.9 flt_otel_conf_sample
FLT_OTEL_CONF_HDR(key) The list containing sample names.
fmt_string Combined sample-expression arguments string.
extra Optional supplementary data.
exprs Used to chain sample expressions.
num_exprs Number of defined expressions.
lf_expr The log-format expression.
lf_used Whether lf_expr is used instead of exprs.
The extra field carries type-specific data: event name strings (OTELC_VALUE_DATA)
for span events, status code integers (OTELC_VALUE_INT32) for span statuses.
When the sample value argument contains the "%[" sequence, the parser treats
it as a log-format string: the lf_used flag is set and the compiled result is
stored in lf_expr, while the exprs list remains empty. At runtime, if lf_used
is true, the log-format expression is evaluated via build_logline() instead of
the sample expression list.
4.10 flt_otel_conf_sample_expr
FLT_OTEL_CONF_HDR(fmt_expr) The original expression format string.
expr The sample expression (struct sample_expr).
4.11 Simple Types
flt_otel_conf_hdr Generic header; used for simple named entries.
flt_otel_conf_str String holder (identical to conf_hdr in layout, but the
HDR field is named "str" instead of "id"); used for
spans_to_finish.
flt_otel_conf_link Span link reference; HDR field named "span".
flt_otel_conf_ph Placeholder; carries a ptr field resolved at check time.
5 Initialization
------------------------------------------------------------------------------
5.1 Macro-Generated Init Functions
The FLT_OTEL_CONF_FUNC_INIT macro (conf_funcs.h) generates a function with the
following signature for each configuration type:
struct flt_otel_conf_<type> *flt_otel_conf_<type>_init(const char *id, int line, struct list *head, char **err);
The generated function performs these steps:
1. Validates that <id> is non-NULL and non-empty.
2. Checks the identifier length against FLT_OTEL_ID_MAXLEN (64).
3. If <head> is non-NULL, iterates the list to reject duplicate identifiers
(strcmp match).
4. Allocates the structure with OTELC_CALLOC (zeroed memory).
5. Records the configuration line number in cfg_line.
6. Duplicates the identifier string with OTELC_STRDUP.
7. If <head> is non-NULL, appends the structure to the list via LIST_APPEND.
8. Executes any custom initialization body provided as the third macro
argument.
If any step fails, the function sets an error message via FLT_OTEL_ERR and
returns NULL.
5.2 Custom Initialization Bodies
Several structure types require additional setup beyond what the macro template
provides. The custom init body runs after the base allocation and list
insertion succeed:
conf_sample:
LIST_INIT for exprs. Calls lf_expr_init for lf_expr.
conf_span:
LIST_INIT for links, attributes, events, baggages, statuses.
conf_scope:
LIST_INIT for acls, contexts, spans, spans_to_finish, instruments,
log_records.
conf_group:
LIST_INIT for ph_scopes.
conf_instrument:
Sets idx and type to OTELC_METRIC_INSTRUMENT_UNSET, aggr_type to
OTELC_METRIC_AGGREGATION_UNSET. LIST_INIT for samples.
conf_log_record:
LIST_INIT for samples.
conf_instr:
Sets rate_limit to FLT_OTEL_FLOAT_U32(100.0) (100%). Calls init_new_proxy
for proxy_log. LIST_INIT for acls, ph_groups, ph_scopes.
Types with no custom body (hdr, str, link, ph, sample_expr, context) rely
entirely on the zeroed OTELC_CALLOC allocation.
5.3 Extended Sample Initialization
The flt_otel_conf_sample_init_ex function (conf.c) provides a higher-level
initialization for sample structures:
1. Verifies sufficient arguments in the args[] array.
2. Calls flt_otel_conf_sample_init with the sample key.
3. Copies extra data (event name string or status code integer).
4. Concatenates remaining arguments into the fmt_string via
flt_otel_args_concat.
5. Counts the number of sample expressions.
This function is used by the parser for span attributes, events, baggages,
statuses, and instrument samples.
5.4 Top-Level Initialization
The flt_otel_conf_init function (conf.c) is hand-written rather than
macro-generated because the root structure does not follow the standard header
pattern:
1. Allocates flt_otel_conf with OTELC_CALLOC.
2. Stores the proxy reference.
3. Initializes the groups and scopes lists.
6 Deallocation
------------------------------------------------------------------------------
6.1 Macro-Generated Free Functions
The FLT_OTEL_CONF_FUNC_FREE macro (conf_funcs.h) generates a function with the
following signature:
void flt_otel_conf_<type>_free(struct flt_otel_conf_<type> **ptr);
The generated function performs these steps:
1. Checks that both <ptr> and <*ptr> are non-NULL.
2. Executes any custom cleanup body provided as the third macro argument.
3. Frees the identifier string with OTELC_SFREE.
4. Removes the structure from its list with FLT_OTEL_LIST_DEL.
5. Frees the structure with OTELC_SFREE_CLEAR and sets <*ptr> to NULL.
6.2 Custom Cleanup Bodies
Custom cleanup runs before the base teardown, allowing child structures to be
freed while the parent is still valid:
conf_sample:
Frees fmt_string. If extra is OTELC_VALUE_DATA, frees the data pointer.
Destroys the exprs list (sample_expr entries). Deinitializes lf_expr via
lf_expr_deinit.
conf_sample_expr:
Releases the HAProxy sample expression via release_sample_expr.
conf_span:
Frees ref_id and ctx_id strings.
Destroys links, attributes, events, baggages, and statuses lists.
conf_instrument:
Frees description, unit, and bounds. Destroys the samples list.
Destroys the attr key-value array via otelc_kv_destroy.
conf_log_record:
Frees event_name and span strings. Destroys the attr key-value array via
otelc_kv_destroy. Destroys the samples list.
conf_scope:
Prunes and frees each ACL entry. Frees the ACL condition via free_acl_cond.
Destroys contexts, spans, spans_to_finish, instruments, and log_records
lists.
conf_group:
Destroys the ph_scopes list.
conf_instr:
Frees the config string. Prunes and frees each ACL entry. Frees each
logger entry from proxy_log.loggers. Destroys the ph_groups and ph_scopes
lists.
Types with no custom cleanup (hdr, str, link, ph, context) only run the base
teardown: free the identifier, unlink, free the structure.
6.3 List Destruction
The FLT_OTEL_LIST_DESTROY(type, head) macro (defined in define.h) iterates a
list and calls flt_otel_conf_<type>_free for each entry. This macro drives the
recursive teardown from parent to leaf.
6.4 Top-Level Deallocation
The flt_otel_conf_free function (conf.c) is hand-written:
1. Frees the id and cfg_file strings.
2. Calls flt_otel_conf_instr_free for the instrumentation.
3. Destroys the groups list (which recursively frees placeholders).
4. Destroys the scopes list (which recursively frees contexts, spans,
instruments, log records, and all their children).
5. Frees the root structure and sets the pointer to NULL.
7 Summary of Init/Free Pairs
------------------------------------------------------------------------------
The following table lists all configuration types and their init/free function
pairs. Types marked "macro" are generated by the FLT_OTEL_CONF_FUNC_(INIT|FREE)
macros. The HDR field column shows which member name is used for the common
header.
Type HDR field Source Custom init body
--------------- --------- ------ -------------------------
conf (none) manual groups, scopes
conf_hdr id macro (none)
conf_str str macro (none)
conf_link span macro (none)
conf_ph id macro (none)
conf_sample_expr fmt_expr macro (none)
conf_sample key macro exprs, lf_expr
conf_sample (ex) key manual extra, fmt_string, exprs
conf_context id macro (none)
conf_span id macro 5 sub-lists
conf_instrument id macro idx, type, samples
conf_log_record id macro samples
conf_scope id macro 6 sub-lists
conf_group id macro ph_scopes
conf_instr id macro rate_limit, proxy_log, acls, ph_groups, ph_scopes

View file

@ -0,0 +1,951 @@
-----------------------------------------
HAProxy OTel filter configuration guide
Version 1.0
( Last update: 2026-03-18 )
-----------------------------------------
Author : Miroslav Zagorac
Contact : mzagorac at haproxy dot com
SUMMARY
--------
1. Overview
2. HAProxy filter declaration
3. OTel configuration file structure
3.1. OTel scope (top-level)
3.2. "otel-instrumentation" section
3.3. "otel-scope" section
3.4. "otel-group" section
4. YAML configuration file
4.1. Exporters
4.2. Samplers
4.3. Processors
4.4. Readers
4.5. Providers
4.6. Signals
5. HAProxy rule integration
6. Complete examples
6.1. Standalone example (sa)
6.2. Frontend / backend example (fe/be)
6.3. Context propagation example (ctx)
6.4. Comparison example (cmp)
6.5. Empty / minimal example (empty)
1. Overview
------------
The OTel filter configuration consists of two files:
1) An OTel configuration file (.cfg) that defines the tracing model: scopes,
groups, spans, attributes, events, instrumentation and log-records.
2) A YAML configuration file (.yml) that configures the OpenTelemetry SDK
pipeline: exporters, samplers, processors, readers, providers and signal
routing.
The OTel configuration file is referenced from the HAProxy configuration using
the 'filter opentelemetry' directive. The YAML file is in turn referenced from
the OTel configuration file using the 'config' keyword inside the
"otel-instrumentation" section.
2. HAProxy filter declaration
------------------------------
The OTel filter requires the 'insecure-fork-wanted' keyword in the HAProxy
'global' section. This is necessary because the OpenTelemetry C++ SDK creates
background threads for data export and batch processing. HAProxy will refuse
to load the configuration if this keyword is missing.
global
insecure-fork-wanted
...
The filter is activated by adding a filter directive in the HAProxy
configuration, in a proxy section (frontend / listen / backend):
frontend my-frontend
...
filter opentelemetry [id <id>] config <otel-cfg-file>
...
If no filter id is specified, 'otel-filter' is used as default. The 'config'
parameter is mandatory and specifies the path to the OTel configuration file.
Example (from test/sa/haproxy.cfg):
frontend otel-test-sa-frontend
bind *:10080
default_backend servers-backend
acl acl-http-status-ok status 100:399
filter opentelemetry id otel-test-sa config sa/otel.cfg
http-response otel-group otel-test-sa http_response_group if acl-http-status-ok
http-after-response otel-group otel-test-sa http_after_response_group if !acl-http-status-ok
backend servers-backend
server server-1 127.0.0.1:8000
3. OTel configuration file structure
--------------------------------------
The OTel configuration file uses a simple section-based format. It contains
three types of sections: one "otel-instrumentation" section (mandatory), zero
or more "otel-scope" sections, and zero or more "otel-group" sections.
3.1. OTel scope (top-level)
-----------------------------
The file is organized into top-level OTel scopes, each identified by a filter
id enclosed in square brackets. The filter id must match the id specified in
the HAProxy 'filter opentelemetry' directive.
[<filter-id>]
otel-instrumentation <name>
...
otel-group <name>
...
otel-scope <name>
...
Multiple OTel scopes (for different filter instances) can coexist in the same
file:
[my-first-filter]
otel-instrumentation instr1
...
[my-second-filter]
otel-instrumentation instr2
...
3.2. "otel-instrumentation" section
-------------------------------------
Exactly one "otel-instrumentation" section must be defined per OTel scope.
It configures the global behavior of the filter and declares which groups
and scopes are active.
Syntax:
otel-instrumentation <name>
Keywords (mandatory):
config <file>
Path to the YAML configuration file for the OpenTelemetry SDK.
Keywords (optional):
acl <aclname> <criterion> [flags] [operator] <value> ...
Declare an ACL. See section 7 of the HAProxy Configuration Manual.
debug-level <value>
Set the debug level bitmask (e.g. 0x77f). Only effective when compiled
with OTEL_DEBUG=1.
groups <name> ...
Declare one or more "otel-group" sections used by this instrumentation.
Can be repeated on multiple lines.
log global
log <addr> [len <len>] [format <fmt>] <facility> [<level> [<minlvl>]]
no log
Enable per-instance logging.
option disabled / no option disabled
Disable or enable the filter. Default: enabled.
option dontlog-normal / no option dontlog-normal
Suppress logging for normal (successful) operations. Default: disabled.
option hard-errors / no option hard-errors
Stop all filter processing in a stream after the first error. Default:
disabled (errors are non-fatal).
rate-limit <value>
Percentage of streams for which the filter is activated. Floating-point
value from 0.0 to 100.0. Default: 100.0.
scopes <name> ...
Declare one or more "otel-scope" sections used by this instrumentation.
Can be repeated on multiple lines.
Example (from test/sa/otel.cfg):
[otel-test-sa]
otel-instrumentation otel-test-instrumentation
debug-level 0x77f
log localhost:514 local7 debug
config sa/otel.yml
option dontlog-normal
option hard-errors
no option disabled
rate-limit 100.0
groups http_response_group
groups http_after_response_group
scopes on_stream_start
scopes on_stream_stop
scopes client_session_start
scopes frontend_tcp_request
...
scopes server_session_end
3.3. "otel-scope" section
---------------------------
An "otel-scope" section defines the actions that take place when a particular
event fires or when a group is triggered.
Syntax:
otel-scope <name>
Supported keywords:
span <name> [parent <ref>] [link <ref>] [root]
Create a new span or reference an already opened one.
- 'root' marks this span as the trace root (only one per trace).
- 'parent <ref>' sets the parent to an existing span or extracted context
name.
- 'link <ref>' adds an inline link to another span or context. Multiple
inline links can be specified within the argument limit.
- If no reference is given, the span becomes a root span.
A span declaration opens a "sub-context" within the scope: the keywords
'link', 'attribute', 'event', 'baggage', 'status' and 'inject' that follow
apply to that span until the next 'span' keyword or the end of the scope.
Examples:
span "HAProxy session" root
span "Client session" parent "HAProxy session"
span "HTTP request" parent "TCP request" link "HAProxy session"
span "Client session" parent "otel_ctx_1"
attribute <key> <sample> ...
Set an attribute on the currently active span. A single sample preserves
its native type; multiple samples are concatenated as a string.
Examples:
attribute "http.method" method
attribute "http.url" url
attribute "http.version" str("HTTP/") req.ver
event <name> <key> <sample> ...
Add a span event (timestamped annotation) to the currently active span.
The data type is always string.
Examples:
event "event_ip" "src" src str(":") src_port
event "event_be" "be" be_id str(" ") be_name
baggage <key> <sample> ...
Set baggage on the currently active span. Baggage propagates to all child
spans. The data type is always string.
Example:
baggage "haproxy_id" var(sess.otel.uuid)
status <code> [<sample> ...]
Set the span status. Valid codes: ignore (default), unset, ok, error.
An optional description follows the code.
Examples:
status "ok"
status "error" str("http.status_code: ") status
link <span> ...
Add non-hierarchical links to the currently active span. Multiple span
names can be specified. Use this keyword for multiple links (the inline
'link' in 'span' is limited to one).
Example:
link "HAProxy session" "Client session"
inject <name-prefix> [use-vars] [use-headers]
Inject span context into an HTTP header carrier and/or HAProxy variables.
The prefix names the context; the special prefix '-' generates the name
automatically. Default storage: use-headers. The 'use-vars' option
requires OTEL_USE_VARS=1 at compile time.
Example:
span "HAProxy session" root
inject "otel_ctx_1" use-headers use-vars
extract <name-prefix> [use-vars | use-headers]
Extract a previously injected span context from an HTTP header or HAProxy
variables. The extracted context can then be used as a parent reference
in 'span ... parent <name-prefix>'.
Example:
extract "otel_ctx_1" use-vars
span "Client session" parent "otel_ctx_1"
finish <name> ...
Close one or more spans or span contexts. Special names:
'*' - finish all open spans
'*req*' - finish all request-channel spans
'*res*' - finish all response-channel spans
Multiple names can be given on one line. A quoted context name after a
span name finishes the associated context as well.
Examples:
finish "Frontend TCP request"
finish "Client session" "otel_ctx_2"
finish *
instrument <type> <name> [aggr <aggregation>] [desc <description>] [unit <unit>] value <sample> [bounds <bounds>]
instrument update <name> [attr <key> <sample> ...]
Create or update a metric instrument.
Supported types:
cnt_int - counter (uint64)
hist_int - histogram (uint64)
udcnt_int - up-down counter (int64)
gauge_int - gauge (int64)
Supported aggregation types:
drop - measurements are discarded
histogram - explicit bucket histogram
last_value - last recorded value
sum - sum of recorded values
default - SDK default for the instrument type
exp_histogram - base-2 exponential histogram
An aggregation type can be specified using the 'aggr' keyword. When
specified, a metrics view is registered with the given aggregation
strategy. If omitted, the SDK default is used.
For histogram instruments (hist_int), optional bucket boundaries can be
specified using the 'bounds' keyword followed by a double-quoted string
of space-separated numbers (order does not matter; values are sorted
internally). When bounds are specified without an explicit aggregation
type, histogram aggregation is used automatically.
Observable (asynchronous) and double-precision types are not supported.
Observable instrument callbacks are invoked by the OTel SDK from an
external background thread; HAProxy sample fetches rely on internal
per-thread-group state and return incorrect results from a non-HAProxy
thread. Double-precision types are not supported because HAProxy sample
fetches do not return double values.
Examples:
instrument cnt_int "name_cnt_int" desc "Integer Counter" value int(1),add(2) unit "unit"
instrument hist_int "name_hist" aggr exp_histogram desc "Latency" value lat_ns_tot unit "ns"
instrument hist_int "name_hist2" desc "Latency" value lat_ns_tot unit "ns" bounds "100 1000 10000"
instrument update "name_cnt_int" attr "attr_1_key" str("attr_1_value")
log-record <severity> [id <integer>] [event <name>] [span <span-name>] [attr <key> <sample>] ... <sample> ...
Emit an OpenTelemetry log record. The first argument is a required
severity level. Optional keywords follow in any order:
id <integer> - numeric event identifier
event <name> - event name string
span <span-name> - associate the log record with an open span
attr <key> <sample> - add an attribute evaluated at runtime (repeatable)
The 'attr' keyword takes an attribute name and a single HAProxy sample
expression. The expression is evaluated at runtime, following the same
rules as span attributes: a bare sample fetch (e.g. src) or a log-format
string (e.g. "%[src]:%[src_port]").
The remaining arguments at the end are sample fetch expressions that form
the log record body. A single sample preserves its native type; multiple
samples are concatenated as a string.
Supported severity levels follow the OpenTelemetry specification:
trace, trace2, trace3, trace4
debug, debug2, debug3, debug4
info, info2, info3, info4
warn, warn2, warn3, warn4
error, error2, error3, error4
fatal, fatal2, fatal3, fatal4
The log record is only emitted if the logger is enabled for the configured
severity (controlled by the 'min_severity' option in the YAML logs signal
configuration). If a 'span' reference is given but the named span is not
found at runtime, the log record is emitted without span correlation.
Examples:
log-record info str("heartbeat")
log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" method method url
log-record trace id 1000 event "session-start" span "Client session" attr "src_ip" src attr "src_port" src_port src str(":") src_port
log-record warn event "server-unavailable" str("503 Service Unavailable")
log-record info event "session-stop" str("stream stopped")
acl <aclname> <criterion> [flags] [operator] <value> ...
Declare an ACL local to this scope.
Example:
acl acl-test-src-ip src 127.0.0.1
otel-event <name> [{ if | unless } <condition>]
Bind this scope to a filter event, optionally with an ACL-based condition.
Supported events (stream lifecycle):
on-stream-start
on-stream-stop
on-idle-timeout
on-backend-set
Supported events (request channel):
on-client-session-start
on-frontend-tcp-request
on-http-wait-request
on-http-body-request
on-frontend-http-request
on-switching-rules-request
on-backend-tcp-request
on-backend-http-request
on-process-server-rules-request
on-http-process-request
on-tcp-rdp-cookie-request
on-process-sticking-rules-request
on-http-headers-request
on-http-end-request
on-client-session-end
on-server-unavailable
Supported events (response channel):
on-server-session-start
on-tcp-response
on-http-wait-response
on-process-store-rules-response
on-http-response
on-http-headers-response
on-http-end-response
on-http-reply
on-server-session-end
The on-stream-start event fires from the stream_start filter callback,
before any channel processing begins. The on-stream-stop event fires from
the stream_stop callback, after all channel processing ends. No channel
is available at that point, so context injection/extraction via HTTP
headers cannot be used in scopes bound to these events.
The on-idle-timeout event fires periodically when the stream has no data
transfer activity. It requires the 'idle-timeout' keyword to set the
interval. Scopes bound to this event can create heartbeat spans, record
idle-time metrics, and emit idle-time log records.
The on-backend-set event fires when a backend is assigned to the stream.
It is not called if the frontend and backend are the same.
The on-http-headers-request and on-http-headers-response events fire after
all HTTP headers have been parsed and analyzed.
The on-http-end-request and on-http-end-response events fire when all HTTP
data has been processed and forwarded.
The on-http-reply event fires when HAProxy generates an internal reply
(error page, deny response, redirect).
Examples:
otel-event on-stream-start if acl-test-src-ip
otel-event on-stream-stop
otel-event on-client-session-start
otel-event on-client-session-start if acl-test-src-ip
otel-event on-http-response if !acl-http-status-ok
otel-event on-idle-timeout
idle-timeout <time>
Set the idle timeout interval for a scope bound to the 'on-idle-timeout'
event. The timer fires periodically at the given interval when the stream
has no data transfer activity. This keyword is mandatory for scopes using
the 'on-idle-timeout' event and cannot be used with any other event.
The <time> argument accepts the standard HAProxy time format: a number
followed by a unit suffix (ms, s, m, h, d). A value of zero is not
permitted.
Example:
scopes on_idle_timeout
..
otel-scope on_idle_timeout
idle-timeout 5s
span "heartbeat" root
attribute "idle.elapsed" str("idle-check")
instrument cnt_int "idle.count" value int(1)
log-record info str("heartbeat")
otel-event on-idle-timeout
3.4. "otel-group" section
---------------------------
An "otel-group" section defines a named collection of scopes that can be
triggered from HAProxy TCP/HTTP rules rather than from filter events.
Syntax:
otel-group <name>
Keywords:
scopes <name> ...
List the "otel-scope" sections that belong to this group. Multiple names
can be given on one line. Scopes that are used only in groups do not need
to define an 'otel-event'.
Example (from test/sa/otel.cfg):
otel-group http_response_group
scopes http_response_1
scopes http_response_2
otel-scope http_response_1
span "HTTP response"
event "event_content" "hdr.content" res.hdr("content-type") str("; length: ") res.hdr("content-length") str(" bytes")
otel-scope http_response_2
span "HTTP response"
event "event_date" "hdr.date" res.hdr("date") str(" / ") res.hdr("last-modified")
4. YAML configuration file
----------------------------
The YAML configuration file defines the OpenTelemetry SDK pipeline. It is
referenced by the 'config' keyword in the "otel-instrumentation" section.
It contains the following top-level sections: exporters, samplers, processors,
readers, providers and signals.
4.1. Exporters
---------------
Each exporter has a user-chosen name and a 'type' that determines which
additional options are available. Options marked with (*) are required.
Supported types:
otlp_grpc - Export via OTLP over gRPC.
type (*) "otlp_grpc"
thread_name exporter thread name (string)
endpoint OTLP/gRPC endpoint URL (string)
use_ssl_credentials enable SSL channel credentials (boolean)
ssl_credentials_cacert_path CA certificate file path (string)
ssl_credentials_cacert_as_string CA certificate as inline string (string)
ssl_client_key_path client private key file path (string)
ssl_client_key_string client private key as inline string (string)
ssl_client_cert_path client certificate file path (string)
ssl_client_cert_string client certificate as inline string (string)
timeout export timeout in seconds (integer)
user_agent User-Agent header value (string)
max_threads maximum exporter threads (integer)
compression compression algorithm name (string)
max_concurrent_requests concurrent request limit (integer)
otlp_http - Export via OTLP over HTTP (JSON or Protobuf).
type (*) "otlp_http"
thread_name exporter thread name (string)
endpoint OTLP/HTTP endpoint URL (string)
content_type payload format: "json" or "protobuf"
json_bytes_mapping binary encoding: "hexid", "utf8" or "base64"
debug enable debug output (boolean)
timeout export timeout in seconds (integer)
http_headers custom HTTP headers (list of key: value)
max_concurrent_requests concurrent request limit (integer)
max_requests_per_connection request limit per connection (integer)
ssl_insecure_skip_verify skip TLS certificate verification (boolean)
ssl_ca_cert_path CA certificate file path (string)
ssl_ca_cert_string CA certificate as inline string (string)
ssl_client_key_path client private key file path (string)
ssl_client_key_string client private key as inline string (string)
ssl_client_cert_path client certificate file path (string)
ssl_client_cert_string client certificate as inline string (string)
ssl_min_tls minimum TLS version (string)
ssl_max_tls maximum TLS version (string)
ssl_cipher TLS cipher list (string)
ssl_cipher_suite TLS 1.3 cipher suite list (string)
compression compression algorithm name (string)
otlp_file - Export to local files in OTLP format.
type (*) "otlp_file"
thread_name exporter thread name (string)
file_pattern output filename pattern (string)
alias_pattern symlink pattern for latest file (string)
flush_interval flush interval in microseconds (integer)
flush_count spans per flush (integer)
file_size maximum file size in bytes (integer)
rotate_size number of rotated files to keep (integer)
ostream - Write to a file (text output, useful for debugging).
type (*) "ostream"
filename output file path (string)
memory - In-memory buffer (useful for testing).
type (*) "memory"
buffer_size maximum buffered items (integer)
zipkin - Export to Zipkin-compatible backends.
type (*) "zipkin"
endpoint Zipkin collector URL (string)
format payload format: "json" or "protobuf"
service_name service name reported to Zipkin (string)
ipv4 service IPv4 address (string)
ipv6 service IPv6 address (string)
elasticsearch - Export to Elasticsearch.
type (*) "elasticsearch"
host Elasticsearch hostname (string)
port Elasticsearch port (integer)
index Elasticsearch index name (string)
response_timeout response timeout in seconds (integer)
debug enable debug output (boolean)
http_headers custom HTTP headers (list of key: value)
4.2. Samplers
--------------
Samplers control which traces are recorded. Each sampler has a user-chosen
name and a 'type' that determines its behavior.
Supported types:
always_on - Sample every trace.
type (*) "always_on"
always_off - Sample no traces.
type (*) "always_off"
trace_id_ratio_based - Sample a fraction of traces.
type (*) "trace_id_ratio_based"
ratio sampling ratio, 0.0 to 1.0 (float)
parent_based - Inherit sampling decision from parent span.
type (*) "parent_based"
delegate fallback sampler name (string)
4.3. Processors
----------------
Processors define how telemetry data is handled before export. Each
processor has a user-chosen name and a 'type' that determines its behavior.
Supported types:
batch - Batch spans before exporting.
type (*) "batch"
thread_name processor thread name (string)
max_queue_size maximum queued spans (integer)
schedule_delay export interval in milliseconds (integer)
max_export_batch_size maximum spans per export call (integer)
When the queue reaches half capacity, a preemptive notification triggers
an early export.
single - Export each span individually (no batching).
type (*) "single"
4.4. Readers
-------------
Readers define how metrics are collected and exported. Each reader has a
user-chosen name.
thread_name reader thread name (string)
export_interval collection interval in milliseconds (integer)
export_timeout export timeout in milliseconds (integer)
4.5. Providers
---------------
Providers define resource attributes attached to all telemetry data. Each
provider has a user-chosen name.
resources key-value resource attributes (list)
Standard resource attribute keys include service.name, service.version,
service.instance.id and service.namespace.
4.6. Signals
-------------
Signals bind exporters, samplers, processors, readers and providers together
for each telemetry type. The supported signal names are "traces", "metrics"
and "logs".
scope_name instrumentation scope name (string)
exporters exporter name reference (string)
samplers sampler name reference (string, traces only)
processors processor name reference (string, traces/logs)
readers reader name reference (string, metrics only)
providers provider name reference (string)
min_severity minimum log severity level (string, logs only)
The "min_severity" option controls which log records are emitted. Only log
records whose severity is equal to or higher than the configured minimum are
passed to the exporter. The value is a severity name as listed under the
"log-record" keyword (e.g. "trace", "debug", "info", "warn", "error", "fatal").
If omitted, the logger accepts all severity levels.
5. HAProxy rule integration
----------------------------
Groups defined in the OTel configuration file can be triggered from HAProxy
TCP/HTTP rules using the 'otel-group' action keyword:
http-request otel-group <filter-id> <group> [condition]
http-response otel-group <filter-id> <group> [condition]
http-after-response otel-group <filter-id> <group> [condition]
tcp-request otel-group <filter-id> <group> [condition]
tcp-response otel-group <filter-id> <group> [condition]
This allows running specific groups of scopes based on ACL conditions defined
in the HAProxy configuration.
Example (from test/sa/haproxy.cfg):
acl acl-http-status-ok status 100:399
filter opentelemetry id otel-test-sa config sa/otel.cfg
# Run response scopes for successful responses
http-response otel-group otel-test-sa http_response_group if acl-http-status-ok
# Run after-response scopes for error responses
http-after-response otel-group otel-test-sa http_after_response_group if !acl-http-status-ok
6. Complete examples
---------------------
The test directory contains several complete example configurations. Each
subdirectory contains an OTel configuration file (otel.cfg), a YAML file
(otel.yml) and a HAProxy configuration file (haproxy.cfg).
6.1. Standalone example (sa)
------------------------------
The most comprehensive example. All possible events are used, with spans,
attributes, events, links, baggage, status, metrics and groups demonstrated.
--- test/sa/otel.cfg (excerpt) -----------------------------------------
[otel-test-sa]
otel-instrumentation otel-test-instrumentation
config sa/otel.yml
option dontlog-normal
option hard-errors
no option disabled
rate-limit 100.0
groups http_response_group
groups http_after_response_group
scopes on_stream_start
scopes on_stream_stop
scopes client_session_start
scopes frontend_tcp_request
...
scopes server_session_end
otel-group http_response_group
scopes http_response_1
scopes http_response_2
otel-scope http_response_1
span "HTTP response"
event "event_content" "hdr.content" res.hdr("content-type") str("; length: ") res.hdr("content-length") str(" bytes")
otel-scope on_stream_start
instrument udcnt_int "haproxy.sessions.active" desc "Active sessions" value int(1) unit "{session}"
span "HAProxy session" root
baggage "haproxy_id" var(sess.otel.uuid)
event "event_ip" "src" src str(":") src_port
acl acl-test-src-ip src 127.0.0.1
otel-event on-stream-start if acl-test-src-ip
otel-scope on_stream_stop
finish *
otel-event on-stream-stop
otel-scope client_session_start
span "Client session" parent "HAProxy session"
otel-event on-client-session-start
otel-scope frontend_http_request
span "Frontend HTTP request" parent "HTTP body request" link "HAProxy session"
attribute "http.method" method
attribute "http.url" url
attribute "http.version" str("HTTP/") req.ver
finish "HTTP body request"
otel-event on-frontend-http-request
otel-scope server_session_start
span "Server session" parent "HAProxy session"
link "HAProxy session" "Client session"
finish "Process sticking rules request"
otel-event on-server-session-start
otel-scope server_session_end
finish *
otel-event on-server-session-end
---------------------------------------------------------------------
6.2. Frontend / backend example (fe/be)
-----------------------------------------
Demonstrates distributed tracing across two cascaded HAProxy instances using
inject/extract to propagate the span context via HTTP headers.
The frontend HAProxy (test/fe) creates the root trace and injects context:
--- test/fe/otel.cfg (excerpt) -----------------------------------------
otel-scope backend_http_request
span "Backend HTTP request" parent "Backend TCP request"
finish "Backend TCP request"
span "HAProxy session"
inject "otel-ctx" use-headers
otel-event on-backend-http-request
---------------------------------------------------------------------
The backend HAProxy (test/be) extracts the context and continues the trace:
--- test/be/otel.cfg (excerpt) -----------------------------------------
otel-scope frontend_http_request
extract "otel-ctx" use-headers
span "HAProxy session" parent "otel-ctx" root
baggage "haproxy_id" var(sess.otel.uuid)
span "Client session" parent "HAProxy session"
span "Frontend HTTP request" parent "Client session"
attribute "http.method" method
attribute "http.url" url
attribute "http.version" str("HTTP/") req.ver
otel-event on-frontend-http-request
---------------------------------------------------------------------
6.3. Context propagation example (ctx)
----------------------------------------
Similar to 'sa', but spans are opened using extracted span contexts as parent
references instead of direct span names. This demonstrates the inject/extract
mechanism using HAProxy variables.
--- test/ctx/otel.cfg (excerpt) ----------------------------------------
otel-scope client_session_start_1
span "HAProxy session" root
inject "otel_ctx_1" use-headers use-vars
baggage "haproxy_id" var(sess.otel.uuid)
otel-event on-client-session-start
otel-scope client_session_start_2
extract "otel_ctx_1" use-vars
span "Client session" parent "otel_ctx_1"
inject "otel_ctx_2" use-headers use-vars
otel-event on-client-session-start
otel-scope frontend_tcp_request
extract "otel_ctx_2" use-vars
span "Frontend TCP request" parent "otel_ctx_2"
inject "otel_ctx_3" use-headers use-vars
otel-event on-frontend-tcp-request
otel-scope http_wait_request
extract "otel_ctx_3" use-vars
span "HTTP wait request" parent "otel_ctx_3"
finish "Frontend TCP request" "otel_ctx_3"
otel-event on-http-wait-request
---------------------------------------------------------------------
6.4. Comparison example (cmp)
-------------------------------
A configuration made for comparison purposes with other tracing implementations.
It uses a simplified span hierarchy without context propagation.
--- test/cmp/otel.cfg (excerpt) ----------------------------------------
otel-scope client_session_start
span "HAProxy session" root
baggage "haproxy_id" var(sess.otel.uuid)
span "Client session" parent "HAProxy session"
otel-event on-client-session-start
otel-scope http_response-error
span "HTTP response"
status "error" str("!acl-http-status-ok")
otel-event on-http-response if !acl-http-status-ok
otel-scope server_session_end
finish "HTTP response" "Server session"
otel-event on-http-response
otel-scope client_session_end
finish "*"
otel-event on-http-response
---------------------------------------------------------------------
6.5. Empty / minimal example (empty)
--------------------------------------
The minimal valid OTel configuration. The filter is initialized but no events
are triggered:
--- test/empty/otel.cfg -------------------------------------------------
otel-instrumentation otel-test-instrumentation
config empty/otel.yml
---------------------------------------------------------------------
This is useful for testing the OTel filter initialization behavior without any
actual telemetry processing.

725
addons/otel/README-design Normal file
View file

@ -0,0 +1,725 @@
OpenTelemetry filter -- design patterns
==========================================================================
This document describes the cross-cutting design patterns used throughout
the OTel filter implementation. It complements README-implementation
(component-by-component architecture) with a pattern-oriented view of the
mechanisms that span multiple source files.
1 X-Macro Code Generation
----------------------------------------------------------------------
The filter uses X-macro lists extensively to generate enums, keyword tables,
event metadata and configuration init/free functions from a single definition.
Each X-macro list is a preprocessor define containing repeated invocations of
a helper macro whose name is supplied by the expansion context.
1.1 Event Definitions
FLT_OTEL_EVENT_DEFINES (event.h) drives the event system. Each entry has the
form:
FLT_OTEL_EVENT_DEF(NAME, CHANNEL, REQ_AN, RES_AN, HTTP_INJ, "string")
The same list is expanded twice:
- In enum FLT_OTEL_EVENT_enum (event.h) to produce symbolic constants
(FLT_OTEL_EVENT_NONE, FLT_OTEL_EVENT_REQ_STREAM_START, etc.) followed by
the FLT_OTEL_EVENT_MAX sentinel.
- In the flt_otel_event_data[] initializer (event.c) to produce a const table
of struct flt_otel_event_data entries carrying the analyzer bit, sample
fetch direction, valid fetch locations, HTTP injection flag and event name
string.
Adding a new event requires a single line in the X-macro list.
1.2 Parser Keyword Definitions
Three X-macro lists drive the configuration parser:
FLT_OTEL_PARSE_INSTR_DEFINES (parser.h, 9 keywords)
FLT_OTEL_PARSE_GROUP_DEFINES (parser.h, 2 keywords)
FLT_OTEL_PARSE_SCOPE_DEFINES (parser.h, 15 keywords)
Each entry has the form:
FLT_OTEL_PARSE_DEF(ENUM, flag, check, min, max, "name", "usage")
Each list is expanded to:
- An enum for keyword indices (FLT_OTEL_PARSE_INSTR_ID, etc.).
- A static parse_data[] table of struct flt_otel_parse_data entries carrying
the keyword index, flag_check_id, check_name type, argument count bounds,
keyword name string and usage string.
The section parsers (flt_otel_parse_cfg_instr, _group, _scope) look up args[0]
in their parse_data table via flt_otel_parse_cfg_check(), which validates
argument counts and character constraints before dispatching to
the keyword-specific handler.
Additional X-macro lists generate:
FLT_OTEL_PARSE_SCOPE_STATUS_DEFINES Span status codes.
FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEFINES Instrument type keywords.
FLT_OTEL_HTTP_METH_DEFINES HTTP method name table.
FLT_OTEL_GROUP_DEFINES Group action metadata.
1.3 Configuration Init/Free Generation
Two macros in conf_funcs.h generate init and free functions for all
configuration structure types:
FLT_OTEL_CONF_FUNC_INIT(_type_, _id_, _func_)
FLT_OTEL_CONF_FUNC_FREE(_type_, _id_, _func_)
The _type_ parameter names the structure (e.g. "span"), _id_ names the
identifier field within the FLT_OTEL_CONF_HDR (e.g. "id", "key", "str"), and
_func_ is a brace-enclosed code block executed after the boilerplate allocation
(for init) or before the boilerplate teardown (for free).
The generated init function:
1. Validates the identifier is non-NULL and non-empty.
2. Checks the length against FLT_OTEL_ID_MAXLEN (64).
3. Detects duplicates in the target list.
4. Handles auto-generated IDs (FLT_OTEL_CONF_HDR_SPECIAL prefix).
5. Allocates with OTELC_CALLOC, duplicates the ID with OTELC_STRDUP.
6. Appends to the parent list via LIST_APPEND.
7. Executes the custom _func_ body.
The generated free function:
1. Executes the custom _func_ body (typically list destruction).
2. Frees the identifier string.
3. Removes the node from its list.
4. Frees the structure with OTELC_SFREE_CLEAR.
conf.c instantiates these macros for all 13 configuration types: hdr, str, ph,
sample_expr, sample, link, context, span, scope, instrument, log_record, group,
instr. The more complex types (span, scope, instr) carry non-trivial _func_
bodies that initialize sub-lists or set default values.
2 Type-Safe Generic Macros
----------------------------------------------------------------------
define.h provides utility macros that use typeof() and statement expressions
for type-safe generic programming without C++ templates.
2.1 Safe Pointer Dereferencing
FLT_OTEL_PTR_SAFE(a, b)
Returns 'a' if non-NULL, otherwise the default value 'b'. Uses typeof(*(a))
to verify type compatibility at compile time.
FLT_OTEL_DEREF(p, m, v)
Safely dereferences p->m, returning 'v' if 'p' is NULL.
FLT_OTEL_DDEREF(a, m, v)
Safely double-dereferences *a->m, returning 'v' if either 'a' or '*a' is
NULL.
These macros eliminate repetitive NULL checks while preserving the correct
pointer types through typeof().
2.2 String Comparison
FLT_OTEL_STR_CMP(S, s)
Compares a runtime string 's' against a compile-time string literal 'S'.
Expands to a call with the literal's address and computed length, avoiding
repeated strlen() on known constants.
FLT_OTEL_CONF_STR_CMP(s, S)
Compares two configuration strings using their cached length fields (from
FLT_OTEL_CONF_STR / FLT_OTEL_CONF_HDR). Falls through to memcmp() only when
lengths match, providing an early-out fast path.
2.3 List Operations
FLT_OTEL_LIST_ISVALID(a)
Checks whether a list head has been initialized (both prev and next are
non-NULL).
FLT_OTEL_LIST_DEL(a)
Safely deletes a list element only if the list head is valid.
FLT_OTEL_LIST_DESTROY(t, h)
Destroys all elements in a typed configuration list by iterating with
list_for_each_entry_safe and calling flt_otel_conf_<t>_free() for each
entry. The type 't' is used to derive both the structure type and the
free function name.
2.4 Thread-Local Rotating Buffers
FLT_OTEL_BUFFER_THR(b, m, n, p)
Declares a thread-local pool of 'n' string buffers, each of size 'm'.
The pool index rotates on each invocation, avoiding the need for explicit
allocation in debug formatting functions. Each call returns a pointer to
the next buffer via the output parameter 'p'.
Used primarily in debug-only functions (flt_otel_analyzer,
flt_otel_list_dump) where temporary strings must survive until their caller
finishes with them.
3 Error Handling Architecture
----------------------------------------------------------------------
3.1 Error Accumulation Macros
Three macros in define.h manage error messages:
FLT_OTEL_ERR(f, ...)
Formats an error message via memprintf() into the caller's char **err
pointer, but only if *err is still NULL. Prevents overwriting the first
error with a cascading secondary error.
FLT_OTEL_ERR_APPEND(f, ...)
Unconditionally appends to the error message regardless of prior content.
Used when a function must report multiple issues.
FLT_OTEL_ERR_FREE(p)
Logs the accumulated error message at WARNING level via FLT_OTEL_ALERT,
then frees the string and NULLs the pointer.
All source functions that can fail accept a char **err parameter. The
convention is: set *err on error, leave it alone on success. Callers check
both the return value and *err to detect problems.
3.2 Parse-Time Error Reporting
Configuration parsing (parser.c) uses additional macros:
FLT_OTEL_PARSE_ERR(e, f, ...)
Sets the error string and ORs ERR_ABORT | ERR_ALERT into the return value.
Used inside section parser switch statements.
FLT_OTEL_PARSE_ALERT(f, ...)
Issues a ha_alert() with the current file and line, then sets the error
flags. Used when the error message should appear immediately.
FLT_OTEL_POST_PARSE_ALERT(f, ...)
Same as PARSE_ALERT but uses cfg_file instead of file, for post-parse
validation that runs after the parser has moved on.
FLT_OTEL_PARSE_IFERR_ALERT()
Checks whether *err was set by a called function, and if so, issues an
alert and clears the error. This bridges between functions that use the
char **err convention and the parser's own alert mechanism.
3.3 Runtime Error Modes
Two helper functions in filter.c implement the runtime error policy:
flt_otel_return_int(f, err, retval)
flt_otel_return_void(f, err)
If an error is detected (retval == FLT_OTEL_RET_ERROR or *err != NULL):
Hard-error mode (rt_ctx->flag_harderr):
Sets rt_ctx->flag_disabled = 1, disabling the filter for the remainder of
the stream. Atomically increments the disabled counter. The error is
logged as "filter hard-error (disabled)".
Soft-error mode (default):
The error is logged as "filter soft-error" and processing continues. The
tracing data for the current event may be incomplete but the stream is not
affected.
In both modes, FLT_OTEL_RET_OK is always returned to HAProxy. A tracing failure
never interrupts stream processing.
3.4 Disabled State Checking
flt_otel_is_disabled() (filter.c) is called at the top of every filter callback
that processes events. It checks three conditions:
1. rt_ctx is NULL (filter was never attached or already detached).
2. rt_ctx->flag_disabled is set (hard-error disabled the filter).
3. conf->instr->flag_disabled is set (globally disabled via CLI).
All three are loaded via _HA_ATOMIC_LOAD() for thread safety. When disabled,
the callback returns immediately without processing.
4 Thread Safety Model
----------------------------------------------------------------------
The filter runs within HAProxy's multi-threaded worker model. Each stream is
bound to a single thread, so per-stream state (the runtime context, spans and
contexts) is accessed without locking. Cross-stream shared state uses atomic
operations.
4.1 Atomic Fields
The following fields are accessed atomically across threads:
conf->instr->flag_disabled Toggled by CLI "otel enable/disable". Checked in
flt_otel_ops_attach() and flt_otel_is_disabled().
conf->instr->flag_harderr Toggled by CLI "otel hard-errors/soft-errors".
Copied to rt_ctx at attach time.
conf->instr->rate_limit Set by CLI "otel rate".
Read in flt_otel_ops_attach().
conf->instr->logging Set by CLI "otel logging".
Copied to rt_ctx at attach time.
flt_otel_drop_cnt Incremented in flt_otel_log_handler_cb().
Read by CLI "otel status".
conf->cnt.disabled[] Incremented in flt_otel_return_int/void().
conf->cnt.attached[] Incremented in flt_otel_ops_attach().
All use _HA_ATOMIC_LOAD(), _HA_ATOMIC_STORE(), _HA_ATOMIC_INC() or
_HA_ATOMIC_ADD() from HAProxy's atomic primitives.
4.2 Metric Instrument Creation (CAS Pattern)
Metric instruments are shared across all threads but created lazily on first
use. The creation uses a compare-and-swap pattern to ensure exactly one thread
performs the creation:
int64_t expected = OTELC_METRIC_INSTRUMENT_UNSET; /* -1 */
if (HA_ATOMIC_CAS(&instr->idx, &expected, OTELC_METRIC_INSTRUMENT_PENDING)) {
/* This thread won the race -- create the instrument. */
idx = meter->create_instrument(...);
HA_ATOMIC_STORE(&instr->idx, idx);
}
The three states are:
UNSET (-1) Not yet created. The CAS target.
PENDING (-2) Creation in progress by another thread.
>= 0 Valid meter index. Ready for recording.
Threads that lose the CAS skip creation. Update-form instruments check that the
referenced create-form's idx is >= 0 before recording; if it is still PENDING or
UNSET, the measurement is silently skipped.
4.3 Per-Thread Initialization Guard
flt_otel_ops_init_per_thread() uses the FLT_CFG_FL_HTX flag on fconf as a
one-shot guard: the tracer, meter and logger background threads are started only
on the first call. Subsequent calls from other proxies sharing the same filter
configuration skip the start sequence.
4.4 CLI Update Propagation
CLI set commands (cli.c) iterate all OTel filter instances across all proxies
using the FLT_OTEL_PROXIES_LIST_START / FLT_OTEL_PROXIES_LIST_END macros
(util.h) and atomically update the target field on each instance. This ensures
that "otel disable" takes effect globally, not just on one proxy.
5 Sample Evaluation Pipeline
----------------------------------------------------------------------
Sample expressions bridge HAProxy's runtime data (headers, variables, connection
properties) into OTel attributes, events, baggage, status, metric values and log
record bodies.
5.1 Two Evaluation Paths
The parser detects which path to use based on the presence of "%[" in the sample
value argument:
Log-format path (sample->lf_used == true):
The value is parsed via parse_logformat_string() and stored in
sample->lf_expr. At runtime, build_logline() evaluates the expression into
a temporary buffer of global.tune.bufsize bytes. The result is always a
string.
Bare sample path (sample->lf_used == false):
Each whitespace-separated token is parsed via sample_parse_expr() and
stored as an flt_otel_conf_sample_expr in sample->exprs. At runtime, each
expression is evaluated via sample_process(). If there is exactly one
expression, the native HAProxy sample type is preserved (bool, int, IP
address, string). If there are multiple expressions, their string
representations are concatenated.
5.2 Type Conversion Chain
flt_otel_sample_to_value() (util.c) converts HAProxy sample types to OTel value
types:
SMP_T_BOOL -> OTELC_VALUE_BOOL
SMP_T_SINT -> OTELC_VALUE_INT64
Other -> OTELC_VALUE_DATA (string, via flt_otel_sample_to_str)
flt_otel_sample_to_str() handles the string conversion for non-trivial types:
SMP_T_BOOL "0" or "1"
SMP_T_SINT snprintf decimal
SMP_T_IPV4 inet_ntop
SMP_T_IPV6 inet_ntop
SMP_T_STR direct copy
SMP_T_METH lookup from static HTTP method table, or raw string for
HTTP_METH_OTHER
For metric instruments, string values are rejected -- the meter requires
OTELC_VALUE_INT64. If the sample evaluates to a string, otelc_value_strtonum()
attempts numeric conversion as a last resort.
5.3 Dispatch to Data Structures
flt_otel_sample_add() (util.c) is the top-level evaluator. After converting the
sample to an otelc_value, it dispatches based on type:
FLT_OTEL_EVENT_SAMPLE_ATTRIBUTE
-> flt_otel_sample_add_kv(&data->attributes, key, &value)
FLT_OTEL_EVENT_SAMPLE_BAGGAGE
-> flt_otel_sample_add_kv(&data->baggage, key, &value)
FLT_OTEL_EVENT_SAMPLE_EVENT
-> flt_otel_sample_add_event(&data->events, sample, &value)
Groups attributes by event name; creates a new flt_otel_scope_data_event
node if the event name is not yet present.
FLT_OTEL_EVENT_SAMPLE_STATUS
-> flt_otel_sample_set_status(&data->status, sample, &value, err)
Sets the status code from sample->extra.num and the description from the
evaluated value. Rejects duplicate status settings.
5.4 Growable Key-Value Arrays
Attribute and baggage arrays use a lazy-allocation / grow-on-demand pattern via
flt_otel_sample_add_kv() (util.c):
- Initial allocation: FLT_OTEL_ATTR_INIT_SIZE (8) elements via otelc_kv_new().
- Growth: FLT_OTEL_ATTR_INC_SIZE (4) additional elements via otelc_kv_resize()
when the array is full.
- The cnt field tracks used elements; size tracks total capacity.
Event attribute arrays follow the same pattern within each
flt_otel_scope_data_event node.
6 Rate Limiting and Filtering
----------------------------------------------------------------------
6.1 Rate Limit Representation
The rate limit is configured as a floating-point percentage (0.0 to 100.0) but
stored internally as a uint32_t for lock-free comparison:
FLT_OTEL_FLOAT_U32(a) Converts a percentage to a uint32 value:
(uint32_t)((a / 100.0) * UINT32_MAX)
FLT_OTEL_U32_FLOAT(a) Converts back for display:
((double)(a) / UINT32_MAX) * 100.0
At attach time, the filter compares a fresh ha_random32() value against the
stored rate_limit. If the random value exceeds the threshold, the filter
returns FLT_OTEL_RET_IGNORE and does not attach to the stream. This provides
uniform sampling without floating-point arithmetic on the hot path.
6.2 ACL-Based Filtering
Each scope may carry an ACL condition (if/unless) evaluated at scope
execution time via acl_exec_cond(). ACL references are resolved through
a priority chain in flt_otel_parse_acl() (parser.c):
1. Scope-local ACLs (declared within the otel-scope section).
2. Instrumentation ACLs (declared in the otel-instrumentation section).
3. Proxy ACLs (declared in the HAProxy frontend/backend section).
The first list that produces a successful build_acl_cond() result is used.
If the condition fails at runtime:
- If the scope contains a root span, the entire stream is disabled
(rt_ctx->flag_disabled = 1) to avoid orphaned child spans.
- Otherwise, the scope is simply skipped.
6.3 Global Disable
The filter can be disabled globally via the CLI ("otel disable") or the
configuration ("option disabled"). The flag_disabled field on the
instrumentation configuration is checked atomically in flt_otel_ops_attach()
before any per-stream allocation occurs.
7 Memory Management Strategy
----------------------------------------------------------------------
7.1 Pool-Based Allocation
Four HAProxy memory pools are registered for frequently allocated structures
(pool.c):
pool_head_otel_scope_span Per-stream span entries.
pool_head_otel_scope_context Per-stream context entries.
pool_head_otel_runtime_context Per-stream runtime context.
pool_head_otel_span_context OTel SDK objects (via C wrapper allocator
callback).
Pool registration is conditional on compile-time flags (USE_POOL_OTEL_* in
config.h). flt_otel_pool_alloc() attempts pool allocation first and falls back
to heap allocation (OTELC_MALLOC) when the pool is NULL or the requested size
exceeds the pool's element size.
The C wrapper library's memory allocations are redirected through
flt_otel_mem_malloc() / flt_otel_mem_free() (filter.c), which use the
otel_span_context pool via otelc_ext_init(). This keeps OTel SDK objects in
HAProxy's pool allocator for cache-friendly allocation.
7.2 Trash Buffers
flt_otel_trash_alloc() (pool.c) provides temporary scratch buffers of
global.tune.bufsize bytes. When USE_TRASH_CHUNK is defined, it uses HAProxy's
alloc_trash_chunk(); otherwise it manually allocates a buffer structure and data
area. Trash buffers are used for:
- Log-format evaluation in metric recording and log record emission.
- HTTP header name construction in flt_otel_http_header_set().
- Temporary string assembly in sample evaluation.
7.3 Scope Data Lifecycle
flt_otel_scope_data (scope.h) is initialized and freed within a single span
processing block in flt_otel_scope_run(). It is stack-conceptual data:
allocated at the start of each span's processing, populated during sample
evaluation, consumed by flt_otel_scope_run_span(), and freed immediately after.
The key-value arrays within it are heap-allocated via otelc_kv_new() and freed
via otelc_kv_destroy().
8 Context Propagation Mechanism
----------------------------------------------------------------------
8.1 Carrier Abstraction
The OTel C wrapper uses a carrier pattern for context propagation. Two carrier
types exist, each with writer and reader variants:
otelc_text_map_writer / otelc_text_map_reader
otelc_http_headers_writer / otelc_http_headers_reader
otelc.c provides callback functions that the C wrapper invokes during
inject/extract operations:
Injection (writer callbacks):
flt_otel_text_map_writer_set_cb()
flt_otel_http_headers_writer_set_cb()
Both append key-value pairs to the carrier's text_map via
OTELC_TEXT_MAP_ADD.
Extraction (reader callbacks):
flt_otel_text_map_reader_foreach_key_cb()
flt_otel_http_headers_reader_foreach_key_cb()
Both iterate the text_map's key-value pairs, invoking a handler function
for each entry. Iteration stops early if the handler returns -1.
8.2 HTTP Header Storage
flt_otel_http_headers_get() (http.c) reads headers from the HTX buffer with
prefix matching. The prefix is stripped from header names in the returned
text_map. A special prefix character ('-') matches all headers without prefix
filtering.
flt_otel_http_header_set() constructs a "prefix-name" header, removes all
existing occurrences via an http_find_header / http_remove_header loop, then
adds the new value via http_add_header.
8.3 Variable Storage
When USE_OTEL_VARS is enabled, span context can also be stored in HAProxy
transaction-scoped variables. Variable names are normalized
(flt_otel_normalize_name in vars.c):
Dashes -> 'D' (FLT_OTEL_VAR_CHAR_DASH)
Spaces -> 'S' (FLT_OTEL_VAR_CHAR_SPACE)
Uppercase -> lowercase
Full variable names are constructed as "scope.prefix.name" with dots as
component separators.
Two implementation paths exist based on the USE_OTEL_VARS_NAME compile flag:
With USE_OTEL_VARS_NAME:
A binary tracking buffer (stored as a HAProxy variable) records the names
of all context variables. flt_otel_ctx_loop() iterates length-prefixed
entries in this buffer, calling a callback for each. This enables efficient
enumeration without scanning the full variable store.
Without USE_OTEL_VARS_NAME:
Direct CEB tree traversal on the HAProxy variable store with prefix
matching. This is simpler but potentially slower for large variable sets.
The choice is auto-detected at build time by checking whether struct var has
a 'name' member.
9 Debug Infrastructure
----------------------------------------------------------------------
9.1 Conditional Compilation
When OTEL_DEBUG=1 is set at build time, -DDEBUG_OTEL is added to the compiler
flags. This enables:
- Additional flt_ops callbacks: deinit_per_thread, http_payload, http_reset,
tcp_payload. In non-debug builds these are NULL.
- FLT_OTEL_USE_COUNTERS (config.h), which activates the per-event counters in
flt_otel_counters (attached[], disabled[] arrays).
- Debug-only functions throughout the codebase, marked with [D] in
README-func: flt_otel_scope_data_dump, flt_otel_http_headers_dump,
flt_otel_vars_dump, flt_otel_args_dump, flt_otel_filters_dump, etc.
- The OTELC_DBG() macro for level-gated debug output.
9.2 Debug Level Bitmask
The debug level is a uint stored in conf->instr and controllable at runtime via
"otel debug <level>". The default value is FLT_OTEL_DEBUG_LEVEL (0b11101111111
in config.h). Each bit enables a category of debug output.
9.3 Logging Integration
The FLT_OTEL_LOG macro (debug.h) integrates with HAProxy's send_log() system.
Its behavior depends on the logging flags:
FLT_OTEL_LOGGING_OFF (0):
No log messages are emitted.
FLT_OTEL_LOGGING_ON (1 << 0):
Log messages are sent to the log servers configured in the instrumentation
block via parse_logger().
FLT_OTEL_LOGGING_NOLOGNORM (1 << 1):
Combined with ON, suppresses normal-level messages (only warnings and above
are emitted).
These flags are set via the "option dontlog-normal" configuration keyword or the
"otel logging" CLI command.
9.4 Counter System
When FLT_OTEL_USE_COUNTERS is defined, the flt_otel_counters structure (conf.h)
maintains:
attached[4] Counters for attach outcomes, incremented atomically in
flt_otel_ops_attach().
disabled[2] Counters for hard-error disables, incremented atomically in
flt_otel_return_int() / flt_otel_return_void().
The counters are reported by the "otel status" CLI command and dumped at deinit
time in debug builds.
10 Idle Timeout Mechanism
----------------------------------------------------------------------
The idle timeout fires periodic events on idle streams, enabling heartbeat-style
span updates or metric recordings.
10.1 Configuration
Each otel-scope bound to the "on-idle-timeout" event must declare an
idle-timeout interval. At check time (flt_otel_ops_check), the minimum idle
timeout across all scopes is stored in conf->instr->idle_timeout.
10.2 Initialization
In flt_otel_ops_stream_start(), the runtime context's idle_timeout and idle_exp
fields are initialized from the precomputed minimum. The expiration tick is
merged into the request channel's analyse_exp to ensure the stream task wakes
at the right time.
10.3 Firing
flt_otel_ops_check_timeouts() checks tick_is_expired(rt_ctx->idle_exp).
When expired:
- The on-idle-timeout event fires via flt_otel_event_run().
- The timer is rescheduled for the next interval.
- If analyse_exp itself has expired (which would cause a tight loop), it is
reset before the new idle_exp is merged.
- STRM_EVT_MSG is set on stream->pending_events to ensure the stream is
re-evaluated.
11 Group Action Integration Pattern
----------------------------------------------------------------------
The "otel-group" action integrates OTel scopes with HAProxy's rule system
(http-request, http-response, http-after-response, tcp-request, tcp-response).
11.1 Registration
group.c registers action keywords via INITCALL1 macros for all five action
contexts. Each registration points to flt_otel_group_parse() as the parse
function.
11.2 Parse-Check-Execute Cycle
Parse (flt_otel_group_parse):
Stores the filter ID and group ID as string duplicates in rule->arg.act.p[].
Sets the check, action and release callbacks.
Check (flt_otel_group_check):
Resolves the string IDs to configuration pointers by scanning the proxy's
filter list. Replaces the string pointers with resolved flt_conf,
flt_otel_conf and flt_otel_conf_ph pointers. Frees the original string
duplicates.
Execute (flt_otel_group_action):
Finds the filter instance in the stream's filter list. Validates rule->from
against the flt_otel_group_data[] table to determine the sample fetch
direction and valid fetch locations. Iterates all scopes in the group and
calls flt_otel_scope_run() for each. Always returns ACT_RET_CONT -- a group
action never interrupts rule processing.
11.3 Group Data Table
The flt_otel_group_data[] table (group.c) maps each HAProxy action context
(ACT_F_*) to:
act_from The action context enum value.
smp_val The valid sample fetch location (FE or BE).
smp_opt_dir The sample fetch direction (REQ or RES).
This table is generated from the FLT_OTEL_GROUP_DEFINES X-macro list (group.h).
12 CLI Runtime Control Architecture
----------------------------------------------------------------------
cli.c registers commands under the "otel" prefix. The command table maps
keyword sequences to handler functions with access level requirements.
12.1 Command Table
"otel status" No access restriction. Read-only status report.
"otel enable" ACCESS_LVL_ADMIN. Clears flag_disabled.
"otel disable" ACCESS_LVL_ADMIN. Sets flag_disabled.
"otel hard-errors" ACCESS_LVL_ADMIN. Sets flag_harderr.
"otel soft-errors" ACCESS_LVL_ADMIN. Clears flag_harderr.
"otel logging" ACCESS_LVL_ADMIN for setting; any for reading.
"otel rate" ACCESS_LVL_ADMIN for setting; any for reading.
"otel debug" ACCESS_LVL_ADMIN. DEBUG_OTEL only.
The "otel enable" and "otel disable" commands share the same handler
(flt_otel_cli_parse_disabled) with the private argument distinguishing the
operation (1 for disable, 0 for enable). The same pattern is used for
hard-errors / soft-errors.
12.2 Global Propagation
All set operations iterate every OTel filter instance across all proxies using
FLT_OTEL_PROXIES_LIST_START / FLT_OTEL_PROXIES_LIST_END macros and atomically
update the target field. A single "otel disable" command disables the filter
in every proxy that has it configured.
12.3 Status Report
The "otel status" command assembles a dynamic string via memprintf() containing:
- Library versions (C++ SDK and C wrapper).
- Debug level (DEBUG_OTEL only).
- Dropped diagnostic message count.
- Per-proxy: config file, group/scope counts, instrumentation details, rate
limit, error mode, disabled state, logging state, analyzer bitmask, and
counters (if FLT_OTEL_USE_COUNTERS).

723
addons/otel/README-func Normal file
View file

@ -0,0 +1,723 @@
OpenTelemetry filter -- function reference
==========================================================================
Functions are grouped by source file. Functions marked with [D] are only
compiled when DEBUG_OTEL is defined.
src/filter.c
----------------------------------------------------------------------
Filter lifecycle callbacks and helpers registered in flt_otel_ops.
flt_otel_mem_malloc
Allocator callback for the OTel C wrapper library. Uses the HAProxy
pool_head_otel_span_context pool.
flt_otel_mem_free
Deallocator callback for the OTel C wrapper library.
flt_otel_log_handler_cb
Diagnostic callback for the OTel C wrapper library. Counts SDK internal
diagnostic messages.
flt_otel_thread_id
Returns the current HAProxy thread ID (tid).
flt_otel_lib_init
Initializes the OTel C wrapper library: verifies the library version,
constructs the configuration path, calls otelc_init(), and creates the
tracer, meter and logger instances.
flt_otel_is_disabled
Checks whether the filter instance is disabled for the current stream.
Logs the event name when DEBUG_OTEL is enabled.
flt_otel_return_int
Error handler for callbacks returning int. In hard-error mode, disables
the filter; in soft-error mode, clears the error and returns OK.
flt_otel_return_void
Error handler for callbacks returning void. Same logic as
flt_otel_return_int but without a return value.
flt_otel_ops_init
Filter init callback (flt_ops.init). Called once per proxy to initialize
the OTel library via flt_otel_lib_init() and register CLI keywords.
flt_otel_ops_deinit
Filter deinit callback (flt_ops.deinit). Destroys the tracer, meter and
logger, frees the configuration, and calls otelc_deinit().
flt_otel_ops_check
Filter check callback (flt_ops.check). Validates the parsed
configuration: checks for duplicate filter IDs, resolves group/scope
placeholder references, verifies root span count, and sets analyzer bits.
flt_otel_ops_init_per_thread
Per-thread init callback (flt_ops.init_per_thread). Starts the OTel
tracer thread and enables HTX filtering.
flt_otel_ops_deinit_per_thread [D]
Per-thread deinit callback (flt_ops.deinit_per_thread).
flt_otel_ops_attach
Filter attach callback (flt_ops.attach). Called when a filter instance is
attached to a stream. Applies rate limiting, creates the runtime context,
and sets analyzer bits.
flt_otel_ops_stream_start
Stream start callback (flt_ops.stream_start). Fires the
on-stream-start event before any channel processing begins. The channel
argument is NULL. After the event, initializes the idle timer in the
runtime context from the precomputed minimum idle_timeout in the
instrumentation configuration.
flt_otel_ops_stream_set_backend
Stream set-backend callback (flt_ops.stream_set_backend). Fires the
on-backend-set event when a backend is assigned to the stream.
flt_otel_ops_stream_stop
Stream stop callback (flt_ops.stream_stop). Fires the
on-stream-stop event after all channel processing ends. The channel
argument is NULL.
flt_otel_ops_detach
Filter detach callback (flt_ops.detach). Frees the runtime context when
the filter is detached from a stream.
flt_otel_ops_check_timeouts
Timeout callback (flt_ops.check_timeouts). When the idle-timeout timer
has expired, fires the on-idle-timeout event and reschedules the timer
for the next interval. Sets the STRM_EVT_MSG pending event flag on the
stream.
flt_otel_ops_channel_start_analyze
Channel start-analyze callback. Registers analyzers on the channel and
runs the client/server session start event. Propagates the idle-timeout
expiry to the channel's analyse_exp so the stream task keeps waking.
flt_otel_ops_channel_pre_analyze
Channel pre-analyze callback. Maps the analyzer bit to an event index and
runs the corresponding event.
flt_otel_ops_channel_post_analyze
Channel post-analyze callback. Non-resumable; called once when a
filterable analyzer finishes.
flt_otel_ops_channel_end_analyze
Channel end-analyze callback. Runs the client/server session end event.
For the request channel, also fires the server-unavailable event if no
response was processed.
flt_otel_ops_http_headers
HTTP headers callback (flt_ops.http_headers). Fires
on-http-headers-request or on-http-headers-response depending on the
channel direction.
flt_otel_ops_http_payload [D]
HTTP payload callback (flt_ops.http_payload).
flt_otel_ops_http_end
HTTP end callback (flt_ops.http_end). Fires on-http-end-request or
on-http-end-response depending on the channel direction.
flt_otel_ops_http_reset [D]
HTTP reset callback (flt_ops.http_reset).
flt_otel_ops_http_reply
HTTP reply callback (flt_ops.http_reply). Fires the on-http-reply event
when HAProxy generates an internal reply.
flt_otel_ops_tcp_payload [D]
TCP payload callback (flt_ops.tcp_payload).
src/event.c
----------------------------------------------------------------------
Event dispatching, metrics recording and scope/span execution engine.
flt_otel_scope_run_instrument_record
Records a measurement for a synchronous metric instrument. Evaluates
update-form attributes via flt_otel_sample_eval() and
flt_otel_sample_add_kv(), evaluates the sample expression from the
create-form instrument (instr_ref), and submits the value to the meter
via update_instrument_kv_n().
flt_otel_scope_run_instrument
Processes all metric instruments for a scope. Runs in two passes: the
first lazily creates create-form instruments via the meter, using
HA_ATOMIC_CAS to guarantee thread-safe one-time initialization; the second
iterates update-form instruments and records measurements via
flt_otel_scope_run_instrument_record(). Instruments whose index is still
negative (UNUSED or PENDING) are skipped.
flt_otel_scope_run_log_record
Emits log records for a scope. Iterates over the configured log-record
list, skipping entries whose severity is below the logger threshold.
Evaluates the body from sample fetch expressions or a log-format string,
optionally resolves a span reference against the runtime context, and
emits the record via the logger. A missing span is non-fatal -- the
record is emitted without span correlation.
flt_otel_scope_run_span
Executes a single span: creates the OTel span on first call, adds links,
baggage, attributes, events and status, then injects the context into HTTP
headers or HAProxy variables.
flt_otel_scope_run
Executes a complete scope: evaluates ACL conditions, extracts contexts,
iterates over configured spans (resolving links, evaluating sample
expressions), calls flt_otel_scope_run_span for each, processes metric
instruments via flt_otel_scope_run_instrument(), emits log records via
flt_otel_scope_run_log_record(), then marks and finishes completed spans.
flt_otel_event_run
Top-level event dispatcher. Called from filter callbacks, iterates over
all scopes matching the event index and calls flt_otel_scope_run() for
each.
src/scope.c
----------------------------------------------------------------------
Runtime context, span and context lifecycle management.
flt_otel_pools_info [D]
Logs the sizes of all registered HAProxy memory pools used by the OTel
filter.
flt_otel_runtime_context_init
Allocates and initializes the per-stream runtime context. Generates a
UUID and stores it in the sess.otel.uuid HAProxy variable.
flt_otel_runtime_context_free
Frees the runtime context: ends all active spans, destroys all extracted
contexts, and releases pool memory.
flt_otel_scope_span_init
Finds an existing scope span by name or creates a new one. Resolves the
parent reference (span or extracted context).
flt_otel_scope_span_free
Frees a scope span entry if its OTel span has been ended. Refuses to free
an active (non-NULL) span.
flt_otel_scope_context_init
Finds an existing scope context by name or creates a new one by extracting
the span context from a text map.
flt_otel_scope_context_free
Frees a scope context entry and destroys the underlying OTel span context.
flt_otel_scope_data_dump [D]
Dumps scope data contents (baggage, attributes, events, links, status) for
debugging.
flt_otel_scope_data_init
Zero-initializes a scope data structure and its event/link lists.
flt_otel_scope_data_free
Frees all scope data contents: key-value arrays, event entries, link
entries, and status description.
flt_otel_scope_finish_mark
Marks spans and contexts for finishing. Supports wildcard ("*"),
channel-specific ("req"/"res"), and named targets.
flt_otel_scope_finish_marked
Ends all spans and destroys all contexts that have been marked for
finishing by flt_otel_scope_finish_mark().
flt_otel_scope_free_unused
Removes scope spans with NULL OTel span and scope contexts with NULL OTel
context. Cleans up associated HTTP headers and variables.
src/parser.c
----------------------------------------------------------------------
Configuration file parsing for otel-instrumentation, otel-group and otel-scope
sections.
flt_otel_parse_strdup
Duplicates a string with error handling; optionally stores the string
length.
flt_otel_parse_keyword
Parses a single keyword argument: checks for duplicates and missing
values, then stores via flt_otel_parse_strdup().
flt_otel_parse_invalid_char
Validates characters in a name according to the specified type
(identifier, domain, context prefix, variable).
flt_otel_parse_cfg_check
Common validation for config keywords: looks up the keyword, checks
argument count and character validity, verifies that the parent section ID
is set.
flt_otel_parse_cfg_sample_expr
Parses a single HAProxy sample expression within a sample definition.
Calls sample_parse_expr().
flt_otel_parse_cfg_sample
Parses a complete sample definition (key plus one or more sample
expressions).
flt_otel_parse_cfg_str
Parses one or more string arguments into a conf_str list (used for the
"finish" keyword).
flt_otel_parse_cfg_file
Parses and validates a file path argument; checks that the file exists and
is readable.
flt_otel_parse_check_scope
Checks whether the current config parsing is within the correct HAProxy
configuration scope (cfg_scope filtering).
flt_otel_parse_cfg_instr
Section parser for the otel-instrumentation block. Handles keywords:
otel-instrumentation ID, log, config, groups, scopes, acl, rate-limit,
option, debug-level.
flt_otel_post_parse_cfg_instr
Post-parse callback for otel-instrumentation. Links the instrumentation
to the config and checks that a config file is specified.
flt_otel_parse_cfg_group
Section parser for the otel-group block. Handles keywords: otel-group ID,
scopes.
flt_otel_post_parse_cfg_group
Post-parse callback for otel-group. Checks that at least one scope is
defined.
flt_otel_parse_cfg_scope_ctx
Parses the context storage type argument ("use-headers" or "use-vars") for
inject/extract keywords.
flt_otel_parse_acl
Builds an ACL condition by trying multiple ACL lists in order
(scope-local, instrumentation, proxy).
flt_otel_parse_bounds
Parses a space-separated string of numbers into a dynamically allocated
array of doubles for histogram bucket boundaries. Sorts the values
internally.
flt_otel_parse_cfg_instrument
Parses the "instrument" keyword inside an otel-scope section. Supports
both "update" form (referencing an existing instrument) and "create" form
(defining a new metric instrument with type, name, optional aggregation
type, description, unit, value, and optional histogram bounds).
flt_otel_parse_cfg_scope
Section parser for the otel-scope block. Handles keywords: otel-scope ID,
span, link, attribute, event, baggage, status, inject, extract, finish,
instrument, log-record, acl, otel-event.
flt_otel_post_parse_cfg_scope
Post-parse callback for otel-scope. Checks that HTTP header injection is
only used on events that support it.
flt_otel_parse_cfg
Parses the OTel filter configuration file. Backs up current sections,
registers temporary otel-instrumentation/group/scope section parsers,
loads and parses the file, then restores the original sections.
flt_otel_parse
Main filter parser entry point, registered for the "otel" filter keyword.
Parses the filter ID and configuration file path from the HAProxy config
line.
src/conf.c
----------------------------------------------------------------------
Configuration structure allocation and deallocation. Most init/free pairs are
generated by the FLT_OTEL_CONF_FUNC_INIT and FLT_OTEL_CONF_FUNC_FREE macros.
flt_otel_conf_hdr_init
Allocates and initializes a conf_hdr structure.
flt_otel_conf_hdr_free
Frees a conf_hdr structure and removes it from its list.
flt_otel_conf_str_init
Allocates and initializes a conf_str structure.
flt_otel_conf_str_free
Frees a conf_str structure and removes it from its list.
flt_otel_conf_link_init
Allocates and initializes a conf_link structure (span link).
flt_otel_conf_link_free
Frees a conf_link structure and removes it from its list.
flt_otel_conf_ph_init
Allocates and initializes a conf_ph (placeholder) structure.
flt_otel_conf_ph_free
Frees a conf_ph structure and removes it from its list.
flt_otel_conf_sample_expr_init
Allocates and initializes a conf_sample_expr structure.
flt_otel_conf_sample_expr_free
Frees a conf_sample_expr structure and releases the parsed sample
expression.
flt_otel_conf_sample_init
Allocates and initializes a conf_sample structure.
flt_otel_conf_sample_init_ex
Extended sample initialization: sets the key, extra data (event name or
status code), concatenated value string, and expression count.
flt_otel_conf_sample_free
Frees a conf_sample structure including its value, extra data, and all
sample expressions.
flt_otel_conf_context_init
Allocates and initializes a conf_context structure.
flt_otel_conf_context_free
Frees a conf_context structure and removes it from its list.
flt_otel_conf_span_init
Allocates and initializes a conf_span structure with empty lists for
links, attributes, events, baggages and statuses.
flt_otel_conf_span_free
Frees a conf_span structure and all its child lists.
flt_otel_conf_instrument_init
Allocates and initializes a conf_instrument structure.
flt_otel_conf_instrument_free
Frees a conf_instrument structure and removes it from its list.
flt_otel_conf_log_record_init
Allocates and initializes a conf_log_record structure with empty
attributes and samples lists.
flt_otel_conf_log_record_free
Frees a conf_log_record structure: event_name, span, attributes and
samples list.
flt_otel_conf_scope_init
Allocates and initializes a conf_scope structure with empty lists for
ACLs, contexts, spans, spans_to_finish and instruments.
flt_otel_conf_scope_free
Frees a conf_scope structure, ACLs, condition, and all child lists.
flt_otel_conf_group_init
Allocates and initializes a conf_group structure with an empty placeholder
scope list.
flt_otel_conf_group_free
Frees a conf_group structure and its placeholder scope list.
flt_otel_conf_instr_init
Allocates and initializes a conf_instr structure. Sets the default rate
limit to 100%, initializes the proxy_log, and creates empty ACL and
placeholder lists.
flt_otel_conf_instr_free
Frees a conf_instr structure including ACLs, loggers, config path, and
placeholder lists.
flt_otel_conf_init
Allocates and initializes the top-level flt_otel_conf structure with empty
group and scope lists.
flt_otel_conf_free
Frees the top-level flt_otel_conf structure and all of its children
(instrumentation, groups, scopes).
src/cli.c
----------------------------------------------------------------------
HAProxy CLI command handlers for runtime filter management.
cmn_cli_set_msg
Sets the CLI appctx response message and state.
flt_otel_cli_parse_debug [D]
CLI handler for "otel debug [level]". Gets or sets the debug level.
flt_otel_cli_parse_disabled
CLI handler for "otel enable" and "otel disable".
flt_otel_cli_parse_option
CLI handler for "otel soft-errors" and "otel hard-errors".
flt_otel_cli_parse_logging
CLI handler for "otel logging [state]". Gets or sets the logging state
(off/on/nolognorm).
flt_otel_cli_parse_rate
CLI handler for "otel rate [value]". Gets or sets the rate limit
percentage.
flt_otel_cli_parse_status
CLI handler for "otel status". Displays filter configuration and runtime
state for all OTel filter instances.
flt_otel_cli_init
Registers the OTel CLI keywords with HAProxy.
src/otelc.c
----------------------------------------------------------------------
OpenTelemetry context propagation bridge (inject/extract) between HAProxy and
the OTel C wrapper library.
flt_otel_text_map_writer_set_cb
Writer callback for text map injection. Appends a key-value pair to the
text map.
flt_otel_http_headers_writer_set_cb
Writer callback for HTTP headers injection. Appends a key-value pair to
the text map.
flt_otel_inject_text_map
Injects span context into a text map carrier.
flt_otel_inject_http_headers
Injects span context into an HTTP headers carrier.
flt_otel_text_map_reader_foreach_key_cb
Reader callback for text map extraction. Iterates over all key-value
pairs in the text map.
flt_otel_http_headers_reader_foreach_key_cb
Reader callback for HTTP headers extraction. Iterates over all key-value
pairs in the text map.
flt_otel_extract_text_map
Extracts a span context from a text map carrier via the tracer.
flt_otel_extract_http_headers
Extracts a span context from an HTTP headers carrier via the tracer.
src/http.c
----------------------------------------------------------------------
HTTP header manipulation for context propagation.
flt_otel_http_headers_dump [D]
Dumps all HTTP headers from the channel's HTX buffer.
flt_otel_http_headers_get
Extracts HTTP headers matching a prefix into a text map. Used by the
"extract" keyword to read span context from incoming request headers.
flt_otel_http_header_set
Sets or removes an HTTP header. Combines prefix and name into the full
header name, removes all existing occurrences, then adds the new value
(if non-NULL).
flt_otel_http_headers_remove
Removes all HTTP headers matching a prefix. Wrapper around
flt_otel_http_header_set() with NULL name and value.
src/vars.c
----------------------------------------------------------------------
HAProxy variable integration for context propagation and storage. Only compiled
when USE_OTEL_VARS is defined.
flt_otel_vars_scope_dump [D]
Dumps all variables for a single HAProxy variable scope.
flt_otel_vars_dump [D]
Dumps all variables across all scopes (PROC, SESS, TXN, REQ/RES).
flt_otel_smp_init
Initializes a sample structure with stream ownership and optional string
data.
flt_otel_smp_add
Appends a context variable name to the binary sample data buffer used for
tracking registered context variables.
flt_otel_normalize_name
Normalizes a variable name: replaces dashes with 'D' and spaces with 'S',
converts to lowercase.
flt_otel_denormalize_name
Reverses the normalization applied by flt_otel_normalize_name(). Restores
dashes from 'D' and spaces from 'S'.
flt_otel_var_name
Constructs a full variable name from scope, prefix and name components,
separated by dots.
flt_otel_ctx_loop
Iterates over all context variable names stored in the binary sample data,
calling a callback for each.
flt_otel_ctx_set_cb
Callback for flt_otel_ctx_loop() that checks whether a context variable
name already exists.
flt_otel_ctx_set
Registers a context variable name in the binary tracking buffer if it is
not already present.
flt_otel_var_register
Registers a HAProxy variable via vars_check_arg() so it can be used at
runtime.
flt_otel_var_set
Sets a HAProxy variable value. For context-scope variables, also
registers the name in the context tracking buffer.
flt_otel_vars_unset_cb
Callback for flt_otel_ctx_loop() that unsets each context variable.
flt_otel_vars_unset
Unsets all context variables for a given prefix and removes the tracking
variable itself.
flt_otel_vars_get_scope
Resolves a scope name string ("proc", "sess", "txn", "req", "res") to the
corresponding HAProxy variable store.
flt_otel_vars_get_cb
Callback for flt_otel_ctx_loop() that reads each context variable value
and adds it to a text map.
flt_otel_vars_get
Reads all context variables for a prefix into a text map. Used by the
"extract" keyword with variable storage.
src/pool.c
----------------------------------------------------------------------
Memory pool and trash buffer helpers.
flt_otel_pool_alloc
Allocates memory from a HAProxy pool (if available) or from the heap.
Optionally zero-fills the allocated block.
flt_otel_pool_strndup
Duplicates a string using a HAProxy pool (if available) or the heap.
flt_otel_pool_free
Returns memory to a HAProxy pool or frees it from the heap.
flt_otel_trash_alloc
Allocates a trash buffer chunk, optionally zero-filled.
flt_otel_trash_free
Frees a trash buffer chunk.
src/util.c
----------------------------------------------------------------------
Utility and conversion functions.
flt_otel_args_dump [D]
Dumps configuration arguments array to stderr.
flt_otel_filters_dump [D]
Dumps all OTel filter instances across all proxies.
flt_otel_chn_label [D]
Returns "REQuest" or "RESponse" based on channel flags.
flt_otel_pr_mode [D]
Returns "HTTP" or "TCP" based on proxy mode.
flt_otel_stream_pos [D]
Returns "frontend" or "backend" based on stream flags.
flt_otel_type [D]
Returns "frontend" or "backend" based on filter flags.
flt_otel_analyzer [D]
Returns the analyzer name string for a given analyzer bit.
flt_otel_list_dump [D]
Returns a summary string for a list (empty, single, count).
flt_otel_args_count
Counts the number of valid (non-NULL) arguments in an args array, handling
gaps from blank arguments.
flt_otel_args_concat
Concatenates arguments starting from a given index into a single
space-separated string.
flt_otel_strtod
Parses a string to double with range validation.
flt_otel_strtoll
Parses a string to int64 with range validation.
flt_otel_sample_to_str
Converts sample data to its string representation. Handles bool, sint,
IPv4, IPv6, str, and HTTP method types.
flt_otel_sample_to_value
Converts sample data to an otelc_value. Preserves native types (bool,
int64) where possible; falls back to string.
flt_otel_sample_add_event
Adds a sample value as a span event attribute. Groups attributes by event
name; dynamically grows the attribute array.
flt_otel_sample_set_status
Sets the span status code and description from sample data.
flt_otel_sample_add_kv
Adds a sample value as a key-value attribute or baggage entry.
Dynamically grows the key-value array.
flt_otel_sample_eval
Evaluates all sample expressions for a configured sample definition and
stores the result in an otelc_value. Supports both log-format and bare
sample expression paths. When flag_native is true and the sample has
exactly one expression, the native HAProxy sample type is preserved;
otherwise results are concatenated into a string.
flt_otel_sample_add
Top-level sample evaluator and dispatcher. Calls flt_otel_sample_eval()
to evaluate the sample, then dispatches the result to the appropriate
handler (attribute, event, baggage, status).
src/group.c
----------------------------------------------------------------------
Group action support for http-response / http-after-response / tcp-request /
tcp-response rules.
flt_otel_group_action
Action callback (action_ptr) for the otel-group rule. Finds the filter
instance on the current stream and runs all scopes defined in the group.
flt_otel_group_check
Check callback (check_ptr) for the otel-group rule. Resolves filter ID
and group ID references against the proxy's filter configuration.
flt_otel_group_release
Release callback (release_ptr) for the otel-group rule.
flt_otel_group_parse
Parses the "otel-group" action keyword from HAProxy config rules.
Registered for tcp-request, tcp-response, http-request, http-response and
http-after-response action contexts.

File diff suppressed because it is too large Load diff

101
addons/otel/README-misc Normal file
View file

@ -0,0 +1,101 @@
OpenTelemetry filter -- miscellaneous notes
==============================================================================
1 Parsing sample expressions in HAProxy
------------------------------------------------------------------------------
HAProxy provides two entry points for turning a configuration string into an
evaluable sample expression.
1.1 sample_parse_expr()
..............................................................................
Parses a bare sample-fetch name with an optional converter chain. The input is
the raw expression without any surrounding syntax.
Declared in: include/haproxy/sample.h
Defined in: src/sample.c
struct sample_expr *sample_parse_expr(char **str, int *idx, const char *file, int line, char **err_msg, struct arg_list *al, char **endptr);
The function reads from str[*idx] and advances *idx past the consumed tokens.
Configuration example (otel-scope instrument keyword):
instrument my_counter "name" desc req.hdr(host),lower ...
Here "req.hdr(host),lower" is a single configuration token that
sample_parse_expr() receives directly. It recognises the fetch "req.hdr(host)"
and the converter "lower" separated by a comma.
1.2 parse_logformat_string()
..............................................................................
Parses a log-format string that may contain literal text mixed with sample
expressions wrapped in %[...] delimiters.
Declared in: include/haproxy/log.h
Defined in: src/log.c
int parse_logformat_string(const char *fmt, struct proxy *curproxy, struct lf_expr *lf_expr, int options, int cap, char **err);
Configuration example (HAProxy log-format directive):
log-format "host=%[req.hdr(host),lower] status=%[status]"
The %[...] wrapper tells parse_logformat_string() where each embedded sample
expression begins and ends. The text outside the brackets ("host=", " status=")
is emitted as-is.
1.3 Which one to use
..............................................................................
Use sample_parse_expr() when the configuration token is a single, standalone
sample expression (no surrounding text). This is the case for the otel filter
keywords such as "attribute", "event", "baggage", "status", "value", and
similar.
Use parse_logformat_string() when the value is a free-form string that may mix
literal text with zero or more embedded expressions.
2 Signal keywords
------------------------------------------------------------------------------
The OTel filter configuration uses one keyword per signal to create or update
signal-specific objects. The keyword names follow the OpenTelemetry
specification's own terminology rather than using informal synonyms.
Signal Keyword Creates / updates
-------- ----------- ------------------------------------------
Tracing span A trace span.
Metrics instrument A metric instrument (counter, gauge, ...).
Logging log-record A log record.
The tracing keyword follows the same logic. A "trace" is the complete
end-to-end path of a request through a distributed system, composed of one or
more "spans". Each span represents a single unit of work within that trace.
The configuration operates at the span level: it creates individual spans, sets
their parent-child relationships, and attaches attributes and events. Using
"trace" as the keyword would be imprecise because one does not configure a trace
directly; one configures the spans that collectively form a trace.
The metrics keyword is analogous. In the OpenTelemetry data model the
terminology is layered: a "metric" is the aggregated output that the SDK
produces after processing recorded measurements, while an "instrument" is the
concrete object through which those measurements are recorded -- a counter,
histogram, gauge, or up-down counter. The configuration operates at the
instrument level: it creates an instrument of a specific type and records values
through it. Using "metric" as the keyword would be imprecise because one does
not configure a metric directly; one configures an instrument that yields
metrics.
The logging keyword follows the same pattern. A "log" is the broad signal
category, while a "log record" is a single discrete entry within that signal.
The configuration operates at the log-record level: it creates individual log
records with a severity, a body, and optional attributes and span context.
Using "log" as the keyword would be imprecise because one does not configure a
log stream directly; one configures the individual log records that comprise it.

288
addons/otel/README.md Normal file
View file

@ -0,0 +1,288 @@
## HAProxy OpenTelemetry Filter (OTel)
The OTel filter enables HAProxy to emit telemetry data -- traces, metrics and
logs -- to any OpenTelemetry-compatible backend via the OpenTelemetry protocol
(OTLP).
It is the successor to the OpenTracing (OT) filter, built on the OpenTelemetry
standard which unifies distributed tracing, metrics and logging into a single
observability framework.
### Features
- **Distributed tracing** -- spans with parent-child relationships, context
propagation via HTTP headers or HAProxy variables, links, baggage and status.
- **Metrics** -- counter, histogram, up-down counter and gauge instruments with
configurable aggregation and bucket boundaries.
- **Logging** -- log records with severity levels, optional span correlation and
runtime-evaluated attributes.
- **Rate limiting** -- percentage-based sampling (0.0--100.0) for controlling
overhead.
- **ACL integration** -- fine-grained conditional execution at instrumentation,
scope and event levels.
- **CLI management** -- runtime enable/disable, rate adjustment, error mode
switching and status inspection.
- **Context propagation** -- inject/extract span contexts between cascaded
HAProxy instances or external services.
### Dependencies
The filter requires the
[OpenTelemetry C Wrapper](https://github.com/haproxytech/opentelemetry-c-wrapper)
library, which wraps the OpenTelemetry C++ SDK.
### Building
The OTel filter is compiled together with HAProxy by adding `USE_OTEL=1` to the
make command.
#### Using pkg-config
```
PKG_CONFIG_PATH=/opt/lib/pkgconfig make -j8 USE_OTEL=1 TARGET=linux-glibc
```
#### Explicit paths
```
make -j8 USE_OTEL=1 OTEL_INC=/opt/include OTEL_LIB=/opt/lib TARGET=linux-glibc
```
#### Build options
| Variable | Description |
|-----------------|-----------------------------------------------------|
| `USE_OTEL` | Enable the OpenTelemetry filter |
| `OTEL_DEBUG` | Compile in debug mode |
| `OTEL_INC` | Force path to opentelemetry-c-wrapper include files |
| `OTEL_LIB` | Force path to opentelemetry-c-wrapper library |
| `OTEL_RUNPATH` | Add opentelemetry-c-wrapper RUNPATH to executable |
| `OTEL_USE_VARS` | Enable context propagation via HAProxy variables |
#### Debug mode
```
PKG_CONFIG_PATH=/opt/lib/pkgconfig make -j8 USE_OTEL=1 OTEL_DEBUG=1 TARGET=linux-glibc
```
#### Variable-based context propagation
```
PKG_CONFIG_PATH=/opt/lib/pkgconfig make -j8 USE_OTEL=1 OTEL_USE_VARS=1 TARGET=linux-glibc
```
#### Verifying the build
```
./haproxy -vv | grep -i opentelemetry
```
If the filter is built in, the output contains:
```
Built with OpenTelemetry support (C++ version 1.26.0, C Wrapper version 1.0.0-842).
[OTEL] opentelemetry
```
#### Library path at runtime
When pkg-config is not used, the executable may not find the library at startup.
Use `LD_LIBRARY_PATH` or build with `OTEL_RUNPATH=1`:
```
LD_LIBRARY_PATH=/opt/lib ./haproxy ...
```
```
make -j8 USE_OTEL=1 OTEL_RUNPATH=1 OTEL_INC=/opt/include OTEL_LIB=/opt/lib TARGET=linux-glibc
```
### Configuration
The filter uses a two-file configuration model:
1. **OTel configuration file** (`.cfg`) -- defines the telemetry model:
instrumentation settings, scopes and groups.
2. **YAML configuration file** (`.yml`) -- defines the OpenTelemetry SDK
pipeline: exporters, samplers, processors, providers and signal routing.
#### Activating the filter
The OTel filter requires the `insecure-fork-wanted` keyword in the HAProxy
`global` section. This is necessary because the OpenTelemetry C++ SDK creates
background threads for data export and batch processing. HAProxy will refuse
to load the configuration if this keyword is missing.
```
global
insecure-fork-wanted
...
```
Add the filter to a HAProxy proxy section (frontend/listen/backend):
```
frontend my-frontend
...
filter opentelemetry [id <id>] config <file>
...
```
If no filter id is specified, `otel-filter` is used as default.
#### OTel configuration file structure
The OTel configuration file contains three section types:
- `otel-instrumentation` -- mandatory; references the YAML file, sets rate
limits, error modes, logging and declares groups and scopes.
- `otel-scope` -- defines actions (spans, attributes, metrics, logs) triggered
by stream events or from groups.
- `otel-group` -- a named collection of scopes triggered from HAProxy TCP/HTTP
rules.
#### Minimal YAML configuration
```yaml
exporters:
my_exporter:
type: otlp_http
endpoint: "http://localhost:4318/v1/traces"
samplers:
my_sampler:
type: always_on
processors:
my_processor:
type: batch
providers:
my_provider:
resources:
- service.name: "haproxy"
signals:
traces:
scope_name: "HAProxy OTel"
exporters: my_exporter
samplers: my_sampler
processors: my_processor
providers: my_provider
```
#### Supported YAML exporters
| Type | Description |
|-----------------|---------------------------------------|
| `otlp_grpc` | OTLP over gRPC |
| `otlp_http` | OTLP over HTTP (JSON or Protobuf) |
| `otlp_file` | Local files in OTLP format |
| `zipkin` | Zipkin-compatible backends |
| `elasticsearch` | Elasticsearch |
| `ostream` | Text output to a file (for debugging) |
| `memory` | In-memory buffer (for testing) |
### Scope keywords
| Keyword | Description |
|----------------|---------------------------------------------------------|
| `span` | Create or reference a span |
| `attribute` | Set key-value span attributes |
| `event` | Add timestamped span events |
| `baggage` | Set context propagation data |
| `status` | Set span status (ok/error/ignore/unset) |
| `link` | Add span links to related spans |
| `inject` | Inject context into headers or variables |
| `extract` | Extract context from headers or variables |
| `finish` | Close spans (supports wildcards: `*`, `*req*`, `*res*`) |
| `instrument` | Create or update metric instruments |
| `log-record` | Emit a log record with severity |
| `otel-event` | Bind scope to a filter event with optional ACL |
| `idle-timeout` | Set periodic event interval for idle streams |
### CLI commands
Available via the HAProxy CLI socket (prefix: `flt-otel`):
| Command | Description |
|----------------------------|------------------------------------|
| `flt-otel status` | Show filter status |
| `flt-otel enable` | Enable the filter |
| `flt-otel disable` | Disable the filter |
| `flt-otel hard-errors` | Enable hard-errors mode |
| `flt-otel soft-errors` | Disable hard-errors mode |
| `flt-otel logging [state]` | Set logging state |
| `flt-otel rate [value]` | Set or show the rate limit |
| `flt-otel debug [level]` | Set debug level (debug build only) |
When invoked without arguments, `rate`, `logging` and `debug` display the
current value.
### Performance
Benchmark results from the standalone (`sa`) configuration, which exercises all
events (worst-case scenario):
| Rate limit | Req/s | Avg latency | Overhead |
|------------|--------|-------------|----------|
| 100.0% | 38,202 | 213.08 us | 21.6% |
| 50.0% | 42,777 | 190.49 us | 12.2% |
| 25.0% | 45,302 | 180.46 us | 7.0% |
| 10.0% | 46,879 | 174.69 us | 3.7% |
| 2.5% | 47,993 | 170.58 us | 1.4% |
| disabled | 48,788 | 167.74 us | ~0 |
| off | 48,697 | 168.00 us | baseline |
With a rate limit of 10% or less, the performance impact is negligible.
Detailed methodology and additional results are in the `test/` directory.
### Test configurations
The `test/` directory contains ready-to-run example configurations:
- **sa** -- standalone; the most comprehensive example, demonstrating spans,
attributes, events, links, baggage, status, metrics, log records, ACL
conditions and idle-timeout events.
- **fe/be** -- distributed tracing across two cascaded HAProxy instances using
HTTP header-based context propagation.
- **ctx** -- context propagation via HAProxy variables using the inject/extract
mechanism.
- **cmp** -- minimal configuration for benchmarking comparison.
- **empty** -- filter initialized with no active telemetry.
#### Quick start with Jaeger
Start a Jaeger all-in-one container:
```
docker run -d --name jaeger -p 4317:4317 -p 4318:4318 -p 16686:16686 jaegertracing/all-in-one:latest
```
Run one of the test configurations:
```
./test/run-sa.sh
```
Open the Jaeger UI at `http://localhost:16686` to view traces.
### Documentation
Detailed documentation is available in the following files:
- [README](README) -- complete reference documentation
- [README-configuration](README-configuration) -- configuration guide
- [README-conf](README-conf) -- configuration details
- [README-design](README-design) -- cross-cutting design patterns
- [README-implementation](README-implementation) -- component architecture
- [README-func](README-func) -- function reference
- [README-misc](README-misc) -- miscellaneous notes
### Copyright
Copyright 2026 HAProxy Technologies
### Author
Miroslav Zagorac <mzagorac@haproxy.com>

28
addons/otel/include/cli.h Normal file
View file

@ -0,0 +1,28 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_CLI_H_
#define _OTEL_CLI_H_
#define FLT_OTEL_CLI_CMD "flt-otel"
#define FLT_OTEL_CLI_LOGGING_OFF "off"
#define FLT_OTEL_CLI_LOGGING_ON "on"
#define FLT_OTEL_CLI_LOGGING_NOLOGNORM "dontlog-normal"
#define FLT_OTEL_CLI_LOGGING_STATE(a) (((a) & FLT_OTEL_LOGGING_ON) ? (((a) & FLT_OTEL_LOGGING_NOLOGNORM) ? "enabled, " FLT_OTEL_CLI_LOGGING_NOLOGNORM : "enabled") : "disabled")
#define FLT_OTEL_CLI_MSG_CAT(a) (((a) == NULL) ? "" : (a)), (((a) == NULL) ? "" : "\n")
/* Register CLI keywords for the OTel filter. */
void flt_otel_cli_init(void);
#endif /* _OTEL_CLI_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

313
addons/otel/include/conf.h Normal file
View file

@ -0,0 +1,313 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_CONF_H_
#define _OTEL_CONF_H_
/* Extract the OTel filter configuration from a filter instance. */
#define FLT_OTEL_CONF(f) ((struct flt_otel_conf *)FLT_CONF(f))
/* Expand to a string pointer and its length for a named member. */
#define FLT_OTEL_STR_HDR_ARGS(p,m) (p)->m, (p)->m##_len
/***
* It should be noted that the macro FLT_OTEL_CONF_HDR_ARGS() does not have
* all the parameters defined that would correspond to the format found in
* the FLT_OTEL_CONF_HDR_FMT macro (first pointer is missing).
*
* This is because during the expansion of the OTELC_DBG_STRUCT() macro, an
* incorrect conversion is performed and instead of the first correct code,
* a second incorrect code is generated:
*
* do {
* if ((p) == NULL)
* ..
* } while (0)
*
* do {
* if ((p), (int) (p)->id_len, (p)->id, (p)->id_len, (p)->cfg_line == NULL)
* ..
* } while (0)
*
*/
#define FLT_OTEL_CONF_HDR_FMT "%p:{ { '%.*s' %zu %d } "
#define FLT_OTEL_CONF_HDR_ARGS(p,m) (int)(p)->m##_len, (p)->m, (p)->m##_len, (p)->cfg_line
/*
* Special two-byte prefix that triggers automatic id generation in
* FLT_OTEL_CONF_FUNC_INIT(): the text after the prefix is combined
* with the configuration line number to form a unique identifier.
*/
#define FLT_OTEL_CONF_HDR_SPECIAL "\x1e\x1f"
#define FLT_OTEL_CONF_STR_CMP(s,S) ((s##_len == S##_len) && (memcmp(s, S, S##_len) == 0))
#define FLT_OTEL_DBG_CONF_SAMPLE_EXPR(h,p) \
OTELC_DBG(DEBUG, h "%p:{ '%s' %p }", (p), (p)->fmt_expr, (p)->expr)
#define FLT_OTEL_DBG_CONF_SAMPLE(h,p) \
OTELC_DBG(DEBUG, h "%p:{ '%s' '%s' %s %s %d %p %hhu }", (p), \
(p)->key, (p)->fmt_string, otelc_value_dump(&((p)->extra), ""), \
flt_otel_list_dump(&((p)->exprs)), (p)->num_exprs, &((p)->lf_expr), (p)->lf_used)
#define FLT_OTEL_DBG_CONF_HDR(h,p,i) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "}", (p), FLT_OTEL_CONF_HDR_ARGS(p, i))
#define FLT_OTEL_DBG_CONF_CONTEXT(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "0x%02hhx }", (p), FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->flags)
#define FLT_OTEL_DBG_CONF_SPAN(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "'%s' %zu %s' %zu %hhu 0x%02hhx %s %s %s %s %s }", \
(p), FLT_OTEL_CONF_HDR_ARGS(p, id), FLT_OTEL_STR_HDR_ARGS(p, ref_id), \
FLT_OTEL_STR_HDR_ARGS(p, ctx_id), (p)->flag_root, (p)->ctx_flags, \
flt_otel_list_dump(&((p)->links)), flt_otel_list_dump(&((p)->attributes)), \
flt_otel_list_dump(&((p)->events)), flt_otel_list_dump(&((p)->baggages)), \
flt_otel_list_dump(&((p)->statuses)))
#define FLT_OTEL_DBG_CONF_SCOPE(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%hhu %d %u %s %p %s %s %s %s %s }", (p), \
FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->flag_used, (p)->event, (p)->idle_timeout, \
flt_otel_list_dump(&((p)->acls)), (p)->cond, flt_otel_list_dump(&((p)->contexts)), \
flt_otel_list_dump(&((p)->spans)), flt_otel_list_dump(&((p)->spans_to_finish)), \
flt_otel_list_dump(&((p)->instruments)), flt_otel_list_dump(&((p)->log_records)))
#define FLT_OTEL_DBG_CONF_GROUP(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%hhu %s }", (p), \
FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->flag_used, flt_otel_list_dump(&((p)->ph_scopes)))
#define FLT_OTEL_DBG_CONF_PH(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%p }", (p), FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->ptr)
#define FLT_OTEL_DBG_CONF_INSTR(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "'%s' %p %p %p %u %hhu %hhu 0x%02hhx %p:%s 0x%08x %u %s %s %s }", \
(p), FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->config, (p)->tracer, (p)->meter, (p)->logger, \
(p)->rate_limit, (p)->flag_harderr, (p)->flag_disabled, (p)->logging, &((p)->proxy_log), \
flt_otel_list_dump(&((p)->proxy_log.loggers)), (p)->analyzers, (p)->idle_timeout, \
flt_otel_list_dump(&((p)->acls)), flt_otel_list_dump(&((p)->ph_groups)), \
flt_otel_list_dump(&((p)->ph_scopes)))
#define FLT_OTEL_DBG_CONF_INSTRUMENT(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%" PRId64 " %d %d '%s' '%s' %s %s %p %zu %p }", (p), \
FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->idx, (p)->type, (p)->aggr_type, OTELC_STR_ARG((p)->description), \
OTELC_STR_ARG((p)->unit), flt_otel_list_dump(&((p)->samples)), flt_otel_list_dump(&((p)->attributes)), \
(p)->ref, (p)->bounds_num, (p)->bounds)
#define FLT_OTEL_DBG_CONF_LOG_RECORD(h,p) \
OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%d %" PRId64 " '%s' '%s' %s %s }", (p), \
FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->severity, (p)->event_id, OTELC_STR_ARG((p)->event_name), \
OTELC_STR_ARG((p)->span), flt_otel_list_dump(&((p)->attributes)), flt_otel_list_dump(&((p)->samples)))
#define FLT_OTEL_DBG_CONF(h,p) \
OTELC_DBG(DEBUG, h "%p:{ %p '%s' '%s' %p %s %s }", (p), \
(p)->proxy, (p)->id, (p)->cfg_file, (p)->instr, \
flt_otel_list_dump(&((p)->groups)), flt_otel_list_dump(&((p)->scopes)))
/* Anonymous struct containing a string pointer and its length. */
#define FLT_OTEL_CONF_STR(p) \
struct { \
char *p; \
size_t p##_len; \
}
/* Common header embedded in all configuration structures. */
#define FLT_OTEL_CONF_HDR(p) \
struct { \
FLT_OTEL_CONF_STR(p); \
int cfg_line; \
struct list list; \
}
/* Generic configuration header used for simple named list entries. */
struct flt_otel_conf_hdr {
FLT_OTEL_CONF_HDR(id); /* A list containing header names. */
};
/* flt_otel_conf_sample->exprs */
struct flt_otel_conf_sample_expr {
FLT_OTEL_CONF_HDR(fmt_expr); /* The original sample expression format string. */
struct sample_expr *expr; /* The sample expression. */
};
/*
* flt_otel_conf_span->attributes
* flt_otel_conf_span->events (event_name -> OTELC_VALUE_STR(&extra))
* flt_otel_conf_span->baggages
* flt_otel_conf_span->statuses (status_code -> extra.u.value_int32)
* flt_otel_conf_instrument->samples
* flt_otel_conf_log_record->samples
*/
struct flt_otel_conf_sample {
FLT_OTEL_CONF_HDR(key); /* The list containing sample names. */
char *fmt_string; /* All sample-expression arguments are combined into a single string. */
struct otelc_value extra; /* Optional supplementary data. */
struct list exprs; /* Used to chain sample expressions. */
int num_exprs; /* Number of defined expressions. */
struct lf_expr lf_expr; /* The log-format expression. */
bool lf_used; /* Whether lf_expr is used instead of exprs. */
};
/*
* flt_otel_conf_scope->spans_to_finish
*
* It can be seen that this structure is actually identical to the structure
* flt_otel_conf_hdr.
*/
struct flt_otel_conf_str {
FLT_OTEL_CONF_HDR(str); /* A list containing character strings. */
};
/* flt_otel_conf_scope->contexts */
struct flt_otel_conf_context {
FLT_OTEL_CONF_HDR(id); /* The name of the context. */
uint8_t flags; /* The type of storage from which the span context is extracted. */
};
/* flt_otel_conf_span->links */
struct flt_otel_conf_link {
FLT_OTEL_CONF_HDR(span); /* The list containing link names. */
};
/*
* Span configuration within a scope.
* flt_otel_conf_scope->spans
*/
struct flt_otel_conf_span {
FLT_OTEL_CONF_HDR(id); /* The name of the span. */
FLT_OTEL_CONF_STR(ref_id); /* The reference name, if used. */
FLT_OTEL_CONF_STR(ctx_id); /* The span context name, if used. */
uint8_t ctx_flags; /* The type of storage used for the span context. */
bool flag_root; /* Whether this is a root span. */
struct list links; /* The set of linked span names. */
struct list attributes; /* The set of key:value attributes. */
struct list events; /* The set of events with key-value attributes. */
struct list baggages; /* The set of key:value baggage items. */
struct list statuses; /* Span status code and description (only one per list). */
};
/*
* Metric instrument configuration within a scope.
* flt_otel_conf_scope->instruments
*/
struct flt_otel_conf_instrument {
FLT_OTEL_CONF_HDR(id); /* The name of the instrument. */
int64_t idx; /* Meter instrument index (-1 if not yet created). */
otelc_metric_instrument_t type; /* Instrument type (or UPDATE). */
otelc_metric_aggregation_type_t aggr_type; /* Aggregation type for the view (create only). */
char *description; /* Instrument description (create only). */
char *unit; /* Instrument unit (create only). */
struct list samples; /* Sample expressions for the value. */
double *bounds; /* Histogram bucket boundaries (create only). */
size_t bounds_num; /* Number of histogram bucket boundaries. */
struct list attributes; /* Instrument attributes (update only, flt_otel_conf_sample). */
struct flt_otel_conf_instrument *ref; /* Resolved create-form instrument (update only). */
};
/*
* Log record configuration within a scope.
* flt_otel_conf_scope->log_records
*/
struct flt_otel_conf_log_record {
FLT_OTEL_CONF_HDR(id); /* Required by macro; member <id> is not used directly. */
otelc_log_severity_t severity; /* The severity level. */
int64_t event_id; /* Optional event identifier. */
char *event_name; /* Optional event name. */
char *span; /* Optional span reference. */
struct list attributes; /* Log record attributes (flt_otel_conf_sample). */
struct list samples; /* Sample expressions for the body. */
};
/* Configuration for a single event scope. */
struct flt_otel_conf_scope {
FLT_OTEL_CONF_HDR(id); /* The scope name. */
bool flag_used; /* The indication that the scope is being used. */
int event; /* FLT_OTEL_EVENT_* */
uint idle_timeout; /* Idle timeout interval in milliseconds (0 = off). */
struct list acls; /* ACLs declared on this scope. */
struct acl_cond *cond; /* ACL condition to meet. */
struct list contexts; /* Declared contexts. */
struct list spans; /* Declared spans. */
struct list spans_to_finish; /* The list of spans scheduled for finishing. */
struct list instruments; /* The list of metric instruments. */
struct list log_records; /* The list of log records. */
};
/* Configuration for a named group of scopes. */
struct flt_otel_conf_group {
FLT_OTEL_CONF_HDR(id); /* The group name. */
bool flag_used; /* The indication that the group is being used. */
struct list ph_scopes; /* List of all used scopes. */
};
/* Placeholder referencing a scope or group by name. */
struct flt_otel_conf_ph {
FLT_OTEL_CONF_HDR(id); /* The scope/group name. */
void *ptr; /* Pointer to real placeholder structure. */
};
#define flt_otel_conf_ph_group flt_otel_conf_ph
#define flt_otel_conf_ph_scope flt_otel_conf_ph
/* Top-level OTel instrumentation settings (tracer, meter, options). */
struct flt_otel_conf_instr {
FLT_OTEL_CONF_HDR(id); /* The OpenTelemetry instrumentation name. */
char *config; /* The OpenTelemetry configuration file name. */
struct otelc_tracer *tracer; /* The OpenTelemetry tracer handle. */
struct otelc_meter *meter; /* The OpenTelemetry meter handle. */
struct otelc_logger *logger; /* The OpenTelemetry logger handle. */
uint32_t rate_limit; /* [0 2^32-1] <-> [0.0 100.0] */
bool flag_harderr; /* [0 1] */
bool flag_disabled; /* [0 1] */
uint8_t logging; /* [0 1 3] */
struct proxy proxy_log; /* The log server list. */
uint analyzers; /* Defined channel analyzers. */
uint idle_timeout; /* Minimum idle timeout across scopes (ms, 0 = off). */
struct list acls; /* ACLs declared on this tracer. */
struct list ph_groups; /* List of all used groups. */
struct list ph_scopes; /* List of all used scopes. */
};
/* Runtime counters for filter diagnostics. */
struct flt_otel_counters {
#ifdef DEBUG_OTEL
struct {
bool flag_used; /* Whether this event is used. */
uint64_t htx[2]; /* htx_is_empty() function result counter. */
} event[FLT_OTEL_EVENT_MAX];
#endif
#ifdef FLT_OTEL_USE_COUNTERS
uint64_t attached[4]; /* [run rate-limit disabled error] */
uint64_t disabled[2]; /* How many times stream processing is disabled. */
#endif
};
/* The OpenTelemetry filter configuration. */
struct flt_otel_conf {
struct proxy *proxy; /* Proxy owning the filter. */
char *id; /* The OpenTelemetry filter id. */
char *cfg_file; /* The OpenTelemetry filter configuration file name. */
struct flt_otel_conf_instr *instr; /* The OpenTelemetry instrumentation settings. */
struct list groups; /* List of all available groups. */
struct list scopes; /* List of all available scopes. */
struct flt_otel_counters cnt; /* Various counters related to filter operation. */
struct list smp_args; /* Deferred OTEL sample fetch args to resolve. */
};
/* Allocate and initialize a sample from parsed arguments. */
struct flt_otel_conf_sample *flt_otel_conf_sample_init_ex(const char **args, int idx, int n, const struct otelc_value *extra, int line, struct list *head, char **err);
/* Allocate and initialize the top-level OTel filter configuration. */
struct flt_otel_conf *flt_otel_conf_init(struct proxy *px);
/* Free the top-level OTel filter configuration. */
void flt_otel_conf_free(struct flt_otel_conf **ptr);
#endif /* _OTEL_CONF_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,128 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_CONF_FUNCS_H_
#define _OTEL_CONF_FUNCS_H_
/*
* Macro that generates a flt_otel_conf_<type>_init() function. The generated
* function allocates and initializes a configuration structure of the given
* type, checks for duplicate names in the list, and optionally runs a custom
* initializer body.
*/
#define FLT_OTEL_CONF_FUNC_INIT(_type_, _id_, _func_) \
struct flt_otel_conf_##_type_ *flt_otel_conf_##_type_##_init(const char *id, int line, struct list *head, char **err) \
{ \
struct flt_otel_conf_##_type_ *retptr = NULL; \
struct flt_otel_conf_##_type_ *ptr; \
char id_buffer[FLT_OTEL_ID_MAXLEN + 16]; \
size_t _id_##_len; \
\
OTELC_FUNC("\"%s\", %d, %p, %p:%p", OTELC_STR_ARG(id), line, head, OTELC_DPTR_ARGS(err)); \
\
if ((id == NULL) || (*id == '\0')) { \
FLT_OTEL_ERR("name not set"); \
\
OTELC_RETURN_PTR(retptr); \
} \
else if ((id[0] == FLT_OTEL_CONF_HDR_SPECIAL[0]) && (id[1] == FLT_OTEL_CONF_HDR_SPECIAL[1])) { \
(void)snprintf(id_buffer, sizeof(id_buffer), "%s:%d", id + 2, line); \
\
id = id_buffer; \
} \
\
_id_##_len = strlen(id); \
if (_id_##_len >= FLT_OTEL_ID_MAXLEN) { \
FLT_OTEL_ERR("'%s' : name too long", id); \
\
OTELC_RETURN_PTR(retptr); \
} \
\
if (head != NULL) \
list_for_each_entry(ptr, head, list) \
if (strcmp(ptr->_id_, id) == 0) { \
FLT_OTEL_ERR("'%s' : already defined", id); \
\
OTELC_RETURN_PTR(retptr); \
} \
\
retptr = OTELC_CALLOC(1, sizeof(*retptr)); \
if (retptr != NULL) { \
retptr->cfg_line = line; \
retptr->_id_##_len = _id_##_len; \
retptr->_id_ = OTELC_STRDUP(id); \
if (retptr->_id_ != NULL) { \
if (head != NULL) \
LIST_APPEND(head, &(retptr->list)); \
\
FLT_OTEL_DBG_CONF_HDR("- conf_" #_type_ " init ", retptr, _id_); \
} \
else \
OTELC_SFREE_CLEAR(retptr); \
} \
\
if (retptr != NULL) { \
_func_ \
} \
\
if (retptr == NULL) \
FLT_OTEL_ERR("out of memory"); \
\
OTELC_RETURN_PTR(retptr); \
}
/*
* Macro that generates a flt_otel_conf_<type>_free() function. The generated
* function runs a custom cleanup body, then frees the name string, removes the
* structure from its list, and frees the structure.
*/
#define FLT_OTEL_CONF_FUNC_FREE(_type_, _id_, _func_) \
void flt_otel_conf_##_type_##_free(struct flt_otel_conf_##_type_ **ptr) \
{ \
OTELC_FUNC("%p:%p", OTELC_DPTR_ARGS(ptr)); \
\
if ((ptr == NULL) || (*ptr == NULL)) \
OTELC_RETURN(); \
\
{ _func_ } \
\
OTELC_SFREE((*ptr)->_id_); \
FLT_OTEL_LIST_DEL(&((*ptr)->list)); \
OTELC_SFREE_CLEAR(*ptr); \
\
OTELC_RETURN(); \
}
/* The FLT_OTEL_LIST_DESTROY() macro uses the following two definitions. */
#define flt_otel_conf_ph_group_free flt_otel_conf_ph_free
#define flt_otel_conf_ph_scope_free flt_otel_conf_ph_free
/* Declare init/free function prototypes for a configuration type. */
#define FLT_OTEL_CONF_FUNC_DECL(_type_) \
struct flt_otel_conf_##_type_ *flt_otel_conf_##_type_##_init(const char *id, int line, struct list *head, char **err); \
void flt_otel_conf_##_type_##_free(struct flt_otel_conf_##_type_ **ptr);
FLT_OTEL_CONF_FUNC_DECL(hdr)
FLT_OTEL_CONF_FUNC_DECL(str)
FLT_OTEL_CONF_FUNC_DECL(ph)
FLT_OTEL_CONF_FUNC_DECL(sample_expr)
FLT_OTEL_CONF_FUNC_DECL(sample)
FLT_OTEL_CONF_FUNC_DECL(link)
FLT_OTEL_CONF_FUNC_DECL(context)
FLT_OTEL_CONF_FUNC_DECL(span)
FLT_OTEL_CONF_FUNC_DECL(scope)
FLT_OTEL_CONF_FUNC_DECL(instrument)
FLT_OTEL_CONF_FUNC_DECL(log_record)
FLT_OTEL_CONF_FUNC_DECL(group)
FLT_OTEL_CONF_FUNC_DECL(instr)
#endif /* _OTEL_CONF_FUNCS_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,34 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_CONFIG_H_
#define _OTEL_CONFIG_H_
/* Memory pool selection flags. */
#define USE_POOL_BUFFER
#define USE_POOL_OTEL_SPAN_CONTEXT
#define USE_POOL_OTEL_SCOPE_SPAN
#define USE_POOL_OTEL_SCOPE_CONTEXT
#define USE_POOL_OTEL_RUNTIME_CONTEXT
#define USE_TRASH_CHUNK
/* Enable per-event and per-stream diagnostic counters in debug builds. */
#if defined(DEBUG_OTEL) && !defined(FLT_OTEL_USE_COUNTERS)
# define FLT_OTEL_USE_COUNTERS
#endif
#define FLT_OTEL_ID_MAXLEN 64 /* Maximum identifier length. */
#define FLT_OTEL_DEBUG_LEVEL 0b11101111111 /* Default debug bitmask. */
#define FLT_OTEL_ATTR_INIT_SIZE 8 /* Initial attribute array capacity. */
#define FLT_OTEL_ATTR_INC_SIZE 4 /* Attribute array growth increment. */
#endif /* _OTEL_CONFIG_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,55 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_DEBUG_H_
#define _OTEL_DEBUG_H_
#ifdef DEBUG_FULL
# define DEBUG_OTEL
#endif
/*
* FLT_OTEL_DBG_ARGS - include extra debug-only function parameters.
* FLT_OTEL_DBG_BUF - dump a buffer structure for debugging.
*
* When DEBUG_OTEL is not defined, these expand to nothing.
*/
#ifdef DEBUG_OTEL
# define FLT_OTEL_DBG_ARGS(a, ...) a, ##__VA_ARGS__
# define FLT_OTEL_DBG_BUF(l,a) OTELC_DBG(l, "%p:{ %zu %p %zu %zu }", (a), (a)->size, (a)->area, (a)->data, (a)->head)
#else
# define FLT_OTEL_DBG_ARGS(...)
# define FLT_OTEL_DBG_BUF(...) while (0)
#endif /* DEBUG_OTEL */
/*
* ON | NOLOGNORM |
* -----+-----------+-------------
* 0 | 0 | no log
* 0 | 1 | no log
* 1 | 0 | log all
* 1 | 1 | log errors
* -----+-----------+-------------
*/
#define FLT_OTEL_LOG(l,f, ...) \
do { \
if (!(conf->instr->logging & FLT_OTEL_LOGGING_ON)) \
OTELC_DBG(DEBUG, "NOLOG[%d]: [" FLT_OTEL_SCOPE "]: [%s] " f, (l), conf->id, ##__VA_ARGS__); \
else if ((conf->instr->logging & FLT_OTEL_LOGGING_NOLOGNORM) && ((l) > LOG_ERR)) \
OTELC_DBG(NOTICE, "NOLOG[%d]: [" FLT_OTEL_SCOPE "]: [%s] " f, (l), conf->id, ##__VA_ARGS__); \
else { \
send_log(&(conf->instr->proxy_log), (l), "[" FLT_OTEL_SCOPE "]: [%s] " f "\n", conf->id, ##__VA_ARGS__); \
\
OTELC_DBG(INFO, "LOG[%d]: %s", (l), logline); \
} \
} while (0)
#endif /* _OTEL_DEBUG_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,98 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_DEFINE_H_
#define _OTEL_DEFINE_H_
/* Safe pointer dereference with default value. */
#define FLT_OTEL_DEREF(p,m,v) (((p) != NULL) ? (p)->m : (v))
/* Check whether argument at index n is in range, non-NULL and non-empty. */
#define FLT_OTEL_ARG_ISVALID(n) ({ typeof(n) _n = (n); OTELC_IN_RANGE(_n, 0, MAX_LINE_ARGS - 1) && (args[_n] != NULL) && (*args[_n] != '\0'); })
/* Convert a uint32_t rate value to a floating-point percentage. */
#define FLT_OTEL_U32_FLOAT(a) ((a) * 100.0 / UINT32_MAX)
/* Convert a floating-point percentage to a uint32_t rate value. */
#define FLT_OTEL_FLOAT_U32(a) ((uint32_t)((a) / 100.0 * UINT32_MAX + 0.5))
#define FLT_OTEL_STR_DASH_72 "------------------------------------------------------------------------"
#define FLT_OTEL_STR_DASH_78 FLT_OTEL_STR_DASH_72 "------"
#define FLT_OTEL_STR_FLAG_YN(a) ((a) ? "yes" : "no")
/* Compile-time string length excluding the null terminator. */
#define FLT_OTEL_STR_SIZE(a) (sizeof(a) - 1)
/* Expand to address and length pair for a string literal. */
#define FLT_OTEL_STR_ADDRSIZE(a) (a), FLT_OTEL_STR_SIZE(a)
/* Compare a runtime string against a compile-time string literal. */
#define FLT_OTEL_STR_CMP(S,s) ((s##_len == FLT_OTEL_STR_SIZE(S)) && (memcmp((s), FLT_OTEL_STR_ADDRSIZE(S)) == 0))
/* Tolerance for double comparison in flt_otel_qsort_compar_double(). */
#define FLT_OTEL_DBL_EPSILON 1e-9
/* Execute a statement exactly once across all invocations. */
#define FLT_OTEL_RUN_ONCE(f) do { static bool _f = 1; if (_f) { _f = 0; { f; } } } while (0)
/* Check whether a list head has been initialized. */
#define FLT_OTEL_LIST_ISVALID(a) ({ typeof(a) _a = (a); (_a != NULL) && (_a->n != NULL) && (_a->p != NULL); })
/* Safely delete a list element if its list head is valid. */
#define FLT_OTEL_LIST_DEL(a) do { if (FLT_OTEL_LIST_ISVALID(a)) LIST_DELETE(a); } while (0)
/* Destroy all elements in a typed configuration list. */
#define FLT_OTEL_LIST_DESTROY(t,h) \
do { \
struct flt_otel_conf_##t *_ptr, *_back; \
\
if (!FLT_OTEL_LIST_ISVALID(h) || LIST_ISEMPTY(h)) \
break; \
\
OTELC_DBG(NOTICE, "- deleting " #t " list %s", flt_otel_list_dump(h)); \
\
list_for_each_entry_safe(_ptr, _back, (h), list) \
flt_otel_conf_##t##_free(&_ptr); \
} while (0)
/* Declare a rotating thread-local string buffer pool. */
#define FLT_OTEL_BUFFER_THR(b,m,n,p) \
static THREAD_LOCAL char b[m][n]; \
static THREAD_LOCAL size_t __idx = 0; \
char *p = b[__idx]; \
__idx = (__idx + 1) % (m)
/* Format an error message if none has been set yet. */
#define FLT_OTEL_ERR(f, ...) \
do { \
if ((err != NULL) && (*err == NULL)) { \
(void)memprintf(err, f, ##__VA_ARGS__); \
\
OTELC_DBG(DEBUG, "err: '%s'", *err); \
} \
} while (0)
/* Append to an existing error message unconditionally. */
#define FLT_OTEL_ERR_APPEND(f, ...) \
do { \
if (err != NULL) \
(void)memprintf(err, f, ##__VA_ARGS__); \
} while (0)
/* Log an error message and free its memory. */
#define FLT_OTEL_ERR_FREE(p) \
do { \
if ((p) == NULL) \
break; \
\
OTELC_DBG(LOG, "%s:%d: ERROR: %s", __func__, __LINE__, (p)); \
OTELC_SFREE_CLEAR(p); \
} while (0)
#endif /* _OTEL_DEFINE_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

152
addons/otel/include/event.h Normal file
View file

@ -0,0 +1,152 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_EVENT_H_
#define _OTEL_EVENT_H_
/*
* This must be defined in order for macro FLT_OTEL_EVENT_DEFINES
* and structure flt_otel_event_data to have the correct contents.
*/
#define AN__NONE 0
#define AN__STREAM_START 0 /* on-stream-start */
#define AN__STREAM_STOP 0 /* on-stream-stop */
#define AN__IDLE_TIMEOUT 0 /* on-idle-timeout */
#define AN__BACKEND_SET 0 /* on-backend-set */
#define AN_REQ_HTTP_HEADERS 0 /* on-http-headers-request */
#define AN_RES_HTTP_HEADERS 0 /* on-http-headers-response */
#define AN_REQ_HTTP_END 0 /* on-http-end-request */
#define AN_RES_HTTP_END 0 /* on-http-end-response */
#define AN_RES_HTTP_REPLY 0 /* on-http-reply */
#define AN_REQ_CLIENT_SESS_START 0
#define AN_REQ_SERVER_UNAVAILABLE 0
#define AN_REQ_CLIENT_SESS_END 0
#define AN_RES_SERVER_SESS_START 0
#define AN_RES_SERVER_SESS_END 0
#define SMP_VAL_FE_ 0
#define SMP_VAL_BE_ 0
#define SMP_OPT_DIR_ 0xff
/*
* Event names are selected to be somewhat compatible with the SPOE filter,
* from which the following names are taken:
* - on-client-session -> on-client-session-start
* - on-frontend-tcp-request
* - on-frontend-http-request
* - on-backend-tcp-request
* - on-backend-http-request
* - on-server-session -> on-server-session-start
* - on-tcp-response
* - on-http-response
*
* FLT_OTEL_EVENT_NONE is used as an index for 'otel-scope' sections that do not
* have an event defined. The 'otel-scope' sections thus defined can be used
* within the 'otel-group' section.
*
* A description of the macro arguments can be found in the structure
* flt_otel_event_data definition.
*
* The following table is derived from the definitions AN_REQ_* and AN_RES_*
* found in the HAProxy include file include/haproxy/channel-t.h.
*/
#define FLT_OTEL_EVENT_DEFINES \
/* Stream lifecycle pseudo-events (an_bit = 0, not tied to a channel analyzer) */ \
FLT_OTEL_EVENT_DEF( NONE, , , , 0, "") \
FLT_OTEL_EVENT_DEF( STREAM_START, , , , 0, "on-stream-start") \
FLT_OTEL_EVENT_DEF( STREAM_STOP, , , , 0, "on-stream-stop") \
FLT_OTEL_EVENT_DEF( CLIENT_SESS_START, REQ, CON_ACC, , 1, "on-client-session-start") \
FLT_OTEL_EVENT_DEF( IDLE_TIMEOUT, , , , 0, "on-idle-timeout") \
FLT_OTEL_EVENT_DEF( BACKEND_SET, , , , 0, "on-backend-set") \
\
/* Request analyzers */ \
/* FLT_OTEL_EVENT_DEF( FLT_START_FE, REQ, , , , "on-filter-start") */ \
FLT_OTEL_EVENT_DEF( INSPECT_FE, REQ, REQ_CNT, , 1, "on-frontend-tcp-request") \
FLT_OTEL_EVENT_DEF( WAIT_HTTP, REQ, , , 1, "on-http-wait-request") \
FLT_OTEL_EVENT_DEF( HTTP_BODY, REQ, , , 1, "on-http-body-request") \
FLT_OTEL_EVENT_DEF( HTTP_PROCESS_FE, REQ, HRQ_HDR, , 1, "on-frontend-http-request") \
FLT_OTEL_EVENT_DEF( SWITCHING_RULES, REQ, , , 1, "on-switching-rules-request") \
/* FLT_OTEL_EVENT_DEF( FLT_START_BE, REQ, , , , "") */ \
FLT_OTEL_EVENT_DEF( INSPECT_BE, REQ, REQ_CNT, REQ_CNT, 1, "on-backend-tcp-request") \
FLT_OTEL_EVENT_DEF( HTTP_PROCESS_BE, REQ, HRQ_HDR, HRQ_HDR, 1, "on-backend-http-request") \
/* FLT_OTEL_EVENT_DEF( HTTP_TARPIT, REQ, , , 1, "on-http-tarpit-request") */ \
FLT_OTEL_EVENT_DEF( SRV_RULES, REQ, , , 1, "on-process-server-rules-request") \
FLT_OTEL_EVENT_DEF( HTTP_INNER, REQ, , , 1, "on-http-process-request") \
FLT_OTEL_EVENT_DEF( PRST_RDP_COOKIE, REQ, , , 1, "on-tcp-rdp-cookie-request") \
FLT_OTEL_EVENT_DEF( STICKING_RULES, REQ, , , 1, "on-process-sticking-rules-request") \
/* FLT_OTEL_EVENT_DEF( FLT_HTTP_HDRS, REQ, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( HTTP_XFER_BODY, REQ, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( WAIT_CLI, REQ, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( FLT_XFER_DATA, REQ, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( FLT_END, REQ, , , , "") */ \
FLT_OTEL_EVENT_DEF( HTTP_HEADERS, REQ, HRQ_HDR, HRQ_HDR, 1, "on-http-headers-request") \
FLT_OTEL_EVENT_DEF( HTTP_END, REQ, , , 1, "on-http-end-request") \
FLT_OTEL_EVENT_DEF( CLIENT_SESS_END, REQ, , , 0, "on-client-session-end") \
FLT_OTEL_EVENT_DEF(SERVER_UNAVAILABLE, REQ, , , 0, "on-server-unavailable") \
\
/* Response analyzers */ \
FLT_OTEL_EVENT_DEF( SERVER_SESS_START, RES, , SRV_CON, 0, "on-server-session-start") \
/* FLT_OTEL_EVENT_DEF( FLT_START_FE, RES, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( FLT_START_BE, RES, , , , "") */ \
FLT_OTEL_EVENT_DEF( INSPECT, RES, RES_CNT, RES_CNT, 0, "on-tcp-response") \
FLT_OTEL_EVENT_DEF( WAIT_HTTP, RES, , , 1, "on-http-wait-response") \
FLT_OTEL_EVENT_DEF( STORE_RULES, RES, , , 1, "on-process-store-rules-response") \
FLT_OTEL_EVENT_DEF( HTTP_PROCESS_BE, RES, HRS_HDR, HRS_HDR, 1, "on-http-response") \
FLT_OTEL_EVENT_DEF( HTTP_HEADERS, RES, HRS_HDR, HRS_HDR, 1, "on-http-headers-response") \
FLT_OTEL_EVENT_DEF( HTTP_END, RES, , , 0, "on-http-end-response") \
FLT_OTEL_EVENT_DEF( HTTP_REPLY, RES, , , 0, "on-http-reply") \
/* FLT_OTEL_EVENT_DEF( HTTP_PROCESS_FE, RES, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( FLT_HTTP_HDRS, RES, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( HTTP_XFER_BODY, RES, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( WAIT_CLI, RES, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( FLT_XFER_DATA, RES, , , , "") */ \
/* FLT_OTEL_EVENT_DEF( FLT_END, RES, , , , "") */ \
FLT_OTEL_EVENT_DEF( SERVER_SESS_END, RES, , , 0, "on-server-session-end")
enum FLT_OTEL_EVENT_enum {
#define FLT_OTEL_EVENT_DEF(a,b,c,d,e,f) FLT_OTEL_EVENT_##b##_##a,
FLT_OTEL_EVENT_DEFINES
FLT_OTEL_EVENT_MAX
#undef FLT_OTEL_EVENT_DEF
};
/* Sample data types associated with a scope event. */
enum FLT_OTEL_EVENT_SAMPLE_enum {
FLT_OTEL_EVENT_SAMPLE_ATTRIBUTE = 0,
FLT_OTEL_EVENT_SAMPLE_EVENT,
FLT_OTEL_EVENT_SAMPLE_BAGGAGE,
FLT_OTEL_EVENT_SAMPLE_STATUS,
};
/* Per-event metadata mapping analyzer bits to filter event names. */
struct flt_otel_event_data {
uint an_bit; /* Used channel analyser. */
const char *an_name; /* Channel analyser name. */
uint smp_opt_dir; /* Fetch direction (request/response). */
uint smp_val_fe; /* Valid FE fetch location. */
uint smp_val_be; /* Valid BE fetch location. */
bool flag_http_inject; /* Span context injection allowed. */
const char *name; /* Filter event name. */
};
struct flt_otel_conf_scope;
/* Per-event metadata table indexed by FLT_OTEL_EVENT_* constants. */
extern const struct flt_otel_event_data flt_otel_event_data[FLT_OTEL_EVENT_MAX];
/* Execute a single scope: create spans, record instruments, evaluate samples. */
int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, struct flt_otel_conf_scope *conf_scope, const struct timespec *ts_steady, const struct timespec *ts_system, uint dir, char **err);
/* Run all scopes matching a filter event on the given stream and channel. */
int flt_otel_event_run(struct stream *s, struct filter *f, struct channel *chn, int event, char **err);
#endif /* _OTEL_EVENT_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,55 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_FILTER_H_
#define _OTEL_FILTER_H_
#define FLT_OTEL_FMT_NAME "'" FLT_OTEL_OPT_NAME "' : "
#define FLT_OTEL_FMT_TYPE "'filter' : "
#define FLT_OTEL_VAR_UUID "sess", "otel", "uuid"
#define FLT_OTEL_ALERT(f, ...) ha_alert(FLT_OTEL_FMT_TYPE FLT_OTEL_FMT_NAME f "\n", ##__VA_ARGS__)
#define FLT_OTEL_CONDITION_IF "if"
#define FLT_OTEL_CONDITION_UNLESS "unless"
/* Return codes for OTel filter operations. */
enum FLT_OTEL_RET_enum {
FLT_OTEL_RET_ERROR = -1,
FLT_OTEL_RET_WAIT = 0,
FLT_OTEL_RET_IGNORE = 0,
FLT_OTEL_RET_OK = 1,
};
/* Dump or iterate a named configuration list for debugging. */
#define FLT_OTEL_DBG_LIST(d,m,p,t,v,f) \
do { \
if (LIST_ISEMPTY(&((d)->m##s))) { \
OTELC_DBG(DEBUG, p "- no " #m "s " t); \
} else { \
const struct flt_otel_conf_##m *v; \
\
OTELC_DBG(DEBUG, p "- " t " " #m "s: %s", \
flt_otel_list_dump(&((d)->m##s))); \
list_for_each_entry(v, &((d)->m##s), list) \
do { f; } while (0); \
} \
} while (0)
extern const char *otel_flt_id;
extern uint64_t flt_otel_drop_cnt;
extern struct flt_ops flt_otel_ops;
/* Check whether the OTel filter is disabled for a stream. */
bool flt_otel_is_disabled(const struct filter *f FLT_OTEL_DBG_ARGS(, int event));
#endif /* _OTEL_FILTER_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,46 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_GROUP_H_
#define _OTEL_GROUP_H_
#define FLT_OTEL_ACTION_GROUP "otel-group"
/* Argument indices for the otel-group action rule. */
enum FLT_OTEL_ARG_enum {
FLT_OTEL_ARG_FILTER_ID = 0,
FLT_OTEL_ARG_GROUP_ID,
FLT_OTEL_ARG_FLT_CONF = 0,
FLT_OTEL_ARG_CONF,
FLT_OTEL_ARG_GROUP,
};
/*
* A description of the macro arguments can be found in the structure
* flt_otel_group_data definition
*/
#define FLT_OTEL_GROUP_DEFINES \
FLT_OTEL_GROUP_DEF(ACT_F_TCP_REQ_CON, SMP_VAL_FE_CON_ACC, SMP_OPT_DIR_REQ) \
FLT_OTEL_GROUP_DEF(ACT_F_TCP_REQ_SES, SMP_VAL_FE_SES_ACC, SMP_OPT_DIR_REQ) \
FLT_OTEL_GROUP_DEF(ACT_F_TCP_REQ_CNT, SMP_VAL_FE_REQ_CNT, SMP_OPT_DIR_REQ) \
FLT_OTEL_GROUP_DEF(ACT_F_TCP_RES_CNT, SMP_VAL_BE_RES_CNT, SMP_OPT_DIR_RES) \
FLT_OTEL_GROUP_DEF(ACT_F_HTTP_REQ, SMP_VAL_FE_HRQ_HDR, SMP_OPT_DIR_REQ) \
FLT_OTEL_GROUP_DEF(ACT_F_HTTP_RES, SMP_VAL_BE_HRS_HDR, SMP_OPT_DIR_RES)
/* Per-action-from metadata mapping action types to fetch directions. */
struct flt_otel_group_data {
enum act_from act_from; /* ACT_F_* */
uint smp_val; /* Valid FE/BE fetch location. */
uint smp_opt_dir; /* Fetch direction (request/response). */
};
#endif /* _OTEL_GROUP_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,31 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_HTTP_H_
#define _OTEL_HTTP_H_
#ifndef DEBUG_OTEL
# define flt_otel_http_headers_dump(...) while (0)
#else
/* Dump all HTTP headers from a channel for debugging. */
void flt_otel_http_headers_dump(const struct channel *chn);
#endif
/* Extract HTTP headers matching a prefix into a text map. */
struct otelc_text_map *flt_otel_http_headers_get(struct channel *chn, const char *prefix, size_t len, char **err);
/* Set or replace an HTTP header in a channel. */
int flt_otel_http_header_set(struct channel *chn, const char *prefix, const char *name, const char *value, char **err);
/* Remove all HTTP headers matching a prefix from a channel. */
int flt_otel_http_headers_remove(struct channel *chn, const char *prefix, char **err);
#endif /* _OTEL_HTTP_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,54 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_INCLUDE_H_
#define _OTEL_INCLUDE_H_
#include <errno.h>
#include <stdbool.h>
#include <math.h>
#include <values.h>
#include <haproxy/api.h>
#include <haproxy/cfgparse.h>
#include <haproxy/acl.h>
#include <haproxy/cli.h>
#include <haproxy/clock.h>
#include <haproxy/filters.h>
#include <haproxy/http_htx.h>
#include <haproxy/http_rules.h>
#include <haproxy/log.h>
#include <haproxy/proxy.h>
#include <haproxy/sample.h>
#include <haproxy/tcp_rules.h>
#include <haproxy/tools.h>
#include <haproxy/vars.h>
#include <opentelemetry-c-wrapper/include.h>
#include "config.h"
#include "debug.h"
#include "define.h"
#include "cli.h"
#include "event.h"
#include "conf.h"
#include "conf_funcs.h"
#include "filter.h"
#include "group.h"
#include "http.h"
#include "otelc.h"
#include "parser.h"
#include "pool.h"
#include "scope.h"
#include "util.h"
#include "vars.h"
#endif /* _OTEL_INCLUDE_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,27 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_OTELC_H_
#define _OTEL_OTELC_H_
/* Inject span context into a text map carrier. */
int flt_otel_inject_text_map(const struct otelc_span *span, struct otelc_text_map_writer *carrier);
/* Inject span context into an HTTP headers carrier. */
int flt_otel_inject_http_headers(const struct otelc_span *span, struct otelc_http_headers_writer *carrier);
/* Extract span context from a text map carrier. */
struct otelc_span_context *flt_otel_extract_text_map(struct otelc_tracer *tracer, struct otelc_text_map_reader *carrier, const struct otelc_text_map *text_map);
/* Extract span context from an HTTP headers carrier. */
struct otelc_span_context *flt_otel_extract_http_headers(struct otelc_tracer *tracer, struct otelc_http_headers_reader *carrier, const struct otelc_text_map *text_map);
#endif /* _OTEL_OTELC_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,221 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_PARSER_H_
#define _OTEL_PARSER_H_
#define FLT_OTEL_SCOPE "OTEL"
/*
* filter FLT_OTEL_OPT_NAME FLT_OTEL_OPT_FILTER_ID <FLT_OTEL_OPT_FILTER_ID_DEFAULT> FLT_OTEL_OPT_CONFIG <file>
*/
#define FLT_OTEL_OPT_NAME "opentelemetry"
#define FLT_OTEL_OPT_FILTER_ID "id"
#define FLT_OTEL_OPT_FILTER_ID_DEFAULT "otel-filter"
#define FLT_OTEL_OPT_CONFIG "config"
#define FLT_OTEL_PARSE_SECTION_INSTR_ID "otel-instrumentation"
#define FLT_OTEL_PARSE_SECTION_GROUP_ID "otel-group"
#define FLT_OTEL_PARSE_SECTION_SCOPE_ID "otel-scope"
#define FLT_OTEL_PARSE_SPAN_ROOT "root"
#define FLT_OTEL_PARSE_SPAN_PARENT "parent"
#define FLT_OTEL_PARSE_SPAN_LINK "link"
#define FLT_OTEL_PARSE_INSTRUMENT_DESC "desc"
#define FLT_OTEL_PARSE_INSTRUMENT_VALUE "value"
#define FLT_OTEL_PARSE_INSTRUMENT_ATTR "attr"
#define FLT_OTEL_PARSE_INSTRUMENT_UNIT "unit"
#define FLT_OTEL_PARSE_INSTRUMENT_BOUNDS "bounds"
#define FLT_OTEL_PARSE_INSTRUMENT_AGGR "aggr"
#define FLT_OTEL_PARSE_LOG_RECORD_ID "id"
#define FLT_OTEL_PARSE_LOG_RECORD_EVENT "event"
#define FLT_OTEL_PARSE_LOG_RECORD_SPAN "span"
#define FLT_OTEL_PARSE_LOG_RECORD_ATTR "attr"
#define FLT_OTEL_PARSE_CTX_AUTONAME "-"
#define FLT_OTEL_PARSE_CTX_IGNORE_NAME '-'
#define FLT_OTEL_PARSE_CTX_USE_HEADERS "use-headers"
#define FLT_OTEL_PARSE_CTX_USE_VARS "use-vars"
#define FLT_OTEL_PARSE_OPTION_HARDERR "hard-errors"
#define FLT_OTEL_PARSE_OPTION_DISABLED "disabled"
#define FLT_OTEL_PARSE_OPTION_NOLOGNORM "dontlog-normal"
/*
* A description of the macro arguments can be found in the structure
* flt_otel_parse_data definition
*/
#define FLT_OTEL_PARSE_INSTR_DEFINES \
FLT_OTEL_PARSE_INSTR_DEF( ID, 0, CHAR, 2, 2, "otel-instrumentation", " <name>") \
FLT_OTEL_PARSE_INSTR_DEF( ACL, 0, CHAR, 3, 0, "acl", " <name> <criterion> [flags] [operator] <value> ...") \
FLT_OTEL_PARSE_INSTR_DEF( LOG, 0, CHAR, 2, 0, "log", " { global | <addr> [len <len>] [format <fmt>] <facility> [<level> [<minlevel>]] }") \
FLT_OTEL_PARSE_INSTR_DEF( CONFIG, 0, NONE, 2, 2, "config", " <file>") \
FLT_OTEL_PARSE_INSTR_DEF( GROUPS, 0, NONE, 2, 0, "groups", " <name> ...") \
FLT_OTEL_PARSE_INSTR_DEF( SCOPES, 0, NONE, 2, 0, "scopes", " <name> ...") \
FLT_OTEL_PARSE_INSTR_DEF( RATE_LIMIT, 0, NONE, 2, 2, "rate-limit", " <value>") \
FLT_OTEL_PARSE_INSTR_DEF( OPTION, 0, NONE, 2, 2, "option", " { disabled | dontlog-normal | hard-errors }") \
FLT_OTEL_PARSE_INSTR_DEF(DEBUG_LEVEL, 0, NONE, 2, 2, "debug-level", " <value>")
#define FLT_OTEL_PARSE_GROUP_DEFINES \
FLT_OTEL_PARSE_GROUP_DEF( ID, 0, CHAR, 2, 2, "otel-group", " <name>") \
FLT_OTEL_PARSE_GROUP_DEF(SCOPES, 0, NONE, 2, 0, "scopes", " <name> ...")
#ifdef USE_OTEL_VARS
# define FLT_OTEL_PARSE_SCOPE_INJECT_HELP " <name-prefix> [use-vars] [use-headers]"
# define FLT_OTEL_PARSE_SCOPE_EXTRACT_HELP " <name-prefix> [use-vars | use-headers]"
#else
# define FLT_OTEL_PARSE_SCOPE_INJECT_HELP " <name-prefix> [use-headers]"
# define FLT_OTEL_PARSE_SCOPE_EXTRACT_HELP " <name-prefix> [use-headers]"
#endif
/*
* The first argument of the FLT_OTEL_PARSE_SCOPE_STATUS_DEF() macro is defined
* as otelc_span_status_t in <opentelemetry-c-wrapper/span.h> .
*/
#define FLT_OTEL_PARSE_SCOPE_STATUS_DEFINES \
FLT_OTEL_PARSE_SCOPE_STATUS_DEF(IGNORE, "ignore") \
FLT_OTEL_PARSE_SCOPE_STATUS_DEF( UNSET, "unset" ) \
FLT_OTEL_PARSE_SCOPE_STATUS_DEF( OK, "ok" ) \
FLT_OTEL_PARSE_SCOPE_STATUS_DEF( ERROR, "error" )
/* Sentinel: instrument has not been created yet. */
#define OTELC_METRIC_INSTRUMENT_UNSET -1
/* Sentinel: instrument creation is in progress by another thread. */
#define OTELC_METRIC_INSTRUMENT_PENDING -2
/* Sentinel: update-form instrument (re-evaluates an existing one). */
#define OTELC_METRIC_INSTRUMENT_UPDATE 0xff
#define OTELC_METRIC_AGGREGATION_UNSET -1
/*
* Observable (asynchronous) instruments are not supported. The OTel SDK
* invokes their callbacks from an external background thread that is not a
* HAProxy thread. HAProxy sample fetches rely on internal per-thread-group
* state and return incorrect results when called from a non-HAProxy thread.
*
* Double-precision instruments are not supported because HAProxy sample fetches
* do not return double values.
*/
#define FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEFINES \
FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEF(UPDATE, "update" ) \
FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEF(COUNTER_UINT64, "cnt_int" ) \
FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEF(HISTOGRAM_UINT64, "hist_int" ) \
FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEF(UDCOUNTER_INT64, "udcnt_int") \
FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEF(GAUGE_INT64, "gauge_int")
/*
* In case the possibility of working with OpenTelemetry context via HAProxy
* variables is not used, args_max member of the structure flt_otel_parse_data
* should be reduced for 'inject' keyword. However, this is not critical
* because in this case the 'use-vars' argument cannot be entered anyway,
* so I will not complicate it here with additional definitions.
*/
#define FLT_OTEL_PARSE_SCOPE_DEFINES \
FLT_OTEL_PARSE_SCOPE_DEF( ID, 0, CHAR, 2, 2, "otel-scope", " <name>") \
FLT_OTEL_PARSE_SCOPE_DEF( SPAN, 0, NONE, 2, 7, "span", " <name> [<reference>] [<link>] [root]") \
FLT_OTEL_PARSE_SCOPE_DEF( LINK, 1, NONE, 2, 0, "link", " <span> ...") \
FLT_OTEL_PARSE_SCOPE_DEF( ATTRIBUTE, 1, NONE, 3, 0, "attribute", " <key> <sample> ...") \
FLT_OTEL_PARSE_SCOPE_DEF( EVENT, 1, NONE, 4, 0, "event", " <name> <key> <sample> ...") \
FLT_OTEL_PARSE_SCOPE_DEF( BAGGAGE, 1, VAR, 3, 0, "baggage", " <key> <sample> ...") \
FLT_OTEL_PARSE_SCOPE_DEF( INJECT, 1, CTX, 2, 4, "inject", FLT_OTEL_PARSE_SCOPE_INJECT_HELP) \
FLT_OTEL_PARSE_SCOPE_DEF( EXTRACT, 0, CTX, 2, 3, "extract", FLT_OTEL_PARSE_SCOPE_EXTRACT_HELP) \
FLT_OTEL_PARSE_SCOPE_DEF( STATUS, 1, NONE, 2, 0, "status", " <code> [<sample> ...]") \
FLT_OTEL_PARSE_SCOPE_DEF( FINISH, 0, NONE, 2, 0, "finish", " <name> ...") \
FLT_OTEL_PARSE_SCOPE_DEF( INSTRUMENT, 0, NONE, 3, 0, "instrument", " { update <name> [<attr> ...] | <type> <name> [<aggr>] [<desc>] [<unit>] <value> [<bounds>] }") \
FLT_OTEL_PARSE_SCOPE_DEF( LOG_RECORD, 0, NONE, 3, 0, "log-record", " <severity> [<id>] [<event>] [<span>] [<attr>] <sample> ...") \
FLT_OTEL_PARSE_SCOPE_DEF(IDLE_TIMEOUT, 0, NONE, 2, 2, "idle-timeout", " <time>") \
FLT_OTEL_PARSE_SCOPE_DEF( ACL, 0, CHAR, 3, 0, "acl", " <name> <criterion> [flags] [operator] <value> ...") \
FLT_OTEL_PARSE_SCOPE_DEF( ON_EVENT, 0, NONE, 2, 0, "otel-event", " <name> [{ if | unless } <condition>]")
/* Invalid character check modes for identifier validation. */
enum FLT_OTEL_PARSE_INVCHAR_enum {
FLT_OTEL_PARSE_INVALID_NONE,
FLT_OTEL_PARSE_INVALID_CHAR,
FLT_OTEL_PARSE_INVALID_DOM,
FLT_OTEL_PARSE_INVALID_CTX,
FLT_OTEL_PARSE_INVALID_VAR,
};
enum FLT_OTEL_PARSE_INSTR_enum {
#define FLT_OTEL_PARSE_INSTR_DEF(a,b,c,d,e,f,g) FLT_OTEL_PARSE_INSTR_##a,
FLT_OTEL_PARSE_INSTR_DEFINES
#undef FLT_OTEL_PARSE_INSTR_DEF
};
enum FLT_OTEL_PARSE_GROUP_enum {
#define FLT_OTEL_PARSE_GROUP_DEF(a,b,c,d,e,f,g) FLT_OTEL_PARSE_GROUP_##a,
FLT_OTEL_PARSE_GROUP_DEFINES
#undef FLT_OTEL_PARSE_GROUP_DEF
};
enum FLT_OTEL_PARSE_SCOPE_enum {
#define FLT_OTEL_PARSE_SCOPE_DEF(a,b,c,d,e,f,g) FLT_OTEL_PARSE_SCOPE_##a,
FLT_OTEL_PARSE_SCOPE_DEFINES
#undef FLT_OTEL_PARSE_SCOPE_DEF
};
/* Context storage type flags for inject/extract operations. */
enum FLT_OTEL_CTX_USE_enum {
FLT_OTEL_CTX_USE_VARS = 1 << 0,
FLT_OTEL_CTX_USE_HEADERS = 1 << 1,
};
/* Logging state flags for the OTel filter. */
enum FLT_OTEL_LOGGING_enum {
FLT_OTEL_LOGGING_OFF = 0,
FLT_OTEL_LOGGING_ON = 1 << 0,
FLT_OTEL_LOGGING_NOLOGNORM = 1 << 1,
};
/* Keyword metadata used by the configuration section parsers. */
struct flt_otel_parse_data {
int keyword; /* Keyword index. */
bool flag_check_id; /* Whether the group ID must be defined for the keyword. */
int check_name; /* Checking allowed characters in the name. */
int args_min; /* The minimum number of arguments required. */
int args_max; /* The maximum number of arguments allowed. */
const char *name; /* Keyword name. */
const char *usage; /* Usage text to be printed in case of an error. */
};
#define FLT_OTEL_PARSE_KEYWORD(n,s) (strcmp(args[n], (s)) == 0)
#define FLT_OTEL_PARSE_WARNING(f, ...) \
ha_warning("parsing [%s:%d] : " FLT_OTEL_FMT_TYPE FLT_OTEL_FMT_NAME "'" f "'\n", ##__VA_ARGS__);
#define FLT_OTEL_PARSE_ALERT(f, ...) \
do { \
ha_alert("parsing [%s:%d] : " FLT_OTEL_FMT_TYPE FLT_OTEL_FMT_NAME "'" f "'\n", ##__VA_ARGS__); \
\
retval |= ERR_ABORT | ERR_ALERT; \
} while (0)
#define FLT_OTEL_POST_PARSE_ALERT(f, ...) \
FLT_OTEL_PARSE_ALERT(f, flt_otel_current_config->cfg_file, ##__VA_ARGS__)
#define FLT_OTEL_PARSE_ERR(e,f, ...) \
do { \
if (*(e) == NULL) \
(void)memprintf((e), f, ##__VA_ARGS__); \
\
retval |= ERR_ABORT | ERR_ALERT; \
} while (0)
#define FLT_OTEL_PARSE_IFERR_ALERT() \
do { \
if (err == NULL) \
break; \
\
FLT_OTEL_PARSE_ALERT("%s", file, line, err); \
FLT_OTEL_ERR_FREE(err); \
} while (0)
#endif /* _OTEL_PARSER_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,71 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_POOL_H_
#define _OTEL_POOL_H_
#define FLT_OTEL_POOL_INIT(p,n,s,r) \
do { \
if (((r) == FLT_OTEL_RET_OK) && ((p) == NULL)) { \
(p) = create_pool(n, (s), MEM_F_SHARED); \
if ((p) == NULL) \
(r) = FLT_OTEL_RET_ERROR; \
\
OTELC_DBG(DEBUG, #p " %p %u", (p), FLT_OTEL_DEREF((p), size, 0)); \
} \
} while (0)
#define FLT_OTEL_POOL_DESTROY(p) \
do { \
if ((p) != NULL) { \
OTELC_DBG(DEBUG, #p " %p %u", (p), (p)->size); \
\
pool_destroy(p); \
(p) = NULL; \
} \
} while (0)
extern struct pool_head *pool_head_otel_scope_span __read_mostly;
extern struct pool_head *pool_head_otel_scope_context __read_mostly;
extern struct pool_head *pool_head_otel_runtime_context __read_mostly;
extern struct pool_head *pool_head_otel_span_context __read_mostly;
/* Allocate memory from a pool with optional zeroing. */
void *flt_otel_pool_alloc(struct pool_head *pool, size_t size, bool flag_clear, char **err);
/* Duplicate a string into pool-allocated memory. */
void *flt_otel_pool_strndup(struct pool_head *pool, const char *s, size_t size, char **err);
/* Release pool-allocated memory and clear the pointer. */
void flt_otel_pool_free(struct pool_head *pool, void **ptr);
/* Initialize OTel filter memory pools. */
int flt_otel_pool_init(void);
/* Destroy OTel filter memory pools. */
void flt_otel_pool_destroy(void);
/* Log debug information about OTel filter memory pools. */
#ifndef DEBUG_OTEL
# define flt_otel_pool_info() while (0)
#else
void flt_otel_pool_info(void);
#endif
/* Allocate a trash buffer with optional zeroing. */
struct buffer *flt_otel_trash_alloc(bool flag_clear, char **err);
/* Release a trash buffer and clear the pointer. */
void flt_otel_trash_free(struct buffer **ptr);
#endif /* _OTEL_POOL_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

174
addons/otel/include/scope.h Normal file
View file

@ -0,0 +1,174 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_SCOPE_H_
#define _OTEL_SCOPE_H_
#define FLT_OTEL_SCOPE_SPAN_FINISH_REQ "*req*"
#define FLT_OTEL_SCOPE_SPAN_FINISH_RES "*res*"
#define FLT_OTEL_SCOPE_SPAN_FINISH_ALL "*"
#define FLT_OTEL_RT_CTX(p) ((struct flt_otel_runtime_context *)(p))
#define FLT_OTEL_DBG_SCOPE_SPAN(h,p) \
OTELC_DBG(DEBUG, h "%p:{ '%s' %zu %u %hhu %p %p %p }", (p), \
FLT_OTEL_STR_HDR_ARGS(p, id), (p)->smp_opt_dir, \
(p)->flag_finish, (p)->span, (p)->ref_span, (p)->ref_ctx)
#define FLT_OTEL_DBG_SCOPE_CONTEXT(h,p) \
OTELC_DBG(DEBUG, h "%p:{ '%s' %zu %u %hhu %p }", (p), \
FLT_OTEL_STR_HDR_ARGS(p, id), (p)->smp_opt_dir, \
(p)->flag_finish, (p)->context)
#define FLT_OTEL_DBG_SCOPE_DATA_EVENT(h,p) \
OTELC_DBG(DEBUG, h "%p:{ '%s' %p %zu %zu %s }", &(p), \
(p).name, (p).attr, (p).cnt, (p).size, \
flt_otel_list_dump(&((p).list)))
#define FLT_OTEL_DBG_SCOPE_DATA_STATUS(h,p) \
OTELC_DBG(DEBUG, h "%p:{ %d '%s' }", (p), (p)->code, OTELC_STR_ARG((p)->description))
#define FLT_OTEL_DBG_SCOPE_DATA_KV_FMT "%p:{ %p %zu %zu }"
#define FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS(p) &(p), (p).attr, (p).cnt, (p).size
#define FLT_OTEL_DBG_SCOPE_DATA(h,p) \
OTELC_DBG(DEBUG, h "%p:{ " FLT_OTEL_DBG_SCOPE_DATA_KV_FMT " " FLT_OTEL_DBG_SCOPE_DATA_KV_FMT " %s %s }", (p), \
FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS((p)->baggage), FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS((p)->attributes), \
flt_otel_list_dump(&((p)->events)), flt_otel_list_dump(&((p)->links)))
#define FLT_OTEL_DBG_RUNTIME_CONTEXT(h,p) \
OTELC_DBG(DEBUG, h "%p:{ %p %p '%s' %hhu %hhu 0x%02hhx 0x%08x %u %d %s %s }", (p), \
(p)->stream, (p)->filter, (p)->uuid, (p)->flag_harderr, (p)->flag_disabled, \
(p)->logging, (p)->analyzers, (p)->idle_timeout, (p)->idle_exp, \
flt_otel_list_dump(&((p)->spans)), flt_otel_list_dump(&((p)->contexts)))
/* Anonymous struct containing a const string pointer and its length. */
#define FLT_OTEL_CONST_STR_HDR(p) \
struct { \
const char *p; \
size_t p##_len; \
}
/* Growable key-value array for span attributes or baggage. */
struct flt_otel_scope_data_kv {
struct otelc_kv *attr; /* Key-value array for storing attributes. */
size_t cnt; /* Number of currently used array elements. */
size_t size; /* Total number of array elements. */
};
/* Named event with its own key-value attribute array. */
struct flt_otel_scope_data_event {
char *name; /* Event name, not used for other data types. */
struct otelc_kv *attr; /* Key-value array for storing attributes. */
size_t cnt; /* Number of currently used array elements. */
size_t size; /* Total number of array elements. */
struct list list; /* Used to chain this structure. */
};
/* Span link referencing another span or span context. */
struct flt_otel_scope_data_link {
struct otelc_span *span; /* Linked span, or NULL. */
struct otelc_span_context *context; /* Linked span context, or NULL. */
struct list list; /* Used to chain this structure. */
};
/* Span status code and description. */
struct flt_otel_scope_data_status {
int code; /* OTELC_SPAN_STATUS_* value. */
char *description; /* Span status description string. */
};
/* Aggregated runtime data collected during scope execution. */
struct flt_otel_scope_data {
struct flt_otel_scope_data_kv baggage; /* Defined scope baggage. */
struct flt_otel_scope_data_kv attributes; /* Defined scope attributes. */
struct list events; /* Defined scope events. */
struct list links; /* Defined scope links. */
struct flt_otel_scope_data_status status; /* Defined scope status. */
};
/* flt_otel_runtime_context->spans */
struct flt_otel_scope_span {
FLT_OTEL_CONST_STR_HDR(id); /* The span operation name/len. */
uint smp_opt_dir; /* SMP_OPT_DIR_RE(Q|S) */
bool flag_finish; /* Whether the span is marked for completion. */
struct otelc_span *span; /* The current span. */
struct otelc_span *ref_span; /* Span to which the current span refers. */
struct otelc_span_context *ref_ctx; /* Span context to which the current span refers. */
struct list list; /* Used to chain this structure. */
};
/* flt_otel_runtime_context->contexts */
struct flt_otel_scope_context {
FLT_OTEL_CONST_STR_HDR(id); /* The span context name/len. */
uint smp_opt_dir; /* SMP_OPT_DIR_RE(Q|S) */
bool flag_finish; /* Whether the span context is marked for completion. */
struct otelc_span_context *context; /* The current span context. */
struct list list; /* Used to chain this structure. */
};
/* The runtime filter context attached to a stream. */
struct flt_otel_runtime_context {
struct stream *stream; /* The stream to which the filter is attached. */
struct filter *filter; /* The OpenTelemetry filter. */
char uuid[40]; /* Randomly generated UUID. */
bool flag_harderr; /* [0 1] */
bool flag_disabled; /* [0 1] */
uint8_t logging; /* [0 1 3] */
uint analyzers; /* Executed channel analyzers. */
uint idle_timeout; /* Idle timeout interval in milliseconds (0 = off). */
int idle_exp; /* Tick at which the next idle timeout fires. */
struct list spans; /* The scope spans. */
struct list contexts; /* The scope contexts. */
};
#ifndef DEBUG_OTEL
# define flt_otel_scope_data_dump(...) while (0)
#else
/* Dump scope data contents for debugging. */
void flt_otel_scope_data_dump(const struct flt_otel_scope_data *data);
#endif
/* Allocate and initialize a runtime context for a stream. */
struct flt_otel_runtime_context *flt_otel_runtime_context_init(struct stream *s, struct filter *f, char **err);
/* Free the runtime context attached to a filter. */
void flt_otel_runtime_context_free(struct filter *f);
/* Allocate and initialize a scope span in the runtime context. */
struct flt_otel_scope_span *flt_otel_scope_span_init(struct flt_otel_runtime_context *rt_ctx, const char *id, size_t id_len, const char *ref_id, size_t ref_id_len, uint dir, char **err);
/* Free a scope span and remove it from the runtime context. */
void flt_otel_scope_span_free(struct flt_otel_scope_span **ptr);
/* Allocate and initialize a scope context in the runtime context. */
struct flt_otel_scope_context *flt_otel_scope_context_init(struct flt_otel_runtime_context *rt_ctx, struct otelc_tracer *tracer, const char *id, size_t id_len, const struct otelc_text_map *text_map, uint dir, char **err);
/* Free a scope context and remove it from the runtime context. */
void flt_otel_scope_context_free(struct flt_otel_scope_context **ptr);
/* Initialize scope data arrays and lists. */
void flt_otel_scope_data_init(struct flt_otel_scope_data *ptr);
/* Free all scope data contents. */
void flt_otel_scope_data_free(struct flt_otel_scope_data *ptr);
/* Mark a span for finishing by name in the runtime context. */
int flt_otel_scope_finish_mark(const struct flt_otel_runtime_context *rt_ctx, const char *id, size_t id_len);
/* End all spans that have been marked for finishing. */
void flt_otel_scope_finish_marked(const struct flt_otel_runtime_context *rt_ctx, const struct timespec *ts_finish);
/* Free scope spans and contexts no longer needed by a channel. */
void flt_otel_scope_free_unused(struct flt_otel_runtime_context *rt_ctx, struct channel *chn);
#endif /* _OTEL_SCOPE_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

104
addons/otel/include/util.h Normal file
View file

@ -0,0 +1,104 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_UTIL_H_
#define _OTEL_UTIL_H_
#define FLT_OTEL_HTTP_METH_DEFINES \
FLT_OTEL_HTTP_METH_DEF(OPTIONS) \
FLT_OTEL_HTTP_METH_DEF(GET) \
FLT_OTEL_HTTP_METH_DEF(HEAD) \
FLT_OTEL_HTTP_METH_DEF(POST) \
FLT_OTEL_HTTP_METH_DEF(PUT) \
FLT_OTEL_HTTP_METH_DEF(DELETE) \
FLT_OTEL_HTTP_METH_DEF(TRACE) \
FLT_OTEL_HTTP_METH_DEF(CONNECT)
/* Iterate over all OTel filter configurations across all proxies. */
#define FLT_OTEL_PROXIES_LIST_START() \
do { \
struct flt_conf *fconf; \
struct proxy *px; \
\
for (px = proxies_list; px != NULL; px = px->next) \
list_for_each_entry(fconf, &(px->filter_configs), list) \
if (fconf->id == otel_flt_id) { \
struct flt_otel_conf *conf = fconf->conf;
#define FLT_OTEL_PROXIES_LIST_END() \
} \
} while (0)
#ifdef DEBUG_OTEL
# define FLT_OTEL_ARGS_DUMP() do { if (otelc_dbg_level & (1 << OTELC_DBG_LEVEL_LOG)) flt_otel_args_dump((const char **)args); } while (0)
#else
# define FLT_OTEL_ARGS_DUMP() while (0)
#endif
#ifndef DEBUG_OTEL
# define flt_otel_filters_dump() while (0)
#else
/* Dump configuration arguments for debugging. */
void flt_otel_args_dump(const char **args);
/* Dump all OTel filter configurations across all proxies. */
void flt_otel_filters_dump(void);
/* Return a label string identifying a channel direction. */
const char *flt_otel_chn_label(const struct channel *chn);
/* Return the proxy mode string for a stream. */
const char *flt_otel_pr_mode(const struct stream *s);
/* Return the stream processing position as a string. */
const char *flt_otel_stream_pos(const struct stream *s);
/* Return the filter type string for a filter instance. */
const char *flt_otel_type(const struct filter *f);
/* Return the analyzer name string for an analyzer bit. */
const char *flt_otel_analyzer(uint an_bit);
/* Dump a linked list of configuration items as a string. */
const char *flt_otel_list_dump(const struct list *head);
#endif
/* Count the number of non-NULL arguments in an argument array. */
int flt_otel_args_count(const char **args);
/* Concatenate argument array elements into a single string. */
int flt_otel_args_concat(const char **args, int idx, int n, char **str);
/* Comparator for qsort: ascending order of doubles with epsilon tolerance. */
int flt_otel_qsort_compar_double(const void *a, const void *b);
/* Parse a string to double with range validation. */
bool flt_otel_strtod(const char *nptr, double *value, double limit_min, double limit_max, char **err);
/* Parse a string to int64_t with range validation. */
bool flt_otel_strtoll(const char *nptr, int64_t *value, int64_t limit_min, int64_t limit_max, char **err);
/* Convert sample data to a string representation. */
int flt_otel_sample_to_str(const struct sample_data *data, char *value, size_t size, char **err);
/* Convert sample data to an OTel value. */
int flt_otel_sample_to_value(const char *key, const struct sample_data *data, struct otelc_value *value, char **err);
/* Add a key-value pair to a growable key-value array. */
int flt_otel_sample_add_kv(struct flt_otel_scope_data_kv *kv, const char *key, const struct otelc_value *value);
/* Evaluate a sample definition into an OTel value. */
int flt_otel_sample_eval(struct stream *s, uint dir, struct flt_otel_conf_sample *sample, bool flag_native, struct otelc_value *value, char **err);
/* Evaluate a sample expression and add the result to scope data. */
int flt_otel_sample_add(struct stream *s, uint dir, struct flt_otel_conf_sample *sample, struct flt_otel_scope_data *data, int type, char **err);
#endif /* _OTEL_UTIL_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

View file

@ -0,0 +1,52 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _OTEL_VARS_H_
#define _OTEL_VARS_H_
#define FLT_OTEL_VARS_SCOPE "txn"
#define FLT_OTEL_VAR_CHAR_DASH 'D'
#define FLT_OTEL_VAR_CHAR_SPACE 'S'
#ifndef USE_OTEL_VARS_NAME
# define FLT_OTEL_VAR_CTX_SIZE int8_t
/* Context buffer for storing a single variable value during iteration. */
struct flt_otel_ctx {
char value[BUFSIZ]; /* Variable value string. */
int value_len; /* Length of the value string. */
};
/* Callback type invoked for each context variable during iteration. */
typedef int (*flt_otel_ctx_loop_cb)(struct sample *, size_t, const char *, const char *, const char *, FLT_OTEL_VAR_CTX_SIZE, char **, void *);
#endif /* !USE_OTEL_VARS_NAME */
#ifndef DEBUG_OTEL
# define flt_otel_vars_dump(...) while (0)
#else
/* Dump all OTel-related variables for a stream. */
void flt_otel_vars_dump(struct stream *s);
#endif
/* Register a HAProxy variable for OTel context storage. */
int flt_otel_var_register(const char *scope, const char *prefix, const char *name, char **err);
/* Set an OTel context variable on a stream. */
int flt_otel_var_set(struct stream *s, const char *scope, const char *prefix, const char *name, const char *value, uint opt, char **err);
/* Unset all OTel context variables matching a prefix on a stream. */
int flt_otel_vars_unset(struct stream *s, const char *scope, const char *prefix, uint opt, char **err);
/* Retrieve all OTel context variables matching a prefix into a text map. */
struct otelc_text_map *flt_otel_vars_get(struct stream *s, const char *scope, const char *prefix, uint opt, char **err);
#endif /* _OTEL_VARS_H_ */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

457
addons/otel/src/cli.c Normal file
View file

@ -0,0 +1,457 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
/***
* NAME
* flt_otel_cli_set_msg - CLI response message setter
*
* SYNOPSIS
* static int flt_otel_cli_set_msg(struct appctx *appctx, char *err, char *msg)
*
* ARGUMENTS
* appctx - CLI application context
* err - error message string (or NULL)
* msg - informational message string (or NULL)
*
* DESCRIPTION
* Sets the CLI response message and state for the given <appctx>. If <err>
* is non-NULL, it is passed to cli_dynerr() and <msg> is freed; otherwise
* <msg> is passed to cli_dynmsg() at LOG_INFO severity. When neither message
* is available, the function returns 0 without changing state.
*
* RETURN VALUE
* Returns 1 when a message was set, or 0 when both pointers were NULL.
*/
static int flt_otel_cli_set_msg(struct appctx *appctx, char *err, char *msg)
{
OTELC_FUNC("%p, %p, %p", appctx, err, msg);
if ((appctx == NULL) || ((err == NULL) && (msg == NULL)))
OTELC_RETURN_INT(0);
if (err != NULL) {
OTELC_DBG(INFO, "err(%d): \"%s\"", appctx->st0, err);
OTELC_SFREE(msg);
OTELC_RETURN_INT(cli_dynerr(appctx, err));
}
OTELC_DBG(INFO, "msg(%d): \"%s\"", appctx->st0, msg);
OTELC_RETURN_INT(cli_dynmsg(appctx, LOG_INFO, msg));
}
#ifdef DEBUG_OTEL
/***
* NAME
* flt_otel_cli_parse_debug - CLI debug level handler
*
* SYNOPSIS
* static int flt_otel_cli_parse_debug(char **args, char *payload, struct appctx *appctx, void *private)
*
* ARGUMENTS
* args - CLI command arguments array
* payload - CLI command payload string
* appctx - CLI application context
* private - unused private data pointer
*
* DESCRIPTION
* Handles the "otel debug [level]" CLI command. When a level argument is
* provided in <args[2]>, parses it as an integer in the range
* [0, OTELC_DBG_LEVEL_MASK] and atomically stores it as the global debug
* level. Setting a level requires admin access level. When no argument is
* given, reports the current debug level. The response message includes the
* debug level in both decimal and hexadecimal format.
*
* RETURN VALUE
* Returns 1, or 0 on memory allocation failure.
*/
static int flt_otel_cli_parse_debug(char **args, char *payload, struct appctx *appctx, void *private)
{
char *err = NULL, *msg = NULL;
OTELC_FUNC("%p, \"%s\", %p, %p", args, OTELC_STR_ARG(payload), appctx, private);
FLT_OTEL_ARGS_DUMP();
if (FLT_OTEL_ARG_ISVALID(2)) {
int64_t value;
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
OTELC_RETURN_INT(1);
if (flt_otel_strtoll(args[2], &value, 0, OTELC_DBG_LEVEL_MASK, &err)) {
_HA_ATOMIC_STORE(&otelc_dbg_level, (int)value);
(void)memprintf(&msg, FLT_OTEL_CLI_CMD " : debug level set to %d (0x%04x)", (int)value, (int)value);
}
} else {
int value = _HA_ATOMIC_LOAD(&otelc_dbg_level);
(void)memprintf(&msg, FLT_OTEL_CLI_CMD " : current debug level is %d (0x%04x)", value, value);
}
OTELC_RETURN_INT(flt_otel_cli_set_msg(appctx, err, msg));
}
#endif /* DEBUG_OTEL */
/***
* NAME
* flt_otel_cli_parse_disabled - CLI enable/disable handler
*
* SYNOPSIS
* static int flt_otel_cli_parse_disabled(char **args, char *payload, struct appctx *appctx, void *private)
*
* ARGUMENTS
* args - CLI command arguments array
* payload - CLI command payload string
* appctx - CLI application context
* private - boolean flag cast to pointer (1 = disable, 0 = enable)
*
* DESCRIPTION
* Handles the "otel enable" and "otel disable" CLI commands. The <private>
* parameter determines the action: a value of 1 disables the filter, 0
* enables it. Requires admin access level. The flag_disabled field is
* atomically updated for all OTel filter instances across all proxies.
*
* RETURN VALUE
* Returns 1, or 0 if no OTel filter instances are configured or on memory
* allocation failure.
*/
static int flt_otel_cli_parse_disabled(char **args, char *payload, struct appctx *appctx, void *private)
{
char *msg = NULL;
bool value = (uintptr_t)private;
OTELC_FUNC("%p, \"%s\", %p, %p", args, OTELC_STR_ARG(payload), appctx, private);
FLT_OTEL_ARGS_DUMP();
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
OTELC_RETURN_INT(1);
FLT_OTEL_PROXIES_LIST_START() {
_HA_ATOMIC_STORE(&(conf->instr->flag_disabled), value);
(void)memprintf(&msg, "%s%s" FLT_OTEL_CLI_CMD " : filter %sabled", FLT_OTEL_CLI_MSG_CAT(msg), value ? "dis" : "en");
} FLT_OTEL_PROXIES_LIST_END();
OTELC_RETURN_INT(flt_otel_cli_set_msg(appctx, NULL, msg));
}
/***
* NAME
* flt_otel_cli_parse_option - CLI error mode handler
*
* SYNOPSIS
* static int flt_otel_cli_parse_option(char **args, char *payload, struct appctx *appctx, void *private)
*
* ARGUMENTS
* args - CLI command arguments array
* payload - CLI command payload string
* appctx - CLI application context
* private - boolean flag cast to pointer (1 = hard-errors, 0 = soft-errors)
*
* DESCRIPTION
* Handles the "otel hard-errors" and "otel soft-errors" CLI commands. The
* <private> parameter determines the error mode: a value of 1 enables
* hard-error mode (filter failure aborts the stream), 0 enables soft-error
* mode (failures are silently ignored). Requires admin access level. The
* flag_harderr field is atomically updated for all OTel filter instances
* across all proxies.
*
* RETURN VALUE
* Returns 1, or 0 if no OTel filter instances are configured or on memory
* allocation failure.
*/
static int flt_otel_cli_parse_option(char **args, char *payload, struct appctx *appctx, void *private)
{
char *msg = NULL;
bool value = (uintptr_t)private;
OTELC_FUNC("%p, \"%s\", %p, %p", args, OTELC_STR_ARG(payload), appctx, private);
FLT_OTEL_ARGS_DUMP();
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
OTELC_RETURN_INT(1);
FLT_OTEL_PROXIES_LIST_START() {
_HA_ATOMIC_STORE(&(conf->instr->flag_harderr), value);
(void)memprintf(&msg, "%s%s" FLT_OTEL_CLI_CMD " : filter set %s-errors", FLT_OTEL_CLI_MSG_CAT(msg), value ? "hard" : "soft");
} FLT_OTEL_PROXIES_LIST_END();
OTELC_RETURN_INT(flt_otel_cli_set_msg(appctx, NULL, msg));
}
/***
* NAME
* flt_otel_cli_parse_logging - CLI logging state handler
*
* SYNOPSIS
* static int flt_otel_cli_parse_logging(char **args, char *payload, struct appctx *appctx, void *private)
*
* ARGUMENTS
* args - CLI command arguments array
* payload - CLI command payload string
* appctx - CLI application context
* private - unused private data pointer
*
* DESCRIPTION
* Handles the "otel logging [state]" CLI command. When a state argument is
* provided in <args[2]>, it is matched against "off", "on", or "nolognorm"
* and the logging field is atomically updated for all OTel filter instances.
* Setting a value requires admin access level. When no argument is given,
* reports the current logging state for all instances. Invalid values
* produce an error with the accepted options listed.
*
* RETURN VALUE
* Returns 1, or 0 if no OTel filter instances are configured (and no error
* occurred) or on memory allocation failure.
*/
static int flt_otel_cli_parse_logging(char **args, char *payload, struct appctx *appctx, void *private)
{
char *err = NULL, *msg = NULL;
bool flag_set = false;
uint8_t value;
OTELC_FUNC("%p, \"%s\", %p, %p", args, OTELC_STR_ARG(payload), appctx, private);
FLT_OTEL_ARGS_DUMP();
if (FLT_OTEL_ARG_ISVALID(2)) {
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
OTELC_RETURN_INT(1);
if (strcasecmp(args[2], FLT_OTEL_CLI_LOGGING_OFF) == 0) {
flag_set = true;
value = FLT_OTEL_LOGGING_OFF;
}
else if (strcasecmp(args[2], FLT_OTEL_CLI_LOGGING_ON) == 0) {
flag_set = true;
value = FLT_OTEL_LOGGING_ON;
}
else if (strcasecmp(args[2], FLT_OTEL_CLI_LOGGING_NOLOGNORM) == 0) {
flag_set = true;
value = FLT_OTEL_LOGGING_ON | FLT_OTEL_LOGGING_NOLOGNORM;
}
else {
(void)memprintf(&err, "'%s' : invalid value, use <" FLT_OTEL_CLI_LOGGING_OFF " | " FLT_OTEL_CLI_LOGGING_ON " | " FLT_OTEL_CLI_LOGGING_NOLOGNORM ">", args[2]);
}
if (flag_set) {
FLT_OTEL_PROXIES_LIST_START() {
_HA_ATOMIC_STORE(&(conf->instr->logging), value);
(void)memprintf(&msg, "%s%s" FLT_OTEL_CLI_CMD " : logging is %s", FLT_OTEL_CLI_MSG_CAT(msg), FLT_OTEL_CLI_LOGGING_STATE(value));
} FLT_OTEL_PROXIES_LIST_END();
}
} else {
FLT_OTEL_PROXIES_LIST_START() {
value = _HA_ATOMIC_LOAD(&(conf->instr->logging));
(void)memprintf(&msg, "%s%s" FLT_OTEL_CLI_CMD " : logging is currently %s", FLT_OTEL_CLI_MSG_CAT(msg), FLT_OTEL_CLI_LOGGING_STATE(value));
} FLT_OTEL_PROXIES_LIST_END();
}
OTELC_RETURN_INT(flt_otel_cli_set_msg(appctx, err, msg));
}
/***
* NAME
* flt_otel_cli_parse_rate - CLI rate limit handler
*
* SYNOPSIS
* static int flt_otel_cli_parse_rate(char **args, char *payload, struct appctx *appctx, void *private)
*
* ARGUMENTS
* args - CLI command arguments array
* payload - CLI command payload string
* appctx - CLI application context
* private - unused private data pointer
*
* DESCRIPTION
* Handles the "otel rate [value]" CLI command. When a value argument is
* provided in <args[2]>, it is parsed as a floating-point number in the
* range [0.0, 100.0], converted to a fixed-point uint32_t representation,
* and atomically stored as the rate limit for all OTel filter instances.
* Setting a value requires admin access level. When no argument is given,
* reports the current rate limit percentage for all instances.
*
* RETURN VALUE
* Returns 1, or 0 if no OTel filter instances are configured (and no error
* occurred) or on memory allocation failure.
*/
static int flt_otel_cli_parse_rate(char **args, char *payload, struct appctx *appctx, void *private)
{
char *err = NULL, *msg = NULL;
OTELC_FUNC("%p, \"%s\", %p, %p", args, OTELC_STR_ARG(payload), appctx, private);
FLT_OTEL_ARGS_DUMP();
if (FLT_OTEL_ARG_ISVALID(2)) {
double value;
if (!cli_has_level(appctx, ACCESS_LVL_ADMIN))
OTELC_RETURN_INT(1);
if (flt_otel_strtod(args[2], &value, 0.0, 100.0, &err)) {
FLT_OTEL_PROXIES_LIST_START() {
_HA_ATOMIC_STORE(&(conf->instr->rate_limit), FLT_OTEL_FLOAT_U32(value));
(void)memprintf(&msg, "%s%s" FLT_OTEL_CLI_CMD " : rate limit set to %.2f", FLT_OTEL_CLI_MSG_CAT(msg), value);
} FLT_OTEL_PROXIES_LIST_END();
}
} else {
FLT_OTEL_PROXIES_LIST_START() {
uint32_t value = _HA_ATOMIC_LOAD(&(conf->instr->rate_limit));
(void)memprintf(&msg, "%s%s" FLT_OTEL_CLI_CMD " : current rate limit is %.2f", FLT_OTEL_CLI_MSG_CAT(msg), FLT_OTEL_U32_FLOAT(value));
} FLT_OTEL_PROXIES_LIST_END();
}
OTELC_RETURN_INT(flt_otel_cli_set_msg(appctx, err, msg));
}
/***
* NAME
* flt_otel_cli_parse_status - CLI status display handler
*
* SYNOPSIS
* static int flt_otel_cli_parse_status(char **args, char *payload, struct appctx *appctx, void *private)
*
* ARGUMENTS
* args - CLI command arguments array
* payload - CLI command payload string
* appctx - CLI application context
* private - unused private data pointer
*
* DESCRIPTION
* Handles the "otel status" CLI command. Builds a formatted status report
* for all OTel filter instances across all proxies. The report includes
* the library version, proxy name, configuration file path, group and scope
* counts, disable counts, instrumentation ID, tracer and meter state, rate
* limit, error mode, disabled state, logging state, and analyzer bits. When
* DEBUG_OTEL is enabled, the current debug level is also included.
*
* RETURN VALUE
* Returns 1, or 0 on memory allocation failure.
*/
static int flt_otel_cli_parse_status(char **args, char *payload, struct appctx *appctx, void *private)
{
const char *nl = "";
char *msg = NULL;
OTELC_FUNC("%p, \"%s\", %p, %p", args, OTELC_STR_ARG(payload), appctx, private);
FLT_OTEL_ARGS_DUMP();
flt_otel_filters_dump();
(void)memprintf(&msg, " " FLT_OTEL_OPT_NAME " filter status\n" FLT_OTEL_STR_DASH_78 "\n");
(void)memprintf(&msg, "%s library: C++ " OTELCPP_VERSION ", C wrapper %s\n", msg, otelc_version());
#ifdef DEBUG_OTEL
(void)memprintf(&msg, "%s debug level: 0x%02hhx\n", msg, otelc_dbg_level);
#endif
(void)memprintf(&msg, "%s dropped count: %" PRId64 "/%" PRId64 " %" PRIu64 "\n", msg, otelc_processor_dropped_count(0), otelc_processor_dropped_count(1), _HA_ATOMIC_LOAD(&flt_otel_drop_cnt));
FLT_OTEL_PROXIES_LIST_START() {
struct flt_otel_conf_group *grp;
struct flt_otel_conf_scope *scp;
int n_groups = 0, n_scopes = 0;
list_for_each_entry(grp, &(conf->groups), list)
n_groups++;
list_for_each_entry(scp, &(conf->scopes), list)
n_scopes++;
(void)memprintf(&msg, "%s\n%s proxy %s, filter %s\n", msg, nl, px->id, conf->id);
(void)memprintf(&msg, "%s configuration: %s\n", msg, conf->cfg_file);
(void)memprintf(&msg, "%s groups/scopes: %d/%d\n\n", msg, n_groups, n_scopes);
(void)memprintf(&msg, "%s instrumentation %s\n", msg, conf->instr->id);
(void)memprintf(&msg, "%s configuration: %s\n", msg, conf->instr->config);
(void)memprintf(&msg, "%s tracer: %s\n", msg, (conf->instr->tracer != NULL) ? "active" : "not initialized");
(void)memprintf(&msg, "%s meter: %s\n", msg, (conf->instr->meter != NULL) ? "active" : "not initialized");
(void)memprintf(&msg, "%s logger: %s\n", msg, (conf->instr->logger != NULL) ? "active" : "not initialized");
(void)memprintf(&msg, "%s rate limit: %.2f %%\n", msg, FLT_OTEL_U32_FLOAT(_HA_ATOMIC_LOAD(&(conf->instr->rate_limit))));
(void)memprintf(&msg, "%s hard errors: %s\n", msg, FLT_OTEL_STR_FLAG_YN(_HA_ATOMIC_LOAD(&(conf->instr->flag_harderr))));
(void)memprintf(&msg, "%s disabled: %s\n", msg, FLT_OTEL_STR_FLAG_YN(_HA_ATOMIC_LOAD(&(conf->instr->flag_disabled))));
(void)memprintf(&msg, "%s logging: %s\n", msg, FLT_OTEL_CLI_LOGGING_STATE(_HA_ATOMIC_LOAD(&(conf->instr->logging))));
(void)memprintf(&msg, "%s analyzers: %08x", msg, conf->instr->analyzers);
#ifdef FLT_OTEL_USE_COUNTERS
(void)memprintf(&msg, "%s\n\n counters\n", msg);
(void)memprintf(&msg, "%s attached: %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 "\n", msg, conf->cnt.attached[0], conf->cnt.attached[1], conf->cnt.attached[2], conf->cnt.attached[3]);
(void)memprintf(&msg, "%s disabled: %" PRIu64 " %" PRIu64, msg, conf->cnt.disabled[0], conf->cnt.disabled[1]);
#endif
nl = "\n";
} FLT_OTEL_PROXIES_LIST_END();
OTELC_RETURN_INT(flt_otel_cli_set_msg(appctx, NULL, msg));
}
/* CLI command table for the OTel filter. */
static struct cli_kw_list cli_kws = { { }, {
#ifdef DEBUG_OTEL
{ { FLT_OTEL_CLI_CMD, "debug", NULL }, FLT_OTEL_CLI_CMD " debug [level] : set the OTEL filter debug level (default: get current debug level)", flt_otel_cli_parse_debug, NULL, NULL, NULL, ACCESS_LVL_ADMIN },
#endif
{ { FLT_OTEL_CLI_CMD, "disable", NULL }, FLT_OTEL_CLI_CMD " disable : disable the OTEL filter", flt_otel_cli_parse_disabled, NULL, NULL, (void *)1, ACCESS_LVL_ADMIN },
{ { FLT_OTEL_CLI_CMD, "enable", NULL }, FLT_OTEL_CLI_CMD " enable : enable the OTEL filter", flt_otel_cli_parse_disabled, NULL, NULL, (void *)0, ACCESS_LVL_ADMIN },
{ { FLT_OTEL_CLI_CMD, "soft-errors", NULL }, FLT_OTEL_CLI_CMD " soft-errors : disable hard-errors mode", flt_otel_cli_parse_option, NULL, NULL, (void *)0, ACCESS_LVL_ADMIN },
{ { FLT_OTEL_CLI_CMD, "hard-errors", NULL }, FLT_OTEL_CLI_CMD " hard-errors : enable hard-errors mode", flt_otel_cli_parse_option, NULL, NULL, (void *)1, ACCESS_LVL_ADMIN },
{ { FLT_OTEL_CLI_CMD, "logging", NULL }, FLT_OTEL_CLI_CMD " logging [state] : set logging state (default: get current logging state)", flt_otel_cli_parse_logging, NULL, NULL, NULL, ACCESS_LVL_ADMIN },
{ { FLT_OTEL_CLI_CMD, "rate", NULL }, FLT_OTEL_CLI_CMD " rate [value] : set the rate limit (default: get current rate value)", flt_otel_cli_parse_rate, NULL, NULL, NULL, ACCESS_LVL_ADMIN },
{ { FLT_OTEL_CLI_CMD, "status", NULL }, FLT_OTEL_CLI_CMD " status : show the OTEL filter status", flt_otel_cli_parse_status, NULL, NULL, NULL, 0 },
{ /* END */ }
}};
/***
* NAME
* flt_otel_cli_init - CLI keyword registration
*
* SYNOPSIS
* void flt_otel_cli_init(void)
*
* ARGUMENTS
* This function takes no arguments.
*
* DESCRIPTION
* Registers the OTel filter CLI keywords with the HAProxy CLI subsystem.
* The keywords include commands for enable/disable, error mode, logging,
* rate limit, status display, and (when DEBUG_OTEL is defined) debug level
* management.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_cli_init(void)
{
OTELC_FUNC("");
/* Register CLI keywords. */
cli_register_kw(&cli_kws);
OTELC_RETURN();
}
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

885
addons/otel/src/conf.c Normal file
View file

@ -0,0 +1,885 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
/***
* NAME
* flt_otel_conf_hdr_init - conf_hdr structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_hdr *flt_otel_conf_hdr_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_hdr structure. The <id> string is
* duplicated and stored as the header identifier. If <head> is non-NULL,
* the structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(hdr, id, )
/***
* NAME
* flt_otel_conf_hdr_free - conf_hdr structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_hdr_free(struct flt_otel_conf_hdr **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_hdr structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(hdr, id,
FLT_OTEL_DBG_CONF_HDR("- conf_hdr free ", *ptr, id);
)
/***
* NAME
* flt_otel_conf_str_init - conf_str structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_str *flt_otel_conf_str_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_str structure. The <id> string is
* duplicated and stored as the string value. If <head> is non-NULL, the
* structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(str, str, )
/***
* NAME
* flt_otel_conf_str_free - conf_str structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_str_free(struct flt_otel_conf_str **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_str structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(str, str,
FLT_OTEL_DBG_CONF_HDR("- conf_str free ", *ptr, str);
)
/***
* NAME
* flt_otel_conf_link_init - conf_link structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_link *flt_otel_conf_link_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_link structure for a span link
* reference. The <id> string is duplicated and stored as the linked
* span name. If <head> is non-NULL, the structure is appended to
* the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(link, span, )
/***
* NAME
* flt_otel_conf_link_free - conf_link structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_link_free(struct flt_otel_conf_link **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_link structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(link, span,
FLT_OTEL_DBG_CONF_HDR("- conf_link free ", *ptr, span);
)
/***
* NAME
* flt_otel_conf_ph_init - conf_ph placeholder structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_ph *flt_otel_conf_ph_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_ph (placeholder) structure. The <id>
* string is duplicated and stored as the placeholder identifier. If <head>
* is non-NULL, the structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(ph, id, )
/***
* NAME
* flt_otel_conf_ph_free - conf_ph structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_ph_free(struct flt_otel_conf_ph **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_ph structure and its contents,
* then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(ph, id,
FLT_OTEL_DBG_CONF_HDR("- conf_ph free ", *ptr, id);
)
/***
* NAME
* flt_otel_conf_sample_expr_init - conf_sample_expr structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_sample_expr *flt_otel_conf_sample_expr_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_sample_expr structure. The <id> string is
* duplicated and stored as the expression value. If <head> is non-NULL, the
* structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(sample_expr, fmt_expr, )
/***
* NAME
* flt_otel_conf_sample_expr_free - conf_sample_expr structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_sample_expr_free(struct flt_otel_conf_sample_expr **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_sample_expr structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(sample_expr, fmt_expr,
FLT_OTEL_DBG_CONF_SAMPLE_EXPR("- conf_sample_expr free ", *ptr);
release_sample_expr((*ptr)->expr);
)
/***
* NAME
* flt_otel_conf_sample_init - conf_sample structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_sample *flt_otel_conf_sample_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_sample structure. The <id> string is
* duplicated and stored as the sample key. If <head> is non-NULL, the
* structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(sample, key,
LIST_INIT(&(retptr->exprs));
lf_expr_init(&(retptr->lf_expr));
)
/***
* NAME
* flt_otel_conf_sample_init_ex - extended sample initialization
*
* SYNOPSIS
* struct flt_otel_conf_sample *flt_otel_conf_sample_init_ex(const char **args, int idx, int n, const struct otelc_value *extra, int line, struct list *head, char **err)
*
* ARGUMENTS
* args - configuration line arguments array
* idx - position where sample value starts
* n - maximum number of arguments to concatenate (0 means all)
* extra - optional extra data (event name or status code)
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Creates and initializes a conf_sample structure with extended data. Calls
* flt_otel_conf_sample_init() with <args[idx - 1]> as the sample key to
* create the base structure, copies <extra> data (event name string or status
* code integer), concatenates the remaining arguments into the sample value
* string, and counts the number of sample expressions.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
struct flt_otel_conf_sample *flt_otel_conf_sample_init_ex(const char **args, int idx, int n, const struct otelc_value *extra, int line, struct list *head, char **err)
{
struct flt_otel_conf_sample *retptr = NULL;
OTELC_FUNC("%p, %d, %d, %p, %d, %p, %p:%p", args, idx, n, extra, line, head, OTELC_DPTR_ARGS(err));
OTELC_DBG_VALUE(DEBUG, "extra ", extra);
/* Ensure the sample value is present in the args[] array. */
if (flt_otel_args_count(args) <= idx) {
FLT_OTEL_ERR("'%s' : too few arguments", args[0]);
OTELC_RETURN_PTR(retptr);
}
/* The sample key is located at the idx location of the args[] field. */
retptr = flt_otel_conf_sample_init(args[idx - 1], line, head, err);
if (retptr == NULL)
OTELC_RETURN_PTR(retptr);
if ((extra == NULL) || (extra->u_type == OTELC_VALUE_NULL)) {
/*
* Do nothing - sample extra data is not set or initialized,
* which means it is not used.
*/
}
else if (extra->u_type == OTELC_VALUE_STRING) {
retptr->extra.u_type = OTELC_VALUE_DATA;
retptr->extra.u.value_data = OTELC_STRDUP(extra->u.value_string);
if (retptr->extra.u.value_data == NULL) {
FLT_OTEL_ERR("out of memory");
flt_otel_conf_sample_free(&retptr);
OTELC_RETURN_PTR(retptr);
}
}
else if (extra->u_type == OTELC_VALUE_INT32) {
retptr->extra.u_type = extra->u_type;
retptr->extra.u.value_int32 = extra->u.value_int32;
}
else {
FLT_OTEL_ERR("invalid sample extra data type: %d", extra->u_type);
flt_otel_conf_sample_free(&retptr);
OTELC_RETURN_PTR(retptr);
}
/* The sample value starts in the args[] array after the key. */
retptr->num_exprs = flt_otel_args_concat(args, idx, n, &(retptr->fmt_string));
if (retptr->num_exprs == FLT_OTEL_RET_ERROR) {
FLT_OTEL_ERR("out of memory");
flt_otel_conf_sample_free(&retptr);
OTELC_RETURN_PTR(retptr);
}
FLT_OTEL_DBG_CONF_SAMPLE("- conf_sample init ", retptr);
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_conf_sample_free - conf_sample structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_sample_free(struct flt_otel_conf_sample **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_sample structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(sample, key,
FLT_OTEL_DBG_CONF_SAMPLE("- conf_sample free ", *ptr);
OTELC_SFREE((*ptr)->fmt_string);
if ((*ptr)->extra.u_type == OTELC_VALUE_DATA)
OTELC_SFREE((*ptr)->extra.u.value_data);
FLT_OTEL_LIST_DESTROY(sample_expr, &((*ptr)->exprs));
lf_expr_deinit(&((*ptr)->lf_expr));
)
/***
* NAME
* flt_otel_conf_context_init - conf_context structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_context *flt_otel_conf_context_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_context structure. The <id> string is
* duplicated and stored as the context identifier. If <head> is non-NULL,
* the structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(context, id, )
/***
* NAME
* flt_otel_conf_context_free - conf_context structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_context_free(struct flt_otel_conf_context **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_context structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(context, id,
FLT_OTEL_DBG_CONF_HDR("- conf_context free ", *ptr, id);
)
/***
* NAME
* flt_otel_conf_span_init - conf_span structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_span *flt_otel_conf_span_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_span structure with empty lists for links,
* attributes, events, baggages, and statuses. The <id> string is duplicated
* and stored as the span name. If <head> is non-NULL, the structure is
* appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(span, id,
LIST_INIT(&(retptr->links));
LIST_INIT(&(retptr->attributes));
LIST_INIT(&(retptr->events));
LIST_INIT(&(retptr->baggages));
LIST_INIT(&(retptr->statuses));
)
/***
* NAME
* flt_otel_conf_span_free - conf_span structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_span_free(struct flt_otel_conf_span **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_span structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(span, id,
FLT_OTEL_DBG_CONF_HDR("- conf_span free ", *ptr, id);
OTELC_SFREE((*ptr)->ref_id);
OTELC_SFREE((*ptr)->ctx_id);
FLT_OTEL_LIST_DESTROY(link, &((*ptr)->links));
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->attributes));
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->events));
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->baggages));
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->statuses));
)
/***
* NAME
* flt_otel_conf_instrument_init - conf_instrument structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_instrument *flt_otel_conf_instrument_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_instrument structure. Sets the instrument
* type and meter index to OTELC_METRIC_INSTRUMENT_UNSET and initializes the
* samples and attributes lists. The <id> string is duplicated and stored as
* the instrument name. If <head> is non-NULL, the structure is appended to
* the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(instrument, id,
retptr->idx = OTELC_METRIC_INSTRUMENT_UNSET;
retptr->type = OTELC_METRIC_INSTRUMENT_UNSET;
retptr->aggr_type = OTELC_METRIC_AGGREGATION_UNSET;
LIST_INIT(&(retptr->samples));
LIST_INIT(&(retptr->attributes));
)
/***
* NAME
* flt_otel_conf_instrument_free - conf_instrument structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_instrument_free(struct flt_otel_conf_instrument **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_instrument structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(instrument, id,
FLT_OTEL_DBG_CONF_INSTRUMENT("- conf_instrument free ", *ptr);
OTELC_SFREE((*ptr)->description);
OTELC_SFREE((*ptr)->unit);
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->samples));
OTELC_SFREE((*ptr)->bounds);
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->attributes));
)
/***
* NAME
* flt_otel_conf_log_record_init - conf_log_record structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_log_record *flt_otel_conf_log_record_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_log_record structure. Initializes the
* attributes and sample expressions lists. The <id> string is required by
* the macro but is not used directly; the severity level is stored
* separately. If <head> is non-NULL, the structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(log_record, id,
LIST_INIT(&(retptr->attributes));
LIST_INIT(&(retptr->samples));
)
/***
* NAME
* flt_otel_conf_log_record_free - conf_log_record structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_log_record_free(struct flt_otel_conf_log_record **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_log_record structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(log_record, id,
FLT_OTEL_DBG_CONF_LOG_RECORD("- conf_log_record free ", *ptr);
OTELC_SFREE((*ptr)->event_name);
OTELC_SFREE((*ptr)->span);
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->attributes));
FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->samples));
)
/***
* NAME
* flt_otel_conf_scope_init - conf_scope structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_scope *flt_otel_conf_scope_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_scope structure with empty lists for ACLs,
* contexts, spans, spans_to_finish, and instruments. The <id> string is
* duplicated and stored as the scope name. If <head> is non-NULL, the
* structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(scope, id,
LIST_INIT(&(retptr->acls));
LIST_INIT(&(retptr->contexts));
LIST_INIT(&(retptr->spans));
LIST_INIT(&(retptr->spans_to_finish));
LIST_INIT(&(retptr->instruments));
LIST_INIT(&(retptr->log_records));
)
/***
* NAME
* flt_otel_conf_scope_free - conf_scope structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_scope_free(struct flt_otel_conf_scope **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_scope structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(scope, id,
struct acl *acl;
struct acl *aclback;
FLT_OTEL_DBG_CONF_SCOPE("- conf_scope free ", *ptr);
list_for_each_entry_safe(acl, aclback, &((*ptr)->acls), list) {
prune_acl(acl);
FLT_OTEL_LIST_DEL(&(acl->list));
OTELC_SFREE(acl);
}
free_acl_cond((*ptr)->cond);
FLT_OTEL_LIST_DESTROY(context, &((*ptr)->contexts));
FLT_OTEL_LIST_DESTROY(span, &((*ptr)->spans));
FLT_OTEL_LIST_DESTROY(str, &((*ptr)->spans_to_finish));
FLT_OTEL_LIST_DESTROY(instrument, &((*ptr)->instruments));
FLT_OTEL_LIST_DESTROY(log_record, &((*ptr)->log_records));
)
/***
* NAME
* flt_otel_conf_group_init - conf_group structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_group *flt_otel_conf_group_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_group structure with an empty placeholder
* scope list. The <id> string is duplicated and stored as the group name.
* If <head> is non-NULL, the structure is appended to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(group, id,
LIST_INIT(&(retptr->ph_scopes));
)
/***
* NAME
* flt_otel_conf_group_free - conf_group structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_group_free(struct flt_otel_conf_group **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_group structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(group, id,
FLT_OTEL_DBG_CONF_GROUP("- conf_group free ", *ptr);
FLT_OTEL_LIST_DESTROY(ph_scope, &((*ptr)->ph_scopes));
)
/***
* NAME
* flt_otel_conf_instr_init - conf_instr structure allocation
*
* SYNOPSIS
* struct flt_otel_conf_instr *flt_otel_conf_instr_init(const char *id, int line, struct list *head, char **err)
*
* ARGUMENTS
* id - identifier string to duplicate
* line - configuration file line number
* head - list to append to (or NULL)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a conf_instr (instrumentation) structure. Sets
* the default rate limit to 100%, initializes the proxy_log for logger
* support, and creates empty lists for ACLs, placeholder groups, and
* placeholder scopes. The <id> string is duplicated and stored as the
* instrumentation name. If <head> is non-NULL, the structure is appended
* to the list.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
FLT_OTEL_CONF_FUNC_INIT(instr, id,
retptr->rate_limit = FLT_OTEL_FLOAT_U32(100.0);
init_new_proxy(&(retptr->proxy_log));
LIST_INIT(&(retptr->acls));
LIST_INIT(&(retptr->ph_groups));
LIST_INIT(&(retptr->ph_scopes));
)
/***
* NAME
* flt_otel_conf_instr_free - conf_instr structure deallocation
*
* SYNOPSIS
* void flt_otel_conf_instr_free(struct flt_otel_conf_instr **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf_instr structure and its
* contents, then removes it from the list of structures of that type.
*
* RETURN VALUE
* This function does not return a value.
*/
FLT_OTEL_CONF_FUNC_FREE(instr, id,
struct acl *acl;
struct acl *aclback;
struct logger *logger;
struct logger *loggerback;
FLT_OTEL_DBG_CONF_INSTR("- conf_instr free ", *ptr);
OTELC_SFREE((*ptr)->config);
OTELC_DBG(NOTICE, "- deleting acls list %s", flt_otel_list_dump(&((*ptr)->acls)));
list_for_each_entry_safe(acl, aclback, &((*ptr)->acls), list) {
prune_acl(acl);
FLT_OTEL_LIST_DEL(&(acl->list));
OTELC_SFREE(acl);
}
OTELC_DBG(NOTICE, "- deleting proxy_log.loggers list %s", flt_otel_list_dump(&((*ptr)->proxy_log.loggers)));
list_for_each_entry_safe(logger, loggerback, &((*ptr)->proxy_log.loggers), list) {
LIST_DELETE(&(logger->list));
ha_free(&logger);
}
FLT_OTEL_LIST_DESTROY(ph_group, &((*ptr)->ph_groups));
FLT_OTEL_LIST_DESTROY(ph_scope, &((*ptr)->ph_scopes));
)
/***
* NAME
* flt_otel_conf_init - top-level filter configuration allocation
*
* SYNOPSIS
* struct flt_otel_conf *flt_otel_conf_init(struct proxy *px)
*
* ARGUMENTS
* px - proxy instance to associate with
*
* DESCRIPTION
* Allocates and initializes the top-level flt_otel_conf structure. Stores
* the <px> proxy reference and creates empty group and scope lists.
*
* RETURN VALUE
* Returns a pointer to the initialized structure, or NULL on failure.
*/
struct flt_otel_conf *flt_otel_conf_init(struct proxy *px)
{
struct flt_otel_conf *retptr;
OTELC_FUNC("%p", px);
retptr = OTELC_CALLOC(1, sizeof(*retptr));
if (retptr == NULL)
OTELC_RETURN_PTR(retptr);
retptr->proxy = px;
LIST_INIT(&(retptr->groups));
LIST_INIT(&(retptr->scopes));
LIST_INIT(&(retptr->smp_args));
FLT_OTEL_DBG_CONF("- conf init ", retptr);
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_conf_free - top-level filter configuration deallocation
*
* SYNOPSIS
* void flt_otel_conf_free(struct flt_otel_conf **ptr)
*
* ARGUMENTS
* ptr - a pointer to the address of a structure
*
* DESCRIPTION
* Deallocates memory used by the flt_otel_conf structure and its contents.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_conf_free(struct flt_otel_conf **ptr)
{
struct arg_list *cur, *back;
OTELC_FUNC("%p:%p", OTELC_DPTR_ARGS(ptr));
if ((ptr == NULL) || (*ptr == NULL))
OTELC_RETURN();
FLT_OTEL_DBG_CONF("- conf free ", *ptr);
OTELC_SFREE((*ptr)->id);
OTELC_SFREE((*ptr)->cfg_file);
flt_otel_conf_instr_free(&((*ptr)->instr));
FLT_OTEL_LIST_DESTROY(group, &((*ptr)->groups));
FLT_OTEL_LIST_DESTROY(scope, &((*ptr)->scopes));
/* Free any unresolved OTEL sample fetch args (error path). */
list_for_each_entry_safe(cur, back, &((*ptr)->smp_args), list) {
LIST_DELETE(&(cur->list));
ha_free(&cur);
}
OTELC_SFREE_CLEAR(*ptr);
OTELC_RETURN();
}
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

849
addons/otel/src/event.c Normal file
View file

@ -0,0 +1,849 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
/* Event data table built from the X-macro list. */
#define FLT_OTEL_EVENT_DEF(a,b,c,d,e,f) { AN_##b##_##a, OTELC_STRINGIFY_ARG(AN_##b##_##a), SMP_OPT_DIR_##b, SMP_VAL_FE_##c, SMP_VAL_BE_##d, e, f },
const struct flt_otel_event_data flt_otel_event_data[FLT_OTEL_EVENT_MAX] = { FLT_OTEL_EVENT_DEFINES };
#undef FLT_OTEL_EVENT_DEF
/***
* NAME
* flt_otel_scope_run_instrument_record - metric instrument value recorder
*
* SYNOPSIS
* static int flt_otel_scope_run_instrument_record(struct stream *s, uint dir, struct otelc_meter *meter, struct flt_otel_conf_instrument *instr_ref, struct flt_otel_conf_instrument *instr, char **err)
*
* ARGUMENTS
* s - the stream providing the sample context
* dir - the sample fetch direction (SMP_OPT_DIR_REQ/RES)
* meter - the OTel meter instance
* instr_ref - the create-form instrument providing samples and meter index
* instr - the update-form instrument providing per-scope attributes
* err - indirect pointer to error message string
*
* DESCRIPTION
* Evaluates sample expressions from a create-form instrument and records
* the resulting value via the <meter> API. Each expression is evaluated
* with sample_process(), converted to an otelc_value via
* flt_otel_sample_to_value(), and recorded via
* <meter>->update_instrument_kv_n().
*
* RETURN VALUE
* Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
*/
static int flt_otel_scope_run_instrument_record(struct stream *s, uint dir, struct otelc_meter *meter, struct flt_otel_conf_instrument *instr_ref, struct flt_otel_conf_instrument *instr, char **err)
{
struct flt_otel_conf_sample *sample;
struct flt_otel_conf_sample_expr *expr;
struct sample smp;
struct otelc_value value;
struct flt_otel_scope_data_kv instr_attr;
int retval = FLT_OTEL_RET_OK;
OTELC_FUNC("%p, %u, %p, %p, %p, %p:%p", s, dir, meter, instr_ref, instr, OTELC_DPTR_ARGS(err));
/* Evaluate instrument attributes from sample expressions. */
(void)memset(&instr_attr, 0, sizeof(instr_attr));
list_for_each_entry(sample, &(instr->attributes), list) {
struct otelc_value attr_value;
OTELC_DBG(DEBUG, "adding instrument attribute '%s' -> '%s'", sample->key, sample->fmt_string);
if (flt_otel_sample_eval(s, dir, sample, true, &attr_value, err) == FLT_OTEL_RET_ERROR) {
retval = FLT_OTEL_RET_ERROR;
continue;
}
if (flt_otel_sample_add_kv(&instr_attr, sample->key, &attr_value) == FLT_OTEL_RET_ERROR) {
if (attr_value.u_type == OTELC_VALUE_DATA)
OTELC_SFREE(attr_value.u.value_data);
retval = FLT_OTEL_RET_ERROR;
}
}
/* The samples list always contains exactly one entry. */
sample = LIST_NEXT(&(instr_ref->samples), struct flt_otel_conf_sample *, list);
(void)memset(&smp, 0, sizeof(smp));
if (sample->lf_used) {
/*
* Log-format path: evaluate into a temporary buffer and present
* the result as a string sample.
*/
smp.data.u.str.area = OTELC_CALLOC(1, global.tune.bufsize);
if (smp.data.u.str.area == NULL) {
FLT_OTEL_ERR("out of memory");
otelc_kv_destroy(&(instr_attr.attr), instr_attr.cnt);
OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
}
smp.data.type = SMP_T_STR;
smp.data.u.str.data = build_logline(s, smp.data.u.str.area, global.tune.bufsize, &(sample->lf_expr));
} else {
/* The expressions list always contains exactly one entry. */
expr = LIST_NEXT(&(sample->exprs), struct flt_otel_conf_sample_expr *, list);
FLT_OTEL_DBG_CONF_SAMPLE_EXPR("sample expression ", expr);
if (sample_process(s->be, s->sess, s, dir | SMP_OPT_FINAL, expr->expr, &smp) == NULL) {
OTELC_DBG(NOTICE, "WARNING: failed to fetch '%s'", expr->fmt_expr);
retval = FLT_OTEL_RET_ERROR;
}
}
if (retval == FLT_OTEL_RET_ERROR) {
/* Do nothing. */
}
else if (flt_otel_sample_to_value(sample->key, &(smp.data), &value, err) == FLT_OTEL_RET_ERROR) {
if (value.u_type == OTELC_VALUE_DATA)
OTELC_SFREE(value.u.value_data);
retval = FLT_OTEL_RET_ERROR;
}
else {
OTELC_DBG_VALUE(DEBUG, "value ", &value);
/*
* Metric instruments expect numeric values (INT64 or DOUBLE).
* Reject OTELC_VALUE_DATA since the meter cannot interpret
* arbitrary string data as a numeric measurement.
*/
if (value.u_type == OTELC_VALUE_DATA) {
OTELC_DBG(NOTICE, "WARNING: non-numeric value type for instrument '%s'", instr_ref->id);
if (otelc_value_strtonum(&value, OTELC_VALUE_INT64) == OTELC_RET_ERROR) {
OTELC_SFREE(value.u.value_data);
retval = FLT_OTEL_RET_ERROR;
}
}
if (retval != FLT_OTEL_RET_ERROR)
if (OTELC_OPS(meter, update_instrument_kv_n, HA_ATOMIC_LOAD(&(instr_ref->idx)), &value, instr_attr.attr, instr_attr.cnt) == OTELC_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
}
otelc_kv_destroy(&(instr_attr.attr), instr_attr.cnt);
if (sample->lf_used)
OTELC_SFREE(smp.data.u.str.area);
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_scope_run_instrument - metric instrument processor
*
* SYNOPSIS
* static int flt_otel_scope_run_instrument(struct stream *s, uint dir, struct flt_otel_conf_scope *scope, struct otelc_meter *meter, char **err)
*
* ARGUMENTS
* s - the stream providing the sample context
* dir - the sample fetch direction (SMP_OPT_DIR_REQ/RES)
* scope - the scope configuration containing the instrument list
* meter - the OTel meter instance
* err - indirect pointer to error message string
*
* DESCRIPTION
* Processes all metric instruments configured in <scope>. Runs in two
* passes: the first pass lazily creates create-form instruments via <meter>
* on first use, using HA_ATOMIC_CAS on the instrument index to guarantee
* thread-safe one-time initialization. The second pass iterates over
* update-form instruments and records measurements via
* flt_otel_scope_run_instrument_record(). Instruments whose index is still
* negative (UNUSED or PENDING) are skipped, so that a concurrent creation by
* another thread does not cause an invalid <meter> access.
*
* RETURN VALUE
* Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
*/
static int flt_otel_scope_run_instrument(struct stream *s, uint dir, struct flt_otel_conf_scope *scope, struct otelc_meter *meter, char **err)
{
struct flt_otel_conf_instrument *conf_instr;
int retval = FLT_OTEL_RET_OK;
OTELC_FUNC("%p, %u, %p, %p, %p:%p", s, dir, scope, meter, OTELC_DPTR_ARGS(err));
list_for_each_entry(conf_instr, &(scope->instruments), list) {
if (conf_instr->type == OTELC_METRIC_INSTRUMENT_UPDATE) {
/* Do nothing. */
}
else if (HA_ATOMIC_LOAD(&(conf_instr->idx)) == OTELC_METRIC_INSTRUMENT_UNSET) {
int64_t expected = OTELC_METRIC_INSTRUMENT_UNSET;
int rc;
OTELC_DBG(DEBUG, "run instrument '%s' -> '%s'", scope->id, conf_instr->id);
FLT_OTEL_DBG_CONF_INSTRUMENT("", conf_instr);
/*
* Create form: use this instrument directly. Lazily
* create the instrument on first use. Use CAS to
* ensure only one thread performs the creation in a
* multi-threaded environment.
*/
if (!HA_ATOMIC_CAS(&(conf_instr->idx), &expected, OTELC_METRIC_INSTRUMENT_PENDING))
continue;
/*
* The view must be created before the instrument,
* otherwise bucket boundaries cannot be set.
*/
if ((conf_instr->bounds != NULL) && (conf_instr->bounds_num > 0))
if (OTELC_OPS(meter, add_view, conf_instr->id, conf_instr->description, conf_instr->id, conf_instr->unit, conf_instr->type, conf_instr->aggr_type, conf_instr->bounds, conf_instr->bounds_num) == OTELC_RET_ERROR)
OTELC_DBG(NOTICE, "WARNING: failed to add view for instrument '%s'", conf_instr->id);
rc = OTELC_OPS(meter, create_instrument, conf_instr->id, conf_instr->description, conf_instr->unit, conf_instr->type, NULL);
if (rc == OTELC_RET_ERROR) {
OTELC_DBG(NOTICE, "WARNING: failed to create instrument '%s'", conf_instr->id);
HA_ATOMIC_STORE(&(conf_instr->idx), OTELC_METRIC_INSTRUMENT_UNSET);
retval = FLT_OTEL_RET_ERROR;
continue;
} else {
HA_ATOMIC_STORE(&(conf_instr->idx), rc);
}
}
}
list_for_each_entry(conf_instr, &(scope->instruments), list)
if (conf_instr->type == OTELC_METRIC_INSTRUMENT_UPDATE) {
struct flt_otel_conf_instrument *instr = conf_instr->ref;
OTELC_DBG(DEBUG, "run instrument '%s' -> '%s'", scope->id, conf_instr->id);
FLT_OTEL_DBG_CONF_INSTRUMENT("", conf_instr);
/*
* Update form: record a measurement using an existing
* create-form instrument.
*/
if (instr == NULL) {
OTELC_DBG(NOTICE, "WARNING: invalid reference instrument '%s'", conf_instr->id);
retval = FLT_OTEL_RET_ERROR;
}
else if (HA_ATOMIC_LOAD(&(instr->idx)) < 0) {
OTELC_DBG(NOTICE, "WARNING: instrument '%s' not yet created, skipping", instr->id);
}
else if (flt_otel_scope_run_instrument_record(s, dir, meter, instr, conf_instr, err) == FLT_OTEL_RET_ERROR) {
retval = FLT_OTEL_RET_ERROR;
}
}
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_scope_run_log_record - log record emitter
*
* SYNOPSIS
* static int flt_otel_scope_run_log_record(struct stream *s, struct filter *f, uint dir, struct flt_otel_conf_scope *scope, struct otelc_logger *logger, const struct timespec *ts, char **err)
*
* ARGUMENTS
* s - the stream providing the sample context
* f - the filter instance
* dir - the sample fetch direction (SMP_OPT_DIR_REQ/RES)
* scope - the scope configuration containing the log record list
* logger - the OTel logger instance
* ts - the wall-clock timestamp for the log record
* err - indirect pointer to error message string
*
* DESCRIPTION
* Processes all log records configured in <scope>. For each record, checks
* whether the logger is enabled for the configured severity, evaluates the
* sample expressions into a body string, resolves the optional span reference
* against the runtime context, and emits the log record via the logger's
* log_span operation.
*
* RETURN VALUE
* Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
*/
static int flt_otel_scope_run_log_record(struct stream *s, struct filter *f, uint dir, struct flt_otel_conf_scope *scope, struct otelc_logger *logger, const struct timespec *ts, char **err)
{
struct flt_otel_conf_log_record *conf_log;
int retval = FLT_OTEL_RET_OK;
OTELC_FUNC("%p, %p, %u, %p, %p, %p, %p:%p", s, f, dir, scope, logger, ts, OTELC_DPTR_ARGS(err));
list_for_each_entry(conf_log, &(scope->log_records), list) {
struct flt_otel_conf_sample *sample;
struct flt_otel_conf_sample_expr *expr;
struct sample smp;
struct otelc_span *otel_span = NULL;
struct flt_otel_scope_data_kv log_attr;
struct buffer buffer;
int rc;
OTELC_DBG(DEBUG, "run log-record '%s' -> '%s'", scope->id, conf_log->id);
/* Skip if the logger is not enabled for this severity. */
if (OTELC_OPS(logger, enabled, conf_log->severity) == 0)
continue;
/* Evaluate log record attributes from sample expressions. */
(void)memset(&log_attr, 0, sizeof(log_attr));
list_for_each_entry(sample, &(conf_log->attributes), list) {
struct otelc_value attr_value;
OTELC_DBG(DEBUG, "adding log-record attribute '%s' -> '%s'", sample->key, sample->fmt_string);
if (flt_otel_sample_eval(s, dir, sample, true, &attr_value, err) == FLT_OTEL_RET_ERROR) {
retval = FLT_OTEL_RET_ERROR;
continue;
}
if (flt_otel_sample_add_kv(&log_attr, sample->key, &attr_value) == FLT_OTEL_RET_ERROR) {
if (attr_value.u_type == OTELC_VALUE_DATA)
OTELC_SFREE(attr_value.u.value_data);
retval = FLT_OTEL_RET_ERROR;
}
}
/* The samples list has exactly one entry. */
sample = LIST_NEXT(&(conf_log->samples), typeof(sample), list);
(void)memset(&buffer, 0, sizeof(buffer));
if (sample->lf_used) {
/*
* Log-format path: evaluate the log-format expression
* into a dynamically allocated buffer.
*/
chunk_init(&buffer, OTELC_CALLOC(1, global.tune.bufsize), global.tune.bufsize);
if (buffer.area != NULL)
buffer.data = build_logline(s, buffer.area, buffer.size, &(sample->lf_expr));
} else {
/*
* Bare sample expression path: evaluate each expression
* and concatenate the results.
*/
list_for_each_entry(expr, &(sample->exprs), list) {
(void)memset(&smp, 0, sizeof(smp));
if (sample_process(s->be, s->sess, s, dir | SMP_OPT_FINAL, expr->expr, &smp) == NULL) {
OTELC_DBG(NOTICE, "WARNING: failed to fetch '%s'", expr->fmt_expr);
retval = FLT_OTEL_RET_ERROR;
break;
}
if (buffer.area == NULL) {
chunk_init(&buffer, OTELC_CALLOC(1, global.tune.bufsize), global.tune.bufsize);
if (buffer.area == NULL)
break;
}
rc = flt_otel_sample_to_str(&(smp.data), buffer.area + buffer.data, buffer.size - buffer.data, err);
if (rc == FLT_OTEL_RET_ERROR) {
retval = FLT_OTEL_RET_ERROR;
break;
}
buffer.data += rc;
}
}
if (buffer.area == NULL) {
FLT_OTEL_ERR("out of memory");
retval = FLT_OTEL_RET_ERROR;
otelc_kv_destroy(&(log_attr.attr), log_attr.cnt);
continue;
}
/*
* If the log record references a span, resolve it against the
* runtime context. A missing span is not fatal -- the log
* record is emitted without span correlation.
*/
if (conf_log->span != NULL) {
struct flt_otel_runtime_context *rt_ctx = FLT_OTEL_RT_CTX(f->ctx);
struct flt_otel_scope_span *sc_span;
list_for_each_entry(sc_span, &(rt_ctx->spans), list)
if (strcmp(sc_span->id, conf_log->span) == 0) {
otel_span = sc_span->span;
break;
}
if (otel_span == NULL)
OTELC_DBG(NOTICE, "WARNING: cannot find span '%s' for log-record", conf_log->span);
}
if (OTELC_OPS(logger, log_span, conf_log->severity, conf_log->event_id, conf_log->event_name, otel_span, ts, log_attr.attr, log_attr.cnt, "%s", buffer.area) == OTELC_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
otelc_kv_destroy(&(log_attr.attr), log_attr.cnt);
OTELC_SFREE(buffer.area);
}
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_scope_run_span - single span execution
*
* SYNOPSIS
* static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct channel *chn, uint dir, struct flt_otel_scope_span *span, struct flt_otel_scope_data *data, const struct flt_otel_conf_span *conf_span, const struct timespec *ts_steady, const struct timespec *ts_system, char **err)
*
* ARGUMENTS
* s - the stream being processed
* f - the filter instance
* chn - the channel used for HTTP header injection
* dir - the sample fetch direction (SMP_OPT_DIR_REQ/RES)
* span - the runtime scope span to execute
* data - the evaluated scope data (attributes, events, links, status)
* conf_span - the span configuration
* ts_steady - the monotonic timestamp for span creation
* ts_system - the wall-clock timestamp for span events
* err - indirect pointer to error message string
*
* DESCRIPTION
* Executes a single span: creates the OTel span on first call via the tracer,
* adds links, baggage, attributes, events and status from <data>, then
* injects the span context into HTTP headers or HAProxy variables if
* configured in <conf_span>.
*
* RETURN VALUE
* Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
*/
static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct channel *chn, uint dir, struct flt_otel_scope_span *span, struct flt_otel_scope_data *data, const struct flt_otel_conf_span *conf_span, const struct timespec *ts_steady, const struct timespec *ts_system, char **err)
{
struct flt_otel_conf *conf = FLT_OTEL_CONF(f);
int retval = FLT_OTEL_RET_OK;
OTELC_FUNC("%p, %p, %p, %u, %p, %p, %p, %p, %p, %p:%p", s, f, chn, dir, span, data, conf_span, ts_steady, ts_system, OTELC_DPTR_ARGS(err));
if (span == NULL)
OTELC_RETURN_INT(retval);
/* Create the OTel span on first invocation. */
if (span->span == NULL) {
span->span = OTELC_OPS(conf->instr->tracer, start_span_with_options, span->id, span->ref_span, span->ref_ctx, ts_steady, ts_system, OTELC_SPAN_KIND_SERVER, NULL, 0);
if (span->span == NULL)
OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
}
/* Add all resolved span links to the current span. */
if (!LIST_ISEMPTY(&(data->links))) {
struct flt_otel_scope_data_link *link;
list_for_each_entry(link, &(data->links), list) {
OTELC_DBG(DEBUG, "adding link %p %p", link->span, link->context);
if (OTELC_OPS(span->span, add_link, link->span, link->context, NULL, 0) == -1)
retval = FLT_OTEL_RET_ERROR;
}
}
/* Set baggage key-value pairs on the span. */
if (data->baggage.attr != NULL)
if (OTELC_OPS(span->span, set_baggage_kv_n, data->baggage.attr, data->baggage.cnt) == -1)
retval = FLT_OTEL_RET_ERROR;
/* Set span attributes. */
if (data->attributes.attr != NULL)
if (OTELC_OPS(span->span, set_attribute_kv_n, data->attributes.attr, data->attributes.cnt) == -1)
retval = FLT_OTEL_RET_ERROR;
/* Add span events in reverse order. */
if (!LIST_ISEMPTY(&(data->events))) {
struct flt_otel_scope_data_event *event;
list_for_each_entry_rev(event, &(data->events), list)
if (OTELC_OPS(span->span, add_event_kv_n, event->name, ts_system, event->attr, event->cnt) == -1)
retval = FLT_OTEL_RET_ERROR;
}
/* Set span status code and description. */
if (data->status.description != NULL)
if (OTELC_OPS(span->span, set_status, data->status.code, data->status.description) == -1)
retval = FLT_OTEL_RET_ERROR;
/* Inject span context into HTTP headers and variables. */
if (conf_span->ctx_id != NULL) {
struct otelc_http_headers_writer writer;
struct otelc_text_map *text_map = NULL;
if (flt_otel_inject_http_headers(span->span, &writer) != FLT_OTEL_RET_ERROR) {
int i = 0;
if (conf_span->ctx_flags & (FLT_OTEL_CTX_USE_VARS | FLT_OTEL_CTX_USE_HEADERS)) {
for (text_map = &(writer.text_map); i < text_map->count; i++) {
#ifdef USE_OTEL_VARS
if (!(conf_span->ctx_flags & FLT_OTEL_CTX_USE_VARS))
/* Do nothing. */;
else if (flt_otel_var_register(FLT_OTEL_VARS_SCOPE, conf_span->ctx_id, text_map->key[i], err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
else if (flt_otel_var_set(s, FLT_OTEL_VARS_SCOPE, conf_span->ctx_id, text_map->key[i], text_map->value[i], dir, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
#endif
if (!(conf_span->ctx_flags & FLT_OTEL_CTX_USE_HEADERS))
/* Do nothing. */;
else if (flt_otel_http_header_set(chn, conf_span->ctx_id, text_map->key[i], text_map->value[i], err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
}
}
otelc_text_map_destroy(&text_map);
}
}
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_scope_run - scope execution engine
*
* SYNOPSIS
* int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, struct flt_otel_conf_scope *conf_scope, const struct timespec *ts_steady, const struct timespec *ts_system, uint dir, char **err)
*
* ARGUMENTS
* s - the stream being processed
* f - the filter instance
* chn - the channel for context extraction and injection
* conf_scope - the scope configuration to execute
* ts_steady - the monotonic timestamp, or NULL to use current time
* ts_system - the wall-clock timestamp, or NULL to use current time
* dir - the sample fetch direction (SMP_OPT_DIR_REQ/RES)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Executes a complete scope: evaluates ACL conditions, extracts contexts
* from HTTP headers or HAProxy variables, iterates over configured spans
* (resolving links, evaluating sample expressions for attributes, events,
* baggage and status), calls flt_otel_scope_run_span() for each, processes
* metric instruments, emits log records, then marks and finishes completed
* spans.
*
* RETURN VALUE
* Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
*/
int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, struct flt_otel_conf_scope *conf_scope, const struct timespec *ts_steady, const struct timespec *ts_system, uint dir, char **err)
{
struct flt_otel_conf *conf = FLT_OTEL_CONF(f);
struct flt_otel_conf_context *conf_ctx;
struct flt_otel_conf_span *conf_span;
struct flt_otel_conf_str *span_to_finish;
struct timespec ts_now_steady, ts_now_system;
int retval = FLT_OTEL_RET_OK;
OTELC_FUNC("%p, %p, %p, %p, %p, %p, %u, %p:%p", s, f, chn, conf_scope, ts_steady, ts_system, dir, OTELC_DPTR_ARGS(err));
OTELC_DBG(DEBUG, "channel: %s, mode: %s (%s)", flt_otel_chn_label(chn), flt_otel_pr_mode(s), flt_otel_stream_pos(s));
OTELC_DBG(DEBUG, "run scope '%s' %d", conf_scope->id, conf_scope->event);
FLT_OTEL_DBG_CONF_SCOPE("run scope ", conf_scope);
if (ts_steady == NULL) {
(void)clock_gettime(CLOCK_MONOTONIC, &ts_now_steady);
ts_steady = &ts_now_steady;
}
if (ts_system == NULL) {
(void)clock_gettime(CLOCK_REALTIME, &ts_now_system);
ts_system = &ts_now_system;
}
/* Evaluate the scope's ACL condition; skip this scope on mismatch. */
if (conf_scope->cond != NULL) {
enum acl_test_res res;
int rc;
res = acl_exec_cond(conf_scope->cond, s->be, s->sess, s, dir | SMP_OPT_FINAL);
rc = acl_pass(res);
if (conf_scope->cond->pol == ACL_COND_UNLESS)
rc = !rc;
OTELC_DBG(DEBUG, "the ACL rule %s", rc ? "matches" : "does not match");
/*
* If the rule does not match, the current scope is skipped.
*
* If it is a root span, further processing of the session is
* disabled. As soon as the first span is encountered which
* is marked as root, further search is interrupted.
*/
if (rc == 0) {
list_for_each_entry(conf_span, &(conf_scope->spans), list)
if (conf_span->flag_root) {
OTELC_DBG(LOG, "session disabled");
FLT_OTEL_RT_CTX(f->ctx)->flag_disabled = 1;
#ifdef FLT_OTEL_USE_COUNTERS
_HA_ATOMIC_ADD(conf->cnt.disabled + 0, 1);
#endif
break;
}
OTELC_RETURN_INT(retval);
}
}
/* Extract and initialize OpenTelemetry propagation contexts. */
list_for_each_entry(conf_ctx, &(conf_scope->contexts), list) {
struct otelc_text_map *text_map = NULL;
OTELC_DBG(DEBUG, "run context '%s' -> '%s'", conf_scope->id, conf_ctx->id);
FLT_OTEL_DBG_CONF_CONTEXT("run context ", conf_ctx);
/*
* The OpenTelemetry context is read from the HTTP header
* or from HAProxy variables.
*/
if (conf_ctx->flags & FLT_OTEL_CTX_USE_HEADERS)
text_map = flt_otel_http_headers_get(chn, conf_ctx->id, conf_ctx->id_len, err);
#ifdef USE_OTEL_VARS
else
text_map = flt_otel_vars_get(s, FLT_OTEL_VARS_SCOPE, conf_ctx->id, dir, err);
#endif
if (text_map != NULL) {
if (flt_otel_scope_context_init(f->ctx, conf->instr->tracer, conf_ctx->id, conf_ctx->id_len, text_map, dir, err) == NULL)
retval = FLT_OTEL_RET_ERROR;
otelc_text_map_destroy(&text_map);
} else {
retval = FLT_OTEL_RET_ERROR;
}
}
/* Process configured spans: resolve links and collect samples. */
list_for_each_entry(conf_span, &(conf_scope->spans), list) {
struct flt_otel_scope_data data;
struct flt_otel_scope_span *span;
struct flt_otel_conf_sample *sample;
OTELC_DBG(DEBUG, "run span '%s' -> '%s'", conf_scope->id, conf_span->id);
FLT_OTEL_DBG_CONF_SPAN("run span ", conf_span);
flt_otel_scope_data_init(&data);
span = flt_otel_scope_span_init(f->ctx, conf_span->id, conf_span->id_len, conf_span->ref_id, conf_span->ref_id_len, dir, err);
if (span == NULL)
retval = FLT_OTEL_RET_ERROR;
/*
* Resolve configured span links against the runtime context.
* Each link name is looked up first in the active spans, then
* in the extracted contexts.
*/
if (!LIST_ISEMPTY(&(conf_span->links))) {
struct flt_otel_runtime_context *rt_ctx = FLT_OTEL_RT_CTX(f->ctx);
struct flt_otel_conf_link *conf_link;
list_for_each_entry(conf_link, &(conf_span->links), list) {
struct flt_otel_scope_data_link *data_link;
struct otelc_span *link_span = NULL;
struct otelc_span_context *link_ctx = NULL;
struct flt_otel_scope_span *sc_span;
struct flt_otel_scope_context *sc_ctx;
/* Try to find a matching span first. */
list_for_each_entry(sc_span, &(rt_ctx->spans), list)
if (FLT_OTEL_CONF_STR_CMP(sc_span->id, conf_link->span)) {
link_span = sc_span->span;
break;
}
/* If no span found, try to find a matching context. */
if (link_span == NULL) {
list_for_each_entry(sc_ctx, &(rt_ctx->contexts), list)
if (FLT_OTEL_CONF_STR_CMP(sc_ctx->id, conf_link->span)) {
link_ctx = sc_ctx->context;
break;
}
}
if ((link_span == NULL) && (link_ctx == NULL)) {
OTELC_DBG(NOTICE, "WARNING: cannot find linked span/context '%s'", conf_link->span);
continue;
}
data_link = OTELC_CALLOC(1, sizeof(*data_link));
if (data_link == NULL) {
retval = FLT_OTEL_RET_ERROR;
break;
}
data_link->span = link_span;
data_link->context = link_ctx;
LIST_APPEND(&(data.links), &(data_link->list));
OTELC_DBG(DEBUG, "resolved link '%s' -> %p %p", conf_link->span, link_span, link_ctx);
}
}
list_for_each_entry(sample, &(conf_span->attributes), list) {
OTELC_DBG(DEBUG, "adding attribute '%s' -> '%s'", sample->key, sample->fmt_string);
if (flt_otel_sample_add(s, dir, sample, &data, FLT_OTEL_EVENT_SAMPLE_ATTRIBUTE, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
}
list_for_each_entry(sample, &(conf_span->events), list) {
OTELC_DBG(DEBUG, "adding event '%s' -> '%s'", sample->key, sample->fmt_string);
if (flt_otel_sample_add(s, dir, sample, &data, FLT_OTEL_EVENT_SAMPLE_EVENT, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
}
list_for_each_entry(sample, &(conf_span->baggages), list) {
OTELC_DBG(DEBUG, "adding baggage '%s' -> '%s'", sample->key, sample->fmt_string);
if (flt_otel_sample_add(s, dir, sample, &data, FLT_OTEL_EVENT_SAMPLE_BAGGAGE, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
}
/*
* Regardless of the use of the list, only one status per event
* is allowed.
*/
list_for_each_entry(sample, &(conf_span->statuses), list) {
OTELC_DBG(DEBUG, "adding status '%s' -> '%s'", sample->key, sample->fmt_string);
if (flt_otel_sample_add(s, dir, sample, &data, FLT_OTEL_EVENT_SAMPLE_STATUS, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
}
/* Attempt to run the span regardless of earlier errors. */
if (span != NULL)
if (flt_otel_scope_run_span(s, f, chn, dir, span, &data, conf_span, ts_steady, ts_system, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
flt_otel_scope_data_free(&data);
}
/* Process metric instruments. */
if (!LIST_ISEMPTY(&(conf_scope->instruments)))
if (flt_otel_scope_run_instrument(s, dir, conf_scope, conf->instr->meter, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
/* Emit log records. */
if (!LIST_ISEMPTY(&(conf_scope->log_records)))
if (flt_otel_scope_run_log_record(s, f, dir, conf_scope, conf->instr->logger, ts_system, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
/* Mark the configured spans for finishing and clean up. */
list_for_each_entry(span_to_finish, &(conf_scope->spans_to_finish), list)
if (flt_otel_scope_finish_mark(f->ctx, span_to_finish->str, span_to_finish->str_len) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
flt_otel_scope_finish_marked(f->ctx, ts_steady);
flt_otel_scope_free_unused(f->ctx, chn);
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_event_run - top-level event dispatcher
*
* SYNOPSIS
* int flt_otel_event_run(struct stream *s, struct filter *f, struct channel *chn, int event, char **err)
*
* ARGUMENTS
* s - the stream being processed
* f - the filter instance
* chn - the channel being analyzed
* event - the event index (FLT_OTEL_EVENT_*)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Top-level event dispatcher called from filter callbacks. It iterates over
* all scopes matching the <event> index and calls flt_otel_scope_run() for
* each. All spans within a single event share the same monotonic and
* wall-clock timestamps.
*
* RETURN VALUE
* Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
*/
int flt_otel_event_run(struct stream *s, struct filter *f, struct channel *chn, int event, char **err)
{
struct flt_otel_conf *conf = FLT_OTEL_CONF(f);
struct flt_otel_conf_scope *conf_scope;
struct timespec ts_steady, ts_system;
int retval = FLT_OTEL_RET_OK;
OTELC_FUNC("%p, %p, %p, %d, %p:%p", s, f, chn, event, OTELC_DPTR_ARGS(err));
OTELC_DBG(DEBUG, "channel: %s, mode: %s (%s)", flt_otel_chn_label(chn), flt_otel_pr_mode(s), flt_otel_stream_pos(s));
OTELC_DBG(DEBUG, "run event '%s' %d %s", flt_otel_event_data[event].name, event, flt_otel_event_data[event].an_name);
#ifdef DEBUG_OTEL
_HA_ATOMIC_ADD(conf->cnt.event[event].htx + ((chn == NULL) ? 1 : (htx_is_empty(htxbuf(&(chn->buf))) ? 1 : 0)), 1);
#endif
FLT_OTEL_RT_CTX(f->ctx)->analyzers |= flt_otel_event_data[event].an_bit;
/* All spans should be created/completed at the same time. */
(void)clock_gettime(CLOCK_MONOTONIC, &ts_steady);
(void)clock_gettime(CLOCK_REALTIME, &ts_system);
/*
* It is possible that there are defined multiple scopes that use the
* same event. Therefore, there must not be a 'break' here, ie an exit
* from the 'for' loop.
*/
list_for_each_entry(conf_scope, &(conf->scopes), list) {
if (conf_scope->event != event)
/* Do nothing. */;
else if (!conf_scope->flag_used)
OTELC_DBG(DEBUG, "scope '%s' %d not used", conf_scope->id, conf_scope->event);
else if (flt_otel_scope_run(s, f, chn, conf_scope, &ts_steady, &ts_system, flt_otel_event_data[event].smp_opt_dir, err) == FLT_OTEL_RET_ERROR)
retval = FLT_OTEL_RET_ERROR;
}
#ifdef USE_OTEL_VARS
flt_otel_vars_dump(s);
#endif
flt_otel_http_headers_dump(chn);
OTELC_DBG(DEBUG, "event = %d %s, chn = %p, s->req = %p, s->res = %p", event, flt_otel_event_data[event].an_name, chn, &(s->req), &(s->res));
OTELC_RETURN_INT(retval);
}
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

1838
addons/otel/src/filter.c Normal file

File diff suppressed because it is too large Load diff

378
addons/otel/src/group.c Normal file
View file

@ -0,0 +1,378 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
/* Group data table built from the X-macro list. */
#define FLT_OTEL_GROUP_DEF(a,b,c) { a, b, c },
const struct flt_otel_group_data flt_otel_group_data[] = { FLT_OTEL_GROUP_DEFINES };
#undef FLT_OTEL_GROUP_DEF
/***
* NAME
* flt_otel_group_action - group action execution callback
*
* SYNOPSIS
* static enum act_return flt_otel_group_action(struct act_rule *rule, struct proxy *px, struct session *sess, struct stream *s, int opts)
*
* ARGUMENTS
* rule - action rule containing group configuration references
* px - proxy instance
* sess - current session
* s - current stream
* opts - action options (ACT_OPT_* flags)
*
* DESCRIPTION
* Executes the action_ptr callback for the FLT_OTEL_ACTION_GROUP action.
* Retrieves the filter configuration, group definition, and runtime context
* from the rule's argument pointers. If the filter is disabled or not
* attached to the stream, processing is skipped. Otherwise, iterates over
* all scopes defined in the group and runs each via flt_otel_scope_run().
* Scope execution errors are logged but do not prevent the remaining scopes
* from executing.
*
* RETURN VALUE
* Returns ACT_RET_CONT.
*/
static enum act_return flt_otel_group_action(struct act_rule *rule, struct proxy *px, struct session *sess, struct stream *s, int opts)
{
const struct filter *filter;
const struct flt_conf *fconf;
const struct flt_otel_conf *conf;
const struct flt_otel_conf_group *conf_group;
const struct flt_otel_runtime_context *rt_ctx = NULL;
const struct flt_otel_conf_ph *ph_scope;
char *err = NULL;
int i, rc;
OTELC_FUNC("%p, %p, %p, %p, %d", rule, px, sess, s, opts);
OTELC_DBG(DEBUG, "from: %d, arg.act %p:{ %p %p %p %p }", rule->from, &(rule->arg.act), rule->arg.act.p[0], rule->arg.act.p[1], rule->arg.act.p[2], rule->arg.act.p[3]);
fconf = rule->arg.act.p[FLT_OTEL_ARG_FLT_CONF];
conf = rule->arg.act.p[FLT_OTEL_ARG_CONF];
conf_group = ((const struct flt_otel_conf_ph *)(rule->arg.act.p[FLT_OTEL_ARG_GROUP]))->ptr;
if ((fconf == NULL) || (conf == NULL) || (conf_group == NULL)) {
FLT_OTEL_LOG(LOG_ERR, FLT_OTEL_ACTION_GROUP ": internal error, invalid group action");
OTELC_RETURN_EX(ACT_RET_CONT, enum act_return, "%d");
}
if (_HA_ATOMIC_LOAD(&(conf->instr->flag_disabled))) {
OTELC_DBG(INFO, "filter '%s' disabled, group action '%s' ignored", conf->id, conf_group->id);
OTELC_RETURN_EX(ACT_RET_CONT, enum act_return, "%d");
}
/* Find the OpenTelemetry filter instance from the current stream. */
list_for_each_entry(filter, &(s->strm_flt.filters), list)
if (filter->config == fconf) {
rt_ctx = filter->ctx;
break;
}
if (rt_ctx == NULL) {
OTELC_DBG(INFO, "cannot find filter, probably not attached to the stream");
OTELC_RETURN_EX(ACT_RET_CONT, enum act_return, "%d");
}
else if (flt_otel_is_disabled(filter FLT_OTEL_DBG_ARGS(, -1))) {
OTELC_RETURN_EX(ACT_RET_CONT, enum act_return, "%d");
}
else {
OTELC_DBG(DEBUG, "run group '%s'", conf_group->id);
FLT_OTEL_DBG_CONF_GROUP("run group ", conf_group);
}
/*
* Check the value of rule->from; in case it is incorrect,
* report an error.
*/
for (i = 0; i < OTELC_TABLESIZE(flt_otel_group_data); i++)
if (flt_otel_group_data[i].act_from == rule->from)
break;
if (i >= OTELC_TABLESIZE(flt_otel_group_data)) {
FLT_OTEL_LOG(LOG_ERR, FLT_OTEL_ACTION_GROUP ": internal error, invalid rule->from=%d", rule->from);
OTELC_RETURN_EX(ACT_RET_CONT, enum act_return, "%d");
}
/* Execute each scope defined in this group. */
list_for_each_entry(ph_scope, &(conf_group->ph_scopes), list) {
rc = flt_otel_scope_run(s, rt_ctx->filter, (flt_otel_group_data[i].smp_opt_dir == SMP_OPT_DIR_REQ) ? &(s->req) : &(s->res), ph_scope->ptr, NULL, NULL, flt_otel_group_data[i].smp_opt_dir, &err);
if ((rc == FLT_OTEL_RET_ERROR) && (opts & ACT_OPT_FINAL)) {
FLT_OTEL_LOG(LOG_ERR, FLT_OTEL_ACTION_GROUP ": scope '%s' failed in group '%s'", ph_scope->id, conf_group->id);
OTELC_SFREE_CLEAR(err);
}
}
OTELC_RETURN_EX(ACT_RET_CONT, enum act_return, "%d");
}
/***
* NAME
* flt_otel_group_check - group action post-parse check callback
*
* SYNOPSIS
* static int flt_otel_group_check(struct act_rule *rule, struct proxy *px, char **err)
*
* ARGUMENTS
* rule - action rule to validate
* px - proxy instance
* err - indirect pointer to error message string
*
* DESCRIPTION
* Validates the check_ptr callback for the FLT_OTEL_ACTION_GROUP action.
* Resolves the filter ID and group ID string references stored during parsing
* into direct pointers to the filter configuration and group configuration
* structures. Searches the proxy's filter list for a matching OTel filter,
* then locates the named group within that filter's configuration. On
* success, replaces the string ID pointers in <rule>->arg.act.p with the
* resolved configuration pointers.
*
* RETURN VALUE
* Returns 1 on success, or 0 on failure with <err> filled.
*/
static int flt_otel_group_check(struct act_rule *rule, struct proxy *px, char **err)
{
struct flt_conf *fconf_tmp, *fconf = NULL;
struct flt_otel_conf *conf;
struct flt_otel_conf_ph *ph_group;
const char *filter_id;
const char *group_id;
bool flag_found = 0;
int i;
OTELC_FUNC("%p, %p, %p:%p", rule, px, OTELC_DPTR_ARGS(err));
filter_id = rule->arg.act.p[FLT_OTEL_ARG_FILTER_ID];
group_id = rule->arg.act.p[FLT_OTEL_ARG_GROUP_ID];
OTELC_DBG(NOTICE, "checking filter_id='%s', group_id='%s'", filter_id, group_id);
/*
* Check the value of rule->from; in case it is incorrect, report an
* error.
*/
for (i = 0; i < OTELC_TABLESIZE(flt_otel_group_data); i++)
if (flt_otel_group_data[i].act_from == rule->from)
break;
if (i >= OTELC_TABLESIZE(flt_otel_group_data)) {
FLT_OTEL_ERR("internal error, unexpected rule->from=%d, please report this bug!", rule->from);
OTELC_RETURN_INT(0);
}
/*
* Try to find the OpenTelemetry filter by checking all filters for the
* proxy <px>.
*/
list_for_each_entry(fconf_tmp, &(px->filter_configs), list) {
conf = fconf_tmp->conf;
if (fconf_tmp->id != otel_flt_id) {
/* This is not an OpenTelemetry filter. */
continue;
}
else if (strcmp(conf->id, filter_id) == 0) {
/* This is the good filter ID. */
fconf = fconf_tmp;
break;
}
}
if (fconf == NULL) {
FLT_OTEL_ERR("unable to find the OpenTelemetry filter '%s' used by the " FLT_OTEL_ACTION_GROUP " '%s'", filter_id, group_id);
OTELC_RETURN_INT(0);
}
/*
* Attempt to find if the group is defined in the OpenTelemetry filter
* configuration.
*/
list_for_each_entry(ph_group, &(conf->instr->ph_groups), list)
if (strcmp(ph_group->id, group_id) == 0) {
flag_found = 1;
break;
}
if (!flag_found) {
FLT_OTEL_ERR("unable to find group '%s' in the OpenTelemetry filter '%s' configuration", group_id, filter_id);
OTELC_RETURN_INT(0);
}
OTELC_SFREE_CLEAR(rule->arg.act.p[FLT_OTEL_ARG_FILTER_ID]);
OTELC_SFREE_CLEAR(rule->arg.act.p[FLT_OTEL_ARG_GROUP_ID]);
/* Replace string IDs with resolved configuration pointers. */
rule->arg.act.p[FLT_OTEL_ARG_FLT_CONF] = fconf;
rule->arg.act.p[FLT_OTEL_ARG_CONF] = conf;
rule->arg.act.p[FLT_OTEL_ARG_GROUP] = ph_group;
OTELC_RETURN_INT(1);
}
/***
* NAME
* flt_otel_group_release - group action release callback
*
* SYNOPSIS
* static void flt_otel_group_release(struct act_rule *rule)
*
* ARGUMENTS
* rule - action rule being released
*
* DESCRIPTION
* Provides the release_ptr callback for the FLT_OTEL_ACTION_GROUP action.
* This is a no-op because the group action's argument pointers reference
* shared configuration structures that are freed separately during filter
* deinitialization.
*
* RETURN VALUE
* This function does not return a value.
*/
static void flt_otel_group_release(struct act_rule *rule)
{
OTELC_FUNC("%p", rule);
OTELC_RETURN();
}
/***
* NAME
* flt_otel_group_parse - group action keyword parser
*
* SYNOPSIS
* static enum act_parse_ret flt_otel_group_parse(const char **args, int *cur_arg, struct proxy *px, struct act_rule *rule, char **err)
*
* ARGUMENTS
* args - configuration line arguments array
* cur_arg - pointer to the current argument index
* px - proxy instance
* rule - action rule to populate
* err - indirect pointer to error message string
*
* DESCRIPTION
* Parses the FLT_OTEL_ACTION_GROUP action keyword from HAProxy configuration
* rules. Expects two arguments: a filter ID and a group ID, optionally
* followed by "if" or "unless" conditions. The filter ID and group ID are
* duplicated and stored in the <rule>'s argument pointers for later
* resolution by flt_otel_group_check(). The <rule>'s callbacks are set to
* flt_otel_group_action(), flt_otel_group_check(), and
* flt_otel_group_release(). This parser is registered for tcp-request,
* tcp-response, http-request, http-response, and http-after-response action
* contexts.
*
* RETURN VALUE
* Returns ACT_RET_PRS_OK on success, or ACT_RET_PRS_ERR on failure.
*/
static enum act_parse_ret flt_otel_group_parse(const char **args, int *cur_arg, struct proxy *px, struct act_rule *rule, char **err)
{
OTELC_FUNC("%p, %p, %p, %p, %p:%p", args, cur_arg, px, rule, OTELC_DPTR_ARGS(err));
FLT_OTEL_ARGS_DUMP();
if (!FLT_OTEL_ARG_ISVALID(*cur_arg) || !FLT_OTEL_ARG_ISVALID(*cur_arg + 1) ||
(FLT_OTEL_ARG_ISVALID(*cur_arg + 2) &&
!FLT_OTEL_PARSE_KEYWORD(*cur_arg + 2, FLT_OTEL_CONDITION_IF) &&
!FLT_OTEL_PARSE_KEYWORD(*cur_arg + 2, FLT_OTEL_CONDITION_UNLESS))) {
FLT_OTEL_ERR("expects: <filter-id> <group-id> [{ if | unless } ...]");
OTELC_RETURN_EX(ACT_RET_PRS_ERR, enum act_parse_ret, "%d");
}
/* Copy the OpenTelemetry filter id. */
rule->arg.act.p[FLT_OTEL_ARG_FILTER_ID] = OTELC_STRDUP(args[*cur_arg]);
if (rule->arg.act.p[FLT_OTEL_ARG_FILTER_ID] == NULL) {
FLT_OTEL_ERR("%s : out of memory", args[*cur_arg]);
OTELC_RETURN_EX(ACT_RET_PRS_ERR, enum act_parse_ret, "%d");
}
/* Copy the OpenTelemetry group id. */
rule->arg.act.p[FLT_OTEL_ARG_GROUP_ID] = OTELC_STRDUP(args[*cur_arg + 1]);
if (rule->arg.act.p[FLT_OTEL_ARG_GROUP_ID] == NULL) {
FLT_OTEL_ERR("%s : out of memory", args[*cur_arg + 1]);
OTELC_SFREE_CLEAR(rule->arg.act.p[FLT_OTEL_ARG_FILTER_ID]);
OTELC_RETURN_EX(ACT_RET_PRS_ERR, enum act_parse_ret, "%d");
}
/* Wire up the rule callbacks. */
rule->action = ACT_CUSTOM;
rule->action_ptr = flt_otel_group_action;
rule->check_ptr = flt_otel_group_check;
rule->release_ptr = flt_otel_group_release;
*cur_arg += 2;
OTELC_RETURN_EX(ACT_RET_PRS_OK, enum act_parse_ret, "%d");
}
/* TCP request content action keywords for the OTel group action. */
static struct action_kw_list tcp_req_action_kws = { ILH, {
{ FLT_OTEL_ACTION_GROUP, flt_otel_group_parse },
{ /* END */ },
}
};
INITCALL1(STG_REGISTER, tcp_req_cont_keywords_register, &tcp_req_action_kws);
/* TCP response content action keywords for the OTel group action. */
static struct action_kw_list tcp_res_action_kws = { ILH, {
{ FLT_OTEL_ACTION_GROUP, flt_otel_group_parse },
{ /* END */ },
}
};
INITCALL1(STG_REGISTER, tcp_res_cont_keywords_register, &tcp_res_action_kws);
/* HTTP request action keywords for the OTel group action. */
static struct action_kw_list http_req_action_kws = { ILH, {
{ FLT_OTEL_ACTION_GROUP, flt_otel_group_parse },
{ /* END */ },
}
};
INITCALL1(STG_REGISTER, http_req_keywords_register, &http_req_action_kws);
/* HTTP response action keywords for the OTel group action. */
static struct action_kw_list http_res_action_kws = { ILH, {
{ FLT_OTEL_ACTION_GROUP, flt_otel_group_parse },
{ /* END */ },
}
};
INITCALL1(STG_REGISTER, http_res_keywords_register, &http_res_action_kws);
/* HTTP after-response action keywords for the OTel group action. */
static struct action_kw_list http_after_res_actions_kws = { ILH, {
{ FLT_OTEL_ACTION_GROUP, flt_otel_group_parse },
{ /* END */ },
}
};
INITCALL1(STG_REGISTER, http_after_res_keywords_register, &http_after_res_actions_kws);
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

324
addons/otel/src/http.c Normal file
View file

@ -0,0 +1,324 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
#ifdef DEBUG_OTEL
/***
* NAME
* flt_otel_http_headers_dump - debug HTTP headers dump
*
* SYNOPSIS
* void flt_otel_http_headers_dump(const struct channel *chn)
*
* ARGUMENTS
* chn - channel to dump HTTP headers from
*
* DESCRIPTION
* Dumps all HTTP headers from the channel's HTX buffer. Iterates over HTX
* blocks, logging each header name-value pair at NOTICE level. Processing
* stops at the end-of-headers marker.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_http_headers_dump(const struct channel *chn)
{
const struct htx *htx;
int32_t pos;
OTELC_FUNC("%p", chn);
if (chn == NULL)
OTELC_RETURN();
htx = htxbuf(&(chn->buf));
if (htx_is_empty(htx))
OTELC_RETURN();
/* Walk HTX blocks and log each header until end-of-headers. */
for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) {
struct htx_blk *blk = htx_get_blk(htx, pos);
enum htx_blk_type type = htx_get_blk_type(blk);
if (type == HTX_BLK_HDR) {
struct ist n = htx_get_blk_name(htx, blk);
struct ist v = htx_get_blk_value(htx, blk);
OTELC_DBG(NOTICE, "'%.*s: %.*s'", (int)n.len, n.ptr, (int)v.len, v.ptr);
}
else if (type == HTX_BLK_EOH)
break;
}
OTELC_RETURN();
}
#endif /* DEBUG_OTEL */
/***
* NAME
* flt_otel_http_headers_get - HTTP header extraction to text map
*
* SYNOPSIS
* struct otelc_text_map *flt_otel_http_headers_get(struct channel *chn, const char *prefix, size_t len, char **err)
*
* ARGUMENTS
* chn - channel containing HTTP headers
* prefix - header name prefix to match (or NULL for all)
* len - length of the prefix string
* err - indirect pointer to error message string
*
* DESCRIPTION
* Extracts HTTP headers matching a <prefix> from the channel's HTX buffer
* into a newly allocated text map. When <prefix> is NULL or its length is
* zero, all headers are extracted. If the prefix starts with
* FLT_OTEL_PARSE_CTX_IGNORE_NAME, prefix matching is bypassed. The prefix
* (including the separator dash) is stripped from header names before storing
* in the text map. Empty header values are replaced with an empty string to
* avoid misinterpretation by otelc_text_map_add(). This function is used by
* the "extract" keyword to read span context from incoming request headers.
*
* RETURN VALUE
* Returns a pointer to the populated text map, or NULL on failure or when
* no matching headers are found.
*/
struct otelc_text_map *flt_otel_http_headers_get(struct channel *chn, const char *prefix, size_t len, char **err)
{
const struct htx *htx;
size_t prefix_len = (!OTELC_STR_IS_VALID(prefix) || (len == 0)) ? 0 : (len + 1);
int32_t pos;
struct otelc_text_map *retptr = NULL;
OTELC_FUNC("%p, \"%s\", %zu, %p:%p", chn, OTELC_STR_ARG(prefix), len, OTELC_DPTR_ARGS(err));
if (chn == NULL)
OTELC_RETURN_PTR(retptr);
/*
* The keyword 'inject' allows you to define the name of the OpenTelemetry
* context without using a prefix. In that case all HTTP headers are
* transferred because it is not possible to separate them from the
* OpenTelemetry context (this separation is usually done via a prefix).
*
* When using the 'extract' keyword, the context name must be specified.
* To allow all HTTP headers to be extracted, the first character of
* that name must be set to FLT_OTEL_PARSE_CTX_IGNORE_NAME.
*/
if (OTELC_STR_IS_VALID(prefix) && (*prefix == FLT_OTEL_PARSE_CTX_IGNORE_NAME))
prefix_len = 0;
htx = htxbuf(&(chn->buf));
for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) {
struct htx_blk *blk = htx_get_blk(htx, pos);
enum htx_blk_type type = htx_get_blk_type(blk);
if (type == HTX_BLK_HDR) {
struct ist v, n = htx_get_blk_name(htx, blk);
if ((prefix_len == 0) || ((n.len >= prefix_len) && (strncasecmp(n.ptr, prefix, len) == 0))) {
if (retptr == NULL) {
retptr = OTELC_TEXT_MAP_NEW(NULL, 8);
if (retptr == NULL) {
FLT_OTEL_ERR("failed to create HTTP header data");
break;
}
}
v = htx_get_blk_value(htx, blk);
/*
* In case the data of the HTTP header is not
* specified, v.ptr will have some non-null
* value and v.len will be equal to 0. The
* otelc_text_map_add() function will not
* interpret this well. In this case v.ptr
* is set to an empty string.
*/
if (v.len == 0)
v = ist("");
/*
* Here, an HTTP header (which is actually part
* of the span context is added to the text_map.
*
* Before adding, the prefix is removed from the
* HTTP header name.
*/
if (OTELC_TEXT_MAP_ADD(retptr, n.ptr + prefix_len, n.len - prefix_len, v.ptr, v.len, OTELC_TEXT_MAP_AUTO) == -1) {
FLT_OTEL_ERR("failed to add HTTP header data");
otelc_text_map_destroy(&retptr);
break;
}
}
}
else if (type == HTX_BLK_EOH)
break;
}
OTELC_TEXT_MAP_DUMP(retptr, "extracted HTTP headers");
if ((retptr != NULL) && (retptr->count == 0)) {
OTELC_DBG(NOTICE, "WARNING: no HTTP headers found");
otelc_text_map_destroy(&retptr);
}
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_http_header_set - HTTP header set or remove
*
* SYNOPSIS
* int flt_otel_http_header_set(struct channel *chn, const char *prefix, const char *name, const char *value, char **err)
*
* ARGUMENTS
* chn - channel containing HTTP headers
* prefix - header name prefix (or NULL)
* name - header name suffix (or NULL)
* value - header value to set (or NULL to remove only)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Sets or removes an HTTP header in the channel's HTX buffer. The full
* header name is constructed by combining <prefix> and <name> with a dash
* separator; if only one is provided, it is used directly. All existing
* occurrences of the header are removed first. If <name> is NULL, all
* headers starting with <prefix> are removed. If <value> is non-NULL, the
* header is then added with the new value. A NULL <value> causes only the
* removal, with no subsequent addition.
*
* RETURN VALUE
* Returns 0 on success, or FLT_OTEL_RET_ERROR on failure.
*/
int flt_otel_http_header_set(struct channel *chn, const char *prefix, const char *name, const char *value, char **err)
{
struct http_hdr_ctx ctx = { .blk = NULL };
struct ist ist_name;
struct buffer *buffer = NULL;
struct htx *htx;
int retval = FLT_OTEL_RET_ERROR;
OTELC_FUNC("%p, \"%s\", \"%s\", \"%s\", %p:%p", chn, OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), OTELC_STR_ARG(value), OTELC_DPTR_ARGS(err));
if ((chn == NULL) || (!OTELC_STR_IS_VALID(prefix) && !OTELC_STR_IS_VALID(name)))
OTELC_RETURN_INT(retval);
htx = htxbuf(&(chn->buf));
/*
* Very rare (about 1% of cases), htx is empty.
* In order to avoid segmentation fault, we exit this function.
*/
if (htx_is_empty(htx)) {
FLT_OTEL_ERR("HTX is empty");
OTELC_RETURN_INT(retval);
}
if (!OTELC_STR_IS_VALID(prefix)) {
ist_name = ist2((char *)name, strlen(name));
}
else if (!OTELC_STR_IS_VALID(name)) {
ist_name = ist2((char *)prefix, strlen(prefix));
}
else {
buffer = flt_otel_trash_alloc(0, err);
if (buffer == NULL)
OTELC_RETURN_INT(retval);
(void)chunk_printf(buffer, "%s-%s", prefix, name);
ist_name = ist2(buffer->area, buffer->data);
}
/* Remove all occurrences of the header. */
while (http_find_header(htx, ist(""), &ctx, 1) == 1) {
struct ist n = htx_get_blk_name(htx, ctx.blk);
#ifdef DEBUG_OTEL
struct ist v = htx_get_blk_value(htx, ctx.blk);
#endif
/*
* If the <name> parameter is not set, then remove all headers
* that start with the contents of the <prefix> parameter.
*/
if (!OTELC_STR_IS_VALID(name))
n.len = ist_name.len;
if (isteqi(n, ist_name))
if (http_remove_header(htx, &ctx) == 1)
OTELC_DBG(DEBUG, "HTTP header '%.*s: %.*s' removed", (int)n.len, n.ptr, (int)v.len, v.ptr);
}
/*
* If the value pointer has a value of NULL, the HTTP header is not set
* after deletion.
*/
if (value == NULL) {
retval = 0;
}
else if (http_add_header(htx, ist_name, ist(value)) == 1) {
retval = 0;
OTELC_DBG(DEBUG, "HTTP header '%s: %s' added", ist_name.ptr, value);
}
else {
FLT_OTEL_ERR("failed to set HTTP header '%s: %s'", ist_name.ptr, value);
}
flt_otel_trash_free(&buffer);
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_http_headers_remove - HTTP headers removal by prefix
*
* SYNOPSIS
* int flt_otel_http_headers_remove(struct channel *chn, const char *prefix, char **err)
*
* ARGUMENTS
* chn - channel containing HTTP headers
* prefix - header name prefix to match for removal
* err - indirect pointer to error message string
*
* DESCRIPTION
* Removes all HTTP headers matching the given <prefix> from the channel's HTX
* buffer. This is a convenience wrapper around flt_otel_http_header_set()
* with NULL <name> and <value> arguments.
*
* RETURN VALUE
* Returns 0 on success, or FLT_OTEL_RET_ERROR on failure.
*/
int flt_otel_http_headers_remove(struct channel *chn, const char *prefix, char **err)
{
int retval;
OTELC_FUNC("%p, \"%s\", %p:%p", chn, OTELC_STR_ARG(prefix), OTELC_DPTR_ARGS(err));
retval = flt_otel_http_header_set(chn, prefix, NULL, NULL, err);
OTELC_RETURN_INT(retval);
}
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

289
addons/otel/src/otelc.c Normal file
View file

@ -0,0 +1,289 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
/***
* NAME
* flt_otel_text_map_writer_set_cb - text map injection writer callback
*
* SYNOPSIS
* static int flt_otel_text_map_writer_set_cb(struct otelc_text_map_writer *writer, const char *key, const char *value)
*
* ARGUMENTS
* writer - text map writer instance
* key - context key name
* value - context key value
*
* DESCRIPTION
* Writer callback for text map injection. Called by the OTel C wrapper
* library during span context injection to store each key-value pair in the
* <writer>'s text map.
*
* RETURN VALUE
* Returns the result of OTELC_TEXT_MAP_ADD().
*/
static int flt_otel_text_map_writer_set_cb(struct otelc_text_map_writer *writer, const char *key, const char *value)
{
OTELC_FUNC("%p, \"%s\", \"%s\"", writer, OTELC_STR_ARG(key), OTELC_STR_ARG(value));
OTELC_RETURN_INT(OTELC_TEXT_MAP_ADD(&(writer->text_map), key, 0, value, 0, OTELC_TEXT_MAP_AUTO));
}
/***
* NAME
* flt_otel_http_headers_writer_set_cb - HTTP headers injection writer callback
*
* SYNOPSIS
* static int flt_otel_http_headers_writer_set_cb(struct otelc_http_headers_writer *writer, const char *key, const char *value)
*
* ARGUMENTS
* writer - HTTP headers writer instance
* key - context key name
* value - context key value
*
* DESCRIPTION
* Writer callback for HTTP headers injection. Called by the OTel C wrapper
* library during span context injection to store each key-value pair in the
* <writer>'s text map.
*
* RETURN VALUE
* Returns the result of OTELC_TEXT_MAP_ADD().
*/
static int flt_otel_http_headers_writer_set_cb(struct otelc_http_headers_writer *writer, const char *key, const char *value)
{
OTELC_FUNC("%p, \"%s\", \"%s\"", writer, OTELC_STR_ARG(key), OTELC_STR_ARG(value));
OTELC_RETURN_INT(OTELC_TEXT_MAP_ADD(&(writer->text_map), key, 0, value, 0, OTELC_TEXT_MAP_AUTO));
}
/***
* NAME
* flt_otel_inject_text_map - text map context injection
*
* SYNOPSIS
* int flt_otel_inject_text_map(const struct otelc_span *span, struct otelc_text_map_writer *carrier)
*
* ARGUMENTS
* span - span instance to inject context from
* carrier - text map writer carrier
*
* DESCRIPTION
* Injects the span context into a text map carrier. Initializes the
* <carrier> structure, sets the writer callback to
* flt_otel_text_map_writer_set_cb(), and delegates to the <span>'s
* inject_text_map() method.
*
* RETURN VALUE
* Returns the result of the <span>'s inject_text_map() method,
* or FLT_OTEL_RET_ERROR if arguments are NULL.
*/
int flt_otel_inject_text_map(const struct otelc_span *span, struct otelc_text_map_writer *carrier)
{
OTELC_FUNC("%p, %p", span, carrier);
if ((span == NULL) || (carrier == NULL))
OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
(void)memset(carrier, 0, sizeof(*carrier));
carrier->set = flt_otel_text_map_writer_set_cb;
OTELC_RETURN_INT(OTELC_OPS(span, inject_text_map, carrier));
}
/***
* NAME
* flt_otel_inject_http_headers - HTTP headers context injection
*
* SYNOPSIS
* int flt_otel_inject_http_headers(const struct otelc_span *span, struct otelc_http_headers_writer *carrier)
*
* ARGUMENTS
* span - span instance to inject context from
* carrier - HTTP headers writer carrier
*
* DESCRIPTION
* Injects the span context into an HTTP headers carrier. Initializes the
* <carrier> structure, sets the writer callback to
* flt_otel_http_headers_writer_set_cb(), and delegates to the <span>'s
* inject_http_headers() method.
*
* RETURN VALUE
* Returns the result of the <span>'s inject_http_headers() method,
* or FLT_OTEL_RET_ERROR if arguments are NULL.
*/
int flt_otel_inject_http_headers(const struct otelc_span *span, struct otelc_http_headers_writer *carrier)
{
OTELC_FUNC("%p, %p", span, carrier);
if ((span == NULL) || (carrier == NULL))
OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
(void)memset(carrier, 0, sizeof(*carrier));
carrier->set = flt_otel_http_headers_writer_set_cb;
OTELC_RETURN_INT(OTELC_OPS(span, inject_http_headers, carrier));
}
/***
* NAME
* flt_otel_text_map_reader_foreach_key_cb - text map extraction reader callback
*
* SYNOPSIS
* static int flt_otel_text_map_reader_foreach_key_cb(const struct otelc_text_map_reader *reader, int (*handler)(void *arg, const char *key, const char *value), void *arg)
*
* ARGUMENTS
* reader - text map reader instance
* handler - callback function invoked for each key-value pair
* arg - opaque argument passed to the handler
*
* DESCRIPTION
* Reader callback for text map extraction. Iterates over all key-value
* pairs in the <reader>'s text map and invokes <handler> for each. Iteration
* stops if the <handler> returns -1.
*
* RETURN VALUE
* Returns the last <handler> return value, or 0 if the text map is empty.
*/
static int flt_otel_text_map_reader_foreach_key_cb(const struct otelc_text_map_reader *reader, int (*handler)(void *arg, const char *key, const char *value), void *arg)
{
size_t i;
int retval = 0;
OTELC_FUNC("%p, %p, %p", reader, handler, arg);
for (i = 0; (retval != -1) && (i < reader->text_map.count); i++) {
OTELC_DBG(OTELC, "\"%s\" -> \"%s\"", OTELC_STR_ARG(reader->text_map.key[i]), OTELC_STR_ARG(reader->text_map.value[i]));
retval = handler(arg, reader->text_map.key[i], reader->text_map.value[i]);
}
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_http_headers_reader_foreach_key_cb - HTTP headers extraction reader callback
*
* SYNOPSIS
* static int flt_otel_http_headers_reader_foreach_key_cb(const struct otelc_http_headers_reader *reader, int (*handler)(void *arg, const char *key, const char *value), void *arg)
*
* ARGUMENTS
* reader - HTTP headers reader instance
* handler - callback function invoked for each key-value pair
* arg - opaque argument passed to the handler
*
* DESCRIPTION
* Reader callback for HTTP headers extraction. Iterates over all key-value
* pairs in the <reader>'s text map and invokes <handler> for each. Iteration
* stops if the <handler> returns -1.
*
* RETURN VALUE
* Returns the last <handler> return value, or 0 if the text map is empty.
*/
static int flt_otel_http_headers_reader_foreach_key_cb(const struct otelc_http_headers_reader *reader, int (*handler)(void *arg, const char *key, const char *value), void *arg)
{
size_t i;
int retval = 0;
OTELC_FUNC("%p, %p, %p", reader, handler, arg);
for (i = 0; (retval != -1) && (i < reader->text_map.count); i++) {
OTELC_DBG(OTELC, "\"%s\" -> \"%s\"", OTELC_STR_ARG(reader->text_map.key[i]), OTELC_STR_ARG(reader->text_map.value[i]));
retval = handler(arg, reader->text_map.key[i], reader->text_map.value[i]);
}
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_extract_text_map - text map context extraction
*
* SYNOPSIS
* struct otelc_span_context *flt_otel_extract_text_map(struct otelc_tracer *tracer, struct otelc_text_map_reader *carrier, const struct otelc_text_map *text_map)
*
* ARGUMENTS
* tracer - OTel tracer instance
* carrier - text map reader carrier
* text_map - text map containing the context data (or NULL)
*
* DESCRIPTION
* Extracts a span context from a text map carrier via the <tracer>.
* Initializes the <carrier> structure, sets the foreach_key callback to
* flt_otel_text_map_reader_foreach_key_cb(), and copies the <text_map> data
* into the <carrier>. Delegates to the <tracer>'s extract_text_map() method.
*
* RETURN VALUE
* Returns a pointer to the extracted span context, or NULL on failure.
*/
struct otelc_span_context *flt_otel_extract_text_map(struct otelc_tracer *tracer, struct otelc_text_map_reader *carrier, const struct otelc_text_map *text_map)
{
OTELC_FUNC("%p, %p, %p", tracer, carrier, text_map);
if ((tracer == NULL) || (carrier == NULL))
OTELC_RETURN_PTR(NULL);
(void)memset(carrier, 0, sizeof(*carrier));
carrier->foreach_key = flt_otel_text_map_reader_foreach_key_cb;
if (text_map != NULL)
(void)memcpy(&(carrier->text_map), text_map, sizeof(carrier->text_map));
OTELC_RETURN_PTR(OTELC_OPS(tracer, extract_text_map, carrier));
}
/***
* NAME
* flt_otel_extract_http_headers - HTTP headers context extraction
*
* SYNOPSIS
* struct otelc_span_context *flt_otel_extract_http_headers(struct otelc_tracer *tracer, struct otelc_http_headers_reader *carrier, const struct otelc_text_map *text_map)
*
* ARGUMENTS
* tracer - OTel tracer instance
* carrier - HTTP headers reader carrier
* text_map - text map containing the context data (or NULL)
*
* DESCRIPTION
* Extracts a span context from an HTTP headers carrier via the <tracer>.
* Initializes the <carrier> structure, sets the foreach_key callback to
* flt_otel_http_headers_reader_foreach_key_cb(), and copies the <text_map>
* data into the <carrier>. Delegates to the <tracer>'s
* extract_http_headers() method.
*
* RETURN VALUE
* Returns a pointer to the extracted span context, or NULL on failure.
*/
struct otelc_span_context *flt_otel_extract_http_headers(struct otelc_tracer *tracer, struct otelc_http_headers_reader *carrier, const struct otelc_text_map *text_map)
{
OTELC_FUNC("%p, %p, %p", tracer, carrier, text_map);
if ((tracer == NULL) || (carrier == NULL))
OTELC_RETURN_PTR(NULL);
(void)memset(carrier, 0, sizeof(*carrier));
carrier->foreach_key = flt_otel_http_headers_reader_foreach_key_cb;
if (text_map != NULL)
(void)memcpy(&(carrier->text_map), text_map, sizeof(carrier->text_map));
OTELC_RETURN_PTR(OTELC_OPS(tracer, extract_http_headers, carrier));
}
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

1828
addons/otel/src/parser.c Normal file

File diff suppressed because it is too large Load diff

385
addons/otel/src/pool.c Normal file
View file

@ -0,0 +1,385 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
struct pool_head *pool_head_otel_scope_span __read_mostly = NULL;
struct pool_head *pool_head_otel_scope_context __read_mostly = NULL;
struct pool_head *pool_head_otel_runtime_context __read_mostly = NULL;
struct pool_head *pool_head_otel_span_context __read_mostly = NULL;
#ifdef USE_POOL_OTEL_SCOPE_SPAN
REGISTER_POOL(&pool_head_otel_scope_span, "otel_scope_span", sizeof(struct flt_otel_scope_span));
#endif
#ifdef USE_POOL_OTEL_SCOPE_CONTEXT
REGISTER_POOL(&pool_head_otel_scope_context, "otel_scope_context", sizeof(struct flt_otel_scope_context));
#endif
#ifdef USE_POOL_OTEL_RUNTIME_CONTEXT
REGISTER_POOL(&pool_head_otel_runtime_context, "otel_runtime_context", sizeof(struct flt_otel_runtime_context));
#endif
#ifdef USE_POOL_OTEL_SPAN_CONTEXT
REGISTER_POOL(&pool_head_otel_span_context, "otel_span_context", MAX(sizeof(struct otelc_span), sizeof(struct otelc_span_context)));
#endif
/***
* NAME
* flt_otel_pool_alloc - pool-aware memory allocation
*
* SYNOPSIS
* void *flt_otel_pool_alloc(struct pool_head *pool, size_t size, bool flag_clear, char **err)
*
* ARGUMENTS
* pool - HAProxy memory pool to allocate from (or NULL for heap)
* size - number of bytes to allocate
* flag_clear - whether to zero-fill the allocated memory
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates <size> bytes of memory from the HAProxy memory <pool>. If <pool>
* is NULL, the allocation falls back to the heap via OTELC_MALLOC(). When
* <flag_clear> is set, the allocated memory is zero-filled. On allocation
* failure, an error message is stored via <err>.
*
* RETURN VALUE
* Returns a pointer to the allocated memory, or NULL on failure.
*/
void *flt_otel_pool_alloc(struct pool_head *pool, size_t size, bool flag_clear, char **err)
{
void *retptr;
OTELC_FUNC("%p, %zu, %hhu, %p:%p", pool, size, flag_clear, OTELC_DPTR_ARGS(err));
if (pool != NULL) {
retptr = pool_alloc(pool);
if (retptr != NULL)
OTELC_DBG(NOTICE, "POOL_ALLOC: %s:%d(%p %zu)", __func__, __LINE__, retptr, FLT_OTEL_DEREF(pool, size, size));
} else {
retptr = OTELC_MALLOC(size);
}
if (retptr == NULL)
FLT_OTEL_ERR("out of memory");
else if (flag_clear)
(void)memset(retptr, 0, size);
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_pool_strndup - pool-aware string duplication
*
* SYNOPSIS
* void *flt_otel_pool_strndup(struct pool_head *pool, const char *s, size_t size, char **err)
*
* ARGUMENTS
* pool - HAProxy memory pool to allocate from (or NULL for heap)
* s - source string to duplicate
* size - maximum number of characters to copy
* err - indirect pointer to error message string
*
* DESCRIPTION
* Duplicates up to <size> characters from the string <s> using the HAProxy
* memory <pool>. If <pool> is NULL, the duplication falls back to
* OTELC_STRNDUP(). When using a pool, the copy is truncated to <pool>->size-1
* bytes and null-terminated.
*
* RETURN VALUE
* Returns a pointer to the duplicated string, or NULL on failure.
*/
void *flt_otel_pool_strndup(struct pool_head *pool, const char *s, size_t size, char **err)
{
void *retptr;
OTELC_FUNC("%p, \"%.*s\", %zu, %p:%p", pool, (int)size, s, size, OTELC_DPTR_ARGS(err));
if (pool != NULL) {
retptr = pool_alloc(pool);
if (retptr != NULL) {
(void)memcpy(retptr, s, MIN(pool->size - 1, size));
((uint8_t *)retptr)[MIN(pool->size - 1, size)] = '\0';
}
} else {
retptr = OTELC_STRNDUP(s, size);
}
if (retptr != NULL)
OTELC_DBG(NOTICE, "POOL_STRNDUP: %s:%d(%p %zu)", __func__, __LINE__, retptr, FLT_OTEL_DEREF(pool, size, size));
else
FLT_OTEL_ERR("out of memory");
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_pool_free - pool-aware memory deallocation
*
* SYNOPSIS
* void flt_otel_pool_free(struct pool_head *pool, void **ptr)
*
* ARGUMENTS
* pool - HAProxy memory pool to return memory to (or NULL for heap)
* ptr - indirect pointer to the memory to free
*
* DESCRIPTION
* Returns memory referenced by <*ptr> to the HAProxy memory <pool>. If
* <pool> is NULL, the memory is freed via OTELC_SFREE(). The pointer <*ptr>
* is set to NULL after freeing. If <ptr> is NULL or <*ptr> is already NULL,
* the function returns immediately.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_pool_free(struct pool_head *pool, void **ptr)
{
OTELC_FUNC("%p, %p:%p", pool, OTELC_DPTR_ARGS(ptr));
if ((ptr == NULL) || (*ptr == NULL))
OTELC_RETURN();
OTELC_DBG(NOTICE, "POOL_FREE: %s:%d(%p %u)", __func__, __LINE__, *ptr, FLT_OTEL_DEREF(pool, size, 0));
if (pool != NULL)
pool_free(pool, *ptr);
else
OTELC_SFREE(*ptr);
*ptr = NULL;
OTELC_RETURN();
}
/***
* NAME
* flt_otel_pool_init - OTel filter memory pool initialization
*
* SYNOPSIS
* int flt_otel_pool_init(void)
*
* ARGUMENTS
* This function takes no arguments.
*
* DESCRIPTION
* Initializes all memory pools used by the OTel filter. Each pool is
* created only when the corresponding USE_POOL_OTEL_* macro is defined.
*
* RETURN VALUE
* Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
*/
int flt_otel_pool_init(void)
{
int retval = FLT_OTEL_RET_OK;
OTELC_FUNC("");
#ifdef USE_POOL_OTEL_SCOPE_SPAN
FLT_OTEL_POOL_INIT(pool_head_otel_scope_span, "otel_scope_span", sizeof(struct flt_otel_scope_span), retval);
#endif
#ifdef USE_POOL_OTEL_SCOPE_CONTEXT
FLT_OTEL_POOL_INIT(pool_head_otel_scope_context, "otel_scope_context", sizeof(struct flt_otel_scope_context), retval);
#endif
#ifdef USE_POOL_OTEL_RUNTIME_CONTEXT
FLT_OTEL_POOL_INIT(pool_head_otel_runtime_context, "otel_runtime_context", sizeof(struct flt_otel_runtime_context), retval);
#endif
#ifdef USE_POOL_OTEL_SPAN_CONTEXT
FLT_OTEL_POOL_INIT(pool_head_otel_span_context, "otel_span_context", OTELC_MAX(sizeof(struct otelc_span), sizeof(struct otelc_span_context)), retval);
#endif
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_pool_destroy - OTel filter memory pool destruction
*
* SYNOPSIS
* void flt_otel_pool_destroy(void)
*
* ARGUMENTS
* This function takes no arguments.
*
* DESCRIPTION
* Destroys all memory pools used by the OTel filter. Each pool is
* destroyed only when the corresponding USE_POOL_OTEL_* macro is defined.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_pool_destroy(void)
{
OTELC_FUNC("");
#ifdef USE_POOL_OTEL_SCOPE_SPAN
FLT_OTEL_POOL_DESTROY(pool_head_otel_scope_span);
#endif
#ifdef USE_POOL_OTEL_SCOPE_CONTEXT
FLT_OTEL_POOL_DESTROY(pool_head_otel_scope_context);
#endif
#ifdef USE_POOL_OTEL_RUNTIME_CONTEXT
FLT_OTEL_POOL_DESTROY(pool_head_otel_runtime_context);
#endif
#ifdef USE_POOL_OTEL_SPAN_CONTEXT
FLT_OTEL_POOL_DESTROY(pool_head_otel_span_context);
#endif
OTELC_RETURN();
}
#ifdef DEBUG_OTEL
/***
* NAME
* flt_otel_pool_info - debug pool sizes logging
*
* SYNOPSIS
* void flt_otel_pool_info(void)
*
* ARGUMENTS
* This function takes no arguments.
*
* DESCRIPTION
* Logs the sizes of all registered HAProxy memory pools used by the OTel
* filter (buffer, trash, scope_span, scope_context, runtime_context).
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_pool_info(void)
{
OTELC_DBG(NOTICE, "--- pool info ----------");
/*
* In case we have some error in the configuration file,
* it is possible that this pool was not initialized.
*/
#ifdef USE_POOL_BUFFER
OTELC_DBG(NOTICE, " buffer: %p %u", pool_head_buffer, FLT_OTEL_DEREF(pool_head_buffer, size, 0));
#endif
#ifdef USE_TRASH_CHUNK
OTELC_DBG(NOTICE, " trash: %p %u", pool_head_trash, FLT_OTEL_DEREF(pool_head_trash, size, 0));
#endif
#ifdef USE_POOL_OTEL_SCOPE_SPAN
OTELC_DBG(NOTICE, " otel_scope_span: %p %u", pool_head_otel_scope_span, FLT_OTEL_DEREF(pool_head_otel_scope_span, size, 0));
#endif
#ifdef USE_POOL_OTEL_SCOPE_CONTEXT
OTELC_DBG(NOTICE, " otel_scope_context: %p %u", pool_head_otel_scope_context, FLT_OTEL_DEREF(pool_head_otel_scope_context, size, 0));
#endif
#ifdef USE_POOL_OTEL_RUNTIME_CONTEXT
OTELC_DBG(NOTICE, " otel_runtime_context: %p %u", pool_head_otel_runtime_context, FLT_OTEL_DEREF(pool_head_otel_runtime_context, size, 0));
#endif
#ifdef USE_POOL_OTEL_SPAN_CONTEXT
OTELC_DBG(NOTICE, " otel_span_context: %p %u", pool_head_otel_span_context, FLT_OTEL_DEREF(pool_head_otel_span_context, size, 0));
#endif
}
#endif /* DEBUG_OTEL */
/***
* NAME
* flt_otel_trash_alloc - trash buffer allocation
*
* SYNOPSIS
* struct buffer *flt_otel_trash_alloc(bool flag_clear, char **err)
*
* ARGUMENTS
* flag_clear - whether to zero-fill the buffer area
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates a temporary buffer chunk for use as scratch space. When
* USE_TRASH_CHUNK is defined, the buffer is obtained via alloc_trash_chunk();
* otherwise, a buffer structure and its data area are allocated from the heap
* using global.tune.bufsize as the buffer size. When <flag_clear> is set,
* the buffer's data area is zero-filled.
*
* RETURN VALUE
* Returns a pointer to the allocated buffer, or NULL on failure.
*/
struct buffer *flt_otel_trash_alloc(bool flag_clear, char **err)
{
struct buffer *retptr;
OTELC_FUNC("%hhu, %p:%p", flag_clear, OTELC_DPTR_ARGS(err));
#ifdef USE_TRASH_CHUNK
retptr = alloc_trash_chunk();
if (retptr != NULL)
OTELC_DBG(NOTICE, "TRASH_ALLOC: %s:%d(%p %zu)", __func__, __LINE__, retptr, retptr->size);
#else
retptr = OTELC_MALLOC(sizeof(*retptr));
if (retptr != NULL) {
chunk_init(retptr, OTELC_MALLOC(global.tune.bufsize), global.tune.bufsize);
if (retptr->area == NULL)
OTELC_SFREE_CLEAR(retptr);
else
*(retptr->area) = '\0';
}
#endif
if (retptr == NULL)
FLT_OTEL_ERR("out of memory");
else if (flag_clear)
(void)memset(retptr->area, 0, retptr->size);
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_trash_free - trash buffer deallocation
*
* SYNOPSIS
* void flt_otel_trash_free(struct buffer **ptr)
*
* ARGUMENTS
* ptr - indirect pointer to the buffer to free
*
* DESCRIPTION
* Frees a trash buffer chunk previously allocated by flt_otel_trash_alloc().
* When USE_TRASH_CHUNK is defined, the buffer is freed via
* free_trash_chunk(); otherwise, both the data area and the buffer structure
* are freed individually. The pointer <*ptr> is set to NULL after freeing.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_trash_free(struct buffer **ptr)
{
OTELC_FUNC("%p:%p", OTELC_DPTR_ARGS(ptr));
if ((ptr == NULL) || (*ptr == NULL))
OTELC_RETURN();
OTELC_DBG(NOTICE, "TRASH_FREE: %s:%d(%p %zu)", __func__, __LINE__, *ptr, (*ptr)->size);
#ifdef USE_TRASH_CHUNK
free_trash_chunk(*ptr);
#else
OTELC_SFREE((*ptr)->area);
OTELC_SFREE(*ptr);
#endif
*ptr = NULL;
OTELC_RETURN();
}
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

745
addons/otel/src/scope.c Normal file
View file

@ -0,0 +1,745 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "../include/include.h"
/***
* NAME
* flt_otel_runtime_context_init - per-stream runtime context allocation
*
* SYNOPSIS
* struct flt_otel_runtime_context *flt_otel_runtime_context_init(struct stream *s, struct filter *f, char **err)
*
* ARGUMENTS
* s - the stream to which the context belongs
* f - the filter instance
* err - indirect pointer to error message string
*
* DESCRIPTION
* Allocates and initializes a per-stream runtime context from pool memory.
* It copies the hard-error, disabled and logging flags from the filter
* configuration, generates a UUID and stores it in the sess.otel.uuid
* HAProxy variable.
*
* RETURN VALUE
* Returns a pointer to the new runtime context, or NULL on failure.
*/
struct flt_otel_runtime_context *flt_otel_runtime_context_init(struct stream *s, struct filter *f, char **err)
{
const struct flt_otel_conf *conf = FLT_OTEL_CONF(f);
struct buffer uuid;
struct flt_otel_runtime_context *retptr = NULL;
OTELC_FUNC("%p, %p, %p:%p", s, f, OTELC_DPTR_ARGS(err));
retptr = flt_otel_pool_alloc(pool_head_otel_runtime_context, sizeof(*retptr), 1, err);
if (retptr == NULL)
OTELC_RETURN_PTR(retptr);
/* Initialize runtime context fields and generate a session UUID. */
retptr->stream = s;
retptr->filter = f;
retptr->flag_harderr = _HA_ATOMIC_LOAD(&(conf->instr->flag_harderr));
retptr->flag_disabled = _HA_ATOMIC_LOAD(&(conf->instr->flag_disabled));
retptr->logging = _HA_ATOMIC_LOAD(&(conf->instr->logging));
retptr->idle_timeout = 0;
retptr->idle_exp = TICK_ETERNITY;
LIST_INIT(&(retptr->spans));
LIST_INIT(&(retptr->contexts));
uuid = b_make(retptr->uuid, sizeof(retptr->uuid), 0, 0);
ha_generate_uuid_v4(&uuid);
#ifdef USE_OTEL_VARS
/*
* The HAProxy variable 'sess.otel.uuid' is registered here,
* after which its value is set to runtime context UUID.
*/
if (flt_otel_var_register(FLT_OTEL_VAR_UUID, err) != -1)
(void)flt_otel_var_set(s, FLT_OTEL_VAR_UUID, retptr->uuid, SMP_OPT_DIR_REQ, err);
#endif
FLT_OTEL_DBG_RUNTIME_CONTEXT("session context: ", retptr);
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_runtime_context_free - per-stream runtime context cleanup
*
* SYNOPSIS
* void flt_otel_runtime_context_free(struct filter *f)
*
* ARGUMENTS
* f - the filter instance whose runtime context is to be freed
*
* DESCRIPTION
* Frees the per-stream runtime context attached to <f>. It ends all active
* spans with the current monotonic timestamp, destroys all extracted
* contexts, and returns the pool memory.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_runtime_context_free(struct filter *f)
{
struct flt_otel_runtime_context *rt_ctx = f->ctx;
OTELC_FUNC("%p", f);
if (rt_ctx == NULL)
OTELC_RETURN();
FLT_OTEL_DBG_RUNTIME_CONTEXT("session context: ", rt_ctx);
/* End all active spans with a common timestamp. */
if (!LIST_ISEMPTY(&(rt_ctx->spans))) {
struct timespec ts_steady;
struct flt_otel_scope_span *span, *span_back;
/* All spans should be completed at the same time. */
(void)clock_gettime(CLOCK_MONOTONIC, &ts_steady);
list_for_each_entry_safe(span, span_back, &(rt_ctx->spans), list) {
OTELC_OPSR(span->span, end_with_options, &ts_steady, OTELC_SPAN_STATUS_IGNORE, NULL);
flt_otel_scope_span_free(&span);
}
}
/* Destroy all extracted span contexts. */
if (!LIST_ISEMPTY(&(rt_ctx->contexts))) {
struct flt_otel_scope_context *ctx, *ctx_back;
list_for_each_entry_safe(ctx, ctx_back, &(rt_ctx->contexts), list)
flt_otel_scope_context_free(&ctx);
}
flt_otel_pool_free(pool_head_otel_runtime_context, &(f->ctx));
OTELC_RETURN();
}
/***
* NAME
* flt_otel_scope_span_init - scope span lookup or creation
*
* SYNOPSIS
* struct flt_otel_scope_span *flt_otel_scope_span_init(struct flt_otel_runtime_context *rt_ctx, const char *id, size_t id_len, const char *ref_id, size_t ref_id_len, uint dir, char **err)
*
* ARGUMENTS
* rt_ctx - the runtime context owning the span list
* id - the span operation name
* id_len - length of the <id> string
* ref_id - the parent span or context name, or NULL
* ref_id_len - length of the <ref_id> string
* dir - the sample fetch direction (SMP_OPT_DIR_REQ/RES)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Finds an existing scope span by <id> in the runtime context or creates a
* new one. If <ref_id> is set, it resolves the parent reference by searching
* the span list first, then the extracted context list.
*
* RETURN VALUE
* Returns the existing or new scope span, or NULL on failure.
*/
struct flt_otel_scope_span *flt_otel_scope_span_init(struct flt_otel_runtime_context *rt_ctx, const char *id, size_t id_len, const char *ref_id, size_t ref_id_len, uint dir, char **err)
{
struct otelc_span *ref_span = NULL;
struct otelc_span_context *ref_ctx = NULL;
struct flt_otel_scope_span *span, *retptr = NULL;
struct flt_otel_scope_context *ctx;
OTELC_FUNC("%p, \"%s\", %zu, \"%s\", %zu, %u, %p:%p", rt_ctx, OTELC_STR_ARG(id), id_len, OTELC_STR_ARG(ref_id), ref_id_len, dir, OTELC_DPTR_ARGS(err));
if ((rt_ctx == NULL) || (id == NULL))
OTELC_RETURN_PTR(retptr);
/* Return the existing span if one matches this ID. */
list_for_each_entry(span, &(rt_ctx->spans), list)
if (FLT_OTEL_CONF_STR_CMP(span->id, id)) {
OTELC_DBG(NOTICE, "found span %p", span);
OTELC_RETURN_PTR(span);
}
/* Resolve the parent reference from spans or contexts. */
if (ref_id != NULL) {
list_for_each_entry(span, &(rt_ctx->spans), list)
if (FLT_OTEL_CONF_STR_CMP(span->id, ref_id)) {
ref_span = span->span;
break;
}
if (ref_span != NULL) {
OTELC_DBG(NOTICE, "found referenced span %p", span);
} else {
list_for_each_entry(ctx, &(rt_ctx->contexts), list)
if (FLT_OTEL_CONF_STR_CMP(ctx->id, ref_id)) {
ref_ctx = ctx->context;
break;
}
if (ref_ctx != NULL) {
OTELC_DBG(NOTICE, "found referenced context %p", ctx);
} else {
FLT_OTEL_ERR("cannot find referenced span/context '%s'", ref_id);
OTELC_RETURN_PTR(retptr);
}
}
}
retptr = flt_otel_pool_alloc(pool_head_otel_scope_span, sizeof(*retptr), 1, err);
if (retptr == NULL)
OTELC_RETURN_PTR(retptr);
/* Populate the new scope span and insert it into the list. */
retptr->id = id;
retptr->id_len = id_len;
retptr->smp_opt_dir = dir;
retptr->ref_span = ref_span;
retptr->ref_ctx = ref_ctx;
LIST_INSERT(&(rt_ctx->spans), &(retptr->list));
FLT_OTEL_DBG_SCOPE_SPAN("new span ", retptr);
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_scope_span_free - scope span cleanup
*
* SYNOPSIS
* void flt_otel_scope_span_free(struct flt_otel_scope_span **ptr)
*
* ARGUMENTS
* ptr - pointer to the scope span pointer to free
*
* DESCRIPTION
* Frees a scope span entry pointed to by <ptr> and removes it from its list.
* If the OTel span is still active (non-NULL), the function refuses to free
* it and returns immediately.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_scope_span_free(struct flt_otel_scope_span **ptr)
{
OTELC_FUNC("%p:%p", OTELC_DPTR_ARGS(ptr));
if ((ptr == NULL) || (*ptr == NULL))
OTELC_RETURN();
FLT_OTEL_DBG_SCOPE_SPAN("", *ptr);
/* If the span is still active, do nothing. */
if ((*ptr)->span != NULL) {
OTELC_DBG(NOTICE, "cannot finish active span");
OTELC_RETURN();
}
FLT_OTEL_LIST_DEL(&((*ptr)->list));
flt_otel_pool_free(pool_head_otel_scope_span, (void **)ptr);
OTELC_RETURN();
}
/***
* NAME
* flt_otel_scope_context_init - scope context extraction
*
* SYNOPSIS
* struct flt_otel_scope_context *flt_otel_scope_context_init(struct flt_otel_runtime_context *rt_ctx, struct otelc_tracer *tracer, const char *id, size_t id_len, const struct otelc_text_map *text_map, uint dir, char **err)
*
* ARGUMENTS
* rt_ctx - the runtime context owning the context list
* tracer - the OTel tracer used for context extraction
* id - the context name
* id_len - length of the <id> string
* text_map - the carrier text map to extract from
* dir - the sample fetch direction (SMP_OPT_DIR_REQ/RES)
* err - indirect pointer to error message string
*
* DESCRIPTION
* Finds an existing scope context by <id> in the runtime context or creates
* a new one by extracting the span context from the <text_map> carrier via
* the <tracer>.
*
* RETURN VALUE
* Returns the existing or new scope context, or NULL on failure.
*/
struct flt_otel_scope_context *flt_otel_scope_context_init(struct flt_otel_runtime_context *rt_ctx, struct otelc_tracer *tracer, const char *id, size_t id_len, const struct otelc_text_map *text_map, uint dir, char **err)
{
struct otelc_http_headers_reader reader;
struct otelc_span_context *span_ctx;
struct flt_otel_scope_context *retptr = NULL;
OTELC_FUNC("%p, %p, \"%s\", %zu, %p, %u, %p:%p", rt_ctx, tracer, OTELC_STR_ARG(id), id_len, text_map, dir, OTELC_DPTR_ARGS(err));
if ((rt_ctx == NULL) || (tracer == NULL) || (id == NULL) || (text_map == NULL))
OTELC_RETURN_PTR(retptr);
/* Return the existing context if one matches this ID. */
list_for_each_entry(retptr, &(rt_ctx->contexts), list)
if (FLT_OTEL_CONF_STR_CMP(retptr->id, id)) {
OTELC_DBG(NOTICE, "found context %p", retptr);
OTELC_RETURN_PTR(retptr);
}
retptr = flt_otel_pool_alloc(pool_head_otel_scope_context, sizeof(*retptr), 1, err);
if (retptr == NULL)
OTELC_RETURN_PTR(retptr);
span_ctx = flt_otel_extract_http_headers(tracer, &reader, text_map);
if (span_ctx == NULL) {
flt_otel_scope_context_free(&retptr);
OTELC_RETURN_PTR(retptr);
}
/* Populate the new scope context and insert it into the list. */
retptr->id = id;
retptr->id_len = id_len;
retptr->smp_opt_dir = dir;
retptr->context = span_ctx;
LIST_INSERT(&(rt_ctx->contexts), &(retptr->list));
FLT_OTEL_DBG_SCOPE_CONTEXT("new context ", retptr);
OTELC_RETURN_PTR(retptr);
}
/***
* NAME
* flt_otel_scope_context_free - scope context cleanup
*
* SYNOPSIS
* void flt_otel_scope_context_free(struct flt_otel_scope_context **ptr)
*
* ARGUMENTS
* ptr - pointer to the scope context pointer to free
*
* DESCRIPTION
* Frees a scope context entry pointed to by <ptr>. It destroys the
* underlying OTel span context, removes the entry from its list, and
* returns the pool memory.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_scope_context_free(struct flt_otel_scope_context **ptr)
{
OTELC_FUNC("%p:%p", OTELC_DPTR_ARGS(ptr));
if ((ptr == NULL) || (*ptr == NULL))
OTELC_RETURN();
FLT_OTEL_DBG_SCOPE_CONTEXT("", *ptr);
if ((*ptr)->context != NULL)
OTELC_OPSR((*ptr)->context, destroy);
FLT_OTEL_LIST_DEL(&((*ptr)->list));
flt_otel_pool_free(pool_head_otel_scope_context, (void **)ptr);
OTELC_RETURN();
}
#ifdef DEBUG_OTEL
/***
* NAME
* flt_otel_scope_data_dump - debug scope data dump
*
* SYNOPSIS
* void flt_otel_scope_data_dump(const struct flt_otel_scope_data *data)
*
* ARGUMENTS
* data - the scope data structure to dump
*
* DESCRIPTION
* Dumps the contents of a scope <data> structure for debugging: baggage
* key-value pairs, attributes, events with their attributes, span links,
* and the status code/description.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_scope_data_dump(const struct flt_otel_scope_data *data)
{
size_t i;
if (data == NULL)
return;
if (data->baggage.attr == NULL) {
OTELC_DBG(WORKER, "baggage %p:{ }", &(data->baggage));
} else {
OTELC_DBG(WORKER, "baggage %p:{", &(data->baggage));
for (i = 0; i < data->baggage.cnt; i++)
OTELC_DBG_KV(WORKER, " ", data->baggage.attr + i);
OTELC_DBG(WORKER, "}");
}
if (data->attributes.attr == NULL) {
OTELC_DBG(WORKER, "attributes %p:{ }", &(data->attributes));
} else {
OTELC_DBG(WORKER, "attributes %p:{", &(data->attributes));
for (i = 0; i < data->attributes.cnt; i++)
OTELC_DBG_KV(WORKER, " ", data->attributes.attr + i);
OTELC_DBG(WORKER, "}");
}
if (LIST_ISEMPTY(&(data->events))) {
OTELC_DBG(WORKER, "events %p:{ }", &(data->events));
} else {
struct flt_otel_scope_data_event *event;
OTELC_DBG(WORKER, "events %p:{", &(data->events));
list_for_each_entry_rev(event, &(data->events), list) {
OTELC_DBG(WORKER, " '%s' %zu/%zu", event->name, event->cnt, event->size);
if (event->attr != NULL)
for (i = 0; i < event->cnt; i++)
OTELC_DBG_KV(WORKER, " ", event->attr + i);
}
OTELC_DBG(WORKER, "}");
}
if (LIST_ISEMPTY(&(data->links))) {
OTELC_DBG(WORKER, "links %p:{ }", &(data->links));
} else {
struct flt_otel_scope_data_link *link;
OTELC_DBG(WORKER, "links %p:{", &(data->links));
list_for_each_entry(link, &(data->links), list)
OTELC_DBG(WORKER, " %p %p", link->span, link->context);
OTELC_DBG(WORKER, "}");
}
if ((data->status.code == 0) && (data->status.description == NULL))
OTELC_DBG(WORKER, "status %p:{ }", &(data->status));
else
FLT_OTEL_DBG_SCOPE_DATA_STATUS("status ", &(data->status));
}
#endif /* DEBUG_OTEL */
/***
* NAME
* flt_otel_scope_data_init - scope data zero-initialization
*
* SYNOPSIS
* void flt_otel_scope_data_init(struct flt_otel_scope_data *ptr)
*
* ARGUMENTS
* ptr - the scope data structure to initialize
*
* DESCRIPTION
* Zero-initializes the scope data structure pointed to by <ptr> and sets up
* the event and link list heads.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_scope_data_init(struct flt_otel_scope_data *ptr)
{
OTELC_FUNC("%p", ptr);
if (ptr == NULL)
OTELC_RETURN();
(void)memset(ptr, 0, sizeof(*ptr));
LIST_INIT(&(ptr->events));
LIST_INIT(&(ptr->links));
OTELC_RETURN();
}
/***
* NAME
* flt_otel_scope_data_free - scope data cleanup
*
* SYNOPSIS
* void flt_otel_scope_data_free(struct flt_otel_scope_data *ptr)
*
* ARGUMENTS
* ptr - the scope data structure to free
*
* DESCRIPTION
* Frees all contents of the scope data structure pointed to by <ptr>: baggage
* and attribute key-value arrays, event entries with their attributes, link
* entries, and the status description string.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_scope_data_free(struct flt_otel_scope_data *ptr)
{
struct flt_otel_scope_data_event *event, *event_back;
struct flt_otel_scope_data_link *link, *link_back;
OTELC_FUNC("%p", ptr);
if (ptr == NULL)
OTELC_RETURN();
FLT_OTEL_DBG_SCOPE_DATA("", ptr);
/* Destroy all dynamic scope data contents. */
otelc_kv_destroy(&(ptr->baggage.attr), ptr->baggage.cnt);
otelc_kv_destroy(&(ptr->attributes.attr), ptr->attributes.cnt);
list_for_each_entry_safe(event, event_back, &(ptr->events), list) {
otelc_kv_destroy(&(event->attr), event->cnt);
OTELC_SFREE(event->name);
OTELC_SFREE(event);
}
/* Free all resolved link entries. */
list_for_each_entry_safe(link, link_back, &(ptr->links), list)
OTELC_SFREE(link);
OTELC_SFREE(ptr->status.description);
(void)memset(ptr, 0, sizeof(*ptr));
OTELC_RETURN();
}
/***
* NAME
* flt_otel_scope_finish_mark - mark spans and contexts for finishing
*
* SYNOPSIS
* int flt_otel_scope_finish_mark(const struct flt_otel_runtime_context *rt_ctx, const char *id, size_t id_len)
*
* ARGUMENTS
* rt_ctx - the runtime context containing spans and contexts
* id - the target name, or a wildcard ("*", "*req*", "*res*")
* id_len - length of the <id> string
*
* DESCRIPTION
* Marks spans and contexts for finishing. The <id> argument supports
* wildcards: "*" marks all spans and contexts, "*req*" marks the request
* channel only, "*res*" marks the response channel only. Otherwise, a named
* span or context is looked up by exact match.
*
* RETURN VALUE
* Returns the number of spans and contexts that were marked.
*/
int flt_otel_scope_finish_mark(const struct flt_otel_runtime_context *rt_ctx, const char *id, size_t id_len)
{
struct flt_otel_scope_span *span;
struct flt_otel_scope_context *ctx;
int span_cnt = 0, ctx_cnt = 0, retval;
OTELC_FUNC("%p, \"%s\", %zu", rt_ctx, OTELC_STR_ARG(id), id_len);
/* Handle wildcard finish marks: all, request-only, response-only. */
if (FLT_OTEL_STR_CMP(FLT_OTEL_SCOPE_SPAN_FINISH_ALL, id)) {
list_for_each_entry(span, &(rt_ctx->spans), list) {
span->flag_finish = 1;
span_cnt++;
}
list_for_each_entry(ctx, &(rt_ctx->contexts), list) {
ctx->flag_finish = 1;
ctx_cnt++;
}
OTELC_DBG(NOTICE, "marked %d span(s), %d context(s)", span_cnt, ctx_cnt);
}
else if (FLT_OTEL_STR_CMP(FLT_OTEL_SCOPE_SPAN_FINISH_REQ, id)) {
list_for_each_entry(span, &(rt_ctx->spans), list)
if (span->smp_opt_dir == SMP_OPT_DIR_REQ) {
span->flag_finish = 1;
span_cnt++;
}
list_for_each_entry(ctx, &(rt_ctx->contexts), list)
if (ctx->smp_opt_dir == SMP_OPT_DIR_REQ) {
ctx->flag_finish = 1;
ctx_cnt++;
}
OTELC_DBG(NOTICE, "marked REQuest channel %d span(s), %d context(s)", span_cnt, ctx_cnt);
}
else if (FLT_OTEL_STR_CMP(FLT_OTEL_SCOPE_SPAN_FINISH_RES, id)) {
list_for_each_entry(span, &(rt_ctx->spans), list)
if (span->smp_opt_dir == SMP_OPT_DIR_RES) {
span->flag_finish = 1;
span_cnt++;
}
list_for_each_entry(ctx, &(rt_ctx->contexts), list)
if (ctx->smp_opt_dir == SMP_OPT_DIR_RES) {
ctx->flag_finish = 1;
ctx_cnt++;
}
OTELC_DBG(NOTICE, "marked RESponse channel %d span(s), %d context(s)", span_cnt, ctx_cnt);
}
else {
list_for_each_entry(span, &(rt_ctx->spans), list)
if (FLT_OTEL_CONF_STR_CMP(span->id, id)) {
span->flag_finish = 1;
span_cnt++;
break;
}
list_for_each_entry(ctx, &(rt_ctx->contexts), list)
if (FLT_OTEL_CONF_STR_CMP(ctx->id, id)) {
ctx->flag_finish = 1;
ctx_cnt++;
break;
}
if (span_cnt > 0)
OTELC_DBG(NOTICE, "marked span '%s'", id);
if (ctx_cnt > 0)
OTELC_DBG(NOTICE, "marked context '%s'", id);
if ((span_cnt + ctx_cnt) == 0)
OTELC_DBG(NOTICE, "cannot find span/context '%s'", id);
}
retval = span_cnt + ctx_cnt;
OTELC_RETURN_INT(retval);
}
/***
* NAME
* flt_otel_scope_finish_marked - finish marked spans and contexts
*
* SYNOPSIS
* void flt_otel_scope_finish_marked(const struct flt_otel_runtime_context *rt_ctx, const struct timespec *ts_finish)
*
* ARGUMENTS
* rt_ctx - the runtime context containing spans and contexts
* ts_finish - the monotonic timestamp to use as the span end time
*
* DESCRIPTION
* Ends all spans and destroys all contexts that have been marked for
* finishing by flt_otel_scope_finish_mark(). Each span is ended with the
* <ts_finish> timestamp; each context's OTel span context is destroyed.
* The finish flags are cleared after processing.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_scope_finish_marked(const struct flt_otel_runtime_context *rt_ctx, const struct timespec *ts_finish)
{
struct flt_otel_scope_span *span;
struct flt_otel_scope_context *ctx;
OTELC_FUNC("%p, %p", rt_ctx, ts_finish);
/* End all spans that have been marked for finishing. */
list_for_each_entry(span, &(rt_ctx->spans), list)
if (span->flag_finish) {
FLT_OTEL_DBG_SCOPE_SPAN("finishing span ", span);
OTELC_OPSR(span->span, end_with_options, ts_finish, OTELC_SPAN_STATUS_IGNORE, NULL);
span->flag_finish = 0;
}
/* Destroy all contexts that have been marked for finishing. */
list_for_each_entry(ctx, &(rt_ctx->contexts), list)
if (ctx->flag_finish) {
FLT_OTEL_DBG_SCOPE_CONTEXT("finishing context ", ctx);
if (ctx->context != NULL)
OTELC_OPSR(ctx->context, destroy);
ctx->flag_finish = 0;
}
OTELC_RETURN();
}
/***
* NAME
* flt_otel_scope_free_unused - remove unused spans and contexts
*
* SYNOPSIS
* void flt_otel_scope_free_unused(struct flt_otel_runtime_context *rt_ctx, struct channel *chn)
*
* ARGUMENTS
* rt_ctx - the runtime context to clean up
* chn - the channel for HTTP header cleanup
*
* DESCRIPTION
* Removes scope spans with a NULL OTel span and scope contexts with a NULL
* OTel context from the runtime context. For each removed context, the
* associated HTTP headers and HAProxy variables are also cleaned up via
* <chn>.
*
* RETURN VALUE
* This function does not return a value.
*/
void flt_otel_scope_free_unused(struct flt_otel_runtime_context *rt_ctx, struct channel *chn)
{
OTELC_FUNC("%p, %p", rt_ctx, chn);
if (rt_ctx == NULL)
OTELC_RETURN();
/* Remove spans that were never successfully created. */
if (!LIST_ISEMPTY(&(rt_ctx->spans))) {
struct flt_otel_scope_span *span, *span_back;
list_for_each_entry_safe(span, span_back, &(rt_ctx->spans), list)
if (span->span == NULL)
flt_otel_scope_span_free(&span);
}
/* Remove contexts that failed extraction and clean up their traces. */
if (!LIST_ISEMPTY(&(rt_ctx->contexts))) {
struct flt_otel_scope_context *ctx, *ctx_back;
list_for_each_entry_safe(ctx, ctx_back, &(rt_ctx->contexts), list)
if (ctx->context == NULL) {
/*
* All headers and variables associated with
* the context in question should be deleted.
*/
(void)flt_otel_http_headers_remove(chn, ctx->id, NULL);
#ifdef USE_OTEL_VARS
(void)flt_otel_vars_unset(rt_ctx->stream, FLT_OTEL_VARS_SCOPE, ctx->id, ctx->smp_opt_dir, NULL);
#endif
flt_otel_scope_context_free(&ctx);
}
}
FLT_OTEL_DBG_RUNTIME_CONTEXT("session context: ", rt_ctx);
OTELC_RETURN();
}
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*
* vi: noexpandtab shiftwidth=8 tabstop=8
*/

1087
addons/otel/src/util.c Normal file

File diff suppressed because it is too large Load diff

1175
addons/otel/src/vars.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,85 @@
Comparison test configuration (cmp/)
====================================
The 'cmp' test is a simplified standalone configuration made for comparison with
other tracing implementations. It uses a reduced set of events and a compact
span hierarchy without context propagation, groups or metrics. This
configuration is closer to a typical production deployment.
All response-side scopes (http_response, http_response-error, server_session_end
and client_session_end) share the on-http-response event, which means they fire
in a single batch at response time.
Files
-----
cmp/otel.cfg OTel filter configuration (scopes)
cmp/haproxy.cfg HAProxy frontend/backend configuration
cmp/otel.yml Exporter, processor, reader and provider definitions
run-cmp.sh Convenience script to launch HAProxy with this config
Events
------
T = Trace (span)
This configuration produces traces only -- no metrics or log-records.
Request analyzer events:
Event Scope T
--------------------------------------------------------
on-client-session-start client_session_start x
on-frontend-tcp-request frontend_tcp_request x
on-frontend-http-request frontend_http_request x
on-backend-tcp-request backend_tcp_request x
on-backend-http-request backend_http_request x
on-server-unavailable server_unavailable x
Response analyzer events:
Event Scope T
--------------------------------------------------------
on-server-session-start server_session_start x
on-tcp-response tcp_response x
on-http-response http_response x
on-http-response http_response-error x (conditional)
on-http-response server_session_end - (finish only)
on-http-response client_session_end - (finish only)
The http_response-error scope fires conditionally when the ACL
acl-http-status-ok (status 100:399) does not match, setting an error status
on the "HTTP response" span.
The server_session_end and client_session_end scopes are bound to the
on-http-response event and only perform finish operations.
Span hierarchy
--------------
"HAProxy session" (root)
+-- "Client session"
+-- "Frontend TCP request"
+-- "Frontend HTTP request"
+-- "Backend TCP request"
+-- "Backend HTTP request"
"HAProxy session" (root)
+-- "Server session"
+-- "TCP response"
+-- "HTTP response"
Running the test
----------------
From the test/ directory:
% ./run-cmp.sh [/path/to/haproxy] [pidfile]
If no arguments are given, the script looks for the haproxy binary three
directories up from the current working directory. The backend origin server
must be running on 127.0.0.1:8000.

149
addons/otel/test/README-ctx Normal file
View file

@ -0,0 +1,149 @@
Context propagation test configuration (ctx/)
=============================================
The 'ctx' test is a standalone configuration that uses inject/extract context
propagation on every scope. Spans are opened using extracted span contexts
stored in HAProxy variables as parent references instead of direct span names.
This adds the overhead of context serialization, variable storage and
deserialization on every scope execution.
The event coverage matches the 'sa' configuration. The key difference is the
propagation mechanism: each scope injects its context into a numbered variable
(otel_ctx_1 through otel_ctx_17) and the next scope extracts from that variable
to establish the parent relationship.
The client_session_start event is split into two scopes (client_session_start_1
and client_session_start_2) to demonstrate inject/extract between scopes
handling the same event.
Files
-----
ctx/otel.cfg OTel filter configuration (scopes, groups, contexts)
ctx/haproxy.cfg HAProxy frontend/backend configuration
ctx/otel.yml Exporter, processor, reader and provider definitions
run-ctx.sh Convenience script to launch HAProxy with this config
Events
------
T = Trace (span)
This configuration produces traces only -- no metrics or log-records.
Stream lifecycle events:
Event Scope T
----------------------------------------------------------
on-client-session-start client_session_start_1 x
on-client-session-start client_session_start_2 x
Request analyzer events:
Event Scope T
--------------------------------------------------------------------
on-frontend-tcp-request frontend_tcp_request x
on-http-wait-request http_wait_request x
on-http-body-request http_body_request x
on-frontend-http-request frontend_http_request x
on-switching-rules-request switching_rules_request x
on-backend-tcp-request backend_tcp_request x
on-backend-http-request backend_http_request x
on-process-server-rules-request process_server_rules_request x
on-http-process-request http_process_request x
on-tcp-rdp-cookie-request tcp_rdp_cookie_request x
on-process-sticking-rules-request process_sticking_rules_request x
on-client-session-end client_session_end -
on-server-unavailable server_unavailable -
Response analyzer events:
Event Scope T
--------------------------------------------------------------------
on-server-session-start server_session_start x
on-tcp-response tcp_response x
on-http-wait-response http_wait_response x
on-process-store-rules-response process_store_rules_response x
on-http-response http_response x
on-http-response http_response-error x (conditional)
on-server-session-end server_session_end -
The http_response_group (http_response_1, http_response_2) and
http_after_response_group (http_after_response) are invoked via http-response
and http-after-response directives in haproxy.cfg.
Context propagation chain
-------------------------
Each scope injects its span context into a HAProxy variable and the next scope
extracts it. The variable names and their flow:
otel_ctx_1 "HAProxy session" -> client_session_start_2
otel_ctx_2 "Client session" -> frontend_tcp_request
otel_ctx_3 "Frontend TCP request" -> http_wait_request
otel_ctx_4 "HTTP wait request" -> http_body_request
otel_ctx_5 "HTTP body request" -> frontend_http_request
otel_ctx_6 "Frontend HTTP request" -> switching_rules_request
otel_ctx_7 "Switching rules request" -> backend_tcp_request
otel_ctx_8 "Backend TCP request" -> backend_http_request
otel_ctx_9 "Backend HTTP request" -> process_server_rules_request
otel_ctx_10 "Process server rules request" -> http_process_request
otel_ctx_11 "HTTP process request" -> tcp_rdp_cookie_request
otel_ctx_12 "TCP RDP cookie request" -> process_sticking_rules_request
otel_ctx_13 "Process sticking rules req." -> server_session_start
otel_ctx_14 "Server session" -> tcp_response
otel_ctx_15 "TCP response" -> http_wait_response
otel_ctx_16 "HTTP wait response" -> process_store_rules_response
otel_ctx_17 "Process store rules response" -> http_response
All contexts use both use-headers and use-vars injection modes, except
otel_ctx_14 and otel_ctx_15 which use use-vars only.
Span hierarchy
--------------
The span hierarchy is identical to the 'sa' configuration, but parent
relationships are established through extracted contexts rather than direct
span name references.
Request path:
"HAProxy session" (root) [otel_ctx_1]
+-- "Client session" [otel_ctx_2]
+-- "Frontend TCP request" [otel_ctx_3]
+-- "HTTP wait request" [otel_ctx_4]
+-- "HTTP body request" [otel_ctx_5]
+-- "Frontend HTTP request" [otel_ctx_6]
+-- "Switching rules request" [otel_ctx_7]
+-- "Backend TCP request" [otel_ctx_8]
+-- (continues to process_sticking_rules_request)
Response path:
"HAProxy session" [otel_ctx_1]
+-- "Server session" [otel_ctx_14]
+-- "TCP response" [otel_ctx_15]
+-- "HTTP wait response" [otel_ctx_16]
+-- "Process store rules response" [otel_ctx_17]
+-- "HTTP response"
Auxiliary spans:
"HAProxy session"
+-- "HAProxy response" (http_after_response_group, on error)
Running the test
----------------
From the test/ directory:
% ./run-ctx.sh [/path/to/haproxy] [pidfile]
If no arguments are given, the script looks for the haproxy binary three
directories up from the current working directory. The backend origin server
must be running on 127.0.0.1:8000.

View file

@ -0,0 +1,53 @@
Empty test configuration (empty/)
=================================
The 'empty' test is a minimal configuration that loads the OTel filter without
defining any scopes, events or groups. The instrumentation block contains only
the config directive pointing to the YAML pipeline definition.
This configuration verifies that the filter initializes and shuts down cleanly
when no telemetry is configured. It exercises the full YAML parsing path
(exporters, processors, readers, samplers, providers and signals) without
producing any trace, metric or log-record data.
Files
-----
empty/otel.cfg OTel filter configuration (instrumentation only)
empty/haproxy.cfg HAProxy frontend/backend configuration
empty/otel.yml Exporter, processor, reader and provider definitions
Events
------
No events are registered. The filter is loaded and attached to the frontend
but performs no per-stream processing.
YAML pipeline
-------------
Despite the empty filter configuration, the otel.yml file defines a complete
pipeline with all three signal types to verify that the YAML parser handles
the full configuration without errors:
Signal Exporter Processor / Reader
-----------------------------------------------------------
traces exporter_traces_otlp_http processor_traces_batch
metrics exporter_metrics_otlp_http reader_metrics
logs exporter_logs_otlp_http processor_logs_batch
Additional exporter definitions (otlp_file, otlp_grpc, ostream, memory,
zipkin, elasticsearch) are present in the YAML but are not wired into the
active signal pipelines.
Running the test
----------------
There is no dedicated run script for the empty configuration. To run it
manually from the test/ directory:
% /path/to/haproxy -f haproxy-common.cfg -f empty/haproxy.cfg

View file

@ -0,0 +1,124 @@
Frontend / backend test configuration (fe/ + be/)
=================================================
The 'fe-be' test uses two cascaded HAProxy instances to demonstrate
inter-process trace context propagation via HTTP headers. The frontend instance
(fe/) creates the root trace and injects span context into the HTTP request
headers. The backend instance (be/) extracts that context and continues the
trace as a child of the frontend's span.
The two instances run as separate processes: the frontend listens on port 10080
and proxies to the backend on port 11080, which in turn proxies to the origin
server on port 8000.
Files
-----
fe/otel.cfg OTel filter configuration for the frontend instance
fe/haproxy.cfg HAProxy configuration for the frontend instance
be/otel.cfg OTel filter configuration for the backend instance
be/haproxy.cfg HAProxy configuration for the backend instance
run-fe-be.sh Convenience script to launch both instances
Events
------
T = Trace (span)
Both instances produce traces only -- no metrics or log-records.
Frontend (fe/) events:
Event Scope T
--------------------------------------------------------
on-client-session-start client_session_start x
on-frontend-tcp-request frontend_tcp_request x
on-frontend-http-request frontend_http_request x
on-backend-tcp-request backend_tcp_request x
on-backend-http-request backend_http_request x
on-client-session-end client_session_end -
on-server-session-start server_session_start x
on-tcp-response tcp_response x
on-http-response http_response x
on-server-session-end server_session_end -
Backend (be/) events:
Event Scope T
--------------------------------------------------------
on-frontend-http-request frontend_http_request x
on-backend-tcp-request backend_tcp_request x
on-backend-http-request backend_http_request x
on-client-session-end client_session_end -
on-server-session-start server_session_start x
on-tcp-response tcp_response x
on-http-response http_response x
on-server-session-end server_session_end -
The backend starts its trace at on-frontend-http-request where it extracts
the span context injected by the frontend. Earlier request events
(on-client-session-start, on-frontend-tcp-request) are not needed because
the context is not yet available in the HTTP headers at that point.
Context propagation
-------------------
The frontend injects context into HTTP headers in the backend_http_request
scope:
span "HAProxy session"
inject "otel-ctx" use-headers
The backend extracts that context in its frontend_http_request scope:
extract "otel-ctx" use-headers
span "HAProxy session" parent "otel-ctx" root
Span hierarchy
--------------
Frontend (fe/):
"HAProxy session" (root)
+-- "Client session"
+-- "Frontend TCP request"
+-- "Frontend HTTP request"
+-- "Backend TCP request"
+-- "Backend HTTP request"
"HAProxy session" (root)
+-- "Server session"
+-- "TCP response"
+-- "HTTP response"
Backend (be/):
"HAProxy session" (root, parent: frontend's "HAProxy session")
+-- "Client session"
+-- "Frontend HTTP request"
+-- "Backend TCP request"
+-- "Backend HTTP request"
"HAProxy session" (root)
+-- "Server session"
+-- "TCP response"
+-- "HTTP response"
Running the test
----------------
From the test/ directory:
% ./run-fe-be.sh [/path/to/haproxy] [pidfile]
If no arguments are given, the script looks for the haproxy binary three
directories up from the current working directory. The backend origin server
must be running on 127.0.0.1:8000.
The script launches both HAProxy instances in the background and waits.
Press CTRL-C to stop both instances.

View file

@ -0,0 +1,158 @@
Full event coverage test configuration (full/)
==============================================
The 'full' test is a standalone single-instance configuration that exercises
every supported OTel filter event with all three signal types: traces (spans),
metrics (instruments) and logs (log-records).
It extends the 'sa' (standalone) configuration by adding the events that 'sa'
does not cover and by attaching log-records to every scope.
Files
-----
full/otel.cfg OTel filter configuration (scopes, groups, instruments)
full/haproxy.cfg HAProxy frontend/backend configuration
full/otel.yml Exporter, processor, reader and provider definitions
run-full.sh Convenience script to launch HAProxy with this config
Events
------
The table below lists every event defined in include/event.h together with the
scope that handles it and the signals produced by that scope.
T = Trace (span) M = Metric (instrument) L = Log (log-record)
Stream lifecycle events:
Event Scope T M L
---------------------------------------------------------------
on-stream-start on_stream_start x x x
on-stream-stop on_stream_stop - - x
on-idle-timeout on_idle_timeout x x x
on-backend-set on_backend_set x x x
Request analyzer events:
Event Scope T M L
--------------------------------------------------------------------------
on-client-session-start client_session_start x x x
on-frontend-tcp-request frontend_tcp_request x x x
on-http-wait-request http_wait_request x - x
on-http-body-request http_body_request x - x
on-frontend-http-request frontend_http_request x x x
on-switching-rules-request switching_rules_request x - x
on-backend-tcp-request backend_tcp_request x x x
on-backend-http-request backend_http_request x - x
on-process-server-rules-request process_server_rules_request x - x
on-http-process-request http_process_request x - x
on-tcp-rdp-cookie-request tcp_rdp_cookie_request x - x
on-process-sticking-rules-request process_sticking_rules_request x - x
on-http-headers-request http_headers_request x x x
on-http-end-request http_end_request x x x
on-client-session-end client_session_end - x x
on-server-unavailable server_unavailable - - x
Response analyzer events:
Event Scope T M L
--------------------------------------------------------------------------
on-server-session-start server_session_start x x x
on-tcp-response tcp_response x x x
on-http-wait-response http_wait_response x - x
on-process-store-rules-response process_store_rules_response x - x
on-http-response http_response x x x
on-http-headers-response http_headers_response x x x
on-http-end-response http_end_response x x x
on-http-reply http_reply x x x
on-server-session-end server_session_end - x x
Additionally, the http_response-error scope fires conditionally on the
on-http-response event when the response status is outside the 100-399 range,
setting an error status on the "HTTP response" span.
The http_response_group (http_response_1, http_response_2) and
http_after_response_group (http_after_response) are invoked via http-response
and http-after-response directives in haproxy.cfg.
Instruments
-----------
Every instrument definition has at least one corresponding update.
Instrument name Type Defined in Updated in
-------------------------------------------------------------------------------
haproxy.sessions.active udcnt_int on_stream_start client_session_end
haproxy.fe.connections gauge_int on_stream_start http_response
idle.count cnt_int on_idle_timeout on_idle_timeout
haproxy.backend.set cnt_int on_backend_set on_backend_set
haproxy.client.session.start cnt_int client_session_start client_session_end
haproxy.tcp.request.fe cnt_int frontend_tcp_request frontend_http_request
haproxy.http.requests cnt_int frontend_http_request http_response
haproxy.http.latency hist_int frontend_http_request frontend_http_request,
http_response
haproxy.tcp.request.be cnt_int backend_tcp_request backend_http_request
haproxy.http.headers.request cnt_int http_headers_request http_end_request
haproxy.http.end.request cnt_int http_end_request client_session_end
haproxy.server.session.start cnt_int server_session_start server_session_end
haproxy.tcp.response cnt_int tcp_response http_wait_response
haproxy.http.headers.response cnt_int http_headers_response http_end_response
haproxy.http.end.response cnt_int http_end_response http_reply
haproxy.http.reply cnt_int http_reply server_session_end
Span hierarchy
--------------
Request path:
"HAProxy session" (root)
+-- "Client session"
+-- "Frontend TCP request"
+-- "HTTP wait request"
+-- "HTTP body request"
+-- "Frontend HTTP request" [link: "HAProxy session"]
+-- "Switching rules request"
+-- "Backend TCP request"
+-- "Backend HTTP request"
+-- "Process server rules request"
+-- "HTTP process request"
+-- "TCP RDP cookie request"
+-- "Process sticking rules request"
+-- "HTTP headers request"
+-- "HTTP end request"
Response path:
"HAProxy session" (root)
+-- "Server session" [link: "HAProxy session", "Client session"]
+-- "TCP response"
+-- "HTTP wait response"
+-- "Process store rules response"
+-- "HTTP response"
+-- "HTTP headers response"
+-- "HTTP end response"
+-- "HTTP reply"
Auxiliary spans:
"HAProxy session"
+-- "Backend set"
+-- "heartbeat" (on-idle-timeout, periodic)
+-- "HAProxy response" (http_after_response_group, on error)
Running the test
----------------
From the test/ directory:
% ./run-full.sh [/path/to/haproxy] [pidfile]
If no arguments are given, the script looks for the haproxy binary three
directories up from the current working directory. The backend origin server
must be running on 127.0.0.1:8000.

134
addons/otel/test/README-sa Normal file
View file

@ -0,0 +1,134 @@
Standalone test configuration (sa/)
=====================================
The 'sa' test is a standalone single-instance configuration that
exercises most HAProxy filter events with spans, attributes, events,
links, baggage, status, metrics, logs and groups. It represents the
most comprehensive single-instance configuration and is used as the
worst-case scenario in speed tests.
Six events are not covered by this configuration: on-backend-set,
on-http-headers-request, on-http-end-request, on-http-headers-response,
on-http-end-response and on-http-reply. The 'full' configuration
extends 'sa' with those events.
Files
------
sa/otel.cfg OTel filter configuration (scopes, groups, instruments)
sa/haproxy.cfg HAProxy frontend/backend configuration
sa/otel.yml Exporter, processor, reader and provider definitions
run-sa.sh Convenience script to launch HAProxy with this config
Events
-------
T = Trace (span) M = Metric (instrument) L = Log (log-record)
Stream lifecycle events:
Event Scope T M L
---------------------------------------------------------------
on-stream-start on_stream_start x x x
on-stream-stop on_stream_stop - - -
on-idle-timeout on_idle_timeout x x x
Request analyzer events:
Event Scope T M L
--------------------------------------------------------------------------
on-client-session-start client_session_start x - -
on-frontend-tcp-request frontend_tcp_request x - -
on-http-wait-request http_wait_request x - -
on-http-body-request http_body_request x - -
on-frontend-http-request frontend_http_request x x x
on-switching-rules-request switching_rules_request x - -
on-backend-tcp-request backend_tcp_request x - -
on-backend-http-request backend_http_request x - -
on-process-server-rules-request process_server_rules_request x - -
on-http-process-request http_process_request x - -
on-tcp-rdp-cookie-request tcp_rdp_cookie_request x - -
on-process-sticking-rules-request process_sticking_rules_request x - -
on-client-session-end client_session_end - x -
on-server-unavailable server_unavailable - - -
Response analyzer events:
Event Scope T M L
--------------------------------------------------------------------------
on-server-session-start server_session_start x - -
on-tcp-response tcp_response x - -
on-http-wait-response http_wait_response x - -
on-process-store-rules-response process_store_rules_response x - -
on-http-response http_response x x -
on-server-session-end server_session_end - - -
Additionally, the http_response-error scope fires conditionally on the
on-http-response event when the response status is outside the 100-399
range, setting an error status on the "HTTP response" span.
The http_response_group (http_response_1, http_response_2) and
http_after_response_group (http_after_response) are invoked via
http-response and http-after-response directives in haproxy.cfg.
Instruments
------------
Instrument name Type Defined in Updated in
--------------------------------------------------------------------------
haproxy.sessions.active udcnt_int on_stream_start client_session_end
haproxy.fe.connections gauge_int on_stream_start http_response
idle.count cnt_int on_idle_timeout on_idle_timeout
haproxy.http.requests cnt_int frontend_http_request http_response
haproxy.http.latency hist_int frontend_http_request frontend_http_request,
http_response
Span hierarchy
---------------
Request path:
"HAProxy session" (root)
+-- "Client session"
+-- "Frontend TCP request"
+-- "HTTP wait request"
+-- "HTTP body request"
+-- "Frontend HTTP request" [link: "HAProxy session"]
+-- "Switching rules request"
+-- "Backend TCP request"
+-- "Backend HTTP request"
+-- "Process server rules request"
+-- "HTTP process request"
+-- "TCP RDP cookie request"
+-- "Process sticking rules request"
Response path:
"HAProxy session" (root)
+-- "Server session" [link: "HAProxy session", "Client session"]
+-- "TCP response"
+-- "HTTP wait response"
+-- "Process store rules response"
+-- "HTTP response"
Auxiliary spans:
"HAProxy session"
+-- "heartbeat" (on-idle-timeout, periodic)
+-- "HAProxy response" (http_after_response_group, on error)
Running the test
-----------------
From the test/ directory:
% ./run-sa.sh [/path/to/haproxy] [pidfile]
If no arguments are given, the script looks for the haproxy binary three
directories up from the current working directory. The backend origin
server must be running on 127.0.0.1:8000.

View file

@ -0,0 +1,144 @@
--- rate-limit 100.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 182.58us 129.11us 16.19ms 98.11%
Req/Sec 5.63k 240.83 6.29k 69.74%
Latency Distribution
50% 169.00us
75% 183.00us
90% 209.00us
99% 367.00us
13438310 requests in 5.00m, 3.24GB read
Requests/sec: 44779.51
Transfer/sec: 11.06MB
----------------------------------------------------------------------
--- rate-limit 75.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 180.10us 122.51us 14.57ms 98.00%
Req/Sec 5.70k 253.08 6.28k 70.41%
Latency Distribution
50% 169.00us
75% 184.00us
90% 206.00us
99% 362.00us
13613023 requests in 5.00m, 3.28GB read
Requests/sec: 45361.63
Transfer/sec: 11.20MB
----------------------------------------------------------------------
--- rate-limit 50.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 176.59us 125.34us 15.58ms 98.02%
Req/Sec 5.81k 230.84 6.42k 72.14%
Latency Distribution
50% 166.00us
75% 182.00us
90% 202.00us
99% 361.00us
13888448 requests in 5.00m, 3.35GB read
Requests/sec: 46279.45
Transfer/sec: 11.43MB
----------------------------------------------------------------------
--- rate-limit 25.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 173.57us 118.35us 13.12ms 97.91%
Req/Sec 5.91k 257.69 6.46k 66.83%
Latency Distribution
50% 162.00us
75% 178.00us
90% 199.00us
99% 362.00us
14122906 requests in 5.00m, 3.41GB read
Requests/sec: 47060.71
Transfer/sec: 11.62MB
----------------------------------------------------------------------
--- rate-limit 10.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 170.85us 112.24us 10.72ms 97.84%
Req/Sec 6.00k 269.81 8.10k 69.46%
Latency Distribution
50% 159.00us
75% 172.00us
90% 194.00us
99% 361.00us
14342642 requests in 5.00m, 3.46GB read
Requests/sec: 47792.96
Transfer/sec: 11.80MB
----------------------------------------------------------------------
--- rate-limit 2.5 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 169.11us 127.52us 16.90ms 98.06%
Req/Sec 6.08k 261.57 6.55k 67.30%
Latency Distribution
50% 158.00us
75% 168.00us
90% 186.00us
99% 367.00us
14527714 requests in 5.00m, 3.50GB read
Requests/sec: 48409.62
Transfer/sec: 11.96MB
----------------------------------------------------------------------
--- rate-limit 0.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 168.09us 108.96us 9.07ms 97.81%
Req/Sec 6.10k 284.22 6.55k 70.65%
Latency Distribution
50% 157.00us
75% 167.00us
90% 184.00us
99% 362.00us
14580762 requests in 5.00m, 3.52GB read
Requests/sec: 48586.42
Transfer/sec: 12.00MB
----------------------------------------------------------------------
--- rate-limit disabled --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 168.57us 118.05us 13.51ms 97.94%
Req/Sec 6.09k 251.47 6.99k 67.33%
Latency Distribution
50% 158.00us
75% 167.00us
90% 184.00us
99% 363.00us
14557824 requests in 5.00m, 3.51GB read
Requests/sec: 48509.96
Transfer/sec: 11.98MB
----------------------------------------------------------------------
--- rate-limit off --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 168.64us 120.15us 14.49ms 98.01%
Req/Sec 6.09k 267.94 6.57k 66.19%
Latency Distribution
50% 158.00us
75% 167.00us
90% 184.00us
99% 361.00us
14551312 requests in 5.00m, 3.51GB read
Requests/sec: 48488.23
Transfer/sec: 11.98MB
----------------------------------------------------------------------

View file

@ -0,0 +1,144 @@
--- rate-limit 100.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 270.35us 136.92us 16.02ms 97.98%
Req/Sec 3.77k 217.57 5.33k 67.74%
Latency Distribution
50% 264.00us
75% 287.00us
90% 309.00us
99% 494.00us
9012538 requests in 5.00m, 2.17GB read
Requests/sec: 30031.85
Transfer/sec: 7.42MB
----------------------------------------------------------------------
--- rate-limit 75.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 242.56us 131.47us 9.82ms 94.77%
Req/Sec 4.21k 218.42 5.61k 68.12%
Latency Distribution
50% 246.00us
75% 279.00us
90% 308.00us
99% 464.00us
10050409 requests in 5.00m, 2.42GB read
Requests/sec: 33490.26
Transfer/sec: 8.27MB
----------------------------------------------------------------------
--- rate-limit 50.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 215.92us 130.82us 9.93ms 96.81%
Req/Sec 4.73k 243.84 7.13k 67.13%
Latency Distribution
50% 208.00us
75% 264.00us
90% 300.00us
99% 439.00us
11307386 requests in 5.00m, 2.73GB read
Requests/sec: 37678.82
Transfer/sec: 9.31MB
----------------------------------------------------------------------
--- rate-limit 25.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 192.36us 132.47us 13.75ms 96.79%
Req/Sec 5.33k 260.46 6.17k 66.30%
Latency Distribution
50% 166.00us
75% 227.00us
90% 280.00us
99% 407.00us
12734770 requests in 5.00m, 3.07GB read
Requests/sec: 42448.91
Transfer/sec: 10.48MB
----------------------------------------------------------------------
--- rate-limit 10.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 180.08us 127.35us 13.34ms 97.06%
Req/Sec 5.71k 272.98 6.40k 67.94%
Latency Distribution
50% 161.00us
75% 183.00us
90% 250.00us
99% 386.00us
13641901 requests in 5.00m, 3.29GB read
Requests/sec: 45457.92
Transfer/sec: 11.23MB
----------------------------------------------------------------------
--- rate-limit 2.5 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 171.64us 107.08us 5.69ms 96.57%
Req/Sec 5.97k 289.99 6.55k 68.53%
Latency Distribution
50% 159.00us
75% 171.00us
90% 195.00us
99% 372.00us
14268464 requests in 5.00m, 3.44GB read
Requests/sec: 47545.77
Transfer/sec: 11.74MB
----------------------------------------------------------------------
--- rate-limit 0.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 168.86us 104.53us 5.75ms 97.73%
Req/Sec 6.07k 282.19 6.59k 67.47%
Latency Distribution
50% 158.00us
75% 168.00us
90% 186.00us
99% 361.00us
14498699 requests in 5.00m, 3.50GB read
Requests/sec: 48312.96
Transfer/sec: 11.93MB
----------------------------------------------------------------------
--- rate-limit disabled --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 168.36us 129.52us 17.68ms 98.13%
Req/Sec 6.11k 263.70 6.83k 70.42%
Latency Distribution
50% 157.00us
75% 167.00us
90% 183.00us
99% 363.00us
14590953 requests in 5.00m, 3.52GB read
Requests/sec: 48620.36
Transfer/sec: 12.01MB
----------------------------------------------------------------------
--- rate-limit off --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 168.73us 120.51us 15.03ms 98.02%
Req/Sec 6.09k 270.88 6.55k 68.99%
Latency Distribution
50% 158.00us
75% 167.00us
90% 185.00us
99% 360.00us
14538507 requests in 5.00m, 3.51GB read
Requests/sec: 48445.53
Transfer/sec: 11.97MB
----------------------------------------------------------------------

View file

@ -0,0 +1,144 @@
--- rate-limit 100.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 238.75us 129.45us 15.59ms 98.45%
Req/Sec 4.27k 75.22 5.28k 77.45%
Latency Distribution
50% 228.00us
75% 243.00us
90% 262.00us
99% 410.00us
10206938 requests in 5.00m, 2.46GB read
Requests/sec: 34011.80
Transfer/sec: 8.40MB
----------------------------------------------------------------------
--- rate-limit 75.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 230.08us 201.50us 32.12ms 99.25%
Req/Sec 4.46k 83.43 5.36k 75.61%
Latency Distribution
50% 222.00us
75% 241.00us
90% 261.00us
99% 401.00us
10641998 requests in 5.00m, 2.57GB read
Requests/sec: 35461.59
Transfer/sec: 8.76MB
----------------------------------------------------------------------
--- rate-limit 50.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 222.33us 242.52us 35.13ms 99.43%
Req/Sec 4.62k 99.86 5.78k 79.00%
Latency Distribution
50% 211.00us
75% 237.00us
90% 259.00us
99% 400.00us
11046951 requests in 5.00m, 2.66GB read
Requests/sec: 36810.91
Transfer/sec: 9.09MB
----------------------------------------------------------------------
--- rate-limit 25.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 210.95us 117.20us 10.57ms 97.85%
Req/Sec 4.84k 101.85 6.04k 68.10%
Latency Distribution
50% 198.00us
75% 222.00us
90% 252.00us
99% 394.00us
11551741 requests in 5.00m, 2.79GB read
Requests/sec: 38493.03
Transfer/sec: 9.51MB
----------------------------------------------------------------------
--- rate-limit 10.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 206.15us 209.34us 31.73ms 99.27%
Req/Sec 4.99k 112.62 5.36k 71.21%
Latency Distribution
50% 193.00us
75% 210.00us
90% 237.00us
99% 387.00us
11924489 requests in 5.00m, 2.88GB read
Requests/sec: 39735.09
Transfer/sec: 9.81MB
----------------------------------------------------------------------
--- rate-limit 2.5 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 202.35us 180.56us 27.28ms 99.10%
Req/Sec 5.08k 145.06 8.27k 71.24%
Latency Distribution
50% 191.00us
75% 205.00us
90% 223.00us
99% 374.00us
12131047 requests in 5.00m, 2.93GB read
Requests/sec: 40423.43
Transfer/sec: 9.98MB
----------------------------------------------------------------------
--- rate-limit 0.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 200.88us 223.19us 32.70ms 99.44%
Req/Sec 5.13k 151.03 6.55k 69.46%
Latency Distribution
50% 190.00us
75% 203.00us
90% 218.00us
99% 367.00us
12256706 requests in 5.00m, 2.96GB read
Requests/sec: 40842.16
Transfer/sec: 10.09MB
----------------------------------------------------------------------
--- rate-limit disabled --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 194.91us 222.55us 33.80ms 99.47%
Req/Sec 5.29k 167.52 5.95k 68.32%
Latency Distribution
50% 184.00us
75% 197.00us
90% 214.00us
99% 353.00us
12633928 requests in 5.00m, 3.05GB read
Requests/sec: 42112.54
Transfer/sec: 10.40MB
----------------------------------------------------------------------
--- rate-limit off --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 194.37us 166.76us 28.75ms 99.09%
Req/Sec 5.28k 160.02 5.86k 68.02%
Latency Distribution
50% 184.00us
75% 197.00us
90% 214.00us
99% 355.00us
12622896 requests in 5.00m, 3.04GB read
Requests/sec: 42062.31
Transfer/sec: 10.39MB
----------------------------------------------------------------------

View file

@ -0,0 +1,144 @@
--- rate-limit 100.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 213.08us 136.99us 17.58ms 98.10%
Req/Sec 4.80k 251.04 5.97k 68.01%
Latency Distribution
50% 203.00us
75% 223.00us
90% 245.00us
99% 405.00us
11464278 requests in 5.00m, 2.77GB read
Requests/sec: 38201.61
Transfer/sec: 9.44MB
----------------------------------------------------------------------
--- rate-limit 75.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 202.18us 121.42us 12.04ms 97.72%
Req/Sec 5.05k 248.48 5.85k 65.69%
Latency Distribution
50% 194.00us
75% 219.00us
90% 245.00us
99% 393.00us
12071015 requests in 5.00m, 2.91GB read
Requests/sec: 40223.31
Transfer/sec: 9.94MB
----------------------------------------------------------------------
--- rate-limit 50.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 190.49us 117.32us 7.33ms 97.62%
Req/Sec 5.37k 265.60 6.98k 65.98%
Latency Distribution
50% 181.00us
75% 208.00us
90% 237.00us
99% 383.00us
12837427 requests in 5.00m, 3.10GB read
Requests/sec: 42777.17
Transfer/sec: 10.57MB
----------------------------------------------------------------------
--- rate-limit 25.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 180.46us 123.40us 13.20ms 97.80%
Req/Sec 5.69k 242.93 7.75k 68.66%
Latency Distribution
50% 165.00us
75% 194.00us
90% 223.00us
99% 375.00us
13595213 requests in 5.00m, 3.28GB read
Requests/sec: 45302.34
Transfer/sec: 11.19MB
----------------------------------------------------------------------
--- rate-limit 10.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 174.69us 129.17us 16.50ms 97.93%
Req/Sec 5.89k 260.40 7.03k 69.57%
Latency Distribution
50% 160.00us
75% 178.00us
90% 210.00us
99% 374.00us
14068388 requests in 5.00m, 3.39GB read
Requests/sec: 46879.07
Transfer/sec: 11.58MB
----------------------------------------------------------------------
--- rate-limit 2.5 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 170.58us 116.89us 11.49ms 97.74%
Req/Sec 6.03k 249.35 6.54k 67.44%
Latency Distribution
50% 158.00us
75% 170.00us
90% 192.00us
99% 375.00us
14402604 requests in 5.00m, 3.47GB read
Requests/sec: 47992.71
Transfer/sec: 11.85MB
----------------------------------------------------------------------
--- rate-limit 0.0 --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 167.96us 114.36us 10.99ms 97.81%
Req/Sec 6.12k 266.16 6.57k 70.70%
Latency Distribution
50% 157.00us
75% 166.00us
90% 183.00us
99% 370.00us
14622790 requests in 5.00m, 3.53GB read
Requests/sec: 48726.40
Transfer/sec: 12.04MB
----------------------------------------------------------------------
--- rate-limit disabled --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 167.74us 114.11us 11.10ms 97.82%
Req/Sec 6.13k 251.71 6.57k 69.59%
Latency Distribution
50% 157.00us
75% 166.00us
90% 182.00us
99% 368.00us
14641307 requests in 5.00m, 3.53GB read
Requests/sec: 48788.18
Transfer/sec: 12.05MB
----------------------------------------------------------------------
--- rate-limit off --------------------------------------------------
Running 5m test @ http://localhost:10080/index.html
8 threads and 8 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 168.00us 112.83us 11.07ms 97.79%
Req/Sec 6.12k 264.12 7.23k 68.95%
Latency Distribution
50% 157.00us
75% 166.00us
90% 183.00us
99% 369.00us
14613970 requests in 5.00m, 3.53GB read
Requests/sec: 48697.01
Transfer/sec: 12.03MB
----------------------------------------------------------------------

View file

@ -0,0 +1,319 @@
-----------------------------------------
HAProxy OTEL filter speed test guide
Version 1.0
( Last update: 2026-04-09 )
-----------------------------------------
Author : Miroslav Zagorac
Contact : mzagorac at haproxy dot com
SUMMARY
--------
1. Overview
2. Prerequisites
3. Running the test
4. Test parameters
5. Rate-limit levels
6. Test configurations
7. Results
7.1. Standalone (sa)
7.2. Comparison (cmp)
7.3. Context propagation (ctx)
7.4. Frontend / backend (fe-be)
8. Summary
1. Overview
------------
The test-speed.sh script measures the performance impact of the OTEL filter on
HAProxy at various rate-limit settings. For each test configuration, the script
iterates through a series of rate-limit values -- from full tracing (100%) down
to the filter being completely removed -- measuring throughput and latency at
each level.
The script uses template files (haproxy.cfg.in and otel.cfg.in) from each test
directory to generate the actual configuration files. A sed substitution
adjusts the rate-limit value (or disables/removes the filter) before each run.
2. Prerequisites
-----------------
The following tools must be installed and available in PATH:
- thttpd : a lightweight HTTP server used as the backend origin server.
It serves a small static HTML file (index.html) on port 8000.
- wrk : an HTTP benchmarking tool that generates the test load.
See https://github.com/wg/wrk
3. Running the test
--------------------
The test is executed from the test directory. It can be run for all
configurations at once or for a single configuration.
To run all configurations:
% ./test-speed.sh all
This produces result files in the _logs directory:
_logs/README-speed-fe-be
_logs/README-speed-sa
_logs/README-speed-cmp
_logs/README-speed-ctx
To run a single configuration:
% ./test-speed.sh <cfg> [<dir>]
Where <cfg> corresponds to a run-<cfg>.sh script and <dir> is the configuration
directory (defaults to <cfg>). For example:
% ./test-speed.sh sa
% ./test-speed.sh fe-be fe
% ./test-speed.sh cmp
% ./test-speed.sh ctx
4. Test parameters
-------------------
The wrk benchmarking tool is invoked with the following parameters:
-t8 8 threads
-c8 8 concurrent connections
-d300 5-minute test duration (300 seconds)
--latency latency distribution reporting
Each rate-limit level is tested sequentially. Between runs, HAProxy is stopped
via SIGUSR1 and restarted with the next rate-limit configuration. A 10-second
pause separates consecutive runs.
The backend origin server (thttpd) serves a small static HTML page (index.html,
approximately 50 bytes) on port 8000. HAProxy listens on port 10080 and proxies
requests to the origin.
5. Rate-limit levels
---------------------
The script tests nine rate-limit levels in the following order:
100.0 - the filter processes every stream (worst case)
75.0 - the filter processes 75% of streams
50.0 - the filter processes 50% of streams
25.0 - the filter processes 25% of streams
10.0 - the filter processes 10% of streams
2.5 - the filter processes 2.5% of streams
0.0 - the filter is loaded and attached to every stream but never
processes any telemetry (the rate-limit check always fails);
this measures the per-stream attach and detach overhead
disabled - the filter is loaded but disabled via 'option disabled'; it is not
attached to streams at all; this measures the cost of loading and
initializing the filter library without any per-stream work
off - the 'filter opentelemetry' and 'otel-group' directives are
commented out of haproxy.cfg; the filter is not loaded and has zero
presence in the processing path; this is the absolute baseline
In the result tables, the 'overhead' column is the throughput loss relative to
the 'off' baseline, expressed as a percentage:
overhead = (req/s_off - req/s_test) / req/s_off * 100
6. Test configurations
-----------------------
Four OTEL filter configurations are tested. They differ in complexity and in
the features they exercise:
sa - Standalone. Uses all possible HAProxy filter events with spans,
attributes, events, links, baggage, status, metrics and groups.
This is the most comprehensive single-instance configuration and
represents the worst-case scenario.
cmp - Comparison. A simplified configuration made for comparison with
other tracing implementations. It uses a reduced span hierarchy
without context propagation, groups or metrics. This is closer to
a typical production deployment.
ctx - Context propagation. Similar to 'sa' in scope coverage, but spans
are opened using extracted span contexts (inject/extract via HAProxy
variables) as parent references instead of direct span names. This
adds the overhead of context serialization, variable storage and
deserialization on every scope execution.
fe-be - Frontend / backend. Two cascaded HAProxy instances: the frontend
(fe) creates the root trace and injects span context into HTTP
headers; the backend (be) extracts the context and continues the
trace. This configuration measures the combined overhead of two
OTEL filter instances plus the inter-process context propagation
cost.
Note: the rate-limit is varied only on the frontend. The backend
always runs with its default configuration (filter enabled,
hard-errors on). The backend configuration is only modified for
the 'disabled' and 'off' levels.
7. Results
-----------
The tables below summarize the benchmarking results. The 'req/s' column shows
the sustained request rate reported by wrk, 'avg latency' is the average
response time, and 'overhead' is the throughput loss relative to the 'off'
baseline.
7.1. Standalone (sa)
---------------------
---------------------------------------------------------------
rate-limit req/s avg latency overhead
---------------------------------------------------------------
100.0% 38,202 213.08 us 21.6%
75.0% 40,223 202.18 us 17.4%
50.0% 42,777 190.49 us 12.2%
25.0% 45,302 180.46 us 7.0%
10.0% 46,879 174.69 us 3.7%
2.5% 47,993 170.58 us 1.4%
0.0% 48,726 167.96 us ~0
disabled 48,788 167.74 us ~0
off 48,697 168.00 us baseline
---------------------------------------------------------------
With all possible events active, the sa configuration at 100% rate-limit shows
a 22% throughput reduction. At 10% rate-limit the overhead drops to 3.7%, and
at 2.5% it is barely measurable at 1.4%. The 'disabled' and '0.0' levels show
no measurable overhead above the baseline.
7.2. Comparison (cmp)
----------------------
---------------------------------------------------------------
rate-limit req/s avg latency overhead
---------------------------------------------------------------
100.0% 44,780 182.58 us 7.6%
75.0% 45,362 180.10 us 6.4%
50.0% 46,279 176.59 us 4.6%
25.0% 47,061 173.57 us 2.9%
10.0% 47,793 170.85 us 1.4%
2.5% 48,410 169.11 us 0.2%
0.0% 48,586 168.09 us ~0
disabled 48,510 168.57 us ~0
off 48,488 168.64 us baseline
---------------------------------------------------------------
The simplified cmp configuration shows significantly lower overhead than sa at
every rate-limit level. At 100% rate-limit the overhead is 7.6%, at 10% it is
1.4%, and below 2.5% it becomes indistinguishable from the baseline.
7.3. Context propagation (ctx)
-------------------------------
---------------------------------------------------------------
rate-limit req/s avg latency overhead
---------------------------------------------------------------
100.0% 30,032 270.35 us 38.0%
75.0% 33,490 242.56 us 30.9%
50.0% 37,679 215.92 us 22.2%
25.0% 42,449 192.36 us 12.4%
10.0% 45,458 180.08 us 6.2%
2.5% 47,546 171.64 us 1.9%
0.0% 48,313 168.86 us 0.3%
disabled 48,620 168.36 us ~0
off 48,446 168.73 us baseline
---------------------------------------------------------------
The ctx configuration is the most expensive due to the inject/extract cycle on
every scope execution. At 100% rate-limit the overhead reaches 38%. However,
the cost scales linearly with the rate-limit: at 10% the overhead is 6.2%, and
at 2.5% it drops to 1.9%. The filter attachment overhead (0.0% vs off) is
negligible at 0.3%.
7.4. Frontend / backend (fe-be)
--------------------------------
---------------------------------------------------------------
rate-limit req/s avg latency overhead
---------------------------------------------------------------
100.0% 34,012 238.75 us 19.1%
75.0% 35,462 230.08 us 15.7%
50.0% 36,811 222.33 us 12.5%
25.0% 38,493 210.95 us 8.5%
10.0% 39,735 206.15 us 5.5%
2.5% 40,423 202.35 us 3.9%
0.0% 40,842 200.88 us 2.9%
disabled 42,113 194.91 us ~0
off 42,062 194.37 us baseline
---------------------------------------------------------------
The fe-be configuration involves two HAProxy instances in series, so the
absolute baseline (off) is already lower at 42,062 req/s due to the extra
network hop. The rate-limit is varied only on the frontend; the backend
always has the filter loaded with hard-errors enabled.
This explains the 2.9% overhead at rate-limit 0.0: even though the frontend
never traces, the backend filter still attaches to every stream, attempts to
extract context from the HTTP headers, fails (because the frontend did not
inject any context), and the hard-errors option stops further processing.
This per-stream attach/extract/error cycle accounts for the residual cost.
When both instances have the filter disabled (disabled level), the overhead
is within measurement noise, consistent with the single-instance
configurations.
8. Summary
-----------
The table below shows the overhead for each configuration at selected rate-limit
levels:
---------------------------------------------------
rate-limit sa cmp ctx fe-be
---------------------------------------------------
100.0% 21.6% 7.6% 38.0% 19.1%
25.0% 7.0% 2.9% 12.4% 8.5%
10.0% 3.7% 1.4% 6.2% 5.5%
2.5% 1.4% 0.2% 1.9% 3.9%
---------------------------------------------------
Key observations:
- The overhead scales approximately linearly with the rate-limit value.
Reducing the rate-limit from 100% to 10% eliminates the vast majority
of the cost in all configurations.
- The cmp configuration, which uses a reduced span hierarchy typical of
production deployments, adds only 1.4% overhead at a 10% rate-limit.
- The sa configuration, which exercises all possible events, stays at about
7% overhead at a 25% rate-limit and below 4% at 10%.
- The ctx configuration is the most expensive due to the inject/extract
context propagation on every scope. It is designed as a stress test for
the propagation mechanism rather than a practical production template.
- The fe-be configuration carries a higher fixed cost because two HAProxy
instances are involved and the backend filter processes context extraction
regardless of the frontend rate-limit setting.
- Loading the filter but disabling it via 'option disabled' adds no measurable
overhead in any configuration.
- The filter attachment cost without any telemetry processing (rate-limit 0.0)
is 0.3% or less for single-instance configurations (sa, cmp, ctx).
- In typical production use with a rate-limit of 10% or less, the performance
impact of the OTEL filter should be negligible for single-instance deployments.

View file

@ -0,0 +1,19 @@
global
stats socket /tmp/haproxy-be.sock mode 666 level admin
listen stats
mode http
bind *:8002
stats uri /
stats admin if TRUE
stats refresh 10s
frontend otel-test-be-frontend
bind *:11080
default_backend servers-backend
# OTel filter
filter opentelemetry id otel-test-be config be/otel.cfg
backend servers-backend
server server-1 127.0.0.1:8000

View file

@ -0,0 +1,61 @@
[otel-test-be]
otel-instrumentation otel-test-instrumentation
config be/otel.yml
# log localhost:514 local7 debug
option dontlog-normal
option hard-errors
no option disabled
scopes frontend_http_request
scopes backend_tcp_request
scopes backend_http_request
scopes client_session_end
scopes server_session_start
scopes tcp_response
scopes http_response
scopes server_session_end
otel-scope frontend_http_request
extract "otel-ctx" use-headers
span "HAProxy session" parent "otel-ctx" root
baggage "haproxy_id" var(sess.otel.uuid)
span "Client session" parent "HAProxy session"
span "Frontend HTTP request" parent "Client session"
attribute "http.method" method
attribute "http.url" url
attribute "http.version" str("HTTP/") req.ver
otel-event on-frontend-http-request
otel-scope backend_tcp_request
span "Backend TCP request" parent "Frontend HTTP request"
finish "Frontend HTTP request"
otel-event on-backend-tcp-request
otel-scope backend_http_request
span "Backend HTTP request" parent "Backend TCP request"
finish "Backend TCP request"
otel-event on-backend-http-request
otel-scope client_session_end
finish "Client session"
otel-event on-client-session-end
otel-scope server_session_start
span "Server session" parent "HAProxy session"
finish "Backend HTTP request"
otel-event on-server-session-start
otel-scope tcp_response
span "TCP response" parent "Server session"
otel-event on-tcp-response
otel-scope http_response
span "HTTP response" parent "TCP response"
attribute "http.status_code" status
finish "TCP response"
otel-event on-http-response
otel-scope server_session_end
finish *
otel-event on-server-session-end

View file

@ -0,0 +1,246 @@
exporters:
exporter_traces_otlp_file:
type: otlp_file
thread_name: "OTLP/file trace"
file_pattern: "__be_traces_log-%F-%N"
alias_pattern: "__traces_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_traces_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC trace"
endpoint: "http://localhost:4317/v1/traces"
use_ssl_credentials: false
# ssl_credentials_cacert_path: ""
# ssl_credentials_cacert_as_string: ""
# ssl_client_key_path: ""
# ssl_client_key_string: ""
# ssl_client_cert_path: ""
# ssl_client_cert_string: ""
# timeout: 10
# user_agent: ""
# max_threads: 0
# compression: ""
# max_concurrent_requests: 0
exporter_traces_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP trace"
endpoint: "http://localhost:4318/v1/traces"
content_type: json
json_bytes_mapping: hexid
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP traces test header #1"
- X-OTel-Header-2: "OTLP HTTP traces test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
# ssl_ca_cert_path: ""
# ssl_ca_cert_string: ""
# ssl_client_key_path: ""
# ssl_client_key_string: ""
# ssl_client_cert_path: ""
# ssl_client_cert_string: ""
# ssl_min_tls: ""
# ssl_max_tls: ""
# ssl_cipher: ""
# ssl_cipher_suite: ""
# compression: ""
exporter_traces_dev_null:
type: ostream
filename: /dev/null
exporter_traces_ostream:
type: ostream
filename: __be_traces
exporter_traces_memory:
type: memory
buffer_size: 256
exporter_traces_zipkin:
type: zipkin
endpoint: "http://localhost:9411/api/v2/spans"
format: json
service_name: "zipkin-service"
# ipv4: ""
# ipv6: ""
exporter_metrics_otlp_file:
type: otlp_file
thread_name: "OTLP/file metr"
file_pattern: "__be_metrics_log-%F-%N"
alias_pattern: "__metrics_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_metrics_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC metr"
endpoint: "http://localhost:4317/v1/metrics"
use_ssl_credentials: false
exporter_metrics_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP metr"
endpoint: "http://localhost:4318/v1/metrics"
content_type: json
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP metrics test header #1"
- X-OTel-Header-2: "OTLP HTTP metrics test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
exporter_metrics_dev_null:
type: ostream
filename: /dev/null
exporter_metrics_ostream:
type: ostream
filename: __be_metrics
exporter_metrics_memory:
type: memory
buffer_size: 256
exporter_logs_otlp_file:
type: otlp_file
thread_name: "OTLP/file logs"
file_pattern: "__be_logs_log-%F-%N"
alias_pattern: "__logs_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_logs_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC logs"
endpoint: "http://localhost:4317/v1/logs"
use_ssl_credentials: false
exporter_logs_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP logs"
endpoint: "http://localhost:4318/v1/logs"
content_type: json
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP logs test header #1"
- X-OTel-Header-2: "OTLP HTTP logs test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
exporter_logs_dev_null:
type: ostream
filename: /dev/null
exporter_logs_ostream:
type: ostream
filename: __be_logs
exporter_logs_elasticsearch:
type: elasticsearch
host: localhost
port: 9200
index: logs
response_timeout: 30
debug: false
http_headers:
- X-OTel-Header-1: "Elasticsearch logs test header #1"
- X-OTel-Header-2: "Elasticsearch logs test header #2"
readers:
reader_metrics:
thread_name: "reader metr"
export_interval: 10000
export_timeout: 5000
samplers:
sampler_traces:
# type: always_on
# type: always_off
# type: trace_id_ratio_based
# ratio: 1.0
type: parent_based
delegate: always_on
processors:
processor_traces_batch:
type: batch
thread_name: "proc/batch trac"
# Note: when the queue is half full, a preemptive notification is sent
# to start the export call.
max_queue_size: 2048
# Time interval (in ms) between two consecutive exports
schedule_delay: 5000
# Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
max_export_batch_size: 512
processor_traces_single:
type: single
processor_logs_batch:
type: batch
thread_name: "proc/batch logs"
max_queue_size: 2048
schedule_delay: 5000
max_export_batch_size: 512
processor_logs_single:
type: single
providers:
provider_traces:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-be"
- service.name: "be"
- service.namespace: "HAProxy traces test"
provider_metrics:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-be"
- service.name: "be"
- service.namespace: "HAProxy metrics test"
provider_logs:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-be"
- service.name: "be"
- service.namespace: "HAProxy logs test"
signals:
traces:
scope_name: "HAProxy OTEL - traces"
exporters: exporter_traces_otlp_http
samplers: sampler_traces
processors: processor_traces_batch
providers: provider_traces
metrics:
scope_name: "HAProxy OTEL - metrics"
exporters: exporter_metrics_otlp_http
readers: reader_metrics
providers: provider_metrics
logs:
scope_name: "HAProxy OTEL - logs"
exporters: exporter_logs_otlp_http
processors: processor_logs_batch
providers: provider_logs

View file

@ -0,0 +1,22 @@
global
stats socket /tmp/haproxy.sock mode 666 level admin
listen stats
mode http
bind *:8001
stats uri /
stats admin if TRUE
stats refresh 10s
frontend otel-test-cmp-frontend
bind *:10080
default_backend servers-backend
# ACL used to distinguish successful from error responses
acl acl-http-status-ok status 100:399
# OTel filter
filter opentelemetry id otel-test-cmp config cmp/otel.cfg
backend servers-backend
server server-1 127.0.0.1:8000

View file

@ -0,0 +1,81 @@
[otel-test-cmp]
otel-instrumentation otel-test-instrumentation
config cmp/otel.yml
# log localhost:514 local7 debug
option dontlog-normal
option hard-errors
no option disabled
rate-limit 100.0
scopes client_session_start
scopes frontend_tcp_request
scopes frontend_http_request
scopes backend_tcp_request
scopes backend_http_request
scopes server_unavailable
scopes server_session_start
scopes tcp_response
scopes http_response http_response-error server_session_end client_session_end
otel-scope client_session_start
span "HAProxy session" root
baggage "haproxy_id" var(sess.otel.uuid)
span "Client session" parent "HAProxy session"
otel-event on-client-session-start
otel-scope frontend_tcp_request
span "Frontend TCP request" parent "Client session"
otel-event on-frontend-tcp-request
otel-scope frontend_http_request
span "Frontend HTTP request" parent "Frontend TCP request"
attribute "http.method" method
attribute "http.url" url
attribute "http.version" str("HTTP/") req.ver
finish "Frontend TCP request"
otel-event on-frontend-http-request
otel-scope backend_tcp_request
span "Backend TCP request" parent "Frontend HTTP request"
finish "Frontend HTTP request"
otel-event on-backend-tcp-request
otel-scope backend_http_request
span "Backend HTTP request" parent "Backend TCP request"
finish "Backend TCP request"
otel-event on-backend-http-request
otel-scope server_unavailable
span "HAProxy session"
status "error" str("503 Service Unavailable")
finish *
otel-event on-server-unavailable
otel-scope server_session_start
span "Server session" parent "HAProxy session"
finish "Backend HTTP request"
otel-event on-server-session-start
otel-scope tcp_response
span "TCP response" parent "Server session"
otel-event on-tcp-response
otel-scope http_response
span "HTTP response" parent "TCP response"
attribute "http.status_code" status
finish "TCP response"
otel-event on-http-response
otel-scope http_response-error
span "HTTP response"
status "error" str("!acl-http-status-ok")
otel-event on-http-response if !acl-http-status-ok
otel-scope server_session_end
finish "HTTP response" "Server session"
otel-event on-http-response
otel-scope client_session_end
finish "*"
otel-event on-http-response

View file

@ -0,0 +1,246 @@
exporters:
exporter_traces_otlp_file:
type: otlp_file
thread_name: "OTLP/file trace"
file_pattern: "__cmp_traces_log-%F-%N"
alias_pattern: "__traces_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_traces_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC trace"
endpoint: "http://localhost:4317/v1/traces"
use_ssl_credentials: false
# ssl_credentials_cacert_path: ""
# ssl_credentials_cacert_as_string: ""
# ssl_client_key_path: ""
# ssl_client_key_string: ""
# ssl_client_cert_path: ""
# ssl_client_cert_string: ""
# timeout: 10
# user_agent: ""
# max_threads: 0
# compression: ""
# max_concurrent_requests: 0
exporter_traces_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP trace"
endpoint: "http://localhost:4318/v1/traces"
content_type: json
json_bytes_mapping: hexid
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP traces test header #1"
- X-OTel-Header-2: "OTLP HTTP traces test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
# ssl_ca_cert_path: ""
# ssl_ca_cert_string: ""
# ssl_client_key_path: ""
# ssl_client_key_string: ""
# ssl_client_cert_path: ""
# ssl_client_cert_string: ""
# ssl_min_tls: ""
# ssl_max_tls: ""
# ssl_cipher: ""
# ssl_cipher_suite: ""
# compression: ""
exporter_traces_dev_null:
type: ostream
filename: /dev/null
exporter_traces_ostream:
type: ostream
filename: __cmp_traces
exporter_traces_memory:
type: memory
buffer_size: 256
exporter_traces_zipkin:
type: zipkin
endpoint: "http://localhost:9411/api/v2/spans"
format: json
service_name: "zipkin-service"
# ipv4: ""
# ipv6: ""
exporter_metrics_otlp_file:
type: otlp_file
thread_name: "OTLP/file metr"
file_pattern: "__cmp_metrics_log-%F-%N"
alias_pattern: "__metrics_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_metrics_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC metr"
endpoint: "http://localhost:4317/v1/metrics"
use_ssl_credentials: false
exporter_metrics_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP metr"
endpoint: "http://localhost:4318/v1/metrics"
content_type: json
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP metrics test header #1"
- X-OTel-Header-2: "OTLP HTTP metrics test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
exporter_metrics_dev_null:
type: ostream
filename: /dev/null
exporter_metrics_ostream:
type: ostream
filename: __cmp_metrics
exporter_metrics_memory:
type: memory
buffer_size: 256
exporter_logs_otlp_file:
type: otlp_file
thread_name: "OTLP/file logs"
file_pattern: "__cmp_logs_log-%F-%N"
alias_pattern: "__logs_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_logs_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC logs"
endpoint: "http://localhost:4317/v1/logs"
use_ssl_credentials: false
exporter_logs_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP logs"
endpoint: "http://localhost:4318/v1/logs"
content_type: json
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP logs test header #1"
- X-OTel-Header-2: "OTLP HTTP logs test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
exporter_logs_dev_null:
type: ostream
filename: /dev/null
exporter_logs_ostream:
type: ostream
filename: __cmp_logs
exporter_logs_elasticsearch:
type: elasticsearch
host: localhost
port: 9200
index: logs
response_timeout: 30
debug: false
http_headers:
- X-OTel-Header-1: "Elasticsearch logs test header #1"
- X-OTel-Header-2: "Elasticsearch logs test header #2"
readers:
reader_metrics:
thread_name: "reader metr"
export_interval: 10000
export_timeout: 5000
samplers:
sampler_traces:
# type: always_on
# type: always_off
# type: trace_id_ratio_based
# ratio: 1.0
type: parent_based
delegate: always_on
processors:
processor_traces_batch:
type: batch
thread_name: "proc/batch trac"
# Note: when the queue is half full, a preemptive notification is sent
# to start the export call.
max_queue_size: 2048
# Time interval (in ms) between two consecutive exports
schedule_delay: 5000
# Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
max_export_batch_size: 512
processor_traces_single:
type: single
processor_logs_batch:
type: batch
thread_name: "proc/batch logs"
max_queue_size: 2048
schedule_delay: 5000
max_export_batch_size: 512
processor_logs_single:
type: single
providers:
provider_traces:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-cmp"
- service.name: "cmp"
- service.namespace: "HAProxy traces test"
provider_metrics:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-cmp"
- service.name: "cmp"
- service.namespace: "HAProxy metrics test"
provider_logs:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-cmp"
- service.name: "cmp"
- service.namespace: "HAProxy logs test"
signals:
traces:
scope_name: "HAProxy OTEL - traces"
exporters: exporter_traces_otlp_http
samplers: sampler_traces
processors: processor_traces_batch
providers: provider_traces
metrics:
scope_name: "HAProxy OTEL - metrics"
exporters: exporter_metrics_otlp_http
readers: reader_metrics
providers: provider_metrics
logs:
scope_name: "HAProxy OTEL - logs"
exporters: exporter_logs_otlp_http
processors: processor_logs_batch
providers: provider_logs

24
addons/otel/test/copy-yml.sh Executable file
View file

@ -0,0 +1,24 @@
#!/bin/sh -u
#
# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
#
SH_FILE="${1:-}"
SH_EXT="${2:-}"
if test ${#} -ne 2; then
echo
echo "usage: $(basename "${0}") input-file test-name"
echo
exit 64
fi
sed '
s/^\( *\)\(filename:\)\( *\)_\(_[a-z]*\)/\1\2\3__'"${SH_EXT}"'\4/g
s/^\( *\)\(file_pattern:\)\( *\)"_\(_[a-z]*_[^"]*\)"/\1\2\3"__'"${SH_EXT}"'\4"/g
s/^\( *\)\(- service.instance.id:\)\( *\).*/\1\2\3"id-'"${SH_EXT}"'"/g
s/^\( *\)\(- service.name:\)\( *\).*/\1\2\3"'"${SH_EXT}"'"/g
s/^\( *\)\(- service.namespace:\)\( *\)\("otelc\)/\1\2\3"HAProxy/g
s/^\( *\)\(scope_name:\)\( *\)"OTEL C wrapper /\1\2 "HAProxy OTEL /g
s/^\( *\)\(exporters:\)\( *\)\(exporter_[a-z]*_\).*/\1\2\3\4otlp_http/g
' "${SH_FILE}"

View file

@ -0,0 +1,28 @@
global
stats socket /tmp/haproxy.sock mode 666 level admin
listen stats
mode http
bind *:8001
stats uri /
stats admin if TRUE
stats refresh 10s
frontend otel-test-ctx-frontend
bind *:10080
default_backend servers-backend
# ACL used to distinguish successful from error responses
acl acl-http-status-ok status 100:399
# OTel filter
filter opentelemetry id otel-test-ctx config ctx/otel.cfg
# run response scopes for successful responses
http-response otel-group otel-test-ctx http_response_group if acl-http-status-ok
# run after-response scopes for error responses
http-after-response otel-group otel-test-ctx http_after_response_group if !acl-http-status-ok
backend servers-backend
server server-1 127.0.0.1:8000

View file

@ -0,0 +1,196 @@
[otel-test-ctx]
otel-instrumentation otel-test-instrumentation
debug-level 0x77f
log localhost:514 local7 debug
config ctx/otel.yml
option dontlog-normal
option hard-errors
no option disabled
rate-limit 100.0
groups http_response_group
groups http_after_response_group
scopes client_session_start_1
scopes client_session_start_2
scopes frontend_tcp_request
scopes http_wait_request
scopes http_body_request
scopes frontend_http_request
scopes switching_rules_request
scopes backend_tcp_request
scopes backend_http_request
scopes process_server_rules_request
scopes http_process_request
scopes tcp_rdp_cookie_request
scopes process_sticking_rules_request
scopes client_session_end
scopes server_unavailable
scopes server_session_start
scopes tcp_response
scopes http_wait_response
scopes process_store_rules_response
scopes http_response http_response-error
scopes server_session_end
otel-group http_response_group
scopes http_response_1
scopes http_response_2
otel-scope http_response_1
span "HTTP response"
event "event_1" "hdr.content" res.hdr("content-type") str("; length: ") res.hdr("content-length") str(" bytes")
otel-scope http_response_2
span "HTTP response"
event "event_2" "hdr.date" res.hdr("date") str(" / ") res.hdr("last-modified")
otel-group http_after_response_group
scopes http_after_response
otel-scope http_after_response
span "HAProxy response" parent "HAProxy session"
status "error" str("http.status_code") status
otel-scope client_session_start_1
span "HAProxy session" root
inject "otel_ctx_1" use-headers use-vars
baggage "haproxy_id" var(sess.otel.uuid)
otel-event on-client-session-start
otel-scope client_session_start_2
extract "otel_ctx_1" use-vars
span "Client session" parent "otel_ctx_1"
inject "otel_ctx_2" use-headers use-vars
otel-event on-client-session-start
otel-scope frontend_tcp_request
extract "otel_ctx_2" use-vars
span "Frontend TCP request" parent "otel_ctx_2"
inject "otel_ctx_3" use-headers use-vars
otel-event on-frontend-tcp-request
otel-scope http_wait_request
extract "otel_ctx_3" use-vars
span "HTTP wait request" parent "otel_ctx_3"
inject "otel_ctx_4" use-headers use-vars
finish "Frontend TCP request" "otel_ctx_3"
otel-event on-http-wait-request
otel-scope http_body_request
extract "otel_ctx_4" use-vars
span "HTTP body request" parent "otel_ctx_4"
inject "otel_ctx_5" use-headers use-vars
finish "HTTP wait request" "otel_ctx_4"
otel-event on-http-body-request
otel-scope frontend_http_request
extract "otel_ctx_5" use-vars
span "Frontend HTTP request" parent "otel_ctx_5"
attribute "http.method" method
attribute "http.url" url
attribute "http.version" str("HTTP/") req.ver
inject "otel_ctx_6" use-headers use-vars
finish "HTTP body request" "otel_ctx_5"
otel-event on-frontend-http-request
otel-scope switching_rules_request
extract "otel_ctx_6" use-vars
span "Switching rules request" parent "otel_ctx_6"
inject "otel_ctx_7" use-headers use-vars
finish "Frontend HTTP request" "otel_ctx_6"
otel-event on-switching-rules-request
otel-scope backend_tcp_request
extract "otel_ctx_7" use-vars
span "Backend TCP request" parent "otel_ctx_7"
inject "otel_ctx_8" use-headers use-vars
finish "Switching rules request" "otel_ctx_7"
otel-event on-backend-tcp-request
otel-scope backend_http_request
extract "otel_ctx_8" use-vars
span "Backend HTTP request" parent "otel_ctx_8"
inject "otel_ctx_9" use-headers use-vars
finish "Backend TCP request" "otel_ctx_8"
otel-event on-backend-http-request
otel-scope process_server_rules_request
extract "otel_ctx_9" use-vars
span "Process server rules request" parent "otel_ctx_9"
inject "otel_ctx_10" use-headers use-vars
finish "Backend HTTP request" "otel_ctx_9"
otel-event on-process-server-rules-request
otel-scope http_process_request
extract "otel_ctx_10" use-vars
span "HTTP process request" parent "otel_ctx_10"
inject "otel_ctx_11" use-headers use-vars
finish "Process server rules request" "otel_ctx_10"
otel-event on-http-process-request
otel-scope tcp_rdp_cookie_request
extract "otel_ctx_11" use-vars
span "TCP RDP cookie request" parent "otel_ctx_11"
inject "otel_ctx_12" use-headers use-vars
finish "HTTP process request" "otel_ctx_11"
otel-event on-tcp-rdp-cookie-request
otel-scope process_sticking_rules_request
extract "otel_ctx_12" use-vars
span "Process sticking rules request" parent "otel_ctx_12"
inject "otel_ctx_13" use-headers use-vars
finish "TCP RDP cookie request" "otel_ctx_12"
otel-event on-process-sticking-rules-request
otel-scope client_session_end
finish "Client session" "otel_ctx_2"
otel-event on-client-session-end
otel-scope server_unavailable
finish *
otel-event on-server-unavailable
otel-scope server_session_start
span "Server session" parent "otel_ctx_1"
inject "otel_ctx_14" use-vars
extract "otel_ctx_13" use-vars
finish "Process sticking rules request" "otel_ctx_13"
otel-event on-server-session-start
otel-scope tcp_response
extract "otel_ctx_14" use-vars
span "TCP response" parent "otel_ctx_14"
inject "otel_ctx_15" use-vars
otel-event on-tcp-response
otel-scope http_wait_response
extract "otel_ctx_15" use-vars
span "HTTP wait response" parent "otel_ctx_15"
inject "otel_ctx_16" use-headers use-vars
finish "TCP response" "otel_ctx_15"
otel-event on-http-wait-response
otel-scope process_store_rules_response
extract "otel_ctx_16" use-vars
span "Process store rules response" parent "otel_ctx_16"
inject "otel_ctx_17" use-headers use-vars
finish "HTTP wait response" "otel_ctx_16"
otel-event on-process-store-rules-response
otel-scope http_response
extract "otel_ctx_17" use-vars
span "HTTP response" parent "otel_ctx_17"
attribute "http.status_code" status
finish "Process store rules response" "otel_ctx_17"
otel-event on-http-response
otel-scope http_response-error
span "HTTP response"
status "error" str("!acl-http-status-ok")
otel-event on-http-response if !acl-http-status-ok
otel-scope server_session_end
finish *
otel-event on-server-session-end

View file

@ -0,0 +1,246 @@
exporters:
exporter_traces_otlp_file:
type: otlp_file
thread_name: "OTLP/file trace"
file_pattern: "__ctx_traces_log-%F-%N"
alias_pattern: "__traces_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_traces_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC trace"
endpoint: "http://localhost:4317/v1/traces"
use_ssl_credentials: false
# ssl_credentials_cacert_path: ""
# ssl_credentials_cacert_as_string: ""
# ssl_client_key_path: ""
# ssl_client_key_string: ""
# ssl_client_cert_path: ""
# ssl_client_cert_string: ""
# timeout: 10
# user_agent: ""
# max_threads: 0
# compression: ""
# max_concurrent_requests: 0
exporter_traces_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP trace"
endpoint: "http://localhost:4318/v1/traces"
content_type: json
json_bytes_mapping: hexid
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP traces test header #1"
- X-OTel-Header-2: "OTLP HTTP traces test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
# ssl_ca_cert_path: ""
# ssl_ca_cert_string: ""
# ssl_client_key_path: ""
# ssl_client_key_string: ""
# ssl_client_cert_path: ""
# ssl_client_cert_string: ""
# ssl_min_tls: ""
# ssl_max_tls: ""
# ssl_cipher: ""
# ssl_cipher_suite: ""
# compression: ""
exporter_traces_dev_null:
type: ostream
filename: /dev/null
exporter_traces_ostream:
type: ostream
filename: __ctx_traces
exporter_traces_memory:
type: memory
buffer_size: 256
exporter_traces_zipkin:
type: zipkin
endpoint: "http://localhost:9411/api/v2/spans"
format: json
service_name: "zipkin-service"
# ipv4: ""
# ipv6: ""
exporter_metrics_otlp_file:
type: otlp_file
thread_name: "OTLP/file metr"
file_pattern: "__ctx_metrics_log-%F-%N"
alias_pattern: "__metrics_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_metrics_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC metr"
endpoint: "http://localhost:4317/v1/metrics"
use_ssl_credentials: false
exporter_metrics_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP metr"
endpoint: "http://localhost:4318/v1/metrics"
content_type: json
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP metrics test header #1"
- X-OTel-Header-2: "OTLP HTTP metrics test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
exporter_metrics_dev_null:
type: ostream
filename: /dev/null
exporter_metrics_ostream:
type: ostream
filename: __ctx_metrics
exporter_metrics_memory:
type: memory
buffer_size: 256
exporter_logs_otlp_file:
type: otlp_file
thread_name: "OTLP/file logs"
file_pattern: "__ctx_logs_log-%F-%N"
alias_pattern: "__logs_log-latest"
flush_interval: 30000000
flush_count: 256
file_size: 134217728
rotate_size: 5
exporter_logs_otlp_grpc:
type: otlp_grpc
thread_name: "OTLP/gRPC logs"
endpoint: "http://localhost:4317/v1/logs"
use_ssl_credentials: false
exporter_logs_otlp_http:
type: otlp_http
thread_name: "OTLP/HTTP logs"
endpoint: "http://localhost:4318/v1/logs"
content_type: json
debug: false
timeout: 10
http_headers:
- X-OTel-Header-1: "OTLP HTTP logs test header #1"
- X-OTel-Header-2: "OTLP HTTP logs test header #2"
max_concurrent_requests: 64
max_requests_per_connection: 8
ssl_insecure_skip_verify: true
exporter_logs_dev_null:
type: ostream
filename: /dev/null
exporter_logs_ostream:
type: ostream
filename: __ctx_logs
exporter_logs_elasticsearch:
type: elasticsearch
host: localhost
port: 9200
index: logs
response_timeout: 30
debug: false
http_headers:
- X-OTel-Header-1: "Elasticsearch logs test header #1"
- X-OTel-Header-2: "Elasticsearch logs test header #2"
readers:
reader_metrics:
thread_name: "reader metr"
export_interval: 10000
export_timeout: 5000
samplers:
sampler_traces:
# type: always_on
# type: always_off
# type: trace_id_ratio_based
# ratio: 1.0
type: parent_based
delegate: always_on
processors:
processor_traces_batch:
type: batch
thread_name: "proc/batch trac"
# Note: when the queue is half full, a preemptive notification is sent
# to start the export call.
max_queue_size: 2048
# Time interval (in ms) between two consecutive exports
schedule_delay: 5000
# Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
max_export_batch_size: 512
processor_traces_single:
type: single
processor_logs_batch:
type: batch
thread_name: "proc/batch logs"
max_queue_size: 2048
schedule_delay: 5000
max_export_batch_size: 512
processor_logs_single:
type: single
providers:
provider_traces:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-ctx"
- service.name: "ctx"
- service.namespace: "HAProxy traces test"
provider_metrics:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-ctx"
- service.name: "ctx"
- service.namespace: "HAProxy metrics test"
provider_logs:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-ctx"
- service.name: "ctx"
- service.namespace: "HAProxy logs test"
signals:
traces:
scope_name: "HAProxy OTEL - traces"
exporters: exporter_traces_otlp_http
samplers: sampler_traces
processors: processor_traces_batch
providers: provider_traces
metrics:
scope_name: "HAProxy OTEL - metrics"
exporters: exporter_metrics_otlp_http
readers: reader_metrics
providers: provider_metrics
logs:
scope_name: "HAProxy OTEL - logs"
exporters: exporter_logs_otlp_http
processors: processor_logs_batch
providers: provider_logs

View file

@ -0,0 +1,19 @@
global
stats socket /tmp/haproxy.sock mode 666 level admin
listen stats
mode http
bind *:8001
stats uri /
stats admin if TRUE
stats refresh 10s
frontend otel-test-empty
bind *:10080
default_backend servers-backend
# OTel filter
filter opentelemetry id otel-test-empty config empty/otel.cfg
backend servers-backend
server server-1 127.0.0.1:8000

View file

@ -0,0 +1,2 @@
otel-instrumentation otel-test-instrumentation
config empty/otel.yml

Some files were not shown because too many files have changed in this diff Show more