OpenTelemetry filter -- design patterns
==========================================================================

This document describes the cross-cutting design patterns used throughout
the OTel filter implementation.  It complements README-implementation
(component-by-component architecture) with a pattern-oriented view of the
mechanisms that span multiple source files.


1  X-Macro Code Generation
----------------------------------------------------------------------

The filter uses X-macro lists extensively to generate enums, keyword tables,
event metadata and configuration init/free functions from a single definition.
Each X-macro list is a preprocessor define containing repeated invocations of
a helper macro whose name is supplied by the expansion context.

1.1  Event Definitions

FLT_OTEL_EVENT_DEFINES (event.h) drives the event system.  Each entry has the
form:

  FLT_OTEL_EVENT_DEF(NAME, CHANNEL, REQ_AN, RES_AN, HTTP_INJ, "string")

The same list is expanded twice:

  - In enum FLT_OTEL_EVENT_enum (event.h) to produce symbolic constants
    (FLT_OTEL_EVENT_NONE, FLT_OTEL_EVENT_REQ_STREAM_START, etc.) followed by
    the FLT_OTEL_EVENT_MAX sentinel.

  - In the flt_otel_event_data[] initializer (event.c) to produce a const table
    of struct flt_otel_event_data entries carrying the analyzer bit, sample
    fetch direction, valid fetch locations, HTTP injection flag and event name
    string.

Adding a new event requires a single line in the X-macro list.

1.2  Parser Keyword Definitions

Three X-macro lists drive the configuration parser:

  FLT_OTEL_PARSE_INSTR_DEFINES   (parser.h, 9 keywords)
  FLT_OTEL_PARSE_GROUP_DEFINES   (parser.h, 2 keywords)
  FLT_OTEL_PARSE_SCOPE_DEFINES   (parser.h, 15 keywords)

Each entry has the form:

  FLT_OTEL_PARSE_DEF(ENUM, flag, check, min, max, "name", "usage")

Each list is expanded to:

  - An enum for keyword indices (FLT_OTEL_PARSE_INSTR_ID, etc.).
  - A static parse_data[] table of struct flt_otel_parse_data entries carrying
    the keyword index, flag_check_id, check_name type, argument count bounds,
    keyword name string and usage string.

The section parsers (flt_otel_parse_cfg_instr, _group, _scope) look up args[0]
in their parse_data table via flt_otel_parse_cfg_check(), which validates
argument counts and character constraints before dispatching to
the keyword-specific handler.

Additional X-macro lists generate:

  FLT_OTEL_PARSE_SCOPE_STATUS_DEFINES      Span status codes.
  FLT_OTEL_PARSE_SCOPE_INSTRUMENT_DEFINES  Instrument type keywords.
  FLT_OTEL_HTTP_METH_DEFINES               HTTP method name table.
  FLT_OTEL_GROUP_DEFINES                   Group action metadata.

1.3  Configuration Init/Free Generation

Two macros in conf_funcs.h generate init and free functions for all
configuration structure types:

  FLT_OTEL_CONF_FUNC_INIT(_type_, _id_, _func_)
  FLT_OTEL_CONF_FUNC_FREE(_type_, _id_, _func_)

The _type_ parameter names the structure (e.g. "span"), _id_ names the
identifier field within the FLT_OTEL_CONF_HDR (e.g. "id", "key", "str"), and
_func_ is a brace-enclosed code block executed after the boilerplate allocation
(for init) or before the boilerplate teardown (for free).

The generated init function:
  1. Validates the identifier is non-NULL and non-empty.
  2. Checks the length against FLT_OTEL_ID_MAXLEN (64).
  3. Detects duplicates in the target list.
  4. Handles auto-generated IDs (FLT_OTEL_CONF_HDR_SPECIAL prefix).
  5. Allocates with OTELC_CALLOC, duplicates the ID with OTELC_STRDUP.
  6. Appends to the parent list via LIST_APPEND.
  7. Executes the custom _func_ body.

The generated free function:
  1. Executes the custom _func_ body (typically list destruction).
  2. Frees the identifier string.
  3. Removes the node from its list.
  4. Frees the structure with OTELC_SFREE_CLEAR.

