                   -----------------------------------------
                    HAProxy OTel filter configuration guide
                               Version 2.0.0
                        ( Last update: 2026-07-20 )
                   -----------------------------------------
                         Author : Miroslav Zagorac
                   Contact : mzagorac at haproxy dot com


SUMMARY
--------

  1.    Overview
  2.    HAProxy filter declaration
  3.    OTel configuration file structure
  3.1.    OTel scope (top-level)
  3.2.    "otel-instrumentation" section
  3.3.    "otel-scope" section
  3.4.    "otel-group" section
  4.    YAML configuration file
  4.1.    Exporters
  4.2.    Samplers
  4.3.    Processors
  4.4.    Readers
  4.5.    Providers
  4.6.    Signals
  5.    HAProxy rule integration
  6.    Complete examples
  6.1.    Standalone example (sa)
  6.2.    Frontend / backend example (fe/be)
  6.3.    Context propagation example (ctx)
  6.4.    TCP-mode example (tcp)
  6.5.    Comparison example (cmp)
  6.6.    Up-down counter example (updown)
  6.7.    Error-logging example (err)
  6.8.    Empty / minimal example (empty)


1. Overview
------------

The OTel filter configuration consists of two files:

  1) An OTel configuration file (.cfg) that defines the tracing model: scopes,
     groups, spans, attributes, events, instrumentation and log-records.

  2) A YAML configuration file (.yml) that configures the OpenTelemetry SDK
     pipeline: exporters, samplers, processors, readers, providers and signal
     routing.

The OTel configuration file is referenced from the HAProxy configuration using
the 'filter opentelemetry' directive.  The YAML file is in turn referenced from
the OTel configuration file using the 'config' keyword inside the
"otel-instrumentation" section.


2. HAProxy filter declaration
------------------------------

The filter is activated by adding a filter directive in the HAProxy
configuration, in a proxy section (frontend / listen / backend):

  frontend my-frontend
    ...
    filter opentelemetry [id <id>] config <otel-cfg-file> [<name>]
    ...

If no filter id is specified, 'otel-filter' is used as default.  The 'config'
parameter is mandatory and specifies the path to the OTel configuration file.
An optional section name may follow the file path: it selects the top-level
OTel scope of the configuration file to use (see section 3.1) and defaults to
the filter id.  Since filter ids must be unique, the section name is what lets
several filters read the same OTel scope: each filter keeps its own id and names
one shared scope instead of holding a private copy of it per filter id.

Note: the parser does not limit the number of OTel filters; it only requires
each filter id to be unique.  Since the wrapper version 3.0.0, every filter
instance holds its own library context, so several filters, each with its own
configuration, may operate simultaneously in one HAProxy process.

Example (from test/sa/haproxy.cfg):

  frontend otel-test-sa-frontend
      bind *:10080
      default_backend servers-backend

      acl acl-http-status-ok status 100:399

      filter opentelemetry id otel-test-sa config sa/otel.cfg

      http-response otel-group otel-test-sa http_response_group if acl-http-status-ok
      http-after-response otel-group otel-test-sa http_after_response_group if !acl-http-status-ok

  backend servers-backend
      server server-1 127.0.0.1:8000

The filter attaches to both HTTP-mode and TCP-mode proxies.  On an HTTP proxy it
runs HTX-level analysis, so all events and HTTP sample fetches are available.  A
TCP proxy carries no HTTP message, so the HTTP-phase events and HTTP sample
fetches do not fire; the connection-lifecycle and tcp-request / tcp-response
content events still do, and section 3.3 lists the full set that fires in TCP
mode.  In place of the HTTP fetches, the forwarded payload is counted and
exposed through the 'otel.bytes_in' and 'otel.bytes_out' sample fetches (see
section 5), so a span opened at connect and finished at close records both the
connection lifetime and the byte volume transferred.  A scope bound to an
HTTP-only event on a non-HTTP proxy is reported as a warning at config load.


3. OTel configuration file structure
--------------------------------------

The OTel configuration file uses a simple section-based format.  It contains
a mandatory "otel-instrumentation" section, zero or more "otel-scope" sections,
and zero or more "otel-group" sections.


3.1. OTel scope (top-level)
-----------------------------

The file is organized into top-level OTel scopes, each identified by a name
enclosed in square brackets.  A filter uses the scope whose name matches the
section name given after the configuration file on its 'filter opentelemetry'
line, or, when no section name is given there, its own filter id.

  [<name>]
      otel-instrumentation <name>
          ...

      otel-group <name>
          ...

      otel-scope <name>
          ...

Multiple OTel scopes (for different filter instances) can coexist in the same
file:

  [my-first-filter]
      otel-instrumentation instr1
          ...

  [my-second-filter]
      otel-instrumentation instr2
          ...

A single OTel scope can also serve several filter instances at once: each filter
declares its own id and names the shared scope on its filter line:

  filter opentelemetry id flt-1 config otel.cfg my-first-filter
  filter opentelemetry id flt-2 config otel.cfg my-first-filter

Several OTel scopes may coexist in a single configuration file, each declared
filter selecting and running one of them.  Since the wrapper version 3.0.0,
every filter instance holds its own library context, so several filters may
operate simultaneously in one HAProxy process, each with its own scope.


3.2. "otel-instrumentation" section
-------------------------------------

Exactly one "otel-instrumentation" section must be defined per OTel scope.
It configures the global behavior of the filter and declares which groups
and scopes are active.

Syntax:

  otel-instrumentation <name>

Keywords (mandatory):

  config <file> [context]
      Path to the YAML configuration file for the OpenTelemetry SDK and, as an
      optional second argument, the name of the signals context to select within
      that file.  Without the argument, the 'default' context is served.

Keywords (optional):

  acl <aclname> <criterion> [flags] [operator] <value> ...
      Declare an ACL.  See section 7 of the HAProxy Configuration Manual.

  debug-level <value>
      Set the debug level bitmask (e.g. 0x77f).  Only effective when compiled
      with OTEL_DEBUG=1.

  groups <name> ...
      Declare one or more "otel-group" sections used by this instrumentation.
      Can be repeated on multiple lines.

  log global
  log <addr> [len <len>] [format <fmt>] <facility> [<level> [<minlevel>]]
  no log
      Enable per-instance logging and set its destination.  The filter reports
      runtime data-path problems here: hard-error episodes at level err and
      swallowed soft errors at level warning.  Each message is edge-triggered
      and rate-limited per instance, so a fault recurring on every stream does
      not flood the log; the 'runtime err' counters in 'flt-otel status' hold
      the true tally.  Without a 'log' line the filter emits nothing at runtime.

  option disabled / no option disabled
      Disable or enable the filter.  Default: enabled.

  option dontlog-normal / no option dontlog-normal
      Drop the below-error log lines (warnings and informational) while keeping
      the error-level messages.  Default: disabled (all levels are logged).

  option hard-errors / no option hard-errors
      Stop all filter processing in a stream after the first error.  Default:
      disabled (errors are non-fatal).

  rate-limit <value>
      Percentage of streams for which the filter is activated.  Floating-point
      value from 0.0 to 100.0.  Default: 100.0.

  scopes <name> ...
      Declare one or more "otel-scope" sections used by this instrumentation.
      Can be repeated on multiple lines.


