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.
This commit is contained in:
Miroslav Zagorac 2026-04-12 11:20:59 +02:00 committed by William Lallemand
parent ea9d05de02
commit 8effcac6f5
7 changed files with 427 additions and 7 deletions

View 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 \

View File

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

View File

@ -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"

View 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);

View 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);
}

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

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

View 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();