conf.c instantiates these macros for all 13 configuration types: hdr, str, ph,
sample_expr, sample, link, context, span, scope, instrument, log_record, group,
instr.  The more complex types (span, scope, instr) carry non-trivial _func_
bodies that initialize sub-lists or set default values.


2  Type-Safe Generic Macros
----------------------------------------------------------------------

define.h provides utility macros that use typeof() and statement expressions
for type-safe generic programming without C++ templates.

2.1  Safe Pointer Dereferencing

  FLT_OTEL_PTR_SAFE(a, b)
    Returns 'a' if non-NULL, otherwise the default value 'b'.  Uses typeof(*(a))
    to verify type compatibility at compile time.

  FLT_OTEL_DEREF(p, m, v)
    Safely dereferences p->m, returning 'v' if 'p' is NULL.

  FLT_OTEL_DDEREF(a, m, v)
    Safely double-dereferences *a->m, returning 'v' if either 'a' or '*a' is
    NULL.

These macros eliminate repetitive NULL checks while preserving the correct
pointer types through typeof().

2.2  String Comparison

  FLT_OTEL_STR_CMP(S, s)
    Compares a runtime string 's' against a compile-time string literal 'S'.
    Expands to a call with the literal's address and computed length, avoiding
    repeated strlen() on known constants.

  FLT_OTEL_CONF_STR_CMP(s, S)
    Compares two configuration strings using their cached length fields (from
    FLT_OTEL_CONF_STR / FLT_OTEL_CONF_HDR).  Falls through to memcmp() only when
    lengths match, providing an early-out fast path.

2.3  List Operations

  FLT_OTEL_LIST_ISVALID(a)
    Checks whether a list head has been initialized (both prev and next are
    non-NULL).

  FLT_OTEL_LIST_DEL(a)
    Safely deletes a list element only if the list head is valid.

  FLT_OTEL_LIST_DESTROY(t, h)
    Destroys all elements in a typed configuration list by iterating with
    list_for_each_entry_safe and calling flt_otel_conf_<t>_free() for each
    entry.  The type 't' is used to derive both the structure type and the
    free function name.

2.4  Thread-Local Rotating Buffers

  FLT_OTEL_BUFFER_THR(b, m, n, p)
    Declares a thread-local pool of 'n' string buffers, each of size 'm'.
    The pool index rotates on each invocation, avoiding the need for explicit
    allocation in debug formatting functions.  Each call returns a pointer to
    the next buffer via the output parameter 'p'.

    Used primarily in debug-only functions (flt_otel_analyzer,
    flt_otel_list_dump) where temporary strings must survive until their caller
    finishes with them.


3  Error Handling Architecture
----------------------------------------------------------------------

3.1  Error Accumulation Macros

Three macros in define.h manage error messages:

  FLT_OTEL_ERR(f, ...)
    Formats an error message via memprintf() into the caller's char **err
    pointer, but only if *err is still NULL.  Prevents overwriting the first
    error with a cascading secondary error.

  FLT_OTEL_ERR_APPEND(f, ...)
    Unconditionally appends to the error message regardless of prior content.
    Used when a function must report multiple issues.

  FLT_OTEL_ERR_FREE(p)
    Logs the accumulated error message at WARNING level via FLT_OTEL_ALERT,
    then frees the string and NULLs the pointer.

All source functions that can fail accept a char **err parameter.  The
convention is: set *err on error, leave it alone on success.  Callers check
both the return value and *err to detect problems.

3.2  Parse-Time Error Reporting

Configuration parsing (parser.c) uses additional macros:

  FLT_OTEL_PARSE_ERR(e, f, ...)
    Sets the error string and ORs ERR_ABORT | ERR_ALERT into the return value.
    Used inside section parser switch statements.

  FLT_OTEL_PARSE_ALERT(f, ...)
    Issues a ha_alert() with the current file and line, then sets the error
    flags.  Used when the error message should appear immediately.

  FLT_OTEL_POST_PARSE_ALERT(f, ...)
    Same as PARSE_ALERT but uses cfg_file instead of file, for post-parse
    validation that runs after the parser has moved on.

  FLT_OTEL_PARSE_IFERR_ALERT()
    Checks whether *err was set by a called function, and if so, issues an
    alert and clears the error.  This bridges between functions that use the
    char **err convention and the parser's own alert mechanism.

