]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: otel: added HTTP header operations for context propagation
authorMiroslav Zagorac <mzagorac@haproxy.com>
Sun, 12 Apr 2026 09:20:59 +0000 (11:20 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 07:23:26 +0000 (09:23 +0200)
Added the HTTP header manipulation layer that enables span context
injection into and extraction from HAProxy's HTX message buffers,
completing the end-to-end context propagation path.

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

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

The unused resource cleanup in flt_otel_scope_free_unused() now also
removes contexts that failed extraction by deleting their associated
HTTP headers via flt_otel_http_headers_remove() before freeing the scope
context structure.

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

index d1436f50c8bb4f23d2925aa7be528f6766b36595..8b5b069160e1aaad68f6b19fb0691465f401f8e1 100644 (file)
@@ -54,6 +54,7 @@ OPTIONS_OBJS += \
        addons/otel/src/conf.o   \
        addons/otel/src/event.o  \
        addons/otel/src/filter.o \
+       addons/otel/src/http.o   \
        addons/otel/src/otelc.o  \
        addons/otel/src/parser.o \
        addons/otel/src/pool.o   \
diff --git a/addons/otel/include/http.h b/addons/otel/include/http.h
new file mode 100644 (file)
index 0000000..40fd4d1
--- /dev/null
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef _OTEL_HTTP_H_
+#define _OTEL_HTTP_H_
+
+#ifndef DEBUG_OTEL
+#  define flt_otel_http_headers_dump(...)   while (0)
+#else
+/* Dump all HTTP headers from a channel for debugging. */
+void                   flt_otel_http_headers_dump(const struct channel *chn);
+#endif
+
+/* Extract HTTP headers matching a prefix into a text map. */
+struct otelc_text_map *flt_otel_http_headers_get(struct channel *chn, const char *prefix, size_t len, char **err);
+
+/* Set or replace an HTTP header in a channel. */
+int                    flt_otel_http_header_set(struct channel *chn, const char *prefix, const char *name, const char *value, char **err);
+
+/* Remove all HTTP headers matching a prefix from a channel. */
+int                    flt_otel_http_headers_remove(struct channel *chn, const char *prefix, char **err);
+
+#endif /* _OTEL_HTTP_H_ */
+
+/*
+ * Local variables:
+ *  c-indent-level: 8
+ *  c-basic-offset: 8
+ * End:
+ *
+ * vi: noexpandtab shiftwidth=8 tabstop=8
+ */
index 6902a4fd2f9e9749d0c71ea6e31ad08bcb5844af..14c24d0d7068fc655302042c6c08ef655352538b 100644 (file)
@@ -32,6 +32,7 @@
 #include "conf.h"
 #include "conf_funcs.h"
 #include "filter.h"
+#include "http.h"
 #include "otelc.h"
 #include "parser.h"
 #include "pool.h"
index b6666541dc7a4080b347d9226672f8e90587ae44..31bae31005de2cb1b7a74f17606cd92cb0ec05d6 100644 (file)
@@ -77,6 +77,27 @@ 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. */
+       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_HEADERS) {
+                               for (text_map = &(writer.text_map); i < text_map->count; i++) {
+                                       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);
 }
 
@@ -110,13 +131,12 @@ static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct ch
  */
 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)
 {
-#ifdef FLT_OTEL_USE_COUNTERS
-       struct flt_otel_conf      *conf = FLT_OTEL_CONF(f);
-#endif
-       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;
+       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));
 
@@ -172,6 +192,29 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn,
                }
        }
 
+       /* 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 headers.
+                */
+               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);
+
+               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;
@@ -298,6 +341,8 @@ int flt_otel_event_run(struct stream *s, struct filter *f, struct channel *chn,
                        retval = FLT_OTEL_RET_ERROR;
        }
 
+       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);
index d50fa00ccba1c9d6e52221eb505892b626241f1e..b8eaaa58b69b8b5db8d18cbe203601a18cc13719 100644 (file)
@@ -895,6 +895,8 @@ 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);
 
+       flt_otel_http_headers_dump(&(s->req));
+
        OTELC_RETURN_INT(FLT_OTEL_RET_OK);
 }
 
