]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: otel: added span link support
authorMiroslav Zagorac <mzagorac@haproxy.com>
Thu, 26 Feb 2026 10:09:33 +0000 (11:09 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 07:23:26 +0000 (09:23 +0200)
Added span link support, allowing a span to reference other spans or
extracted contexts without establishing a parent relationship.

Introduced the flt_otel_conf_link structure and added a links list to
flt_otel_conf_span.  The parser accepted both an inline syntax on the span
declaration line ("span <name> link <target>") and a standalone multi-
argument form ("link <span> ..."), each creating a conf_link entry
appended to the span's link list.

At runtime, each configured link name was resolved against the active
spans and extracted contexts in the runtime context.  Resolved references
were collected into flt_otel_scope_data_link entries and passed to the C
wrapper add_link API during span creation.

Initialization, cleanup, and debug dump routines were added for the link
data structures at both configuration and runtime levels.

addons/otel/include/conf.h
addons/otel/include/conf_funcs.h
addons/otel/include/parser.h
addons/otel/include/scope.h
addons/otel/src/conf.c
addons/otel/src/event.c
addons/otel/src/parser.c
addons/otel/src/scope.c
addons/otel/test/sa/otel.cfg

index 99c37cad4f3a687b1487337d28e9893ee07f3f1f..08e82b8459e9ec93652906c08f3fae02d57debcc 100644 (file)
 #define FLT_OTEL_DBG_CONF_CONTEXT(h,p) \
        OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "0x%02hhx }", (p), FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->flags)
 
-#define FLT_OTEL_DBG_CONF_SPAN(h,p)                                                                        \
-       OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "'%s' %zu %s' %zu %hhu 0x%02hhx %s %s %s %s }", \
-                        (p), FLT_OTEL_CONF_HDR_ARGS(p, id), FLT_OTEL_STR_HDR_ARGS(p, ref_id),             \
-                        FLT_OTEL_STR_HDR_ARGS(p, ctx_id), (p)->flag_root, (p)->ctx_flags,                 \
-                        flt_otel_list_dump(&((p)->attributes)), flt_otel_list_dump(&((p)->events)),       \
-                        flt_otel_list_dump(&((p)->baggages)), flt_otel_list_dump(&((p)->statuses)))
+#define FLT_OTEL_DBG_CONF_SPAN(h,p)                                                                           \
+       OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "'%s' %zu %s' %zu %hhu 0x%02hhx %s %s %s %s %s }", \
+                        (p), FLT_OTEL_CONF_HDR_ARGS(p, id), FLT_OTEL_STR_HDR_ARGS(p, ref_id),                \
+                        FLT_OTEL_STR_HDR_ARGS(p, ctx_id), (p)->flag_root, (p)->ctx_flags,                    \
+                        flt_otel_list_dump(&((p)->links)), flt_otel_list_dump(&((p)->attributes)),           \
+                        flt_otel_list_dump(&((p)->events)), flt_otel_list_dump(&((p)->baggages)),            \
+                        flt_otel_list_dump(&((p)->statuses)))
 
 #define FLT_OTEL_DBG_CONF_SCOPE(h,p)                                                                        \
        OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%hhu %d %u %s %p %s %s %s }", (p),              \
@@ -138,6 +139,11 @@ struct flt_otel_conf_context {
        uint8_t flags;         /* The type of storage from which the span context is extracted.  */
 };
 
+/* flt_otel_conf_span->links */
+struct flt_otel_conf_link {
+       FLT_OTEL_CONF_HDR(span); /* The list containing link names. */
+};
+
 /*
  * Span configuration within a scope.
  *   flt_otel_conf_scope->spans
@@ -148,6 +154,7 @@ struct flt_otel_conf_span {
        FLT_OTEL_CONF_STR(ctx_id); /* The span context name, if used. */
        uint8_t     ctx_flags;     /* The type of storage used for the span context. */
        bool        flag_root;     /* Whether this is a root span. */
+       struct list links;         /* The set of linked span names. */
        struct list attributes;    /* The set of key:value attributes. */
        struct list events;        /* The set of events with key-value attributes. */
        struct list baggages;      /* The set of key:value baggage items. */