3.3  Runtime Error Modes

Two helper functions in filter.c implement the runtime error policy:

  flt_otel_return_int(f, err, retval)
  flt_otel_return_void(f, err)

If an error is detected (retval == FLT_OTEL_RET_ERROR or *err != NULL):

  Hard-error mode (rt_ctx->flag_harderr):
    Sets rt_ctx->flag_disabled = 1, disabling the filter for the remainder of
    the stream.  Atomically increments the disabled counter.  The error is
    logged as "filter hard-error (disabled)".

  Soft-error mode (default):
    The error is logged as "filter soft-error" and processing continues.  The
    tracing data for the current event may be incomplete but the stream is not
    affected.

In both modes, FLT_OTEL_RET_OK is always returned to HAProxy.  A tracing failure
never interrupts stream processing.

3.4  Disabled State Checking

flt_otel_is_disabled() (filter.c) is called at the top of every filter callback
that processes events.  It checks three conditions:

  1. rt_ctx is NULL (filter was never attached or already detached).
  2. rt_ctx->flag_disabled is set (hard-error disabled the filter).
  3. conf->instr->flag_disabled is set (globally disabled via CLI).

All three are loaded via _HA_ATOMIC_LOAD() for thread safety.  When disabled,
the callback returns immediately without processing.


4  Thread Safety Model
----------------------------------------------------------------------

The filter runs within HAProxy's multi-threaded worker model.  Each stream is
bound to a single thread, so per-stream state (the runtime context, spans and
contexts) is accessed without locking.  Cross-stream shared state uses atomic
operations.

4.1  Atomic Fields

The following fields are accessed atomically across threads:

  conf->instr->flag_disabled  Toggled by CLI "otel enable/disable".  Checked in
                              flt_otel_ops_attach() and flt_otel_is_disabled().

  conf->instr->flag_harderr   Toggled by CLI "otel hard-errors/soft-errors".
                              Copied to rt_ctx at attach time.

  conf->instr->rate_limit     Set by CLI "otel rate".
                              Read in flt_otel_ops_attach().

  conf->instr->logging        Set by CLI "otel logging".
                              Copied to rt_ctx at attach time.

  flt_otel_drop_cnt           Incremented in flt_otel_log_handler_cb().
                              Read by CLI "otel status".

  conf->cnt.disabled[]        Incremented in flt_otel_return_int/void().
  conf->cnt.attached[]        Incremented in flt_otel_ops_attach().

All use _HA_ATOMIC_LOAD(), _HA_ATOMIC_STORE(), _HA_ATOMIC_INC() or
_HA_ATOMIC_ADD() from HAProxy's atomic primitives.

4.2  Metric Instrument Creation (CAS Pattern)

Metric instruments are shared across all threads but created lazily on first
use.  The creation uses a compare-and-swap pattern to ensure exactly one thread
performs the creation:

  int64_t expected = OTELC_METRIC_INSTRUMENT_UNSET;  /* -1 */
  if (HA_ATOMIC_CAS(&instr->idx, &expected, OTELC_METRIC_INSTRUMENT_PENDING)) {
      /* This thread won the race -- create the instrument. */
      idx = meter->create_instrument(...);
      HA_ATOMIC_STORE(&instr->idx, idx);
  }

The three states are:
  UNSET   (-1)  Not yet created.  The CAS target.
  PENDING (-2)  Creation in progress by another thread.
  >= 0          Valid meter index.  Ready for recording.

Threads that lose the CAS skip creation.  Update-form instruments check that the
referenced create-form's idx is >= 0 before recording; if it is still PENDING or
UNSET, the measurement is silently skipped.

4.3  Per-Thread Initialization Guard

flt_otel_ops_init_per_thread() uses the FLT_CFG_FL_HTX flag on fconf as a
one-shot guard: the tracer, meter and logger background threads are started only
on the first call.  Subsequent calls from other proxies sharing the same filter
configuration skip the start sequence.

4.4  CLI Update Propagation