Example (from test/sa/otel.cfg):

  [otel-test-sa]
      otel-instrumentation otel-test-instrumentation
          debug-level 0x77f
          log localhost:514 local7 debug
          config sa/otel.yml
          option dontlog-normal
          option hard-errors
          no option disabled
          rate-limit 100.0

          groups http_response_group
          groups http_after_response_group

          scopes skip_health_path

          scopes on_stream_start
          scopes on_stream_stop
          scopes on_idle_timeout

          scopes client_session_start
          scopes frontend_tcp_request
          ...
          scopes server_session_end


3.3. "otel-scope" section
---------------------------

An "otel-scope" section defines the actions that take place when a particular
event fires or when a group is triggered.

Syntax:

  otel-scope <name>

Supported keywords:

  span <name> [parent <ref>] [link <ref>] [root] [kind <kind>]
      Create a new span or reference an already opened one.

      - 'root' marks this span as the trace root (only one per trace).
      - 'parent <ref>' sets the parent to an existing span or extracted context
        name.  A reference that resolves to no span or extracted context --
        for instance a 'parent' on a context whose extraction found nothing --
        is an error, and the span is not created.
      - 'link <ref>' adds an inline link to another span or context.  It is
        meant to be given once: the parser warns from the second inline link
        onward, and the span argument limit caps how many can fit.  Use the
        standalone 'link' keyword to add more.
      - 'kind <kind>' sets the span kind to one of the following: 'internal',
        'server' (default), 'client', 'producer' or 'consumer'.
      - If no reference is given, the span becomes a root span.

      A span declaration opens a "sub-context" within the scope: the keywords
      'link', 'attribute', 'event', 'baggage', 'status' and 'inject' that follow
      apply to that span until the next 'span' keyword or the end of the scope.

      Examples:
        span "HAProxy session" root
        span "Client session" parent "HAProxy session"
        span "HTTP request" parent "TCP request" link "HAProxy session"
        span "Client session" parent "otel_ctx_1"
        span "Backend request" parent "Client session" kind client


  attribute <key> <sample> ... [{ if | unless } <condition>]
      Set an attribute on the currently active span.  A single sample preserves
      its native type; multiple samples are concatenated as a string.

      An optional trailing 'if'/'unless' ACL condition gates the attribute: it
      is set only when the condition is met at request time.  The keyword may be
      repeated to set several attributes on one span, and every line whose
      condition holds is applied.

      Examples:
        attribute "http.method" method
        attribute "http.url" url
        attribute "http.version" str("HTTP/") req.ver
        attribute "error.flag" str("1") if { status ge 500 }


  event <name> [time [<unit>] <sample>] <key> <sample> ... [{ if | unless } <condition>]
      Add a span event (timestamped annotation) to the currently active span.
      The data type is always string.

      - 'time [<unit>] <sample>' overrides the event timestamp.  The optional
        unit is 's' (default), 'ms', 'us' or 'ns', and <sample> must evaluate
        to a number.  Without it, the event inherits the span timestamp.
        Note that a 'time' token directly after <name> is read as this clause,
        so it cannot also serve as an attribute key in that position.

      An optional trailing 'if'/'unless' ACL condition gates the event.  The
      keyword may be repeated to add several events to one span, and every line
      whose condition holds is applied.

      Examples:
        event "event_ip" "src" src str(":") src_port
        event "event_be" "be" be_id str(" ") be_name
        event "received" time ms req.hdr("x-start-ms") "stage" str("recv")


  baggage <key> <sample> ... [{ if | unless } <condition>]
      Set baggage on the currently active span.  Baggage propagates to all child
      spans.  The data type is always string.

      An optional trailing 'if'/'unless' ACL condition gates the baggage.  The
      keyword may be repeated to set several baggage items on one span, and
      every line whose condition holds is applied.

      Examples:
        baggage "haproxy_id" var(sess.otel.uuid)
        baggage "area" str("api") if { path_beg /api }


  status <code> [<sample> ...] [{ if | unless } <condition>]
      Set the span status.  Valid codes: ignore, unset, ok, error; without a
      'status' directive a span defaults to 'ignore'.  An optional description
      follows the code, but the OTLP exporter keeps the status message only for
      the 'error' code, dropping it for 'ok' and 'unset'.

      An optional trailing 'if'/'unless' ACL condition gates the status.  The
      keyword may be given more than once per span; the lines are tried in
      configuration order and the first whose condition holds sets the status,
      the remaining lines being skipped.  A line without a condition always
      matches, so an unconditional 'status' acts as a default when placed last.
      This first-match rule is specific to 'status'; 'attribute', 'event' and
      'baggage' instead apply every line whose condition holds.

      Examples:
        status "ok"
        status "error" str("http.status_code: ") status
        status "error" str("server error") if { status ge 500 }


  exception <type> [message <sample> ...] [attr <key> <sample> ...] [{ if | unless } <condition>]
      Record an exception on the currently active span via the OpenTelemetry
      record_exception() operation, which emits a span event named 'exception'
      following the semantic conventions.  <type> is a literal string stored as
      the 'exception.type' attribute.

      The optional 'message' keyword introduces sample expressions whose values
      are concatenated into 'exception.message'.  Repeatable 'attr <key>
      <sample>' clauses add further attributes to the event.  HAProxy has no
      call stack, so 'exception.stacktrace' is never emitted.

      Recording an exception does not change the span status, so pair it with a
      'status error' line when the exception marks the span as failed.  An
      optional 'if'/'unless' ACL condition gates the record, and the keyword may
      be repeated to record several exceptions on one span.

      Examples:
        exception upstream_error message str("HTTP ") status
        exception bad_gateway attr "http.route" path if { status ge 500 }


  link { <ref> ... | <ref> attr <key> <sample> ... }
      Add non-hierarchical links to the currently active span.  Multiple span
      or context names can be specified.  Use this keyword when a link must
      carry attributes, which the inline 'link' in 'span' cannot.

      A single link may carry attributes: when 'attr' follows the one span
      name, the rest of the line is <key> <sample> pairs, each a single sample
      expression.  Link attributes require this standalone keyword.
      Because 'attr' right after the first name selects this form, a link
      target in that position cannot itself be the literal word 'attr'.

      Examples:
        link "HAProxy session" "Client session"
        link "HAProxy session" attr "link.kind" str("follows_from")


  inject <name-prefix> [use-vars] [use-headers]
      Inject span context into an HTTP header carrier and/or HAProxy variables.
      The prefix names the context; the special prefix '-' generates the name
      automatically.  An autogenerated name keeps a leading '-', so the injected
      headers carry only the bare propagation headers (see below).  Default
      storage: use-headers.  The 'use-vars' option requires OTEL_USE_VARS=1 at
      compile time.

      A span carries a single context, so 'inject' may appear at most once per
      span; a second is a configuration error.

      Header injection (use-headers) works only on events that expose a writable
      HTTP header carrier; using it on any other event is a configuration error.
      It is rejected for these events:

        on-stream-start, on-stream-stop, on-idle-timeout, on-backend-set,
        on-client-session-end, on-server-unavailable, on-server-session-start,
        on-tcp-response, on-http-end-request, on-http-end-response,
        on-http-reply, on-server-session-end

      Variable injection (use-vars) has no such restriction.

      When transferring the context via HTTP headers, if the first character of
      the context name is '-', the context name is excluded from the header.
      This enables the use of OpenTelemetry contexts with external services that
      do not expect a HAProxy-specific name prefix.

      Independently of 'inject', a build with OTEL_USE_VARS=1 registers the
      HAProxy variable 'sess.otel.uuid' on every stream the filter attaches to
      and stores the stream's runtime-context UUID in it; several examples read
      it through 'var(sess.otel.uuid)'.  Without this build option the variable
      does not exist and the fetch yields an empty value.

      Example:
        span "HAProxy session" root
            inject "otel_ctx_1" use-headers use-vars


  extract <name-prefix> [use-vars | use-headers]
      Extract a previously injected span context from an HTTP header or HAProxy
      variables.  The extracted context can then be used as a parent reference
      in 'span ... parent <name-prefix>'.  Default storage: use-headers.  The
      'use-vars' option requires OTEL_USE_VARS=1 at compile time.  A stream
      that carries no matching context data is not an error -- the extraction
      is simply skipped.  A span that then names the missing context as its
      parent fails to resolve the reference and is not created.

      When extracting context from HTTP headers, if the first character of the
      context name is '-', the context name is excluded from the header.  This
      enables the use of OpenTelemetry contexts with external services that do
      not expect a HAProxy-specific name prefix.

      Example:
        extract "otel_ctx_1" use-vars
        span "Client session" parent "otel_ctx_1"


  finish <name> ...
      Close one or more spans or span contexts.  Special names:
        '*'      - finish all open spans
        '*req*'  - finish all request-channel spans
        '*res*'  - finish all response-channel spans

      Multiple names can be given on one line; each is matched independently
      against the open spans and contexts by exact name.

      Examples:
        finish "Frontend TCP request"
        finish "Client session" "otel_ctx_2"
        finish *


  instrument <type> <name> [aggr <aggregation>] [desc <description>] [unit <unit>] value <sample> [bounds <bounds>] [{ if | unless } <condition>]
  instrument update <name> [attr <key> <sample> ...] [{ if | unless } <condition>]
      Create or update a metric instrument.  For the create form, the 'aggr',
      'desc', 'unit', 'value' and 'bounds' keywords may be given in any order.
      A create-form instrument name must be unique across all scopes of the
      configuration, and each scope may hold at most one update form for a given
      name; a duplicate of either kind is a configuration error.

      Supported types:
        cnt_int   - counter (uint64)
        hist_int  - histogram (uint64)
        udcnt_int - up-down counter (int64)
        gauge_int - gauge (int64)

      Supported aggregation types:
        drop           - measurements are discarded
        histogram      - explicit bucket histogram
        last_value     - last recorded value
        sum            - sum of recorded values
        default        - SDK default for the instrument type
        exp_histogram  - base-2 exponential histogram

      An aggregation type can be specified using the 'aggr' keyword.  When
      specified, a metrics view is registered with the given aggregation
      strategy.  If omitted, the SDK default is used.

      For histogram instruments (hist_int), optional bucket boundaries can be
      specified using the 'bounds' keyword followed by a double-quoted string
      of space-separated numbers; values are sorted internally and duplicate
      boundaries are rejected.  A 'hist_int' instrument whose aggregation type
      is left unset defaults to histogram aggregation, whether or not bounds
      are given.

      Observable (asynchronous) and double-precision types are not supported.
      Observable instrument callbacks are invoked by the OTel SDK from an
      external background thread; HAProxy sample fetches rely on internal
      per-thread-group state and return incorrect results from a non-HAProxy
      thread.  Double-precision types are not supported because HAProxy sample
      fetches do not return double values.

      A create form registers the instrument and supplies the value to record;
      it emits no measurement on its own.  A data point is produced only when an
      update form for that instrument runs, recording the create form's value.
      An optional trailing 'if'/'unless' ACL condition gates that measurement:
      on a create form it gates every recording of the instrument, on an update
      form only that one.  Registration produces no data point and is never
      gated.

      The update form itself supplies no value, only attributes and an optional
      condition; to record a different quantity, define a separate instrument.
      To feed a computed or externally-set measurement into one instrument, set
      the create form's 'value' to a 'var(...)' fetch and assign that variable
      with 'set-var'; the instrument then records whatever the variable holds.

      For example, a request size read from a header is stored in a variable
      with 'set-var', then recorded through it:

        otel-scope request_size
            set-var txn.otel_size req.hdr(content-length)
            instrument hist_int req_bytes value var(txn.otel_size) unit "By"
            instrument update req_bytes
            otel-event on-frontend-http-request

      Observable (pull) instruments are unsupported, but the create/update split
      covers the same need when the update form runs periodically.  Bind a scope
      to the 'on-idle-timeout' event, whose timer fires on a HAProxy thread at
      the set interval, and run the gauge's update form there.  Each tick then
      records a data point from a sample fetch, evaluated on-thread, which an
      observable callback cannot do.  The example below records the frontend
      connection count every ten seconds:

        otel-scope active_connections
            idle-timeout 10s
            instrument gauge_int frontend_connections value fe_conn(otel-fe)
            instrument update frontend_connections
            otel-event on-idle-timeout

      List the scope under the instrumentation's 'scopes' directive to activate
      it.  The cadence comes from the idle streams, so at least one long-lived
      idle stream (a keep-alive or long-poll connection) must be present for the
      gauge to keep updating; under last-value aggregation, concurrent idle
      streams overwrite it with the most recent reading.

      Examples:
        instrument cnt_int  "name_cnt_int" desc "Integer Counter" value int(1),add(2) unit "unit"
        instrument hist_int "name_hist" aggr exp_histogram desc "Latency" value lat_ns_tot unit "ns"
        instrument hist_int "name_hist2" desc "Latency" value lat_ns_tot unit "ns" bounds "100 1000 10000"
        instrument cnt_int "name_cnt_local" value int(1) if { src 127.0.0.1 }
        instrument update "name_cnt_int" attr "attr_1_key" str("attr_1_value") if { src 127.0.0.1 }


  log-record <severity> [id <integer> event <name>] [time [<unit>] <sample>] [span <ref>] [attr <key> <sample>] ... <sample> ... [{ if | unless } <condition>]
      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
        time [<unit>] <sample> - override the log event timestamp at runtime
        span <ref>             - associate the log record with an open span
        attr <key> <sample>    - add an attribute evaluated at runtime (repeatable)

      The 'id' and 'event' keywords must be specified together or both omitted;
      any other combination is a configuration error.  The identifier must be
      at least 1; a zero is accepted but treated as if no 'id' was given.

      The 'time' keyword takes an optional unit and a single HAProxy sample
      expression.  The supported units are 's' (seconds, the default), 'ms'
      (milliseconds), 'us' (microseconds) and 'ns' (nanoseconds); the value
      is interpreted as a count since the Unix epoch in the given unit.  The
      sample is evaluated at runtime and must yield an integer (or a string
      convertible to one).  The value sets the event timestamp; if evaluation
      or numeric conversion fails, it defaults to the wall-clock, which is
      always recorded as the observed timestamp.  Typical sources are HAProxy's
      'date' fetch family (e.g. date, date(0,ms), date(0,us)) or a numeric value
      extracted from a header or variable.

      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
      samples are concatenated as a string.  The body may also be a HAProxy
      log-format string wrapped in '%[ ... ]'; the outer wrapper is stripped
      before parsing, and the inner string may then contain log-format aliases
      such as %ci, %t or %ft.

      Supported severity levels follow the OpenTelemetry specification:
        trace, trace2, trace3, trace4
        debug, debug2, debug3, debug4
        info,  info2,  info3,  info4
        warn,  warn2,  warn3,  warn4
        error, error2, error3, error4
        fatal, fatal2, fatal3, fatal4

      The log record is only emitted if the logger is enabled for the configured
      severity (controlled by the 'min_severity' option in the YAML logs signal
      configuration).  If a 'span' reference is given but the named span is not
      found at runtime, the log record is emitted without span correlation.

      An optional trailing 'if'/'unless' ACL condition gates the whole record;
      when it does not pass at runtime, the record is skipped entirely, with no
      body or attributes evaluated.

      Examples:
        log-record info str("heartbeat")
        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 id 1018 event "server-unavailable" str("503 Service Unavailable")
        log-record info id 1001 event "session-stop" str("stream stopped")
        log-record info id 1002 event "server-session-end" "%[%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts]"
        log-record info time us date(0,us) str("microsecond-accurate event")
        log-record info time ns hdr(x-event-time-ns) str("client-supplied timestamp")
        log-record info str("local heartbeat") if { src 127.0.0.1 }


  set-var <var-name> <sample> ... [{ if | unless } <condition>]
      Set a HAProxy variable to the string value of a sample expression or a
      log-format, evaluated when the scope's event fires.  The value grammar
      matches the 'attribute' keyword: bare sample fetches are concatenated and
      a '%[ ... ]' wrapper marks a HAProxy log-format string.  The result is
      always stored as a string.  The variable name includes its scope (proc,
      sess, txn, req or res) and is registered at parse time.  Assignments run
      before the scope's spans, so span attributes, instruments and log records
      can read the new value through 'var(...)'.

      An optional trailing 'if'/'unless' ACL condition gates the assignment.

      Examples:
        set-var txn.otel_method method
        set-var txn.otel_path str("path=") path
        set-var sess.otel_tid var(sess.otel.uuid)
        set-var txn.otel_internal str("1") if { src 127.0.0.1 }


  set-var-ctx <var-name> <ref> <field> [{ if | unless } <condition>]
      Set a HAProxy variable from a field of a referenced span or span context,
      evaluated when the scope's event fires.  <ref> names a span or a context
      extracted in this scope; it is resolved against the active spans first and
      then the extracted contexts; an unresolved reference leaves the variable
      unchanged.  Because these assignments run after the scope's spans, a span
      created in the same scope may be referenced.

      An optional trailing 'if'/'unless' ACL condition gates the assignment.

      Supported fields:
        trace-id          - trace id, lowercase hex
        span-id           - span id, lowercase hex
        trace-flags       - W3C trace-flags byte, lowercase hex
        traceparent       - full W3C "00-<trace>-<span>-<flags>" string
        tracestate        - full tracestate header (context references only)
        tracestate(<key>) - a single tracestate entry (context references only)
        baggage           - the whole baggage carrier (all entries)
        baggage(<key>)    - a single baggage entry value
        sampled           - "1" if the sampled flag is set, else "0"
        valid             - "1" if the context is valid, else "0"
        remote            - "1" if the context is remote, else "0"

      For 'baggage(<key>)', a span reference reads the value set on that span,
      while a context reference reads the named entry from the inbound carrier.
      Keyless 'baggage' returns the whole carrier instead: the inbound carrier
      for a context reference, or the serialized outbound baggage for a span
      reference.  The 'tracestate' fields are available only when <ref> names a
      context.

      The <ref> is a span or context name, quoted as in 'span', 'inject' and
      'extract'.  The example below first declares them, then references them.

      Examples:
        extract "inbound" use-headers
        span "HAProxy session" root
        set-var-ctx txn.in_traceid  "inbound" trace-id
        set-var-ctx txn.traceparent "HAProxy session" traceparent
        set-var-ctx txn.user        "inbound" baggage(userId)
        set-var-ctx txn.local_tid   "HAProxy session" trace-id if { src 127.0.0.1 }


  unset-var <var-name> ... [{ if | unless } <condition>]
      Remove one or more HAProxy variables when the scope's event fires.  A
      variable that is not currently set is silently ignored.  Removal runs
      after the scope's spans, instruments and log records, so all still read
      the variables; only 'finish' and 'otel-stop' run afterwards.

      An optional trailing 'if'/'unless' ACL condition gates the whole
      directive; when it does not pass, none of the listed variables are
      removed.

      Examples:
        unset-var txn.otel_scratch txn.otel_tmp
        unset-var txn.otel_scratch if { src 127.0.0.1 }


  acl <aclname> <criterion> [flags] [operator] <value> ...
      Declare an ACL local to this scope.

      Example:
        acl acl-test-src-ip src 127.0.0.1


  otel-event <name> [{ if | unless } <condition>]
      Bind this scope to a filter event, optionally with an ACL-based condition.
      A scope binds to a single event, so 'otel-event' may appear at most once;
      a second is a configuration error.

      The filter attaches to both HTTP-mode and TCP-mode proxies.  A TCP proxy
      carries no HTTP message and runs no HTTP analysis, so the events tagged
      '(HTTP only)' below never fire on one: a scope bound to such an event on a
      non-HTTP proxy is inert and reported as a warning at config load.  Every
      unmarked event fires on both proxy modes.

      Supported events (stream lifecycle):
        on-stream-start
        on-stream-stop
        on-idle-timeout
        on-backend-set

      Supported events (request channel):
        on-client-session-start
        on-frontend-tcp-request
        on-http-wait-request               (HTTP only)
        on-http-body-request               (HTTP only)
        on-frontend-http-request           (HTTP only)
        on-switching-rules-request
        on-backend-tcp-request
        on-backend-http-request            (HTTP only)
        on-http-tarpit-request             (HTTP only)
        on-process-server-rules-request
        on-http-process-request            (HTTP only)
        on-tcp-rdp-cookie-request
        on-process-sticking-rules-request
        on-http-headers-request            (HTTP only)
        on-http-end-request                (HTTP only)
        on-client-session-end
        on-server-unavailable

      Supported events (response channel):
        on-server-session-start
        on-tcp-response
        on-http-wait-response              (HTTP only)
        on-process-store-rules-response
        on-http-response                   (HTTP only)
        on-http-headers-response           (HTTP only)
        on-http-end-response               (HTTP only)
        on-http-reply                      (HTTP only)
        on-server-session-end

      The on-stream-start event fires from the stream_start filter callback,
      before any channel processing begins.  The on-stream-stop event fires from
      the stream_stop callback, after all channel processing ends.  No channel
      is available at that point, so context injection/extraction via HTTP
      headers cannot be used in scopes bound to these events.

      The on-idle-timeout event fires periodically when the stream has no data
      transfer activity.  It requires the 'idle-timeout' keyword to set the
      interval.  Scopes bound to this event can create heartbeat spans, record
      idle-time metrics, and emit idle-time log records.  The idle timer is
      driven by the stream task itself, so it keeps firing while an analyser
      holds the stream, such as during a tarpit timeout.  See the 'instrument'
      keyword for driving a periodic gauge from it.

      The on-backend-set event fires when a backend is assigned to the stream,
      including when the frontend and the backend are the same proxy.

      The on-http-headers-request and on-http-headers-response events fire after
      all HTTP headers have been parsed and analyzed.

      The on-http-end-request and on-http-end-response events fire when all HTTP
      data has been processed and forwarded.

      The on-http-reply event is meant to fire when HAProxy returns an internal
      reply (error page, deny response, redirect), but it never fires on any
      current HAProxy.  The http_reply filter callback has had no caller since
      HAProxy 2.2-dev8 (2020), when its HTTP error handling was reworked around
      the 'struct http_reply' mechanism; the flt_http_reply() dispatcher still
      exists but nothing ever calls it.  To trace such replies, attach the scope
      through an 'http-after-response otel-group' action instead, which runs on
      every response, including the ones that HAProxy synthesises.

      Examples:
        otel-event on-stream-start if acl-test-src-ip
        otel-event on-stream-stop
        otel-event on-client-session-start
        otel-event on-client-session-start if acl-test-src-ip
        otel-event on-http-response if !acl-http-status-ok
        otel-event on-idle-timeout


  otel-stop [{ if | unless } <condition>]
      Stop the OpenTelemetry filter for the current connection.  Every span and
      span context still open on the stream is finished, and the filter is then
      disabled for the rest of that connection, so no further scopes run for it,
      whether triggered by a later event or by an 'otel-group' action.  It is
      the per-connection counterpart to the global 'flt-otel disable' CLI
      command.

      The stop is evaluated after the rest of the scope has run, so a stopping
      scope still emits the spans, log records and metrics it defines first.

      Without a condition the stop always fires.  An optional 'if' or 'unless'
      condition (same syntax as 'otel-event') makes the stop conditional.

      Examples:
        otel-stop
        otel-stop if { path_beg /health }


  idle-timeout <time>
      Set the idle timeout interval for a scope bound to the 'on-idle-timeout'
      event.  The timer fires periodically at the given interval when the stream
      has no data transfer activity.  This keyword is mandatory for scopes using
      the 'on-idle-timeout' event and cannot be used with any other event.  Only
      one 'idle-timeout' may be defined per scope; a second is a configuration
      error.

      The <time> argument accepts the standard HAProxy time format: a number
      followed by a unit suffix (us, ms, s, m, h, d).  A value of zero is not
      permitted.

      Example:
        scopes on_idle_timeout
        ..
        otel-scope on_idle_timeout
            idle-timeout 5s
            span "heartbeat" root
                attribute "idle.elapsed" str("idle-check")
            instrument cnt_int "idle.count" value int(1)
            log-record info str("heartbeat")
            otel-event on-idle-timeout