diff --git a/addons/otel/src/http.c b/addons/otel/src/http.c
new file mode 100644 (file)
index 0000000..1835605
--- /dev/null
@@ -0,0 +1,324 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "../include/include.h"
+
+
+#ifdef DEBUG_OTEL
+
+/***
+ * NAME
+ *   flt_otel_http_headers_dump - debug HTTP headers dump
+ *
+ * SYNOPSIS
+ *   void flt_otel_http_headers_dump(const struct channel *chn)
+ *
+ * ARGUMENTS
+ *   chn - channel to dump HTTP headers from
+ *
+ * DESCRIPTION
+ *   Dumps all HTTP headers from the channel's HTX buffer.  Iterates over HTX
+ *   blocks, logging each header name-value pair at NOTICE level.  Processing
+ *   stops at the end-of-headers marker.
+ *
+ * RETURN VALUE
+ *   This function does not return a value.
+ */
+void flt_otel_http_headers_dump(const struct channel *chn)
+{
+       const struct htx *htx;
+       int32_t           pos;
+
+       OTELC_FUNC("%p", chn);
+
+       if (chn == NULL)
+               OTELC_RETURN();
+
+       htx = htxbuf(&(chn->buf));
+
+       if (htx_is_empty(htx))
+               OTELC_RETURN();
+
+       /* Walk HTX blocks and log each header until end-of-headers. */
+       for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) {
+               struct htx_blk    *blk  = htx_get_blk(htx, pos);
+               enum htx_blk_type  type = htx_get_blk_type(blk);
+
+               if (type == HTX_BLK_HDR) {
+                       struct ist n = htx_get_blk_name(htx, blk);
+                       struct ist v = htx_get_blk_value(htx, blk);
+
+                       OTELC_DBG(NOTICE, "'%.*s: %.*s'", (int)n.len, n.ptr, (int)v.len, v.ptr);
+               }
+               else if (type == HTX_BLK_EOH)
+                       break;
+       }
+
+       OTELC_RETURN();
+}
+
+#endif /* DEBUG_OTEL */
+
+
+/***
+ * NAME
+ *   flt_otel_http_headers_get - HTTP header extraction to text map
+ *
+ * SYNOPSIS
+ *   struct otelc_text_map *flt_otel_http_headers_get(struct channel *chn, const char *prefix, size_t len, char **err)
+ *
+ * ARGUMENTS
+ *   chn    - channel containing HTTP headers
+ *   prefix - header name prefix to match (or NULL for all)
+ *   len    - length of the prefix string
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Extracts HTTP headers matching a <prefix> from the channel's HTX buffer
+ *   into a newly allocated text map.  When <prefix> is NULL or its length is
+ *   zero, all headers are extracted.  If the prefix starts with
+ *   FLT_OTEL_PARSE_CTX_IGNORE_NAME, prefix matching is bypassed.  The prefix
+ *   (including the separator dash) is stripped from header names before storing
+ *   in the text map.  Empty header values are replaced with an empty string to
+ *   avoid misinterpretation by otelc_text_map_add().  This function is used by
+ *   the "extract" keyword to read span context from incoming request headers.
+ *
+ * RETURN VALUE
+ *   Returns a pointer to the populated text map, or NULL on failure or when
+ *   no matching headers are found.
+ */
+struct otelc_text_map *flt_otel_http_headers_get(struct channel *chn, const char *prefix, size_t len, char **err)
+{
+       const struct htx    *htx;
+       size_t               prefix_len = (!OTELC_STR_IS_VALID(prefix) || (len == 0)) ? 0 : (len + 1);
+       int32_t              pos;
+       struct otelc_text_map *retptr = NULL;
+
+       OTELC_FUNC("%p, \"%s\", %zu, %p:%p", chn, OTELC_STR_ARG(prefix), len, OTELC_DPTR_ARGS(err));
+
+       if (chn == NULL)
+               OTELC_RETURN_PTR(retptr);
+
+       /*
+        * The keyword 'inject' allows you to define the name of the OpenTelemetry
+        * context without using a prefix.  In that case all HTTP headers are
+        * transferred because it is not possible to separate them from the
+        * OpenTelemetry context (this separation is usually done via a prefix).
+        *
+        * When using the 'extract' keyword, the context name must be specified.
+        * To allow all HTTP headers to be extracted, the first character of
+        * that name must be set to FLT_OTEL_PARSE_CTX_IGNORE_NAME.
+        */
+       if (OTELC_STR_IS_VALID(prefix) && (*prefix == FLT_OTEL_PARSE_CTX_IGNORE_NAME))
+               prefix_len = 0;
+
+       htx = htxbuf(&(chn->buf));
+
+       for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) {
+               struct htx_blk    *blk  = htx_get_blk(htx, pos);
+               enum htx_blk_type  type = htx_get_blk_type(blk);
+
+               if (type == HTX_BLK_HDR) {
+                       struct ist v, n = htx_get_blk_name(htx, blk);
+
+                       if ((prefix_len == 0) || ((n.len >= prefix_len) && (strncasecmp(n.ptr, prefix, len) == 0))) {
+                               if (retptr == NULL) {
+                                       retptr = OTELC_TEXT_MAP_NEW(NULL, 8);
+                                       if (retptr == NULL) {
+                                               FLT_OTEL_ERR("failed to create HTTP header data");
+
+                                               break;
+                                       }
+                               }
+
+                               v = htx_get_blk_value(htx, blk);
+
+                               /*
+                                * In case the data of the HTTP header is not
+                                * specified, v.ptr will have some non-null
+                                * value and v.len will be equal to 0.  The
+                                * otelc_text_map_add() function will not
+                                * interpret this well.  In this case v.ptr
+                                * is set to an empty string.
+                                */
+                               if (v.len == 0)
+                                       v = ist("");
+
+                               /*
+                                * Here, an HTTP header (which is actually part
+                                * of the span context is added to the text_map.
+                                *
+                                * Before adding, the prefix is removed from the
+                                * HTTP header name.
+                                */
+                               if (OTELC_TEXT_MAP_ADD(retptr, n.ptr + prefix_len, n.len - prefix_len, v.ptr, v.len, OTELC_TEXT_MAP_AUTO) == -1) {
+                                       FLT_OTEL_ERR("failed to add HTTP header data");
+
+                                       otelc_text_map_destroy(&retptr);
+
+                                       break;
+                               }
+                       }
+               }
+               else if (type == HTX_BLK_EOH)
+                       break;
+       }
+
+       OTELC_TEXT_MAP_DUMP(retptr, "extracted HTTP headers");
+
+       if ((retptr != NULL) && (retptr->count == 0)) {
+               OTELC_DBG(NOTICE, "WARNING: no HTTP headers found");
+
+               otelc_text_map_destroy(&retptr);
+       }
+
+       OTELC_RETURN_PTR(retptr);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_http_header_set - HTTP header set or remove
+ *
+ * SYNOPSIS
+ *   int flt_otel_http_header_set(struct channel *chn, const char *prefix, const char *name, const char *value, char **err)
+ *
+ * ARGUMENTS
+ *   chn    - channel containing HTTP headers
+ *   prefix - header name prefix (or NULL)
+ *   name   - header name suffix (or NULL)
+ *   value  - header value to set (or NULL to remove only)
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Sets or removes an HTTP header in the channel's HTX buffer.  The full
+ *   header name is constructed by combining <prefix> and <name> with a dash
+ *   separator; if only one is provided, it is used directly.  All existing
+ *   occurrences of the header are removed first.  If <name> is NULL, all
+ *   headers starting with <prefix> are removed.  If <value> is non-NULL, the
+ *   header is then added with the new value.  A NULL <value> causes only the
+ *   removal, with no subsequent addition.
+ *
+ * RETURN VALUE
+ *   Returns 0 on success, or FLT_OTEL_RET_ERROR on failure.
+ */
+int flt_otel_http_header_set(struct channel *chn, const char *prefix, const char *name, const char *value, char **err)
+{
+       struct http_hdr_ctx  ctx = { .blk = NULL };
+       struct ist           ist_name;
+       struct buffer       *buffer = NULL;
+       struct htx          *htx;
+       int                  retval = FLT_OTEL_RET_ERROR;
+
+       OTELC_FUNC("%p, \"%s\", \"%s\", \"%s\", %p:%p", chn, OTELC_STR_ARG(prefix), OTELC_STR_ARG(name), OTELC_STR_ARG(value), OTELC_DPTR_ARGS(err));
+
+       if ((chn == NULL) || (!OTELC_STR_IS_VALID(prefix) && !OTELC_STR_IS_VALID(name)))
+               OTELC_RETURN_INT(retval);
+
+       htx = htxbuf(&(chn->buf));
+
+       /*
+        * Very rare (about 1% of cases), htx is empty.
+        * In order to avoid segmentation fault, we exit this function.
+        */
+       if (htx_is_empty(htx)) {
+               FLT_OTEL_ERR("HTX is empty");
+
+               OTELC_RETURN_INT(retval);
+       }
+
+       if (!OTELC_STR_IS_VALID(prefix)) {
+               ist_name = ist2((char *)name, strlen(name));
+       }
+       else if (!OTELC_STR_IS_VALID(name)) {
+               ist_name = ist2((char *)prefix, strlen(prefix));
+       }
+       else {
+               buffer = flt_otel_trash_alloc(0, err);
+               if (buffer == NULL)
+                       OTELC_RETURN_INT(retval);
+
+               (void)chunk_printf(buffer, "%s-%s", prefix, name);
+
+               ist_name = ist2(buffer->area, buffer->data);
+       }
+
+       /* Remove all occurrences of the header. */
+       while (http_find_header(htx, ist(""), &ctx, 1) == 1) {
+               struct ist n = htx_get_blk_name(htx, ctx.blk);
+#ifdef DEBUG_OTEL
+               struct ist v = htx_get_blk_value(htx, ctx.blk);
+#endif
+
+               /*
+                * If the <name> parameter is not set, then remove all headers
+                * that start with the contents of the <prefix> parameter.
+                */
+               if (!OTELC_STR_IS_VALID(name))
+                       n.len = ist_name.len;
+
+               if (isteqi(n, ist_name))
+                       if (http_remove_header(htx, &ctx) == 1)
+                               OTELC_DBG(DEBUG, "HTTP header '%.*s: %.*s' removed", (int)n.len, n.ptr, (int)v.len, v.ptr);
+       }
+
+       /*
+        * If the value pointer has a value of NULL, the HTTP header is not set
+        * after deletion.
+        */
+       if (value == NULL) {
+               retval = 0;
+       }
+       else if (http_add_header(htx, ist_name, ist(value)) == 1) {
+               retval = 0;
+
+               OTELC_DBG(DEBUG, "HTTP header '%s: %s' added", ist_name.ptr, value);
+       }
+       else {
+               FLT_OTEL_ERR("failed to set HTTP header '%s: %s'", ist_name.ptr, value);
+       }
+
+       flt_otel_trash_free(&buffer);
+
+       OTELC_RETURN_INT(retval);
+}
+
+
+/***
+ * NAME
+ *   flt_otel_http_headers_remove - HTTP headers removal by prefix
+ *
+ * SYNOPSIS
+ *   int flt_otel_http_headers_remove(struct channel *chn, const char *prefix, char **err)
+ *
+ * ARGUMENTS
+ *   chn    - channel containing HTTP headers
+ *   prefix - header name prefix to match for removal
+ *   err    - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Removes all HTTP headers matching the given <prefix> from the channel's HTX
+ *   buffer.  This is a convenience wrapper around flt_otel_http_header_set()
+ *   with NULL <name> and <value> arguments.
+ *
+ * RETURN VALUE
+ *   Returns 0 on success, or FLT_OTEL_RET_ERROR on failure.
+ */
+int flt_otel_http_headers_remove(struct channel *chn, const char *prefix, char **err)
+{
+       int retval;
+
+       OTELC_FUNC("%p, \"%s\", %p:%p", chn, OTELC_STR_ARG(prefix), OTELC_DPTR_ARGS(err));
+
+       retval = flt_otel_http_header_set(chn, prefix, NULL, NULL, err);
+
+       OTELC_RETURN_INT(retval);
+}
+
+/*
+ * Local variables:
+ *  c-indent-level: 8
+ *  c-basic-offset: 8
+ * End:
+ *
+ * vi: noexpandtab shiftwidth=8 tabstop=8
+ */
index 63effc6a036ce88651d03f0e34ef308efe835d70..0b096b16e7ddadcf183af15bdfc18ebd131a5fa3 100644 (file)
@@ -685,6 +685,22 @@ void flt_otel_scope_free_unused(struct flt_otel_runtime_context *rt_ctx, struct
                                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 associated with the context in
+                                * question should be deleted.
+                                */
+                               (void)flt_otel_http_headers_remove(chn, ctx->id, NULL);
+
+                               flt_otel_scope_context_free(&ctx);
+                       }
+       }
+
        FLT_OTEL_DBG_RUNTIME_CONTEXT("session context: ", rt_ctx);
 
        OTELC_RETURN();