CLI set commands (cli.c) iterate all OTel filter instances across all proxies
using the FLT_OTEL_PROXIES_LIST_START / FLT_OTEL_PROXIES_LIST_END macros
(util.h) and atomically update the target field on each instance.  This ensures
that "otel disable" takes effect globally, not just on one proxy.


5  Sample Evaluation Pipeline
----------------------------------------------------------------------

Sample expressions bridge HAProxy's runtime data (headers, variables, connection
properties) into OTel attributes, events, baggage, status, metric values and log
record bodies.

5.1  Two Evaluation Paths

The parser detects which path to use based on the presence of "%[" in the sample
value argument:

  Log-format path (sample->lf_used == true):
    The value is parsed via parse_logformat_string() and stored in
    sample->lf_expr.  At runtime, build_logline() evaluates the expression into
    a temporary buffer of global.tune.bufsize bytes.  The result is always a
    string.

  Bare sample path (sample->lf_used == false):
    Each whitespace-separated token is parsed via sample_parse_expr() and
    stored as an flt_otel_conf_sample_expr in sample->exprs.  At runtime, each
    expression is evaluated via sample_process().  If there is exactly one
    expression, the native HAProxy sample type is preserved (bool, int, IP
    address, string).  If there are multiple expressions, their string
    representations are concatenated.

5.2  Type Conversion Chain

flt_otel_sample_to_value() (util.c) converts HAProxy sample types to OTel value
types:

  SMP_T_BOOL  ->  OTELC_VALUE_BOOL
  SMP_T_SINT  ->  OTELC_VALUE_INT64
  Other       ->  OTELC_VALUE_DATA (string, via flt_otel_sample_to_str)

flt_otel_sample_to_str() handles the string conversion for non-trivial types:

  SMP_T_BOOL     "0" or "1"
  SMP_T_SINT     snprintf decimal
  SMP_T_IPV4     inet_ntop
  SMP_T_IPV6     inet_ntop
  SMP_T_STR      direct copy
  SMP_T_METH     lookup from static HTTP method table, or raw string for
                 HTTP_METH_OTHER

For metric instruments, string values are rejected -- the meter requires
OTELC_VALUE_INT64.  If the sample evaluates to a string, otelc_value_strtonum()
attempts numeric conversion as a last resort.

5.3  Dispatch to Data Structures

flt_otel_sample_add() (util.c) is the top-level evaluator.  After converting the
sample to an otelc_value, it dispatches based on type:

  FLT_OTEL_EVENT_SAMPLE_ATTRIBUTE
    -> flt_otel_sample_add_kv(&data->attributes, key, &value)

  FLT_OTEL_EVENT_SAMPLE_BAGGAGE
    -> flt_otel_sample_add_kv(&data->baggage, key, &value)

  FLT_OTEL_EVENT_SAMPLE_EVENT
    -> flt_otel_sample_add_event(&data->events, sample, &value)
       Groups attributes by event name; creates a new flt_otel_scope_data_event
       node if the event name is not yet present.

  FLT_OTEL_EVENT_SAMPLE_STATUS
    -> flt_otel_sample_set_status(&data->status, sample, &value, err)
       Sets the status code from sample->extra.num and the description from the
       evaluated value.  Rejects duplicate status settings.

5.4  Growable Key-Value Arrays

Attribute and baggage arrays use a lazy-allocation / grow-on-demand pattern via
flt_otel_sample_add_kv() (util.c):

  - Initial allocation: FLT_OTEL_ATTR_INIT_SIZE (8) elements via otelc_kv_new().
  - Growth: FLT_OTEL_ATTR_INC_SIZE (4) additional elements via otelc_kv_resize()
    when the array is full.
  - The cnt field tracks used elements; size tracks total capacity.

Event attribute arrays follow the same pattern within each
flt_otel_scope_data_event node.


6  Rate Limiting and Filtering
----------------------------------------------------------------------

6.1  Rate Limit Representation

The rate limit is configured as a floating-point percentage (0.0 to 100.0) but
stored internally as a uint32_t for lock-free comparison:

  FLT_OTEL_FLOAT_U32(a)  Converts a percentage to a uint32 value:
                          (uint32_t)((a / 100.0) * UINT32_MAX)

  FLT_OTEL_U32_FLOAT(a)  Converts back for display:
                          ((double)(a) / UINT32_MAX) * 100.0