index af19aa9e1710c87a50932033c56b68256c18a677..4ca6da943b1e78ec1c5a332e6e8a9a66f9c7544c 100644 (file)
@@ -101,6 +101,7 @@ FLT_OTEL_CONF_FUNC_DECL(str)
 FLT_OTEL_CONF_FUNC_DECL(ph)
 FLT_OTEL_CONF_FUNC_DECL(sample_expr)
 FLT_OTEL_CONF_FUNC_DECL(sample)
+FLT_OTEL_CONF_FUNC_DECL(link)
 FLT_OTEL_CONF_FUNC_DECL(context)
 FLT_OTEL_CONF_FUNC_DECL(span)
 FLT_OTEL_CONF_FUNC_DECL(scope)
index 9eb45e6123e8b1f6ac0da12a901962abff832e8d..282ffd8f6c973dc67895c3e0d9f1871c14f38be8 100644 (file)
@@ -19,6 +19,7 @@
 
 #define FLT_OTEL_PARSE_SPAN_ROOT              "root"
 #define FLT_OTEL_PARSE_SPAN_PARENT            "parent"
+#define FLT_OTEL_PARSE_SPAN_LINK              "link"
 #define FLT_OTEL_PARSE_CTX_AUTONAME           "-"
 #define FLT_OTEL_PARSE_CTX_IGNORE_NAME        '-'
 #define FLT_OTEL_PARSE_CTX_USE_HEADERS        "use-headers"
@@ -73,7 +74,8 @@
  */
 #define FLT_OTEL_PARSE_SCOPE_DEFINES                                                                                                \
        FLT_OTEL_PARSE_SCOPE_DEF(          ID, 0, CHAR, 2, 2, "otel-scope",   " <name>")                                            \
-       FLT_OTEL_PARSE_SCOPE_DEF(        SPAN, 0, NONE, 2, 7, "span",         " <name> [<reference>] [root]")                       \
+       FLT_OTEL_PARSE_SCOPE_DEF(        SPAN, 0, NONE, 2, 7, "span",         " <name> [<reference>] [<link>] [root]")              \
+       FLT_OTEL_PARSE_SCOPE_DEF(        LINK, 1, NONE, 2, 0,   "link",       " <span> ...")                                        \
        FLT_OTEL_PARSE_SCOPE_DEF(   ATTRIBUTE, 1, NONE, 3, 0,   "attribute",  " <key> <sample> ...")                                \
        FLT_OTEL_PARSE_SCOPE_DEF(       EVENT, 1, NONE, 4, 0,   "event",      " <name> <key> <sample> ...")                         \
        FLT_OTEL_PARSE_SCOPE_DEF(     BAGGAGE, 1,  VAR, 3, 0,   "baggage",    " <key> <sample> ...")                                \
index 7570e6222699b8eb8e2804b44bb1774bbfb7ef9e..669a8ad67fa12a38bcbe3910c50c93df968f8c64 100644 (file)
 
 #define FLT_OTEL_DBG_SCOPE_DATA_KV_FMT       "%p:{ %p %zu %zu }"
 #define FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS(p)   &(p), (p).attr, (p).cnt, (p).size
