MINOR: otel: changed log-record attr to use sample expressions

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.
This commit is contained in:
Miroslav Zagorac 2026-04-03 12:33:26 +02:00 committed by William Lallemand
parent d96ce16cef
commit 651e9fd8a7
11 changed files with 86 additions and 43 deletions

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

View 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

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

View 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

View 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. */
};

View file

@ -121,7 +121,7 @@
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>]")

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

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

View 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 {
/*

View 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
@ -132,7 +132,7 @@
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

View 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
@ -109,7 +109,7 @@
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