Compare commits

...

282 commits

Author SHA1 Message Date
Willy Tarreau
e8c9aabd62 BUG/MINOR: haterm: fix the random suffix multiplication
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
Passing a size or anything with suffix "r" is supposed to apply a
random factor form 0 to 1. However due to the replacement of random()
with ha_random64(), all 64 bits are random before the divide, so the
end result is a random 32-bit value. In addition, ha_random64() is
slow since shared between threads.

Let's use statistical_prng() which is designed for this purpose and
is much cheaper. No backport is needed, this is only in 3.4.
2026-05-25 20:49:22 +02:00
Willy Tarreau
32fc35ef09 CLEANUP: resolvers: fix comment typos and wrong filenames in file headers
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
A few asorted comment fixes for resolvers (incorrect file name etc).
2026-05-25 10:57:14 +02:00
Willy Tarreau
6bb8cb51e6 CLEANUP: resolvers: remove pool_free(NULL) in SRV additional record matching
In resolv_validate_dns_response(), when matching an additional A/AAAA
record to an SRV record, the code checked tmp_record->ar_item == NULL
then called pool_free(resolv_answer_item_pool, tmp_record->ar_item).
This is a copy-paste mistake from similar patterns elsewhere since
the pointer is confirmed to be NULL a few lines above, so let's just
drop the confusing pool_free.
2026-05-25 10:57:14 +02:00
Willy Tarreau
8fe8d5fbe3 CLEANUP: resolvers: use read_n32() instead of open-coded big-endian read
In resolv_validate_dns_response(), the second DNS record parsing path
manually constructs a 32-bit big-endian TTL value from four individual
bytes using the expression:

  reader[0] * 16777216 + reader[1] * 65536 + reader[2] * 256 + reader[3]

We have read_n32() to do this, and it's more robust against unexpected
signedness surprises (which should not happen right here since reader is
unsigned char and we use -fwrapv so the result is defined). Also, let's
make the ttl an uint instead of an int. The TTL is only retrieved and not
used for now, so better clean it now.
2026-05-25 10:57:13 +02:00
Willy Tarreau
b78b023d55 BUG/MINOR: sample: limit the be2hex converter's chunk size
In 2.5, commit da0264a96 ("MINOR: sample: Add be2hex converter")
introduced the be2hex() converter, which reads input data of a given
chunk size, processes it as a big endian block and turns it to hex
output.

There's an issue if the configured chunk_size (2nd argument) is larger
than tune.bufsize/2, because the max_size calculation will underflow,
and the later loop will always match since it compares a size_t to an
int (BTW, compilers love to annoy us with useless warnings but I never
found how to see some for these ones). This can result in overflowing
the output trash if  the input sample is at least as large as half a
buffer.

Let's add an explicit check for this, and change the max_size type to
size_t so that the comparison is always right. While we're at it, let's
ask the trash buffer to be twice as large, just like bin2hex() does, as
it may result in offering a larger buffer in 3.4. thanks to the large
buffers support.

Despite the risk, this is marked as minor because a config with that
large an argument in the converter makes absolutely no sense.

This should be backported to 2.6. The *2 for the trash allocation will
conflict and have to be dropped in stable versions, which is safe.
2026-05-25 10:57:13 +02:00
Willy Tarreau
7d182a2ed5 BUG/MINOR: init: use more than ha_random64() for the cluster secret
When not set, the cluster secret is randomly generated by two
consecutive calls to ha_random64(). However, the random64 PRNG may be
partially observed on a fully idle machine (QUIC retry tokens, UUID,
WS key), and it could be rolled back to the initial call that produced
the secret. This is purely theoretical as a normally loaded system
wouldn't reveal meaningful sequences, but better address this while
it's still easy.

The first here consists in isolating the cluster_secret from the PRNG
sequence. When RAND_bytes() is available and works, it's used. Otherwise
ha_random64() is mixed with uncorrelated bits from random().

This could be backported to stable releases.
2026-05-25 10:52:42 +02:00
Willy Tarreau
c0e302fe79 BUG/MINOR: dict: fix refcount race on insert collision
In dict_insert(), when ebis_insert() returns an existing node n indicating
that another thread inserted the same key concurrently, the code freed its
own newly-allocated entry and returned the winner without bumping its
refcount. Both callers then held a reference with refcount=1 instead of 2,
so when one expires the other becomes a use-after-free or double-free.

The bug likely comes from the fact that new_dict_entry() creates an entry
with a refcount preset to 1 (saves an atomic op) and that because of this
there is no refcount increment upon a successful insertion in the tree,
resulting in requiring different code paths for collision and normal
insertion.

A simple fix consists in bumping the refcount under the lock and unlocking
only at the end, but this would mean performing two free() calls under a
lock, which we always try to avoid. The code was slightly rearranged so
that we can now bump the existing entry's refcount under the lock in case
of duplicate, or unlock immediately in the common case, so that the free()
call is done out of the lock.

The probably of the race is very low (at peers connection setup only),
reason why it's marked low. This should be backported to all versions.
2026-05-25 10:52:42 +02:00
Willy Tarreau
478e7e52cb BUG/MINOR: log: look for the end of priority before the end of the buffer
In parse_log_message(), the first loop looks for '>' that finishes the
priority field, and unfortunately it stops once it has checked the first
byte after the end of the buffer. This means that a priority made only
of digits for the whole buffer would read one extra byte. In practice
since pools have a tag at the end this is only detectable when using ASAN,
but this should be fixed nevertheless.

This can be backported to all versions.

It's worth noting that RFC5424 now says that the PRI field is 1..3
digits only, so maybe at some point we could seriously limit the
length as well.
2026-05-25 10:52:42 +02:00
Willy Tarreau
8e1d33a648 BUG/MINOR: mux-h2: validate HEADERS frame length before reading stream dep
When the PRIORITY flag is present on a HEADERS frame, the frame must
contain a stream dependency and a weight, for a total of 5 bytes. The
length is checked after reading the stream dep field so theoretically
such a frame could cause up to 4-byte OOB read at the end of the buffer,
though in practice buffers allocated from pools never end on a page
boundary (one extra word at the end) and the anomaly is still detected
after reading the stream ID and the connection aborted with the glitch
count incremented. Thus while not technically correct, practically
speaking it's harmless.