At attach time, the filter compares a fresh ha_random32() value against the
stored rate_limit.  If the random value exceeds the threshold, the filter
returns FLT_OTEL_RET_IGNORE and does not attach to the stream.  This provides
uniform sampling without floating-point arithmetic on the hot path.

6.2  ACL-Based Filtering

Each scope may carry an ACL condition (if/unless) evaluated at scope
execution time via acl_exec_cond().  ACL references are resolved through
a priority chain in flt_otel_parse_acl() (parser.c):

  1. Scope-local ACLs (declared within the otel-scope section).
  2. Instrumentation ACLs (declared in the otel-instrumentation section).
  3. Proxy ACLs (declared in the HAProxy frontend/backend section).

The first list that produces a successful build_acl_cond() result is used.
If the condition fails at runtime:

  - If the scope contains a root span, the entire stream is disabled
    (rt_ctx->flag_disabled = 1) to avoid orphaned child spans.
  - Otherwise, the scope is simply skipped.

6.3  Global Disable

The filter can be disabled globally via the CLI ("otel disable") or the
configuration ("option disabled").  The flag_disabled field on the
instrumentation configuration is checked atomically in flt_otel_ops_attach()
before any per-stream allocation occurs.


7  Memory Management Strategy
----------------------------------------------------------------------

7.1  Pool-Based Allocation

Four HAProxy memory pools are registered for frequently allocated structures
(pool.c):

  pool_head_otel_scope_span       Per-stream span entries.
  pool_head_otel_scope_context    Per-stream context entries.
  pool_head_otel_runtime_context  Per-stream runtime context.
  pool_head_otel_span_context     OTel SDK objects (via C wrapper allocator
                                  callback).

Pool registration is conditional on compile-time flags (USE_POOL_OTEL_* in
config.h).  flt_otel_pool_alloc() attempts pool allocation first and falls back
to heap allocation (OTELC_MALLOC) when the pool is NULL or the requested size
exceeds the pool's element size.

The C wrapper library's memory allocations are redirected through
flt_otel_mem_malloc() / flt_otel_mem_free() (filter.c), which use the
otel_span_context pool via otelc_ext_init().  This keeps OTel SDK objects in
HAProxy's pool allocator for cache-friendly allocation.

7.2  Trash Buffers

flt_otel_trash_alloc() (pool.c) provides temporary scratch buffers of
global.tune.bufsize bytes.  When USE_TRASH_CHUNK is defined, it uses HAProxy's
alloc_trash_chunk(); otherwise it manually allocates a buffer structure and data
area.  Trash buffers are used for:

  - Log-format evaluation in metric recording and log record emission.
  - HTTP header name construction in flt_otel_http_header_set().
  - Temporary string assembly in sample evaluation.

7.3  Scope Data Lifecycle

flt_otel_scope_data (scope.h) is initialized and freed within a single span
processing block in flt_otel_scope_run().  It is stack-conceptual data:
allocated at the start of each span's processing, populated during sample
evaluation, consumed by flt_otel_scope_run_span(), and freed immediately after.
The key-value arrays within it are heap-allocated via otelc_kv_new() and freed
via otelc_kv_destroy().


8  Context Propagation Mechanism
----------------------------------------------------------------------

8.1  Carrier Abstraction

The OTel C wrapper uses a carrier pattern for context propagation.  Two carrier
types exist, each with writer and reader variants:

  otelc_text_map_writer / otelc_text_map_reader
  otelc_http_headers_writer / otelc_http_headers_reader

otelc.c provides callback functions that the C wrapper invokes during
inject/extract operations:

  Injection (writer callbacks):
    flt_otel_text_map_writer_set_cb()
    flt_otel_http_headers_writer_set_cb()
    Both append key-value pairs to the carrier's text_map via
    OTELC_TEXT_MAP_ADD.

  Extraction (reader callbacks):
    flt_otel_text_map_reader_foreach_key_cb()
    flt_otel_http_headers_reader_foreach_key_cb()
    Both iterate the text_map's key-value pairs, invoking a handler function
    for each entry.  Iteration stops early if the handler returns -1.

