]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: otel: added HAProxy variable storage for context propagation
authorMiroslav Zagorac <mzagorac@haproxy.com>
Sun, 12 Apr 2026 09:25:14 +0000 (11:25 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 07:23:26 +0000 (09:23 +0200)
Added support for storing OTel span context in HAProxy transaction
variables as an alternative to HTTP headers, enabled by the OTEL_USE_VARS
compile flag.

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

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

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

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

The feature is conditionally compiled: the OTEL_USE_VARS flag controls
whether vars.c is included in the build and whether the "use-vars" keyword
is available in the configuration parser.

Makefile
addons/otel/Makefile
addons/otel/include/filter.h
addons/otel/include/include.h
addons/otel/include/parser.h
addons/otel/include/vars.h [new file with mode: 0644]
addons/otel/src/event.c
addons/otel/src/filter.c
addons/otel/src/parser.c
addons/otel/src/scope.c
addons/otel/src/vars.c [new file with mode: 0644]

index ff09a1219c2a7a9ad996032328ba1c7e572ac63c..00b7fe225809895f0f15d7d92f107cee7b246afd 100644 (file)
--- a/Makefile
+++ b/Makefile
 #   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, ...).
index 8b5b069160e1aaad68f6b19fb0691465f401f8e1..96a5c488c023dcc137a0565f39625480c038fdc5 100644 (file)
@@ -3,6 +3,7 @@
 # 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    =
@@ -61,4 +62,9 @@ OPTIONS_OBJS += \
        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
+endif
+
 OTEL_CFLAGS := $(OTEL_CFLAGS) -Iaddons/otel/include $(OTEL_DEFINE)
index 6ca82800a32093d06591d74cda452eb6df262fc9..8af922b82907df39453e755c892dccb2c9b3a590 100644 (file)
@@ -5,6 +5,7 @@
 
 #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"
index 14c24d0d7068fc655302042c6c08ef655352538b..e4047d2f13b7b7788923f176f87042881e756fe7 100644 (file)
@@ -38,6 +38,7 @@
 #include "pool.h"
 #include "scope.h"
 #include "util.h"
+#include "vars.h"
 
 #endif /* _OTEL_INCLUDE_H_ */
 
index 7ef1954e330544471e3178bd39f49b4664b19b68..9eb45e6123e8b1f6ac0da12a901962abff832e8d 100644 (file)
@@ -22,6 +22,7 @@
 #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"
        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> ...")
 
-#define FLT_OTEL_PARSE_SCOPE_INJECT_HELP      " <name-prefix> [use-headers]"
-#define FLT_OTEL_PARSE_SCOPE_EXTRACT_HELP     " <name-prefix> [use-headers]"
+#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
@@ -108,7 +114,8 @@ enum FLT_OTEL_PARSE_SCOPE_enum {
 
 /* Context storage type flags for inject/extract operations. */
 enum FLT_OTEL_CTX_USE_enum {
-       FLT_OTEL_CTX_USE_HEADERS = 1 << 0,
+       FLT_OTEL_CTX_USE_VARS    = 1 << 0,
+       FLT_OTEL_CTX_USE_HEADERS = 1 << 1,
 };
 
 /* Logging state flags for the OTel filter. */
diff --git a/addons/otel/include/vars.h b/addons/otel/include/vars.h
new file mode 100644 (file)
index 0000000..969dbc6
--- /dev/null
@@ -0,0 +1,49 @@
+/* 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_CTX_SIZE     int8_t
+#define FLT_OTEL_VAR_CHAR_DASH    'D'
+#define FLT_OTEL_VAR_CHAR_SPACE   'S'
+
+/* 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 *);
+
+
+#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
+ */
index 31bae31005de2cb1b7a74f17606cd92cb0ec05d6..38a064458f7bb0c5a2898caf85f1d14617ba9354 100644 (file)
@@ -31,7 +31,8 @@ const struct flt_otel_event_data flt_otel_event_data[FLT_OTEL_EVENT_MAX] = { FLT
  * 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 if configured in <conf_span>.
+ *   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.
@@ -77,7 +78,7 @@ static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct ch
                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. */
+       /* 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;
@@ -85,8 +86,17 @@ static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct ch
                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_HEADERS) {
+                       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)
@@ -121,10 +131,10 @@ static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct ch
  *
  * DESCRIPTION
  *   Executes a complete scope: evaluates ACL conditions, extracts contexts
- *   from HTTP headers, 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,
- *   then marks and finishes completed spans.
+ *   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, then marks and finishes completed spans.
  *
  * RETURN VALUE
  *   Returns FLT_OTEL_RET_OK on success, FLT_OTEL_RET_ERROR on failure.
@@ -200,10 +210,15 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn,
                FLT_OTEL_DBG_CONF_CONTEXT("run context ", conf_ctx);
 
                /*
-                * The OpenTelemetry context is read from the HTTP headers.
+                * 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)
@@ -341,6 +356,9 @@ int flt_otel_event_run(struct stream *s, struct filter *f, struct channel *chn,
                        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));
index b8eaaa58b69b8b5db8d18cbe203601a18cc13719..939909201a878a94a52d1ce14ed48c4c0cd9c9e5 100644 (file)
@@ -895,6 +895,9 @@ static int flt_otel_ops_attach(struct stream *s, struct filter *f)
 #endif
        FLT_OTEL_LOG(LOG_INFO, "%08x %08x", f->pre_analyzers, f->post_analyzers);
 
+#ifdef USE_OTEL_VARS
+       flt_otel_vars_dump(s);
+#endif
        flt_otel_http_headers_dump(&(s->req));
 
        OTELC_RETURN_INT(FLT_OTEL_RET_OK);
index bcc815ee8e95795a256adf14f4d7cc9e895b7f33..2186b0d3ce3a6c15056f4dc181ae03cb6a10f086 100644 (file)
@@ -756,6 +756,8 @@ static int flt_otel_post_parse_cfg_group(void)
  *
  * DESCRIPTION
  *   Parses the context storage type argument for inject/extract keywords.
+ *   Accepts "use-headers" or (when USE_OTEL_VARS is defined) "use-vars".
+ *   Both types may be used simultaneously on the same span.
  *
  * RETURN VALUE
  *   Returns ERR_NONE (== 0) in case of success,
@@ -770,6 +772,10 @@ static int flt_otel_parse_cfg_scope_ctx(char **args, int cur_arg, char **err)
 
        if (FLT_OTEL_PARSE_KEYWORD(cur_arg, FLT_OTEL_PARSE_CTX_USE_HEADERS))
                flags = FLT_OTEL_CTX_USE_HEADERS;
+#ifdef USE_OTEL_VARS
+       else if (FLT_OTEL_PARSE_KEYWORD(cur_arg, FLT_OTEL_PARSE_CTX_USE_VARS))
+               flags = FLT_OTEL_CTX_USE_VARS;
+#endif
        else
                FLT_OTEL_PARSE_ERR(err, "'%s' : invalid context storage type", args[0]);
 
@@ -1030,6 +1036,10 @@ static int flt_otel_parse_cfg_scope(const char *file, int line, char **args, int
                        conf_ctx->flags = FLT_OTEL_CTX_USE_HEADERS;
                else if (FLT_OTEL_PARSE_KEYWORD(2, FLT_OTEL_PARSE_CTX_USE_HEADERS))
                        conf_ctx->flags = FLT_OTEL_CTX_USE_HEADERS;
+#ifdef USE_OTEL_VARS
+               else if (FLT_OTEL_PARSE_KEYWORD(2, FLT_OTEL_PARSE_CTX_USE_VARS))
+                       conf_ctx->flags = FLT_OTEL_CTX_USE_VARS;
+#endif
                else
                        FLT_OTEL_PARSE_ERR(&err, "'%s' : invalid context storage type", args[2]);
        }
index 0b096b16e7ddadcf183af15bdfc18ebd131a5fa3..cdef4c19fa04c39c686364218c6afec19ea3dd24 100644 (file)
@@ -50,6 +50,15 @@ struct flt_otel_runtime_context *flt_otel_runtime_context_init(struct stream *s,
        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);
@@ -664,7 +673,8 @@ void flt_otel_scope_finish_marked(const struct flt_otel_runtime_context *rt_ctx,
  * 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 are also cleaned up via <chn>.
+ *   associated HTTP headers and HAProxy variables are also cleaned up via
+ *   <chn>.
  *
  * RETURN VALUE
  *   This function does not return a value.
@@ -692,10 +702,13 @@ void flt_otel_scope_free_unused(struct flt_otel_runtime_context *rt_ctx, struct
                list_for_each_entry_safe(ctx, ctx_back, &(rt_ctx->contexts), list)
                        if (ctx->context == NULL) {
                                /*
-                                * All headers associated with the context in
-                                * question should be deleted.
+                                * 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);
                        }
diff --git a/addons/otel/src/vars.c b/addons/otel/src/vars.c
new file mode 100644 (file)
index 0000000..ea395a8
--- /dev/null
@@ -0,0 +1,955 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "../include/include.h"
+
+
+#ifdef DEBUG_OTEL
+
+/***
+ * NAME
+ *   flt_otel_vars_scope_dump - debug variable scope dump
+ *
+ * SYNOPSIS
+ *   static void flt_otel_vars_scope_dump(struct vars *vars, const char *scope)
+ *
+ * ARGUMENTS
+ *   vars  - HAProxy variable store to dump
+ *   scope - scope label for log output
+ *
+ * DESCRIPTION
+ *   Dumps the contents of all variables defined for a particular <scope>.
+ *   Acquires a read lock on the variable store, iterates over all name root
+ *   trees, and logs each variable's name hash and string value.
+ *
+ * RETURN VALUE
+ *   This function does not return a value.
+ */
+static void flt_otel_vars_scope_dump(struct vars *vars, const char *scope)
+{
+       int i;
+
+       if (vars == NULL)
+               return;
+
+       vars_rdlock(vars);
+       for (i = 0; i < VAR_NAME_ROOTS; i++) {
+               struct ceb_node *node = cebu64_imm_first(&(vars->name_root[i]));
+
+               for ( ; node != NULL; node = cebu64_imm_next(&(vars->name_root[i]), node)) {
+                       struct var *var = container_of(node, struct var, name_node);
+
+                       OTELC_DBG(NOTICE, "'%s.%016" PRIx64 "' -> '%.*s'", scope, var->name_hash, (int)b_data(&(var->data.u.str)), b_orig(&(var->data.u.str)));
+               }
+       }
+       vars_rdunlock(vars);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_vars_dump - debug all variables dump
+ *
+ * SYNOPSIS
+ *   void flt_otel_vars_dump(struct stream *s)
+ *
+ * ARGUMENTS
+ *   s - stream whose variables to dump
+ *
+ * DESCRIPTION
+ *   Dumps all variables across all scopes (PROC, SESS, TXN, REQ/RES) by calling
+ *   flt_otel_vars_scope_dump() for each scope's variable store.
+ *
+ * RETURN VALUE
+ *   This function does not return a value.
+ */
+void flt_otel_vars_dump(struct stream *s)
+{
+       OTELC_FUNC("%p", s);
+
+       /*
+        * It would be nice if we could use the get_vars() function from HAProxy
+        * source here to get the value of the 'vars' pointer, but it is defined
+        * as 'static inline', so unfortunately none of this is possible.
+        */
+       flt_otel_vars_scope_dump(&(proc_vars), "PROC");
+       flt_otel_vars_scope_dump(&(s->sess->vars), "SESS");
+       flt_otel_vars_scope_dump(&(s->vars_txn), "TXN");
+       flt_otel_vars_scope_dump(&(s->vars_reqres), "REQ/RES");
+
+       OTELC_RETURN();
+}
+
+#endif /* DEBUG_OTEL */
+
+
+/***
+ * NAME
+ *   flt_otel_normalize_name - variable name normalization
+ *
+ * SYNOPSIS
+ *   static int flt_otel_normalize_name(char *var_name, size_t size, int *len, const char *name, bool flag_cpy, char **err)
+ *
+ * ARGUMENTS
+ *   var_name - output buffer for the normalized name
+ *   size     - output buffer size
+ *   len      - pointer to the current position in the output buffer
+ *   name     - source name to normalize
+ *   flag_cpy - whether to copy name without normalization
+ *   err      - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Normalizes a variable name component into the output buffer.  Adds a
+ *   dot separator between components when needed.  When <flag_cpy> is set,
+ *   the name is copied verbatim; otherwise, dashes are replaced with
+ *   FLT_OTEL_VAR_CHAR_DASH, spaces with FLT_OTEL_VAR_CHAR_SPACE, and uppercase
+ *   letters are converted to lowercase.
+ *
+ * RETURN VALUE
+ *   Returns the number of characters written, or FLT_OTEL_RET_ERROR on failure.
+ */
+static int flt_otel_normalize_name(char *var_name, size_t size, int *len, const char *name, bool flag_cpy, char **err)
+{
+       int retval = 0;
+
+       OTELC_FUNC("%p, %zu, %p, \"%s\", %hhu, %p:%p", var_name, size, len, OTELC_STR_ARG(name), flag_cpy, OTELC_DPTR_ARGS(err));
+
+       if (!OTELC_STR_IS_VALID(name))
+               OTELC_RETURN_INT(retval);
+
+       /*
+        * In case the name of the variable consists of several elements,
+        * the character '.' is added between them.
+        */
+       if ((*len == 0) || (var_name[*len - 1] == '.'))
+               /* Do nothing. */;
+       else if (*len < (size - 1))
+               var_name[(*len)++] = '.';
+       else {
+               FLT_OTEL_ERR("failed to normalize variable name, buffer too small");
+
+               retval = FLT_OTEL_RET_ERROR;
+       }
+
+       if (retval == FLT_OTEL_RET_ERROR) {
+               /* Do nothing. */
+       }
+       else if (flag_cpy) {
+               /* Copy variable name without modification. */
+               retval = strlen(name);
+               if ((*len + retval + 1) > size) {
+                       FLT_OTEL_ERR("failed to normalize variable name, buffer too small");
+
+                       retval = FLT_OTEL_RET_ERROR;
+               } else {
+                       (void)memcpy(var_name + *len, name, retval + 1);
+
+                       *len += retval;
+               }
+       } else {
+               /*
+                * HAProxy does not allow the use of variable names containing
+                * '-' or ' '.  This of course applies to HTTP header names as
+                * well.  Also, here the capital letters are converted to
+                * lowercase.
+                */
+               while (retval != FLT_OTEL_RET_ERROR)
+                       if (*len >= (size - 1)) {
+                               FLT_OTEL_ERR("failed to normalize variable name, buffer too small");
+
+                               retval = FLT_OTEL_RET_ERROR;
+                       } else {
+                               uint8_t ch = name[retval];
+
+                               if (ch == '\0')
+                                       break;
+                               else if (ch == '-')
+                                       ch = FLT_OTEL_VAR_CHAR_DASH;
+                               else if (ch == ' ')
+                                       ch = FLT_OTEL_VAR_CHAR_SPACE;
+                               else if (isupper(ch))
+                                       ch = ist_lc[ch];
+
+                               var_name[(*len)++] = ch;
+                               retval++;
+                       }
+
+               var_name[*len] = '\0';
+       }
+
+       OTELC_DBG(DEBUG, "var_name: \"%s\" %d/%d", OTELC_STR_ARG(var_name), retval, *len);
+
+       if (retval == FLT_OTEL_RET_ERROR)
+               *len = retval;
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_denormalize_name - reverse variable name normalization
+ *
+ * SYNOPSIS
+ *   static int flt_otel_denormalize_name(const char *var_name, char *name, size_t size, char **err)
+ *
+ * ARGUMENTS
+ *   var_name - normalized variable name
+ *   name     - output buffer for the denormalized name
+ *   size     - output buffer size
+ *   err      - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Reverses the normalization applied by flt_otel_normalize_name().  Restores
+ *   dashes from FLT_OTEL_VAR_CHAR_DASH and spaces from FLT_OTEL_VAR_CHAR_SPACE.
+ *
+ * RETURN VALUE
+ *   Returns the length of the denormalized name, or FLT_OTEL_RET_ERROR if the
+ *   output buffer is too small.
+ */
+static int flt_otel_denormalize_name(const char *var_name, char *name, size_t size, char **err)
+{
+       int len;
+
+       /* Reverse character substitutions applied during normalization. */
+       for (len = 0; var_name[len] != '\0'; len++) {
+               if (len >= (size - 1)) {
+                       FLT_OTEL_ERR("failed to reverse variable name, buffer too small");
+
+                       return FLT_OTEL_RET_ERROR;
+               }
+
+               if (var_name[len] == FLT_OTEL_VAR_CHAR_DASH)
+                       name[len] = '-';
+               else if (var_name[len] == FLT_OTEL_VAR_CHAR_SPACE)
+                       name[len] = ' ';
+               else
+                       name[len] = var_name[len];
+       }
+       name[len] = '\0';
+
+       return len;
+}
+
+
+/***
+ * NAME
+ *   flt_otel_var_name - full variable name construction
+ *
+ * SYNOPSIS
+ *   static int flt_otel_var_name(const char *scope, const char *prefix, const char *name, bool flag_cpy, char *var_name, size_t size, char **err)
+ *
+ * ARGUMENTS
+ *   scope    - variable scope component
+ *   prefix   - variable prefix component
+ *   name     - variable name component
+ *   flag_cpy - whether to copy name without normalization
+ *   var_name - output buffer for the constructed name
+ *   size     - output buffer size
+ *   err      - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Constructs a full variable name from <scope>, <prefix>, and <name>
+ *   components, separated by dots.  Each component is normalized via
+ *   flt_otel_normalize_name().  NULL components are skipped.
+ *
+ * RETURN VALUE
+ *   Returns the total name length, or FLT_OTEL_RET_ERROR on failure.
+ */
+static int flt_otel_var_name(const char *scope, const char *prefix, const char *name, bool flag_cpy, char *var_name, size_t size, char **err)
+{
+       int retval = 0;
+
+       OTELC_FUNC("\"%s\", \"%s\", \"%s\", %hhu, %p, %zu, %p:%p", OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), flag_cpy, var_name, size, OTELC_DPTR_ARGS(err));
+
+       if (flt_otel_normalize_name(var_name, size, &retval, scope, 0, err) >= 0)
+               if (flt_otel_normalize_name(var_name, size, &retval, prefix, 0, err) >= 0)
+                       (void)flt_otel_normalize_name(var_name, size, &retval, name, flag_cpy, err);
+
+       if (retval == FLT_OTEL_RET_ERROR)
+               FLT_OTEL_ERR("failed to construct variable name '%s.%s.%s'", scope, prefix, name);
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_smp_init - sample structure initialization
+ *
+ * SYNOPSIS
+ *   static inline void flt_otel_smp_init(struct stream *s, struct sample *smp, uint opt, int type, const char *data)
+ *
+ * ARGUMENTS
+ *   s    - current stream
+ *   smp  - sample structure to initialize
+ *   opt  - sample option flags
+ *   type - sample data type
+ *   data - string data to store (or NULL)
+ *
+ * DESCRIPTION
+ *   Initializes the <smp> structure and sets stream ownership via
+ *   smp_set_owner().  If the <data> argument is non-NULL, the sample_data
+ *   member is also initialized with the given <type> and string content.
+ *
+ * RETURN VALUE
+ *   This function does not return a value.
+ */
+static inline void flt_otel_smp_init(struct stream *s, struct sample *smp, uint opt, int type, const char *data)
+{
+       (void)memset(smp, 0, sizeof(*smp));
+       (void)smp_set_owner(smp, s->be, s->sess, s, opt | SMP_OPT_FINAL);
+
+       if (data != NULL) {
+               smp->data.type = type;
+
+               chunk_initstr(&(smp->data.u.str), data);
+       }
+}
+
+
+/***
+ * NAME
+ *   flt_otel_smp_add - context variable name registration
+ *
+ * SYNOPSIS
+ *   static int flt_otel_smp_add(struct sample_data *data, const char *name, size_t len, char **err)
+ *
+ * ARGUMENTS
+ *   data - binary sample data buffer
+ *   name - context variable name to append
+ *   len  - length of the variable name
+ *   err  - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Appends a context variable name to the binary sample data buffer used for
+ *   tracking registered context variables.  If the buffer is not yet allocated,
+ *   it is initialized with global.tune.bufsize bytes.  The name is stored as a
+ *   length-prefixed entry (FLT_OTEL_VAR_CTX_SIZE byte followed by the name
+ *   data).  Validates that the name length fits in the size field and that the
+ *   buffer has sufficient room.
+ *
+ * RETURN VALUE
+ *   Returns the buffer offset before appending, or FLT_OTEL_RET_ERROR on
+ *   failure.
+ */
+static int flt_otel_smp_add(struct sample_data *data, const char *name, size_t len, char **err)
+{
+       bool flag_alloc = 0;
+       int  retval = FLT_OTEL_RET_ERROR;
+
+       OTELC_FUNC("%p, \"%.*s\", %zu, %p:%p", data, (int)len, name, len, OTELC_DPTR_ARGS(err));
+
+       FLT_OTEL_DBG_BUF(INFO, &(data->u.str));
+
+       /* Lazily allocate the sample buffer on first use. */
+       if (b_orig(&(data->u.str)) == NULL) {
+               data->type = SMP_T_BIN;
+               chunk_init(&(data->u.str), OTELC_MALLOC(global.tune.bufsize), global.tune.bufsize);
+
+               flag_alloc = (b_orig(&(data->u.str)) != NULL);
+       }
+
+       /* Verify the buffer allocation succeeded. */
+       if (b_orig(&(data->u.str)) == NULL) {
+               FLT_OTEL_ERR("failed to add ctx '%.*s', not enough memory", (int)len, name);
+       }
+       else if (len > ((UINT64_C(1) << ((sizeof(FLT_OTEL_VAR_CTX_SIZE) << 3) - 1)) - 1)) {
+               FLT_OTEL_ERR("failed to add ctx '%.*s', name too long", (int)len, name);
+       }
+       else if ((len + sizeof(FLT_OTEL_VAR_CTX_SIZE)) > b_room(&(data->u.str))) {
+               FLT_OTEL_ERR("failed to add ctx '%.*s', too many names", (int)len, name);
+       }
+       else {
+               retval = b_data(&(data->u.str));
+
+               b_putchr(&(data->u.str), len);
+               (void)__b_putblk(&(data->u.str), name, len);
+
+               FLT_OTEL_DBG_BUF(INFO, &(data->u.str));
+       }
+
+       if ((retval == FLT_OTEL_RET_ERROR) && flag_alloc)
+               OTELC_SFREE(b_orig(&(data->u.str)));
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_ctx_loop - context variable name iterator
+ *
+ * SYNOPSIS
+ *   static int flt_otel_ctx_loop(struct sample *smp, const char *scope, const char *prefix, char **err, flt_otel_ctx_loop_cb func, void *ptr)
+ *
+ * ARGUMENTS
+ *   smp    - sample used to retrieve the context tracking variable
+ *   scope  - variable scope
+ *   prefix - variable prefix
+ *   err    - indirect pointer to error message string
+ *   func   - callback function invoked for each context variable
+ *   ptr    - opaque data passed to the callback
+ *
+ * DESCRIPTION
+ *   Iterates over all context variable names stored in the binary tracking
+ *   buffer.  Retrieves the tracking variable by constructing its name from
+ *   <scope> and <prefix>.  Each stored entry (length-prefixed name) is
+ *   extracted and passed to the <func> callback.  Iteration stops if the
+ *   callback returns a positive value (match found) or FLT_OTEL_RET_ERROR.
+ *
+ * RETURN VALUE
+ *   Returns the match position (positive), 0 if no match,
+ *   or FLT_OTEL_RET_ERROR on failure.
+ */
+static int flt_otel_ctx_loop(struct sample *smp, const char *scope, const char *prefix, char **err, flt_otel_ctx_loop_cb func, void *ptr)
+{
+       FLT_OTEL_VAR_CTX_SIZE var_ctx_size;
+       char                  var_name[BUFSIZ], var_ctx[BUFSIZ];
+       int                   i, var_name_len, var_ctx_len, rc, n = 1, retval = 0;
+
+       OTELC_FUNC("%p, \"%s\", \"%s\", %p:%p, %p, %p", smp, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_DPTR_ARGS(err), func, ptr);
+
+       /*
+        * The variable in which we will save the name of the OpenTelemetry
+        * context variable.
+        */
+       var_name_len = flt_otel_var_name(scope, prefix, NULL, 0, var_name, sizeof(var_name), err);
+       if (var_name_len == FLT_OTEL_RET_ERROR)
+               OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
+
+       /*
+        * Here we will try to find all the previously recorded variables from
+        * the currently set OpenTelemetry context.  If we find the required
+        * variable and it is marked as deleted, we will mark it as active.
+        * If we do not find it, then it is added to the end of the previously
+        * saved names.
+        */
+       if (vars_get_by_name(var_name, var_name_len, smp, NULL) == 0) {
+               OTELC_DBG(NOTICE, "ctx '%s' no variable found", var_name);
+       }
+       else if (smp->data.type != SMP_T_BIN) {
+               FLT_OTEL_ERR("ctx '%s' invalid data type %d", var_name, smp->data.type);
+
+               retval = FLT_OTEL_RET_ERROR;
+       }
+       else {
+               FLT_OTEL_DBG_BUF(INFO, &(smp->data.u.str));
+
+               for (i = 0; i < b_data(&(smp->data.u.str)); i += sizeof(var_ctx_size) + var_ctx_len, n++) {
+                       var_ctx_size = *((typeof(var_ctx_size) *)(b_orig(&(smp->data.u.str)) + i));
+                       var_ctx_len  = abs(var_ctx_size);
+
+                       if ((i + sizeof(var_ctx_size) + var_ctx_len) > b_data(&(smp->data.u.str))) {
+                               FLT_OTEL_ERR("ctx '%s' invalid data size", var_name);
+
+                               retval = FLT_OTEL_RET_ERROR;
+
+                               break;
+                       }
+
+                       (void)memcpy(var_ctx, b_orig(&(smp->data.u.str)) + i + sizeof(var_ctx_size), var_ctx_len);
+                       var_ctx[var_ctx_len] = '\0';
+
+                       rc = func(smp, i, scope, prefix, var_ctx, var_ctx_size, err, ptr);
+                       if (rc == FLT_OTEL_RET_ERROR) {
+                               retval = FLT_OTEL_RET_ERROR;
+
+                               break;
+                       }
+                       else if (rc > 0) {
+                               retval = n;
+
+                               break;
+                       }
+               }
+       }
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_ctx_set_cb - context variable existence check callback
+ *
+ * SYNOPSIS
+ *   static int flt_otel_ctx_set_cb(struct sample *smp, size_t idx, const char *scope, const char *prefix, const char *name, FLT_OTEL_VAR_CTX_SIZE name_len, char **err, void *ptr)
+ *
+ * ARGUMENTS
+ *   smp      - current sample (unused)
+ *   idx      - buffer offset (unused)
+ *   scope    - variable scope (unused)
+ *   prefix   - variable prefix (unused)
+ *   name     - context variable name to check
+ *   name_len - length of the name
+ *   err      - unused
+ *   ptr      - pointer to flt_otel_ctx structure with the search target
+ *
+ * DESCRIPTION
+ *   Callback for flt_otel_ctx_loop() that checks whether a context variable
+ *   <name> matches the search target stored in the flt_otel_ctx structure.
+ *
+ * RETURN VALUE
+ *   Returns 1 if the <name> matches, or 0 otherwise.
+ */
+static int flt_otel_ctx_set_cb(struct sample *smp, size_t idx, const char *scope, const char *prefix, const char *name, FLT_OTEL_VAR_CTX_SIZE name_len, char **err, void *ptr)
+{
+       struct flt_otel_ctx *ctx = ptr;
+       int                  retval = 0;
+
+       OTELC_FUNC("%p, %zu, \"%s\", \"%s\", \"%s\", %hhd, %p:%p, %p", smp, idx, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), name_len, OTELC_DPTR_ARGS(err), ptr);
+
+       if ((name_len == ctx->value_len) && (strncmp(name, ctx->value, name_len) == 0)) {
+               OTELC_DBG(NOTICE, "ctx '%s' found", name);
+
+               retval = 1;
+       }
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_ctx_set - context variable tracking registration
+ *
+ * SYNOPSIS
+ *   static int flt_otel_ctx_set(struct stream *s, const char *scope, const char *prefix, const char *name, uint opt, char **err)
+ *
+ * ARGUMENTS
+ *   s      - current stream
+ *   scope  - variable scope
+ *   prefix - variable prefix
+ *   name   - context variable name to register
+ *   opt    - sample option flags
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Registers a context variable name in the binary tracking buffer if it is
+ *   not already present.  Constructs the tracking variable name from <scope>
+ *   and <prefix>, then uses flt_otel_ctx_loop() with flt_otel_ctx_set_cb() to
+ *   check for duplicates.  If not found, the normalized name is appended to the
+ *   tracking buffer via flt_otel_smp_add() and the updated buffer is stored
+ *   back into the HAProxy variable.
+ *
+ * RETURN VALUE
+ *   Returns the buffer data size on success, a positive value if already
+ *   registered, or FLT_OTEL_RET_ERROR on failure.
+ */
+static int flt_otel_ctx_set(struct stream *s, const char *scope, const char *prefix, const char *name, uint opt, char **err)
+{
+       struct flt_otel_ctx ctx;
+       struct sample       smp_ctx;
+       char                var_name[BUFSIZ];
+       bool                flag_alloc = 0;
+       int                 rc, var_name_len, retval = FLT_OTEL_RET_ERROR;
+
+       OTELC_FUNC("%p, \"%s\", \"%s\", \"%s\", %u, %p:%p", s, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), opt, OTELC_DPTR_ARGS(err));
+
+       /*
+        * The variable in which we will save the name of the OpenTelemetry
+        * context variable.
+        */
+       var_name_len = flt_otel_var_name(scope, prefix, NULL, 0, var_name, sizeof(var_name), err);
+       if (var_name_len == FLT_OTEL_RET_ERROR)
+               OTELC_RETURN_INT(retval);
+
+       /* Normalized name of the OpenTelemetry context variable. */
+       ctx.value_len = flt_otel_var_name(name, NULL, NULL, 0, ctx.value, sizeof(ctx.value), err);
+       if (ctx.value_len == FLT_OTEL_RET_ERROR)
+               OTELC_RETURN_INT(retval);
+
+       flt_otel_smp_init(s, &smp_ctx, opt, 0, NULL);
+
+       /* Loop through existing context variables and apply set operations. */
+       retval = flt_otel_ctx_loop(&smp_ctx, scope, prefix, err, flt_otel_ctx_set_cb, &ctx);
+       if (retval == 0) {
+               rc = flt_otel_smp_add(&(smp_ctx.data), ctx.value, ctx.value_len, err);
+               if (rc == FLT_OTEL_RET_ERROR)
+                       retval = FLT_OTEL_RET_ERROR;
+
+               flag_alloc = (rc == 0);
+       }
+
+       /* Persist the context data as a HAProxy variable. */
+       if (retval == FLT_OTEL_RET_ERROR) {
+               /* Do nothing. */
+       }
+       else if (retval > 0) {
+               OTELC_DBG(NOTICE, "ctx '%s' data found", ctx.value);
+       }
+       else if (vars_set_by_name_ifexist(var_name, var_name_len, &smp_ctx) == 0) {
+               FLT_OTEL_ERR("failed to set ctx '%s'", var_name);
+
+               retval = FLT_OTEL_RET_ERROR;
+       }
+       else {
+               OTELC_DBG(NOTICE, "ctx '%s' -> '%.*s' set", var_name, (int)b_data(&(smp_ctx.data.u.str)), b_orig(&(smp_ctx.data.u.str)));
+
+               retval = b_data(&(smp_ctx.data.u.str));
+       }
+
+       if (flag_alloc)
+               OTELC_SFREE(b_orig(&(smp_ctx.data.u.str)));
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_vars_unset_cb - context variable unset callback
+ *
+ * SYNOPSIS
+ *   static int flt_otel_vars_unset_cb(struct sample *smp, size_t idx, const char *scope, const char *prefix, const char *name, FLT_OTEL_VAR_CTX_SIZE name_len, char **err, void *ptr)
+ *
+ * ARGUMENTS
+ *   smp      - current sample with stream context
+ *   idx      - buffer offset (unused)
+ *   scope    - variable scope
+ *   prefix   - variable prefix
+ *   name     - context variable name to unset
+ *   name_len - length of the name (unused)
+ *   err      - indirect pointer to error message string
+ *   ptr      - unused
+ *
+ * DESCRIPTION
+ *   Callback for flt_otel_ctx_loop() that unsets a single context variable.
+ *   Constructs the full variable name from <scope>, <prefix>, and <name>, then
+ *   calls vars_unset_by_name_ifexist() to remove it.
+ *
+ * RETURN VALUE
+ *   Returns 0 on success, or FLT_OTEL_RET_ERROR on failure.
+ */
+static int flt_otel_vars_unset_cb(struct sample *smp, size_t idx, const char *scope, const char *prefix, const char *name, FLT_OTEL_VAR_CTX_SIZE name_len, char **err, void *ptr)
+{
+       struct sample smp_ctx;
+       char          var_ctx[BUFSIZ];
+       int           var_ctx_len, retval = FLT_OTEL_RET_ERROR;
+
+       OTELC_FUNC("%p, %zu, \"%s\", \"%s\", \"%s\", %hhd, %p:%p, %p", smp, idx, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), name_len, OTELC_DPTR_ARGS(err), ptr);
+
+       var_ctx_len = flt_otel_var_name(scope, prefix, name, 1, var_ctx, sizeof(var_ctx), err);
+       if (var_ctx_len == FLT_OTEL_RET_ERROR) {
+               FLT_OTEL_ERR("ctx '%s' invalid", name);
+
+               OTELC_RETURN_INT(retval);
+       }
+
+       flt_otel_smp_init(smp->strm, &smp_ctx, smp->opt, 0, NULL);
+
+       if (vars_unset_by_name_ifexist(var_ctx, var_ctx_len, &smp_ctx) == 0) {
+               FLT_OTEL_ERR("ctx '%s' no variable found", var_ctx);
+       } else {
+               OTELC_DBG(NOTICE, "ctx '%s' unset", var_ctx);
+
+               retval = 0;
+       }
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_vars_unset - context variables bulk unset
+ *
+ * SYNOPSIS
+ *   int flt_otel_vars_unset(struct stream *s, const char *scope, const char *prefix, uint opt, char **err)
+ *
+ * ARGUMENTS
+ *   s      - current stream
+ *   scope  - variable scope
+ *   prefix - variable prefix
+ *   opt    - sample option flags
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Unsets all context variables for a given <prefix> by iterating the tracking
+ *   buffer via flt_otel_ctx_loop() with flt_otel_vars_unset_cb().  After all
+ *   individual context variables are removed, the tracking variable itself
+ *   (which stores the list of names) is also unset.
+ *
+ * RETURN VALUE
+ *   Returns 1 on success, 0 if no tracking variable exists,
+ *   or FLT_OTEL_RET_ERROR on failure.
+ */
+int flt_otel_vars_unset(struct stream *s, const char *scope, const char *prefix, uint opt, char **err)
+{
+       struct sample smp_ctx;
+       char          var_name[BUFSIZ];
+       int           var_name_len, retval;
+
+       OTELC_FUNC("%p, \"%s\", \"%s\", %u, %p:%p", s, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), opt, OTELC_DPTR_ARGS(err));
+
+       flt_otel_smp_init(s, &smp_ctx, opt, 0, NULL);
+
+       retval = flt_otel_ctx_loop(&smp_ctx, scope, prefix, err, flt_otel_vars_unset_cb, NULL);
+       if (retval != FLT_OTEL_RET_ERROR) {
+               /*
+                * After all ctx variables have been unset, the variable used
+                * to store their names should also be unset.
+                */
+               var_name_len = flt_otel_var_name(scope, prefix, NULL, 0, var_name, sizeof(var_name), err);
+               if (var_name_len == FLT_OTEL_RET_ERROR)
+                       OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
+
+               flt_otel_smp_init(s, &smp_ctx, opt, 0, NULL);
+
+               if (vars_unset_by_name_ifexist(var_name, var_name_len, &smp_ctx) == 0) {
+                       OTELC_DBG(NOTICE, "variable '%s' not found", var_name);
+               } else {
+                       OTELC_DBG(NOTICE, "variable '%s' unset", var_name);
+
+                       retval = 1;
+               }
+       }
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_vars_get_cb - context variable value reader callback
+ *
+ * SYNOPSIS
+ *   static int flt_otel_vars_get_cb(struct sample *smp, size_t idx, const char *scope, const char *prefix, const char *name, FLT_OTEL_VAR_CTX_SIZE name_len, char **err, void *ptr)
+ *
+ * ARGUMENTS
+ *   smp      - current sample with stream context
+ *   idx      - buffer offset (unused)
+ *   scope    - variable scope
+ *   prefix   - variable prefix
+ *   name     - normalized context variable name
+ *   name_len - length of the name (unused)
+ *   err      - indirect pointer to error message string
+ *   ptr      - pointer to the output text map pointer
+ *
+ * DESCRIPTION
+ *   Callback for flt_otel_ctx_loop() that reads a single context variable value
+ *   and adds it to a text map.  Constructs the full variable name, reads its
+ *   value via vars_get_by_name(), reverses the <name> normalization (restoring
+ *   dashes and spaces), and stores the key-value pair in the text map.  The
+ *   text map is lazily allocated on first use.
+ *
+ * RETURN VALUE
+ *   Returns 0 on success, or FLT_OTEL_RET_ERROR on failure.
+ */
+static int flt_otel_vars_get_cb(struct sample *smp, size_t idx, const char *scope, const char *prefix, const char *name, FLT_OTEL_VAR_CTX_SIZE name_len, char **err, void *ptr)
+{
+       struct otelc_text_map **map = ptr;
+       struct sample           smp_ctx;
+       char                    var_ctx[BUFSIZ], otel_var_name[BUFSIZ];
+       int                     var_ctx_len, retval = FLT_OTEL_RET_ERROR;
+
+       OTELC_FUNC("%p, %zu, \"%s\", \"%s\", \"%s\", %hhd, %p:%p, %p", smp, idx, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), name_len, OTELC_DPTR_ARGS(err), ptr);
+
+       /* Build the HAProxy variable name for this context key. */
+       var_ctx_len = flt_otel_var_name(scope, prefix, name, 1, var_ctx, sizeof(var_ctx), err);
+       if (var_ctx_len == FLT_OTEL_RET_ERROR) {
+               FLT_OTEL_ERR("ctx '%s' invalid", name);
+
+               OTELC_RETURN_INT(retval);
+       }
+
+       flt_otel_smp_init(smp->strm, &smp_ctx, smp->opt, 0, NULL);
+
+       /* Retrieve the context variable and build a text map entry. */
+       if (vars_get_by_name(var_ctx, var_ctx_len, &smp_ctx, NULL) != 0) {
+               OTELC_DBG(NOTICE, "'%s' -> '%.*s'", var_ctx, (int)b_data(&(smp_ctx.data.u.str)), b_orig(&(smp_ctx.data.u.str)));
+
+               if (*map == NULL) {
+                       *map = OTELC_TEXT_MAP_NEW(NULL, 8);
+                       if (*map == NULL) {
+                               FLT_OTEL_ERR("failed to create map data");
+
+                               OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
+                       }
+               }
+
+               /*
+                * Eh, because the use of some characters is not allowed in the
+                * variable name, the conversion of the replaced characters to
+                * the original is performed here.
+                */
+               retval = flt_otel_denormalize_name(name, otel_var_name, OTELC_TABLESIZE_1(otel_var_name), err);
+               if (retval >= 0)
+                       retval = OTELC_TEXT_MAP_ADD(*map, otel_var_name, retval, b_orig(&(smp_ctx.data.u.str)), b_data(&(smp_ctx.data.u.str)), OTELC_TEXT_MAP_AUTO);
+               if (retval == FLT_OTEL_RET_ERROR) {
+                       FLT_OTEL_ERR("failed to add map data");
+
+                       otelc_text_map_destroy(map);
+               } else {
+                       retval = 0;
+               }
+       } else {
+               OTELC_DBG(NOTICE, "ctx '%s' no variable found", var_ctx);
+       }
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_vars_get - context variables to text map extraction
+ *
+ * SYNOPSIS
+ *   struct otelc_text_map *flt_otel_vars_get(struct stream *s, const char *scope, const char *prefix, uint opt, char **err)
+ *
+ * ARGUMENTS
+ *   s      - current stream
+ *   scope  - variable scope
+ *   prefix - variable prefix
+ *   opt    - sample option flags
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Reads all context variables for a given <prefix> into a text map.  Iterates
+ *   the tracking buffer via flt_otel_ctx_loop() with flt_otel_vars_get_cb().
+ *   If the resulting text map is empty, it is destroyed and NULL is returned.
+ *   This function is used by the "extract" keyword with variable storage.
+ *
+ * RETURN VALUE
+ *   Returns a pointer to the populated text map, or NULL if no variables are
+ *   found.
+ */
+struct otelc_text_map *flt_otel_vars_get(struct stream *s, const char *scope, const char *prefix, uint opt, char **err)
+{
+       struct sample          smp_ctx;
+       struct otelc_text_map *retptr = NULL;
+
+       OTELC_FUNC("%p, \"%s\", \"%s\", %u, %p:%p", s, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), opt, OTELC_DPTR_ARGS(err));
+
+       flt_otel_smp_init(s, &smp_ctx, opt, 0, NULL);
+
+       (void)flt_otel_ctx_loop(&smp_ctx, scope, prefix, err, flt_otel_vars_get_cb, &retptr);
+
+       OTELC_TEXT_MAP_DUMP(retptr, "extracted variables");
+
+       if ((retptr != NULL) && (retptr->count == 0)) {
+               OTELC_DBG(NOTICE, "WARNING: no variables found");
+
+               otelc_text_map_destroy(&retptr);
+       }
+
+       OTELC_RETURN_PTR(retptr);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_var_register - HAProxy variable registration
+ *
+ * SYNOPSIS
+ *   int flt_otel_var_register(const char *scope, const char *prefix, const char *name, char **err)
+ *
+ * ARGUMENTS
+ *   scope  - variable scope
+ *   prefix - variable prefix
+ *   name   - variable name
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Registers a HAProxy variable by constructing its full name from <scope>,
+ *   <prefix>, and <name>, then calling vars_check_arg() to make it available
+ *   at runtime.
+ *
+ * RETURN VALUE
+ *   Returns the variable name length on success, or FLT_OTEL_RET_ERROR on
+ *   failure.
+ */
+int flt_otel_var_register(const char *scope, const char *prefix, const char *name, char **err)
+{
+       struct arg arg;
+       char       var_name[BUFSIZ];
+       int        retval = FLT_OTEL_RET_ERROR, var_name_len;
+
+       OTELC_FUNC("\"%s\", \"%s\", \"%s\", %p:%p", OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), OTELC_DPTR_ARGS(err));
+
+       var_name_len = flt_otel_var_name(scope, prefix, name, 0, var_name, sizeof(var_name), err);
+       if (var_name_len == FLT_OTEL_RET_ERROR)
+               OTELC_RETURN_INT(retval);
+
+       /* Set <size> to 0 to not release var_name memory in vars_check_arg(). */
+       (void)memset(&arg, 0, sizeof(arg));
+       arg.type          = ARGT_STR;
+       arg.data.str.area = var_name;
+       arg.data.str.data = var_name_len;
+
+       if (vars_check_arg(&arg, err) == 0) {
+               FLT_OTEL_ERR_APPEND("failed to register variable '%s': %s", var_name, *err);
+       } else {
+               OTELC_DBG(NOTICE, "variable '%s' registered", var_name);
+
+               retval = var_name_len;
+       }
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_var_set - HAProxy variable value setter
+ *
+ * SYNOPSIS
+ *   int flt_otel_var_set(struct stream *s, const char *scope, const char *prefix, const char *name, const char *value, uint opt, char **err)
+ *
+ * ARGUMENTS
+ *   s      - current stream
+ *   scope  - variable scope
+ *   prefix - variable prefix
+ *   name   - variable name
+ *   value  - string value to set
+ *   opt    - sample option flags
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Sets a HAProxy variable to the given string <value>.  The full variable
+ *   name is constructed from <scope>, <prefix>, and <name>.  If the variable's
+ *   scope matches FLT_OTEL_VARS_SCOPE, the name is also registered in the
+ *   context tracking buffer via flt_otel_ctx_set().
+ *
+ * RETURN VALUE
+ *   Returns the variable name length on success, the context tracking result
+ *   for context-scope variables, or FLT_OTEL_RET_ERROR on failure.
+ */
+int flt_otel_var_set(struct stream *s, const char *scope, const char *prefix, const char *name, const char *value, uint opt, char **err)
+{
+       struct sample smp;
+       char          var_name[BUFSIZ];
+       int           retval = FLT_OTEL_RET_ERROR, var_name_len;
+
+       OTELC_FUNC("%p, \"%s\", \"%s\", \"%s\", \"%s\", %u, %p:%p", s, OTELC_STR_ARG(scope), OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), OTELC_STR_ARG(value), opt, OTELC_DPTR_ARGS(err));
+
+       var_name_len = flt_otel_var_name(scope, prefix, name, 0, var_name, sizeof(var_name), err);
+       if (var_name_len == FLT_OTEL_RET_ERROR)
+               OTELC_RETURN_INT(retval);
+
+       flt_otel_smp_init(s, &smp, opt, SMP_T_STR, value);
+
+       /* Set the variable if it already exists. */
+       if (vars_set_by_name_ifexist(var_name, var_name_len, &smp) == 0) {
+               FLT_OTEL_ERR("failed to set variable '%s'", var_name);
+       } else {
+               OTELC_DBG(NOTICE, "variable '%s' set", var_name);
+
+               retval = var_name_len;
+
+               if (strcmp(scope, FLT_OTEL_VARS_SCOPE) == 0)
+                       retval = flt_otel_ctx_set(s, scope, prefix, name, opt, err);
+       }
+
+       OTELC_RETURN_INT(retval);
+}
+
+/*
+ * Local variables:
+ *  c-indent-level: 8
+ *  c-basic-offset: 8
+ * End:
+ *
+ * vi: noexpandtab shiftwidth=8 tabstop=8
+ */