3.4. "otel-group" section
---------------------------

An "otel-group" section defines a named collection of scopes that can be
triggered from HAProxy TCP/HTTP rules rather than from filter events.

Syntax:

  otel-group <name>

Keywords:

  scopes <name> ...
      List the "otel-scope" sections that belong to this group.  Multiple names
      can be given on one line.  Scopes that are used only in groups do not need
      to define an 'otel-event'.

Example (from test/sa/otel.cfg):

  otel-group http_response_group
      scopes http_response_1
      scopes http_response_2

  otel-scope http_response_1
      span "HTTP response"
          event "event_content" "hdr.content" res.hdr("content-type") str("; length: ") res.hdr("content-length") str(" bytes")

  otel-scope http_response_2
      span "HTTP response"
          event "event_date" time ms date(0,ms) "hdr.date" res.hdr("date") str(" / ") res.hdr("last-modified")


4. YAML configuration file
----------------------------

The YAML configuration file defines the OpenTelemetry SDK pipeline.  It is
referenced by the 'config' keyword in the "otel-instrumentation" section.
It contains the following top-level sections: exporters, samplers, processors,
readers, providers and signals.


4.1. Exporters
---------------

Each exporter has a user-chosen name and a 'type' that determines which
additional options are available.  Options marked with (*) are required.