-#define FLT_OTEL_DBG_SCOPE_DATA(h,p)                                                                               \
-       OTELC_DBG(DEBUG, h "%p:{ " FLT_OTEL_DBG_SCOPE_DATA_KV_FMT " " FLT_OTEL_DBG_SCOPE_DATA_KV_FMT " %s }", (p), \
-                 FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS((p)->baggage), FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS((p)->attributes), \
-                 flt_otel_list_dump(&((p)->events)))
+#define FLT_OTEL_DBG_SCOPE_DATA(h,p)                                                                                   \
+       OTELC_DBG(DEBUG, h "%p:{ " FLT_OTEL_DBG_SCOPE_DATA_KV_FMT " " FLT_OTEL_DBG_SCOPE_DATA_KV_FMT " %s %s }", (p), \
+                 FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS((p)->baggage), FLT_OTEL_DBG_SCOPE_DATA_KV_ARGS((p)->attributes),    \
+                 flt_otel_list_dump(&((p)->events)), flt_otel_list_dump(&((p)->links)))
 
 #define FLT_OTEL_DBG_RUNTIME_CONTEXT(h,p)                                                     \
        OTELC_DBG(DEBUG, h "%p:{ %p %p '%s' %hhu %hhu 0x%02hhx 0x%08x %u %d %s %s }", (p),    \
@@ -64,15 +64,25 @@ struct flt_otel_scope_data_event {
        struct list      list; /* Used to chain this structure. */
 };
 
+/* Span link referencing another span or span context. */
+struct flt_otel_scope_data_link {
+       struct otelc_span         *span;    /* Linked span, or NULL. */
+       struct otelc_span_context *context; /* Linked span context, or NULL. */
+       struct list                list;    /* Used to chain this structure. */
+};
+
+/* Span status code and description. */
 struct flt_otel_scope_data_status {
        int   code;        /* OTELC_SPAN_STATUS_* value. */
        char *description; /* Span status description string. */
 };
 
+/* Aggregated runtime data collected during scope execution. */
 struct flt_otel_scope_data {
        struct flt_otel_scope_data_kv     baggage;    /* Defined scope baggage. */
        struct flt_otel_scope_data_kv     attributes; /* Defined scope attributes. */
        struct list                       events;     /* Defined scope events. */
+       struct list                       links;      /* Defined scope links. */
        struct flt_otel_scope_data_status status;     /* Defined scope status. */
 };
 
index f2f46b1d3f088eebeb4de8b144f514d0f4ec2248..0914a962d232684f2879d4c754148a014455fc7e 100644 (file)
@@ -95,6 +95,53 @@ FLT_OTEL_CONF_FUNC_FREE(str, str,
 )
 
 
+/***
+ * NAME
+ *   flt_otel_conf_link_init - conf_link structure allocation
+ *
+ * SYNOPSIS
+ *   struct flt_otel_conf_link *flt_otel_conf_link_init(const char *id, int line, struct list *head, char **err)
+ *
+ * ARGUMENTS
+ *   id   - identifier string to duplicate
+ *   line - configuration file line number
+ *   head - list to append to (or NULL)
+ *   err  - indirect pointer to error message string
+ *
+ * DESCRIPTION
+ *   Allocates and initializes a conf_link structure for a span link
+ *   reference.  The <id> string is duplicated and stored as the linked
+ *   span name.  If <head> is non-NULL, the structure is appended to
+ *   the list.
+ *
+ * RETURN VALUE
+ *   Returns a pointer to the initialized structure, or NULL on failure.
+ */
+FLT_OTEL_CONF_FUNC_INIT(link, span, )
+
+
+/***
+ * NAME
+ *   flt_otel_conf_link_free - conf_link structure deallocation
+ *
+ * SYNOPSIS
+ *   void flt_otel_conf_link_free(struct flt_otel_conf_link **ptr)
+ *
+ * ARGUMENTS
+ *   ptr - a pointer to the address of a structure
+ *
+ * DESCRIPTION
+ *   Deallocates memory used by the flt_otel_conf_link structure and its
+ *   contents, then removes it from the list of structures of that type.
+ *
+ * RETURN VALUE
+ *   This function does not return a value.
+ */
+FLT_OTEL_CONF_FUNC_FREE(link, span,
+       FLT_OTEL_DBG_CONF_HDR("- conf_link free ", *ptr, span);
+)
+
+
 /***
  * NAME
  *   flt_otel_conf_ph_init - conf_ph placeholder structure allocation
@@ -401,6 +448,7 @@ FLT_OTEL_CONF_FUNC_FREE(context, id,
  *   Returns a pointer to the initialized structure, or NULL on failure.
  */
 FLT_OTEL_CONF_FUNC_INIT(span, id,
+       LIST_INIT(&(retptr->links));
        LIST_INIT(&(retptr->attributes));
        LIST_INIT(&(retptr->events));
        LIST_INIT(&(retptr->baggages));
@@ -430,6 +478,7 @@ FLT_OTEL_CONF_FUNC_FREE(span, id,
 
        OTELC_SFREE((*ptr)->ref_id);
        OTELC_SFREE((*ptr)->ctx_id);
+       FLT_OTEL_LIST_DESTROY(link, &((*ptr)->links));
        FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->attributes));
        FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->events));
        FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->baggages));
index 38a064458f7bb0c5a2898caf85f1d14617ba9354..e2c6ddffa201ac82d25a294ff4b57ee9d212f189 100644 (file)
@@ -54,6 +54,18 @@ static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct ch
                        OTELC_RETURN_INT(FLT_OTEL_RET_ERROR);
        }
 
