]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: otel: changed log-record attr to use sample expressions
authorMiroslav Zagorac <mzagorac@haproxy.com>
Fri, 3 Apr 2026 10:33:26 +0000 (12:33 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 07:23:26 +0000 (09:23 +0200)
Replaced the static key-value attribute storage in log-record with
sample-evaluated attributes.  The 'attr' keyword now accepts a key and a
HAProxy sample expression evaluated at runtime, instead of a static string
value.

The struct (conf.h) changed from otelc_kv/attr_len to a list of
flt_otel_conf_sample entries.  The parser (parser.c) calls
flt_otel_parse_cfg_sample() with n=1 per attr keyword.  At runtime
(event.c) each attribute is evaluated via flt_otel_sample_eval() and added
via flt_otel_sample_add_kv() to a bare flt_otel_scope_data_kv, which is
passed to logger->log_span().

Updated documentation, debug macro and test configurations.

addons/otel/README
addons/otel/README-conf
addons/otel/README-configuration
addons/otel/README-implementation
addons/otel/include/conf.h
addons/otel/include/parser.h
addons/otel/src/conf.c
addons/otel/src/event.c
addons/otel/src/parser.c
addons/otel/test/full/otel.cfg
addons/otel/test/sa/otel.cfg

index 5f083b29d4dd6d381f6578ba0456e5bbc032b091..c3d8ac17a843e316fd7d2fc3994f613a9a036244 100644 (file)
@@ -669,15 +669,15 @@ instrument { update <name> [<attr>] | <type> <name> [<aggr>] [<desc>] [<unit>] <
     attr   - attribute key-value pairs (update form only)
 
 
-log-record <severity> [id <integer>] [event <name>] [span <span-name>] [attr <key> <value>] ... <sample> ...
+log-record <severity> [id <integer>] [event <name>] [span <span-name>] [attr <key> <sample>] ... <sample> ...
   This keyword emits an OpenTelemetry log record within the scope.  The first
   argument is a required severity level.  Optional keywords follow in any order
   before the trailing sample expressions that form the log record body:
 
-    id <integer>       - numeric event identifier
-    event <name>       - event name string
-    span <span-name>   - associate the log record with an open span
-    attr <key> <value> - add a key-value attribute (repeatable)
+    id <integer>        - numeric event identifier
+    event <name>        - event name string
+    span <span-name>    - associate the log record with an open span
+    attr <key> <sample> - add an attribute evaluated at runtime (repeatable)
 
   The remaining arguments at the end are sample fetch expressions.  A single
   sample preserves its native type; multiple samples are concatenated as a
@@ -694,8 +694,8 @@ log-record <severity> [id <integer>] [event <name>] [span <span-name>] [attr <ke
 
   For example:
     log-record info str("heartbeat")
-    log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" "GET" method url
-    log-record trace id 1000 event "session-start" span "Client session" attr "attr_1_key" "attr_1_value" src str(":") src_port
+    log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" method method url
+    log-record trace id 1000 event "session-start" span "Client session" attr "src_ip" src src str(":") src_port
     log-record warn event "server-unavailable" str("503 Service Unavailable")
 
   Arguments :
index 64992399dc41029ff41e00d8b85f08a6b0a0dc81..2920216ac677df1eaa4288625e932f768f2a163b 100644 (file)
@@ -86,6 +86,8 @@ The complete ownership tree, from root to leaves:
       |   +-- flt_otel_conf_sample           (samples list)
       |       +-- flt_otel_conf_sample_expr  (exprs list)
       +-- flt_otel_conf_log_record           (log_records list)
+          +-- flt_otel_conf_sample           (attributes list)
+          |   +-- flt_otel_conf_sample_expr  (exprs list)
           +-- flt_otel_conf_sample           (samples list)
               +-- flt_otel_conf_sample_expr  (exprs list)
 
@@ -217,14 +219,14 @@ an existing instrument via the ref pointer).
   event_id               Optional event identifier.
   event_name             Optional event name.
   span                   Optional span reference.
-  attr                   Log record attributes.
-  attr_len               Number of log record attributes.
+  attributes             Log record attributes (flt_otel_conf_sample list).
   samples                Sample expressions for the body.
 
 Log records are emitted via the OTel logger at the configured severity.  The
 optional span reference associates the log record with an open span at runtime.
-Attributes are stored as key-value pairs added via the 'attr' keyword, which
-can be repeated.
+Attributes are stored as flt_otel_conf_sample entries added via the 'attr'
+keyword, which can be repeated.  Attribute values are HAProxy sample expressions
+evaluated at runtime.
 
 4.8  flt_otel_conf_context
 
index 7f5b106b86e955c86699d419691d39aa7178ce9a..15a5e3f44ee0e56a53f6d24906960f97474c283d 100644 (file)
@@ -361,14 +361,19 @@ Supported keywords:
         instrument update "name_cnt_int" attr "attr_1_key" "attr_1_value"
 
 
-  log-record <severity> [id <integer>] [event <name>] [span <span-name>] [attr <key> <value>] ... <sample> ...
+  log-record <severity> [id <integer>] [event <name>] [span <span-name>] [attr <key> <sample>] ... <sample> ...
       Emit an OpenTelemetry log record.  The first argument is a required
       severity level.  Optional keywords follow in any order:
 
-        id <integer>       - numeric event identifier
-        event <name>       - event name string
-        span <span-name>   - associate the log record with an open span
-        attr <key> <value> - add a key-value attribute (repeatable)
+        id <integer>        - numeric event identifier
+        event <name>        - event name string
+        span <span-name>    - associate the log record with an open span
+        attr <key> <sample> - add an attribute evaluated at runtime (repeatable)
+
+      The 'attr' keyword takes an attribute name and a single HAProxy sample
+      expression.  The expression is evaluated at runtime, following the same
+      rules as span attributes: a bare sample fetch (e.g. src) or a log-format
+      string (e.g. "%[src]:%[src_port]").
 
       The remaining arguments at the end are sample fetch expressions that form
       the log record body.  A single sample preserves its native type; multiple
@@ -389,8 +394,8 @@ Supported keywords:
 
       Examples:
         log-record info str("heartbeat")
-        log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" "GET" method url
-        log-record trace id 1000 event "session-start" span "Client session" attr "attr_1_key" "attr_1_value" attr "attr_2_key" "attr_2_value" src str(":") src_port
+        log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" method method url
+        log-record trace id 1000 event "session-start" span "Client session" attr "src_ip" src attr "src_port" src_port src str(":") src_port
         log-record warn event "server-unavailable" str("503 Service Unavailable")
         log-record info event "session-stop" str("stream stopped")
 
index 3dc6934f7bfcf2840b211cecd2a121ee5ccd0ab6..4ce66db3f46ab825f35a5c20587337328243e605 100644 (file)
@@ -155,7 +155,7 @@ executes all referenced scopes when the rule fires.
         inject <name-prefix> [use-headers] [use-vars]
       finish <name> ...
       instrument <type> <name> ... / instrument update <name> ...
-      log-record <severity> [id <int>] [event <name>] [span <ref>] [attr <k> <v>] ... <sample> ...
+      log-record <severity> [id <int>] [event <name>] [span <ref>] [attr <key> <sample>] ... <sample> ...
       acl <name> <criterion> ...
 
 Each scope ties to a single HAProxy analyzer event (or none, if used only
@@ -1157,10 +1157,14 @@ The flt_otel_conf_log_record structure (conf.h) holds:
   event_id     Optional numeric event identifier (int64).
   event_name   Optional event name string.
   span         Optional span reference name (resolved at runtime).
-  attr         Key-value attribute array (from "attr" keywords).
-  attr_len     Number of attributes.
+  attributes   List of flt_otel_conf_sample entries for attributes.
   samples      List of sample expressions for the body.
 
+The attributes list contains flt_otel_conf_sample entries, one per "attr"
+keyword.  Each entry's key field holds the attribute name and its sample
+expressions are evaluated at runtime, following the same two-path model
+(bare sample or log-format) as span attributes.
+
 The samples list contains exactly one flt_otel_conf_sample entry, which
 in turn holds either a list of bare sample expressions or a single
 log-format expression (when the value contains "%[").
@@ -1178,7 +1182,12 @@ For each configured log record the function performs:
      skipped.  The threshold is controlled by the "min_severity" option
      in the YAML logs signal configuration.
 
-  2. Body evaluation: the single sample entry is evaluated using one of
+  2. Attribute evaluation: each entry in the attributes list is evaluated via
+     flt_otel_sample_add() into a temporary flt_otel_scope_data structure.
+     The evaluated key-value array is passed to logger->log_span() and freed
+     after emission.
+
+  3. Body evaluation: the single sample entry is evaluated using one of
      two paths:
 
      Log-format path (sample->lf_used is true):
@@ -1191,14 +1200,14 @@ For each configured log record the function performs:
        flt_otel_sample_to_str().  Results are concatenated into a
        single buffer.
 
-  3. Span resolution: if conf_log->span is non-NULL, the runtime
+  4. Span resolution: if conf_log->span is non-NULL, the runtime
      context's spans list is searched for a scope_span with a matching
      name.  If found, the OTel span pointer is captured for correlation.
      A missing span is non-fatal -- a NOTICE warning is logged and the
      record is emitted without span correlation.
 
-  4. Emission: logger->log_span() is called with the severity, event_id,
-     event_name, resolved span (or NULL), wall-clock timestamp,
+  5. Emission: logger->log_span() is called with the severity, event_id,
+     event_name, resolved span (or NULL), wall-clock timestamp, the evaluated
      attributes and the evaluated body string.
 
 18.6.3  Logger Lifecycle Summary
index 9549957f861c1046ab19c91e9617df22ce0d528f..9bd4c7a49be18629e66e38016f842d317904a71f 100644 (file)
@@ -91,9 +91,9 @@
                         (p)->bounds_num, (p)->bounds)
 
 #define FLT_OTEL_DBG_CONF_LOG_RECORD(h,p)                                                                             \
-       OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%d %" PRId64 " '%s' '%s' %p %zu %p }", (p),               \
+       OTELC_DBG_STRUCT(DEBUG, h, h FLT_OTEL_CONF_HDR_FMT "%d %" PRId64 " '%s' '%s' %s %s }", (p),                  \
                         FLT_OTEL_CONF_HDR_ARGS(p, id), (p)->severity, (p)->event_id, OTELC_STR_ARG((p)->event_name), \
-                        OTELC_STR_ARG((p)->span), (p)->attr, (p)->attr_len, flt_otel_list_dump(&((p)->samples)))
+                        OTELC_STR_ARG((p)->span), flt_otel_list_dump(&((p)->attributes)), flt_otel_list_dump(&((p)->samples)))
 
 #define FLT_OTEL_DBG_CONF(h,p)                                    \
        OTELC_DBG(DEBUG, h "%p:{ %p '%s' '%s' %p %s %s }", (p),   \
@@ -212,8 +212,7 @@ struct flt_otel_conf_log_record {
        int64_t               event_id;   /* Optional event identifier. */
        char                 *event_name; /* Optional event name. */
        char                 *span;       /* Optional span reference. */
-       struct otelc_kv      *attr;       /* Log record attributes. */
-       size_t                attr_len;   /* Number of log record attributes. */
+       struct list           attributes; /* Log record attributes (flt_otel_conf_sample). */
        struct list           samples;    /* Sample expressions for the body. */
 };
 
index 8e4a79a1c15e33118b348e7903e10618d646acba..8412b2066ed79d4da5e2b096d6520fa60ba7a33f 100644 (file)
        FLT_OTEL_PARSE_SCOPE_DEF(      STATUS, 1, NONE, 2, 0,   "status",     " <code> [<sample> ...]")                                                                        \
        FLT_OTEL_PARSE_SCOPE_DEF(      FINISH, 0, NONE, 2, 0,   "finish",     " <name> ...")                                                                                   \
        FLT_OTEL_PARSE_SCOPE_DEF(  INSTRUMENT, 0, NONE, 3, 0, "instrument",   " { update <name> [<attr> ...] | <type> <name> [<aggr>] [<desc>] [<unit>] <value> [<bounds>] }") \
-       FLT_OTEL_PARSE_SCOPE_DEF(  LOG_RECORD, 0, NONE, 3, 0, "log-record",   " <severity> [<id>] [<event>] [<span>] [<attr>] <sample>")                                       \
+       FLT_OTEL_PARSE_SCOPE_DEF(  LOG_RECORD, 0, NONE, 3, 0, "log-record",   " <severity> [<id>] [<event>] [<span>] [<attr>] <sample> ...")                                   \
        FLT_OTEL_PARSE_SCOPE_DEF(IDLE_TIMEOUT, 0, NONE, 2, 2, "idle-timeout", " <time>")                                                                                       \
        FLT_OTEL_PARSE_SCOPE_DEF(         ACL, 0, CHAR, 3, 0, "acl",          " <name> <criterion> [flags] [operator] <value> ...")                                            \
        FLT_OTEL_PARSE_SCOPE_DEF(    ON_EVENT, 0, NONE, 2, 0, "otel-event",   " <name> [{ if | unless } <condition>]")
index 8a10df21944162711031238645d69321ff5ae42e..e86baec28f14736fe7e28317400c6b33feeb32ae 100644 (file)
@@ -559,14 +559,15 @@ FLT_OTEL_CONF_FUNC_FREE(instrument, id,
  *
  * DESCRIPTION
  *   Allocates and initializes a conf_log_record structure.  Initializes the
- *   sample expressions list.  The <id> string is required by the macro but is
- *   not used directly; the severity level is stored separately.  If <head> is
- *   non-NULL, the structure is appended to the list.
+ *   attributes and sample expressions lists.  The <id> string is required by
+ *   the macro but is not used directly; the severity level is stored
+ *   separately.  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(log_record, id,
+       LIST_INIT(&(retptr->attributes));
        LIST_INIT(&(retptr->samples));
 )
 
@@ -593,7 +594,7 @@ FLT_OTEL_CONF_FUNC_FREE(log_record, id,
 
        OTELC_SFREE((*ptr)->event_name);
        OTELC_SFREE((*ptr)->span);
-       otelc_kv_destroy(&((*ptr)->attr), (*ptr)->attr_len);
+       FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->attributes));
        FLT_OTEL_LIST_DESTROY(sample, &((*ptr)->samples));
 )
 