8.2  HTTP Header Storage

flt_otel_http_headers_get() (http.c) reads headers from the HTX buffer with
prefix matching.  The prefix is stripped from header names in the returned
text_map.  A special prefix character ('-') matches all headers without prefix
filtering.

flt_otel_http_header_set() constructs a "prefix-name" header, removes all
existing occurrences via an http_find_header / http_remove_header loop, then
adds the new value via http_add_header.

8.3  Variable Storage

When USE_OTEL_VARS is enabled, span context can also be stored in HAProxy
transaction-scoped variables.  Variable names are normalized
(flt_otel_normalize_name in vars.c):

  Dashes    -> 'D'  (FLT_OTEL_VAR_CHAR_DASH)
  Spaces    -> 'S'  (FLT_OTEL_VAR_CHAR_SPACE)
  Uppercase -> lowercase

Full variable names are constructed as "scope.prefix.name" with dots as
component separators.

Two implementation paths exist based on the USE_OTEL_VARS_NAME compile flag:

  With USE_OTEL_VARS_NAME:
    A binary tracking buffer (stored as a HAProxy variable) records the names
    of all context variables.  flt_otel_ctx_loop() iterates length-prefixed
    entries in this buffer, calling a callback for each.  This enables efficient
    enumeration without scanning the full variable store.

  Without USE_OTEL_VARS_NAME:
    Direct CEB tree traversal on the HAProxy variable store with prefix
    matching.  This is simpler but potentially slower for large variable sets.

The choice is auto-detected at build time by checking whether struct var has
a 'name' member.


9  Debug Infrastructure
----------------------------------------------------------------------

9.1  Conditional Compilation

When OTEL_DEBUG=1 is set at build time, -DDEBUG_OTEL is added to the compiler
flags.  This enables:

  - Additional flt_ops callbacks: deinit_per_thread, http_payload, http_reset,
    tcp_payload.  In non-debug builds these are NULL.

  - FLT_OTEL_USE_COUNTERS (config.h), which activates the per-event counters in
    flt_otel_counters (attached[], disabled[] arrays).

  - Debug-only functions throughout the codebase, marked with [D] in
    README-func: flt_otel_scope_data_dump, flt_otel_http_headers_dump,
    flt_otel_vars_dump, flt_otel_args_dump, flt_otel_filters_dump, etc.

  - The OTELC_DBG() macro for level-gated debug output.

9.2  Debug Level Bitmask

The debug level is a uint stored in conf->instr and controllable at runtime via
"otel debug <level>".  The default value is FLT_OTEL_DEBUG_LEVEL (0b11101111111
in config.h).  Each bit enables a category of debug output.

9.3  Logging Integration

The FLT_OTEL_LOG macro (debug.h) integrates with HAProxy's send_log() system.
Its behavior depends on the logging flags:

  FLT_OTEL_LOGGING_OFF (0):
    No log messages are emitted.

  FLT_OTEL_LOGGING_ON (1 << 0):
    Log messages are sent to the log servers configured in the instrumentation
    block via parse_logger().

  FLT_OTEL_LOGGING_NOLOGNORM (1 << 1):
    Combined with ON, suppresses normal-level messages (only warnings and above
    are emitted).

These flags are set via the "option dontlog-normal" configuration keyword or the
"otel logging" CLI command.

9.4  Counter System

When FLT_OTEL_USE_COUNTERS is defined, the flt_otel_counters structure (conf.h)
maintains:

  attached[4]   Counters for attach outcomes, incremented atomically in
                flt_otel_ops_attach().
  disabled[2]   Counters for hard-error disables, incremented atomically in
                flt_otel_return_int() / flt_otel_return_void().

The counters are reported by the "otel status" CLI command and dumped at deinit
time in debug builds.


10  Idle Timeout Mechanism
----------------------------------------------------------------------

The idle timeout fires periodic events on idle streams, enabling heartbeat-style
span updates or metric recordings.

10.1  Configuration

Each otel-scope bound to the "on-idle-timeout" event must declare an
idle-timeout interval.  At check time (flt_otel_ops_check), the minimum idle
timeout across all scopes is stored in conf->instr->idle_timeout.