The 'cpu_id' option, accepted by the OTLP exporters, the batch processors and
the readers, pins that component's background thread to the given CPU.  The
default of -1 leaves the thread unpinned.

Supported types:

  otlp_grpc - Export via OTLP over gRPC.
    type (*)                          "otlp_grpc"
    thread_name                       exporter thread name (string)
    cpu_id                            exporter thread CPU affinity (integer)
    endpoint                          OTLP/gRPC endpoint URL (string)
    use_ssl_credentials               enable SSL channel credentials (boolean)
    ssl_credentials_cacert_path       CA certificate file path (string)
    ssl_credentials_cacert_as_string  CA certificate as inline string (string)
    ssl_client_key_path               client private key file path (string)
    ssl_client_key_string             client private key as inline string (string)
    ssl_client_cert_path              client certificate file path (string)
    ssl_client_cert_string            client certificate as inline string (string)
    timeout                           export timeout in seconds (integer)
    user_agent                        User-Agent header value (string)
    max_threads                       maximum exporter threads (integer)
    compression                       compression algorithm name (string)
    max_concurrent_requests           concurrent request limit (integer)


  otlp_http - Export via OTLP over HTTP (JSON or Protobuf).
    type (*)                        "otlp_http"
    thread_name                     exporter thread name (string)
    cpu_id                          exporter thread CPU affinity (integer)
    endpoint                        OTLP/HTTP endpoint URL (string)
    content_type                    payload format: "json" or "protobuf"
    json_bytes_mapping              binary encoding: "hexid", "utf8" or "base64"
    use_json_name                   use protobuf JSON field names (boolean)
    debug                           enable debug output (boolean)
    timeout                         export timeout in seconds (integer)
    http_headers                    custom HTTP headers (list of key: value)
    max_concurrent_requests         concurrent request limit (integer)
    max_requests_per_connection     request limit per connection (integer)
    background_thread_wait_for      idle timeout for the HTTP background thread
                                    in milliseconds; 0 means the thread never
                                    exits on its own (integer, default: 0).  If
                                    this option is set, 'insecure-fork-wanted'
                                    must be used in the HAProxy configuration,
                                    otherwise HAProxy may crash while exporting
                                    OTel data
    ssl_insecure_skip_verify        skip TLS certificate verification (boolean)
    ssl_ca_cert_path                CA certificate file path (string)
    ssl_ca_cert_string              CA certificate as inline string (string)
    ssl_client_key_path             client private key file path (string)
    ssl_client_key_string           client private key as inline string (string)
    ssl_client_cert_path            client certificate file path (string)
    ssl_client_cert_string          client certificate as inline string (string)
    ssl_min_tls                     minimum TLS version (string)
    ssl_max_tls                     maximum TLS version (string)
    ssl_cipher                      TLS cipher list (string)
    ssl_cipher_suite                TLS 1.3 cipher suite list (string)
    compression                     compression algorithm name (string)


  otlp_file - Export to local files in OTLP format.
    type (*)                        "otlp_file"
    thread_name                     exporter thread name (string)
    cpu_id                          exporter thread CPU affinity (integer)
    file_pattern                    output filename pattern (string)
    alias_pattern                   symlink pattern for latest file (string)
    flush_interval                  flush interval in microseconds (integer)
    flush_count                     spans per flush (integer)
    file_size                       maximum file size in bytes (integer)
    rotate_size                     number of rotated files to keep (integer)


  ostream - Write to a file (text output, useful for debugging).
    type (*)                        "ostream"
    filename                        output file path (string)


  memory - In-memory buffer (useful for testing).
    type (*)                        "memory"
    buffer_size                     maximum buffered items (integer)


  zipkin - Export to Zipkin-compatible backends.
    type (*)                        "zipkin"
    endpoint                        Zipkin collector URL (string)
    format                          payload format: "json" or "protobuf"
    service_name                    service name reported to Zipkin (string)
    ipv4                            service IPv4 address (string)
    ipv6                            service IPv6 address (string)


  elasticsearch - Export to Elasticsearch.
    type (*)                        "elasticsearch"
    host                            Elasticsearch hostname (string)
    port                            Elasticsearch port (integer)
    index                           Elasticsearch index name (string)
    response_timeout                response timeout in seconds (integer)
    debug                           enable debug output (boolean)
    http_headers                    custom HTTP headers (list of key: value)


