]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MEDIUM: otel: added post-parse configuration check
authorMiroslav Zagorac <mzagorac@haproxy.com>
Sun, 12 Apr 2026 07:58:34 +0000 (09:58 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 07:23:26 +0000 (09:23 +0200)
Implemented the flt_otel_ops_check() callback that validates the parsed
OTel filter configuration after all HAProxy configuration sections have
been processed.

The check callback performs the following validations: resolves deferred
sample fetch arguments under full frontend and backend capabilities,
verifies uniqueness of filter IDs across all proxies, ensures the
instrumentation section and its configuration file are present, checks
for duplicate group and scope section names, verifies that groups are not
empty, resolves group-to-scope and instrumentation-to-group/scope
cross-references by linking placeholder entries to their definitions,
detects unused scopes, counts root spans and warns when the count differs
from one, and accumulates the required channel analyzer bits from all used
scopes into the instrumentation configuration.

The commit also added the flt_otel_counters structure to track per-event
diagnostic counters in debug builds, the FLT_OTEL_ALERT macro for
filter-scoped error messages, and the FLT_OTEL_DBG_LIST macro for
iterating and dumping named configuration lists.

addons/otel/include/conf.h
addons/otel/include/define.h
addons/otel/include/filter.h
addons/otel/src/filter.c

index e0ce4c081d7961e54250174af8157b366f3db2a1..6986e0da35180e918d499b73eb8abf1fd4260945 100644 (file)
@@ -197,6 +197,16 @@ struct flt_otel_conf_instr {
        struct list          ph_scopes;     /* List of all used scopes. */
 };
 
+/* Runtime counters for filter diagnostics. */
+struct flt_otel_counters {
+#ifdef DEBUG_OTEL
+       struct {
+               bool     flag_used; /* Whether this event is used. */
+               uint64_t htx[2];    /* htx_is_empty() function result counter. */
+       } event[FLT_OTEL_EVENT_MAX];
+#endif
+};
+
 /* The OpenTelemetry filter configuration. */
 struct flt_otel_conf {
        struct proxy               *proxy;    /* Proxy owning the filter. */
@@ -205,6 +215,7 @@ struct flt_otel_conf {
        struct flt_otel_conf_instr *instr;    /* The OpenTelemetry instrumentation settings. */
        struct list                 groups;   /* List of all available groups. */
        struct list                 scopes;   /* List of all available scopes. */
+       struct flt_otel_counters    cnt;      /* Various counters related to filter operation. */
        struct list                 smp_args; /* Deferred OTEL sample fetch args to resolve. */
 };
 
index 9e71df77e1516be12c5b9dcf628daf61b9e17316..4ca3c28ae6002d89bd75dc49adf901dd9dc9d026 100644 (file)
@@ -3,6 +3,9 @@
 #ifndef _OTEL_DEFINE_H_
 #define _OTEL_DEFINE_H_
 
+/* Safe pointer dereference with default value. */
+#define FLT_OTEL_DEREF(p,m,v)        (((p) != NULL) ? (p)->m : (v))
+
 /* Check whether argument at index n is in range, non-NULL and non-empty. */
 #define FLT_OTEL_ARG_ISVALID(n)      ({ typeof(n) _n = (n); OTELC_IN_RANGE(_n, 0, MAX_LINE_ARGS - 1) && (args[_n] != NULL) && (*args[_n] != '\0'); })
 
index 713eea4d08c41fffcc87834e7635e0c0b03c621e..cb495f90d1af98595d36e210c7fa92144084b1f3 100644 (file)
@@ -5,6 +5,7 @@
 
 #define FLT_OTEL_FMT_NAME           "'" FLT_OTEL_OPT_NAME "' : "
 #define FLT_OTEL_FMT_TYPE           "'filter' : "
+#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"
 #define FLT_OTEL_CONDITION_UNLESS   "unless"
@@ -17,6 +18,21 @@ enum FLT_OTEL_RET_enum {
        FLT_OTEL_RET_OK     = 1,
 };
 
+/* Dump or iterate a named configuration list for debugging. */
+#define FLT_OTEL_DBG_LIST(d,m,p,t,v,f)                               \
+       do {                                                         \
+               if (LIST_ISEMPTY(&((d)->m##s))) {                    \
+                       OTELC_DBG(DEBUG, p "- no " #m "s " t);       \
+               } else {                                             \
+                       const struct flt_otel_conf_##m *v;           \
+                                                                    \
+                       OTELC_DBG(DEBUG, p "- " t " " #m "s: %s",    \
+                                 flt_otel_list_dump(&((d)->m##s))); \
+                       list_for_each_entry(v, &((d)->m##s), list)   \
+                               do { f; } while (0);                 \
+               }                                                    \
+       } while (0)
+
 
 extern const char     *otel_flt_id;
 extern struct flt_ops  flt_otel_ops;
index b012f27c9e84383ef7172bcf328acafea449ec3d..1d00996213dc52e859958a351972308dc6a526e1 100644 (file)
@@ -90,9 +90,265 @@ static void flt_otel_ops_deinit(struct proxy *p, struct flt_conf *fconf)
  */
 static int flt_otel_ops_check(struct proxy *p, struct flt_conf *fconf)
 {
+       struct proxy               *px;
+       struct flt_otel_conf       *conf = FLT_OTEL_DEREF(fconf, conf, NULL);
+       struct flt_otel_conf_group *conf_group;
+       struct flt_otel_conf_scope *conf_scope;
+       struct flt_otel_conf_ph    *ph_group, *ph_scope;
+       int                         retval = 0, scope_unused_cnt = 0, span_root_cnt = 0;
+
        OTELC_FUNC("%p, %p", p, fconf);
 
-       OTELC_RETURN_INT(0);
+       if (conf == NULL)
+               OTELC_RETURN_INT(++retval);
+
+       /*
+        * Resolve deferred OTEL sample fetch arguments.
+        *
+        * These were kept out of the proxy's arg list during parsing to avoid
+        * the global smp_resolve_args() call, which would reject backend-only
+        * fetches on a frontend proxy.  All backends and servers are now
+        * available, so resolve under full FE+BE capabilities.
+        */
+       if (!LIST_ISEMPTY(&(conf->smp_args))) {
+               char *err = NULL;
+               uint  saved_cap = p->cap;
+
+               LIST_SPLICE(&(p->conf.args.list), &(conf->smp_args));
+               LIST_INIT(&(conf->smp_args));
+               p->cap |= PR_CAP_LISTEN;
+
+               if (smp_resolve_args(p, &err) != 0) {
+                       FLT_OTEL_ALERT("%s", err);
+                       ha_free(&err);
+
+                       retval++;
+               }
+
+               p->cap = saved_cap;
+       }
+
+       /*
+        * If only the proxy specified with the <p> parameter is checked, then
+        * no duplicate filters can be found that are not defined in the same
+        * configuration sections.
+        */
+       for (px = proxies_list; px != NULL; px = px->next) {
+               struct flt_conf *fconf_tmp;
+
+               OTELC_DBG(NOTICE, "proxy '%s'", px->id);
+
+               /*
+                * The names of all OTEL filters (filter ID) should be checked,
+                * they must be unique.
+                */
+               list_for_each_entry(fconf_tmp, &(px->filter_configs), list)
+                       if ((fconf_tmp != fconf) && (fconf_tmp->id == otel_flt_id)) {
+                               struct flt_otel_conf *conf_tmp = fconf_tmp->conf;
+
+                               OTELC_DBG(NOTICE, "  OTEL filter '%s'", conf_tmp->id);
+
+                               if (strcmp(conf_tmp->id, conf->id) == 0) {
+                                       FLT_OTEL_ALERT("''%s' : duplicated filter ID'", conf_tmp->id);
+
+                                       retval++;
+                               }
+                       }
+       }
+
+       if (FLT_OTEL_DEREF(conf->instr, id, NULL) == NULL) {
+               FLT_OTEL_ALERT("''%s' : no instrumentation found'", conf->id);
+
+               retval++;
+       }
+
+       if ((conf->instr != NULL) && (conf->instr->config == NULL)) {
+               FLT_OTEL_ALERT("''%s' : no configuration file specified'", conf->instr->id);
+
+               retval++;
+       }
+
+       /*
+        * Checking that defined 'otel-group' section names are unique.
+        */
+       list_for_each_entry(conf_group, &(conf->groups), list) {
+               struct flt_otel_conf_group *conf_group_tmp;
+
+               list_for_each_entry(conf_group_tmp, &(conf->groups), list) {
+                       if ((conf_group_tmp != conf_group) && (strcmp(conf_group_tmp->id, conf_group->id) == 0)) {
+                               FLT_OTEL_ALERT("''%s' : duplicated " FLT_OTEL_PARSE_SECTION_GROUP_ID " '%s''", conf->id, conf_group->id);
+
+                               retval++;
+
+                               break;
+                       }
+               }
+       }
+
+       /*
+        * Checking that defined 'otel-scope' section names are unique.
+        */
+       list_for_each_entry(conf_scope, &(conf->scopes), list) {
+               struct flt_otel_conf_scope *conf_scope_tmp;
+
+               list_for_each_entry(conf_scope_tmp, &(conf->scopes), list) {
+                       if ((conf_scope_tmp != conf_scope) && (strcmp(conf_scope_tmp->id, conf_scope->id) == 0)) {
+                               FLT_OTEL_ALERT("''%s' : duplicated " FLT_OTEL_PARSE_SECTION_SCOPE_ID " '%s''", conf->id, conf_scope->id);
+
+                               retval++;
+
+                               break;
+                       }
+               }
+       }
+
+       /*
+        * Checking that defined 'otel-group' sections are not empty.
+        */
+       list_for_each_entry(conf_group, &(conf->groups), list)
+               if (LIST_ISEMPTY(&(conf_group->ph_scopes)))
+                       FLT_OTEL_ALERT("''%s' : " FLT_OTEL_PARSE_SECTION_GROUP_ID " '%s' has no scopes'", conf->id, conf_group->id);
+
+       /*
+        * Checking that all defined 'otel-group' sections have correctly declared
+        * 'otel-scope' sections (ie whether the declared 'otel-scope' sections have
+        * corresponding definitions).
+        */
+       list_for_each_entry(conf_group, &(conf->groups), list)
+               list_for_each_entry(ph_scope, &(conf_group->ph_scopes), list) {
+                       bool flag_found = 0;
+
+                       list_for_each_entry(conf_scope, &(conf->scopes), list)
+                               if (strcmp(ph_scope->id, conf_scope->id) == 0) {
+                                       ph_scope->ptr         = conf_scope;
+                                       conf_scope->flag_used = 1;
+                                       flag_found            = 1;
+
+                                       break;
+                               }
+
+                       if (!flag_found) {
+                               FLT_OTEL_ALERT("'" FLT_OTEL_PARSE_SECTION_GROUP_ID " '%s' : references undefined " FLT_OTEL_PARSE_SECTION_SCOPE_ID " '%s''", conf_group->id, ph_scope->id);
+
+                               retval++;
+                       }
+               }
+
+       if (conf->instr != NULL) {
+               /*
+                * Checking that all declared 'groups' keywords have correctly
+                * defined 'otel-group' sections.
+                */
+               list_for_each_entry(ph_group, &(conf->instr->ph_groups), list) {
+                       bool flag_found = 0;
+
+                       list_for_each_entry(conf_group, &(conf->groups), list)
+                               if (strcmp(ph_group->id, conf_group->id) == 0) {
+                                       ph_group->ptr         = conf_group;
+                                       conf_group->flag_used = 1;
+                                       flag_found            = 1;
+
+                                       break;
+                               }
+
+                       if (!flag_found) {
+                               FLT_OTEL_ALERT("'" FLT_OTEL_PARSE_SECTION_INSTR_ID " '%s' : references undefined " FLT_OTEL_PARSE_SECTION_GROUP_ID " '%s''", conf->instr->id, ph_group->id);
+
+                               retval++;
+                       }
+               }
+
+               /*
+                * Checking that all declared 'scopes' keywords have correctly
+                * defined 'otel-scope' sections.
+                */
+               list_for_each_entry(ph_scope, &(conf->instr->ph_scopes), list) {
+                       bool flag_found = 0;
+
+                       list_for_each_entry(conf_scope, &(conf->scopes), list)
+                               if (strcmp(ph_scope->id, conf_scope->id) == 0) {
+                                       ph_scope->ptr         = conf_scope;
+                                       conf_scope->flag_used = 1;
+                                       flag_found            = 1;
+
+                                       break;
+                               }
+
+                       if (!flag_found) {
+                               FLT_OTEL_ALERT("'" FLT_OTEL_PARSE_SECTION_INSTR_ID " '%s' : references undefined " FLT_OTEL_PARSE_SECTION_SCOPE_ID " '%s''", conf->instr->id, ph_scope->id);
+
+                               retval++;
+                       }
+               }
+       }
+
+       OTELC_DBG(DEBUG, "--- filter '%s' configuration ----------", conf->id);
+       OTELC_DBG(DEBUG, "- defined spans ----------");
+
+       /*
+        * Walk every configured scope: for used ones, log the defined spans,
+        * count root spans, and set the required analyzer bits; for unused
+        * ones, record a warning so the operator is notified.
+        */
+       list_for_each_entry(conf_scope, &(conf->scopes), list) {
+               if (conf_scope->flag_used) {
+                       struct flt_otel_conf_span *conf_span;
+
+                       /*
+                        * In principle, only one span should be labeled
+                        * as a root span.
+                        */
+                       list_for_each_entry(conf_span, &(conf_scope->spans), list) {
+                               FLT_OTEL_DBG_CONF_SPAN("   ", conf_span);
+
+                               span_root_cnt += conf_span->flag_root ? 1 : 0;
+                       }
+
+#ifdef DEBUG_OTEL
+                       conf->cnt.event[conf_scope->event].flag_used = 1;
+#endif
+
+                       /* Set the flags of the analyzers used. */
+                       conf->instr->analyzers |= flt_otel_event_data[conf_scope->event].an_bit;
+
+                       /* Track the minimum idle timeout. */
+                       if (conf_scope->event == FLT_OTEL_EVENT__IDLE_TIMEOUT)
+                               if ((conf->instr->idle_timeout == 0) || (conf_scope->idle_timeout < conf->instr->idle_timeout))
+                                       conf->instr->idle_timeout = conf_scope->idle_timeout;
+               } else {
+                       FLT_OTEL_ALERT("''%s' : unused " FLT_OTEL_PARSE_SECTION_SCOPE_ID " '%s''", conf->id, conf_scope->id);
+
+                       scope_unused_cnt++;
+               }
+       }
+
+       /*
+        * Unused scopes or a number of root spans other than one do not
+        * necessarily have to be errors, but it is good to print it when
+        * starting HAProxy.
+        */
+       if (scope_unused_cnt > 0)
+               FLT_OTEL_ALERT("''%s' : %d scope(s) not in use'", conf->id, scope_unused_cnt);
+
+       if (LIST_ISEMPTY(&(conf->scopes)))
+               /* Do nothing. */;
+       else if (span_root_cnt == 0)
+               FLT_OTEL_ALERT("''%s' : no span is marked as the root span'", conf->id);
+       else if (span_root_cnt > 1)
+               FLT_OTEL_ALERT("''%s' : multiple spans are marked as the root span'", conf->id);
+
+       FLT_OTEL_DBG_LIST(conf, group, "", "defined", _group,
+                         FLT_OTEL_DBG_CONF_GROUP("   ", _group);
+                         FLT_OTEL_DBG_LIST(_group, ph_scope, "   ", "used", _scope, FLT_OTEL_DBG_CONF_PH("      ", _scope)));
+       FLT_OTEL_DBG_LIST(conf, scope, "", "defined", _scope, FLT_OTEL_DBG_CONF_SCOPE("   ", _scope));
+
+       if (conf->instr != NULL) {
+               OTELC_DBG(DEBUG, "   --- instrumentation '%s' configuration ----------", conf->instr->id);
+               FLT_OTEL_DBG_LIST(conf->instr, ph_group, "   ", "used", _group, FLT_OTEL_DBG_CONF_PH("      ", _group));
+               FLT_OTEL_DBG_LIST(conf->instr, ph_scope, "   ", "used", _scope, FLT_OTEL_DBG_CONF_PH("      ", _scope));
+       }
+
+       OTELC_RETURN_INT(retval);
 }