+       /* Add all resolved span links to the current span. */
+       if (!LIST_ISEMPTY(&(data->links))) {
+               struct flt_otel_scope_data_link *link;
+
+               list_for_each_entry(link, &(data->links), list) {
+                       OTELC_DBG(DEBUG, "adding link %p %p", link->span, link->context);
+
+                       if (OTELC_OPS(span->span, add_link, link->span, link->context, NULL, 0) == -1)
+                               retval = FLT_OTEL_RET_ERROR;
+               }
+       }
+
        /* Set baggage key-value pairs on the span. */
        if (data->baggage.attr != NULL)
                if (OTELC_OPS(span->span, set_baggage_kv_n, data->baggage.attr, data->baggage.cnt) == -1)
@@ -245,6 +257,61 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn,
                if (span == NULL)
                        retval = FLT_OTEL_RET_ERROR;
 
+               /*
+                * Resolve configured span links against the runtime context.
+                * Each link name is looked up first in the active spans, then
+                * in the extracted contexts.
+                */
+               if (!LIST_ISEMPTY(&(conf_span->links))) {
+                       struct flt_otel_runtime_context *rt_ctx = FLT_OTEL_RT_CTX(f->ctx);
+                       struct flt_otel_conf_link       *conf_link;
+
+                       list_for_each_entry(conf_link, &(conf_span->links), list) {
+                               struct flt_otel_scope_data_link *data_link;
+                               struct otelc_span               *link_span = NULL;
+                               struct otelc_span_context       *link_ctx = NULL;
+                               struct flt_otel_scope_span      *sc_span;
+                               struct flt_otel_scope_context   *sc_ctx;
+
+                               /* Try to find a matching span first. */
+                               list_for_each_entry(sc_span, &(rt_ctx->spans), list)
+                                       if (FLT_OTEL_CONF_STR_CMP(sc_span->id, conf_link->span)) {
+                                               link_span = sc_span->span;
+
+                                               break;
+                                       }
+
+                               /* If no span found, try to find a matching context. */
+                               if (link_span == NULL) {
+                                       list_for_each_entry(sc_ctx, &(rt_ctx->contexts), list)
+                                               if (FLT_OTEL_CONF_STR_CMP(sc_ctx->id, conf_link->span)) {
+                                                       link_ctx = sc_ctx->context;
+
+                                                       break;
+                                               }
+                               }
+
+                               if ((link_span == NULL) && (link_ctx == NULL)) {
+                                       OTELC_DBG(NOTICE, "WARNING: cannot find linked span/context '%s'", conf_link->span);
+
+                                       continue;
+                               }
+
+                               data_link = OTELC_CALLOC(1, sizeof(*data_link));
+                               if (data_link == NULL) {
+                                       retval = FLT_OTEL_RET_ERROR;
+
+                                       break;
+                               }
+
+                               data_link->span    = link_span;
+                               data_link->context = link_ctx;
+                               LIST_APPEND(&(data.links), &(data_link->list));
+
+                               OTELC_DBG(DEBUG, "resolved link '%s' -> %p %p", conf_link->span, link_span, link_ctx);
+                       }
+               }
+
                list_for_each_entry(sample, &(conf_span->attributes), list) {
                        OTELC_DBG(DEBUG, "adding attribute '%s' -> '%s'", sample->key, sample->fmt_string);
 
index 18627b62b2863d614607e708fee859651eb0882f..bcb3b71eb34f1612f2239fe7e79bc5ab9b41ca62 100644 (file)
@@ -955,6 +955,14 @@ static int flt_otel_parse_cfg_scope(const char *file, int line, char **args, int
                                        else
                                                FLT_OTEL_PARSE_ERR(&err, "'%s' : too few arguments (use '%s%s')", args[i], pdata->name, pdata->usage);
                                }
+                               else if (FLT_OTEL_PARSE_KEYWORD(i, FLT_OTEL_PARSE_SPAN_LINK)) {
+                                       if (FLT_OTEL_ARG_ISVALID(i + 1)) {
+                                               if (flt_otel_conf_link_init(args[++i], line, &(flt_otel_current_span->links), &err) == NULL)
+                                                       retval |= ERR_ABORT | ERR_ALERT;
+                                       } else {
+                                               FLT_OTEL_PARSE_ERR(&err, "'%s' : too few arguments (use '%s%s')", args[i], pdata->name, pdata->usage);
+                                       }
+                               }
                                else {
                                        FLT_OTEL_PARSE_ERR(&err, "'%s' : invalid argument (use '%s%s')", args[i], pdata->name, pdata->usage);
                                }
@@ -968,6 +976,11 @@ static int flt_otel_parse_cfg_scope(const char *file, int line, char **args, int
                        OTELC_DBG(DEBUG, "new span '%s' without reference", flt_otel_current_span->id);
                }
        }