4.2. Samplers
--------------

Samplers control which traces are recorded.  Each sampler has a user-chosen
name and a 'type' that determines its behavior.

Supported types:

  always_on - Sample every trace.
    type (*)                        "always_on"


  always_off - Sample no traces.
    type (*)                        "always_off"


  trace_id_ratio_based - Sample a fraction of traces.
    type (*)                        "trace_id_ratio_based"
    ratio (*)                       sampling ratio, 0.0 to 1.0 (float)


  parent_based - Inherit sampling decision from parent span.
    type (*)                        "parent_based"
    delegate                        root-span sampler: "always_on" (default),
                                    "always_off" or "trace_id_ratio_based"
                                    (the latter reads 'ratio' as above)
    remote_sampled                  sampler for a sampled remote parent:
                                    "always_on" (default) or "always_off"
    remote_not_sampled              sampler for a not-sampled remote parent:
                                    "always_on" or "always_off" (default)
    local_sampled                   sampler for a sampled local parent:
                                    "always_on" (default) or "always_off"
    local_not_sampled               sampler for a not-sampled local parent:
                                    "always_on" or "always_off" (default)


4.3. Processors
----------------

Processors define how telemetry data is handled before export.  Each
processor has a user-chosen name and a 'type' that determines its behavior.

Supported types:

  batch - Batch spans before exporting.
    type (*)                        "batch"
    thread_name                     processor thread name (string)
    cpu_id                          processor thread CPU affinity (integer)
    max_queue_size                  maximum queued spans (integer)
    schedule_delay                  export interval in milliseconds (integer)
    export_timeout                  export timeout in milliseconds (integer)
    max_export_batch_size           maximum spans per export call (integer)

    When the queue reaches half capacity, a preemptive notification triggers
    an early export.  The 'max_queue_size' value must not be lower than the
    'max_export_batch_size' value, and 'export_timeout' is accepted but not
    yet used by the OpenTelemetry C++ SDK batch processor.

  single - Export each span individually (no batching).
    type (*)                        "single"