This should be backported to all stable releases.
2026-05-25 10:52:42 +02:00
Willy Tarreau
49d6306de3 BUG/MINOR: resolvers: fix risk of appending garbage past the domain name
The previous fix 75f72c2eb ("BUG/MEDIUM: resolvers: Fix test on dn label
size in resolv_dn_label_to_str()") may still leave garbage from the input
buffer into the response: if a component length is passed as zero, it
should mark the end, but instead a dot will be emitted, and whatever
follows it in the input buffer would continue to be appended as extra
components. While having no direct consequences beyond the domain not
being properly decoded, it could at least complicate troubleshooting.

This should be backported where the fix above is backported.
2026-05-25 10:52:42 +02:00
Willy Tarreau
01ebb668a4 BUG/MINOR: resolvers: fix room for trailing zero in resolv_dn_label_to_str()
The previous fix 75f72c2eb ("BUG/MEDIUM: resolvers: Fix test on dn label
size in resolv_dn_label_to_str()") can still be fooled by an input exactly
the size of str_len, in which case the trailing zero appended at the end
was not being accounted for. Let's add 1 to the condition to prepare for
it.

This needs to be backported wherever the fix above is backported.
2026-05-25 10:52:42 +02:00
Willy Tarreau
340cc86efb BUG/MINOR: log: free logformat expr on compile failure in cfg_parse_log_profile
When lf_expr_compile() fails in cfg_parse_log_profile, the code leaves
without freeing the previously strdup()'d strings in target_lf->str and
target_lf->conf.file. Let's add a call to lf_expr_deinit() there to
release it.

It was harmless anyway since the startup will abort when this happens,
but better clean it because with increasingly dynamic setups, one day
it could become a runtime leak.

No backport is needed.
2026-05-25 10:52:42 +02:00
Willy Tarreau
f62d020140 BUG/MEDIUM: cache: fix a refcount leak for missed secondary entries
When a primary cache hit has a Vary secondary_key_signature, the code calls
retain_entry() and shctx_row_detach() before performing the secondary lookup.
If get_secondary_entry() returns NULL (no stored variant matches), res is set
to NULL and the function falls through to return ACT_RET_CONT without calling
release_entry() or shctx_row_reattach(). Each such request leaks one refcount
and pins one shctx row permanently, eventually exhausting the cache if this
happens to all objects. This is visible when requesting a secondary key
covered by vary for an object that is already stored without that key.
"show cache" then shows the object's refcount increasing after each request.

In order to fix this we must do like when no secondary key could be built
and release everything. We only reattach to the row if we previously
detached.

The issue was introduced in 2.4 with commit 1785f3dd9 ("MEDIUM: cache: Add
the Vary header support"). The code changed a bit in 2.9 with commit
48f81ec09 ("MAJOR: cache: Delay cache entry delete in reserve_hot function"),
so in order to backport to 2.8 and older, the patch will have to be manually
applied (no test on detached).
2026-05-25 10:52:42 +02:00
Willy Tarreau
bbef74fb21 BUG/MEDIUM: tcpcheck/spoe: bound the SPOP error code to valid values
tcpcheck_spop_expect_hello() stores the SPOA agent-supplied status-code
varint directly into check->code (signed short) without range validation.
The code is later used as an index into spop_err_reasons[100]. Let's
just replace invalid status codes with SPOP_ERR_UNKNOWN to avoid any
problem.

The SPOP tcp-check was introduced in 3.1 so this fix must be backported
to 3.2.
2026-05-25 10:16:06 +02:00
Willy Tarreau
608951844e BUG/MEDIUM: regex: allocate a large enough pcre2 match for all matches
In 3.3 with commit fda6dc959 ("MINOR: regex: use a thread-local match
pointer for pcre2") we got a thread-local match that saves us from having
to allocate a match array with each match. However something was clearly
overlooked or misunderstood in the pcre2 API because the local match
array was initialized via pcre2_match_data_create() for MAX_MATCH-1
entries instead of MAX_MATCH, despite the commit message mentioning
MAX_MATCH entries. It was possibly confused with an index. Due to this
there is a risk of crash when matching more than 9 groups in a regex.

This fix must be backported to 3.3.
2026-05-25 10:16:06 +02:00
Willy Tarreau
f9088a5d75 BUG/MEDIUM: log-forward: make sure the month is unsigned
In 2.3, in preparation for log forwarding, commit 546488559 ("MEDIUM:
log/sink: re-work and merge of build message API.") extended the log
send API to be able to use metadata from an existing header. However
the month number is parsed from the passed meta-data and compared
against 11 but there's no check for negative values which could in
theory cause a negative monthname[] index.

It can be a problem when the date is received as RFC5424 and forced
to RFC3164 because certain characters in the month field could result
in a negative month value. Let's fix it by turning the month to unsigned
to make sure we only accept months 0..11.

This should be backported to all branches.
2026-05-25 10:16:06 +02:00
Willy Tarreau
007d5946b4 BUILD: intops: mask the fail value in array_size_or_fail()
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
Cross-compilation on m68k fails in ssl_sock_resize_passphrase_cache()
where the compiler noticed the SIZE_MAX passed to realloc() in the
error path and complained that it's larger than PTRDIFF_MAX. This can
be disabled with -Walloc-size-larger-than=SIZE_MAX but in practice we
can simply hide the value and keep the warning to detect real failures
elsewhere. Let's pass it through DISGUISE() and also take this
opportunity for doing that inside an unlikely() clause since it's never
supposed to happen.
2026-05-25 07:33:35 +02:00
CyberpsychoJacob
4db85fc53e BUG/MEDIUM: acme: NUL terminate response buffer before PEM parsing
acme_res_certificate() passes the httpclient response buffer to
ssl_sock_load_pem_into_ckch(), which will then call BIO_new_mem_buf(buf, -1).
The "-1" flag will make the OpenSSL PEM parser determine the length by
using strlen(). However, the httpclient populates the response buffer with
__b_putblk() without writing a trailing NUL to it. The byte at area[data]
is whatever data previously resided there in the memory pool.

Thus, a malicious or compromised ACME CA can perform an arbitrary-length
out-of-bounds read until hitting the first NULL byte past the response
body. The OpenSSL PEM loader will try to iterate to load the chain
certificates, thus the PEM-looking garbage found in freed memory chunks
can be erroneously loaded as additional intermediate certificates. The
presence of a single NUL inside the valid response body will result in
silent truncation of the certificate.

Make sure that the area[data] contains a terminating NULL before passing
the buffer to the parser. Fail on insufficient room for the NUL terminator.

No backport required: The ACME client has been added in 3.x and this
code path didn't exist in 2.x.
2026-05-23 18:09:59 +02:00
Christopher Faulet
41bb1c24f6 BUG/MEDIUM: cli: Fix parsing of pattern finishing a command payload
Some checks failed
Contrib / admin/halog/ (push) Has been cancelled
Contrib / dev/flags/ (push) Has been cancelled
Contrib / dev/haring/ (push) Has been cancelled
Contrib / dev/hpack/ (push) Has been cancelled
Contrib / dev/poll/ (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 dedidacted buffer to store the command payload was added (c5ae0da62
"MEDIUM: cli: Make a buffer for the command payload"), an bug was
introduced. When the pattern finishing the command payload is found, it is
removed from the buffer. A NULL-bytes is added before it, skipping the
previous newline character.

It worked well in all cases before the commit above, because the commandline
was already parsed and was placed at the beginning of the cmdline buffer.
So, there is always a line before the payload.

Now, the payload is stored in a dedicated buffer. So there is nothing
preceeding it in a buffer. If the payload is empty, we cannot rewind to the
previous line to set the NULL-byte character. We must handle this case to
avoid integer underflow on the payload buffer length.

It is a 3.4-specific bug. No backport needed.
2026-05-22 17:17:01 +02:00
Christopher Faulet
9091cfa617 BUG/MEDIUM: hlua: Fix integer underflow when receiving line from lua cosocket
In hlua_socket_receive_yield(), when we try to get a line, the trailing CRLF is
stripped by decrementing the block length. The '\n' is first skipped, then,
possible a preceeding '\r'. But the block lenght is never checked. If an empty
line is returned, this leads to an integer underflow and most probably to a
crash because this length is used to copy data into a LUA string.

To fix the issue, the block length is now properly tested against 0 before
decrementing it.

This patch must be backported to all stable versions.
2026-05-22 17:17:01 +02:00
Christopher Faulet
57b526e022 BUG/MINOR: tcpchecks: Limit parsing of agent-check reply to the buffer
When parsing the agent-check reply, we first loop on the response to find
the newline character, to add a NULL-byte at the end of the line. However,
this loop is not bounded to the data available in the buffer. So it is
possible to read bytes outside the buffer and eventually write a NULL-byte
ouside the buffer.

So let's check for the end of the buffer when looping on the agent-check
reply.

This patch must be backported to all stable versions.
2026-05-22 17:17:01 +02:00
Christopher Faulet
2644f9ddf9 BUG/MEDIUM: dict: hold lock while decrementing refcount in dict_entry_unref
In dict_entry_unref(), the write lock on d->rwlock was only acquired after
decrementing the refcount. However, between the decrement and the lock,
another thread could increment it by calling dict_insert(). That could lead
to a UAF.

To fix the issue, the call to HA_ATOMIC_SUB_FETCH is moved inside the write
lock.

This patch must be backported to all stable versions.
2026-05-22 17:17:01 +02:00
Amaury Denoyelle
7cab3a3c3a BUG/MINOR: quic: fix ODCID lookup from derived value
Some checks failed
Contrib / admin/halog/ (push) Has been cancelled
Contrib / dev/flags/ (push) Has been cancelled
Contrib / dev/haring/ (push) Has been cancelled
Contrib / dev/hpack/ (push) Has been cancelled
Contrib / dev/poll/ (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
In haproxy, when an Initial packet is received, a new connection may be
created and a DCID must be attributed. This CID is derived from the
original DCID used by the client in its first packet. This is an
optimization to avoid storing two CIDs values in the CID tree.

On CID lookup, if the DCID used is not found, derivation is performed
again. This should permit to retrieve the DCID node. However, this
operation is not performed as expected in quic_get_cid_tid(), as the
wrong value is used on the second lookup. Fix this function by using
derive CID for it. Note that retrieve_qc_conn_from_cid() performs the
same lookup but the bug was not present there.

The impact of this bug is relatively low as most clients send a single
Initial packet. Even in case of multiple packets in a single datagram,
this does not cause any issue as the current thread is assigned as
default.

This should be backported up to 2.8.
2026-05-22 16:03:10 +02:00
Christopher Faulet
04b9215a2e BUG/MEDIUM: ssl-gencert: Unlock LRU cache if failing to generate certificate
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
In ssl_sock_generate_certificate(), if the LRU cache for generated
certificates is used, the LRU tree is not unlocked on cache miss if the
certificate generation failed. So let's unlock it on error path.

The bug was introduced by the commit fbc98ebcd ("BUG/MEDIUM: ssl: fix error
path on generate-certificates"). So this patch must be backported with the
commit above, so to all stable versions.
2026-05-22 11:37:00 +02:00
Christopher Faulet
75f72c2eb9 BUG/MEDIUM: resolvers: Fix test on dn label size in resolv_dn_label_to_str()
In resolv_dn_label_to_str(), size for a dn label was stored into an integer
from a signed char without a cast to unsigned. So dn label with a size of
128 bytes or more become negative, skipping this way the copy loop and
desynchronizing input vs output.

In addition, the size of the destination string was only checked at the
begining, against the dn string length. But it must also be checked for
every dn label, to be sure. The dn string can be forged to copied more bytes
than expected.

This patch must be backported to all stable versions.
2026-05-22 11:13:33 +02:00
Christopher Faulet
1ed4ef6659 BUG/MEDIUM: applet: Properly handle receives of size 0
when appctx_rcv_buf() function was called to get data from the applet, but
to get zero bytes, nothing was performed and the function early
returned. However, we must at least take care to set SE_FL_WANT_ROOM if
necessary. Otherwise, if data are still blocked in the applet's output
buffer while the EOI/EOS are pending, the information can be reported to the
upper layer and remaining data can be lost.

Indeed, in such case, SE_FL_WANT_ROOM flag is here to specify the applet has
more data to deliver. Thanks to this flag, the stream will wait before
closing. But when appctx_rcv_buf() function is called, this flag is removed by
the stconn. It is the function responsibility to set it again when necessary.

This patch should fix second part of the issue #3366. It must be backported
to 3.0.
2026-05-22 08:45:57 +02:00
Amaury Denoyelle
3fab21ea42 MINOR: mux_quic: do not crash on unhandled QMux frame reception
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
Completes qmux_parse_frm() to ensure every frames allowed by QMux
protocol are listed. For now, nothing is implemented except a CHECK_IF()
to report such events.

This is necessary to prevent a crash on abort. Frames not supported by
QMux should already have been rejected prior via qmux_is_frm_valid().
2026-05-21 15:57:20 +02:00
Amaury Denoyelle
f9d4d659a4 MINOR: mux_quic: handle MAX_STREAMS for uni stream in QMux
Handle reception of a MAX_STREAMS frame for unidirectional stream usage
when using QMux. This simply consists in using qcc_recv_max_streams() as
with QUIC protocol.
2026-05-21 15:57:20 +02:00
Amaury Denoyelle
c0aa91a202 MINOR: mux_quic: handle STOP_SENDING in QMux
Ensure reception of STOP_SENDING via QMux protocol is properly handled.
This simply consists in using qcc_recv_stop_sending() which will update
the associated QCS if found.
2026-05-21 15:57:20 +02:00
Remi Tricot-Le Breton
e2c3cd9eb7 BUG/MINOR: ocsp: Manage date too far away in the future
The check on the OCSP response expire time is based on the "Next Update"
field of the response, converted by my_timegm function that returns a
time_t (signed long). It is then stored in the 'expire' field of the
certificate_ocsp structure which is typed as a signed long.
When loading an OCSP response, if the "Next Update" time is too far in
the future and we are running on a 32 bits machine, we might end up with
negative times ireturned by my_timegm, which make the comparison with
the current date fail and raises the "OCSP single response: no longer
valid." error message.

This problem typically happens in the ocsp_auto_update.vtc regtest since
the loaded OCSP response have a "Next Update" field in 2050.

This patch simply changes the type of the expire field to an unsigned
long since the 'my_timegm' function does not return '-1' in case of
error, contrary to the standard 'timegm' one.

Ths patch can be backported to all stable branches.
2026-05-21 15:43:49 +02:00
Amaury Denoyelle
6717531053 MINOR: backend: support QMux in clear for BE side
Some checks failed
Contrib / admin/halog/ (push) Has been cancelled
Contrib / dev/flags/ (push) Has been cancelled
Contrib / dev/haring/ (push) Has been cancelled
Contrib / dev/hpack/ (push) Has been cancelled
Contrib / dev/poll/ (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
Use xprt_add_l6hs() at the end of connect_server() if selected MUX layer
relies on a temporary handshake prior to its initialization. This
functions is noop is SSL layer is active.

This change is necessary to support clear QMux on the backend side.
Recently defined <init_xprt> from mux_proto_list is used to render the
code as generic as possible.
2026-05-21 15:09:10 +02:00
Amaury Denoyelle
812962d110 MINOR: session: support QMux in clear on FE side
Activates xprt_qmux layer if necessary via session_accept_fd(). This is
necessary to be able to support QMux in clear. This operation is noop if
SSL is active, as in this case xprt_qmux will be activated after the SSL
handshake completion.

To ensure MUX init is delayed when running with clear QMux, mask
CO_FL_WAIT_XPRT_L6 is added to test if the embryonic task must be
started instead.
2026-05-21 15:09:10 +02:00
Amaury Denoyelle
8fe8f78473 MINOR: connection: define mask CO_FL_WAIT_XPRT_L6
Define a new connection flag mask CO_FL_WAIT_XPRT_L6. This will be used
to indicate that a XPRT layer is running on top of layer 6. For now,
only xprt_qmux implements this method of operation.
2026-05-21 15:09:10 +02:00
Amaury Denoyelle
cdeb2aa4ef MINOR: xprt_qmux: define default value for get_alpn
Extend get_alpn() for xprt_qmux layer. If lower layer does not implement
ALPN negotiation, return a statically default protocol value. Currently
this is set to "h3".

This change is required to support QMux in clear without SSL. In the
future, it could be useful to configure the default protocol, for
example by extending the syntax for the "proto" keyword.
2026-05-21 15:09:10 +02:00
Amaury Denoyelle
9e6e0fd149 MINOR: connection: define xprt_add_l6hs()
When QMux protocol is used, xprt_qmux layer is setup after SSL handshake
completion but prior to the MUX initialization. Once transport
parameters exchange is successful, the layer is removed and the MUX is
started.

The layer setup operation was performed directly on ssl_sock_io_cb().
Simplify the code by extracting it in a dedicated function
xprt_add_l6hs(). The function is generic so the requested XPRT layer
must be passed as argument.

The code is mostly identical. One difference is that a check is
performed to ensure no SSL handshake is pending. If this is the case,
the function is a noop. This will become useful to support QMux
transparently both in clear or on top of SSL.

Another minor addition is that CO_FL_XPRT_READY flag is automatically
resetted by xprt_add_l6hs(). This allows the code to use
conn_xprt_start() standard function after XPRT init.
2026-05-21 15:09:10 +02:00
Amaury Denoyelle
e98595e4e5 MINOR: ssl_sock: remove unneeded check on QMux flags
A recent patch has introduced <init_xprt> mux_proto_list member. This
allows to activate QMux on SSL handshake completion without explicit
"proto qmux" setting.

Thanks to this change, on SSL handshake completion it is not necessary
anymore to check for CO_FL_QMUX_* flags.
2026-05-21 15:09:10 +02:00
Willy Tarreau
413f6f9a1f BUG/MEDIUM: net_helper: fix a remaining possibly infinite loop in converters
The various tcp_option_* converters rely on tcp_fullhdr_find_opt() to
find the option. However, the same bug as fixed in commit dbf471f99a
("BUG/MAJOR: net_helper: ip.fp infinite loop on malformed tcp options")
was also present there, by which an option of length 0 could be looped
over indefinitely. In practice this does not happen since such options
are not valid, but if passed encoded in an HTTP header for example, it
could possibly be passed.

While fixing it, let's check for length >1 in all 3 locations insteead
of only non-zero, since there's no point processing a malformed option
that wouldn't even be properly skipped.

This fix doesn't need to be backported, unless the ip.fp series is.

Thanks to @Vincent55 for reporting this issue.
2026-05-21 15:05:39 +02:00
Willy Tarreau
3475a5bb9f BUILD: proxy: unstatify the proxies_del_lock to avoid a warning without threads
When threads are disabled, "static __decl_spinlock(foo);" ends up as
"static;", causing a build warning when threads are disabled. We don't
need it to be static so let's drop "static" here. No backport is needed,
this is 3.4-only.
2026-05-21 09:03:03 +02:00
Willy Tarreau
050e06dd66 MINOR: config: shm-stats-file is no longer experimental
As confirmed by Aurlien, there isn't any point in keeping this feature
in experimental status, it's now stable.
2026-05-21 08:50:20 +02:00
Willy Tarreau
bcf768f157 [RELEASE] Released version 3.4-dev13
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
Released version 3.4-dev13 with the following main changes :
    - BUG/MINOR: backend: correct parameter value validation in get_server_ph_post()
    - BUG/MINOR: config/dns: properly fail on duplicate nameserver name detection
    - BUG/MEDIUM: dns: fix long loops in additional records parse on name failure
    - BUG/MEDIUM: resolvers: fix name compression pointer validation in resolv_read_name()
    - BUG/MEDIUM: dns: fix memory leak of sockaddr in dns_session_init() error path
    - CLEANUP: proxy: fix tiny mistakes in parse error messages
    - CLEANUP: dns: fix misleading error messages in dns_stream_init()
    - BUG/MINOR: server: better handling of OOM in srv_set_fqdn()
    - BUG/MINOR: servers: use proper source of pool_conn_name in srv_settings_cpy()
    - BUG/MEDIUM: server/cli: unlock server lock on failure in cli_parse_set_server
    - BUG/MINOR: resolvers: fix dangling list pointer in resolvers_new() error paths
    - BUG/MINOR: dns: fix dangling dgram pointer on dns_dgram_init() failure path
    - BUG/MINOR: proxy: use proxy_drop() in parse_new_proxy() error path
    - CLEANUP: resolvers: properly initialize the sample in resolv_action_do_resolve()
    - BUG/MINOR: resolvers: report the expression error in the do-resolve() action parser
    - BUG/MINOR: resolvers: fix leaked dgram and dns_ring struct in parse_resolve_conf()
    - BUG/MINOR: resolvers: fix leaked fields on cfg_parse_resolvers() error paths
    - BUG/MINOR: resolvers: fix missing task_idle destruction in resolvers_destroy()
    - CLEANUP: proxy: fix duplicate declaration of cli_find_frontend in proxy.h
    - CLEANUP: address a few typos and copy-paste errors in httpclient and dns
    - DOC: internal: add a few rules about internal core principles
    - BUG/MINOR: session/trace: use distinct flags for SESS_EV_END and _ERR
    - CLEANUP: stick-table: uniformize the different action_inc_gpc*()
    - REGTESTS: do not run quic/tls13_ssl_crt-list_filters in quic openssl compat mode
    - REGTESTS: quic/issuers_chain_path: do not forget to enable QUIC compat mode
    - BUG/MINOR: sock: store the connection error status
    - BUG/MINOR: check: properly report errno in chk_report_conn_err()
    - CLEANUP: tcpcheck: mention that we're a bit far for a sync errno
    - BUG/MINOR: jwt: fix possible memory leak in convert_ecdsa_sig() error path
    - CLEANUP: jwe: fix theoretical overflow in AAD length calculation
    - DOC: config: further clarify that resolvers "default" exists
    - MINOR: proxy: remove the experimental status on dynamic backends
    - BUG/MEDIUM: limits: properly account for global.maxpipes in compute_ideal_maxconn()
    - BUG/MINOR: jws: fix OpenSSL 3.0 version check from > to >=
    - BUG/MINOR: jws: Add missing return value check (EVP_PKEY_get_bn_param)
    - BUG/MINOR: server: Properly handle init-state value during haproxy startup
    - BUG/MINOR: httpclient-cli: Destroy http-client context if failing to start it
    - BUG/MEDIUM: h1: Skip all h2c values from Upgrade headers during parsing
    - BUG/MINOR: h1: Don't mask websocket protocol if multiple protocols used
    - MINOR: haterm: Don't init haterm master pipe if not used
    - CLEANUP: haterm: Remove "(too old kernel)" from warning message during init
    - BUG/MINOR: httpclient-cli: fix uninit variable in error label
    - MINOR: mux: Rename the "token" from mux_proto_list to mux_proto
    - MEDIUM: connections: Use both mux_proto and alpn to pick a mux
    - MINOR: connection: define conn_select_mux_fe()
    - MINOR: connection: define conn_select_mux_be()
    - MINOR: connection/mux_quic: add MUX <init_xprt> field for QMux handshake
    - MINOR: proxy/server: reject TCP ALPN h3 without experimental
    - MEDIUM: ssl: allow h3/QMux negotiation without explicit proto
    - BUG/MINOR: server: accept server IDs above 2^31 and clarify error message
    - BUG/MINOR: backend: fix balance hash calculation when using hash-type none
    - MINOR: server: support hash-key id32 for a cleaner distribution
    - MINOR: backend: support hash-key guid for a stabler distribution
    - MINOR: startup: support unprivileged chroot if possible
    - MEDIUM: startup: add automatic chroot feature
    - MINOR: h2: explain committed_extra_streams dec on h2_init() error
    - OPTIM: h2: do not update committed streams if elasticity disabled
    - MINOR: mux_quic: implement basic committed_extra_streams accounting
    - MINOR: quic: use stream elasticity value for initial advertisement
    - MINOR: mux_quic: define ms_bidi_rel QCC member
    - MAJOR: mux_quic: support stream elasticity during connection lifetime
    - BUG/MEDIUM: servers: Store the connection hash with the parameter cache
    - BUG/MINOR: prevent conn leak in case of xprt_qmux init failure
    - BUILD: traces: set a few __maybe_unused on vars used only for traces
    - BUILD: traces: add USE_TRACE allowing to disable traces
    - MINOR: startup: do not execute chroot() when "/"
    - MEDIUM: startup: warn when chroot is not set for root
    - BUG/MEDIUM: servers: Don't forget to set srv_hash when needed
    - DOC: fix typo on QUIC stream.max-concurrent reference
    - BUG/MINOR: mux_quic: do not exceed stream.max-concurrent on backend side
    - BUG/MINOR: htx: Fix value of HTX_XFER_HDRS_ONLY flag
    - MEDIUM: htx: Improve htx_xfer API to not count HTX meta-data
    - BUG/MEDIUM: applet: Fix transfer of HTX data to the applet
    - BUG/MEDIUM: htx: Alloc a chunk of right size in htx_replace_blk_value()
    - MEDIUM: stick-tables: Avoid freeing elements while holding a lock
    - MINOR: intops: add a multiply overflow detection for ulong and size_t
    - CLEANUP: tree-wide: use array_size_or_fail() in array size for allocations
    - DOC: update supported gcc and openssl versions in INSTALL
2026-05-20 17:46:36 +02:00
Willy Tarreau
897c5ddb8c DOC: update supported gcc and openssl versions in INSTALL
Gcc 16.1 was tested, clang 21 and OpenSSL 4.0. Let's mention this.
2026-05-20 17:45:23 +02:00
Willy Tarreau
f5477c8d45 CLEANUP: tree-wide: use array_size_or_fail() in array size for allocations
Instead of relying on malloc(n*size), we now pass array_size_or_fail(n,m)
so that it becomes possible to detect overflow. This is particularly
interesting for global settings that might be set large enough to cause
overflows on 32-bit systems for example, resulting in small values that
then cause trouble. Now the overflow will be detected at allocation time.
Around 25 locations were updated.
2026-05-20 17:05:19 +02:00
Willy Tarreau
b62ba7592a MINOR: intops: add a multiply overflow detection for ulong and size_t
Sometimes we'd like to know if some products overflow, so let's add a
pair of functions for this, for ulong and for size_t. For recent enough
compilers (gcc >= 5, clang >= 3.4) we just use __builtin_mul_overflow()
otherwise we rely on a division and a comparison before performing the
operation.

A third function, array_size_or_fail() computes the size of an array
of m elements of n bytes each, and returns the total size if it fits
in a size_t, otherwise ~0 if it does not so that passing this to
malloc() or any other variant would fail by trying to exhaust the
entire memory space.
2026-05-20 17:05:19 +02:00
Olivier Houchard
3e25104a9c MEDIUM: stick-tables: Avoid freeing elements while holding a lock
In stksess_trash_oldest(), and process_tables_expire(), avoid freeing
elements while holding two locks, as it could be very costly.
Instead, build a linked list of elements to be free'd, and do so once we
no longer hold any lock.

This may help with github issue #3380, and may be backported to 3.3.
2026-05-20 16:23:30 +02:00
Christopher Faulet
482b6763a3 BUG/MEDIUM: htx: Alloc a chunk of right size in htx_replace_blk_value()
Since support for large buffers was added, we must be careful when chunks
are allocated. Indeed, depending on the context a large chunks may be
required if data are copied from a large buffer.

In htx_replace_blk_value() function, when a defragmentation is necessary,
the data to be replaced are copied to a chunk before the
defragmentation. However, I forgot to get large chunk when necessary by
calling alloc_trash_chunk_sz() instead of alloc_trash_chunk(). Because of
this issue, it is possible to copy data to a too small chunk, leading to a
crash.

So let's fix the issue.

Thanks to Vincent55 for finding and reporting this.

No backport needed.
2026-05-20 16:21:02 +02:00
Christopher Faulet
2a87629052 BUG/MEDIUM: applet: Fix transfer of HTX data to the applet
appctx_htx_snd_buf() function is relying on htx_xfer() function to transfer
HTX blocks when a swap of buffers is not possible. However, it was not
properly using this function.

Indeed, originally htx_xfer() was designed to transfer blocks with a limit,
the <count> parameter, which included the blocks payload and the
meta-data. It was aligned with all calls, except for the transfer of HTX
data to the applet, in appctx_htx_snd_buf() function. In that case, the
<count> parameter is the amount of data forwarded by the stream to the
applet. So meta-data are not included.

Thanks to the previous commit ("MEDIUM: htx: Improve htx_xfer API to not count
HTX meta-data"), it is now possible to instruct htx_xfer() function that
<count> parameter does not include the meta-data.

Because of this bug, crashes can be experienced when transferring HTX data
to an applet. At first glance, lua HTTP applets and the http client are
concerned.

Stable versions from 3.3 to 3.0 are also affected. But this patch cannot be
backported as is because htx_xfer() function does not exist on these
versions.

Thaks to Yon Harlicaj for finding and reporting this.
(https://x.com/nvmb3r - https://www.linkedin.com/in/eljon-harlicaj/)
2026-05-20 16:21:02 +02:00
Christopher Faulet
56e7f8ef31 MEDIUM: htx: Improve htx_xfer API to not count HTX meta-data
This patch add the ability to the htx_xfer() function to transfer data
without acounting the meta-data. By default, the <count> variable includes
the meta-data. But by setting the flag HTX_XFER_NO_METADATA, It is possible
to transfer HTX blocks without count meta-data. In that case, <count> will
not contain the blocks meta-data and the return value will not include them.
2026-05-20 16:21:02 +02:00
Christopher Faulet
99d48c3aec BUG/MINOR: htx: Fix value of HTX_XFER_HDRS_ONLY flag
HTX_XFER_* flags must be declared as a bitfield. However, value of
HTX_XFER_HDRS_ONLY was set of 0x03 while it should be 0x04. So let's fix it.

This patch must be backported where the htx_xfer() function was backported
(5ead611cc "MEDIUM: htx: Add htx_xfer function to replace htx_xfer_blks").
2026-05-20 16:21:02 +02:00
Amaury Denoyelle
47a61eb86d BUG/MINOR: mux_quic: do not exceed stream.max-concurrent on backend side
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
Fix usage of stream.max-concurrent QUIC setting on the backend side.
Contrary to frontend connections, this limit must be enforced by QUIC
MUX directly. This is necessary as the peer may allow a larger number of
concurrent streams via its flow control.

First, QUIC TP initial max bidi streams value is now set to 0. This is
fine as only the HTTP/3 client is expected to open bidirectional
streams.

The most important changes is performed in qcm_avail_streams(). The
value first depends on the peer flow control. Now, it is further reduced
if necessary to not exceed the configured BE stream.max-concurrent.

Note that this new behavior may further increases current limitation on
QUIC BE reuse when a QCS instance is kept while its upper stream layer
is detached. In this case there is a risk that the connection is not
reinserted in the correct server pool, as an idle or avail one.

This is a breaking change as BE stream.max-concurrent keyword setting
meaning is changed in effect. However, this does not necessitate extra
warnings as the previous usage was in effect useless. Furthermore, QUIC
on the backend side is still considered as experimental.

This can be backported up to 3.3.
2026-05-20 14:42:03 +02:00
Amaury Denoyelle
b7c607e207 DOC: fix typo on QUIC stream.max-concurrent reference
Add a missing "fe" prefix for the QUIC keyword reference in
tune.streams-elasticity documentation.
2026-05-20 13:40:53 +02:00
Olivier Houchard
05e65489cb BUG/MEDIUM: servers: Don't forget to set srv_hash when needed
Commit 8aa854ab26a7daa613a17548f1fe1d0adb8cf61b made it so we'd store
the hash corresponding to the server parameters, so that we could detect
if we're still talking to the same server, and not use those parameters
if not.
However, when updating those parameters, we forgot to store the new
hash, which would result in the new parameters never be used, and
breakling 0RTT.
Fix that by properly update the hash when needed.
This should be backported when 8aa854ab26a7daa613a17548f1fe1d0adb8cf61b
is backported.
2026-05-20 12:32:19 +02:00
Willy Tarreau
b9acb4415f MEDIUM: startup: warn when chroot is not set for root
We're still regularly seeing insecure configs where chroot is missing.
Now that we have "chroot auto", there's no excuse for not knowing where
to chroot, so let's detect that we're starting as root, detect that the
process is allowed to chroot (i.e. no capability issue, or some hardened
containers), and if no chroot is set, let's emit a warning explaining how
to silence it, i.e. either "chroot auto" or "chroot /".

Most likely we'll start using "chroot auto" by default in 3.5 if no
usability issue is reported.
2026-05-20 11:51:45 +02:00
Willy Tarreau
3c35e7f137 MINOR: startup: do not execute chroot() when "/"
We'll recommend to use "chroot /" to explicitly disable chroot, however
there might be configurations where it would cause problems to just issue
the syscall (typically some hardened containers), so let's make sure that
"chroot /" is a nop in this case.
2026-05-20 11:46:43 +02:00
Willy Tarreau
d142c7f421 BUILD: traces: add USE_TRACE allowing to disable traces
This reduces the total code size by 6-10% and speeds up the build a
bit. It can be further reduced by disabling the trace decoding code
inside certain subsystems like muxes. But at least like this it will
help users on small systems to reduce the footprint when not needed
by explicitly passing USE_TRACE=0 (they remain enabled by default).
2026-05-20 11:46:43 +02:00
Willy Tarreau
8dd31dcd07 BUILD: traces: set a few __maybe_unused on vars used only for traces
Certain variables are used only for traces in mux, ssl and quic
essentially, and disabling traces emits warnings, so let's mark
them appropriately.
2026-05-20 11:46:43 +02:00
Amaury Denoyelle
f521581922 BUG/MINOR: prevent conn leak in case of xprt_qmux init failure
In case of XPRT_QMUX init failure on the frontend side, the connection
must immediately be released. This is not the case on the backend side
as a stream can supervize the connection lifetime.

This patch performs the connection free via conn_complete_session(). As
conn is flagged with CO_FL_ERROR, this will automatically fail and
invoke session_kill_embryonic(), which ensures the session and its
connection are both freed as wanted in this case.

No need to backport.
2026-05-20 11:13:56 +02:00
Olivier Houchard
de3f245df0 BUG/MEDIUM: servers: Store the connection hash with the parameter cache
When we store the negociated server parameters, such as the ALPN, also
store the calculated hash with the connection. If it is different, as
can happen because the IP address is different because set-dst was used,
we certainly do not want to reuse the information in the cache,
otherwise we could end up using the wrong ALPN and mux.
That means we already have to calculate the hash in connect_server()
now, while before we would not do it for Websockets, if we could not do
connection reuse, as that's all the hash was used for.

This should fix Github issue #3386

This should be backported as far as 3.2.
2026-05-20 10:29:22 +02:00
Amaury Denoyelle
e139dd90e3 MAJOR: mux_quic: support stream elasticity during connection lifetime
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
qcc_release_remote_stream() is called each time a remote stream is
closed. Flow control accounting is updated and when necessary, a
MAX_STREAMS_BIDI frame is prepared to allow the peer to initiate new
streams.

This patch extends stream elasticity features with the QUIC bidirection
stream flow control mechanism. The announced value can now be possibly
reduced depending on conn_calc_max_streams().

The first step is to decrement closed streams from the global committed
extra streams total. This must be performed conn_calc_max_streams() to
ensure the calculation will be valid.

Then, there is two cases depending on conn_calc_max_streams() result. If
the value is less than the peer still remaining stream window, nothing
more is performed. If the opposite case, flow control must be increased
and a MAX_STREAMS_BIDI frame is prepared, with the value adjusted to not
exceed the stream elasticity limit. Global extra streams total is then
finally incremented.

This calcul also ensures that when all streams are closed, global extra
streams accounting operations are decremented by 1, as a connection
always has access to one stream which is excluded from the global total.

Note that if stream elasticity is not active, flow control increases
principle is unchanged and remains statically performed.

This patch is labelled as major as it complexifies bidirectional stream
flow control mechanisme. This is a sensitive operation as there is a
risk of connection freeze if flow control updates are inadvertently
skipped.
2026-05-20 09:52:50 +02:00
Amaury Denoyelle
89f3975acc MINOR: mux_quic: define ms_bidi_rel QCC member
Add a new QCC member <ms_bidi_rel>. This represents the number of
concurrent streams advertised similarly to ms_bidi, but as a relative
value.

This patch does not introduce any functional change. For now,
<ms_bidi_rel> will be equal to <ms_bidi_init>. However, with the
implementation of stream elasticity and dynamic adjustment for
concurrent max-streams-bidi, the former will be required to keep the
last advertised value.
2026-05-20 09:52:50 +02:00
Amaury Denoyelle
d21ec4c707 MINOR: quic: use stream elasticity value for initial advertisement
When stream elasticity is active, the maximum number of concurrent bidi
streams advertised via transport parameters is now reduced depending on
the connection load. This is implemented via conn_calc_max_streams()
which returns the value to use.

This is not applied on listeners with enabled 0-RTT. Indeed, for such
connections, clients are expected to reuse the previously seen transport
parameters. The server on the other hand must not decrease several
values on the newly advertised params, in particular for the maximum
number of concurrent bidi streams. The simplest way to prevent 0-RTT
failure is to not mix stream elasticity with it.

Note that the 0-RTT limitation is only applied for the initial value :
during the connection lifetime, stream elasticity can still be used by
the MUX to dynamically reduce the stream window. This will be
implemented in a future patch.
2026-05-20 09:52:50 +02:00
Amaury Denoyelle
e4adba6e64 MINOR: mux_quic: implement basic committed_extra_streams accounting
Account QUIC frontend connections into committed_extra_streams when
stream elasticity setting is active. This is performed in QCC init and
release functions.

This patch has no impact on QUIC subsystem for now. Connections will
still allow a static number of concurrent streams based on
tune.quic.fe.stream.max-concurrent. However, this has a direct
repercussion on H2 subsystem, as a higher count of QUIC connections will
reduce the concurrent streams allowed there.
2026-05-20 09:52:50 +02:00
Amaury Denoyelle
33c8270903 OPTIM: h2: do not update committed streams if elasticity disabled
When streams-elasticity is enabled in the configuration, H2 mux is
responsible to update the global committed_extra_streams value.

Adjust these operations to ensure they are skipped if streams-elasticity
is disabled, which is the current default. This prevents unnecessary
atomic operations in this case.

No need to backport unless streams-elasticity feature is picked in older
releases.
2026-05-20 09:52:50 +02:00
Amaury Denoyelle
ad3562fea1 MINOR: h2: explain committed_extra_streams dec on h2_init() error
h2_init() is now responsible to increment committed_extra_streams for
new frontend connections, in relation to the newly implemented
stream-elasticity feature. In case of an early error, a mirroring
decrement is executed on fail_stream label.

However, for now this error label can only be selected via BE conns. In
fact, it's not yet possible for h2_init() to fail after the extra
streams increment.

However, the decrement operation is kept to prevent any omissions in
case of future evolutions of h2_init() error path. To prevent reporting
of a possible dead code, add an extra comment which summarizes the
situation.
2026-05-20 09:52:50 +02:00
Maxime Henrion
641fe4f119 MEDIUM: startup: add automatic chroot feature
It is now possible to use "chroot auto" in the configuration. This lets
haproxy create an anonymous (cleaned up after the process terminates)
and read-only directory for chroot. This directory is created in /tmp;
we might want to support creating it in a different directory in the
future, either by respecting $TMPDIR or by allowing an optional
directory after the "auto" keyword.
2026-05-20 08:34:24 +02:00
Maxime Henrion
2d2980408f MINOR: startup: support unprivileged chroot if possible
Try to use unshare(CLONE_NEWUSER) if available so we can have a chroot
as an unprivileged user. This is a Linux-only mechanism.
2026-05-20 08:34:17 +02:00
Willy Tarreau
7004bb3b8c MINOR: backend: support hash-key guid for a stabler distribution
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
When server fleets are constantly updated, using a stable distribution
across a bunch of load balancers can be convenient. The addr and port
already provide a bit of this but for situations were addresses might
differ between sites or change dynamically this does not work. The guid
is perfect for this because by definition it's supposed to designate a
single server and be unique. So when two servers anywhere have the same,
the tool that provisionned them promises that they are the same server.

So here we introduce "hash-key guid" which performs a 32-bit hash on
the GUID value. When no guid is provided, a fallback is performed on
ID, as is done for other keys.
2026-05-19 19:11:25 +02:00
Willy Tarreau
a59e6e5efd MINOR: server: support hash-key id32 for a cleaner distribution
The "id" hash-key scales the ID by a factor of 16 that tries to leave
room between the nodes on the 32-bit space to permit smooth weight
variations (e.g. during slowstart). However this does not deal well
with overlaps between server IDs. For example, assigning IDs that are
only multiples of 256 million to 16 servers yields traffic only on
one since in practice they all have the same 28 lower bits.

The new "id32" hash key bridges this gap by using the full 32-bit ID
of the server as the key. On the other hand, the user must be careful
not to switch the hash function to "none" when using incremental IDs
because in this case they might be very poorly distributed. But this
can be convenient for automated provisionning systems which assign
IDs themselves, as the full 32 bits are used now.
2026-05-19 19:11:25 +02:00
Willy Tarreau
cb5d98c495 BUG/MINOR: backend: fix balance hash calculation when using hash-type none
The "hash-type xxx none" is broken for keys that are not in type string
because the sample fetch call casts them to SMP_T_BIN, that tends to
preserve the original format (integers, IP addresses etc), but the
gen_hash() function in case of BE_LB_HFCN_NONE expects to read a string
representing a number, that it parses to retrieve the value, and just
fails on many binary types. For example, the following just always
returns key 0:

    balance hash rand()
    hash type consistent none

An ugly workaround is to make sure the expression returns a string, for
example this:

    balance hash rand(),concat()
    hash type consistent none

In order to fix most cases here, we force the conversion to type string
when using BE_LB_HFCN_NONE, but a better approach would require a larger
rework and split gen_hash() or change it to accept an integer as well,
so that the caller could cast to SMP_T_INT for BE_LB_HFCN_NONE and pass
the resulting number already parsed with the least information loss. In
this case even IPv4 addresses would be preserved.

The current approach at least addresses the initially envisioned use
cases, and the limitations have been added to the doc. This can be
backported to 3.0 though it's not really important.
2026-05-19 19:11:25 +02:00
Willy Tarreau
f2bf3483ba BUG/MINOR: server: accept server IDs above 2^31 and clarify error message
Due to the check of the stored value instead of the parsed one, it was not
permitted to use server IDs above 2^31 while they are perfectly possible.
Let's refine the parsing and also update the error message to indicate the
range. The doc was also refined to reflect the relation with hash-key.

This may be backported though it wouldn't have any effect on working
configs.
2026-05-19 19:11:25 +02:00
Amaury Denoyelle
f2b152c95e MEDIUM: ssl: allow h3/QMux negotiation without explicit proto
Implements automatic selection of QMux MUX if "h3" ALPN has been
negotiated on top of TCP/SSL.

The first part of this change is to define "alpn" member of
mux_proto_list. This is necessary so that conn_get_best_mux_entry() can
select it when "h3" has been chosen. As a side-effect, this also
automatically sets a default ALPN to "h3" for bind lines with "proto
qmux".

The most important change is to adapt the SSL layer. On handshake
completion, the eligible MUX is retrieved via conn_select_mux_fe/be()
functions. If xprt_qmux is required by it, MUX init is delayed and QMux
handshake is started first.

This last change is necessary as connection flags CO_FL_QMUX_RECV/SEND
are only set if "proto qmux" is explicitely set. In case xprt_qmux is
activated via pure ALPN negotiation, these flags are also set on
xprt_qmux_init(). This is mandatory to ensure emission/reception of QMux
transport parameters will be performed as expected.
2026-05-19 18:40:50 +02:00
Amaury Denoyelle
e30bcfe6cd MINOR: proxy/server: reject TCP ALPN h3 without experimental
Add a postparsing check on TCP ALPN bind and server setting. An error is
reported if the token "h3" is present and expose-experimental-directives
is not globally activated. This ensures that QMux protocol won't be
selected if experimental features are not explicitely requested.

The check is not performed though if "proto qmux" is explicitely
defined, as this setting already checks for experimental support.

Currently, it's not possible to activate QMux without any explicit
"proto qmux" config. However, this will be implemented in a next patch,
so this check will become necessary.
2026-05-19 18:40:50 +02:00
Amaury Denoyelle
879c78c909 MINOR: connection/mux_quic: add MUX <init_xprt> field for QMux handshake
The first part of this patch defines a new mux_proto_list field named
<xprt_init>. This allows to define an extra XPRT layer which should be
activated first prior to the MUX creation both on frontend and backend
sides.

This is immediately used for QMux mux_proto_list to require XPRT_QMUX
handshake. With this change, activation of QMux connection flags in
session_accept_fd() and connect_server() are adjusted to take into
account <init_xprt> field. This approach is much more evolutive than
relying on the previous MUX name.

Change in connect_server() will also be necessary to support QMux
activation on a TCP server with h3 ALPN without explicit "proto qmux".
This guarantees that MUX initialization is delayed after QMux handshake.
2026-05-19 18:40:50 +02:00
Amaury Denoyelle
356f1ab5d7 MINOR: connection: define conn_select_mux_be()
This patch is similar to the previous one but this time for backend
connections. The MUX selection code is directly extracted from
conn_install_mux_chk() and conn_install_mux_be().
2026-05-19 18:40:46 +02:00
Amaury Denoyelle
86ffbaa0f5 MINOR: connection: define conn_select_mux_fe()
Define a new function conn_select_mux_fe().

The objective is to have a preliminary function to determine the MUX
which will be used without initializing it. This will be useful for MUX
which relies on a specific XPRT handshake prior to its startup, which is
the case for QMux protocol.

The code of conn_select_mux_fe() is identical to the beginning of
conn_install_mux_fe() with a similar MUX selection logic. However,
connection MUX initialization is not performed in this case. In a future
patch, both functions should be merged together to reduce code
duplication.
2026-05-19 18:33:54 +02:00
Olivier Houchard
6aab6d4e98 MEDIUM: connections: Use both mux_proto and alpn to pick a mux
In conn_get_best_mux() and conn_get_best_mux_entry(), the mux name was
provided sometimes based on the "proto" directive, sometimes based on
the ALPN, but in any case, it was compared again the mux_proto_list
mux_proto field. This is not correct, as ALPN can be different from the
internal mux_proto. So enhance those functions so that they wll accept
an ALPN as well. If a mux_proto is provided, that will be used, if not,
and if an ALPN is provided, then that will be used, and compared against
the ALPN provided by the mux, if any.
2026-05-19 18:33:54 +02:00
Olivier Houchard
022681eca2 MINOR: mux: Rename the "token" from mux_proto_list to mux_proto
In struct mux_proto_list, rename the "token" field to "mux_proto". That
field should only be used to match the name provided in the "proto"
directive, and it will be soon.
This should be a no-op.
2026-05-19 18:33:54 +02:00
Amaury Denoyelle
50354f929d BUG/MINOR: httpclient-cli: fix uninit variable in error label
The following patch fixes a leak in case of httpclient_start() failure
in the httpclient_cli code by adding httpclient_destroy() call on error
path.

  c53256adbc
  BUG/MINOR: httpclient-cli: Destroy http-client context if failing to start it

However, error label may be selected prior to httpclient allocation if
CLI arguments are incorrect. This can cause a crash due to a deferencing
of an uninitialized variable. This has been detected via a compilation
error :

  src/httpclient_cli.c: In function 'hc_cli_parse':
  src/httpclient_cli.c:162:2: error: 'hc' may be used uninitialized in this function [-Werror=maybe-uninitialized]
    162 |  httpclient_destroy(hc);
        |  ^~~~~~~~~~~~~~~~~~~~~~

This must be backported along the above patch, which is scheduled up to
the 2.6 release.
2026-05-19 18:33:13 +02:00
Christopher Faulet
6f6bf3fecc CLEANUP: haterm: Remove "(too old kernel)" from warning message during init
During initialization of the haterm master pipe, If its size is limited
(lower than the configured one * 5/4), a warning is emitted. In this
warning, it is specified this happened because the kernel is too old. But it
is unrelated. So let's remove this part.
2026-05-19 17:50:50 +02:00
Christopher Faulet
1279bd80e9 MINOR: haterm: Don't init haterm master pipe if not used
There is no reason to initialize the haterm master pipe if haterm is not
used. So now, it is only performed if a non-disabled haterm frontend is
found. To do so, in addition to test the proxy's flags and capabilities, we
also check if "stream_new_from_sc" points on "hstream_new".
2026-05-19 17:50:50 +02:00
Christopher Faulet
b74b5289c8 BUG/MINOR: h1: Don't mask websocket protocol if multiple protocols used
During H1 message parsing, the Upgrade header values are checked to detect
"websocket" prototol, to properly handle websocket upgrades between H1 and
H2 and to possibly reject messages if mandatory headers are missing.

However, the flag is reset for each new Upgrade header and the information
may be lost. So never reset it.

This patch must be backported as far as 2.4.
2026-05-19 17:50:50 +02:00
Christopher Faulet
8dd49dfaba BUG/MEDIUM: h1: Skip all h2c values from Upgrade headers during parsing
During the H1 message parsing, the Upgrade header values are checked to
detect "h2c" and "h2" tokens and skip them. To do so, we rely on
H1_MF_UPG_H2C flag, set during the parsing. And during the request
post-parsing, if this flag was set, all Upgrade headers are removed.

This was fixed by the commit 7b89aa5b1 ("BUG/MINOR: h1: do not forward h2c
upgrade header token").

However, there are two issues here and the commit above must be refined.
First, the flag is reset for each new Upgrade header. So "h2c" or "h2"
tokens will be properly detected if all tokens are set on the same Upgrade
header. But if splitted on several headers, previously detected tokens will
be hidden by a next ones.

Concretly, the following will be properly caught

  Connection: upgrade
  Upgrade: foo, h2c, bar

But then following not:

  Connection: upgrade:
  Upgrade: foo, h2c
  Upgrade: bar

Then, when a "h2c" or "h2" token is finally reported, all Upgrade headers
are removed, regardless other tokens.

So, to fix the both issues, everything is now handled during the message
parsing by skipping "h2c" and "h2" tokens, rebuilding the Upgrade header
value without then offending tokens. The same was already performed for the
Connection header, to skip "keep-alive" and "close" value. So it is not a so
fancy change.

Thanks to this change, it is no longer necessary to handle H1_MF_UPG_H2C
during the request post-parsing. And in fact, this flag is no longer
necessary. So let's remove it too.

Thanks to Vincent55 for finding and reporting this.

This patch must be backported as far as 2.4.
2026-05-19 17:50:50 +02:00
Christopher Faulet
c53256adbc BUG/MINOR: httpclient-cli: Destroy http-client context if failing to start it
When the call to httpclient_start() failed, it is the caller responsibilty
to destroy the http-client context by calling httpclient_destroy(). It is
performed at several places but it was missing in the httpclient_cli
code. So let's fix it.

This patch must be backported as far as 2.6. On 3.2 and lower, it must be
applied on http_client.c.
2026-05-19 17:50:50 +02:00
Christopher Faulet
18c5cd6674 BUG/MINOR: server: Properly handle init-state value during haproxy startup
Unlike stated in the configuration manual, the server 'init-state' parameter
was not evaluated during haproxy startup/reload. After a review, it appeared
there were also issues if combined with the 'track' parameter. In addtition,
this parameter was only evaluated when health-checks were enabled for the
server, leading to unexpected behavior if the serve settings are dynamically
changed via the CLI.

To fix those issues, behavior of the 'init-state' parameter was slightly
adapted. It is always evaluated, even when there is no running health-checks
for the server. An error is reported if the 'track' parameter is also
defined. Both cannot work together.

In addition, the "none" state was introduced to be able to restore the
default behavior. It will be especially useful when the parameter is
inherited from a 'default-server' directive.

This patch should fix the issue #3298. It must be backported as far as 3.2.
2026-05-19 17:50:50 +02:00
Remi Tricot-Le Breton
b786eaf1b1 BUG/MINOR: jws: Add missing return value check (EVP_PKEY_get_bn_param)
Two calls of 'EVP_PKEY_get_bn_param' did not have their return value
checked.

This patch can be backported up to 3.2.
2026-05-19 15:21:26 +02:00
Willy Tarreau
307294b30a BUG/MINOR: jws: fix OpenSSL 3.0 version check from > to >=
Three #if directives used > 0x30000000L which excluded OpenSSL 3.0.0
exactly from the modern code path, treating it as pre-3.0. Changed all
three to >= 0x30000000L to match jwe.c and openssl-compat.h conventions.

This affects EC key thumbprint generation, RSA JWK generation, and
JWS algorithm detection for OpenSSL 3.0.0.
2026-05-19 15:21:24 +02:00
Willy Tarreau
0284be5456 BUG/MEDIUM: limits: properly account for global.maxpipes in compute_ideal_maxconn()
Starting a config with maxpipes and no maxconn always ended up in error
because the number of FDs needed for pipes was not deduced from the total
number of FDs when calculating maxconn, and was later found to exceed the
total number of allocatable FD during final checks.

When global.maxpipes is set, it must be used during compute_ideal_maxconn()
so that it's properly deduced.

Without this, just having "maxpipes 500" in a config prevents it from
starting. With the fix, it properly starts with a maxconn adjusted
depending on the number of splice-enabled proxies.

This should be backported, theoretically everywhere, but preferably
progressively. The following config should fail on affected versions
and load with fixed ones:

   global
        maxpipes 500

   frontend srv1
        bind :8001
2026-05-19 15:19:23 +02:00
Willy Tarreau
11bad01760 MINOR: proxy: remove the experimental status on dynamic backends
As initially planned, if no trouble was reported on dynamic backend
commands on the CLI, the experimental status could be dropped before the
release. The feedback was not very broad, but was conclusive in that the
operations work as expected and the current syntax can be preserved even
for future evolutions. So we can drop the experimental status.
2026-05-19 14:56:45 +02:00
Willy Tarreau
b59fe471a5 DOC: config: further clarify that resolvers "default" exists
It was explained in the general presentation of resolvers but not in
the "resolvers" keyword description itself, which might be where users
could be looking for that info, so let's quickly repeat that info there.
2026-05-19 14:48:27 +02:00
Willy Tarreau
29b9da7821 CLEANUP: jwe: fix theoretical overflow in AAD length calculation
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
The expression items[JWE_ELT_JOSE].length << 3 performs the shift on an
unsigned int (32-bit) before being cast to uint64_t instead of after.
This means that we don't cover for a possible overflow (which would
never happen as it would need a header length beyond 512MB). At least
fixing it will avoid code check reports.
2026-05-18 18:52:28 +02:00
Willy Tarreau
d4a4be6c34 BUG/MINOR: jwt: fix possible memory leak in convert_ecdsa_sig() error path
The allocated ec_R and ec_S were not released in case one of the two
would fail to be allocated/created, and would cause a memory leak. Let's
add the missing BN_free(). This may be backported to 2.4.
2026-05-18 18:50:30 +02:00
Willy Tarreau
bbc41785d9 CLEANUP: tcpcheck: mention that we're a bit far for a sync errno
The collection of errno in tcpcheck_eval_connect() and tcpcheck_main()
is quite far from the production location, and the risk of having a
zero errno is definitely not null. Tests show that this works, so
better not try to fix something not broken, but at least place a
comment there indicating that it's not necessarily super-reliable.
This would need to be revisited the day we finally store errno in
the connection.
2026-05-18 18:47:41 +02:00
Willy Tarreau
3b825d2745 BUG/MINOR: check: properly report errno in chk_report_conn_err()
When in 2.2, with commit c8dc20a825 ("BUG/MINOR: checks: refine which
errno values are really errors."), errno reporting was refined, an
extra check was added before calling retrieve_errno_from_socket(), and
by mistake the test on !errno got inverted so that we only call the
function to retrieve the error from the socket when errno is set!
The first test in the function detects it and returns without changing
anything, so this didn't have much effect, however when errno is not
set (certain call places purposely pass zero so that getsockopt() is
used), this wasn't called so the error wasn't reported. Apparently it
only happened when called from process_chk_conn() after an async
error was detected, so probably just cases where POLLERR is reported,
which remains infrequent.

Let's fix the direction of this flag. It can be backported if needed
but it's unlikely anyone really noticed.
2026-05-18 18:40:37 +02:00
Willy Tarreau
3da2b63274 BUG/MINOR: sock: store the connection error status
When an async connect() fails in sock_conn_check(), it returns an errno
that will not be retrieved later by a subsequent getsockopt(SO_ERROR).
The problem is that this errno is then definitely lost. This is visible
in the 4be_1srv_smtpchk_httpchk_layer47errors regtest that fails on
certain systems (e.g. glibc 2.31 on arm32 running Linux 6.1), where the
connect() error is systematically lost and the "Connection refused" is
never seen in the check status. It also matches a few random reports of
the past indicating that the connection error was sometimes not reported
in the stats page in front of a down server.

Ideally we should store errno in connections as soon as the error is
seen. However this would require significant changes that are not
acceptable yet for 3.4 nor stable releases. A more acceptable fix is to
make use of the extra CO_ER_* flags set by conn_set_errno() as soon as
the error is detected. This will recognize a sufficiently large number
of errors and the check status will report them (here we'll have
"ECONNREFUSED" in the check). Note that on systems where the error is
seen synchronously, we can have "ECONNREFUSED (Connection refused)",
but this is not a problem.

This fix adds the missing conn_set_errno() call to sock_conn_check(),
that is thus sufficient to catch this error. In addition, the two
affected regtests were updated to search for ECONNREFUSED here.

This might be backported to older releases if users request it, but it
is probably not necessary.
2026-05-18 18:16:25 +02:00
Willy Tarreau
fdb569c2ea REGTESTS: quic/issuers_chain_path: do not forget to enable QUIC compat mode
This test is compatible with QUIC_OPENSSL_COMPAT but the "limited-quic"
directive was not set, making it fail on older libs with no QUIC support
despite being declared as compatible.
2026-05-18 18:01:53 +02:00
Willy Tarreau
fd31df765f REGTESTS: do not run quic/tls13_ssl_crt-list_filters in quic openssl compat mode
This test uses the the backend, it fails in QUIC_OPENSSL_COMPAT so let's
disable it in this case, like other similar tests.
2026-05-18 18:01:53 +02:00
Willy Tarreau
b44d60eb42 CLEANUP: stick-table: uniformize the different action_inc_gpc*()
Some checks failed
Contrib / admin/halog/ (push) Has been cancelled
Contrib / dev/flags/ (push) Has been cancelled
Contrib / dev/haring/ (push) Has been cancelled
Contrib / dev/hpack/ (push) Has been cancelled
Contrib / dev/poll/ (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
While action_inc_gpc1() explicitly checks if s->stkctr or sess->stkctr
are set since 2.8 with commit 6c0117168 ("MEDIUM: stick-table: set the
track-sc limit at boottime via tune.stick-counters"), action_inc_gpc0()
and the generic action_inc_gpc() still stuck to the old approach of not
checking them, causing confusion when reviewing the code.

Upon closer inspection, the only case where the pointer may be NULL is
when global.tune.nb_stk_ctr is zero, which happens when the global
section contains "tune.stick-counters 0". However in this case, the
config parser "parse_inc_gpc()" will reject any reference to any stick
counter, so in theory there is no problem.

Regardless, the difference of treatment between sibling functions remains
confusing and the check is cheap, so let's generalize it, it will save a
future reader from the need to inspect stream_new() and session_new().
2026-05-17 23:10:27 +02:00
Willy Tarreau
015933794e BUG/MINOR: session/trace: use distinct flags for SESS_EV_END and _ERR
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
Session traces were brought in 3.1 by commit abb07af67 ("MINOR:
session/trace: enable very minimal session tracing") though there was
an issue, because SESS_EV_END and SESS_EV_ERR have the same value (it's
a copy-paste mistake).

This can be backported to 3.2.
2026-05-16 20:29:40 +02:00
Willy Tarreau
4519906c70 DOC: internal: add a few rules about internal core principles
The new file core-principles.txt quickly enumerates a number of rules
and invariants across the project. These can be used as quick reminders
as well as basic rules for reviews. It's still lacking a lot of info but
should be a good start.
2026-05-16 20:12:32 +02:00
Willy Tarreau
2f88b4bc4b CLEANUP: address a few typos and copy-paste errors in httpclient and dns
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
These are either typos or copy-paste mistakes (mostly mouse-induced
spaces instead of tabs for dns.c).
2026-05-15 18:25:13 +02:00
Willy Tarreau
9ebb00e673 CLEANUP: proxy: fix duplicate declaration of cli_find_frontend in proxy.h
The function cli_find_frontend was declared twice identically at lines 98-99
of include/haproxy/proxy.h. The second declaration should have been for
cli_find_backend, which is defined in src/proxy.c and used in several places
but was missing from the header's exported symbols.

This is a simple copy-paste mistake where line 99 duplicated line 98 verbatim
instead of declaring cli_find_backend.
2026-05-15 18:24:57 +02:00
Willy Tarreau
3460626148 BUG/MINOR: resolvers: fix missing task_idle destruction in resolvers_destroy()
When destroying a stream-based DNS nameserver, task_req and task_rsp
were destroyed but task_idle was missed, causing a task object leak.
This doesn't necessarily have to be backported since it's only upon
exit that it is visible.
2026-05-15 18:19:41 +02:00
Willy Tarreau
6cbcb4f9db BUG/MINOR: resolvers: fix leaked fields on cfg_parse_resolvers() error paths
cfg_parse_resolvers() has many error paths on allocation failure when
parsing "nameserver". These paths handle their own cleanup instead of
centralizing it. The result is that some errors paths leak some fields.
The most complex ones are the strdup() failures which require to check
for stream or dgram to figure what to free. These can be detected via
ASAN on a dummy strdup() allocation failure:

  Indirect leak of 131080 byte(s) in 1 object(s) allocated from:
      #0 0x7f0b7ed1f0ab in malloc (/usr/lib64/libasan.so.8+0x11f0ab)
      #1 0x000000c73e19 in dns_ring_new src/dns_ring.c:59
      #2 0x000000af1848 in dns_dgram_init src/dns.c:480
      #3 0x000000922005 in cfg_parse_resolvers src/resolvers.c:3792
      #4 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #5 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #6 0x000000447e8c in main src/haproxy.c:3474
      #7 0x7f0b7e02ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #8 0x7ffd35f1531c  ([stack]+0x2031c)

  Indirect leak of 304 byte(s) in 1 object(s) allocated from:
      #0 0x7f0b7ed1ea23 in calloc (/usr/lib64/libasan.so.8+0x11ea23)
      #1 0x000000af1681 in dns_dgram_init src/dns.c:468
      #2 0x000000922005 in cfg_parse_resolvers src/resolvers.c:3792
      #3 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #4 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #5 0x000000447e8c in main src/haproxy.c:3474
      #6 0x7f0b7e02ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #7 0x7ffd35f1531c  ([stack]+0x2031c)

  Indirect leak of 104 byte(s) in 1 object(s) allocated from:
      #0 0x7f0b7ed1ea23 in calloc (/usr/lib64/libasan.so.8+0x11ea23)
      #1 0x000000921f83 in cfg_parse_resolvers src/resolvers.c:3772
      #2 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #3 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #4 0x000000447e8c in main src/haproxy.c:3474
      #5 0x7f0b7e02ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #6 0x7ffd35f1531c  ([stack]+0x2031c)

  Indirect leak of 64 byte(s) in 1 object(s) allocated from:
      #0 0x7f0b7ed1f0ab in malloc (/usr/lib64/libasan.so.8+0x11f0ab)
      #1 0x000000c73e09 in dns_ring_new src/dns_ring.c:55
      #2 0x000000af1848 in dns_dgram_init src/dns.c:480
      #3 0x000000922005 in cfg_parse_resolvers src/resolvers.c:3792
      #4 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #5 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #6 0x000000447e8c in main src/haproxy.c:3474
      #7 0x7f0b7e02ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #8 0x7ffd35f1531c  ([stack]+0x2031c)

  Indirect leak of 15 byte(s) in 1 object(s) allocated from:
      #0 0x7f0b7ed18e20 in strdup (/usr/lib64/libasan.so.8+0x118e20)
      #1 0x00000092203b in cfg_parse_resolvers src/resolvers.c:3798
      #2 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #3 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #4 0x000000447e8c in main src/haproxy.c:3474
      #5 0x7f0b7e02ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #6 0x7ffd35f1531c  ([stack]+0x2031c)

This should be completely reworked so that the cleanup is performed in
a central place, as the risk to get it wrong remains high.

This patch does the minimal changes to clean this up. It does not need
to be backported since it only triggers on boot OOM.
2026-05-15 18:07:50 +02:00
Willy Tarreau
677fdfe126 BUG/MINOR: resolvers: fix leaked dgram and dns_ring struct in parse_resolve_conf()
Some strdup() failures in parse_resolve_conf() do not release everything
due to the way the function is built, resulting in leaks on error that are
caught by ASAN:

  Direct leak of 304 byte(s) in 1 object(s) allocated from:
      #0 0x7fe74231ea23 in calloc (/usr/lib64/libasan.so.8+0x11ea23)
      #1 0x000000af1681 in dns_dgram_init src/dns.c:468
      #2 0x00000091cbbf in parse_resolve_conf src/resolvers.c:3559
      #3 0x00000092179e in cfg_parse_resolvers src/resolvers.c:3815
      #4 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #5 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #6 0x000000447e8c in main src/haproxy.c:3474
      #7 0x7fe74162ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #8 0x7ffc0a43e31f  ([stack]+0x2031f)

  Indirect leak of 131080 byte(s) in 1 object(s) allocated from:
      #0 0x7fe74231f0ab in malloc (/usr/lib64/libasan.so.8+0x11f0ab)
      #1 0x000000c73e19 in dns_ring_new src/dns_ring.c:59
      #2 0x000000af1848 in dns_dgram_init src/dns.c:480
      #3 0x00000091cbbf in parse_resolve_conf src/resolvers.c:3559
      #4 0x00000092179e in cfg_parse_resolvers src/resolvers.c:3815
      #5 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #6 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #7 0x000000447e8c in main src/haproxy.c:3474
      #8 0x7fe74162ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #9 0x7ffc0a43e31f  ([stack]+0x2031f)

  Indirect leak of 64 byte(s) in 1 object(s) allocated from:
      #0 0x7fe74231f0ab in malloc (/usr/lib64/libasan.so.8+0x11f0ab)
      #1 0x000000c73e09 in dns_ring_new src/dns_ring.c:55
      #2 0x000000af1848 in dns_dgram_init src/dns.c:480
      #3 0x00000091cbbf in parse_resolve_conf src/resolvers.c:3559
      #4 0x00000092179e in cfg_parse_resolvers src/resolvers.c:3815
      #5 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #6 0x0000009e0a39 in read_cfg src/haproxy.c:1142
      #7 0x000000447e8c in main src/haproxy.c:3474
      #8 0x7fe74162ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #9 0x7ffc0a43e31f  ([stack]+0x2031f)

  SUMMARY: AddressSanitizer: 131448 byte(s) leaked in 3 allocation(s).

Let's free the dgram and the dns ring. This can be backported though it's
not important as it only happens on OOM condition during boot.
2026-05-15 18:00:04 +02:00
Willy Tarreau
b15e9b1b29 BUG/MINOR: resolvers: report the expression error in the do-resolve() action parser
When an expression is used for do-resolve(), an error may be reported.
Unfortunately it was scratched and replaced by the do-resolve() error,
leaving no chance to know exactly what was wrong. Let's report the
contents of the error when available. It will indicate identifiers that
are not found or invalid ranges or types being used.

This can be backported to all versions.
2026-05-15 17:53:00 +02:00
Willy Tarreau
0c8c9b1c2a CLEANUP: resolvers: properly initialize the sample in resolv_action_do_resolve()
The sample used to pass the IP address only had its data, px, sess and
strm fields initialized before being passed to vars_set_by_name(). It
turns out that this latter one doesn't seem to touch ctx, flags nor opt
but nothing guarantees it. Let's at least initialize the fields properly
to avoid passing random garbage.

No backport is needed.
2026-05-15 17:51:58 +02:00
Willy Tarreau
bed842390f BUG/MINOR: proxy: use proxy_drop() in parse_new_proxy() error path
In parse_new_proxy(), when proxy_defproxy_cpy() fails, the error path used
ha_free(&curproxy) to release the partially constructed proxy. However, the
proxy was allocated via alloc_new_proxy() which performs significant setup:
  - setup_new_proxy() inserts it into the proxy_by_name tree (proxy_store_name)
  - It appends to the global proxies list (LIST_APPEND)
  - proxy_take() increments its refcount

Additionally, proxy_defproxy_cpy() may have allocated further resources
(strdup'd strings, compression structures, email alert fields, etc).

Using ha_free() only freed the proxy struct itself, leaving:
  - The proxy still registered in the name tree (dangling pointer)
  - The proxy still linked in the global proxies list
  - All strdup'd strings and other allocations leaked

This is visible with ASAN when causing random allocation errors:

  [NOTICE]   (27033) : haproxy version is 3.4-dev12-b15468-11
  [NOTICE]   (27033) : path to executable is ./haproxy
  [ALERT]    (27033) : config : parsing [/dev/stdin:5015] : proxy 'bk3': failed to duplicate tcpcheck preset-vars
  [ALERT]    (27033) : config : Error(s) found in configuration file : /dev/stdin

  =================================================================
  ==27033==ERROR: LeakSanitizer: detected memory leaks

  Direct leak of 4 byte(s) in 1 object(s) allocated from:
      #0 0x7f113e518e20 in strdup (/usr/lib64/libasan.so.8+0x118e20)
      #1 0x000000955410 in setup_new_proxy src/proxy.c:3178
      #2 0x000000955816 in alloc_new_proxy src/proxy.c:3221
      #3 0x000000956c33 in parse_new_proxy src/proxy.c:3554
      #4 0x000000a24d03 in cfg_parse_listen src/cfgparse-listen.c:495
      #5 0x00000089d33e in parse_cfg src/cfgparse.c:2202
      #6 0x0000009e0bb9 in read_cfg src/haproxy.c:1142
      #7 0x000000447e8c in main src/haproxy.c:3474
      #8 0x7f113d82ad13 in __libc_start_call_main (/lib64/libc.so.6+0x2ad13)
      #9 0x7fff65b4e320  ([stack]+0x20320)

  SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).

The fix replaces ha_free(&curproxy) with proxy_drop(curproxy), which
properly calls deinit_proxy() to release all internal resources, removes
the proxy from trees and lists, decrements the refcount, and frees the
struct.

No backport is needed since proxy_drop() is only in 3.4.
2026-05-15 17:39:25 +02:00
Willy Tarreau
569f1e2f37 BUG/MINOR: dns: fix dangling dgram pointer on dns_dgram_init() failure path
In dns_dgram_init(), the newly created dgram is assigned to the name server
before the ring is attached. In case of errors, e.g. due to too many watchers,
the dgram is released but not removed from ns->dgram. Let's only assign the
pointer on success to avoid this, as it's not needed before. The problem
was introduced in 2.4 with commit c943799c86 ("MEDIUM: resolvers/dns: split
dns.c into dns.c and resolvers.c"), and was possibly there before. The fix
may be backported to all stable versions.
2026-05-15 17:39:25 +02:00
Willy Tarreau
493dc352ad BUG/MINOR: resolvers: fix dangling list pointer in resolvers_new() error paths
The resolver 'r' is appended to the global sec_resolvers list, but upon failure
later, pointers are released but the element remains in the list, corrupting it,
and possibly causing a crash during deinit() when releasing remaining ones.
Adding a LIST_DEL_INIT() on the error unrolling path is sufficient.

Note that the issue will only happen on failure to allocate memory via
strdup() so the risk is low. The bug was introduced in 2.6 by commit
e7f5776800 ("MINOR: resolvers: resolvers_new() create a resolvers with
default values"), so the fix may be backported to several releases, but
does not necessarily have to go that far.
2026-05-15 17:39:25 +02:00
Willy Tarreau
8aa99dfc74 BUG/MEDIUM: server/cli: unlock server lock on failure in cli_parse_set_server
In cli_parse_set_server()'s 'ssl' branch, the server lock is taken,
and not released in case srv_set_ssl() fails, resulting in a dead lock
and a panic the next time an attempt to touch this server is made. The
lock must be released on all error paths.

This was introduced in 3.3 by commit f8f94ffc9 ("BUG/MEDIUM: server:
Use sni as pool connection name for SSL server only") which was marked
for backporting to 3.0, so this must likely be backported that far.
2026-05-15 17:39:25 +02:00
Willy Tarreau
5b468a0820 BUG/MINOR: servers: use proper source of pool_conn_name in srv_settings_cpy()
The condition 'if (srv->pool_conn_name)' was checking the destination
instead of the source 'src->pool_conn_name', meaning the strdup() would
never fire (since newly calloc'd servers start with NULL pool_conn_name),
and the pool_conn_name setting from default-server was silently ignored.

Introduced in 3.2 with commit f0f1816f1 ("MINOR: check: implement
check-pool-conn-name srv keyword") when pool_conn_name support was added
to srv_settings_cpy(). The bug caused any 'pool-conn-name' setting in a
'default-server' line to be lost for all servers inheriting from it.

Note that it's not the first time this function induces such a bug due
to the poor choice of "srv" vs "src" that should be renamed to avoid
keyboard mistakes and visual confusion.

This needs to be backported to 3.2.
2026-05-15 17:39:25 +02:00
Willy Tarreau
6c663a9374 BUG/MINOR: server: better handling of OOM in srv_set_fqdn()
This function may face an OOM on strdup() in the middle of the hostname
or hostname_dn replacement, leaving NULLs in either or both of the server's
fields, which is definitely not good for other call places.

Let's perform a safe replacement instead: we first allocate the new
values, and only if they are successful, then we release the previous
ones and replace them.

It is not necessary to backport this unless the issue is reported (it
was found via code review).
2026-05-15 17:39:25 +02:00
Willy Tarreau
2a43a1306b CLEANUP: dns: fix misleading error messages in dns_stream_init()
All task allocation errors report "memory allocation error initializing
the ring" when the actual failure was task_new_anywhere() returning NULL.
This clearly is a copy-paste. Let's fix the error messages to help when
debugging. Since it's only about allocation failures during init, there
is probably no point in backporting this.
2026-05-15 17:39:25 +02:00
Willy Tarreau
b6bd6f5b9a CLEANUP: proxy: fix tiny mistakes in parse error messages
One is s/keyworld/keyword in the retry-on parser. The other one is a
wrong argument "len" being printed in case of parse error for
"declare capture" instead of the length itself.

These can be backported though they are not important.
2026-05-15 15:46:46 +02:00
Willy Tarreau
ace19fd638 BUG/MEDIUM: dns: fix memory leak of sockaddr in dns_session_init() error path
In dns_session_init(), sockaddr_alloc() allocates 'addr' from the sockaddr
pool, but on failure of appctx_finalize_startup() we jump to the error label
without calling sockaddr_free(&addr), leaking the allocation. Let's add the
missing sockaddr_free() on the error branch.

This must be backported to 2.6.
2026-05-15 15:40:29 +02:00
Willy Tarreau
bb5c18ab74 BUG/MEDIUM: resolvers: fix name compression pointer validation in resolv_read_name()
The original DNS code would only use the 8 lower bits of the compression
offset. This was fixed in 2.0 with commit 2fa66c3b9 ("BUG/MEDIUM: dns:
overflowed dns name start position causing invalid dns error") but it was
not sufficient because the anti-loop check continues to use only 8 of the
14 bits, thus a crafted response where the 8 lower bits pass the check and
the 6 higher should fail it would be accepted. The impacts remains limited
thanks to the bounds check and the recursion limits, but such invalid
responses could still cost a lot to process. Let's compute the 14-bit
offset once for all and use it everywhere.
2026-05-15 15:33:14 +02:00
Willy Tarreau
fefce297ab BUG/MEDIUM: dns: fix long loops in additional records parse on name failure
In resolv_validate_dns_response(), the additional records loop calls
resolv_read_name(). When it returns zero due to a bad response, the main
loop does a "continue" without making the "reader" pointer progress, so it
evaluates the exact same field again and again. Fortunately this is limited
by arcount which is 16 bits, but it means it can still iterate 65535 times
there, allocating and releasing an answer_record at each turn. Let's just
jump to the invalid_resp label that handles the cleaning. There was the
same pattern (without the allocation) with nscount a few lines above BTW.
These can possibly explain some situations where a high CPU usage observed
processing responses.

Seems like these were introduced in 2.2 with commit 37950c8d2
("BUG/MEDIUM: dns: improper parsing of aditional records")

This must be backported to stable versions.
2026-05-15 15:26:49 +02:00
Willy Tarreau
bcb4f9cd4a BUG/MINOR: config/dns: properly fail on duplicate nameserver name detection
In cfg_parse_resolvers(), two duplicate name checks set err_code but lacked
'goto out', allowing execution to fall through and create the duplicate entry.
This would result in new resolvers and nameservers to be created after the
error was displayed, and a leak of the previous one. It's mostly harmless
since we're exiting after such errors. This can be backported if desired.
2026-05-15 15:04:00 +02:00
Willy Tarreau
da4a4976d7 BUG/MINOR: backend: correct parameter value validation in get_server_ph_post()
In the inner while loop that validates each character of a POST parameter
value, the code checks *p via HTTP_IS_TOKEN() and HTTP_IS_LWS() instead
of *end, while the loop condition only advances "end", so only the first
character of each value is validated.

This means spaces or binary data embedded in parameter values after the
first character goes undetected. Fix by replacing both references to *p
with *end to properly scan through all characters as intended.

This bug was introduced in 1.5-dev20 by commit 98634f0c7 ("MEDIUM:
backend: Enhance hash-type directive with an algorithm options") so
the fix must be backported to all versions.
2026-05-15 15:03:16 +02:00
Willy Tarreau
4a499938d0 [RELEASE] Released version 3.4-dev12
Some checks failed
Contrib / admin/halog/ (push) Has been cancelled
Contrib / dev/flags/ (push) Has been cancelled
Contrib / dev/haring/ (push) Has been cancelled
Contrib / dev/hpack/ (push) Has been cancelled
Contrib / dev/poll/ (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
Released version 3.4-dev12 with the following main changes :
    - SCRIPTS: announce-release: add a link to the OpenTelemetry filter
    - BUG/MEDIUM: servers: Only requeue servers if they are up
    - MINOR: tinfo: store the number of committed extra streams in the tgroup
    - MINOR: connection: add a function to calculate elastic streams limit
    - MINOR: mux-h2: consider the elastic streams limit on frontend
    - MINOR: lb: make LB initialization even more declarative
    - BUG/MINOR: cfgparse-listen: do not emit extraneous line in rule order warnings
    - CLEANUP: tree-wide: fix typos in non user-visible comments in 15 files
    - CLEANUP: h1/htx: fix a few typos in warning, debug and trace messages
    - BUG/MINOR: mux-h1: only check h1s if not NULL
    - BUG/MINOR: http-fetch: fix smp_fetch_hdr_ip()'s handling of brackets for IPv6
    - BUG/MINOR: http-fetch: make http_first_req() check for HTTP first
    - BUG/MINOR: http-act: set-status() must check the response message, not the request
    - BUG/MINOR: tools: fix memory leak in env_expand() error path
    - BUG/MINOR: auth: free user groups on error paths in userlist_postinit()
    - BUG/MINOR: uri-auth: avoid leaks on initialization error
    - BUG/MINOR: cache: fix memory leak in parse_cache_rule error path
    - BUG/MINOR: cfgcond: make KQUEUE check for GTUNE_USE_KQUEUE not GTUNE_USE_EPOLL
    - BUG/MINOR: mqtt: connack parser returns MQTT_NEED_MORE_DATA on unknown property
    - BUG/MINOR: mqtt: connect parser uses wrong bit field for TOPIC_ALIAS_MAXIMUM
    - BUG/MINOR: mqtt: connack parser uses wrong bit for SUBSCRIPTION_IDENTIFIERS_AVAILABLE
    - BUG/MINOR: mqtt: fix PUBLISH flags validation that want all bits to be set
    - CLEANUP: http_htx: rename inner 'type' to 'ptype' to avoid variable shadowing
    - CLEANUP: mux-h2: fix minor output debugging format issues
    - CLEANUP: http-rules: fix a few '&' vs '&&' checks for clarity
    - CLEANUP: auth: remove undeclared auth_resolve_groups() from auth.h
    - CLEANUP: cache: remove redundant res_htx assignment in http_cache_io_handler()
    - CLEANUP: channel: remove bogus and unused definition of channel_empty()
    - CLEANUP: flt_http_comp: remove duplicate rate limit and CPU usage checks
    - CLEANUP: mqtt: remove duplicate MQTT_FN_BIT_USER_PROPERTY in CONNECT fields
    - BUG/MINOR: uri-auth: fix possible null-deref in latest fix for leaks
    - BUG/MEDIUM: tasks: Keep the TASK_RUNNING flag until queued
    - CLEANUP: mqtt: fix spelling of shared_subscription_available
    - CLEANUP: regex: pre-initialize error variable in regex_comp() to calm analysis
    - BUILD: compiler: fix redefinition of __nonstring
    - CLEANUP: defaults: adjust MAX_THREADS multiplier number in comment
    - CLEANUP: src/cpuset.c: fix missing return in functions returning int
    - REGTESTS: Use ${tmpdir} instead of hardcoded /tmp/
    - REGTESTS: Don't try to use real nameservers for testcases
    - CLEANUP: tree-wide: fix typos in non user-visible comments in 3 more files
    - MINOR: cli: improve forward compatibility for show fd
    - DOC: management: document the <tgid>/<fd> form of show fd
    - CLEANUP: tree-wide: fix more typos and outdated explanations in comments
    - BUG/MEDIUM: dict: hold read lock while incrementing refcount in dict_insert
    - BUG/MEDIUM: http-client: Only consume input buffer when hc one is empty
    - BUG/MINOR: xprt_qstrm: fix conflicting prototype
    - REORG: mux_quic: use newer qcm prefix for legacy qmux files
    - MINOR: mux_quic: use qcm prefix for mux callbacks
    - MINOR: mux_quic: use qcm prefix for mux functions
    - MINOR: mux_quic: use qcm prefix for traces functions/structs
    - MINOR: mux_quic: rename qstrm files to qmux
    - MINOR: mux_quic: remove qstrm naming in QUIC MUX
    - MINOR: connection: rename QMux related flags
    - MINOR: xprt_qmux: use qmux instead of qstrm naming
    - MINOR: trace: implement source alias
    - MEDIUM: mux_quic: rename qmux traces to qcm
    - MINOR: sample: add a generic reverse converter
    - MINOR: sample: add a reverse_dom converter
    - DOC: proxy-protocol: clarify UDP usage
    - BUILD: 51d.c: cleanup, fix preprocessor ifdefs
    - CLEANUP: tree-wide: fix typos in user-invisible files
    - CLEANUP: htx: Adjust numbering of HTX blocks' types in the description
2026-05-13 17:22:12 +02:00
Egor Shestakov
b08cf94ae2 CLEANUP: htx: Adjust numbering of HTX blocks' types in the description
Support of pseudo-headers was removed as unused, but mention of it
in the description remains and disrupt the numbering in comment, which
can be confusing.
2026-05-13 17:03:48 +02:00
Egor Shestakov
68f6522add CLEANUP: tree-wide: fix typos in user-invisible files
Fix typos and spelling mistakes in sources files and in the BRANCHES.
These mistakes are harmless, no backport needed.
2026-05-13 17:03:48 +02:00
Ilia Shipitsin
61aa17aec8 BUILD: 51d.c: cleanup, fix preprocessor ifdefs
The ifdef spans over function boundaries, making some combinations produce
code that does not build:

addons/51degrees/51d.c:638:1: error: Unmatched '}'. Configuration: ''. [syntaxError]
2026-05-13 17:00:20 +02:00
Kevin Ludwig
6e9b9196bd DOC: proxy-protocol: clarify UDP usage
the proxy protocol spec didn't specify UDP and therefore most
implementations treat it as a TCP connection and re-use the last send
information for a ip/port pair.

This change makes it more clear.
2026-05-13 16:53:58 +02:00
Manu Nicolas
f4edcdf4de MINOR: sample: add a reverse_dom converter
In domain-based routing and policy rules, suffix matching on hostnames is
often easier to express as a prefix match on reversed labels. A dedicated
converter makes this convenient with existing fetches and matchers.

This also has a performance benefit for large maps. Prefix string matches use
the prefix-tree index (PAT_MATCH_BEG with pat_idx_tree_pfx), while end matches
use the string-list index (PAT_MATCH_END with pat_idx_list_str), so
reversed-label lookups can avoid linear suffix scans.

This patch adds "reverse_dom", a string converter that reverses domain labels,
ignores one optional trailing dot on input, and rejects empty labels. It
intentionally leaves trailing-dot handling to the caller so configurations can
choose between exact matches, subdomain-only matches, or an explicit dotted
form built with "concat(.)" for prefix lookups.

Examples:
  example.com      -> com.example
  mail.example.com -> com.example.mail

The documentation is updated and a reg-test covers the converter itself, the
explicit dotted form for "map_beg()", and the subdomain-only "-m beg" case.
2026-05-13 16:49:53 +02:00
Manu Nicolas
f3fc68e3a2 MINOR: sample: add a generic reverse converter
Some use cases benefit from reversing a string before passing it to other
converters or lookups. While reverse_dom addresses domain-specific label
reversal, a generic byte-wise string reversal remains useful on its own and can
also be combined with other converters such as concat().

A common lookup use case is turning a suffix match on the original string into
a prefix match on the reversed string. Prefix string matches use the
prefix-tree index (PAT_MATCH_BEG with pat_idx_tree_pfx), while end matches use
the string-list index (PAT_MATCH_END with pat_idx_list_str), so reversing
before map_beg can avoid linear suffix scans for large maps.

This patch adds a new string converter named "reverse". It reverses the input
string byte by byte and returns the resulting string unchanged otherwise. It
does not apply any domain-specific semantics or character-encoding semantics.

The documentation is updated and a reg-test is added to cover the basic
conversion as well as a simple composition with concat(.).
2026-05-13 16:45:25 +02:00
Amaury Denoyelle
c090e51502 MEDIUM: mux_quic: rename qmux traces to qcm
This patch is part of a renaming affecting mux_quic and related files.
Naming "qcm" will become the new marker for common mux_quic stuffs,
shared both on QUIC and QMux protocols. The final objective is to limit
"qmux" naming for stuffs related to the new experimental protocol of the
same name.

The current patch renames mux_quic traces token to "qcm". This is the
only change with external visible impacts in the whole renaming series.
However, to preserve compatibility, trace source alias is defined with
the older name "qmux". This relies on the previous patch which
introduced "alias" new trace_source member.
2026-05-13 16:23:58 +02:00
Amaury Denoyelle
8e1b46f8d7 MINOR: trace: implement source alias
Add a new "alias" member in trace_source structure. Its purpose is to be
an alternative to the member "name". This will be used in the next patch
to allow renaming of QUIC mux traces while preserving compatibility.

This new member is only used in trace_find_source() which is the helper
used to retrieve a trace source from its name.
2026-05-13 16:23:58 +02:00
Amaury Denoyelle
19a3c29d3c MINOR: xprt_qmux: use qmux instead of qstrm naming
This is a follow-up on the QUIC MUX renaming process.

The current patch performs renaming in xprt_qmux layer. Older "qstrm"
identifier is replaced by the new name "qmux". Every remaining functions
and structures in xprt_qmux are changed. Outside effects are only
present in QUIC MUX which directly uses some of these functions.
2026-05-13 16:23:58 +02:00
Amaury Denoyelle
1bb879cb3f MINOR: connection: rename QMux related flags
This is a follow-up on the QUIC MUX renaming process.

The current patch performs renaming of "qstrm" to "qmux" in connection
flags. These flags are only used in linked with the xprt_qmux layer.
This has an impact on every files which manipulates these flags, namely
backend, session and ssl_sock sources.

Also, internal xprt identifier is renamed from XPRT_QSTRM to XPRT_QMUX,
2026-05-13 16:23:58 +02:00
Amaury Denoyelle
96b72fd461 MINOR: mux_quic: remove qstrm naming in QUIC MUX
This is a follow-up on the QUIC MUX renaming process.

The current patch replaces "qstrm" naming with "qmux" in QUIC MUX source
file. Some members are impacted in qcc and qcs structures, as well as
some internal functions used for QMux receive/send. Internal mux_ops is
also rename to qmux_ops. This is not a breaking change as its externally
visible name was already set to "qmux" originally.
2026-05-13 16:22:43 +02:00
Amaury Denoyelle
57ab169747 MINOR: mux_quic: rename qstrm files to qmux
This is a follow-up on the QUIC MUX renaming process. Now most of "qmux"
generic usages as been replaced in favor of "qcm" naming. The next part
of the renaming is to replace "qstrm" naming with "qmux" for stuffs
related to the new QMux protocol specifically.

This is first applied on filenames. As with the previous renaming,
Makefile and include statements are updated as well to prevent
compilation issues.
2026-05-13 16:15:48 +02:00
Amaury Denoyelle
e68d4c9c36 MINOR: mux_quic: use qcm prefix for traces functions/structs
This is a follow-up on the QUIC MUX renaming process.

This patch renames several definitions in QUIC MUX traces source code.
This is only an internal change. Trace source name is kept to "qmux" for
now, but will be changed in a dedicated patch.
2026-05-13 16:15:48 +02:00
Amaury Denoyelle
1a9ab14efc MINOR: mux_quic: use qcm prefix for mux functions
This is a follow-up on the QUIC MUX renaming process.

A previous patch already renames MUX ops callbacks. The current patch
proceed to a similar operation for the rest of the MUX functions.
2026-05-13 16:15:47 +02:00
Amaury Denoyelle
545351f2c1 MINOR: mux_quic: use qcm prefix for mux callbacks
This is a follow-up on the QUIC MUX renaming process.

The current patch renames all MUX functions used as stream ops
callbacks. Also, internally defined mux_ops is also renamed from
"qmux_ops" to "quic_ops". There is no breaking change as mux_ops name
field remain set to "QUIC".
2026-05-13 16:13:46 +02:00
Amaury Denoyelle
af3560fa0a REORG: mux_quic: use newer qcm prefix for legacy qmux files
This patch is the first one of the renaming serie, affecting the QUIC
MUX module. The objective is to remove older "qmux" naming which was
used as a generic identifier. Now it should be restricted to the QMux
experimental protocol. A new "qcm" naming will replace the generic
usage.

The current patch renames the files themselves. Token "qmux" is replaced
by the new "qcm" identifier. Makefile and include statements are
adjusted as required.
2026-05-13 16:11:50 +02:00
Amaury Denoyelle
7e2f0fa178 BUG/MINOR: xprt_qstrm: fix conflicting prototype
This patch adds the missing include of xprt_qstrm header into its
companion source file. This helped to detect an incoherence in the
xprt_qstrm_xfer_rxbuf() prototype which is now fixed.

Header files is also updated with mandatory include statements and
forward declaration.

No backport needed.
2026-05-13 16:11:50 +02:00
Christopher Faulet
b24260ec94 BUG/MEDIUM: http-client: Only consume input buffer when hc one is empty
Since http-client applet uses its own buffers, it is possible to have data
stuck in the applet input buffer while the http-client response buffer is
full, preventing the applet to consume these data. If this happens on the
last part of the response payload, the upper stream can decide to shut the
applet. In this case, the applet using the http client will not be able to
retrieve these last data because they will never be move into the hc
response buffer.

The main reason for this bug is that, for now, the applets cannot survive
the upper stream unlike multiplexers. It could be a good improvement for the
3.5. However, some applets still uses the stream-connector and the upper
stream (peer and stat applets for instance). So it is not an easy task.

In the mean time, to fix the issue on stable branches, the http-client
applet now stops to consume data when the hc response buffer is not empty.
This way, the applet shut will be deferred. Data will be consumed when they
can be fully moved in the httpclient response buffer.

This patch should fix the issue #3366. It must be backported to 3.3.
2026-05-13 16:08:43 +02:00
Willy Tarreau
de6a26e3c8 BUG/MEDIUM: dict: hold read lock while incrementing refcount in dict_insert
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
In dict_insert(), the read lock on d->rwlock was released before
incrementing the entry's refcount. Between the RDUNLOCK and the
HA_ATOMIC_INC, another thread could call dict_entry_unref() to drop
the refcount to zero, acquire the write lock, delete the entry from
the tree, and free it. The subsequent HA_ATOMIC_INC would then be a
use-after-free on freed memory.

The fix moves the HA_ATOMIC_INC inside the read lock, matching the
pattern used in stick_table.c for identical refcount-then-unlock
sequences.

It can be backported to the branches where this is relevant.
2026-05-13 13:37:53 +02:00
Willy Tarreau
31a3e16e16 CLEANUP: tree-wide: fix more typos and outdated explanations in comments
Some outdated comments, as well as typos were fixed in the following files:

  dgram.h protocol.h queue-t.h cpu_topo.c debug.c dict.c
  protocol.c queue.c raw_sock.c trace.c wdt.c
2026-05-13 11:24:27 +02:00
Maxime Henrion
a9f38c19b4 DOC: management: document the <tgid>/<fd> form of show fd
Add the syntax description, including the wildcard forms and the
note that <tgid> is currently parsed but ignored pending future
support for per-thread-group fd tables.
2026-05-13 10:33:20 +02:00
Maxime Henrion
4f9b8574d2 MINOR: cli: improve forward compatibility for show fd
The "<tgid>/" and "/" wildcard forms previously produced no output.
This isn't a bug since they are new, but a script written for future
versions (where the slash form will gain per-thread-group semantics)
would not work the same on 3.4. Make them produce output by dropping
the redundant ctx->fd = -1 wildcard sentinel; also tighten tgid
validation to reject values <= 0.
2026-05-13 10:33:20 +02:00
Willy Tarreau
f9e9ab8c90 CLEANUP: tree-wide: fix typos in non user-visible comments in 3 more files
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
This fixes typos and spelling mistakes in the following files:

xprt_quic.c, buf.c, dynbuf.h.
2026-05-12 17:07:55 +02:00
Christian Ruppert
ae614e24c3 REGTESTS: Don't try to use real nameservers for testcases
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
The test doesn't need a real nameserver and in a isolated, restricted
test environment it might not be able to reach one at all, like with a
network sandbox. So lets just use 127.0.0.1:53. Even if there is none,
that's not a problem for this particular test.

Signed-off-by: Christian Ruppert <idl0r@qasl.de>
2026-05-12 09:03:02 +02:00
Christian Ruppert
80fd275773 REGTESTS: Use ${tmpdir} instead of hardcoded /tmp/
Tests may be excuted in sandboxed or minimalistic / restricted
environments, so incosistencies might cause trouble, like missing
permissions. So lets use the tmpdir variable instead, so the user might
define some path

Signed-off-by: Christian Ruppert <idl0r@qasl.de>
2026-05-12 09:03:02 +02:00
Ilia Shipitsin
d9a7ff9b6c CLEANUP: src/cpuset.c: fix missing return in functions returning int
Cppcheck found the issue described in github #2124, which can cause these
errors if no CPUSET implementation is supported (and CPUSET_USE_ULONG is
not enabled):

src/cpuset.c:21:11: error: Found an exit path from function with non-void return type that has missing return statement [missingReturn]
src/cpuset.c:36:11: error: Found an exit path from function with non-void return type that has missing return statement [missingReturn]
src/cpuset.c💯1: error: Found an exit path from function with non-void return type that has missing return statement [missingReturn]
src/cpuset.c:124:1: error: Found an exit path from function with non-void return type that has missing return statement [missingReturn]
src/cpuset.c:152:1: error: Found an exit path from function with non-void return type that has missing return statement [missingReturn]
src/cpuset.c:163:1: error: Found an exit path from function with non-void return type that has missing return statement [missingReturn]

This can be backported.
2026-05-12 08:55:19 +02:00
Egor Shestakov
e0144843a4 CLEANUP: defaults: adjust MAX_THREADS multiplier number in comment
After e049bd00ab the MAX_THREADS limit was increased, but the comment about
multiplier wasn't changed.
2026-05-12 08:50:29 +02:00
Willy Tarreau
58f3e191e8 BUILD: compiler: fix redefinition of __nonstring
Dmitry Sivachenko reported a build warning on FreeBSD -dev, where
__nonstring is apparently already defined. Let's guard our own
definition to avoid such issues. It could make sense to backport
this to recent stable versions which may soon be exposed to modern
compilers.
2026-05-12 08:40:32 +02:00
Willy Tarreau
648b5b6e50 CLEANUP: regex: pre-initialize error variable in regex_comp() to calm analysis
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
In regex_comp(), the error variable is either a const char* (USE_PCRE)
or a a uchar[] (USE_PCRE2), and navigating through the ifdefs is quite a
mess, making it hard to figure if it's always properly initialized when
printing an error message. Let's just preset it to NULL to clarify what
comes from where.
2026-05-11 17:29:56 +02:00
Willy Tarreau
57c3e4b4e2 CLEANUP: mqtt: fix spelling of shared_subscription_available
The struct member 'shared_subsription_available' was misspelled (missing
'c' in 'subscription'). Let's fix it to ease maintenance.
2026-05-11 17:28:21 +02:00
Olivier Houchard
82d723dd8e BUG/MEDIUM: tasks: Keep the TASK_RUNNING flag until queued
In task_schedule(), it is not enough to get the TASK_RUNNING flag before
setting the expire field, we also have to keep it while queueing the
taks, otherwise the task may run in the meanwhile and set expire to 0,
triggering the BUG_ON() in __task_queue() again. So now, only drop the
running flag once it's done.

This should be backported up to 2.8.
2026-05-11 16:17:40 +02:00
Willy Tarreau
aa2c7034e1 BUG/MINOR: uri-auth: fix possible null-deref in latest fix for leaks
Latest commit 2dfbc311a8 ("BUG/MINOR: uri-auth: avoid leaks on
initialization error") left a possible null-deref case which was
surprisingly only detected by certain compiler combinations. No
backport needed.
2026-05-11 16:33:44 +02:00
Willy Tarreau
241cfb2483 CLEANUP: mqtt: remove duplicate MQTT_FN_BIT_USER_PROPERTY in CONNECT fields
The mqtt_fields_per_packet[MQTT_CPT_CONNECT] entry listed MQTT_FN_BIT_USER_PROPERTY
twice due to a copy-paste issue.
2026-05-11 16:04:19 +02:00
Willy Tarreau
e32cc2e805 CLEANUP: flt_http_comp: remove duplicate rate limit and CPU usage checks
In comp_prepare_compress_request(), the compression rate limit and CPU
usage checks were duplicated. The first set runs before selecting the
algorithm, and the second set runs after. That's definitely a copy-paste
issue or a patch being applied twice. Let's just drop one.
2026-05-11 16:04:19 +02:00
Willy Tarreau
4eb6e8daa3 CLEANUP: channel: remove bogus and unused definition of channel_empty()
The function was mistakenly checking chn->flags instead of
chn_strm(chn)->flags, and is not used. Better drop it before someone
attempts to use it.
2026-05-11 16:04:19 +02:00
Willy Tarreau
827defccda CLEANUP: cache: remove redundant res_htx assignment in http_cache_io_handler()
It's probably a leftover of an old check, res_htx is assigned twice the
same way. Let's just drop one.
2026-05-11 16:04:19 +02:00
Willy Tarreau
adb9a5f82f CLEANUP: auth: remove undeclared auth_resolve_groups() from auth.h
The function auth_resolve_groups() is declared in auth.h but has no
definition anywhere in the codebase anymore, let's just drop it.
2026-05-11 16:04:19 +02:00
Willy Tarreau
e4e614022b CLEANUP: http-rules: fix a few '&' vs '&&' checks for clarity
In http_re{q,s}_get_intercept_rule(), there are two occurrences of '&'
being used instead of '&&' which fortunately work thanks to the tests
being negations (hence 0/1 on each branch). Let's fix that and take this
opportunity for adding explicit precedence in http_apply_redirect_rule().
2026-05-11 16:04:19 +02:00
Willy Tarreau
e9cc913e3c CLEANUP: mux-h2: fix minor output debugging format issues
In h2_dump_h2s_info(), the tl.calls was being printed as signed instead
of unsigned, which is not correct but harmless (only used with "show
fd"). In the same function, we don't check if h2s->sd is valid while
dereferencing it. In practise it is valid since "show fd" is run under
thread isolation, but it's far from being obvious, and if conditions
would later change, we don't know it could be printed between h2s_new()
and h2s_frt_stream_new(). Finally in h2s_make_data() a wrong set of
H2_EV_RX_* flags were used instead of H2_EV_TX_* to emit traces.
2026-05-11 16:04:19 +02:00
Willy Tarreau
fa9cefd277 CLEANUP: http_htx: rename inner 'type' to 'ptype' to avoid variable shadowing
In http_add_header() there are "type" variables of the same type at two
levels, which is a bit confusing. The inner one is for the "prev" block,
so let's rename it "ptype" by analogy with "pblk".
2026-05-11 16:04:19 +02:00
Willy Tarreau
be2851f304 BUG/MINOR: mqtt: fix PUBLISH flags validation that want all bits to be set
The definition of the PUBLISH message type indicates that the LSB are
independent, but uses a value of 0xF that clearly shows an attempt to
use a mask instead, but it results in all messages not having all flags
set to be rejected. A sane approach would have been to check for a mask
and an expected value. Let's just add a special case for it in function
mqtt_read_fixed_hdr() since that's for a single message type.

This can be backported anywhere.
2026-05-11 16:04:19 +02:00
Willy Tarreau
e2ab156fa2 BUG/MINOR: mqtt: connack parser uses wrong bit for SUBSCRIPTION_IDENTIFIERS_AVAILABLE
In mqtt_parse_connack(), the MQTT_PROP_SUBSCRIPTION_IDENTIFIERS_AVAILABLE
case was checking and setting MQTT_FN_BIT_SUBSCRIPTION_IDENTIFIER instead
of MQTT_FN_BIT_SUBSCRIPTION_IDENTIFIERS_AVAILABLE, due to a copy-paste
mistake. This can be backported where needed.
2026-05-11 16:04:19 +02:00
Willy Tarreau
57878f3b5c BUG/MINOR: mqtt: connect parser uses wrong bit field for TOPIC_ALIAS_MAXIMUM
In mqtt_parse_connect(), the MQTT_PROP_TOPIC_ALIAS_MAXIMUM case was checking
and setting MQTT_FN_BIT_TOPIC_ALIAS instead of MQTT_FN_BIT_TOPIC_ALIAS_MAXIMUM.
This means duplicate detection for Topic-Alias-Maximum property was using the
wrong bitmask, and the actual Topic-Alias-Maximum bit was never set, making
duplicate detection ineffective for this property. The CONNACK parser already
had this correct.
2026-05-11 16:04:19 +02:00
Willy Tarreau
448cc829e5 BUG/MINOR: mqtt: connack parser returns MQTT_NEED_MORE_DATA on unknown property
In mqtt_parse_connack(), the switch statement's default case for unknown
MQTT properties was using 'return 0' which returns MQTT_NEED_MORE_DATA.
This is misleading: an unknown property should be treated as an invalid
message (MQTT_INVALID_MESSAGE), like other functions do. This branches to
the "end" label without touching the preset return value instead. This can
be backported if needed.
2026-05-11 16:04:19 +02:00
Willy Tarreau
128c654aac BUG/MINOR: cfgcond: make KQUEUE check for GTUNE_USE_KQUEUE not GTUNE_USE_EPOLL
In cfg_eval_cond_enabled(), the "KQUEUE" option incorrectly checks
GTUNE_USE_EPOLL instead of GTUNE_USE_KQUEUE. This is a copy-paste bug
from the preceding EPOLL case. It can be backported though it's harmless.
2026-05-11 16:04:19 +02:00
Willy Tarreau
5830cf3ccf BUG/MINOR: cache: fix memory leak in parse_cache_rule error path
When the filter config (fconf) allocation fails in parse_cache_rule,
the previously allocated cache_flt_conf (cconf) and its strdup'd name
string are not freed. The error path only freed cconf but not
cconf->c.name, causing a memory leak.

No backport is needed.
2026-05-11 16:04:19 +02:00
Willy Tarreau
2dfbc311a8 BUG/MINOR: uri-auth: avoid leaks on initialization error
When stats_add_scope() and stats_add_auth() fail to initialize a field,
they just leave a partially allocated and initialized structure behind
them that is leaked. The whole architecture doesn't provide clean
unrolling abilities since everything is shared and assigned unconditionally,
but let's at least release what was just allocated. The whole approach would
probably deserve being revisited if one day this becomes more dynamic.

No backport needed.
2026-05-11 16:04:19 +02:00
Willy Tarreau
fdfecc5589 BUG/MINOR: auth: free user groups on error paths in userlist_postinit()
In userlist_postinit(), when an error occurs (missing group, missing user, or
allocation failure), the function returned immediately without freeing the
auth_groups_list linked lists that were built for all users in the first loop.
Each user's curuser->u.groups pointed to these allocated nodes, which leaked
on every error path.

Fix by replacing direct returns with a goto to a centralized cleanup label
that frees all users' groups lists before returning the error. Also fix a
trailing double space in one error return statement while refactoring.

Note that the impact is very low since we're supposed to fail to boo after
such errors.
2026-05-11 16:04:19 +02:00
Willy Tarreau
0995c914bd BUG/MINOR: tools: fix memory leak in env_expand() error path
When my_realloc2() fails in env_expand(), the code jumps to 'leave:' and
returns NULL, but the original input 'in' is never freed (it's only freed
at line 4919 in the success case). Given that callers typically pass it
the direct return of strdup(), it looks like it is expected to always be
freed. This can be backported everywhere.
2026-05-11 16:04:19 +02:00
Willy Tarreau
cbdbc96e36 BUG/MINOR: http-act: set-status() must check the response message, not the request
action_http_set_status() checks for soft rewrite on the request message
by mistake instead of the response message. This could possibly cause a
rewrite failure when soft rewrite is enabled since it will not be seen
there, though the impact is extremely low. It can be backported.
2026-05-11 16:04:19 +02:00
Willy Tarreau
8941cc5f6d BUG/MINOR: http-fetch: make http_first_req() check for HTTP first
smp_fetch_http_first_req() reads ->txn.http->flags without first
checking if txn.http is properly allocated. In theory if called from
the wrong context it could crash, even though tests where it's called
from "tcp-request content" don't seem to have any effect. Let's fix
it regardless, at least to dissipate the doubt. It can be backported
everywhere.
2026-05-11 16:04:19 +02:00
Willy Tarreau
15c5226bd3 BUG/MINOR: http-fetch: fix smp_fetch_hdr_ip()'s handling of brackets for IPv6
IPv6 addresses can be read enclosed in brackets, but the length of the
string is not checked before checking them. If by lack of luck, the
buffer is empty but already contains '[' in the first place, we'd read
the byte at position -1, possibly crashing (even though in practice it
will not since allocated blocks will be precedeed by the malloc meta-
data). At least it could make asan/valgrind unhappy.

This can be backported to all versions.
2026-05-11 16:04:19 +02:00
Willy Tarreau
009c32d863 BUG/MINOR: mux-h1: only check h1s if not NULL
Since we can emit glitches during an H2 upgrade, we no longer have a
guaranteed h1s, so _h1_report_glitch() must check h1s before
dereferencing it. No backport is needed as this arrived in 3.4-dev11
with commit 72fd357814 ("MEDIUM: mux-h1: Return an error on h2 upgrade
attempts if not allowed").
2026-05-11 16:04:19 +02:00
Willy Tarreau
d7f8a25db1 CLEANUP: h1/htx: fix a few typos in warning, debug and trace messages
Just a few minor user visible issues issues found in mux_h1 and http_htx
(traces, warnings and debug output). This may be backported though isn't
important at all.
2026-05-11 16:02:16 +02:00
Willy Tarreau
af067e17fb CLEANUP: tree-wide: fix typos in non user-visible comments in 15 files
This fixes typos and spelling mistakes in the following files:

  channel-t.h channel.h filters-t.h http_htx.h htx-t.h tools.h
  cfgcond.c channel.c flt_http_comp.c http_ana.c htx.c mqtt.c
  mux_h1.c regex.c stats-proxy.c
2026-05-11 16:01:50 +02:00
Willy Tarreau
3df1fbc6b9 BUG/MINOR: cfgparse-listen: do not emit extraneous line in rule order warnings
Some checks are pending
Contrib / admin/halog/ (push) Waiting to run
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
Windows / Windows, gcc, all features (push) Waiting to run
Some functions such as tcp_parse_tcp_req() are able to emit their own
warnings by relying on warnif_misplaced_*() which directly prints the
warning. However when doing so they still increment the warning counter
which makes cfg_parse_listen() try to emit it, except that what's in the
variable is NULL, so we end up with:

  [WARNING]  (260) : config : parsing [/etc/haproxy/haproxy.cfg:17] : (null)

Let's just check the errmsg variable before printing the error. If it's
NULL, it's because the message was already printed.

This can be backported to all branches.
2026-05-11 09:32:41 +02:00
Maxime Henrion
87a4f6d47e MINOR: lb: make LB initialization even more declarative
This lets lb_ops specify the conditions necessary to bind to this set of
ops. The condition is expressed as a list of mask and match fields on
the algorithm flags. This is then used in proxy_finalize() to locate the
lb_ops corresponding to the current configuration, by iterating  over
the list of lb_ops structures. This list is implemented using the same
mechanisms used for configuration keywords: an INITCALL1 macro to a
registration function.

This also moves the lookup and property flags into the lb_ops structure
that were previously applied manually on a case by case basis.
2026-05-11 08:50:40 +02:00
Willy Tarreau
731fc033dd MINOR: mux-h2: consider the elastic streams limit on frontend
Now the streams-elasticity limit applies to h2 frontend connections.
It allows to reduce the number of advertised streams based on the
number of concurrent connections.
2026-05-10 14:36:08 +02:00
Willy Tarreau
dd36c84a7b MINOR: connection: add a function to calculate elastic streams limit
This adds a new tune.streams-elasticity parameter. This parameter
indicates, as a percentage, the average number of streams per connection
at full load. It is used to calculate limits of the number of streams to
advertise on new connections. 0 means that no such limit is set.

When a limit is set, the new function conn_calc_max_streams() determines
the optimal number of streams to allow on a connection. It will assign at
least the ratio of streams left to connections left, and at least a fair
share of what's left times the number of desired streams. It will always
ensure that each connection gets at least 1 stream, and everything beyond
this will be evenly distributed. For now the function is not used.
2026-05-10 14:36:08 +02:00
Willy Tarreau
7f17512d18 MINOR: tinfo: store the number of committed extra streams in the tgroup
In order to be able to enforce global streams limitations, we'll first
have to be able to account how many streams we promised to serve via
frontend muxes. We'll always need to support at least one stream, which
is why here we're only counting extra streams beyond the first one. It
also has the benefit of leaving H1 out of this, and save it from updating
a variable. Also in order to avoid an important update cost, we're storing
this value per thread group. For now only H2 is implemented, but QUIC
should follow shortly and should only count bidirectional streams.
2026-05-10 14:36:08 +02:00
Olivier Houchard
2a1599297b BUG/MEDIUM: servers: Only requeue servers if they are up
In init_srv_requeue(), only attempt to run the tasklet if the server is
actually running, otherwise it will end up being queued a second time,
when the server is actually brought up, and that will lead to a
corrupted mt_list.
This can easily be reproduced by adding a dynamic server, as those start
disabled, and then enabling and disabling it a couple of times.
This should fix github issue #3360.

This should be backported up to 3.2.
2026-05-09 19:06:10 +02:00
Willy Tarreau
efb36c0daf SCRIPTS: announce-release: add a link to the OpenTelemetry filter
It moved to its own repository, but we forgot to add the link, and
the build instructions are there.
2026-05-08 12:05:09 +02:00
Willy Tarreau
5d26fe6082 [RELEASE] Released version 3.4-dev11
Released version 3.4-dev11 with the following main changes :
    - BUG/MEDIUM: acme: fix segfault on newOrder with empty authorizations
    - BUG/MINOR: acme: skip auth/challenge steps when newOrder returns a certificate
    - BUG/MINOR: sink: do not free existing sinks on allocation error
    - CLEANUP: net_helper: fix incorrect const pointers in writev_n16()
    - BUG/MINOR: vars: make parse_store() return error on var_set() failure
    - BUG/MINOR: vars: don't store the variable twice with set-var-fmt
    - BUG/MINOR: vars: only print first invalid char in fill_desc()
    - BUG/MINOR: hpack: validate idx > 0 in hpack_valid_idx()
    - MINOR: add an MPSC ring buffer implementation
    - OPTIM: quic: rework the QUIC RX code
    - MINOR: quic: store the DCID as an offset
    - OPTIM: quic: reduce the size of struct quic_dgram
    - BUG/MINOR: quic: handle cases where we don't have an address
    - BUG/MEDIUM: cli: fix master CLI connection slot leak on client disconnect
    - MEDIUM: mux-quic: extend shut to app proto layer
    - MINOR: h3/hq_interop: implement stream reset on shut abort/kill-conn
    - BUG/MINOR: acl: fix a possible arg corruption in smp_fetch_acl_parse()
    - BUG/MINOR: map: do not leak a map descriptor on load error
    - CLEANUP: map/cli: fix some map-related help messages
    - BUG/MINOR: pattern: release the reference on failure to load from file
    - CLEANUP: acl: remove duplicate test in parse_acl_expr() and unused variable
    - CI: github: add DEBUG_STRICT=2 to ASAN jobs
    - BUG/MINOR: quic: fix buffer overflow with sockaddr_in46
    - BUG/MEDIUM: acme: fix stalled renewal when opportunistic DNS check fails
    - BUG/MINOR: quic: fix trace crash on datagram receive
    - MINOR: quic: fix trace spacing when datagram is displayed
    - CLEANUP: mux-h2: remove the outdated condition to release h2c on timeout
    - BUILD: add an EXTRA_MAKE option to build addons easily
    - BUILD: otel: removed USE_OTEL, addon is now built via EXTRA_MAKE
    - CLEANUP: otel: move opentelemetry outside haproxy sources
    - BUG/MEDIUM: mux-h2: fix the body_len to check when parsing request trailers
    - BUG/MAJOR: mux-h2: preset MSGF_BODY_CL on H2_SF_DATA_CLEN in h2c_dec_hdrs()
    - DOC: otel: update the filter's status and URL in the docs
    - DOC: acme: document missing acme-vars and provider-name keywords
    - BUG/MINOR: dns: always validate the source address in responses
    - BUG/MINOR: tcpcheck: Properly report error for http health-checks
    - CLEANUP: resolvers: Remove duplicated line when resolvers proxy is initialized
    - BUG/MINOR: resolvers: Free new requester on error when linking a resolution
    - BUG/MINOR: resolvers: Fix lookup for a hostname in the state-file tree
    - BUG/MINOR: resolvers: Free opts on parse error in resolv_parse_do_resolve()
    - BUG/MAJOR: net_helper: also fix tcp_options_list for OOB write loop
    - BUG/MEDIUM: ssl/sample: check output buffer size in aes_cbc_enc converter
    - BUG/MAJOR: http-ana: fix private session retrieval on NTLM
    - REGTESTS: add a regtest to validate various NTLM transitions
    - BUG/MEDIUM: mworker/cli: fix user and operator permission via @@<pid> in master CLI
    - BUG/MINOR: mworker/cli: check ci_insert() return value in pcli_parse_request()
    - REGTESTS: http-messaging: always send RFC8441 client settings to use ext connect
    - BUG/MINOR: h2: add decoding for :protocol in traces
    - BUG/MINOR: mux-h2: condition the processing of 8441 extension to global setting
    - MINOR: mux-h2: add a new message flag to indicate ext connect support
    - BUG/MINOR: h2: only accept :protocol with extended CONNECT
    - BUG/MINOR: acme: contact mail should be optional, don't pass ToS bool
    - CLEANUP: http-fetch: Remove duplcated return statement in smp_fetch_stver()
    - CLEANUP: http-fetch: Adjust smp_fetch_url32_src() comment
    - CLEANUP: http-fetch: Fix indentation of sample_fetch_keywords
    - BUG/MINOR: http_fetch: Check return values of unchecked buffer operations
    - BUG/MINOR: http-fetch: Fix http_auth_bearer() when custom header is used
    - BUG/MEDIUM: h1_htx: Remove reverved block on error during contig chunks parsing
    - CLEANUP: haterm: Remove duplicated bloc to know if haterm must drain
    - BUG/MINOR: haterm: Immediately report error when draining the request
    - CLEANUP: haterm: Remove useless IS_HTX_SC() test
    - BUG/MINOR: haterm: Fix a possible integer overflow on the request body length
    - BUG/MEDIUM: haterm: Subscribe for receives until request was fully drained
    - BUG/MINOR: haterm: Don't set HTX_FL_EOM flag on 100-Continue responses
    - BUG/MEDIUM: haterm: Properly handle end of request and end of response
    - BUG/MEDIUM: haterm: Properly handle client timeout
    - BUG/MINOR: haterm: Fix condition to use direct data forwarding
    - BUG/MINOR: haterm: Report a 400-bad-request error on receive error
    - DEBUG: haterm: Add hstream flags in the trace messages
    - MINOR: haterm: Remove now useless req_body field from hstream
    - MINOR: mux_quic: reset stream after app shutdown for HTTP/0.9
    - MINOR: mux_quic: do not perform unnecessary timeout handling on BE side
    - BUG/MEDIUM: mux_quic: adjust qcc_is_dead() to account detached streams
    - MINOR: mux_quic: simplify MUX_CTL_GET_NBSTRM
    - MINOR: ssl: Export 'current_crtstore_name'
    - MINOR: ssl: Factorize code from "new/set ssl cert" CLI command
    - MINOR: ssl: Factorize ckch instance rebuild process
    - MEDIUM: ssl: Refactorize "commit ssl cert"
    - BUG/MINOR: ssl: Use the sequence number with kTLS and TLS 1.2
    - BUG/MINOR: mux_quic: fix max stream ID reuse estimation
    - MINOR: mux_quic: release BE conns if reuse definitely blocked
    - BUG/MINOR: mux_quic: refresh timeout only if I/O performed
    - MEDIUM: mux-h1: Return an error on h2 upgrade attempts if not allowed
    - BUG/MEDIUM: mux-h2: Properly consume padding for DATA frames
    - MEDIUM: tools: read_line_to_trash() handle empty files without \n
    - MINOR: jws: support HMAC in jws_b64_protected(), make nonce optional
    - MINOR: jws: introduce jws_b64_hmac_signature() function for HMAC signing
    - MINOR: acme: implement EAB - external account binding
    - MINOR: acme: allow specifying custom MAC alg for EAB
    - REGTESTS: Fix h1_to_h2_upgrade.vtc to force h2 on first bind line
    - MINOR: cli: allow specifying a tgid with show fd
    - Revert "BUG/MEDIUM: cli: fix master CLI connection slot leak on client disconnect"
    - BUILD: use Makefile.mk instead of Makefile.inc in EXTRA_MAKE
    - Revert "BUG/MINOR: mux-h2: condition the processing of 8441 extension to global setting"
    - BUG/MEDIUM: mux-h2: fix the detection of the ext connect support
    - MINOR: jwe: Add option to enable/disable algorithms or encryption algorithms for jwt_decrypt
    - MINOR: jwe: Disable 'RSA1_5' algorithm by default in jwt_decrypt converters
    - BUG/MEDIUM: jwe: Fix jwt.decrypt_alg_list to work correctly
    - BUG/MEDIUM: stick-table: properly check permissions on CLI's set/clear cmd
    - DOC: acme: EAB is now supported
2026-05-08 05:22:55 +02:00
William Lallemand
815845f17e DOC: acme: EAB is now supported
Remove the line mentioning than External Account Binding is not
supported. Since it was implemented in 3.4.
2026-05-07 18:50:54 +02:00
Willy Tarreau
d04a56e17d BUG/MEDIUM: stick-table: properly check permissions on CLI's set/clear cmd
The "set stick-table" CLI command's permissions are checked a bit too
late in the I/O handler, because the lookups performed at parsing time
can already cause an entry to be created at level "user" even though the
user does not have the permission to go further and to fill the data in.

Note that the impact remains pretty low since the entry is created without
data being touchable, and all within the table's settings (max entries,
expire etc). In addition it cannot even be used to periodically refresh
an entry and prevent it from expiring because only a creation is handled
at this point.

Let's add the check in cli_parse_table_req() so that these privileged
commands are entirely denied past the table lookup. This way it remains
possible to know that the table doesn't exist, like for the "show" command
but not more.

This should be backported to all stable branches, because the bug right
now cannot result in an accidental use (entries are not properly created
and deletion does not work).

Thanks to Omkhar Arasaratnam for finding and reporting this.
2026-05-07 18:46:44 +02:00
Olivier Houchard
81abfaa4df BUG/MEDIUM: jwe: Fix jwt.decrypt_alg_list to work correctly
Function jwe_parse_global_alg_enc_list() handles both
jwt.decrypt_alg_list and jwd.decrypt_enc_list, but to know which array
to use, between the algorithms and encoding arrays to use, it was
checking the string to see if it matched jwe.supported_algorithms, so it
was always considering we were dealing with encodings, and
jwt.decrypt_alg_list could not possibly work.
Fix that by checking the right string.
2026-05-07 18:09:47 +02:00
Remi Tricot-Le Breton
495eb7b0e0 MINOR: jwe: Disable 'RSA1_5' algorithm by default in jwt_decrypt converters
In RFC8725, section 3.2, they suggest to "Avoid all RSA-PKCS1 v1.5
encryption algorithms" so this algorithm gets disabled by default.
Tokens having this "alg" won't be decrypted unless it is explicitly
reenabled thanks to 'jwt.decrypt_alg_list' global option.

Thanks to Omkhar Arasaratnam for raising our awareness about this!
2026-05-07 18:00:29 +02:00
Remi Tricot-Le Breton
f82a242c8f MINOR: jwe: Add option to enable/disable algorithms or encryption algorithms for jwt_decrypt
Some users of the jwt_decrypt_XXX converters might want to reject JWT
tokens with a specific algorithm or encryption algorithm ("alg" or "enc"
field respectively) in order to avoid weak algorithms for instance.
This could be done from the configuration but would be tedious.

This patch adds the new 'jwt.decrypt_alg_list' and
'jwt.decrypt_enc_list' global options that can be used to define a
subset of accepted algorithms
2026-05-07 18:00:27 +02:00
Willy Tarreau
00941af7b7 BUG/MEDIUM: mux-h2: fix the detection of the ext connect support
As reported by Huangbin Zhan (@zhanhb) in github issue #3355, latest
commit 96f7ff4fdd ("MINOR: mux-h2: add a new message flag to indicate
ext connect support") was not correct and can break RFC8441-compliant
clients, as it did for them with a variant of Chrome 142.

The problem is that while RFC9113 says that new pseudo-headers are only
permitted with *negotiated* extensions, and RFC8441 doesn't indicate
whether or not SETTINGS_ENABLE_CONNECT_PROTOCOL is needed from clients,
it only says that clients know that servers support the extension when
seeing it in their settings and can use it, which seems to imply that
they don't need to send it to indicate their willingness to use it.
This also means that the server cannot know if a client is expected to
use it or not by default. It only know that a client is not allowed to
use it if the server didn't emit support mentioning it, which haproxy
can do using h2-workaround-bogus-websocket-clients.

Thus the fix proposed by @zhanhb is right, when presetting the flag for
the parser to indicate whether or not we're willing to accept RFC8441's
:protocol pseudo-header, we should:
  - consider the received setting on the backend side (though the
    pseudo-header is neither used nor supported there, but at least
    we pass the info regarding the support of the extension)
  - consider the configuration for the frontend (since it's the only
    place where we can decide on support or not)

This patch does just that and reverts the accompanying changes to the
regtests that made them want to see the client's setting. It must be
backported to 2.6.

In the mean time, placing this option in the global section will force
the clients to downgrade to h1:

    h2-workaround-bogus-websocket-clients

Many thanks again to @zhanbb this feedback and proposing a tested fix.
2026-05-07 17:34:39 +02:00
Willy Tarreau
b587ea1f27 Revert "BUG/MINOR: mux-h2: condition the processing of 8441 extension to global setting"
This reverts commit 9986ad65a4.

The protocol was not super clear on one point when compared to RFC9113
and our internal setting GTUNE_DISABLE_H2_WEBSOCKET. While RFC9113 says
that protocol extensions are negotiated, RFC8441 is only advertised by
the server, which thus doesn't know if the client supports it or not
until it faces it. In addition, GTUNE_DISABLE_H2_WEBSOCKET doesn't
apply to the protocol support as it name seems to imply, but to the
frontend only since the corresponding option is
"h2-workaround-bogus-websocket-clients". As such, haproxy should not
expect the client to advertise anything regarding the setting, and
should not consider the option when receiving the server's setting.

This needs to be backported to 2.6 where the commit above was
backported.
2026-05-07 17:34:39 +02:00
William Lallemand
157e24272f BUILD: use Makefile.mk instead of Makefile.inc in EXTRA_MAKE
Use an external Makefile called Makefile.mk in order to build complex
addons.

    make TARGET=linux-glibc ... EXTRA_MAKE="/path/to/addon1" \
    EXTRA_MAKE+="/path/to/addon2"
2026-05-07 16:50:52 +02:00
Willy Tarreau
782336c21b Revert "BUG/MEDIUM: cli: fix master CLI connection slot leak on client disconnect"
This reverts commit 64383e655b.

As reported by Alexander Stephan in issue #3351, it causes problems.
First, as seen in the issue, the "reload" operation, handled by an applet
local to the master process, is being interrupted by the timeout so that
the client never gets the result (though the timeout is applied). A fix
for this was found (ignore client-fin/server-fin on applets, as they make
no sense there), but it only hides a deeper problem. Indeed, issuing
"@1 debug dev delay 2000" still stops at 1s with an error, indicating
that commands are systematically being sent with a shutdown, and thus
that the server-fin always applies. This is a problem because it means
that any long command will now be interrupted after one second.

All of this needs to be put back into perspective before progressing
further on this issue, and the reason for sending the shutdown should
be reconsidered in the context of the current version, as it looks
like this was once necessary but no longer is.

In addition, the issue encountered by Alexander, of a frozen worker,
was essentially reported once in many years, so it's totally acceptable
to leave older versions unfixed and figure what's the best solution for
modern versions only.

Let's just revert to the pre-fix situation so as to avoid causing
breakage everywhere. This revert should be backported to all versions
(2.4 included).
2026-05-07 16:37:33 +02:00
Maxime Henrion
da554b7ef7 MINOR: cli: allow specifying a tgid with show fd
This will become useful when we implement using unshare() to split fd
tables per thread group. For now, the tgid is parsed but completely
ignored.
2026-05-07 16:02:37 +02:00
Christopher Faulet
972d0a4183 REGTESTS: Fix h1_to_h2_upgrade.vtc to force h2 on first bind line
With VTEST, It seems possible to receive the H2 preface in 2 packets. So the
preface cannot be matched and the H1 to H2 upgrade is not performed as
expected. The script was fixed by forcing the H2 proto on the first bind
line.

The problem with the preface matching will be reviewed later.
2026-05-07 16:19:10 +02:00
Mia Kanashi
5f91cf1b7d MINOR: acme: allow specifying custom MAC alg for EAB
This implements configuration for custom mac alg in EAB.
I don't think there are any reasons to allow that TBH,
but it is something that exists in the spec.

Depends on the EAB impl.
No backport needed
2026-05-07 15:19:15 +02:00
Mia Kanashi
187b1250dd MINOR: acme: implement EAB - external account binding
Patch introduces ACME EAB support.

Configuring EAB requires two parts: Key ID and MAC Key.
Key ID is an ASCII string that specifies the name of the record CA
should look up. MAC Key is a base64url encoded key that is used
for the sake of JWS signing, using HS256 or other algorithms.
They are the credentials so must be stored securely.

A thing about EAB is that it is required only during account creation
so it is unexpectedly complex to think about.
Some CAs provide EAB credential pair that is reused between
multiple account order requests, for example ZeroSSL, but others like
Google Trusted Services require an unique EAB credential for each new
account creation request.

There are a lot of ways config could be implemented, I decided to make
so that Key ID and MAC Key are stored in separate files on disk,
that decision was made because of the security concerns.
File based approach in particular works well with systemd credentials,
works well with systems that have config world readable, or immutable,
and is compatible with existing setups that specify credentials in a
file.

EAB is configured through options like this in an acme section:

eab-mac-alg HS512
eab-mac-key pebble.eab.mac-key
eab-key-id pebble.eab.key-id

I decided to not error out on empty files, but issue a log msg instead,
so that credentials can be removed without changing the haproxy config.

Used read_line_to_trash function from tools.c for reading files,
that is something that could be replaced by a dedicated function too.

No backport needed
2026-05-07 15:19:15 +02:00
Mia Kanashi
c9e76e5bb1 MINOR: jws: introduce jws_b64_hmac_signature() function for HMAC signing
New jws_b64_hmac_signature() duplicates the same functionality as
jws_b64_signature(), but for the use case of HMAC signing.
Intended to be used for ACME EAB.

OpenSSL allows to use EVP_PKEY for HMAC functionality, so
jws_b64_signature() could be reused, but the problem is that although
isn't deprecated it was removed in BoringSSL, and was removed
(due to BoringSSL roots) but then readded back in AWS-LC, because of
"legacy clients" (citing them), for that reason alone I say that having
a dedicated function for hmac is better, HMAC() macro seems to be widely
supported unlike other ways of doing same thing. Another alternative
would be to use EVP_MD API, but it was introduced in OpenSSL 3.0,
so not as widely supported.
2026-05-07 15:19:15 +02:00
Mia Kanashi
6900278ac6 MINOR: jws: support HMAC in jws_b64_protected(), make nonce optional
This adds support for HMAC algorithms in jws_b64_protected(), but also
makes nonce field optional, because it isn't needed in some cases where
HMAC is used, primarily ACME EAB requires that nonce field must not
exist.
2026-05-07 15:19:15 +02:00
Mia Kanashi
83e6ae3334 MEDIUM: tools: read_line_to_trash() handle empty files without \n
fgets() returns NULL when EOF is reached before newline, handle
that as a success for consistency, current behaviour is arguably a bug,
the API of fgets() is pretty weird after all so someone probably forgot.
2026-05-07 15:19:15 +02:00
Christopher Faulet
faf3e9ac3a BUG/MEDIUM: mux-h2: Properly consume padding for DATA frames
Since the commit 617592c9e ("MEDIUM: mux-h2: try to coalesce outgoing
WINDOW_UPDATE frames"), padding of DATA frames is no longer
consumed. Instead, this padidng is left in the demux buffer and used as the
header of the next frame. Because all bytes of the padding must be zero,
this lead to trigger a PROTOCOL_ERROR because haproxy erroneously thinks the
peer sent a DATA frame for the stream-id 0. It is true for a padding of 9
bytes or more, but similar issues may be exprienced with smaller padding.

Before the commit above, the padding was consumed in h2_process_demux to
restore the H2_CS_FRAME_H state at the end of the while loop processing
received frames.

However, it seems a bit strange to deal with the padding at this stage,
espcially because it is not obvious at all. So to fix the issue, the padding
is now consumed at the end of h2_frt_transfer_data(), inside "end_tranfer"
label. At the stage, we know all payload of the current DATA frame were
consumed and only the padding is still there, if any. We must only take care
to not consume more than available in the demux buffer. The padding may have
been partially received.

This patch should fix the issue #3354. It must be backported as far as 2.8.
2026-05-07 14:59:28 +02:00
Christopher Faulet
72fd357814 MEDIUM: mux-h1: Return an error on h2 upgrade attempts if not allowed
If h1 to h2 upgrades are not allowed, a 405-method-not-allowed error is now
returned from the H1 multiplexer itself instead of dealing with "PRI *
HTTP/2.0\r\n\r\n" request as a normal request.

Before this kind of request was caught by the HTTP analyzers and a
400-bad-request was returned. This was added before the multiplexers era to
protect backend apps against unexpected H1 to H2 uprade on server side.

Now, it is possible to handle the error in the H1 multiplexer. One benefit
is to be able to increment the glichtes counters. However, the error is
still handled in HTTP analyzers to be sure to detect unwanted upgrades that
can be hidden in H2 or H3 requests.

There is a special case. TCP > H1 > H2 upgrades. In that case, a H1 stream
exist. So we must report an error to the upper layer too.

A reg-test script was added to validate the feature. In addition,
tcp_to_http_upgrade.vtc was updated accordingly.
2026-05-07 14:59:28 +02:00
Amaury Denoyelle
419cc6e2f6 BUG/MINOR: mux_quic: refresh timeout only if I/O performed
Some checks failed
Contrib / admin/halog/ (push) Has been cancelled
Contrib / dev/flags/ (push) Has been cancelled
Contrib / dev/haring/ (push) Has been cancelled
Contrib / dev/hpack/ (push) Has been cancelled
Contrib / dev/poll/ (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
Previously, QUIC MUX timeout was refreshed on every qcc_io_cb()
execution. This is not desired if no send/receive were performed, as in
this case the connection may be stuck.

This patch fixes this by refreshing timeout only if some progress is
performed during qcc_io_cb(). To implement this, return value of
qcc_io_recv() has been adjusted to return the number of newly decoded
bytes.

This patch is considered as a bug fix as without it there is a risk of
QUIC MUX inactivity timeout to be less efficient and to maintain a
connection too long.

This should be backported up to 2.8, after a period of observation.
2026-05-07 14:34:29 +02:00
Amaury Denoyelle
b8961ee8b3 MINOR: mux_quic: release BE conns if reuse definitely blocked
avail_streams callback serves to indicates how many streams can be
attached for a backend connection. On QUIC mux, this relies on several
parameters, first based on static limitation which only decreases over
time, but also on flow control which is dynamically adjusted by the peer
and can be increased or decreased at will.

qcc_is_dead() on the other hand serves to determine if a connection can
be removed. First, it must be inactive (no request in progress). Then,
if a backend connection cannot be reused due to some of the above
limitation reached, it is definitely useless and should be removed as
soon as possible. However, prior to this patch, qcc_is_dead() did not
take into account the same set of parameters than avail_streams : only
if graceful shutdown was initiated by the peer was considered.

The purpose of this patch is to linked these two functions together.
Reuse calcul based on static limits is extracted from avail_streams()
into new qcc_be_is_reusable(). This function is used directly in
qcc_is_dead(), which now for example takes into account the server
max-reuse parameter.

This patch should ensure that a backend connection which can not be
reuse anymore is release as soon as possible. This could improve
slightly reuse rate in some specific scenarii as non-reusable
connections should not pollute the idle cache.

Return value of QUIC avail_streams() is changed by this patch as server
max-reuse and max stream ID limits are now only taken into account when
already exceeded or if a single stream remains. However, this has no
consequence as callers of avail_streams() do not differentiates return
value of 2 or more.
2026-05-07 11:19:22 +02:00
Amaury Denoyelle
e586458ec0 BUG/MINOR: mux_quic: fix max stream ID reuse estimation
The following patch adjusts QUIC mux avail_streams() to ensure maximum
stream ID is never exceeded.

  commit 143d0034c9
  BUG/MINOR: mux_quic: limit avail_streams() to 2^62

However, the calcul is incorrect, as <next_bidi_l> member value is set
to the next ID available, not the last one in use. Also, when the last
stream is closed, it will be greater than QCS_ID_MAX_STRM_CL_BIDI,
resulting in a substraction wrapping.

Fix this by using the simplest approach. Return value of avail_streams()
is only reduced if either the maximum stream ID limit is already
exceeded, or there is only a single stream still usable. In other cases,
return value is left as is.

Note that this bug is unlikely to have any impact as the maximum stream
ID is a very large value.

This should be backported up to 3.3.
2026-05-07 10:53:56 +02:00
Olivier Houchard
753a282373 BUG/MINOR: ssl: Use the sequence number with kTLS and TLS 1.2
When using TLS 1.2 and kTLS, use the sequence number as the explicit
nonce (what the linux kTLS API calls "iv"), as is strongly recommanded,
and done by most TLS implementations, instead of trying to generate a
pseudo random-number.
In practice, it changes nothing, because the kernel would override that
with the sequence number anyway, but there is no need to have confusing
code that uses statistical_prng_range() anyway.

This should be backported to 3.3.
2026-05-06 21:37:18 +02:00
Remi Tricot-Le Breton
2be6744189 MEDIUM: ssl: Refactorize "commit ssl cert"
In order for the code behind the "commit ssl cert" logic to be usable
outside of the CLI context, some new "ckch_store_update_" functions are
created. They allow to perform all the operations on ckch_stores to be
performed without needing an appctx.
The first function being called is ckch_store_update_init which mainly
takes the ckch_store lock and checks that there is an ongoing
transaction with the proper path (which was already done in
cli_parse_commit_cert).
The main one is ckch_store_update_process which replicates the logic
that could be found in the cli_io_handler_commit_cert function. We
iterate over the ckch instances of an existing ckch store and duplicate
them in the new ckch store which is still detached from the tree, before
replacing the old store with the new one. This whole operation could
take some time so we were yielding every 10 instances or when
applet_putstr calls would fail. The actual ckch_store operations and the
applet related calls are now decorrelated in order to stop having to
have an appctx during the ckch store/instances processing.
The ckch_store_update_process will now update a "msg" buffer and a
"state" that allow to send processing messages to the caller as well as
keep the state of the processing "state machine".
When the ckch_store_update_process loop is over,
ckch_store_update_cleanup can be called to release the lock and free
some now useless structures.
2026-05-06 21:37:18 +02:00
Remi Tricot-Le Breton
53ecb81781 MINOR: ssl: Factorize ckch instance rebuild process
The ckch instances for a given ckch_store have to be rebuilt when a
certificate is updated during runtime (via cli or lua). The code was
duplicated in lua so factorizing the actual loop avoids future errors
if the code changes. The new 'ckch_store_rebuild_instances' will have a
dedicated 0 return code if it needs to be called again (because of the
yielding logic since ckch instance rebuild might take some time).
2026-05-06 21:37:18 +02:00
Remi Tricot-Le Breton
efe6c97488 MINOR: ssl: Factorize code from "new/set ssl cert" CLI command
This allows to perform the same kind of operation without the need for
an appctx.
2026-05-06 21:37:18 +02:00
Remi Tricot-Le Breton
acf1331ed8 MINOR: ssl: Export 'current_crtstore_name'
Make the 'current_crtstore_name' global variable visible during parsing.
2026-05-06 21:37:18 +02:00
Amaury Denoyelle
1614204d28 MINOR: mux_quic: simplify MUX_CTL_GET_NBSTRM
Since the previous patch, accounting of HTTP requests in progress on MUX
QUIC as been simplified. Now QCC <nb_hreq> identifies them until the QCS
free.

Thus, MUX_CTL_GET_NBSTRM can be simplified. Instead of relying on
<nb_sc> plus the <opening_list>, simply return <nb_hreq> value which
should be slighly identical.
2026-05-06 10:21:16 +02:00
Amaury Denoyelle
3cfb08c07b BUG/MEDIUM: mux_quic: adjust qcc_is_dead() to account detached streams
Muxes are responsible to release connections once they are inactive and
won't be reusable. In QUIC mux, such connections are detected via
qcc_is_dead(). The first precondition is that there is no more upper
streams attached. This was accounted via QCC <nb_sc> counter.

A special characteristic of QCS instances is that they can be in
detached state : upper stream has been removed but there is still data
to emit. Such QCS were not taken into account in qcc_is_dead(), so a
connection could be freed with some remaining data not yet emitted.

It is also not possible for QUIC MUX to simply look at the QCS tree to
determine if the connection is inactive. Indeed, some streams are opened
for protocol internal usage. This is the case for example with HTTP/3
unidirectional control stream or QPACK encoder/decoder streams. These
streams are never closed. In the end, only requests streams should be
taken into account for the connection activity.

This patch improves the situation by reworking <nb_hreq> QCC counter.
Previously, it served for http-request timeout implementation. However,
this timeout only relies on <opening_list> now. Thus, <nb_hreq> scope is
changed : it is now incremented via qcs_wait_http_req(), used by app
protocol layer once a request stream is identified. Decrement is
performed on qcs_free(), so this guarantees that a connection cannot be
freed anymore if request streams still exists, unless if inactivity
timeout fires. As such, <nb_hreq> now supersedes <nb_sc> entirely, so
the qcc_is_dead() can now relies on the former.

Along with this change, qcc_timeout_task() must be updated. Call to
qcc_is_dead() was unnecessary prior to this patch as timeout handling
was only active when no upper streams were attached. When tested, both
<nb_sc> and QCC <task> were already null, so a connection was always
released on timeout, as expected. With qcc_is_dead() now checking
<nb_hreq> instead, this is not always the case anymore. In fact, this
check is unnecessary as inactivity timeout serves precisely to free a
stucked connection with remaining data to emit.

This patch also has some impact on http-keep-alive timeout. Previously,
this timeout could be armed if only detached streams remained. Now, it
is only applicable if all QCS request instances are closed and freed.
Thus, qcc_reset_idle_start() is now closed directly on qcs_free().

Ideally this should be backported up to 2.6, or at least 2.8 as QUIC
experimental status was removed there.
2026-05-06 10:19:25 +02:00
Amaury Denoyelle
81eda41d5c MINOR: mux_quic: do not perform unnecessary timeout handling on BE side
MUX implements a timeout for HTTP keep-alive which monitors the delay
between two HTTP requests. This is only applicable for frontend
connections, as on the backend side idle connections can be kept in the
server pool. In QUIC mux, this timeout relies on QCC <idle_start> which
is refresh when the last request is terminated.

This patch modifies the refresh operation so that it is only performed
for frontend connections. This is not strictly necessary but the timeout
timeout management is now clearer and it eliminates an unnecessary
operation for backend connections.

Similarly, http-request timeout is also only applicable for frontend
connections. This relies on qcs_wait_http_req() function. A request QCS
is inserted in <opening_list> until the headers are received. This is
unnecessary on the backend side so this is excluded as well.
2026-05-06 08:57:35 +02:00
Amaury Denoyelle
af49294633 MINOR: mux_quic: reset stream after app shutdown for HTTP/0.9
HTTP/3 implements a GOAWAY frame for graceful shutdown. This allows to
reject any new stream opening with a larger ID. This is implemented via
HTTP/3 attach() callback called by qcs_new().

When HTTP/0.9 is used, there is no similar mechanism. This renders some
feature such as server max-reuse difficult to implement. This patch now
provides a method for such protocols with no graceful shutdown support.
Instead of invoking attach() callback, a stream is now immediately
resetted if the application protocol layer is already closed.

This patch does not change the behavior for HTTP/3. Only limited
protocols (currently only HTTP/0.9) without graceful shutdown are
impacted. These protocols are identified as their shutdown() callback is
nul.

This change is only necessary for HTTP/0.9 as there is no equivalent of
HTTP/3 GOAWAY in this case.
2026-05-06 08:51:27 +02:00
Christopher Faulet
b71a0e7874 MINOR: haterm: Remove now useless req_body field from hstream
req_body field is no longer used, except in trace messages. And in fact, it
is not necessarily true if some data are received with the request headers.
So no reason to still use it.
2026-05-05 19:07:59 +02:00
Christopher Faulet
a68b96ad36 DEBUG: haterm: Add hstream flags in the trace messages
It could be useful to know the hstream state on debugging sessions.
2026-05-05 19:07:59 +02:00
Christopher Faulet
6e7802ca36 BUG/MINOR: haterm: Report a 400-bad-request error on receive error
When an error is reported when reading request data, the hstream now try to
send a 400-bad-request to the client. Before, the connection was just closed
with no error message.
2026-05-05 19:07:59 +02:00
Christopher Faulet
72e010fca3 BUG/MINOR: haterm: Fix condition to use direct data forwarding
The direct fowarding support was only relying on "hs->to_write" value. But
we must be sure to retry if fast-forward data are still there in the I/O
buffer.
2026-05-05 19:07:44 +02:00
Christopher Faulet
b6503f70e2 BUG/MEDIUM: haterm: Properly handle client timeout
No client timeout was set with haterm. It could be an issue with
unresponsive clients. So the I/O timeout of the SC is initialized to the
frontend client timeout when the hstream is created. Then a read activity is
reported when data are received. This read activity is used to set an
expiration date on the hstream task and test it when the hstream is woken up
with TASK_WOKEN_TIMER reason.

When a client timeout is detected, the hstream try to send a 408 and report
an error.
2026-05-05 19:03:31 +02:00
Christopher Faulet
c0f137b704 BUG/MEDIUM: haterm: Properly handle end of request and end of response
There were several issues with the handling of end of the request or end of
response. The main problem was about the request draining.

To help to fix these issues, two flags were introduced:

 * HS_ST_HTTP_EOM_RCVD: to know the request was fully received
 * HS_ST_HTTP_EOM_SENT: to know the response was fully sent

Thanks to these flags some parts were reviewed and simplified.

In the I/O callback function, outside of any error, the hstream task is now
woken when one of the direction is not finished or when there are still some
data in a buffer.

The function hstream_must_drain() was reworked to properly drain request
with no content-length before replying.

The condition to wake hstream up to drain the request after replying was
also reworked, and moved ouside of the else block. Indeed, it must also be
evaluated when the response was fully sent in one call, when request headers
were processed.

Finally, the condition to shut the hstream was slighly adapted to use the
new flags. In addition, we now rely on se_shutdown().
2026-05-05 19:01:03 +02:00
Christopher Faulet
b945a3207b BUG/MINOR: haterm: Don't set HTX_FL_EOM flag on 100-Continue responses
A 100-Continue response is an intermediary message. So the end of message
must not be announed.
2026-05-05 18:54:20 +02:00
Christopher Faulet
1bc050bc49 BUG/MEDIUM: haterm: Subscribe for receives until request was fully drained
When draining the request, if some data were received, no subscribe for
receives was performed to get the remaining. However, because request data
are just ignored, we must always subscribe until it was fully
drained. Otherwise, haterm will never be woken up to drain more data.
2026-05-05 18:54:20 +02:00
Christopher Faulet
999d71560d BUG/MINOR: haterm: Fix a possible integer overflow on the request body length
When request data were received, the request body length was decremented
accordingly with no check on it to be sure it was set. However, it remains
equal to 0 for chunked requests or H2/H3 requests with no content-length.

So now, it is only decremented when it is greater than 0.
2026-05-05 18:54:16 +02:00
Christopher Faulet
3f7b2023c9 CLEANUP: haterm: Remove useless IS_HTX_SC() test
Haterm is an HTTP endpoint. No reason to test if its sc is an HTX sc or
not. Let's remove Is_HTX_SC() test.
2026-05-05 18:36:34 +02:00
Christopher Faulet
f19312ab4b BUG/MINOR: haterm: Immediately report error when draining the request
When draining the request data, if an error was reported while some data
were received, the error was not processed immediately. This part was copied
from tcpchecks where the response should be processed first. For haterm, the
request data are ignored. So no reason to wait to handle the error. It may
be an issue because the response may be sent in the meanwhile.
2026-05-05 18:36:34 +02:00
Christopher Faulet
e373fd6319 CLEANUP: haterm: Remove duplicated bloc to know if haterm must drain
When haterm was waiting for request headers, there was two test to know if
it had to drain the request data before replying. One of them was useless
and was thus removed.
2026-05-05 18:36:34 +02:00
Christopher Faulet
4af4feed33 BUG/MEDIUM: h1_htx: Remove reverved block on error during contig chunks parsing
In h1_parse_full_contig_chunks(), we first try to reserve the bigger HTX
DATA block as possible. It is ajusted at the end of chunks parsing or
removed if no data was copied. However, it should also be removed when a
parsing error is triggered. It could be an issue for http health checks and
haterm to properly handle errors.

This patch should be backported as far as 2.6.
2026-05-05 18:36:34 +02:00
Christopher Faulet
9095785203 BUG/MINOR: http-fetch: Fix http_auth_bearer() when custom header is used
When http_auth_bearer() sample fetch function is called with a custom header
and the header is not found or type didn't match 'Bearer', a mismatch must
be reported instead of an empty string.

This patch should be backported as far as 2.6.
2026-05-05 18:36:04 +02:00
Willy Tarreau
9abfbbf0ba BUG/MINOR: http_fetch: Check return values of unchecked buffer operations
Several return value for chunk_istcat() or chunk_memcat() calls were not
tested. Now, 0 is returned on failure.

Concretly, for now, it is unexpected to trigger error because the result
cannot exceed the buffer size. Data are extracted from an HTX message.

At first glance, no reason to backport it.
2026-05-05 18:36:04 +02:00
Christopher Faulet
51f8bb46af CLEANUP: http-fetch: Fix indentation of sample_fetch_keywords
Misplaced spaces before comma in 'urlp' keyword table entry.
2026-05-05 18:36:04 +02:00
Christopher Faulet
45ca881a6b CLEANUP: http-fetch: Adjust smp_fetch_url32_src() comment
smp_fetch_base32() function was referenced instead of
smp_fetch_url32(). Let's fix it.
2026-05-05 18:36:04 +02:00
Christopher Faulet
e7482c4d0e CLEANUP: http-fetch: Remove duplcated return statement in smp_fetch_stver()
the return statement was needlessly repeated. Let's remove the second one.
2026-05-05 18:36:04 +02:00
Mia Kanashi
3fa0aa3664 BUG/MINOR: acme: contact mail should be optional, don't pass ToS bool
According to ACME RFC contact email is optional.
Letsencrypt used it some long time ago, but not today.
Currently HAProxy always sets the value of the contact mail to a string
that is read from the config, but if that string is not specified,
it sets %s in mailto:%s to null, which cases new account request
to fail in pebble.

Also HAProxy currently passes termsOfServiceAgreed bool to requests
that contain onlyReturnExisting, that isn't needed according to the RFC
and other ACME impls.

This patch dynamically builds the account request JSON to address that.

Can be backported to 3.2
2026-05-05 18:04:19 +02:00
Willy Tarreau
b52a0e6782 BUG/MINOR: h2: only accept :protocol with extended CONNECT
As reported by Huangbin Zhan in github issue #3355, we're too lax on
the :protocol pseudo header. It is currently accepted with regular
CONNECT as well as non-CONNECT methods while it only ought to be
accepted with extended CONNECT (i.e. CONNECT after the connection
negotiated the RFC8441 extension). Let's refine the check in H2 by
leveraging the new flag H2_MSGF_EXT_CONN_OK that is passed by the
caller when the connection supports the extension. This is sufficient
to sort the various cases.

The proto upgrade regtest was updated to verify that CONNECT with
:protocol without nego and another method with nego and :protocol
both fail.

Thanks to Huangbin Zhan (@zhanhb) for the report and helpful reproducer.

This needs to be backported to all versions. It relies on these patches
first:

  REGTESTS: http-messaging: always send RFC8441 client settings to use ext connect
  BUG/MINOR: mux-h2: condition the processing of 8441 extension to global setting
  MINOR: mux-h2: add a new message flag to indicate ext connect support
2026-05-05 14:10:36 +02:00
Willy Tarreau
96f7ff4fdd MINOR: mux-h2: add a new message flag to indicate ext connect support
The new message flag H2_MSGF_EXT_CONN_OK indicates that the connection
supports extended connect. This will be used in a subsequent fix.
2026-05-05 14:09:49 +02:00
Willy Tarreau
9986ad65a4 BUG/MINOR: mux-h2: condition the processing of 8441 extension to global setting
When rfc8441 (extended connect) is disabled via
h2-workaround-bogus-websocket-clients, we properly refrain from
advertising support for extended connect, but we should also ignore
the incoming setting, otherwise it can remain enabled if the client
advertises it.

This should be backported to stable versions.
2026-05-05 14:09:49 +02:00
Willy Tarreau
a950c4b72d BUG/MINOR: h2: add decoding for :protocol in traces
Function h2_phdr_to_list() was missing the decoding for the :protocol
header and would emit :UNKNOWN in this case. It's only used in traces
so it's not important. The fix can be backported in all versions.
2026-05-05 14:09:49 +02:00
Willy Tarreau
47bb725d88 REGTESTS: http-messaging: always send RFC8441 client settings to use ext connect
The tests were validating extended connect without sending the setting
in the client settings frame. It currently works due to a bug, so let's
fix the vtc first.
2026-05-05 14:09:49 +02:00
William Lallemand
166dfcbbee BUG/MINOR: mworker/cli: check ci_insert() return value in pcli_parse_request()
All ci_insert() calls in pcli_parse_request() were ignoring the return
value, silently miscounting the bytes to forward if an insertion failed.

Add a check on each call and return -1 on failure. In practice this has
no impact: the channel receive machinery enforces a maxrewrite reserve,
so there is always sufficient room in the buffer for these small
prefixes by the time pcli_parse_request() is called.

Must be backported in every maintained versions, the list of available
commands might change.
2026-05-04 19:03:48 +02:00
William Lallemand
946921c199 BUG/MEDIUM: mworker/cli: fix user and operator permission via @@<pid> in master CLI
When @@<pid> is matched in pcli_parse_request(), no "operator -" or
"user -" is being sent before the command, like it's done for @<pid>.

It leads to privileges not being respected and commands are sent as
admin.

Fix this by applying the access-level downgrade in the @@<pid> path,
like it's done for @<pid>.

Must be backported to 3.2.

Reported-by: Omkhar Arasaratnam <omkhar@linkedin.com>
2026-05-04 19:03:48 +02:00
Willy Tarreau
dae302d479 REGTESTS: add a regtest to validate various NTLM transitions
This test first performs two successive requests over the same
connection where reuse is expected, then perform two 401 which must
both work, testing both the transition from null->sess, and sess->sess.

This test could be backported to detect changes related to private
sessions.

Thanks to Omkhar Arasaratnam for the test.
2026-05-04 18:58:19 +02:00
Willy Tarreau
4fc1e39371 BUG/MAJOR: http-ana: fix private session retrieval on NTLM
During the architectural review leading to commit 90b2154d93 ("MEDIUM:
muxes: always set conn->owner to the session that owns the connection"),
we wondered whether srv_conn->owner could ever be NULL or even invalid,
or if it ought to be changed to sess, and the projections of various use
cases, as well as a number of attempts to fool it led us to conclude
that it was always valid since the connection is private. So it was
considered safer not to start fiddling with the pointer in case it
could still match a previous session after a reuse, which would match
the scenario described in the session_add_conn() comment.

Actually there was exactly one case where a NULL could be met, and that
was covered by the preliminary call to conn_set_owner() that was removed
in that patch, precisely related to the one that the next patch tried to
address: in http-reuse always, after the second request on a connection
releases the connection, the owner can now become NULL, so if an NTLM
header is seen at this point, it will crash.

Interestingly, after the immediately following commit was merged,
d93c53b0df ("MEDIUM: session: always reset the conn->owner on backend
when installing mux"), the problem became immediate as the conn's
owner is now null during operation if the connection is not private, and
now the first response in NTLM is sufficient to crash the process. On
the other hand, thanks to the two patches above, we're now certain never
to meet a different session, which was the sought goal: either the session
is normal and it has no owner, or it's private and it has <sess> as owner.
Also with HTTP/1 (since the code explicitly checks for H1), there may be
a single request at a time on a connection so the owner should either be
the session or NULL.

So this patch finally implements the original plan, to pass <sess> to
session_add_conn(). The call is idempotent if the owner is already set,
but at least the function performs some preliminary sanity checks which
are quite welcome, so better continue to always call it.

Note that this is only for 3.4 or any branch that has exactly the two
patches above. And if the patches above were to ever be backported
(together), this one would be needed as well.

Thanks to Omkhar Arasaratnam for reporting this regression.
2026-05-04 18:57:15 +02:00
William Lallemand
726aa2dfb2 BUG/MEDIUM: ssl/sample: check output buffer size in aes_cbc_enc converter
AES-CBC uses a 16-byte block size, and PKCS padding always adds at least
one byte (up to a full 16 bytes when input is already block-aligned), so
the encrypted output is always larger than the input. Without checking
that the output buffer can hold the padded result, encryption could
overflow it. Add a pre-encryption guard for block cipher (blksize > 1)
that rejects the operation when the output buffer is too small.

No backport needed.

Reported-by: Omkhar Arasaratnam <omkhar@linkedin.com>
2026-05-04 17:38:15 +02:00
Willy Tarreau
b9028ee1e4 BUG/MAJOR: net_helper: also fix tcp_options_list for OOB write loop
The exact same issue that was fixed for ip.fp with commit dbf471f99a
("BUG/MAJOR: net_helper: ip.fp infinite loop on malformed tcp options")
also exists for tcp_options_list: an option with a zero size will prevent
the offset from making any progress and will loop forever. In this case,
since we produce output, trash->data gets incremented on each loop and
the byte at ofs is used to fill the memory there, quickly overflowing the
area.

The exact same fix as above was applied here again, including the uchar
cast to make sure we don't go back-and-forth between two positions.

Such TCP options are not valid and will not be reported by the TCP stack,
however they can happen in case options are passed in headers by a first
proxy, or are offered in a return directive fed from a query string as a
means of providing a debugging tool for admins.

Thanks to Omkhar Arasaratnam for reporting this issue.

This fix must be backported to 3.2 where the commits above were
backported.
2026-05-04 17:26:44 +02:00
Christopher Faulet
1d54b9f70e BUG/MINOR: resolvers: Free opts on parse error in resolv_parse_do_resolve()
The error handler at do_resolve_parse_error freed varname and resolvers_id
but missed freeing rule->arg.resolv.opts (allocated via calloc). Added
ha_free(&rule->arg.resolv.opts) to the cleanup path.

This patch could be backported to all stable branches.
2026-05-04 16:33:56 +02:00
Christopher Faulet
3b1db89283 BUG/MINOR: resolvers: Fix lookup for a hostname in the state-file tree
In resolv_check_response(), the condition 'if (!srvrq->named_servers)' was
inverted - it ran the named server lookup when the tree was empty/NULL
instead of when it was populated. This prevented proper hostname-based
matching of SRV records to servers from the state file.

The issue was introduced when tree of servers was replaced by a cebis tree
(fdf6fd5b45).

This patch must be backported to 3.3.
2026-05-04 16:33:56 +02:00
Christopher Faulet
baf0924a50 BUG/MINOR: resolvers: Free new requester on error when linking a resolution
If an error is triggered while the requester was allocated, it is not
immediately released. It is not really a memory leak because the requester
will be reused laster, on a next attempt, or released on deinit. For a
do-resolv action, the requester is released with the stream. So no leak
here. But it is probably not expected to keep a newly allocated requester in
case of error.

This patch could be backported as far as 2.6.
2026-05-04 16:33:56 +02:00
Christopher Faulet
95ee47fd32 CLEANUP: resolvers: Remove duplicated line when resolvers proxy is initialized
In resolvers_setup_proxy(), "px->conn_retries" was initialized twice. Let's
remove the line with no comment.
2026-05-04 16:33:56 +02:00
Christopher Faulet
e46090f424 BUG/MINOR: tcpcheck: Properly report error for http health-checks
When an error is reported on an expect rule for tcp and http health-checks,
a dedicated message is reported with details about the wrong match. However,
this was never performed for HTTP health-check because of the wrong test on
the check type.

In addition, when an error was reported on an "expect hdr" rule, a break
statement was missing. So the error message could be truncated (but never
emitted because of the issue above).

This patch should be backported to all stable versions.
2026-05-04 16:33:56 +02:00
Willy Tarreau
4ef31cb5c2 BUG/MINOR: dns: always validate the source address in responses
When we removed the use of connect() to reach DNS servers in 3.3 with
commit 2c7e05f80e ("MEDIUM: dns: don't call connect to dest socket for
AF_INET*"), we accidentally lost a check on the server's address in
responses, opening the possibility of spoofed DNS responses for someone
who knows both haproxy's IP:port and the transaction ID.

In practice, the impact is very low, because:
  - DNS servers IP addresses are almost always known, and often even among
    the widely used ones (1.1.1.1, 8.8.8.8 etc), and their port is 53.
  - all DNS "security" relies on the ignorance of the transaction ID and
    is possible source port, so if either the attacker is on-path and sees
    them, or it's off-path and has to guess them, but in any case it's
    trivial to spoof the known server in responses, with or without the
    check.

Regardless, let's not further weaken the protocol and do the check.

Thanks to Omkhar Arasaratnam for reporting this issue.

An interesting observation while testing this fix was that the code does
support UNIX dgram sockets (via connect()) but that since we don't bind
to a local UNIX socket to send requests, the server's recvfrom() doesn't
get any address and has nowhere to respond to. So in practice while the
code is designed to deal with UNIX sockets, these cannot work by design.

This fix must be backported to 3.2 where the commit above was backported.
2026-05-04 16:27:26 +02:00
William Lallemand
4153aae932 DOC: acme: document missing acme-vars and provider-name keywords
Both keywords are used with dns-01 and dns-persist-01 challenges to pass
information to an external DNS provisioning tool (e.g. the dataplaneAPI)
via the "dpapi" sink. provider-name sets the DNS provider identifier and
acme-vars passes arbitrary tool-specific variables.

Thanks to @oliwer for reporting the issue.

Must be backported to 3.2, however previous version don't have
"dns-persist-01".
2026-05-04 14:44:53 +02:00
Willy Tarreau
8ffb4b5a09 DOC: otel: update the filter's status and URL in the docs
The docs (readme and configuration.txt) still used to mention that OTEL
was under development. Now that it's released, let's indicate that it's
ready with the download URL.
2026-05-04 14:38:35 +02:00
Willy Tarreau
7e09e2a916 BUG/MAJOR: mux-h2: preset MSGF_BODY_CL on H2_SF_DATA_CLEN in h2c_dec_hdrs()
Commit d12edebe4a ("BUG/MAJOR: mux-h2: detect incomplete transfers on
HEADERS frames as well") tried to enforce strict matching between
advertised content-length and transferred data when dealing with ES on
a headers frame. It purposely arranged the code so that it would cover
both headers and trailers. The problem is, in h2c_dec_hdrs() we preset
message flags (msgf) based on the current state and knowledge related
to the stream being processed, then we pass these flags to the headers
parser and use their final state to perform some extra checks.

MSGF_BODY_CL was set by the parsers themselves when processing a
content-length header, but when parsing a trailers frame, it will not
be set, and due to this the matching between the remaining expected
content-length and the transferred data is not verified, so the fix
above doesn't work for trailers.

This patch sets MSGF_BODY_CL to the same value as H2_SF_DATA_CLEN so
that during headers it remains zero, but it matches what was learned
during headers when processing trailers. It is sufficient to re-enable
the check that was attempted in the commit above.

The impact remains the same as the one indicated in the commit above: in
practice this can be used to force subsequent requests to fail, or when
running with "http-reuse never" or when running with a totally idle server,
to perform a request smuggling by constructing specially crafted request
pairs where the first one is used to trigger an early response and hide
parts of or all headers of the second one, to instead use a second
embedded one that was not subject to analysis. As such, the risk remains
moderate given the low prevalence of "http-reuse never" in production
environments, and of idle servers. Again, a temporary alternative to the
fix is to disable HTTP/2 by specifying "alpn http/1.1" on "bind" lines,
and adding "option disable-h2-upgrade" in HTTP frontends.

Many thanks to Pratham Gupta / alchemy1729 for spotting and analyzing
this problem, and for providing a lightweight reproducer to illustrate
the problem!

This fix must be backported to all versions where the fix above was
backported (i.e. all). Note that it depends on this previous commit
otherwise trailers will always break:

  BUG/MEDIUM: mux-h2: fix the body_len to check when parsing request trailers

As a side note, it's worth noting that these temporary message flags have
reached a level of pain and fragility that really warrants a complete
rework. Ideally we should have a pair of such flags in the h2s (one per
direction) and the callers of the parsers should point to them so that
they are always up to date. And by having generic HTTP flags instead of
H2, we could better unify the h1/h2/h3/fcgi processors (and maybe avoid
some HTX conversion). One flag could even indicate that trailers are
being parsed (since they're last) so as to ease this detection down the
chain.
2026-05-04 14:33:22 +02:00
Willy Tarreau
b6995d25d1 BUG/MEDIUM: mux-h2: fix the body_len to check when parsing request trailers
The h2 content-length validation in commit d12edebe4a ("BUG/MAJOR:
mux-h2: detect incomplete transfers on HEADERS frames as well") was
insufficient. The content-length check is still ineffective on request
trailers and it could not work by default due to the fact that the
default body_len is used in h2c_frt_handle_headers() when processing
trailers, instead of passing h2s->body_len, which was necessarily parsed
before reaching trailers. Let's fix this point first, otherwise fixing
the second issue would break trailers.

Many thanks to Pratham Gupta / alchemy1729 for spotting and analyzing
this problem, and for providing a lightweight reproducer to illustrate
the problem!

This fix must be backported to all versions where the fix above was
backported (i.e. all).
2026-05-04 14:33:22 +02:00
William Lallemand
a8b1af277f CLEANUP: otel: move opentelemetry outside haproxy sources
The opentelemetry addons now live outside haproxy sources and is
available at https://github.com/haproxytech/haproxy-opentelemetry/

The addon must be built using the EXTRA_MAKE option from HAProxy
Makefile:

$ PKG_CONFIG_PATH=/opt/lib/pkgconfig make -j8 TARGET=linux-glibc
  EXTRA_MAKE="/tmp/a/a/haproxy-opentelemetry" OTEL_DEBUG=1 OTEL_USE_VARS=1
2026-05-04 14:18:40 +02:00
Miroslav Zagorac
e16b1a44f8 BUILD: otel: removed USE_OTEL, addon is now built via EXTRA_MAKE
The OpenTelemetry filter has been moved out of the haproxy source tree and
now lives and is developed in the haproxy-opentelemetry repository as an
external addon.  Removed all hard-coded references to USE_OTEL and to the
addons/otel directory from the main Makefile, as the addon is now plugged
in through the generic EXTRA_MAKE hook added in the previous commit.
2026-05-04 14:15:17 +02:00
William Lallemand
66bccb7a3a BUILD: add an EXTRA_MAKE option to build addons easily
Allow to call an external Makefile called Makefile.inc in order to build
complex addons.

make TARGET=linux-glibc ... EXTRA_MAKE="/path/to/addon1" \
     EXTRA_MAKE+="/path/to/addon2"
2026-05-04 13:49:26 +02:00
Willy Tarreau
0896ea43fc CLEANUP: mux-h2: remove the outdated condition to release h2c on timeout
The historical code dealing with timeout was left with a confusing
condition that is always true but always requires some analysis. It
would check if some streams were left pending before deciding to
release the connection. It could indeed be problematic to leave with
no timeout and an active connection!

As Christopher figured, the situation cannot exist because a first
check ensures there's no more h2c via h2c_may_expire(), then a call
to h2_wake_some_streams() will call h2s_wake_one_stream() for each
of the h2s, and all those with no h2c are purged. Thus on return
from h2_wake_some_streams() we're guaranteed to have an empty tree.

Let's just remove the condition and clean up the code.
2026-05-04 12:01:57 +02:00
Amaury Denoyelle
95ea3c5510 MINOR: quic: fix trace spacing when datagram is displayed
Adjust spacing between individual arguments for the QUIC trace
QUIC_EV_CONN_RCV when a datagram is displayed.
2026-05-04 11:18:47 +02:00
Amaury Denoyelle
a0a510f0d2 BUG/MINOR: quic: fix trace crash on datagram receive
Recently, datagram reception architecture has been completely reworked
to improve performance. A regression has been introduced when using
traces in qc_rcv_buf() : datagram argument is uninitialized after recv
syscall. This may cause a crash as CIDs buffer is dereferenced.

Fix this by removing dgram argument from the affected trace. A new trace
is added after quic_dgram_init() to keep the ability to display the
received content.

This issue has caused failure of all QUIC interop testing.

No need to backport.
2026-05-04 11:18:35 +02:00
William Lallemand
71267bc6a5 BUG/MEDIUM: acme: fix stalled renewal when opportunistic DNS check fails
In ACME_INITIAL_RSLV_READY, when the opportunistic DNS propagation check
fails and the code falls back to ACME_CLI_WAIT, ACME_RDY_INITIAL_DNS was
left set in cond_ready. Since the CLI-wait path only ever sets ACME_RDY_CLI
on auth->ready, the readiness check in ACME_CLI_WAIT could never be
satisfied, permanently stalling certificate renewal.

Fix this by stripping ACME_RDY_INITIAL_DNS from cond_ready before falling
back to the regular CLI-wait flow. Also replace the &= with a plain
assignment in the success path to make the intent explicit.

No backport needed, 3.4 only.
2026-05-04 11:06:12 +02:00
Amaury Denoyelle
63f853957a BUG/MINOR: quic: fix buffer overflow with sockaddr_in46
New type sockaddr_in46 has been recently introduced. It serves as a
union which can store either an IPv4 or IPv6 address. The objective is
to reduce the storage size for QUIC datagrams which previously uses a
sockaddr_storage field.

On qc_new_conn(), source and destination addresses from the datagram are
passed to the function as sockaddr_storage so that they are copied into
the newly built quic_conn instance. However, the involved memcpy() is
producing a buffer overflow as sockaddr_in46 is smaller than
sockaddr_storage type.

This patch fixes this by defining a new helper function
in46un_to_addr(). This allows to convert safely sockaddr_in46 to a plain
sockaddr type. The function is now used before invoking qc_new_conn().

Note that there is still other several places where union sockaddr_in46
is casted as sockaddr_storage type. However, these should be safe as in
these cases sockaddr fields are accessed individually after checking
ss_family. The memory issue only exists when plain memcpy is used.

This bug was detected using ASAN. It generates the following traces when
a QUIC connection is instantiated.

==37474==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7c3bb9a61100 at pc 0x5631f52c3946 bp 0x7ffc83e45b50 sp 0x7ffc83e45310
READ of size 128 at 0x7c3bb9a61100 thread T0
    #0 0x5631f52c3945 in __asan_memcpy (/home/amaury/work/haproxy-quic-dev/haproxy+0x3ae945) (BuildId: a7ccfd74b7a71a869b8ff8d13f6dcde8c82c1487)
    #1 0x5631f55f9e34 in qc_new_conn /home/amaury/work/haproxy-quic-dev/src/quic_conn.c:1311:2
    #2 0x5631f558d5c3 in quic_rx_pkt_retrieve_conn /home/amaury/work/haproxy-quic-dev/src/quic_rx.c:1875:10
    #3 0x5631f558330b in quic_dgram_parse /home/amaury/work/haproxy-quic-dev/src/quic_rx.c:2463:29
    #4 0x5631f5625da6 in quic_lstnr_dghdlr /home/amaury/work/haproxy-quic-dev/src/quic_sock.c:206:3
    #5 0x5631f6a64173 in run_tasks_from_lists /home/amaury/work/haproxy-quic-dev/src/task.c:660:26
    #6 0x5631f6a6ba1e in process_runnable_tasks /home/amaury/work/haproxy-quic-dev/src/task.c:913:9
    #7 0x5631f5e984c3 in run_poll_loop /home/amaury/work/haproxy-quic-dev/src/haproxy.c:2982:3
    #8 0x5631f5e9a715 in run_thread_poll_loop /home/amaury/work/haproxy-quic-dev/src/haproxy.c:3212:2
    #9 0x5631f5e9f732 in main /home/amaury/work/haproxy-quic-dev/src/haproxy.c:3853:2
    #10 0x7f2bba8276c0  (/usr/lib/libc.so.6+0x276c0) (BuildId: ca0db5ab57a36507d61bbcf4988d344974331f19)
    #11 0x7f2bba8277f8 in __libc_start_main (/usr/lib/libc.so.6+0x277f8) (BuildId: ca0db5ab57a36507d61bbcf4988d344974331f19)
    #12 0x5631f51be594 in _start (/home/amaury/work/haproxy-quic-dev/haproxy+0x2a9594) (BuildId: a7ccfd74b7a71a869b8ff8d13f6dcde8c82c1487)

No need to backport.
2026-05-04 10:49:49 +02:00
William Lallemand
9b722520a9 CI: github: add DEBUG_STRICT=2 to ASAN jobs
Add an DEBUG_STRICT=2 option to the ASAN jobs in order to trigger the
BUG_ON_HOT() conditions.
2026-04-30 17:46:30 +02:00
Willy Tarreau
e6b3dd7b34 CLEANUP: acl: remove duplicate test in parse_acl_expr() and unused variable
aclkw->match_type was compared twice to PAT_MATCH_DOM in parse_acl_expr().
After auditing the involved types, it's only a copy-paste mistake, as no
other matching method is missing, so let's drop it to avoid the confusion.
Also drop variable ckw which is assigned NULL and passed to free(), and is
clearly a leftover from a previous version.

No backport needed.
2026-04-30 17:39:26 +02:00
Willy Tarreau
dc93168f22 BUG/MINOR: pattern: release the reference on failure to load from file
In pattern_read_from_file(), in case of failure to load from the file,
the newly allocated reference remains attached to the list and is never
freed. Let's just do it.

This can be backported to all versions since it arrived in 1.5 with
commit 1e00d3853b ("MAJOR: pattern/map: Extends the map edition system
in the patterns").
2026-04-30 17:39:26 +02:00
Willy Tarreau
ecb901ff6c CLEANUP: map/cli: fix some map-related help messages
Some CLI help messages used to mention "acl" instead of "map", others had
unbalanced brackets. This can be backported if desired.
2026-04-30 17:39:26 +02:00
Willy Tarreau
67fdcebd79 BUG/MINOR: map: do not leak a map descriptor on load error
Maps can leak a map descriptor in sample_load_map() on error. This is
harmless since the process won't start after this, and this cannot be
used at run time. but it's cleaner to fix it. This can be backported.
2026-04-30 17:39:26 +02:00
Willy Tarreau
3fe3a189c2 BUG/MINOR: acl: fix a possible arg corruption in smp_fetch_acl_parse()
smp_fetch_acl_parse() first places the newly allocated ACL sample into
the first argument to be parsed, *before* parsing it. The type is not
changed so the first argument remains of type string. In case of error,
the allocated sample is released and release_sample_expr() will call
release_sample_arg() to release the argument, possibly freeing the
string present there. And here's the catch: by overwriting the first
arguments's ->ptr entry, it happens to be located over the ->str.size
location, to not be null and to still be freed, but by pure chance
thanks to aliasing. A slight reorder of the args or buffer fields
could place it in the ->area and provoke a double-free, or even always
make the first argument's parsing fail.

Let's move the assignment after the loop has succeeded instead, and
properly set type=ARGT_PTR so that we never try to free it. The bug
appeared with the "acl()" sample fetch in 2.9 with commit 7fccccccea
("MINOR: acl: add acl() sample fetch") so it can be backported to 3.0.
2026-04-30 17:39:26 +02:00
Amaury Denoyelle
2d6ebd0410 MINOR: h3/hq_interop: implement stream reset on shut abort/kill-conn
Adjust QUIC mux stream shut procedure when abort or kill-conn is
performed. Changes are implemented directly into lclose callback for
h3/h09 protocols.

On abort, the stream is resetted as previously. The only change is that
now a proper error code can be used, with REQUEST_CANCELLED specified
for HTTP/3 protocol.

Kill-conn is requested when a tcp-requect connection reject rule has
been executed. In this case the stream is resetted, and the connection
is also closed. This is identical to the H2 multiplexer. HTTP/3 protocol
uses EXCESSIVE_LOAD as error code in this case.
2026-04-30 17:18:20 +02:00
Amaury Denoyelle
d0bd6a946b MEDIUM: mux-quic: extend shut to app proto layer
Previously, shut callback was entirely implemented in QUIC mux layer.
However, this operation depends on the above application protocol, as it
may define its own closure procedure and error codes. This is the case
notably with HTTP/3 specification.

This patch defines a stream shut API between QUIC mux and application
protocol layers via the new qcc_app_ops callback lclose(). The closure
reason is specified via an enum argument. Application protcol can then
perform the stream closure as intended.

This patch is only an architecture adjustment but should not have any
functional impact. Stream closure logic was moved identically from QUIC
mux into h3 and h09 lclose callback.
2026-04-30 17:18:20 +02:00
Alexander Stephan
64383e655b BUG/MEDIUM: cli: fix master CLI connection slot leak on client disconnect
In master-worker mode the master CLI proxy (mworker_proxy) has a
hardcoded maxconn of 10. When a client connects to the master CLI
socket and issues a command that gets forwarded to an unresponsive
worker (e.g. one that is stuck or very slow), the connection hangs
waiting for the worker's response. If the client then disconnects
(timeout, Ctrl-C, etc.), the connection slot is never released because
the client-side FIN is never acknowledged by the unresponsive worker.

After 10 such leaked slots the master CLI socket becomes completely
unreachable, returning "Resource temporarily unavailable" to any new
connection attempt. In containerized deployments this means readiness
probes start failing and the pod gets restarted.

The fix adds a timeout server-fin of 1s to the mworker_proxy. When
the client disconnects while waiting for a worker response, this
timeout ensures the dangling backend connection is cleaned up after
1s, freeing the connection slot. This does not affect normal CLI
operations since the timeout only starts after the client has already
closed its side of the connection.

A regression test is included that blocks the worker CLI thread using
"debug dev delay" with nbthread 1, fills all 10 master CLI slots,
waits for client-side timeouts, then verifies a new connection still
succeeds.

This fixes GH issue #3351.

This should be backported to all stable branches.

Co-authored-by: Martin Strenge <github@trixer.net>
Co-authored-by: William Lallemand <wlallemand@haproxy.com>
2026-04-30 17:06:19 +02:00
Maxime Henrion
55ba951f3d BUG/MINOR: quic: handle cases where we don't have an address
It is possible to not have addresses in certain conditions, so we now
also allow the address family to be AF_UNSPEC in quic_dgram_init().

No backport necessary since this code is not yet in a release.
2026-04-30 16:54:28 +02:00
Maxime Henrion
cc231f3468 OPTIM: quic: reduce the size of struct quic_dgram
The QUIC code can only handle IPv4 or IPv6 addresses, so using two
sockaddr_storage structs wastes a lot of space in the quic_dgram struct.
This is a very large overhead since this structure is written in the MPSC
ring buffers before every datagram, while many of those datagrams are only
50 bytes or less. Using an union instead saves 200 bytes per datagram,
increasing the capacity of the buffers significantly.
2026-04-30 15:33:07 +02:00
Maxime Henrion
df0614b177 MINOR: quic: store the DCID as an offset
Using an offset instead of a pointer into the datagram buffer is less
error-prone as we do not have to manually fixup that pointer when the
datagram is moved somewhere else in memory.
2026-04-30 15:33:07 +02:00
Maxime Henrion
9b5f11cd3d OPTIM: quic: rework the QUIC RX code
Use an MPSC ring buffer to hold data for each datagram handler. Holding
this data in a per-handler buffer avoids the HoL blocking we experienced
when we had per-listener buffers with data from all threads mixed up
in them.

This also gets rid of the mt_list contention we were suffering before,
that was causing some threads to be stuck for a significant amount of
time, causing warnings and even crashes in some cases.
2026-04-30 15:33:07 +02:00
Maxime Henrion
57d8f06215 MINOR: add an MPSC ring buffer implementation
This is to be used in the QUIC code, where the multiple producers are
the listener threads, and the single consumer is the datagram handler
thread. Entries are variable-length with a size header, and are kept
contiguous in the buffer, so padding is inserted at the end when an
entry would otherwise wrap around. The size field is overloaded to also
mark padding (-1) and entries that are still free or not yet ready for
reads (0).

Headers and payloads are aligned on 8 bytes. Aligning on 16 bytes might
be beneficial on some architectures to let memcpy() use 128-bit SIMD
instructions.

The head and tail offsets are 64-bit unsigned integers, making ABA
issues from integer overflow impossible on current or near-future
hardware. Reservation uses a CAS rather than FAA because of the need to
insert padding to keep entries contiguous.
2026-04-30 15:33:07 +02:00
Willy Tarreau
66b00543c5 BUG/MINOR: hpack: validate idx > 0 in hpack_valid_idx()
HPACK indices start at 1, so idx=0 is invalid. The function only checked
the upper bound before, allowing idx=0 to pass as valid. This is harmless
as the code properly checks for existing name and values everywhere, but
then due to the call to hpack_idx_to_phdr(), index 0 will be taken for
:authority. Let's just make sure it's never zero.

This can be backported.
2026-04-30 09:33:31 +02:00
Willy Tarreau
2464c73526 BUG/MINOR: vars: only print first invalid char in fill_desc()
The variable name is passed as (ptr,len) suggesting that certain callers
might not always have 0-terminated strings (e.g. when used in arguments
where closing parenthesis or commas might follow), the error message
carefully tries to designate the first invalid character, yet by mistake
it prints the whole string. This mistake has been there since commit
4834bc773c ("MEDIUM: vars: adds support of variables") in 1.6. Let's
properly report the char as promised instead. This could be backported
but is totally unimportant.
2026-04-30 09:19:53 +02:00
Willy Tarreau
37b1f15d85 BUG/MINOR: vars: don't store the variable twice with set-var-fmt
In 2.5, commit 9a621ae76d ("MEDIUM: vars: add a new "set-var-fmt" action")
introduced the set-var-fmt action. However the storage (by then
sample_store_stream() now var_set()) was added for this specific
branch without any return, leaving the sample copied again over the
variable via the final call, meaning that the variable name is looked
up twice and for proc scope, the lock is taken twice for each call to
set-var-fmt.

This patch removes the first call. Tests show that proc operations
now jump from 1.1M to 1.67M/s on a 64-core CPU (lower lock contention),
while other scopes only observe a modest improvement with few vars
(10 goes from 43.3M to 44M/s). This could be backported.
2026-04-30 09:11:05 +02:00
Willy Tarreau
2b30330cdc BUG/MINOR: vars: make parse_store() return error on var_set() failure
In 2.5, variables in the scope "proc" were pre-created with commit
df8eeb1619 ("MEDIUM: vars: pre-create parsed SCOPE_PROC variables as
permanent ones"). However one test on var_set() was copy-pasted from
vars_check_args() into parse_store(), and the former returns 0 on
error while for the latter it's a success. This means that some errors
on variables of scope "proc" (typically alloc failure) can be missed
at boot time, probably either making that variable invisible or causing
a crash during boot.

Let's return ACT_RET_PRS_ERR instead. This can be backported.
2026-04-30 08:37:10 +02:00
Willy Tarreau
64e5489864 CLEANUP: net_helper: fix incorrect const pointers in writev_n16()
It's interesting to see that output pointers p1 and p2 were declared
as const, and that thisremained unnoticed due to the explicit casts
to u8 when writing to them. The function is currently not used, but
better clean it up to avoid surprises.
2026-04-30 08:21:33 +02:00
Willy Tarreau
76d894956f BUG/MINOR: sink: do not free existing sinks on allocation error
In 3.1 with commit 1bdf6e884a ("MEDIUM: sink: implement sink_find_early()")
sink creation started to support plugging to an existing sink and only
updating the description. However if the strdup() for the new description
failed, it would go down the error path releasing everything, while leaving
the released entry in the sink list and leaving other users still attached
to it.

Here we take a different approach, we first allocate the new description,
and release the old one on success, otherwise we leave the entry unchanged.
And we only release new sinks, not old ones. This way allocation errors
just do not change what is already referenced elsewhere, so that error
unrolling remains clean.

This remains of low importance anyway since it's only a case of boot
error aggravation. Not really needed to be backported.
2026-04-30 08:01:24 +02:00
William Lallemand
19e17fd6e2 BUG/MINOR: acme: skip auth/challenge steps when newOrder returns a certificate
When an ACME server returns a certificate URL directly in the newOrder
response (order already validated), parse it and transition straight to
ACME_CERTIFICATE, bypassing the auth/challenge steps.

This needs to be backported to 3.2.
2026-04-29 18:22:45 +02:00
William Lallemand
0f02a62da0 BUG/MEDIUM: acme: fix segfault on newOrder with empty authorizations
When an ACME server returns a newOrder response with an empty
authorizations array (certificate already validated), ctx->auths
remains NULL. The state machine then transitions to ACME_AUTH which
immediately dereferences ctx->next_auth, causing a segfault.

Return an error from acme_res_neworder() so the caller retries.

This needs to be backported to 3.2.
2026-04-29 18:22:45 +02:00
287 changed files with 4828 additions and 24185 deletions

1
.github/matrix.py vendored
View file

@ -207,6 +207,7 @@ def main(ref_name):
'OPT_CFLAGS="-O1"',
"USE_ZLIB=1",
"USE_OT=1",
"DEBUG=-DDEBUG_STRICT=2",
"OT_INC=${HOME}/opt-ot/include",
"OT_LIB=${HOME}/opt-ot/lib",
"OT_RUNPATH=1",

View file

@ -137,7 +137,7 @@ release. These branches were emitted at a pace of one per year since 1.5 in
2014. As of 2019, 1.5 is still supported and widely used, even though it very
rarely receives updates. After a few years these LTS branches enter a
"critical fixes only" status, which means that they will rarely receive a fix
but if that a critital issue affects them, a release will be made, with or
but if that a critical issue affects them, a release will be made, with or
without any other fix. Once a version is not supported anymore, it will not
receive any fix at all and it will really be time for you to upgrade to a more
recent branch. Please note that even when an upgrade is needed, a great care is

246
CHANGELOG
View file

@ -1,6 +1,252 @@
ChangeLog :
===========
2026/05/20 : 3.4-dev13
- BUG/MINOR: backend: correct parameter value validation in get_server_ph_post()
- BUG/MINOR: config/dns: properly fail on duplicate nameserver name detection
- BUG/MEDIUM: dns: fix long loops in additional records parse on name failure
- BUG/MEDIUM: resolvers: fix name compression pointer validation in resolv_read_name()
- BUG/MEDIUM: dns: fix memory leak of sockaddr in dns_session_init() error path
- CLEANUP: proxy: fix tiny mistakes in parse error messages
- CLEANUP: dns: fix misleading error messages in dns_stream_init()
- BUG/MINOR: server: better handling of OOM in srv_set_fqdn()
- BUG/MINOR: servers: use proper source of pool_conn_name in srv_settings_cpy()
- BUG/MEDIUM: server/cli: unlock server lock on failure in cli_parse_set_server
- BUG/MINOR: resolvers: fix dangling list pointer in resolvers_new() error paths
- BUG/MINOR: dns: fix dangling dgram pointer on dns_dgram_init() failure path
- BUG/MINOR: proxy: use proxy_drop() in parse_new_proxy() error path
- CLEANUP: resolvers: properly initialize the sample in resolv_action_do_resolve()
- BUG/MINOR: resolvers: report the expression error in the do-resolve() action parser
- BUG/MINOR: resolvers: fix leaked dgram and dns_ring struct in parse_resolve_conf()
- BUG/MINOR: resolvers: fix leaked fields on cfg_parse_resolvers() error paths
- BUG/MINOR: resolvers: fix missing task_idle destruction in resolvers_destroy()
- CLEANUP: proxy: fix duplicate declaration of cli_find_frontend in proxy.h
- CLEANUP: address a few typos and copy-paste errors in httpclient and dns
- DOC: internal: add a few rules about internal core principles
- BUG/MINOR: session/trace: use distinct flags for SESS_EV_END and _ERR
- CLEANUP: stick-table: uniformize the different action_inc_gpc*()
- REGTESTS: do not run quic/tls13_ssl_crt-list_filters in quic openssl compat mode
- REGTESTS: quic/issuers_chain_path: do not forget to enable QUIC compat mode
- BUG/MINOR: sock: store the connection error status
- BUG/MINOR: check: properly report errno in chk_report_conn_err()
- CLEANUP: tcpcheck: mention that we're a bit far for a sync errno
- BUG/MINOR: jwt: fix possible memory leak in convert_ecdsa_sig() error path
- CLEANUP: jwe: fix theoretical overflow in AAD length calculation
- DOC: config: further clarify that resolvers "default" exists
- MINOR: proxy: remove the experimental status on dynamic backends
- BUG/MEDIUM: limits: properly account for global.maxpipes in compute_ideal_maxconn()
- BUG/MINOR: jws: fix OpenSSL 3.0 version check from > to >=
- BUG/MINOR: jws: Add missing return value check (EVP_PKEY_get_bn_param)
- BUG/MINOR: server: Properly handle init-state value during haproxy startup
- BUG/MINOR: httpclient-cli: Destroy http-client context if failing to start it
- BUG/MEDIUM: h1: Skip all h2c values from Upgrade headers during parsing
- BUG/MINOR: h1: Don't mask websocket protocol if multiple protocols used
- MINOR: haterm: Don't init haterm master pipe if not used
- CLEANUP: haterm: Remove "(too old kernel)" from warning message during init
- BUG/MINOR: httpclient-cli: fix uninit variable in error label
- MINOR: mux: Rename the "token" from mux_proto_list to mux_proto
- MEDIUM: connections: Use both mux_proto and alpn to pick a mux
- MINOR: connection: define conn_select_mux_fe()
- MINOR: connection: define conn_select_mux_be()
- MINOR: connection/mux_quic: add MUX <init_xprt> field for QMux handshake
- MINOR: proxy/server: reject TCP ALPN h3 without experimental
- MEDIUM: ssl: allow h3/QMux negotiation without explicit proto
- BUG/MINOR: server: accept server IDs above 2^31 and clarify error message
- BUG/MINOR: backend: fix balance hash calculation when using hash-type none
- MINOR: server: support hash-key id32 for a cleaner distribution
- MINOR: backend: support hash-key guid for a stabler distribution
- MINOR: startup: support unprivileged chroot if possible
- MEDIUM: startup: add automatic chroot feature
- MINOR: h2: explain committed_extra_streams dec on h2_init() error
- OPTIM: h2: do not update committed streams if elasticity disabled
- MINOR: mux_quic: implement basic committed_extra_streams accounting
- MINOR: quic: use stream elasticity value for initial advertisement
- MINOR: mux_quic: define ms_bidi_rel QCC member
- MAJOR: mux_quic: support stream elasticity during connection lifetime
- BUG/MEDIUM: servers: Store the connection hash with the parameter cache
- BUG/MINOR: prevent conn leak in case of xprt_qmux init failure
- BUILD: traces: set a few __maybe_unused on vars used only for traces
- BUILD: traces: add USE_TRACE allowing to disable traces
- MINOR: startup: do not execute chroot() when "/"
- MEDIUM: startup: warn when chroot is not set for root
- BUG/MEDIUM: servers: Don't forget to set srv_hash when needed
- DOC: fix typo on QUIC stream.max-concurrent reference
- BUG/MINOR: mux_quic: do not exceed stream.max-concurrent on backend side
- BUG/MINOR: htx: Fix value of HTX_XFER_HDRS_ONLY flag
- MEDIUM: htx: Improve htx_xfer API to not count HTX meta-data
- BUG/MEDIUM: applet: Fix transfer of HTX data to the applet
- BUG/MEDIUM: htx: Alloc a chunk of right size in htx_replace_blk_value()
- MEDIUM: stick-tables: Avoid freeing elements while holding a lock
- MINOR: intops: add a multiply overflow detection for ulong and size_t
- CLEANUP: tree-wide: use array_size_or_fail() in array size for allocations
- DOC: update supported gcc and openssl versions in INSTALL
2026/05/13 : 3.4-dev12
- SCRIPTS: announce-release: add a link to the OpenTelemetry filter
- BUG/MEDIUM: servers: Only requeue servers if they are up
- MINOR: tinfo: store the number of committed extra streams in the tgroup
- MINOR: connection: add a function to calculate elastic streams limit
- MINOR: mux-h2: consider the elastic streams limit on frontend
- MINOR: lb: make LB initialization even more declarative
- BUG/MINOR: cfgparse-listen: do not emit extraneous line in rule order warnings
- CLEANUP: tree-wide: fix typos in non user-visible comments in 15 files
- CLEANUP: h1/htx: fix a few typos in warning, debug and trace messages
- BUG/MINOR: mux-h1: only check h1s if not NULL
- BUG/MINOR: http-fetch: fix smp_fetch_hdr_ip()'s handling of brackets for IPv6
- BUG/MINOR: http-fetch: make http_first_req() check for HTTP first
- BUG/MINOR: http-act: set-status() must check the response message, not the request
- BUG/MINOR: tools: fix memory leak in env_expand() error path
- BUG/MINOR: auth: free user groups on error paths in userlist_postinit()
- BUG/MINOR: uri-auth: avoid leaks on initialization error
- BUG/MINOR: cache: fix memory leak in parse_cache_rule error path
- BUG/MINOR: cfgcond: make KQUEUE check for GTUNE_USE_KQUEUE not GTUNE_USE_EPOLL
- BUG/MINOR: mqtt: connack parser returns MQTT_NEED_MORE_DATA on unknown property
- BUG/MINOR: mqtt: connect parser uses wrong bit field for TOPIC_ALIAS_MAXIMUM
- BUG/MINOR: mqtt: connack parser uses wrong bit for SUBSCRIPTION_IDENTIFIERS_AVAILABLE
- BUG/MINOR: mqtt: fix PUBLISH flags validation that want all bits to be set
- CLEANUP: http_htx: rename inner 'type' to 'ptype' to avoid variable shadowing
- CLEANUP: mux-h2: fix minor output debugging format issues
- CLEANUP: http-rules: fix a few '&' vs '&&' checks for clarity
- CLEANUP: auth: remove undeclared auth_resolve_groups() from auth.h
- CLEANUP: cache: remove redundant res_htx assignment in http_cache_io_handler()
- CLEANUP: channel: remove bogus and unused definition of channel_empty()
- CLEANUP: flt_http_comp: remove duplicate rate limit and CPU usage checks
- CLEANUP: mqtt: remove duplicate MQTT_FN_BIT_USER_PROPERTY in CONNECT fields
- BUG/MINOR: uri-auth: fix possible null-deref in latest fix for leaks
- BUG/MEDIUM: tasks: Keep the TASK_RUNNING flag until queued
- CLEANUP: mqtt: fix spelling of shared_subscription_available
- CLEANUP: regex: pre-initialize error variable in regex_comp() to calm analysis
- BUILD: compiler: fix redefinition of __nonstring
- CLEANUP: defaults: adjust MAX_THREADS multiplier number in comment
- CLEANUP: src/cpuset.c: fix missing return in functions returning int
- REGTESTS: Use ${tmpdir} instead of hardcoded /tmp/
- REGTESTS: Don't try to use real nameservers for testcases
- CLEANUP: tree-wide: fix typos in non user-visible comments in 3 more files
- MINOR: cli: improve forward compatibility for show fd
- DOC: management: document the <tgid>/<fd> form of show fd
- CLEANUP: tree-wide: fix more typos and outdated explanations in comments
- BUG/MEDIUM: dict: hold read lock while incrementing refcount in dict_insert
- BUG/MEDIUM: http-client: Only consume input buffer when hc one is empty
- BUG/MINOR: xprt_qstrm: fix conflicting prototype
- REORG: mux_quic: use newer qcm prefix for legacy qmux files
- MINOR: mux_quic: use qcm prefix for mux callbacks
- MINOR: mux_quic: use qcm prefix for mux functions
- MINOR: mux_quic: use qcm prefix for traces functions/structs
- MINOR: mux_quic: rename qstrm files to qmux
- MINOR: mux_quic: remove qstrm naming in QUIC MUX
- MINOR: connection: rename QMux related flags
- MINOR: xprt_qmux: use qmux instead of qstrm naming
- MINOR: trace: implement source alias
- MEDIUM: mux_quic: rename qmux traces to qcm
- MINOR: sample: add a generic reverse converter
- MINOR: sample: add a reverse_dom converter
- DOC: proxy-protocol: clarify UDP usage
- BUILD: 51d.c: cleanup, fix preprocessor ifdefs
- CLEANUP: tree-wide: fix typos in user-invisible files
- CLEANUP: htx: Adjust numbering of HTX blocks' types in the description
2026/05/08 : 3.4-dev11
- BUG/MEDIUM: acme: fix segfault on newOrder with empty authorizations
- BUG/MINOR: acme: skip auth/challenge steps when newOrder returns a certificate
- BUG/MINOR: sink: do not free existing sinks on allocation error
- CLEANUP: net_helper: fix incorrect const pointers in writev_n16()
- BUG/MINOR: vars: make parse_store() return error on var_set() failure
- BUG/MINOR: vars: don't store the variable twice with set-var-fmt
- BUG/MINOR: vars: only print first invalid char in fill_desc()
- BUG/MINOR: hpack: validate idx > 0 in hpack_valid_idx()
- MINOR: add an MPSC ring buffer implementation
- OPTIM: quic: rework the QUIC RX code
- MINOR: quic: store the DCID as an offset
- OPTIM: quic: reduce the size of struct quic_dgram
- BUG/MINOR: quic: handle cases where we don't have an address
- BUG/MEDIUM: cli: fix master CLI connection slot leak on client disconnect
- MEDIUM: mux-quic: extend shut to app proto layer
- MINOR: h3/hq_interop: implement stream reset on shut abort/kill-conn
- BUG/MINOR: acl: fix a possible arg corruption in smp_fetch_acl_parse()
- BUG/MINOR: map: do not leak a map descriptor on load error
- CLEANUP: map/cli: fix some map-related help messages
- BUG/MINOR: pattern: release the reference on failure to load from file
- CLEANUP: acl: remove duplicate test in parse_acl_expr() and unused variable
- CI: github: add DEBUG_STRICT=2 to ASAN jobs
- BUG/MINOR: quic: fix buffer overflow with sockaddr_in46
- BUG/MEDIUM: acme: fix stalled renewal when opportunistic DNS check fails
- BUG/MINOR: quic: fix trace crash on datagram receive
- MINOR: quic: fix trace spacing when datagram is displayed
- CLEANUP: mux-h2: remove the outdated condition to release h2c on timeout
- BUILD: add an EXTRA_MAKE option to build addons easily
- BUILD: otel: removed USE_OTEL, addon is now built via EXTRA_MAKE
- CLEANUP: otel: move opentelemetry outside haproxy sources
- BUG/MEDIUM: mux-h2: fix the body_len to check when parsing request trailers
- BUG/MAJOR: mux-h2: preset MSGF_BODY_CL on H2_SF_DATA_CLEN in h2c_dec_hdrs()
- DOC: otel: update the filter's status and URL in the docs
- DOC: acme: document missing acme-vars and provider-name keywords
- BUG/MINOR: dns: always validate the source address in responses
- BUG/MINOR: tcpcheck: Properly report error for http health-checks
- CLEANUP: resolvers: Remove duplicated line when resolvers proxy is initialized
- BUG/MINOR: resolvers: Free new requester on error when linking a resolution
- BUG/MINOR: resolvers: Fix lookup for a hostname in the state-file tree
- BUG/MINOR: resolvers: Free opts on parse error in resolv_parse_do_resolve()
- BUG/MAJOR: net_helper: also fix tcp_options_list for OOB write loop
- BUG/MEDIUM: ssl/sample: check output buffer size in aes_cbc_enc converter
- BUG/MAJOR: http-ana: fix private session retrieval on NTLM
- REGTESTS: add a regtest to validate various NTLM transitions
- BUG/MEDIUM: mworker/cli: fix user and operator permission via @@<pid> in master CLI
- BUG/MINOR: mworker/cli: check ci_insert() return value in pcli_parse_request()
- REGTESTS: http-messaging: always send RFC8441 client settings to use ext connect
- BUG/MINOR: h2: add decoding for :protocol in traces
- BUG/MINOR: mux-h2: condition the processing of 8441 extension to global setting
- MINOR: mux-h2: add a new message flag to indicate ext connect support
- BUG/MINOR: h2: only accept :protocol with extended CONNECT
- BUG/MINOR: acme: contact mail should be optional, don't pass ToS bool
- CLEANUP: http-fetch: Remove duplcated return statement in smp_fetch_stver()
- CLEANUP: http-fetch: Adjust smp_fetch_url32_src() comment
- CLEANUP: http-fetch: Fix indentation of sample_fetch_keywords
- BUG/MINOR: http_fetch: Check return values of unchecked buffer operations
- BUG/MINOR: http-fetch: Fix http_auth_bearer() when custom header is used
- BUG/MEDIUM: h1_htx: Remove reverved block on error during contig chunks parsing
- CLEANUP: haterm: Remove duplicated bloc to know if haterm must drain
- BUG/MINOR: haterm: Immediately report error when draining the request
- CLEANUP: haterm: Remove useless IS_HTX_SC() test
- BUG/MINOR: haterm: Fix a possible integer overflow on the request body length
- BUG/MEDIUM: haterm: Subscribe for receives until request was fully drained
- BUG/MINOR: haterm: Don't set HTX_FL_EOM flag on 100-Continue responses
- BUG/MEDIUM: haterm: Properly handle end of request and end of response
- BUG/MEDIUM: haterm: Properly handle client timeout
- BUG/MINOR: haterm: Fix condition to use direct data forwarding
- BUG/MINOR: haterm: Report a 400-bad-request error on receive error
- DEBUG: haterm: Add hstream flags in the trace messages
- MINOR: haterm: Remove now useless req_body field from hstream
- MINOR: mux_quic: reset stream after app shutdown for HTTP/0.9
- MINOR: mux_quic: do not perform unnecessary timeout handling on BE side
- BUG/MEDIUM: mux_quic: adjust qcc_is_dead() to account detached streams
- MINOR: mux_quic: simplify MUX_CTL_GET_NBSTRM
- MINOR: ssl: Export 'current_crtstore_name'
- MINOR: ssl: Factorize code from "new/set ssl cert" CLI command
- MINOR: ssl: Factorize ckch instance rebuild process
- MEDIUM: ssl: Refactorize "commit ssl cert"
- BUG/MINOR: ssl: Use the sequence number with kTLS and TLS 1.2
- BUG/MINOR: mux_quic: fix max stream ID reuse estimation
- MINOR: mux_quic: release BE conns if reuse definitely blocked
- BUG/MINOR: mux_quic: refresh timeout only if I/O performed
- MEDIUM: mux-h1: Return an error on h2 upgrade attempts if not allowed
- BUG/MEDIUM: mux-h2: Properly consume padding for DATA frames
- MEDIUM: tools: read_line_to_trash() handle empty files without \n
- MINOR: jws: support HMAC in jws_b64_protected(), make nonce optional
- MINOR: jws: introduce jws_b64_hmac_signature() function for HMAC signing
- MINOR: acme: implement EAB - external account binding
- MINOR: acme: allow specifying custom MAC alg for EAB
- REGTESTS: Fix h1_to_h2_upgrade.vtc to force h2 on first bind line
- MINOR: cli: allow specifying a tgid with show fd
- Revert "BUG/MEDIUM: cli: fix master CLI connection slot leak on client disconnect"
- BUILD: use Makefile.mk instead of Makefile.inc in EXTRA_MAKE
- Revert "BUG/MINOR: mux-h2: condition the processing of 8441 extension to global setting"
- BUG/MEDIUM: mux-h2: fix the detection of the ext connect support
- MINOR: jwe: Add option to enable/disable algorithms or encryption algorithms for jwt_decrypt
- MINOR: jwe: Disable 'RSA1_5' algorithm by default in jwt_decrypt converters
- BUG/MEDIUM: jwe: Fix jwt.decrypt_alg_list to work correctly
- BUG/MEDIUM: stick-table: properly check permissions on CLI's set/clear cmd
- DOC: acme: EAB is now supported
2026/04/29 : 3.4-dev10
- DOC: config: fix spelling of "max-threads-per-group" in the index
- MEDIUM: threads: change the default max-threads-per-group value to 16

View file

@ -111,12 +111,12 @@ HAProxy requires a working GCC or Clang toolchain and GNU make :
may want to retry with "gmake" which is the name commonly used for GNU make
on BSD systems.
- GCC >= 4.7 (up to 15 tested). Older versions are no longer supported due to
- GCC >= 4.7 (up to 16 tested). Older versions are no longer supported due to
the latest mt_list update which only uses c11-like atomics. Newer versions
may sometimes break due to compiler regressions or behaviour changes. The
version shipped with your operating system is very likely to work with no
trouble. Clang >= 3.0 is also known to work as an alternative solution, and
versions up to 19 were successfully tested. Recent versions may emit a bit
versions up to 21 were successfully tested. Recent versions may emit a bit
more warnings that are worth reporting as they may reveal real bugs. TCC
(https://repo.or.cz/tinycc.git) is also usable for developers but will not
support threading and was found at least once to produce bad code in some
@ -237,7 +237,7 @@ to forcefully enable it using "USE_LIBCRYPT=1".
-----------------
For SSL/TLS, it is necessary to use a cryptography library. HAProxy currently
supports the OpenSSL library, and is known to build and work with branches
1.0.0, 1.0.1, 1.0.2, 1.1.0, 1.1.1, and 3.0 to 3.6. It is recommended to use
1.0.0, 1.0.1, 1.0.2, 1.1.0, 1.1.1, and 3.0 to 4.0. It is recommended to use
at least OpenSSL 1.1.1 to have support for all SSL keywords and configuration
in HAProxy. OpenSSL follows a long-term support cycle similar to HAProxy's,
and each of the branches above receives its own fixes, without forcing you to

View file

@ -44,6 +44,7 @@
# USE_CLOSEFROM : enable use of closefrom() on *bsd, solaris. Automatic.
# USE_PRCTL : enable use of prctl(). Automatic.
# USE_PROCCTL : enable use of procctl(). Automatic.
# USE_TRACE : enable trace subsystem. Always on.
# USE_ZLIB : enable zlib library support and disable SLZ
# USE_SLZ : enable slz library instead of zlib (default=enabled)
# USE_CPU_AFFINITY : enable pinning processes to CPU on Linux. Automatic.
@ -60,7 +61,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
# EXTRA_MAKE : space-separated list of external addons using a Makefile.inc
# 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
@ -129,11 +130,6 @@
# 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, ...).
@ -348,12 +344,12 @@ use_opts = USE_EPOLL USE_KQUEUE USE_NETFILTER USE_POLL \
USE_TPROXY USE_LINUX_TPROXY USE_LINUX_CAP \
USE_LINUX_SPLICE USE_LIBCRYPT USE_CRYPT_H USE_ENGINE \
USE_GETADDRINFO USE_OPENSSL USE_OPENSSL_WOLFSSL USE_OPENSSL_AWSLC \
USE_ECH \
USE_ECH USE_TRACE \
USE_SSL USE_LUA USE_ACCEPT4 USE_CLOSEFROM USE_ZLIB USE_SLZ \
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_OTEL USE_QUIC USE_PROMEX \
USE_THREAD_DUMP USE_EVPORTS USE_OT 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 \
@ -371,6 +367,9 @@ $(warn_unknown_options)
# on the make command line.
USE_POLL = default
# traces are always enabled
USE_TRACE = default
# SLZ is always supported unless explicitly disabled by passing USE_SLZ=""
# or disabled by enabling ZLIB using USE_ZLIB=1
ifeq ($(USE_ZLIB:0=),)
@ -672,11 +671,12 @@ OPTIONS_OBJS += src/mux_quic.o src/h3.o src/quic_rx.o src/quic_tx.o \
src/quic_cc_bbr.o src/quic_retry.o \
src/cfgparse-quic.o src/xprt_quic.o src/quic_token.o \
src/quic_ack.o src/qpack-dec.o src/quic_cc_newreno.o \
src/qmux_http.o src/qmux_trace.o src/quic_rules.o \
src/qcm_http.o src/qcm_trace.o src/quic_rules.o \
src/quic_cc_nocc.o src/quic_cc.o src/quic_pacing.o \
src/h3_stats.o src/quic_stats.o src/qpack-enc.o \
src/qpack-tbl.o src/quic_cc_drs.o src/quic_fctl.o \
src/quic_enc.o src/mux_quic_qstrm.o src/xprt_qstrm.o
src/quic_enc.o src/qcm_qmux.o src/xprt_qmux.o \
src/mpring.o
endif
ifneq ($(USE_QUIC_OPENSSL_COMPAT:0=),)
@ -869,8 +869,8 @@ ifneq ($(USE_OT:0=),)
include addons/ot/Makefile
endif
ifneq ($(USE_OTEL:0=),)
include addons/otel/Makefile
ifneq ($(EXTRA_MAKE),)
include $(addsuffix /Makefile.mk,$(EXTRA_MAKE))
endif
# better keep this one close to the end, as several libs above may need it
@ -1181,7 +1181,6 @@ 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

@ -1,2 +1,2 @@
$Format:%ci$
2026/04/29
2026/05/20

View file

@ -1 +1 @@
3.4-dev10
3.4-dev13

View file

@ -550,6 +550,8 @@ static void _51d_process_match(const struct arg *args, struct sample *smp)
char valuesBuffer[1024];
#endif
#if defined(FIFTYONEDEGREES_H_PATTERN_INCLUDED) || defined(FIFTYONEDEGREES_H_TRIE_INCLUDED) || defined(FIFTYONE_DEGREES_HASH_INCLUDED)
char no_data[] = "NoData"; /* response when no data could be found */
struct buffer *temp = get_trash_chunk();
int i = 0, found;
@ -636,6 +638,7 @@ static void _51d_process_match(const struct arg *args, struct sample *smp)
smp->data.u.str.area = temp->area;
smp->data.u.str.data = temp->data;
}
#endif
/* Sets the sample data as a constant string. This ensures that the
* string will be processed correctly.

View file

@ -48,13 +48,12 @@ Currently, tracers that support this API include Datadog, Jaeger, LightStep
and Zipkin.
Note: The OpenTracing filter shouldn't be used for new designs as OpenTracing
itself is no longer maintained nor supported by its authors. A
replacement filter base on OpenTelemetry is currently under development
and is expected to be ready around HAProxy 3.2. As such OpenTracing will
be deprecated in 3.3 and removed in 3.5.
itself is no longer maintained nor supported by its authors. As such
OpenTracing will be deprecated in 3.3 and removed in 3.5. A replacement
filter based on OpenTelemetry is available since 3.4 with complete build
instructions currently at:
The OT filter was primarily tested with the Jaeger tracer, while configurations
for both Datadog and Zipkin tracers were also set in the test directory.
https://github.com/haproxytech/haproxy-opentelemetry/
The OT filter is a standard HAProxy filter, so what applies to others also
applies to this one (of course, by that I mean what is described in the

View file

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

View file

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

View file

@ -1,80 +0,0 @@
# 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)

File diff suppressed because it is too large Load diff

View file

@ -1,456 +0,0 @@
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

@ -1,949 +0,0 @@
-----------------------------------------
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 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)
background_thread_wait_for idle timeout for the HTTP background thread
in milliseconds; 0 means the thread never
exits on its own (integer, default: 0). If
this option is set, 'insecure-fork-wanted'
must be used in the HAProxy configuration,
otherwise HAProxy may crash while exporting
OTel data
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.

View file

@ -1,725 +0,0 @@
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).

View file

@ -1,723 +0,0 @@
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/dontlog-normal).
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

View file

@ -1,101 +0,0 @@
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.

View file

@ -1,277 +0,0 @@
## 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
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>

View file

@ -1,28 +0,0 @@
/* 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
*/

View file

@ -1,313 +0,0 @@
/* 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

@ -1,128 +0,0 @@
/* 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

@ -1,34 +0,0 @@
/* 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

@ -1,55 +0,0 @@
/* 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

@ -1,98 +0,0 @@
/* 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
*/

View file

@ -1,152 +0,0 @@
/* 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

@ -1,55 +0,0 @@
/* 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

@ -1,46 +0,0 @@
/* 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

@ -1,31 +0,0 @@
/* 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

@ -1,57 +0,0 @@
/* 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>
#ifdef USE_THREAD
# include <pthread.h>
#endif
#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

@ -1,27 +0,0 @@
/* 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

@ -1,221 +0,0 @@
/* 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

@ -1,71 +0,0 @@
/* 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
*/

View file

@ -1,174 +0,0 @@
/* 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
*/

View file

@ -1,104 +0,0 @@
/* 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

@ -1,52 +0,0 @@
/* 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
*/

View file

@ -1,457 +0,0 @@
/* 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 "dontlog-normal"
* 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
*/

View file

@ -1,885 +0,0 @@
/* 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 - 1) 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
* 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, and spans_to_finish. 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
*/

View file

@ -1,849 +0,0 @@
/* 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
*/

File diff suppressed because it is too large Load diff

View file

@ -1,378 +0,0 @@
/* 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
*/

View file

@ -1,324 +0,0 @@
/* 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) == 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
*/

View file

@ -1,289 +0,0 @@
/* 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
*/

File diff suppressed because it is too large Load diff

View file

@ -1,385 +0,0 @@
/* 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
*/

View file

@ -1,745 +0,0 @@
/* 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
*/

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,85 +0,0 @@
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.

View file

@ -1,149 +0,0 @@
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

@ -1,53 +0,0 @@
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

@ -1,124 +0,0 @@
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

@ -1,158 +0,0 @@
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.

View file

@ -1,134 +0,0 @@
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

@ -1,144 +0,0 @@
--- 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

@ -1,144 +0,0 @@
--- 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

@ -1,144 +0,0 @@
--- 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

@ -1,144 +0,0 @@
--- 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

@ -1,319 +0,0 @@
-----------------------------------------
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

@ -1,19 +0,0 @@
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

@ -1,61 +0,0 @@
[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

@ -1,246 +0,0 @@
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

@ -1,22 +0,0 @@
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

@ -1,81 +0,0 @@
[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

@ -1,246 +0,0 @@
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

View file

@ -1,24 +0,0 @@
#!/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

@ -1,28 +0,0 @@
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

@ -1,196 +0,0 @@
[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

@ -1,246 +0,0 @@
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

@ -1,19 +0,0 @@
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

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

View file

@ -1,246 +0,0 @@
exporters:
exporter_traces_otlp_file:
type: otlp_file
thread_name: "OTLP/file trace"
file_pattern: "__empty_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: __empty_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: "__empty_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: __empty_metrics
exporter_metrics_memory:
type: memory
buffer_size: 256
exporter_logs_otlp_file:
type: otlp_file
thread_name: "OTLP/file logs"
file_pattern: "__empty_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: __empty_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-empty"
- service.name: "empty"
- service.namespace: "HAProxy traces test"
provider_metrics:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-empty"
- service.name: "empty"
- service.namespace: "HAProxy metrics test"
provider_logs:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-empty"
- service.name: "empty"
- 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

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

View file

@ -1,73 +0,0 @@
[otel-test-fe]
otel-instrumentation otel-test-instrumentation
config fe/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 client_session_end
scopes server_session_start
scopes tcp_response
scopes http_response
scopes server_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"
span "HAProxy session"
inject "otel-ctx" use-headers
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

@ -1,246 +0,0 @@
exporters:
exporter_traces_otlp_file:
type: otlp_file
thread_name: "OTLP/file trace"
file_pattern: "__fe_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: __fe_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: "__fe_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: __fe_metrics
exporter_metrics_memory:
type: memory
buffer_size: 256
exporter_logs_otlp_file:
type: otlp_file
thread_name: "OTLP/file logs"
file_pattern: "__fe_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: __fe_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-fe"
- service.name: "fe"
- service.namespace: "HAProxy traces test"
provider_metrics:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-fe"
- service.name: "fe"
- service.namespace: "HAProxy metrics test"
provider_logs:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-fe"
- service.name: "fe"
- 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

@ -1,28 +0,0 @@
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-full-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-full config full/otel.cfg
# run response scopes for successful responses
http-response otel-group otel-test-full http_response_group if acl-http-status-ok
# run after-response scopes for error responses
http-after-response otel-group otel-test-full http_after_response_group if !acl-http-status-ok
backend servers-backend
server server-1 127.0.0.1:8000

View file

@ -1,280 +0,0 @@
[otel-test-full]
otel-instrumentation otel-test-instrumentation
debug-level 0x77f
log localhost:514 local7 debug
config full/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 on_idle_timeout
scopes on_backend_set
scopes client_session_start
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 http_headers_request
scopes http_end_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 http_headers_response
scopes http_end_response
scopes http_reply
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 http_response_2
span "HTTP response"
event "event_date" "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 on_stream_start
instrument udcnt_int "haproxy.sessions.active" desc "Active sessions" value int(1) unit "{session}"
instrument gauge_int "haproxy.fe.connections" desc "Frontend connections" value fe_conn unit "{connection}"
span "HAProxy session" root
baggage "haproxy_id" var(sess.otel.uuid)
event "event_ip" "src" src str(":") src_port
event "event_be" "be" be_id str(" ") be_name
event "event_ip" "dst" dst str(":") dst_port
event "event_fe" "fe" fe_id str(" ") fe_name
log-record trace id 1000 event "session-start" span "HAProxy session" attr "attr_1_key" src attr "attr_2_key" src_port 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 *
log-record info event "session-stop" str("stream stopped")
otel-event on-stream-stop
otel-scope on_idle_timeout
idle-timeout 1s
span "heartbeat" parent "HAProxy session"
attribute "idle.elapsed" str("idle-check")
instrument cnt_int "idle.count" value int(1)
instrument update "idle.count"
log-record info str("heartbeat")
otel-event on-idle-timeout
otel-scope on_backend_set
span "Backend set" parent "HAProxy session"
attribute "backend.name" be_name
attribute "backend.id" be_id
instrument cnt_int "haproxy.backend.set" desc "Backend assignments" value int(1) unit "{assignment}"
instrument update "haproxy.backend.set"
log-record info id 1010 event "backend-set" span "Backend set" be_name
otel-event on-backend-set
otel-scope client_session_start
span "Client session" parent "HAProxy session"
instrument cnt_int "haproxy.client.session.start" desc "Client session starts" value int(1) unit "{session}"
log-record info id 1001 event "client-session-start" span "Client session" src str(":") src_port
otel-event on-client-session-start
otel-scope frontend_tcp_request
span "Frontend TCP request" parent "Client session"
instrument cnt_int "haproxy.tcp.request.fe" desc "Frontend TCP requests" value int(1) unit "{request}"
log-record info event "frontend-tcp-request" span "Frontend TCP request" src str(":") src_port
otel-event on-frontend-tcp-request
otel-scope http_wait_request
span "HTTP wait request" parent "Frontend TCP request"
finish "Frontend TCP request"
log-record info event "http-wait-request" span "HTTP wait request" str("waiting")
otel-event on-http-wait-request
otel-scope http_body_request
span "HTTP body request" parent "HTTP wait request"
finish "HTTP wait request"
log-record info event "http-body-request" span "HTTP body request" str("body")
otel-event on-http-body-request
otel-scope frontend_http_request
instrument cnt_int "haproxy.http.requests" desc "HTTP request count" value int(1) unit "{request}"
instrument hist_int "haproxy.http.latency" desc "HTTP request latency" value lat_ns_tot unit "ns" aggr "histogram" bounds "1000 1000000 1000000000"
instrument update "haproxy.http.latency" attr "phase" str("request")
instrument update "haproxy.tcp.request.fe"
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"
log-record info id 1002 event "http-request" span "Frontend HTTP request" attr "http.method" method method url
otel-event on-frontend-http-request
otel-scope switching_rules_request
span "Switching rules request" parent "Frontend HTTP request"
finish "Frontend HTTP request"
log-record info event "switching-rules" span "Switching rules request" be_name
otel-event on-switching-rules-request
otel-scope backend_tcp_request
span "Backend TCP request" parent "Switching rules request"
finish "Switching rules request"
instrument cnt_int "haproxy.tcp.request.be" desc "Backend TCP requests" value int(1) unit "{request}"
log-record info event "backend-tcp-request" span "Backend TCP request" be_name
otel-event on-backend-tcp-request
otel-scope backend_http_request
instrument update "haproxy.tcp.request.be"
span "Backend HTTP request" parent "Backend TCP request"
finish "Backend TCP request"
log-record info event "backend-http-request" span "Backend HTTP request" be_name
otel-event on-backend-http-request
otel-scope process_server_rules_request
span "Process server rules request" parent "Backend HTTP request"
finish "Backend HTTP request"
log-record info event "server-rules" span "Process server rules request" str("processing")
otel-event on-process-server-rules-request
otel-scope http_process_request
span "HTTP process request" parent "Process server rules request"
finish "Process server rules request"
log-record info event "http-process" span "HTTP process request" str("processing")
otel-event on-http-process-request
otel-scope tcp_rdp_cookie_request
span "TCP RDP cookie request" parent "HTTP process request"
finish "HTTP process request"
log-record info event "tcp-rdp-cookie" span "TCP RDP cookie request" str("cookie")
otel-event on-tcp-rdp-cookie-request
otel-scope process_sticking_rules_request
span "Process sticking rules request" parent "TCP RDP cookie request"
finish "TCP RDP cookie request"
log-record info event "sticking-rules" span "Process sticking rules request" str("sticking")
otel-event on-process-sticking-rules-request
otel-scope http_headers_request
span "HTTP headers request" parent "Process sticking rules request"
finish "Process sticking rules request"
instrument cnt_int "haproxy.http.headers.request" desc "Request headers processed" value int(1) unit "{header}"
log-record info event "http-headers-request" span "HTTP headers request" method url
otel-event on-http-headers-request
otel-scope http_end_request
span "HTTP end request" parent "HTTP headers request"
finish "HTTP headers request"
instrument cnt_int "haproxy.http.end.request" desc "Request end events" value int(1) unit "{request}"
instrument update "haproxy.http.headers.request"
log-record info event "http-end-request" span "HTTP end request" str("end")
otel-event on-http-end-request
otel-scope client_session_end
instrument update "haproxy.sessions.active"
instrument update "haproxy.client.session.start"
instrument update "haproxy.http.end.request"
finish "*req*"
log-record info event "client-session-end" str("session ended")
otel-event on-client-session-end
otel-scope server_unavailable
finish "*req*" "*res*"
log-record warn event "server-unavailable" str("503 Service Unavailable")
otel-event on-server-unavailable
otel-scope server_session_start
span "Server session" parent "HAProxy session"
link "HAProxy session" "Client session"
finish "HTTP end request"
instrument cnt_int "haproxy.server.session.start" desc "Server session starts" value int(1) unit "{session}"
log-record info event "server-session-start" span "Server session" str("server session")
otel-event on-server-session-start
otel-scope tcp_response
span "TCP response" parent "Server session"
instrument cnt_int "haproxy.tcp.response" desc "TCP responses" value int(1) unit "{response}"
log-record info event "tcp-response" span "TCP response" str("tcp response")
otel-event on-tcp-response
otel-scope http_wait_response
instrument update "haproxy.tcp.response"
span "HTTP wait response" parent "TCP response"
finish "TCP response"
log-record info event "http-wait-response" span "HTTP wait response" str("waiting")
otel-event on-http-wait-response
otel-scope process_store_rules_response
span "Process store rules response" parent "HTTP wait response"
finish "HTTP wait response"
log-record info event "store-rules" span "Process store rules response" str("store rules")
otel-event on-process-store-rules-response
otel-scope http_response
instrument update "haproxy.http.requests" attr "phase" str("response")
instrument update "haproxy.http.latency" attr "phase" str("response")
instrument update "haproxy.fe.connections"
span "HTTP response" parent "Process store rules response"
attribute "http.status_code" status
finish "Process store rules response"
log-record info id 1003 event "http-response" span "HTTP response" status
otel-event on-http-response
otel-scope http_response-error
span "HTTP response"
status "error" str("http.status_code: ") status
otel-event on-http-response if !acl-http-status-ok
otel-scope http_headers_response
span "HTTP headers response" parent "HTTP response"
finish "HTTP response"
instrument cnt_int "haproxy.http.headers.response" desc "Response headers processed" value int(1) unit "{header}"
log-record info event "http-headers-response" span "HTTP headers response" status
otel-event on-http-headers-response
otel-scope http_end_response
span "HTTP end response" parent "HTTP headers response"
finish "HTTP headers response"
instrument cnt_int "haproxy.http.end.response" desc "Response end events" value int(1) unit "{response}"
instrument update "haproxy.http.headers.response"
log-record info event "http-end-response" span "HTTP end response" str("end")
otel-event on-http-end-response
otel-scope http_reply
span "HTTP reply" parent "HTTP end response"
finish "HTTP end response"
instrument cnt_int "haproxy.http.reply" desc "HTTP replies" value int(1) unit "{reply}"
instrument update "haproxy.http.end.response"
log-record info event "http-reply" span "HTTP reply" status
otel-event on-http-reply
otel-scope server_session_end
instrument update "haproxy.server.session.start"
instrument update "haproxy.http.reply"
finish "*res*"
log-record info event "server-session-end" str("server session ended")
otel-event on-server-session-end

View file

@ -1,246 +0,0 @@
exporters:
exporter_traces_otlp_file:
type: otlp_file
thread_name: "OTLP/file trace"
file_pattern: "__full_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: __full_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: "__full_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: __full_metrics
exporter_metrics_memory:
type: memory
buffer_size: 256
exporter_logs_otlp_file:
type: otlp_file
thread_name: "OTLP/file logs"
file_pattern: "__full_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: __full_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-full"
- service.name: "full"
- service.namespace: "HAProxy traces test"
provider_metrics:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-full"
- service.name: "full"
- service.namespace: "HAProxy metrics test"
provider_logs:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-full"
- service.name: "full"
- 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

@ -1,18 +0,0 @@
global
# nbthread 1
maxconn 5000
hard-stop-after 10s
# log localhost:514 local7 debug
# debug
defaults
# log global
# option httplog
# option dontlognull
# option httpclose
mode http
retries 3
maxconn 4000
timeout connect 5000
timeout client 50000
timeout server 50000

View file

@ -1 +0,0 @@
<html><body><p>Did I err?</p></body></html>

View file

@ -1 +0,0 @@
run-test-config.sh

View file

@ -1 +0,0 @@
run-test-config.sh

View file

@ -1 +0,0 @@
run-test-config.sh

View file

@ -1,50 +0,0 @@
#!/bin/sh -u
#
# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
#
SH_ARG_HAPROXY="${1:-$(realpath -L ${PWD}/../../../haproxy)}"
SH_ARG_PIDFILE="${2:-haproxy.pid}"
SH_TIME="$(date +%s)"
SH_LOG_DIR="_logs"
SH_LOG_FE="${SH_LOG_DIR}/_log-$(basename "${0}" fe-be.sh)fe-${SH_TIME}"
SH_LOG_BE="${SH_LOG_DIR}/_log-$(basename "${0}" fe-be.sh)be-${SH_TIME}"
__exit ()
{
test -z "${2}" && {
echo
echo "Script killed!"
echo "Waiting for jobs to complete..."
pkill --signal SIGUSR1 haproxy
wait
}
test -n "${1}" && {
echo
echo "${1}"
echo
}
exit ${2:-100}
}
trap __exit INT TERM
test -x "${SH_ARG_HAPROXY}" || __exit "${SH_ARG_HAPROXY}: executable does not exist" 1
mkdir -p "${SH_LOG_DIR}" || __exit "${SH_ARG_HAPROXY}: cannot create log directory" 2
echo "\n------------------------------------------------------------------------"
set -- -f haproxy-common.cfg -f be/haproxy.cfg -p "${SH_ARG_PIDFILE}"
echo "--- executing: ${SH_ARG_HAPROXY} ${@}" >${SH_LOG_BE}
"${SH_ARG_HAPROXY}" "${@}" >>"${SH_LOG_BE}" 2>&1 &
set -- -f haproxy-common.cfg -f fe/haproxy.cfg -p "${SH_ARG_PIDFILE}"
echo "--- executing: ${SH_ARG_HAPROXY} ${@}" >${SH_LOG_FE}
"${SH_ARG_HAPROXY}" "${@}" >>"${SH_LOG_FE}" 2>&1 &
echo "------------------------------------------------------------------------\n"
echo "Press CTRL-C to quit..."
wait

View file

@ -1 +0,0 @@
run-test-config.sh

View file

@ -1 +0,0 @@
run-test-config.sh

View file

@ -1,18 +0,0 @@
#!/bin/sh -u
#
# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
#
SH_ARG_HAPROXY="${1:-$(realpath -L ${PWD}/../../../haproxy)}"
SH_ARG_PIDFILE="${2:-haproxy.pid}"
SH_NAME="$(basename "${0}" .sh)"
SH_CONFDIR="${SH_NAME#run-}"
SH_LOG_DIR="_logs"
SH_LOG="${SH_LOG_DIR}/_log-${SH_NAME}-$(date +%s)"
test -x "${SH_ARG_HAPROXY}" || exit 1
mkdir -p "${SH_LOG_DIR}" || exit 2
set -- -f haproxy-common.cfg -f "${SH_CONFDIR}/haproxy.cfg" -p "${SH_ARG_PIDFILE}"
echo "executing: ${SH_ARG_HAPROXY} ${@}" >${SH_LOG}
"${SH_ARG_HAPROXY}" "${@}" >>"${SH_LOG}" 2>&1

View file

@ -1,28 +0,0 @@
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-sa-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-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
backend servers-backend
server server-1 127.0.0.1:8000

View file

@ -1,195 +0,0 @@
[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 on_idle_timeout
scopes client_session_start
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_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")
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 on_stream_start
instrument udcnt_int "haproxy.sessions.active" desc "Active sessions" value int(1) unit "{session}"
instrument gauge_int "haproxy.fe.connections" desc "Frontend connections" value fe_conn unit "{connection}"
span "HAProxy session" root
baggage "haproxy_id" var(sess.otel.uuid)
event "event_ip" "src" src str(":") src_port
event "event_be" "be" be_id str(" ") be_name
event "event_ip" "dst" dst str(":") dst_port
event "event_fe" "fe" fe_id str(" ") fe_name
log-record trace id 1000 event "session-start" span "Client session" attr "attr_1_key" src attr "attr_2_key" src_port 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 on_idle_timeout
idle-timeout 1s
span "heartbeat" parent "HAProxy session"
attribute "idle.elapsed" str("idle-check")
instrument cnt_int "idle.count" value int(1)
instrument update "idle.count"
log-record info str("heartbeat")
otel-event on-idle-timeout
otel-scope client_session_start
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 http_wait_request
span "HTTP wait request" parent "Frontend TCP request"
finish "Frontend TCP request"
otel-event on-http-wait-request
otel-scope http_body_request
span "HTTP body request" parent "HTTP wait request"
finish "HTTP wait request"
otel-event on-http-body-request
otel-scope frontend_http_request
instrument cnt_int "haproxy.http.requests" desc "HTTP request count" value int(1) unit "{request}"
instrument hist_int "haproxy.http.latency" desc "HTTP request latency" value lat_ns_tot unit "ns"
instrument update "haproxy.http.latency" attr "phase" str("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"
log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" method method url
otel-event on-frontend-http-request
otel-scope switching_rules_request
span "Switching rules request" parent "Frontend HTTP request"
finish "Frontend HTTP request"
otel-event on-switching-rules-request
otel-scope backend_tcp_request
span "Backend TCP request" parent "Switching rules request"
finish "Switching rules 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 process_server_rules_request
span "Process server rules request" parent "Backend HTTP request"
finish "Backend HTTP request"
otel-event on-process-server-rules-request
otel-scope http_process_request
span "HTTP process request" parent "Process server rules request"
finish "Process server rules request"
otel-event on-http-process-request
otel-scope tcp_rdp_cookie_request
span "TCP RDP cookie request" parent "HTTP process request"
finish "HTTP process request"
otel-event on-tcp-rdp-cookie-request
otel-scope process_sticking_rules_request
span "Process sticking rules request" parent "TCP RDP cookie request"
finish "TCP RDP cookie request"
otel-event on-process-sticking-rules-request
otel-scope client_session_end
instrument update "haproxy.sessions.active"
finish "*req*"
otel-event on-client-session-end
otel-scope server_unavailable
finish "*req*" "*res*"
otel-event on-server-unavailable
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 tcp_response
span "TCP response" parent "Server session"
otel-event on-tcp-response
otel-scope http_wait_response
span "HTTP wait response" parent "TCP response"
finish "TCP response"
otel-event on-http-wait-response
otel-scope process_store_rules_response
span "Process store rules response" parent "HTTP wait response"
finish "HTTP wait response"
otel-event on-process-store-rules-response
otel-scope http_response
instrument update "haproxy.http.requests" attr "phase" str("response")
instrument update "haproxy.http.latency" attr "phase" str("response")
instrument update "haproxy.fe.connections"
span "HTTP response" parent "Process store rules response"
attribute "http.status_code" status
finish "Process store rules response"
otel-event on-http-response
otel-scope http_response-error
span "HTTP response"
status "error" str("http.status_code: ") status
otel-event on-http-response if !acl-http-status-ok
otel-scope server_session_end
finish "*res*"
otel-event on-server-session-end

View file

@ -1,249 +0,0 @@
exporters:
exporter_traces_otlp_file:
type: otlp_file
thread_name: "OTLP/file trace"
file_pattern: "__sa_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"
# background_thread_wait_for: 10000
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: __sa_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: "__sa_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"
# background_thread_wait_for: 15000
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: __sa_metrics
exporter_metrics_memory:
type: memory
buffer_size: 256
exporter_logs_otlp_file:
type: otlp_file
thread_name: "OTLP/file logs"
file_pattern: "__sa_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"
# background_thread_wait_for: 20000
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: __sa_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-sa"
- service.name: "sa"
- service.namespace: "HAProxy traces test"
provider_metrics:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-sa"
- service.name: "sa"
- service.namespace: "HAProxy metrics test"
provider_logs:
resources:
- service.version: "1.0.0"
- service.instance.id: "id-sa"
- service.name: "sa"
- 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

@ -1,182 +0,0 @@
#!/bin/sh -u
#
__= # echo
SH_ARG_CFG=
SH_ARG_DIR=
SH_ARG_RATE="100.0 75.0 50.0 25.0 10.0 2.5 0.0 disabled off"
SH_ARG_DURATION="300"
SH_NAME="$(basename "${0}")"
SH_LOG_DIR="_logs"
SH_HAPROXY_PIDFILE="${SH_LOG_DIR}/haproxy.pid"
SH_HTTPD_PIDFILE="${SH_LOG_DIR}/thttpd.pid"
SH_USAGE_MSG="usage: ${SH_NAME} [-d duration] [-h] [-r rate-limits] cfg [dir]"
sh_exit ()
{
sh_backup_clean "${SH_ARG_DIR}"
test -z "${2:-}" && {
echo
echo "Script killed!"
}
test -n "${1:-}" && {
echo
echo "${1}"
echo
}
${__} sh_httpd_stop
rmdir -p "${SH_LOG_DIR}" 2>/dev/null
exit ${2:-64}
}
sh_backup_make()
{
_arg_dir="${1}"
_var_file=
for _var_file in haproxy.cfg otel.cfg otel.yml; do
test -e "${_arg_dir}/${_var_file}.orig" || cp -af "${_arg_dir}/${_var_file}" "${_arg_dir}/${_var_file}.orig"
done
test "${_arg_dir}" = "fe" && sh_backup_make "be"
}
sh_backup_clean()
{
_arg_dir="${1}"
_var_file=
for _var_file in haproxy.cfg otel.cfg otel.yml; do
test -e "${_arg_dir}/${_var_file}.orig" && mv "${_arg_dir}/${_var_file}.orig" "${_arg_dir}/${_var_file}"
done
test "${_arg_dir}" = "fe" && sh_backup_clean "be"
}
sh_httpd_run ()
{
test -e "${SH_HTTPD_PIDFILE}" && return
thttpd -p 8000 -d . -nos -nov -l /dev/null -i "${SH_HTTPD_PIDFILE}"
}
sh_httpd_stop ()
{
test -e "${SH_HTTPD_PIDFILE}" || return
kill -TERM "$(cat ${SH_HTTPD_PIDFILE})"
rm "${SH_HTTPD_PIDFILE}"
}
sh_haproxy_run ()
{
_arg_cfg="${1}"
_arg_dir="${2}"
_arg_ratio="${3}"
_var_sed_haproxy=
_var_sed_otel=
_var_sed_yml="s/\(exporters: *exporter_[a-z]*_\).*/\1dev_null/g"
if test "${_arg_ratio}" = "disabled"; then
_var_sed_otel="s/no \(option disabled\)/\1/"
elif test "${_arg_ratio}" = "off"; then
_var_sed_haproxy="s/^\(.* filter opentelemetry .*\)/#\1/g; s/^\(.* otel-group .*\)/#\1/g"
else
_var_sed_otel="s/\(rate-limit\) 100.0/\1 ${_arg_ratio}/"
fi
sed "${_var_sed_haproxy}" "${_arg_dir}/haproxy.cfg.orig" > "${_arg_dir}/haproxy.cfg"
sed "${_var_sed_otel}" "${_arg_dir}/otel.cfg.orig" > "${_arg_dir}/otel.cfg"
sed "${_var_sed_yml}" "${_arg_dir}/otel.yml.orig" > "${_arg_dir}/otel.yml"
if test "${_arg_dir}" = "fe"; then
sed "${_var_sed_yml}" "be/otel.yml.orig" > "be/otel.yml"
if test "${_arg_ratio}" = "disabled" -o "${_arg_ratio}" = "off"; then
sed "${_var_sed_haproxy}" "be/haproxy.cfg.orig" > "be/haproxy.cfg"
sed "${_var_sed_otel}" "be/otel.cfg.orig" > "be/otel.cfg"
fi
fi
./run-${_arg_cfg}.sh "" "${SH_HAPROXY_PIDFILE}" &
sleep 5
}
sh_haproxy_stop ()
{
# HAProxy does not create a pidfile if it is not running in daemon mode,
# this is not used but is left regardless.
#
if test -e "${SH_HAPROXY_PIDFILE}"; then
kill -TERM "$(cat ${SH_HAPROXY_PIDFILE})"
rm "${SH_HAPROXY_PIDFILE}"
fi
pkill --signal SIGUSR1 haproxy
wait
}
sh_wrk_run ()
{
_arg_ratio="${1}"
echo "--- rate-limit ${_arg_ratio} --------------------------------------------------"
wrk -c8 -d${SH_ARG_DURATION} -t8 --latency http://localhost:10080/index.html
echo "----------------------------------------------------------------------"
echo
sleep 10
}
while getopts d:hr: _var_getopt; do
case "${_var_getopt}" in
d) SH_ARG_DURATION="${OPTARG}" ;;
h) sh_exit "${SH_USAGE_MSG}" 0 ;;
r) SH_ARG_RATE="${OPTARG}" ;;
\?) sh_exit "${SH_USAGE_MSG}" 64
esac
done
shift $(expr ${OPTIND} - 1)
SH_ARG_CFG="${1:-}"
SH_ARG_DIR="${2:-${SH_ARG_CFG}}"
command -v thttpd >/dev/null 2>&1 || sh_exit "thttpd: command not found" 5
command -v wrk >/dev/null 2>&1 || sh_exit "wrk: command not found" 6
test -z "${SH_ARG_CFG}" -o -z "${SH_ARG_DIR}" && sh_exit "${SH_USAGE_MSG}" 64
mkdir -p "${SH_LOG_DIR}" || sh_exit "${SH_LOG_DIR}: Cannot create log directory" 1
if test "${SH_ARG_CFG}" = "all"; then
_var_args="-r ${SH_ARG_RATE}"
"${0}" "${_var_args}" sa sa
"${0}" "${_var_args}" cmp cmp
"${0}" "${_var_args}" ctx ctx
"${0}" "${_var_args}" fe-be fe
exit 0
elif test "${SH_ARG_CFG}" = "fe-be"; then
SH_ARG_DIR="fe"
fi
test -f "run-${SH_ARG_CFG}.sh" || sh_exit "run-${SH_ARG_CFG}.sh: No such test script" 2
test -d "${SH_ARG_DIR}" || sh_exit "${SH_ARG_DIR}: No such directory" 3
trap sh_exit INT TERM
echo "Running speed test ${SH_ARG_CFG}, start at $(date +"%F %T")"
exec 1>"${SH_LOG_DIR}/README-speed-${SH_ARG_CFG}"
${__} sh_backup_make "${SH_ARG_DIR}"
${__} sh_httpd_run
for _var_rate in ${SH_ARG_RATE}; do
${__} sh_haproxy_run "${SH_ARG_CFG}" "${SH_ARG_DIR}" "${_var_rate}"
${__} sh_wrk_run "${_var_rate}"
${__} sh_haproxy_stop
done
sh_exit "" 0

View file

@ -3,7 +3,7 @@
Configuration Manual
----------------------
version 3.4
2026/04/29
2026/05/20
This document covers the configuration language as implemented in the version
@ -1791,6 +1791,8 @@ The following keywords are supported in the "global" section :
- insecure-fork-wanted
- insecure-setuid-wanted
- issuers-chain-path
- jwt.decrypt_alg_list
- jwt.decrypt_enc_list
- key-base
- limited-quic
- localpeer
@ -1999,6 +2001,7 @@ The following keywords are supported in the "global" section :
- tune.sndbuf.client
- tune.sndbuf.frontend
- tune.sndbuf.server
- tune.streams-elasticity
- tune.stick-counters
- tune.ssl.cachesize
- tune.ssl.capture-buffer-size
@ -2123,13 +2126,28 @@ ca-base <dir>
directives. Absolute locations specified in "ca-file", "ca-verify-file" and
"crl-file" prevail and ignore "ca-base".
chroot <jail dir>
chroot { <jail dir> | auto }
Changes current directory to <jail dir> and performs a chroot() there before
dropping privileges. This increases the security level in case an unknown
vulnerability would be exploited, since it would make it very hard for the
attacker to exploit the system. This only works when the process is started
with superuser privileges. It is important to ensure that <jail_dir> is both
empty and non-writable to anyone.
attacker to exploit the system. It is important to ensure that <jail dir>
is both empty and non-writable to anyone. When the process is started with
superuser privileges, the chroot() is performed directly. On Linux, when
started unprivileged, haproxy attempts to perform it from inside a new
user namespace created with unshare(CLONE_NEWUSER); if that mechanism is
unavailable the chroot() will fail with the usual error.
As a special case, <jail dir> may be set to "auto", in which case haproxy
creates an anonymous temporary directory, unlinks it, and chroots into it.
The resulting jail has no name in the filesystem and is empty and read-only,
removing the need to prepare a dedicated jail directory.
When starting with superuser privileges, a warning will be displayed if no
chroot is used, in order to encourage users to always use the mechanism. If
for any reason there is a compelling reason not to use chroot (e.g. access to
a server via a UNIX socket with an unconvenient path), it remains possible to
silence the warning by adding an explicit "chroot /", which has the benefit
of being visible in a configuration.
close-spread-time <time>
Define a time window during which idle connections and active connections
@ -2888,6 +2906,40 @@ issuers-chain-path <dir>
The OCSP features are able to use the completed chain when no .issuer was
used, or no chain was provided in the PEM.
jwt.decrypt_alg_list <list>
Set the list of algorithms allowed in the jwt_decrypt_XXX converters. JWT
tokens using an unsupported or disabled algorithms will never be decrypted.
The specified algorithms must have the same format as in section 4.1 of
RFC7518 and must be colon-separated. The special "ALL" name can be used to
enable all the supported algorithms (see "jwt_decrypt_jwk" converter for a
complete list) and a '!' can be appended to an algorithm name to explicitly
disable it.
Please note that unless "ALL" is specified, using this option will disable
any algorithm that is not explicitly mentioned in the provided list.
Examples:
# Enable all algorithms but the "ECDH-ES" one
jwt.decrypt_alg_list ALL:!ECDH-ES
# Only enable ECDH-ES algorithms
jwt.decrypt_alg_list ECDH-ES:ECDH-ES+A128KW:ECDH-ES+A192KW:ECDH-ES+A256KW
jwt.decrypt_enc_list <list>
Set the list of encryption algorithms allowed in the jwt_decrypt_XXX
converters. JWT tokens using an unsupported or disabled encryption algorithms
will never be decrypted.
The specified algorithms must have the same format as in section 5.1 of
RFC7518 and must be colon-separated. The special "ALL" name can be used to
enable all the supported algorithms (see "jwt_decrypt_jwk" converter for a
complete list) and a '!' can be appended to an algorithm name to explicitly
disable it.
Please note that unless "ALL" is specified, using this option will disable
any algorithm that is not explicitly mentioned in the provided list.
Examples:
# Enable only AES GCM encrypting algorithms
jwt.decrypt_enc_list A128GCM:A192GCM:A256GCM
key-base <dir>
Assigns a default directory to fetch SSL private keys from when a relative
path is used with "key" directives. Absolute locations specified prevail and
@ -3277,7 +3329,7 @@ setenv <name> <value>
the configuration file sees the new value. See also "presetenv", "resetenv",
and "unsetenv".
shm-stats-file <name> [ EXPERIMENTAL ]
shm-stats-file <name>
When this directive is set, it enables the use of shared memory for storing
stats counters. <name> is used as argument to shm_open() to open the shared
memory at a unique location. It also means that the directive is only
@ -3293,7 +3345,7 @@ shm-stats-file <name> [ EXPERIMENTAL ]
See also "guid", "guid-prefix" and "shm-stats-file-max-objects"
shm-stats-file-max-objects <number> [ EXPERIMENTAL ]
shm-stats-file-max-objects <number>
This setting defines the maximum number of objects the shared memory used
for shared counters will be able to store per thread group. It is directly
related to the maximum memory size of the shm and is used to "premap" the
@ -5269,17 +5321,26 @@ tune.quic.frontend.stream-data-ratio <0..100, in percent> (deprecated)
tune.quic.be.stream.max-concurrent <number>
tune.quic.fe.stream.max-concurrent <number>
Sets the QUIC initial_max_streams_bidi transport parameter either on frontend
or backend side. This is the maximum number of bidirectional streams that the
remote peer will be authorized to open concurrently during the connection
lifetime. On frontend side, this limits the number of concurrent HTTP/3
client requests.
On frontend side, this is used as the value for the advertised
initial_max_streams_bidi transport parameter. This is enforced as the maximum
number of bidirectional streams that the remote peer will be authorized to
open concurrently during the connection lifetime. This effectively limits the
number of concurrent HTTP/3 client requests.
The default value is 100. Note that if you reduces it, it can restrict the
buffering capabilities of streams on receive, which would result in poor
upload throughput. It can be corrected by increasing the QUIC stream rxbuf
connection setting.
On backend side, this is enforced locally by haproxy to limit the number of
concurrent requests multiplexed over a single connection. This may be further
restricted by the peer flow control. It may be necessary to reduce the
default value of 100 to improve a site's responsiveness at the expense of a
higher number of opened backend connections. Similarly to the frontend side,
this setting also directly impacts the Rx buffering capability, this time
though limiting the HTTP download capacity. QUIC stream rxbuf setting can be
increased when dealing mostly with HTTP responses larger than "tune.bufsize".
See also: "tune.quic.be.stream.rxbuf", "tune.quic.fe.stream.rxbuf",
"tune.quic.be.stream.data-ratio", "tune.quic.fe.stream.data-ratio"
@ -5653,6 +5714,49 @@ tune.ssl.ssl-ctx-cache-size <number>
dynamically is expensive, they are cached. The default cache size is set to
1000 entries.
tune.streams-elasticity <number>
Defines a target percentage of streams per frontend connection relative to
the maximum number of concurrent connections (maxconn) when all connections
are established. This metric applies to multiplexed protocols like HTTP/2 or
QUIC, where each connection may receive multiple streams. At least one is
always guaranteed, so the percentage must be at least 100%. During connection
setup, HAProxy dynamically advertises additional streams up to the configured
limit, maintaining the target ratio. At connection establishment, every
frontend connection receives at least one stream; extra streams are assigned
based on the target percentage and configured stream limits. This ensures
efficient stream allocation under varying load conditions (more streams at
low loads, fewer at high loads).
Highly dynamic sites with many objects per page benefit from high ratios,
enabling many streams per connection. Sites using fewer streams on average
(WebSocket, application code) may prefer small ratios closer to 120 or 150
(20 to 50% more streams than connections) preventing excessive stream counts
under sustained loads.
The default value is 0, meaning no enforcement at this level, so only H2 and
QUIC configurations apply (with the default setting of 100 streams per
connection, this corresponds to 10000%). This remains the recommended setting
for small deployments (maxconn around a thousand). Moderately sized setups
(few thousands to tens of thousands connections) typically set the ratio
between 1000 and 5000, allowing 10 to 50 streams per connection at full load.
Large-scale deployments (hundreds of thousands to millions connections) might
use lower values (120 to 200) to support 1.2 to 2 streams per connection on
average at full load.
Contrary to HTTP/2, QUIC is capable to dynamically adjust the number of
concurrent streams during the connection lifetime. However, QUIC flow control
is stricter than HTTP/2, thus it is preferable when using it to specify
values big enough to prevent extra latency on the connection. There is also a
limitation for QUIC listeners with enabled 0-RTT. In this case, the initial
value advertised to the peer will ignore stream elasticity and instead rely
solely on the "tune.quic.fe.stream.max-concurrent" setting. However, the
stream elasticity principle will still be effective past this initial
annoucement during the connection lifetime.
Monitoring the total number of active streams on backends, including queues,
provides a practical indicator of a sustainable target load and helps avoid
over-provisioning.
tune.stick-counters <number>
Sets the number of stick-counters that may be tracked at the same time by a
connection or a request via "track-sc*" actions in "tcp-request" or
@ -8190,7 +8294,10 @@ hash-type <method> <function> <modifier>
none don't hash the key, the key will be used as a hash, this can be
useful to manually hash the key using a converter for that purpose
and let haproxy use the result directly.
and let haproxy use the result directly. The operation will
convert the key to a string if it is not already, and parse it as
an integer whose value will be used as the key. Some input key
types might not be relevant here (e.g. IP addresses).
<modifier> indicates an optional method applied after hashing the key :
@ -18777,6 +18884,21 @@ hash-key <key>
better only use values comprised between 1 and this value to
avoid overlap.
id32 The node keys will be derived from the server's numeric
identifier as set from "id" or which defaults to its position
in the server list, but the full 32 bits of the ID will be
used so that there is no collision. This one is not scaled
like "id" is, so it is recommended to either always use it
with a hash function (see "hash-key") or with explicitly
assigned ID values that are evenly distributed over the 32-bit
space.
guid The node keys will be derived from the server's guid, when
available, otherwise they will fall back on "id". The benefit
is that it does not depend on ordering at all, only on an
internal stable identifier that can be replicated across many
load balancers.
addr The node keys will be derived from the server's address, when
available, or else fall back on "id".
@ -18806,9 +18928,13 @@ healthcheck <name>
id <value>
May be used in the following contexts: tcp, http, log
Set a persistent ID for the server. This ID must be positive and unique for
the proxy. An unused ID will automatically be assigned if unset. The first
assigned value will be 1. This ID is currently only returned in statistics.
Set a persistent ID for the server. This ID must be a 32-bit positive number
and unique for the proxy. An unused ID will automatically be assigned if
unset. The first assigned value will be 1. This ID is currently only returned
in statistics, and is used to place LB nodes when using consistent hash
algorithms when "hash-key" is set to "id" (the default). In this case, only
the 28 lowest bits of the value are used (i.e. (id % 268435356)), so better
only use values comprised between 1 and this value to avoid overlap.
idle-ping <delay>
May be used in the following contexts: tcp, http, log
@ -18902,7 +19028,7 @@ downinter <delay>
"inter" setting will have a very limited effect as it will not be able to
reduce the time spent in the queue.
init-state { fully-up | up | down | fully-down }
init-state { fully-up | up | down | fully-down | none }
May be used in the following contexts: tcp, http
May be used in sections : defaults | frontend | listen | backend
@ -18910,20 +19036,25 @@ init-state { fully-up | up | down | fully-down }
The "init-state" option sets the initial state of the server:
- when set to 'fully-up', the server is considered immediately available
and can turn to the DOWN state when ALL health checks fail.
- when set to 'up' (the default), the server is considered immediately
available and will initiate a health check that can turn it to the DOWN
state immediately if it fails.
- when set to 'down', the server initially is considered unavailable and
will initiate a health check that can turn it to the UP state immediately
if it succeeds.
and, if health checks are enabled for this server, it will be turned to
the DOWN state when ALL health checks fail.
- when set to 'up', the server is considered immediately available and, if
health checks are enabled for this server, it will be turned to the DOWN
state immediately if the next health check fails.
- when set to 'down', the server initially is considered unavailable and,
if health checks are enabled for this server, it can be turned to the UP
state if the next health check succeeds.
- when set to 'fully-down', the server is initially considered unavailable
and can turn to the UP state when ALL health checks succeed.
and, if health checks are enabled for this server, it will turned to the
UP state when ALL health checks succeed.
- when set to 'none' (the default value), init-state management is
disabled. It can be used to restore the default behavior when this
parameter was inherited from a 'default-server' directive.
The server's init-state is considered when the HAProxy instance is
(re)started, a new server is detected (for example via service discovery /
DNS resolution), a dynamic server is inlived, a server exits maintenance,
etc.
etc. This directive cannot be used when the server is tracking another one.
Examples:
# pass client traffic ONLY to Redis "master" node
@ -20119,7 +20250,11 @@ a cache of previous answers, an answer will be considered obsolete after
resolvers <resolvers id>
Creates a new name server list labeled <resolvers id>
Creates a new name server list labeled <resolvers id>. As mentioned above,
the special name "default" always exists and will be automatically created if
not explicitly declared; this will be the one internal services such as
httpclient rely on. Declaring a "default" entry will affect how such services
perform their name resolution.
A resolvers section accept the following parameters:
@ -21075,6 +21210,8 @@ param(name[,delim]) string string
port_only string integer
protobuf(field_number[,field_type]) binary binary
regsub(regex,subst[,flags]) string string
reverse string string
reverse_dom string string
rfc7239_field(field) string string
rfc7239_is_valid string boolean
rfc7239_n2nn string address / str
@ -21894,8 +22031,15 @@ jwt_decrypt_cert(<cert>)
format (five dot-separated base64-url encoded strings).
This converter can be used for tokens that have an algorithm ("alg" field of
the JOSE header) among the following: RSA1_5, RSA-OAEP, RSA-OAEP-256,
ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW or ECDH-ES+A256KW.
the JOSE header) among the following: RSA-OAEP, RSA-OAEP-256, ECDH-ES,
ECDH-ES+A128KW, ECDH-ES+A192KW or ECDH-ES+A256KW.
The RSA1_5 algorithm is implemented but disabled by default following what is
suggested in section 3.2 of RFC 8725. It can be reenabled if needed thanks to
'jwt.decrypt_alg_list' global option.
The supported algorithms and encryption algorithms ("alg" and "enc" fields of
the JOSE header respectively) can be modified thanks to the
'jwt.decrypt_alg_list' and 'jwt.decrypt_enc_list' global options.
The JWE token must be provided base64url-encoded and the output will be
provided "raw". If an error happens during token parsing, signature
@ -21924,14 +22068,21 @@ jwt_decrypt_jwk(<jwk>)
the provided JWK to be of the 'oct' type.
This converter also manages tokens that have an algorithm ("alg" field of the
JOSE header) in the RSA family (RSA1_5, RSA-OAEP or RSA-OAEP-256) when
provided an 'RSA' JWK, or in the ECDH family (ECDH-ES, ECDH-ES+A128KW,
ECDH-ES+A192KW or ECDH-ES+A256KW) when provided an 'EC' JWK.
JOSE header) in the RSA family (RSA-OAEP or RSA-OAEP-256) when provided an
'RSA' JWK, or in the ECDH family (ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW or
ECDH-ES+A256KW) when provided an 'EC' JWK.
The RSA1_5 algorithm is implemented but disabled by default following what is
suggested in section 3.2 of RFC 8725. It can be reenabled if needed thanks to
'jwt.decrypt_alg_list' global option.
Please note that the A128KW and A192KW algorithms are not available on AWS-LC
so the A128KW, A192KW, ECDH-ES+A128KW and ECDH-ES+A192KW algorithms won't
work.
The supported algorithms and encryption algorithms ("alg" and "enc" fields of
the JOSE header respectively) can be modified thanks to the
'jwt.decrypt_alg_list' and 'jwt.decrypt_enc_list' global options.
The JWE token must be provided base64url-encoded and the output will be
provided "raw". If an error happens during token parsing, signature
verification or content decryption, an empty string will be returned.
@ -22558,6 +22709,58 @@ regsub(<regex>,<subst>[,<flags>])
http-request redirect location %[url,'regsub("(foo|bar)([0-9]+)?","\2\1",i)']
http-request redirect location %[url,regsub(\"(foo|bar)([0-9]+)?\",\"\2\1\",i)]
reverse
Reverses the input string byte by byte.
This converter is encoding-agnostic and reverses bytes, not characters; it is
not suitable for reversing human text encoded as UTF-8.
This can turn suffix lookups on the original string into prefix lookups on
the reversed string, allowing the use of indexed prefix matchers such as
"map_beg" on large maps.
Examples:
"example.com" -> "moc.elpmaxe"
"ab cd" -> "dc ba"
# Given a map file where each key contains a reversed hostname:
# moc.elpmaxe.ppa app1
# moc.elpmaxe.bd dbcluster
# Pick a backend based on the domain suffix of the Host header:
use_backend %[req.hdr(host),lower,reverse,map_beg(/etc/haproxy/hosts.map,default)]
reverse_dom
Converts a string containing an FQDN-like hostname into its reversed-label
form. A single trailing dot on the input is ignored. Empty labels cause the
converter to fail.
This converter does not lowercase its input and does not strip any port.
It is meant to be combined with existing converters such as "lower" or
"host_only" when needed.
The trailing-dot policy is intentionally left to the caller. This allows
callers to decide whether they want to match the apex too or only
subdomains.
The reversed-label form is useful for large domain maps because it turns
domain suffix lookups into prefix lookups, allowing the use of indexed prefix
matchers such as "map_beg".
Examples:
"example.com" -> "com.example"
"mail.example.com" -> "com.example.mail"
"example.com." -> "com.example"
# match only subdomains of example.net, not the apex
acl example_net_sub req.hdr(Host),host_only,reverse_dom -m beg net.example.
# match only the apex
acl example_net_apex req.hdr(Host),host_only,reverse_dom -i net.example
# exact-or-subdomain prefix lookup using an explicit dotted form
http-request set-var(txn.rev_host) req.hdr(Host),host_only,reverse_dom,concat(.)
use_backend %[var(txn.rev_host),map_beg(/etc/haproxy/domains.map)]
rfc7239_field(<field>)
Extracts a single field/parameter from RFC 7239 compliant header value input.
@ -30494,10 +30697,12 @@ More detailed documentation related to the operation, configuration and use
of the filter can be found in the addons/ot directory.
Note: The OpenTracing filter shouldn't be used for new designs as OpenTracing
itself is no longer maintained nor supported by its authors. A
replacement filter base on OpenTelemetry is currently under development
and is expected to be ready around HAProxy 3.2. As such OpenTracing will
be deprecated in 3.3 and removed in 3.5.
itself is no longer maintained nor supported by its authors. As such
OpenTracing will be deprecated in 3.3 and removed in 3.5. A replacement
filter based on OpenTelemetry is available since 3.4 with complete build
instructions currently at:
https://github.com/haproxytech/haproxy-opentelemetry/
9.7. Bandwidth limitation
@ -32419,7 +32624,6 @@ Current limitations:
from outside HAProxy using "dump ssl cert" on the stats socket. It's possible
to automate the dump of the certificates by using the dataplaneAPI or the
haproxy-dump-certs script provided in the admin/cli/ directory.
- External Account Binding (EAB) is not supported.
The ACME scheduler starts at HAProxy startup, it will loop over the
certificates and start an ACME renewal task when the notAfter task is past
@ -32445,6 +32649,16 @@ account-key <filename>
openssl ecparam -name secp384r1 -genkey -noout -out account.key
acme-vars <string>
Pass arbitrary variables to the external DNS provisioning tool (e.g. the
dataplaneAPI) via the "dpapi" sink. The semantics are tool-specific; refer
to your DNS provisioning tool's documentation.
This keyword is only meaningful when the challenge type is "dns-01" or
"dns-persist-01".
See also: "challenge", "provider-name"
bits <number>
Configure the number of bits to generate an RSA certificate. Default to 2048.
Setting a too high value can trigger a warning if your machine is not
@ -32594,6 +32808,16 @@ profile <string>
# Request short-lived certificates
profile shortlived
provider-name <string>
Set the DNS provider name passed to the external DNS provisioning tool (e.g.
the dataplaneAPI) via the "dpapi" sink. The accepted values are
tool-specific; refer to your DNS provisioning tool's documentation.
This keyword is only meaningful when the challenge type is "dns-01" or
"dns-persist-01".
See also: "challenge", "acme-vars"
reuse-key { on | off }
If set to "on", HAProxy won't generate a new private key and will keep the
previous one. Rotating private keys is recommended, when enabling this option
@ -32638,6 +32862,40 @@ Example:
curves P-384
map virt@acme
eab-key-id <filename>
Configure the path to the EAB key id file. The credential is provided by
the CA and must be placed at the specified path before starting HAProxy.
It is used during account creation only.
The file must contain a plain ASCII string.
EAB credentials are only required during the initial ACME account creation
and can be removed afterwards, either from the config or by emptying the
files. An empty file is silently ignored. Whitespace is not ignored, except
for the trailing newline.
See also: "eab-mac-key", "eab-mac-alg"
eab-mac-key <filename>
Configure the path to the EAB MAC key file. The credential is provided by
the CA and must be placed at the specified path before starting HAProxy.
It is used during account creation only.
The file must contain a base64url encoded MAC key.
EAB credentials are only required during the initial ACME account creation
and can be removed afterwards, either from the config or by emptying the
files. An empty file is silently ignored. Whitespace is not ignored, except
for the trailing newline.
See also: "eab-key-id", "eab-mac-alg"
eab-mac-alg { HS256 | HS384 | HS512 }
Configure MAC algorithm used for EAB signing. Default is HS256. EAB MAC key
must be large enough to support specified MAC algorithm. Not all CAs support
algorithms other than HS256.
See also: "eab-key-id", "eab-mac-key"
12.9. Healthchecks
------------------

View file

@ -0,0 +1,229 @@
HAPROXY CORE PRINCIPLES
0. RULE ZERO: EXCEPTIONS AND JUSTIFICATION
- These rules are mandatory; violations are bugs unless explicitly justified.
- A violation is acceptable if accompanied by a comment explaining WHY the
standard approach was insufficient (e.g., "Performance-critical bypass").
- Reviews should flag unjustified violations but accept commented ones.
1. PROJECT ORGANIZATION
- header files all under "include/", and split between haproxy/<file>-t.h for
type definitions (types, enums, structures), and haproxy/<file>.h for static
definitions and exported symbols. A few imported libs under include/import.
- C source files in src/.
- some API doc in doc/internals/api/ (not always up to date, check date or
version at the top).
2. ENVIRONMENT AND DATA TYPES
- The project targets 32/64-bit POSIX systems (little or big endian).
- Char is signed or unsigned 8-bit, short signed 16-bit, int signed 32-bit.
- Long and pointers always match the native word size. Long long is 64-bit.
- Aliases: uchar (unsigned char), uint (unsigned int), ulong (unsigned long),
ushort (unsigned short), ullong (unsigned long long), llong (long long),
schar (signed char).
- size_t always same size as long but often declared as uint on 32-bit and
ulong on 64-bit. Do not use in printf() without a cast (ulong with "%lu").
- Main platforms are x86_64 and aarch64 with high thread counts (>=64).
- Unaligned accesses are permitted for archs that support them; portable
wrappers in net_helper.h (read_u32(), write_u32() etc).
- signed integer wrapping well-defined via -fwrapv.
- arch-specific asm() statements OK as long as equivalent C-code exists for
generic archs.
- Pointer arithmetics used a lot via container_of(), offset_of(), and void*
casts.
- Floating point not used.
3. MEMORY MANAGEMENT AND POOLS
- Pools are used for runtime allocation; malloc/free are for boot code only.
- pool_alloc() semantics match malloc(); the return must always be tested.
- pool_alloc() and malloc() are not interchangeable / compatible.
- pool_free() semantics match free(); it is a no-op on NULL.
- pool_free() makes the pointer invalid immediately; it must not be touched
or passed to pool_free() again.
- Memory allocated from one pool must be released to the same pool.
- ha_free() calls free() and sets the pointer to NULL before returning.
- my_realloc2() frees the original pointer if the allocation fails.
- never leave dangling pointers in structs after free().
4. BUFFER INVARIANTS (struct buffer)
- Buffers are 4-word inline structs used for data in transit (wrapping,
sliding window).
- Members: area (storage), size (capacity), head (offset), data (count).
- The area pointer is allowed to be NULL when size is zero.
- always true: 0<=data<=size; always true when size>0: 0<=head<size.
- contents start at <head>, for <data> bytes, and may wrap at the end of the
storage area (area+size).
- API (b_*, in buf.h and dynbuf.h) supports empty or unallocated buffers.
- idempotent functions b_alloc() and b_free() use pools to manage the
storage area and check <size> to know if alloc/free still needed.
- a non-contiguous version exists (ncbuf, ncbmbuf), allowing holes anywhere
in data. The former mandates holes of at least 8 bytes. The second relies
on a bitmap of populated places.
- another string API exists, "ist", representing a pointer and a length in a
struct that is returned by inline functions and macros. It is described in
doc/internals/api/ist.txt
- buffers can switch to and from HTX, which is an internal representation of
HTTP elements, with an API supporting header addition/modification/removal,
start-line manipulation, data appending/consumption etc. HTX functions are
all prefixed with "htx_". Between htx_from_buf() and htx_to_buf(), only the
HTX API may be used, not the b_* API.
5. DATA MANIPULATION (CHUNKS, TRASH, LISTS, TREES)
- Chunks use the buffer API but are NOT allowed to wrap.
- Chunks are used for linear operations like chunk_printf().
- Trash is a thread-local temporary buffer; scope stays within the caller.
- trash always the same size as a buffer (global.tune.bufsize).
- get_trash_chunk() provides up to 3 rotating thread-local trash chunks (with
a scope spanning from the call to the next function call).
- For longer lived trash chunks, alloc_trash_chunk() is available but must be
released using free_trash_chunk() on leaving.
- standard doubly-linked lists (struct list) are provided via macros LIST_*.
- LIST_INIT() must be used on new heads and elements. LIST_DELETE() only
removes the element and does not reinitialize it, so the idempotent
LIST_DEL_INIT() is generally preferred. Iterators like list_for_each_* are
available, some safe against item removal. See doc/internals/api/list.txt
for details (grep -i "^list_" to list available macros).
- thread-safe doubly-linked lists (struct mt_list) are provided via macros
mt_list_*. They work like lists and use compatible storage, though they may
not be mixed. See doc/internals/api/mt_list.txt (grep -i "^mt_list_" to
list available operations).
- elastic binary trees (ebtree) are used for fast access (O(logN) operations,
O(1) deletion). Idempotent deletion. Main functions are lookup, insert,
delete, first, next, with type-based prefix eb{32,64,st,mb,pt}_*().
- compact elastic binary trees (cebtree) are used for read-mostly focusing on
space savings (O(logN) operations, but higher cost than ebtree). Same ops
as ebtree, with type-based prefix ceb{32,u32,64,u64,s,is}_*.
6. THREAD SYNCHRONIZATION
- Threads are started at boot (one per CPU) and persist for the process life,
arranged in thread groups (tg) by cache locality.
- Each thread has its own polling loop and scheduler. Total parallelism.
- thread_isolate()/thread_release() for total thread isolation (very heavy).
- "tid" always current thread number, "th_ctx" always current thread's context,
"ti" current thread info.
- "tgid" always current tg number, "tg_ctx" current tg context.
- HA_ATOMIC_* for atomic operations on integers and pointers (includes load
and store). DWCAS available on some platforms but requires an equivalent
for other ones.
- The _HA_ATOMIC_* version (leading underscore) do not use barriers so these
must be explicit (__ha_barrier_*).
- Atomic loops must use CPU relaxation or exponential back-off.
- For multiple changes at once, threads may use spinlocks (HA_SPIN_LOCK()/
HA_SPIN_UNLOCK/HA_SPIN_TRYLOCK), and upgradable RW locks (HA_RWLOCK_*) if
read accesses dominate.
- No sleeping locks (mutex etc), only spinning/rwlocks/atomic loops.
7. SCHEDULING AND LATENCY
- Latency is critical.
- No runtime filesystem access, no blocking calls, no long loops.
- Complex processing must be split into small steps; the task must yield.
- CPUs are not dedicated to haproxy, high risk of a thread being interrupted
by another process if it works too long, catastrophic if it happens with a
lock held.
- A watchdog kills the process if a task hogs a CPU for > few milliseconds.
- Tasks vs Tasklets: Tasks have tree storage (rq) and timers (wq); tasklets
use list elements instead of rq and are smaller (no wq). Only task.c/h may
distinguish rq vs list access.
- Tasks are aliased to tasklet while they are running (hence why some
functions cast task to tasklets and conversely to access certain fields).
- inter-thread task/tasklet wakeups always safe using the task_* API.
- task/tasklet->state field must always be accessed atomically.
8. ARCHITECTURAL LAYERS (MUX AND STREAMS)
- Naming: Lower layer (multiplexed), attached to the connection uses suffix
'c' (h1c, h2c, qcc, muxc); Upper layer (demultiplexed/application, often a
stream) uses suffix 's' (h1s, h2s, qcs, muxs).
- Application layer stream (struct stream) has two stream connectors (stconn):
front (scf) and back (scb). Responsible for processing requests/responses,
deciding which server to route it, finding a backend connection or creating
one, and exchanging data between the two sides.
- Stream connectors link to a muxs or applet via a stream endpoint descriptor
(sedesc/sd), and exchange data via buffers, which for an HTTP muxs are HTX
buffers containing HTX blocks.
- The sd carries the shared context between layers.
- When a stream detaches from a mux, a new sd is allocated for the stream and
the mux keeps its previous sd: stconn and muxs both always have a valid sd.
- Front connections/streams are tied to the creator thread forever.
- Idle back connections can be stolen via mux->takeover(), but become
thread-bound once a stream attaches. => all streams of a mux are on the
same thread.
- session vs connection vs stream: connection is transport; session lasts for
the client connection's life; stream are request/response pairs.
- applets carry a context specific to the service being executed or the CLI
command in appctx->svcctx, and this one is always zeroed before the handler
is first called.
9. FUNCTION RETURN CONVENTIONS
- Boolean style: Functions named as actions/sentences return 0 (failure) or
non-zero (success).
- Integer style: some syscall-like functions return <0 (error) or >=0 (success).
- Tri-state style, e.g. counts: <0 (error), 0 (no progress), >0 (success).
10. DIAGNOSTICS AND SAFETY
- When DEBUG_STRICT is set, ABORT_NOW() crashes the program immediately, and
BUG_ON(cond[,msg]) crashes the program if the condition is true.
- COUNT_IF() / CHECK_IF() only track if a condition occurs (non-fatal).
- Glitches are counters for uncommon events used to detect hostile behavior.
- strcpy(), strcat() and sprintf() are totally forbidden (the program will
not build).
11. BASIC CODING STYLE
- Linux Kernel-like, but uses tabs for indent, spaces for alignment. Function
definitions have their opening brace on a new line, never on the same line.
- All local variables must be declared at the beginning of the function
block, before any executable statements (gnu89-like).
- Avoid variable shadowing in code blocks.
- Beware of local static and global variables.
- Use const arguments whenever possible.
- Avoid static storage when persistence is not needed.
- Macros in uppercase unless they're used to wrap functions which then get a
leading underscore.
- Explicitly compare functions returning non-zero with 0 (e.g. strcmp) unless
they explicitly return a boolean (e.g. isalnum) or a pointer (e.g. strchr).
- Unsigned int comparisons to zero never use >0 but !=0 to avoid signedness
mistakes.
- turn non-zero integer to boolean using "!" or "!!".
12. BUILD AND TEST
- Preferred build command:
$ make -j$(nproc) TARGET=linux-glibc OPT_CFLAGS='-std=gnu89 -Os' \
USE_OPENSSL=1 USE_QUIC_OPENSSL_COMPAT=1 USE_QUIC=1 USE_LUA=1
- Individual files can be tested by passing src/file.o as a make argument.
- Compiler warnings are not permitted for new code.
13. COMMIT MESSAGES AND DOCUMENTATION
- Commit messages must follow the project's strict format below. Do not try
to learn better from previous commits, which might be wrong during reviews.
- Structure: <TAG>: <location>: <subject> (max ~70 chars), then blank line,
then description.
- Tags:
- CLEANUP: spelling fixes, refactoring, no new code nor functional change.
- MINOR: new feature or low-impact change, may be backported if needed.
- MEDIUM: new feature or change with moderate severity/impact/risk.
- MAJOR: new feature or change with important severity/impact/risk.
- OPTIM: Performance improvements, may always be reverted if it breaks.
- DOC: Documentation updates or fixes.
- BUG/<severity>: Fixes a bug. Specify if regression or long-standing.
Valid severities are MINOR (low impact), MEDIUM (perf/stability risk
in uncommon configs, MAJOR (most configs), CRITICAL (stability risk
without workaround).
- Regressions: Find original commit via `git blame`; designate using
`git log -1 --format='%h ("%s")'` and version via `git describe --tags`.
- Location: subsystem (stream, tasks, mux-h2, qpack etc).
- Description: Explain technical "WHY", "HOW", and technical impact. Explain
how to trigger the bug for developer testing.
- Backports: only for fixes, mention versions ("Must be backported to 3.0").
- Style: No generic messages like "fix(xxx): blah". Be technically precise.
- Do not mix spelling fixes in comments (not important) with other changes.
However it's preferred to have a single commit for many typo fixes at once.
- Spelling mistakes in user-visible parts (doc, logs, traces, error messages)
must be in their own commit (may need backport).
- One commit per bug.
- Example:
BUG/MEDIUM: sample: fix null pointer dereference in h1_parse_line
When parsing malformed headers, the line buffer was not initialized.
This caused a crash on certain edge cases. Let's fix this by always
initializing the line buffer when first calling the parser. This was
brought by commit 04c9e8f5 ("MINOR: add h1_parse_line") in latest -dev
so no backport is needed.

View file

@ -3095,37 +3095,40 @@ show events [<sink>] [-w] [-n] [-0]
delimited by a line feed character ('\n' or 10 or 0x0A). It is possible to
change this to the NUL character ('\0' or 0) by passing the "-0" argument.
show fd [-!plcfbsd]* [<fd>]
show fd [-!plcfbsd]* [[<tgid>]/[<fd>] | <fd>]
Dump the list of either all open file descriptors or just the one number <fd>
if specified. A set of flags may optionally be passed to restrict the dump
only to certain FD types or to omit certain FD types. When '-' or '!' are
encountered, the selection is inverted for the following characters in the
same argument. The inversion is reset before each argument word delimited by
white spaces. Selectable FD types include 'p' for pipes, 'l' for listeners,
'c' for connections (any type), 'f' for frontend connections, 'b' for backend
connections (any type), 's' for connections to servers, 'd' for connections
to the "dispatch" address or the backend's transparent address. With this,
'b' is a shortcut for 'sd' and 'c' for 'fb' or 'fsd'. 'c!f' is equivalent to
'b' ("any connections except frontend connections" are indeed backend
connections). This is only aimed at developers who need to observe internal
states in order to debug complex issues such as abnormal CPU usages. One fd
is reported per lines, and for each of them, its state in the poller using
upper case letters for enabled flags and lower case for disabled flags, using
"P" for "polled", "R" for "ready", "A" for "active", the events status using
"H" for "hangup", "E" for "error", "O" for "output", "P" for "priority" and
"I" for "input", a few other flags like "N" for "new" (just added into the fd
cache), "U" for "updated" (received an update in the fd cache), "L" for
"linger_risk", "C" for "cloned", then the cached entry position, the pointer
to the internal owner, the pointer to the I/O callback and its name when
known. When the owner is a connection, the connection flags, and the target
are reported (frontend, proxy or server). When the owner is a listener, the
listener's state and its frontend are reported. There is no point in using
this command without a good knowledge of the internals. It's worth noting
that the output format may evolve over time so this output must not be parsed
by tools designed to be durable. Some internal structure states may look
suspicious to the function listing them, in this case the output line will be
suffixed with an exclamation mark ('!'). This may help find a starting point
when trying to diagnose an incident.
if specified. The form "<tgid>/<fd>" is also accepted, where either side may
be empty as a wildcard ("/<fd>" for fd <fd> across thread groups, "<tgid>/"
for all fds of <tgid>). The <tgid> is currently parsed but ignored, pending
future support for per-thread-group fd tables. A set of flags may optionally
be passed to restrict the dump only to certain FD types or to omit certain FD
types. When '-' or '!' are encountered, the selection is inverted for the
following characters in the same argument. The inversion is reset before each
argument word delimited by white spaces. Selectable FD types include 'p' for
pipes, 'l' for listeners, 'c' for connections (any type), 'f' for frontend
connections, 'b' for backend connections (any type), 's' for connections to
servers, 'd' for connections to the "dispatch" address or the backend's
transparent address. With this, 'b' is a shortcut for 'sd' and 'c' for 'fb' or
'fsd'. 'c!f' is equivalent to 'b' ("any connections except frontend
connections" are indeed backend connections). This is only aimed at developers
who need to observe internal states in order to debug complex issues such as
abnormal CPU usages. One fd is reported per lines, and for each of them, its
state in the poller using upper case letters for enabled flags and lower case
for disabled flags, using "P" for "polled", "R" for "ready", "A" for "active",
the events status using "H" for "hangup", "E" for "error", "O" for "output",
"P" for "priority" and "I" for "input", a few other flags like "N" for "new"
(just added into the fd cache), "U" for "updated" (received an update in the
fd cache), "L" for "linger_risk", "C" for "cloned", then the cached entry
position, the pointer to the internal owner, the pointer to the I/O callback
and its name when known. When the owner is a connection, the connection flags,
and the target are reported (frontend, proxy or server). When the owner is a
listener, the listener's state and its frontend are reported. There is no
point in using this command without a good knowledge of the internals. It's
worth noting that the output format may evolve over time so this output must
not be parsed by tools designed to be durable. Some internal structure states
may look suspicious to the function listing them, in this case the output line
will be suffixed with an exclamation mark ('!'). This may help find a starting
point when trying to diagnose an incident.
show info [typed|json] [desc] [float]
Dump info about haproxy status on current process. If "typed" is passed as an

View file

@ -1,4 +1,4 @@
2020/03/05 Willy Tarreau
2026/04/27 Willy Tarreau
HAProxy Technologies
The PROXY protocol
Versions 1 & 2
@ -31,6 +31,7 @@ Revision history
2025/09/09 - added SSL-related TLVs for key exchange group and signature
scheme (Steven Collison)
2026/01/15 - added SSL client certificate TLV (Simon Ser)
2026/04/27 - clarified UDP usage (Valaphee)
1. Background
@ -175,6 +176,11 @@ The receiver may apply a short timeout and decide to abort the connection if
the protocol header is not seen within a few seconds (at least 3 seconds to
cover a TCP retransmit).
For UDP, the PROXY protocol header and the proxied UDP payload MUST be sent in
the same datagram. The sender MUST NOT split the PROXY protocol header across
multiple UDP datagrams, and the receiver MUST parse the header independently
for each received datagram.
The receiver MUST be configured to only receive the protocol described in this
specification and MUST not try to guess whether the protocol header is present
or not. This means that the protocol explicitly prevents port sharing between

View file

@ -16,6 +16,6 @@ traces
trace applet verbosity complete start now
trace h3 start now
trace quic start now
trace qmux start now
trace qcm start now
trace peers start now
.endif

View file

@ -4,6 +4,8 @@
#include <haproxy/acme_resolvers-t.h>
#include <haproxy/istbuf.h>
#include <haproxy/buf-t.h>
#include <haproxy/jwt-t.h>
#include <haproxy/openssl-compat.h>
#if defined(HAVE_ACME)
@ -40,6 +42,13 @@ struct acme_cfg {
int bits; /* bits for RSA */
int curves; /* NID of curves */
} key;
struct {
char *kid_file; /* EAB key id filename */
char *mac_key_file; /* base64url encoded EAB hmac key filename */
char *kid; /* EAB key id */
struct buffer mac_key; /* raw EAB hmac key */
enum jwt_alg mac_alg; /* MAC algorithm for EAB signature */
} eab;
char *challenge; /* HTTP-01, DNS-01, etc */
char *profile; /* ACME profile */
char *vars; /* variables put in the dpapi sink */

View file

@ -22,7 +22,6 @@
extern struct userlist *userlist;
struct userlist *auth_find_userlist(char *name);
unsigned int auth_resolve_groups(struct userlist *l, char *groups);
int userlist_postinit();
void userlist_free(struct userlist *ul);
struct pattern *pat_match_auth(struct sample *smp, struct pattern_expr *expr, int fill);

View file

@ -156,6 +156,7 @@ struct lbprm_per_tgrp {
* The other ones might take it themselves if needed.
*/
struct lb_ops {
struct list link;
int (*proxy_init)(struct proxy *); /* set up per-proxy LB state at config time; <0=fail */
void (*update_server_eweight)(struct server *); /* to be called after eweight change // srvlock */
void (*set_server_status_up)(struct server *); /* to be called after status changes to UP // srvlock */
@ -166,6 +167,11 @@ struct lb_ops {
void (*proxy_deinit)(struct proxy *); /* to be called when we're destroying the proxy */
void (*server_deinit)(struct server *); /* to be called when we're destroying the server */
int (*server_init)(struct server *); /* initialize a freshly added server (runtime); <0=fail. */
uint32_t algo_prop; /* load balancing algorithm lookup and properties */
struct {
uint32_t mask;
uint32_t match;
} map[VAR_ARRAY];
};
/* LB parameters for all algorithms */

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