From 8effcac6f5beec73feb20ee55603a84dc5a7e5a5 Mon Sep 17 00:00:00 2001 From: Miroslav Zagorac Date: Sun, 12 Apr 2026 11:20:59 +0200 Subject: [PATCH] MEDIUM: otel: added HTTP header operations for context propagation Added the HTTP header manipulation layer that enables span context injection into and extraction from HAProxy's HTX message buffers, completing the end-to-end context propagation path. The new http.c module implements three public functions: flt_otel_http_headers_get() extracts HTTP headers matching a name prefix from the channel's HTX buffer into an otelc_text_map structure, stripping the prefix and separator dash from header names before storage; flt_otel_http_header_set() constructs a full header name from a prefix and suffix joined by a dash, removes all existing occurrences, and optionally adds the header with a new value; and flt_otel_http_headers_remove() removes all headers matching a given prefix. A debug-only flt_otel_http_headers_dump() logs all HTTP headers from a channel at NOTICE level. The scope runner in event.c now extracts propagation contexts from HTTP headers before processing spans: for each configured extract context, it calls flt_otel_http_headers_get() to read matching headers into a text map, then passes the text map to flt_otel_scope_context_init() which extracts the OTel span context from the carrier. After span execution, the span runner injects the span context back into HTTP headers via flt_otel_inject_http_headers() followed by flt_otel_http_header_set() for each propagation key. The unused resource cleanup in flt_otel_scope_free_unused() now also removes contexts that failed extraction by deleting their associated HTTP headers via flt_otel_http_headers_remove() before freeing the scope context structure. --- addons/otel/Makefile | 1 + addons/otel/include/http.h | 31 ++++ addons/otel/include/include.h | 1 + addons/otel/src/event.c | 59 ++++++- addons/otel/src/filter.c | 2 + addons/otel/src/http.c | 324 ++++++++++++++++++++++++++++++++++ addons/otel/src/scope.c | 16 ++ 7 files changed, 427 insertions(+), 7 deletions(-) create mode 100644 addons/otel/include/http.h create mode 100644 addons/otel/src/http.c diff --git a/addons/otel/Makefile b/addons/otel/Makefile index d1436f50c..8b5b06916 100644 --- a/addons/otel/Makefile +++ b/addons/otel/Makefile @@ -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 index 000000000..40fd4d190 --- /dev/null +++ b/addons/otel/include/http.h @@ -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 + */ diff --git a/addons/otel/include/include.h b/addons/otel/include/include.h index 6902a4fd2..14c24d0d7 100644 --- a/addons/otel/include/include.h +++ b/addons/otel/include/include.h @@ -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" diff --git a/addons/otel/src/event.c b/addons/otel/src/event.c index b6666541d..31bae3100 100644 --- a/addons/otel/src/event.c +++ b/addons/otel/src/event.c @@ -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); diff --git a/addons/otel/src/filter.c b/addons/otel/src/filter.c index d50fa00cc..b8eaaa58b 100644 --- a/addons/otel/src/filter.c +++ b/addons/otel/src/filter.c @@ -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 index 000000000..183560586 --- /dev/null +++ b/addons/otel/src/http.c @@ -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 from the channel's HTX buffer + * into a newly allocated text map. When 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 and with a dash + * separator; if only one is provided, it is used directly. All existing + * occurrences of the header are removed first. If is NULL, all + * headers starting with are removed. If is non-NULL, the + * header is then added with the new value. A NULL 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 parameter is not set, then remove all headers + * that start with the contents of the 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 from the channel's HTX + * buffer. This is a convenience wrapper around flt_otel_http_header_set() + * with NULL and 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 + */ diff --git a/addons/otel/src/scope.c b/addons/otel/src/scope.c index 63effc6a0..0b096b16e 100644 --- a/addons/otel/src/scope.c +++ b/addons/otel/src/scope.c @@ -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();