4.4. Readers
-------------

Readers define how metrics are collected and exported.  Each reader has a
user-chosen name.

    thread_name                     reader thread name (string)
    cpu_id                          reader thread CPU affinity (integer)
    export_interval                 collection interval in milliseconds (integer)
    export_timeout                  export timeout in milliseconds (integer)

The 'export_timeout' value must not exceed the 'export_interval' value.


4.5. Providers
---------------

Providers define resource attributes attached to all telemetry data.  Each
provider has a user-chosen name.

    resources                       key-value resource attributes (list)

Standard resource attribute keys include service.name, service.version,
service.instance.id and service.namespace.


4.6. Signals
-------------

Signals bind exporters, samplers, processors, readers and providers together
for each telemetry type.  The supported signal names are "traces", "metrics"
and "logs".

Under each signal name, the bindings are grouped into one or more named signals
contexts; the 'config' keyword of the "otel-instrumentation" section selects
the context that serves the filter, and the name 'default' is used when the
keyword names no context:

    traces:
      default:
        scope_name: ...

The flat layout without the context level, written for the wrapper 2.x releases,
is still accepted.

    scope_name (*)                  instrumentation scope name (string)
    exporters                       exporter name reference (string or list)
    samplers                        sampler name reference (string, traces only)
    processors                      processor name reference (string or list,
                                    traces/logs)
    readers                         reader name reference (string, metrics only)
    providers                       provider name reference (string)
    min_severity                    minimum log severity level (string, logs only)