+       else if (pdata->keyword == FLT_OTEL_PARSE_SCOPE_LINK) {
+               for (i = 1; !(retval & ERR_CODE) && FLT_OTEL_ARG_ISVALID(i); i++)
+                       if (flt_otel_conf_link_init(args[i], line, &(flt_otel_current_span->links), &err) == NULL)
+                               retval |= ERR_ABORT | ERR_ALERT;
+       }
        else if (pdata->keyword == FLT_OTEL_PARSE_SCOPE_ATTRIBUTE) {
                retval = flt_otel_parse_cfg_sample(file, line, args, 2, 0, NULL, &(flt_otel_current_span->attributes), &err);
        }
index cdef4c19fa04c39c686364218c6afec19ea3dd24..eca2741ad53ff0fac1b794a29e15d4a89de66187 100644 (file)
@@ -418,6 +418,17 @@ void flt_otel_scope_data_dump(const struct flt_otel_scope_data *data)
                OTELC_DBG(WORKER, "}");
        }
 
+       if (LIST_ISEMPTY(&(data->links))) {
+               OTELC_DBG(WORKER, "links %p:{ }", &(data->links));
+       } else {
+               struct flt_otel_scope_data_link *link;
+
+               OTELC_DBG(WORKER, "links %p:{", &(data->links));
+               list_for_each_entry(link, &(data->links), list)
+                       OTELC_DBG(WORKER, "  %p %p", link->span, link->context);
+               OTELC_DBG(WORKER, "}");
+       }
+
        if ((data->status.code == 0) && (data->status.description == NULL))
                OTELC_DBG(WORKER, "status %p:{ }", &(data->status));
        else
@@ -453,6 +464,7 @@ void flt_otel_scope_data_init(struct flt_otel_scope_data *ptr)
 
        (void)memset(ptr, 0, sizeof(*ptr));
        LIST_INIT(&(ptr->events));
+       LIST_INIT(&(ptr->links));
 
        OTELC_RETURN();
 }
@@ -479,6 +491,7 @@ void flt_otel_scope_data_init(struct flt_otel_scope_data *ptr)
 void flt_otel_scope_data_free(struct flt_otel_scope_data *ptr)
 {
        struct flt_otel_scope_data_event *event, *event_back;
+       struct flt_otel_scope_data_link  *link, *link_back;
 
        OTELC_FUNC("%p", ptr);
 
@@ -495,6 +508,9 @@ void flt_otel_scope_data_free(struct flt_otel_scope_data *ptr)
                OTELC_SFREE(event->name);
                OTELC_SFREE(event);
        }
+       /* Free all resolved link entries. */
+       list_for_each_entry_safe(link, link_back, &(ptr->links), list)
+               OTELC_SFREE(link);
        OTELC_SFREE(ptr->status.description);
 
        (void)memset(ptr, 0, sizeof(*ptr));
index 147222b5faefc8a76f53bd5ec946bc887eb1e978..69f2f32ef865e1866f45ab62d0a81a3bfbd267d0 100644 (file)
@@ -95,7 +95,7 @@
         otel-event on-http-body-request
 
     otel-scope frontend_http_request
-        span "Frontend HTTP request" parent "HTTP body request"
+        span "Frontend HTTP request" parent "HTTP body request" link "HAProxy session"
             attribute "http.method" method
             attribute "http.url" url
             attribute "http.version" str("HTTP/") req.ver
 
     otel-scope server_session_start
         span "Server session" parent "HAProxy session"
+        link "HAProxy session" "Client session"
         finish "Process sticking rules request"
         otel-event on-server-session-start