index 4151eca61ded20ac2c9b9835de0a8460a4455275..e4368b7cac034b19d770e54bb67fd4b4aefcc637 100644 (file)
@@ -258,6 +258,7 @@ static int flt_otel_scope_run_log_record(struct stream *s, struct filter *f, uin
                struct flt_otel_conf_sample_expr *expr;
                struct sample                     smp;
                struct otelc_span                *otel_span = NULL;
+               struct flt_otel_scope_data_kv     log_attr;
                struct buffer                     buffer;
                int                               rc;
 
@@ -267,6 +268,28 @@ static int flt_otel_scope_run_log_record(struct stream *s, struct filter *f, uin
                if (OTELC_OPS(logger, enabled, conf_log->severity) == 0)
                        continue;
 
+               /* Evaluate log record attributes from sample expressions. */
+               (void)memset(&log_attr, 0, sizeof(log_attr));
+
+               list_for_each_entry(sample, &(conf_log->attributes), list) {
+                       struct otelc_value attr_value;
+
+                       OTELC_DBG(DEBUG, "adding log-record attribute '%s' -> '%s'", sample->key, sample->fmt_string);
+
+                       if (flt_otel_sample_eval(s, dir, sample, true, &attr_value, err) == FLT_OTEL_RET_ERROR) {
+                               retval = FLT_OTEL_RET_ERROR;
+
+                               continue;
+                       }
+
+                       if (flt_otel_sample_add_kv(&log_attr, sample->key, &attr_value) == FLT_OTEL_RET_ERROR) {
+                               if (attr_value.u_type == OTELC_VALUE_DATA)
+                                       OTELC_SFREE(attr_value.u.value_data);
+
+                               retval = FLT_OTEL_RET_ERROR;
+                       }
+               }
+
                /* The samples list has exactly one entry. */
                sample = LIST_NEXT(&(conf_log->samples), typeof(sample), list);
 
@@ -318,6 +341,8 @@ static int flt_otel_scope_run_log_record(struct stream *s, struct filter *f, uin
 
                        retval = FLT_OTEL_RET_ERROR;
 
+                       otelc_kv_destroy(&(log_attr.attr), log_attr.cnt);
+
                        continue;
                }
 
@@ -341,9 +366,10 @@ static int flt_otel_scope_run_log_record(struct stream *s, struct filter *f, uin
                                OTELC_DBG(NOTICE, "WARNING: cannot find span '%s' for log-record", conf_log->span);
                }
 
-               if (OTELC_OPS(logger, log_span, conf_log->severity, conf_log->event_id, conf_log->event_name, otel_span, ts, conf_log->attr, conf_log->attr_len, "%s", buffer.area) == OTELC_RET_ERROR)
+               if (OTELC_OPS(logger, log_span, conf_log->severity, conf_log->event_id, conf_log->event_name, otel_span, ts, log_attr.attr, log_attr.cnt, "%s", buffer.area) == OTELC_RET_ERROR)
                        retval = FLT_OTEL_RET_ERROR;
 
+               otelc_kv_destroy(&(log_attr.attr), log_attr.cnt);
                OTELC_SFREE(buffer.area);
        }
 
index df4f05b8afe5cccf67808e14c57548a2beaa4a40..38b81f9e2cc84dc6e4f8e56885b73cf3f1fc42e8 100644 (file)
@@ -1224,10 +1224,11 @@ static int flt_otel_parse_cfg_log_record(const char *file, int line, char **args
                else if (FLT_OTEL_PARSE_KEYWORD(i, FLT_OTEL_PARSE_LOG_RECORD_ATTR)) {
                        if (!FLT_OTEL_ARG_ISVALID(i + 1) || !FLT_OTEL_ARG_ISVALID(i + 2))
                                FLT_OTEL_PARSE_ERR(err, "'%s' : too few arguments (use '%s%s')", args[i], pdata->name, pdata->usage);
-                       else if (otelc_kv_add(&(log->attr), &(log->attr_len), args[i + 1], args[i + 2], strlen(args[i + 2])) == OTELC_RET_ERROR)
-                               FLT_OTEL_PARSE_ERR(err, "'%s' : out of memory", args[0]);
-                       else
-                               i += 2;
+                       else {
+                               retval = flt_otel_parse_cfg_sample(file, line, args, i + 2, 1, NULL, &(log->attributes), err);
+                               if (!(retval & ERR_CODE))
+                                       i += 2;
+                       }
                }
                else {
                        /*
index 78f70fab06ac1d81677e366b187da3ef72b79131..86b3a3a547879634e467ec6cd8b4bb5944e46191 100644 (file)
@@ -71,7 +71,7 @@
             event "event_be" "be"  be_id str(" ") be_name
             event "event_ip" "dst" dst str(":") dst_port
             event "event_fe" "fe"  fe_id str(" ") fe_name
-        log-record trace id 1000 event "session-start" span "HAProxy session" attr "attr_1_key" "attr_1_value" attr "attr_2_key" "attr_2_value" src str(":") src_port
+        log-record trace id 1000 event "session-start" span "HAProxy session" attr "attr_1_key" src attr "attr_2_key" src_port src str(":") src_port
         acl acl-test-src-ip src 127.0.0.1
         otel-event on-stream-start if acl-test-src-ip
 
             attribute "http.url" url
             attribute "http.version" str("HTTP/") req.ver
         finish "HTTP body request"
-        log-record info id 1002 event "http-request" span "Frontend HTTP request" attr "http.method" "GET" method url
+        log-record info id 1002 event "http-request" span "Frontend HTTP request" attr "http.method" method method url
         otel-event on-frontend-http-request
 
     otel-scope switching_rules_request
index e7b25f790394d4938437ecf5550345e578328479..2e1d2733566664d9a0298212101fa95635185ff9 100644 (file)
@@ -65,7 +65,7 @@
             event "event_be" "be"  be_id str(" ") be_name
             event "event_ip" "dst" dst str(":") dst_port
             event "event_fe" "fe"  fe_id str(" ") fe_name
-        log-record trace id 1000 event "session-start" span "Client session" attr "attr_1_key" "attr_1_value" attr "attr_2_key" "attr_2_value" src str(":") src_port
+        log-record trace id 1000 event "session-start" span "Client session" attr "attr_1_key" src attr "attr_2_key" src_port src str(":") src_port
         acl acl-test-src-ip src 127.0.0.1
         otel-event on-stream-start if acl-test-src-ip
 
             attribute "http.url" url
             attribute "http.version" str("HTTP/") req.ver
         finish "HTTP body request"
-        log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" "GET" method url
+        log-record info id 1001 event "http-request" span "Frontend HTTP request" attr "http.method" method method url
         otel-event on-frontend-http-request
 
     otel-scope switching_rules_request