For the traces and logs signals, 'processors' may be a YAML sequence to build
several pipelines: each processor is paired with the exporter at the same
position in the 'exporters' sequence, and when the exporter list is shorter
its last entry is reused for the remaining processors.

The "min_severity" option controls which log records are emitted.  Only log
records whose severity is equal to or higher than the configured minimum are
passed to the exporter.  The value is a severity name as listed under the
"log-record" keyword (e.g. "trace", "debug", "info", "warn", "error", "fatal").
If omitted, the logger accepts all severity levels.


5. HAProxy rule integration
----------------------------

Groups defined in the OTel configuration file can be triggered from HAProxy
TCP/HTTP rules using the 'otel-group' action keyword:

  http-request         otel-group <filter-id> <group> [condition]
  http-response        otel-group <filter-id> <group> [condition]
  http-after-response  otel-group <filter-id> <group> [condition]
  tcp-request content  otel-group <filter-id> <group> [condition]
  tcp-response content otel-group <filter-id> <group> [condition]

This allows running specific groups of scopes based on ACL conditions defined
in the HAProxy configuration.

Example (from test/sa/haproxy.cfg):

  acl acl-http-status-ok status 100:399

  filter opentelemetry id otel-test-sa config sa/otel.cfg

  # Run response scopes for successful responses
  http-response otel-group otel-test-sa http_response_group if acl-http-status-ok

  # Run after-response scopes for error responses
  http-after-response otel-group otel-test-sa http_after_response_group if !acl-http-status-ok

The filter also registers a sample fetch, usable in any HAProxy ACL or rule,
that reports whether a usable span context was extracted on the current stream
by the 'extract' keyword:

  otel.context(<name>) : boolean

It returns true when the context named <name> was extracted and is valid.  When
no such context was extracted the fetch produces no sample, so a '-m found'
match reads as false; this tells "a context arrived" apart from "none arrived"
without first copying a context field into a variable with 'set-var-ctx'.

Example:

  acl otel-ctx-valid otel.context(otel-ctx)
  acl otel-ctx-seen  otel.context(otel-ctx) -m found

  # honour the upstream trace decision rather than re-deriving it
  http-request set-header X-Trace-Continued 1 if otel-ctx-valid

The filter also exposes the raw payload byte counts accumulated on the stream:

  otel.bytes_in  : integer
  otel.bytes_out : integer

'otel.bytes_in' counts the payload forwarded on the request channel (client to
HAProxy) and 'otel.bytes_out' the payload on the response channel (HAProxy to
client).  Both grow as data is forwarded, so they are best read from a close
scope -- on-client-session-end or on-server-session-end -- where they hold the
final connection totals.  They are populated in TCP mode; on an HTTP proxy they
read 0 because an HTX buffer carries protocol framing rather than raw payload.

Example (record the transferred volume on a TCP connection span):

  otel-scope client_session_end
      span "TCP session"
          attribute "tcp.bytes_in"  otel.bytes_in
          attribute "tcp.bytes_out" otel.bytes_out
      finish *
      otel-event on-client-session-end


6. Complete examples
---------------------

The test directory contains several complete example configurations.  Each
subdirectory contains an OTel configuration file (otel.cfg), a YAML file
(otel.yml) and a HAProxy configuration file (haproxy.cfg).


6.1. Standalone example (sa)
------------------------------

The most comprehensive single-instance example: most of the events are used
(the test/README-sa file lists the exact coverage), with spans, attributes,
events, links, baggage, status, metrics and groups demonstrated.

--- test/sa/otel.cfg (excerpt) -----------------------------------------

[otel-test-sa]
    otel-instrumentation otel-test-instrumentation
        config sa/otel.yml
        option dontlog-normal
        option hard-errors
        no option disabled
        rate-limit 100.0

        groups http_response_group
        groups http_after_response_group

        scopes skip_health_path
        scopes on_stream_start
        scopes on_stream_stop
        scopes on_idle_timeout
        scopes client_session_start
        scopes frontend_tcp_request
        ...
        scopes server_session_end

    otel-group http_response_group
        scopes http_response_1
        scopes http_response_2

    otel-scope http_response_1
        span "HTTP response"
            event "event_content" "hdr.content" res.hdr("content-type") str("; length: ") res.hdr("content-length") str(" bytes")

    otel-scope on_stream_start
        instrument udcnt_int "haproxy.sessions.active" desc "Active sessions" value int(1) unit "{session}"
        span "HAProxy session" root
            baggage "haproxy_id" var(sess.otel.uuid)
            event "event_ip" "src" src str(":") src_port
        acl acl-test-src-ip src 127.0.0.1
        otel-event on-stream-start if acl-test-src-ip

    otel-scope on_stream_stop
        finish *
        otel-event on-stream-stop

    otel-scope client_session_start
        span "Client session" parent "HAProxy session"
        otel-event on-client-session-start

    otel-scope frontend_http_request
        span "Frontend HTTP request" parent "HTTP body request" link "HAProxy session" kind server
            attribute "http.method" method
            attribute "http.url" url
            attribute "http.version" str("HTTP/") req.ver
        finish "HTTP body request"
        otel-event on-frontend-http-request

    otel-scope server_session_start
        span "Server session" parent "HAProxy session" kind client
        link "HAProxy session" "Client session"
        finish "Process sticking rules request"
        otel-event on-server-session-start

    otel-scope server_session_end
        finish "*res*"
        otel-event on-server-session-end

---------------------------------------------------------------------


6.2. Frontend / backend example (fe/be)
-----------------------------------------

Demonstrates distributed tracing across two cascaded HAProxy instances using
inject/extract to propagate the span context via HTTP headers.

The frontend HAProxy (test/fe) creates the root trace and injects context:

--- test/fe/otel.cfg (excerpt) -----------------------------------------

    otel-scope backend_http_request
        span "Backend HTTP request" parent "Backend TCP request"
        finish "Backend TCP request"
        span "HAProxy session"
            inject "otel-ctx" use-headers
        otel-event on-backend-http-request