10.2  Initialization

In flt_otel_ops_stream_start(), the runtime context's idle_timeout and idle_exp
fields are initialized from the precomputed minimum.  The expiration tick is
merged into the request channel's analyse_exp to ensure the stream task wakes
at the right time.

10.3  Firing

flt_otel_ops_check_timeouts() checks tick_is_expired(rt_ctx->idle_exp).
When expired:
  - The on-idle-timeout event fires via flt_otel_event_run().
  - The timer is rescheduled for the next interval.
  - If analyse_exp itself has expired (which would cause a tight loop), it is
    reset before the new idle_exp is merged.
  - STRM_EVT_MSG is set on stream->pending_events to ensure the stream is
    re-evaluated.


11  Group Action Integration Pattern
----------------------------------------------------------------------

The "otel-group" action integrates OTel scopes with HAProxy's rule system
(http-request, http-response, http-after-response, tcp-request, tcp-response).

11.1  Registration

group.c registers action keywords via INITCALL1 macros for all five action
contexts.  Each registration points to flt_otel_group_parse() as the parse
function.

11.2  Parse-Check-Execute Cycle

  Parse (flt_otel_group_parse):
    Stores the filter ID and group ID as string duplicates in rule->arg.act.p[].
    Sets the check, action and release callbacks.

  Check (flt_otel_group_check):
    Resolves the string IDs to configuration pointers by scanning the proxy's
    filter list.  Replaces the string pointers with resolved flt_conf,
    flt_otel_conf and flt_otel_conf_ph pointers.  Frees the original string
    duplicates.

  Execute (flt_otel_group_action):
    Finds the filter instance in the stream's filter list.  Validates rule->from
    against the flt_otel_group_data[] table to determine the sample fetch
    direction and valid fetch locations.  Iterates all scopes in the group and
    calls flt_otel_scope_run() for each.  Always returns ACT_RET_CONT -- a group
    action never interrupts rule processing.

11.3  Group Data Table

The flt_otel_group_data[] table (group.c) maps each HAProxy action context
(ACT_F_*) to:

  act_from     The action context enum value.
  smp_val      The valid sample fetch location (FE or BE).
  smp_opt_dir  The sample fetch direction (REQ or RES).

This table is generated from the FLT_OTEL_GROUP_DEFINES X-macro list (group.h).


12  CLI Runtime Control Architecture
----------------------------------------------------------------------

cli.c registers commands under the "otel" prefix.  The command table maps
keyword sequences to handler functions with access level requirements.

12.1  Command Table

  "otel status"       No access restriction.  Read-only status report.
  "otel enable"       ACCESS_LVL_ADMIN.  Clears flag_disabled.
  "otel disable"      ACCESS_LVL_ADMIN.  Sets flag_disabled.
  "otel hard-errors"  ACCESS_LVL_ADMIN.  Sets flag_harderr.
  "otel soft-errors"  ACCESS_LVL_ADMIN.  Clears flag_harderr.
  "otel logging"      ACCESS_LVL_ADMIN for setting; any for reading.
  "otel rate"         ACCESS_LVL_ADMIN for setting; any for reading.
  "otel debug"        ACCESS_LVL_ADMIN.  DEBUG_OTEL only.

The "otel enable" and "otel disable" commands share the same handler
(flt_otel_cli_parse_disabled) with the private argument distinguishing the
operation (1 for disable, 0 for enable).  The same pattern is used for
hard-errors / soft-errors.

12.2  Global Propagation

All set operations iterate every OTel filter instance across all proxies using
FLT_OTEL_PROXIES_LIST_START / FLT_OTEL_PROXIES_LIST_END macros and atomically
update the target field.  A single "otel disable" command disables the filter
in every proxy that has it configured.

12.3  Status Report

The "otel status" command assembles a dynamic string via memprintf() containing:

  - Library versions (C++ SDK and C wrapper).
  - Debug level (DEBUG_OTEL only).
  - Dropped diagnostic message count.
  - Per-proxy: config file, group/scope counts, instrumentation details, rate
    limit, error mode, disabled state, logging state, analyzer bitmask, and
    counters (if FLT_OTEL_USE_COUNTERS).