---------------------------------------------------------------------

The backend HAProxy (test/be) extracts the context and continues the trace:

--- test/be/otel.cfg (excerpt) -----------------------------------------

    otel-scope frontend_http_request
        extract "otel-ctx" use-headers
        span "HAProxy session" parent "otel-ctx" root
            baggage "haproxy_id" var(sess.otel.uuid)
        span "Client session" parent "HAProxy session"
        span "Frontend HTTP request" parent "Client session"
            attribute "http.method" method
            attribute "http.url" url
            attribute "http.version" str("HTTP/") req.ver
        otel-event on-frontend-http-request

---------------------------------------------------------------------


6.3. Context propagation example (ctx)
----------------------------------------

Similar to 'sa', but spans are opened using extracted span contexts as parent
references instead of direct span names.  This demonstrates the inject/extract
mechanism using HAProxy variables.

--- test/ctx/otel.cfg (excerpt) ----------------------------------------

    otel-scope client_session_start_1
        span "HAProxy session" root
            inject "otel_ctx_1" use-headers use-vars
            baggage "haproxy_id" var(sess.otel.uuid)
        otel-event on-client-session-start

    otel-scope client_session_start_2
        extract "otel_ctx_1" use-vars
        span "Client session" parent "otel_ctx_1"
            inject "otel_ctx_2" use-headers use-vars
        otel-event on-client-session-start

    otel-scope frontend_tcp_request
        extract "otel_ctx_2" use-vars
        span "Frontend TCP request" parent "otel_ctx_2"
            inject "otel_ctx_3" use-headers use-vars
        otel-event on-frontend-tcp-request

    otel-scope http_wait_request
        extract "otel_ctx_3" use-vars
        span "HTTP wait request" parent "otel_ctx_3"
        finish "Frontend TCP request" "otel_ctx_3"
        otel-event on-http-wait-request

---------------------------------------------------------------------


6.4. TCP-mode example (tcp)
-----------------------------

Runs the filter on a 'mode tcp' proxy.  With no HTTP message to inspect, it
traces the raw stream: a single "TCP session" span covers the whole connection,
each TCP-phase event records a timestamped span event onto it, and the forwarded
payload is counted through the 'otel.bytes_in' and 'otel.bytes_out' fetches.
The configuration exercises every event that fires on a TCP-mode proxy; the
HTTP-only events are left out, since they never fire there.

--- test/tcp/otel.cfg (excerpt) ----------------------------------------

    otel-scope stream_start
        span "TCP session" root
            attribute "client.addr"   src str(":") src_port
            attribute "frontend.addr" dst str(":") dst_port
            event "stream-start" "fired" int(1)
        otel-event on-stream-start

    otel-scope client_session_end
        span "TCP session"
            attribute "tcp.bytes_in"  otel.bytes_in
            attribute "tcp.bytes_out" otel.bytes_out
            event "client-session-end" "fired" int(1)
        otel-event on-client-session-end

    otel-scope stream_stop
        span "TCP session"
            event "stream-stop" "fired" int(1)
        finish *
        otel-event on-stream-stop

---------------------------------------------------------------------


6.5. Comparison example (cmp)
-------------------------------

A configuration made for comparison purposes with other tracing implementations.
It uses a simplified span hierarchy without context propagation.

--- test/cmp/otel.cfg (excerpt) ----------------------------------------

    otel-scope client_session_start
        span "HAProxy session" root
            baggage "haproxy_id" var(sess.otel.uuid)
        span "Client session" parent "HAProxy session"
        otel-event on-client-session-start

    otel-scope http_response-error
        span "HTTP response"
            status "error" str("!acl-http-status-ok")
        otel-event on-http-response if !acl-http-status-ok

    otel-scope server_session_end
        finish "HTTP response" "Server session"
        otel-event on-http-response

    otel-scope client_session_end
        finish "*"
        otel-event on-http-response

---------------------------------------------------------------------


6.6. Up-down counter example (updown)
---------------------------------------

An up-down counter (udcnt_int) that rises on one event and falls on another:
the 'haproxy.sessions.active' instrument tracks how many client sessions are
currently open, +1 when a session starts and -1 when it ends.  The create form
supplies a single value expression, so the per-event sign is carried through a
session variable: 'set-var' stores +1 in the session_start scope and -1 in the
session_end scope, and the instrument reads the variable back when each 'update'
form records the measurement.  A second counter tallies the total number of
sessions started.  The metrics are written by an ostream exporter and read from
its output file after a clean shutdown.

--- test/updown/otel.cfg (excerpt) -------------------------------------

    otel-scope session_start
        instrument udcnt_int "haproxy.sessions.total" desc "Total client sessions" value int(1) unit "{session}"
        instrument update "haproxy.sessions.total"
        set-var sess.otel_updown_delta int(1)
        instrument udcnt_int "haproxy.sessions.active" desc "Active client sessions" value var(sess.otel_updown_delta) unit "{session}"
        instrument update "haproxy.sessions.active"
        otel-event on-client-session-start

    otel-scope session_end
        set-var sess.otel_updown_delta int(-1)
        instrument update "haproxy.sessions.active"
        otel-event on-client-session-end

---------------------------------------------------------------------


6.7. Error-logging example (err)
----------------------------------

A configuration that drives the filter's runtime error reporting on ordinary
traffic.  The response scope parents its span under 'orphan span', which only
the on-server-unavailable scope creates; that event does not fire while the
backend is alive, so the parent is absent from the runtime context and span
creation fails on every response.  In the default soft-errors mode the error is
swallowed at warning level and the request completes; switching the instance to
hard-errors mode logs at error level and disables the filter for the offending
stream.  The log lines are edge-triggered and rate-limited; the error counters
are tracked over the CLI with 'flt-otel status'.

--- test/err/otel.cfg (excerpt) ----------------------------------------

    otel-scope session_start
        span "HAProxy session" root
        otel-event on-client-session-start

    otel-scope server_unavailable
        span "orphan span" parent "HAProxy session"
        otel-event on-server-unavailable

    otel-scope http_response
        span "HTTP response" parent "orphan span"
            attribute "http.status_code" status
        otel-event on-http-response

---------------------------------------------------------------------


6.8. Empty / minimal example (empty)
--------------------------------------

The minimal valid OTel configuration.  The filter is initialized but no events
are triggered:

--- test/empty/otel.cfg -------------------------------------------------

  otel-instrumentation otel-test-instrumentation
      config empty/otel.yml

---------------------------------------------------------------------

This is useful for testing the OTel filter initialization behavior without any
actual telemetry processing.
