]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: otel: test: added test and benchmark suite for the OTel filter
authorMiroslav Zagorac <mzagorac@haproxy.com>
Tue, 27 Jan 2026 12:02:59 +0000 (13:02 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 13 Apr 2026 07:23:26 +0000 (09:23 +0200)
Added a test suite under addons/otel/test/ for the OpenTelemetry filter.
Five scenarios exercise different filter capabilities: standalone (sa)
covers all hook points including idle-timeout heartbeats, metrics and log
records; compact (cmp) covers the full request/response lifecycle with
ACL-based error handling; context (ctx) tests explicit inject/extract
propagation through numbered context variables; frontend/backend (fe/be)
tests distributed tracing across two HAProxy instances; and empty tests
bare filter initialisation with no active scopes.

A performance benchmarking script (test-speed.sh) uses wrk to measure
throughput and latency at different rate-limit settings (100% through 0%,
disabled, and filter-off).  Each scenario includes comprehensive YAML
exporter definitions covering OTLP file/gRPC/HTTP, ostream, memory,
Zipkin, and Elasticsearch backends.

26 files changed:
addons/otel/test/be/haproxy.cfg [new file with mode: 0644]
addons/otel/test/be/otel.cfg [new file with mode: 0644]
addons/otel/test/be/otel.yml [new file with mode: 0644]
addons/otel/test/cmp/haproxy.cfg [new file with mode: 0644]
addons/otel/test/cmp/otel.cfg [new file with mode: 0644]
addons/otel/test/cmp/otel.yml [new file with mode: 0644]
addons/otel/test/copy-yml.sh [new file with mode: 0755]
addons/otel/test/ctx/haproxy.cfg [new file with mode: 0644]
addons/otel/test/ctx/otel.cfg [new file with mode: 0644]
addons/otel/test/ctx/otel.yml [new file with mode: 0644]
addons/otel/test/empty/haproxy.cfg [new file with mode: 0644]
addons/otel/test/empty/otel.cfg [new file with mode: 0644]
addons/otel/test/empty/otel.yml [new file with mode: 0644]
addons/otel/test/fe/haproxy.cfg [new file with mode: 0644]
addons/otel/test/fe/otel.cfg [new file with mode: 0644]
addons/otel/test/fe/otel.yml [new file with mode: 0644]
addons/otel/test/haproxy-common.cfg [new file with mode: 0644]
addons/otel/test/index.html [new file with mode: 0644]
addons/otel/test/run-cmp.sh [new file with mode: 0755]
addons/otel/test/run-ctx.sh [new file with mode: 0755]
addons/otel/test/run-fe-be.sh [new file with mode: 0755]
addons/otel/test/run-sa.sh [new file with mode: 0755]
addons/otel/test/sa/haproxy.cfg [new file with mode: 0644]
addons/otel/test/sa/otel.cfg [new file with mode: 0644]
addons/otel/test/sa/otel.yml [new file with mode: 0644]
addons/otel/test/test-speed.sh [new file with mode: 0755]

diff --git a/addons/otel/test/be/haproxy.cfg b/addons/otel/test/be/haproxy.cfg
new file mode 100644 (file)
index 0000000..3860f9f
--- /dev/null
@@ -0,0 +1,19 @@
+global
+    stats socket /tmp/haproxy-be.sock mode 666 level admin
+
+listen stats
+    mode http
+    bind *:8002
+    stats uri /
+    stats admin if TRUE
+    stats refresh 10s
+
+frontend otel-test-be-frontend
+    bind *:11080
+    default_backend servers-backend
+
+    # OTel filter
+    filter opentelemetry id otel-test-be config be/otel.cfg
+
+backend servers-backend
+    server server-1 127.0.0.1:8000
diff --git a/addons/otel/test/be/otel.cfg b/addons/otel/test/be/otel.cfg
new file mode 100644 (file)
index 0000000..1f26f46
--- /dev/null
@@ -0,0 +1,61 @@
+[otel-test-be]
+    otel-instrumentation otel-test-instrumentation
+        config be/otel.yml
+#       log localhost:514 local7 debug
+        option dontlog-normal
+        option hard-errors
+        no option disabled
+
+        scopes frontend_http_request
+        scopes backend_tcp_request
+        scopes backend_http_request
+        scopes client_session_end
+
+        scopes server_session_start
+        scopes tcp_response
+        scopes http_response
+        scopes server_session_end
+
+    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
+
+    otel-scope backend_tcp_request
+        span "Backend TCP request" parent "Frontend HTTP request"
+        finish "Frontend HTTP request"
+        otel-event on-backend-tcp-request
+
+    otel-scope backend_http_request
+        span "Backend HTTP request" parent "Backend TCP request"
+        finish "Backend TCP request"
+        otel-event on-backend-http-request
+
+    otel-scope client_session_end
+        finish "Client session"
+        otel-event on-client-session-end
+
+    otel-scope server_session_start
+        span "Server session" parent "HAProxy session"
+        finish "Backend HTTP request"
+        otel-event on-server-session-start
+
+    otel-scope tcp_response
+        span "TCP response" parent "Server session"
+        otel-event on-tcp-response
+
+    otel-scope http_response
+        span "HTTP response" parent "TCP response"
+            attribute "http.status_code" status
+        finish "TCP response"
+        otel-event on-http-response
+
+    otel-scope server_session_end
+        finish *
+        otel-event on-server-session-end
diff --git a/addons/otel/test/be/otel.yml b/addons/otel/test/be/otel.yml
new file mode 100644 (file)
index 0000000..374b871
--- /dev/null
@@ -0,0 +1,246 @@
+exporters:
+  exporter_traces_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file trace"
+    file_pattern:   "__be_traces_log-%F-%N"
+    alias_pattern:  "__traces_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_traces_otlp_grpc:
+    type:                             otlp_grpc
+    thread_name:                      "OTLP/gRPC trace"
+    endpoint:                         "http://localhost:4317/v1/traces"
+    use_ssl_credentials:              false
+#   ssl_credentials_cacert_path:      ""
+#   ssl_credentials_cacert_as_string: ""
+#   ssl_client_key_path:              ""
+#   ssl_client_key_string:            ""
+#   ssl_client_cert_path:             ""
+#   ssl_client_cert_string:           ""
+#   timeout:                          10
+#   user_agent:                       ""
+#   max_threads:                      0
+#   compression:                      ""
+#   max_concurrent_requests:          0
+
+  exporter_traces_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP trace"
+    endpoint:                    "http://localhost:4318/v1/traces"
+    content_type:                json
+    json_bytes_mapping:          hexid
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP traces test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP traces test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+#   ssl_ca_cert_path:            ""
+#   ssl_ca_cert_string:          ""
+#   ssl_client_key_path:         ""
+#   ssl_client_key_string:       ""
+#   ssl_client_cert_path:        ""
+#   ssl_client_cert_string:      ""
+#   ssl_min_tls:                 ""
+#   ssl_max_tls:                 ""
+#   ssl_cipher:                  ""
+#   ssl_cipher_suite:            ""
+#   compression:                 ""
+
+  exporter_traces_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_traces_ostream:
+    type:     ostream
+    filename: __be_traces
+
+  exporter_traces_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_traces_zipkin:
+    type:           zipkin
+    endpoint:       "http://localhost:9411/api/v2/spans"
+    format:         json
+    service_name:   "zipkin-service"
+#   ipv4:           ""
+#   ipv6:           ""
+
+  exporter_metrics_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file metr"
+    file_pattern:   "__be_metrics_log-%F-%N"
+    alias_pattern:  "__metrics_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_metrics_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC metr"
+    endpoint:            "http://localhost:4317/v1/metrics"
+    use_ssl_credentials: false
+
+  exporter_metrics_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP metr"
+    endpoint:                    "http://localhost:4318/v1/metrics"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP metrics test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP metrics test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_metrics_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_metrics_ostream:
+    type:     ostream
+    filename: __be_metrics
+
+  exporter_metrics_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_logs_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file logs"
+    file_pattern:   "__be_logs_log-%F-%N"
+    alias_pattern:  "__logs_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_logs_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC logs"
+    endpoint:            "http://localhost:4317/v1/logs"
+    use_ssl_credentials: false
+
+  exporter_logs_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP logs"
+    endpoint:                    "http://localhost:4318/v1/logs"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP logs test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP logs test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_logs_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_logs_ostream:
+    type:     ostream
+    filename: __be_logs
+
+  exporter_logs_elasticsearch:
+    type:             elasticsearch
+    host:             localhost
+    port:             9200
+    index:            logs
+    response_timeout: 30
+    debug:            false
+    http_headers:
+      - X-OTel-Header-1: "Elasticsearch logs test header #1"
+      - X-OTel-Header-2: "Elasticsearch logs test header #2"
+
+readers:
+  reader_metrics:
+    thread_name:     "reader metr"
+    export_interval: 10000
+    export_timeout:  5000
+
+samplers:
+  sampler_traces:
+#   type:  always_on
+#   type:  always_off
+#   type:  trace_id_ratio_based
+#   ratio: 1.0
+    type:     parent_based
+    delegate: always_on
+
+processors:
+  processor_traces_batch:
+    type:                  batch
+    thread_name:           "proc/batch trac"
+    # Note: when the queue is half full, a preemptive notification is sent
+    # to start the export call.
+    max_queue_size:        2048
+    # Time interval (in ms) between two consecutive exports
+    schedule_delay:        5000
+    # Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
+    max_export_batch_size: 512
+
+  processor_traces_single:
+    type:                  single
+
+  processor_logs_batch:
+    type:                  batch
+    thread_name:           "proc/batch logs"
+    max_queue_size:        2048
+    schedule_delay:        5000
+    max_export_batch_size: 512
+
+  processor_logs_single:
+    type:                  single
+
+providers:
+  provider_traces:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-be"
+      - service.name:        "be"
+      - service.namespace:   "HAProxy traces test"
+
+  provider_metrics:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-be"
+      - service.name:        "be"
+      - service.namespace:   "HAProxy metrics test"
+
+  provider_logs:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-be"
+      - service.name:        "be"
+      - service.namespace:   "HAProxy logs test"
+
+signals:
+  traces:
+    scope_name: "HAProxy OTEL - traces"
+    exporters:  exporter_traces_otlp_http
+    samplers:   sampler_traces
+    processors: processor_traces_batch
+    providers:  provider_traces
+
+  metrics:
+    scope_name: "HAProxy OTEL - metrics"
+    exporters:  exporter_metrics_otlp_http
+    readers:    reader_metrics
+    providers:  provider_metrics
+
+  logs:
+    scope_name: "HAProxy OTEL - logs"
+    exporters:  exporter_logs_otlp_http
+    processors: processor_logs_batch
+    providers:  provider_logs
diff --git a/addons/otel/test/cmp/haproxy.cfg b/addons/otel/test/cmp/haproxy.cfg
new file mode 100644 (file)
index 0000000..c2b1b0c
--- /dev/null
@@ -0,0 +1,22 @@
+global
+    stats socket /tmp/haproxy.sock mode 666 level admin
+
+listen stats
+    mode http
+    bind *:8001
+    stats uri /
+    stats admin if TRUE
+    stats refresh 10s
+
+frontend otel-test-cmp-frontend
+    bind *:10080
+    default_backend servers-backend
+
+    # ACL used to distinguish successful from error responses
+    acl acl-http-status-ok status 100:399
+
+    # OTel filter
+    filter opentelemetry id otel-test-cmp config cmp/otel.cfg
+
+backend servers-backend
+    server server-1 127.0.0.1:8000
diff --git a/addons/otel/test/cmp/otel.cfg b/addons/otel/test/cmp/otel.cfg
new file mode 100644 (file)
index 0000000..582ff68
--- /dev/null
@@ -0,0 +1,81 @@
+[otel-test-cmp]
+    otel-instrumentation otel-test-instrumentation
+        config cmp/otel.yml
+#       log localhost:514 local7 debug
+        option dontlog-normal
+        option hard-errors
+        no option disabled
+        rate-limit 100.0
+
+        scopes client_session_start
+        scopes frontend_tcp_request
+        scopes frontend_http_request
+        scopes backend_tcp_request
+        scopes backend_http_request
+        scopes server_unavailable
+
+        scopes server_session_start
+        scopes tcp_response
+        scopes http_response http_response-error server_session_end client_session_end
+
+    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 frontend_tcp_request
+        span "Frontend TCP request" parent "Client session"
+        otel-event on-frontend-tcp-request
+
+    otel-scope frontend_http_request
+        span "Frontend HTTP request" parent "Frontend TCP request"
+            attribute "http.method" method
+            attribute "http.url" url
+            attribute "http.version" str("HTTP/") req.ver
+        finish "Frontend TCP request"
+        otel-event on-frontend-http-request
+
+    otel-scope backend_tcp_request
+        span "Backend TCP request" parent "Frontend HTTP request"
+        finish "Frontend HTTP request"
+        otel-event on-backend-tcp-request
+
+    otel-scope backend_http_request
+        span "Backend HTTP request" parent "Backend TCP request"
+        finish "Backend TCP request"
+        otel-event on-backend-http-request
+
+    otel-scope server_unavailable
+        span "HAProxy session"
+            status "error" str("503 Service Unavailable")
+        finish *
+        otel-event on-server-unavailable
+
+    otel-scope server_session_start
+        span "Server session" parent "HAProxy session"
+        finish "Backend HTTP request"
+        otel-event on-server-session-start
+
+    otel-scope tcp_response
+        span "TCP response" parent "Server session"
+        otel-event on-tcp-response
+
+    otel-scope http_response
+        span "HTTP response" parent "TCP response"
+            attribute "http.status_code" status
+        finish "TCP response"
+        otel-event on-http-response
+
+    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
diff --git a/addons/otel/test/cmp/otel.yml b/addons/otel/test/cmp/otel.yml
new file mode 100644 (file)
index 0000000..2cd1ea6
--- /dev/null
@@ -0,0 +1,246 @@
+exporters:
+  exporter_traces_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file trace"
+    file_pattern:   "__cmp_traces_log-%F-%N"
+    alias_pattern:  "__traces_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_traces_otlp_grpc:
+    type:                             otlp_grpc
+    thread_name:                      "OTLP/gRPC trace"
+    endpoint:                         "http://localhost:4317/v1/traces"
+    use_ssl_credentials:              false
+#   ssl_credentials_cacert_path:      ""
+#   ssl_credentials_cacert_as_string: ""
+#   ssl_client_key_path:              ""
+#   ssl_client_key_string:            ""
+#   ssl_client_cert_path:             ""
+#   ssl_client_cert_string:           ""
+#   timeout:                          10
+#   user_agent:                       ""
+#   max_threads:                      0
+#   compression:                      ""
+#   max_concurrent_requests:          0
+
+  exporter_traces_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP trace"
+    endpoint:                    "http://localhost:4318/v1/traces"
+    content_type:                json
+    json_bytes_mapping:          hexid
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP traces test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP traces test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+#   ssl_ca_cert_path:            ""
+#   ssl_ca_cert_string:          ""
+#   ssl_client_key_path:         ""
+#   ssl_client_key_string:       ""
+#   ssl_client_cert_path:        ""
+#   ssl_client_cert_string:      ""
+#   ssl_min_tls:                 ""
+#   ssl_max_tls:                 ""
+#   ssl_cipher:                  ""
+#   ssl_cipher_suite:            ""
+#   compression:                 ""
+
+  exporter_traces_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_traces_ostream:
+    type:     ostream
+    filename: __cmp_traces
+
+  exporter_traces_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_traces_zipkin:
+    type:           zipkin
+    endpoint:       "http://localhost:9411/api/v2/spans"
+    format:         json
+    service_name:   "zipkin-service"
+#   ipv4:           ""
+#   ipv6:           ""
+
+  exporter_metrics_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file metr"
+    file_pattern:   "__cmp_metrics_log-%F-%N"
+    alias_pattern:  "__metrics_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_metrics_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC metr"
+    endpoint:            "http://localhost:4317/v1/metrics"
+    use_ssl_credentials: false
+
+  exporter_metrics_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP metr"
+    endpoint:                    "http://localhost:4318/v1/metrics"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP metrics test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP metrics test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_metrics_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_metrics_ostream:
+    type:     ostream
+    filename: __cmp_metrics
+
+  exporter_metrics_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_logs_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file logs"
+    file_pattern:   "__cmp_logs_log-%F-%N"
+    alias_pattern:  "__logs_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_logs_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC logs"
+    endpoint:            "http://localhost:4317/v1/logs"
+    use_ssl_credentials: false
+
+  exporter_logs_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP logs"
+    endpoint:                    "http://localhost:4318/v1/logs"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP logs test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP logs test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_logs_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_logs_ostream:
+    type:     ostream
+    filename: __cmp_logs
+
+  exporter_logs_elasticsearch:
+    type:             elasticsearch
+    host:             localhost
+    port:             9200
+    index:            logs
+    response_timeout: 30
+    debug:            false
+    http_headers:
+      - X-OTel-Header-1: "Elasticsearch logs test header #1"
+      - X-OTel-Header-2: "Elasticsearch logs test header #2"
+
+readers:
+  reader_metrics:
+    thread_name:     "reader metr"
+    export_interval: 10000
+    export_timeout:  5000
+
+samplers:
+  sampler_traces:
+#   type:  always_on
+#   type:  always_off
+#   type:  trace_id_ratio_based
+#   ratio: 1.0
+    type:     parent_based
+    delegate: always_on
+
+processors:
+  processor_traces_batch:
+    type:                  batch
+    thread_name:           "proc/batch trac"
+    # Note: when the queue is half full, a preemptive notification is sent
+    # to start the export call.
+    max_queue_size:        2048
+    # Time interval (in ms) between two consecutive exports
+    schedule_delay:        5000
+    # Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
+    max_export_batch_size: 512
+
+  processor_traces_single:
+    type:                  single
+
+  processor_logs_batch:
+    type:                  batch
+    thread_name:           "proc/batch logs"
+    max_queue_size:        2048
+    schedule_delay:        5000
+    max_export_batch_size: 512
+
+  processor_logs_single:
+    type:                  single
+
+providers:
+  provider_traces:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-cmp"
+      - service.name:        "cmp"
+      - service.namespace:   "HAProxy traces test"
+
+  provider_metrics:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-cmp"
+      - service.name:        "cmp"
+      - service.namespace:   "HAProxy metrics test"
+
+  provider_logs:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-cmp"
+      - service.name:        "cmp"
+      - service.namespace:   "HAProxy logs test"
+
+signals:
+  traces:
+    scope_name: "HAProxy OTEL - traces"
+    exporters:  exporter_traces_otlp_http
+    samplers:   sampler_traces
+    processors: processor_traces_batch
+    providers:  provider_traces
+
+  metrics:
+    scope_name: "HAProxy OTEL - metrics"
+    exporters:  exporter_metrics_otlp_http
+    readers:    reader_metrics
+    providers:  provider_metrics
+
+  logs:
+    scope_name: "HAProxy OTEL - logs"
+    exporters:  exporter_logs_otlp_http
+    processors: processor_logs_batch
+    providers:  provider_logs
diff --git a/addons/otel/test/copy-yml.sh b/addons/otel/test/copy-yml.sh
new file mode 100755 (executable)
index 0000000..1a4115e
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh -u
+#
+# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
+#
+SH_FILE="${1:-}"
+ SH_EXT="${2:-}"
+
+
+if test ${#} -ne 2; then
+       echo
+       echo "usage: $(basename "${0}") input-file test-name"
+       echo
+       exit 64
+fi
+
+sed '
+       s/^\( *\)\(filename:\)\( *\)_\(_[a-z]*\)/\1\2\3__'"${SH_EXT}"'\4/g
+       s/^\( *\)\(file_pattern:\)\( *\)"_\(_[a-z]*_[^"]*\)"/\1\2\3"__'"${SH_EXT}"'\4"/g
+       s/^\( *\)\(- service.instance.id:\)\( *\).*/\1\2\3"id-'"${SH_EXT}"'"/g
+       s/^\( *\)\(- service.name:\)\( *\).*/\1\2\3"'"${SH_EXT}"'"/g
+       s/^\( *\)\(- service.namespace:\)\( *\)\("otelc\)/\1\2\3"HAProxy/g
+       s/^\( *\)\(scope_name:\)\( *\)"OTEL C wrapper /\1\2 "HAProxy OTEL /g
+       s/^\( *\)\(exporters:\)\( *\)\(exporter_[a-z]*_\).*/\1\2\3\4otlp_http/g
+' "${SH_FILE}"
diff --git a/addons/otel/test/ctx/haproxy.cfg b/addons/otel/test/ctx/haproxy.cfg
new file mode 100644 (file)
index 0000000..5d817c7
--- /dev/null
@@ -0,0 +1,28 @@
+global
+    stats socket /tmp/haproxy.sock mode 666 level admin
+
+listen stats
+    mode http
+    bind *:8001
+    stats uri /
+    stats admin if TRUE
+    stats refresh 10s
+
+frontend otel-test-ctx-frontend
+    bind *:10080
+    default_backend servers-backend
+
+    # ACL used to distinguish successful from error responses
+    acl acl-http-status-ok status 100:399
+
+    # OTel filter
+    filter opentelemetry id otel-test-ctx config ctx/otel.cfg
+
+    # run response scopes for successful responses
+    http-response otel-group otel-test-ctx http_response_group if acl-http-status-ok
+
+    # run after-response scopes for error responses
+    http-after-response otel-group otel-test-ctx http_after_response_group if !acl-http-status-ok
+
+backend servers-backend
+    server server-1 127.0.0.1:8000
diff --git a/addons/otel/test/ctx/otel.cfg b/addons/otel/test/ctx/otel.cfg
new file mode 100644 (file)
index 0000000..fba37b1
--- /dev/null
@@ -0,0 +1,196 @@
+[otel-test-ctx]
+    otel-instrumentation otel-test-instrumentation
+        debug-level 0x77f
+        log localhost:514 local7 debug
+        config ctx/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 client_session_start_1
+        scopes client_session_start_2
+        scopes frontend_tcp_request
+        scopes http_wait_request
+        scopes http_body_request
+        scopes frontend_http_request
+        scopes switching_rules_request
+        scopes backend_tcp_request
+        scopes backend_http_request
+        scopes process_server_rules_request
+        scopes http_process_request
+        scopes tcp_rdp_cookie_request
+        scopes process_sticking_rules_request
+        scopes client_session_end
+        scopes server_unavailable
+
+        scopes server_session_start
+        scopes tcp_response
+        scopes http_wait_response
+        scopes process_store_rules_response
+        scopes http_response http_response-error
+        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_1" "hdr.content" res.hdr("content-type") str("; length: ") res.hdr("content-length") str(" bytes")
+
+    otel-scope http_response_2
+        span "HTTP response"
+            event "event_2" "hdr.date" res.hdr("date") str(" / ") res.hdr("last-modified")
+
+    otel-group http_after_response_group
+        scopes http_after_response
+
+    otel-scope http_after_response
+        span "HAProxy response" parent "HAProxy session"
+            status "error" str("http.status_code") status
+
+    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"
+            inject "otel_ctx_4" use-headers use-vars
+        finish "Frontend TCP request" "otel_ctx_3"
+        otel-event on-http-wait-request
+
+    otel-scope http_body_request
+        extract "otel_ctx_4" use-vars
+        span "HTTP body request" parent "otel_ctx_4"
+            inject "otel_ctx_5" use-headers use-vars
+        finish "HTTP wait request" "otel_ctx_4"
+        otel-event on-http-body-request
+
+    otel-scope frontend_http_request
+        extract "otel_ctx_5" use-vars
+        span "Frontend HTTP request" parent "otel_ctx_5"
+            attribute "http.method" method
+            attribute "http.url" url
+            attribute "http.version" str("HTTP/") req.ver
+            inject "otel_ctx_6" use-headers use-vars
+        finish "HTTP body request" "otel_ctx_5"
+        otel-event on-frontend-http-request
+
+    otel-scope switching_rules_request
+        extract "otel_ctx_6" use-vars
+        span "Switching rules request" parent "otel_ctx_6"
+            inject "otel_ctx_7" use-headers use-vars
+        finish "Frontend HTTP request" "otel_ctx_6"
+        otel-event on-switching-rules-request
+
+    otel-scope backend_tcp_request
+        extract "otel_ctx_7" use-vars
+        span "Backend TCP request" parent "otel_ctx_7"
+            inject "otel_ctx_8" use-headers use-vars
+        finish "Switching rules request" "otel_ctx_7"
+        otel-event on-backend-tcp-request
+
+    otel-scope backend_http_request
+        extract "otel_ctx_8" use-vars
+        span "Backend HTTP request" parent "otel_ctx_8"
+            inject "otel_ctx_9" use-headers use-vars
+        finish "Backend TCP request" "otel_ctx_8"
+        otel-event on-backend-http-request
+
+    otel-scope process_server_rules_request
+        extract "otel_ctx_9" use-vars
+        span "Process server rules request" parent "otel_ctx_9"
+            inject "otel_ctx_10" use-headers use-vars
+        finish "Backend HTTP request" "otel_ctx_9"
+        otel-event on-process-server-rules-request
+
+    otel-scope http_process_request
+        extract "otel_ctx_10" use-vars
+        span "HTTP process request" parent "otel_ctx_10"
+            inject "otel_ctx_11" use-headers use-vars
+        finish "Process server rules request" "otel_ctx_10"
+        otel-event on-http-process-request
+
+    otel-scope tcp_rdp_cookie_request
+        extract "otel_ctx_11" use-vars
+        span "TCP RDP cookie request" parent "otel_ctx_11"
+            inject "otel_ctx_12" use-headers use-vars
+        finish "HTTP process request" "otel_ctx_11"
+        otel-event on-tcp-rdp-cookie-request
+
+    otel-scope process_sticking_rules_request
+        extract "otel_ctx_12" use-vars
+        span "Process sticking rules request" parent "otel_ctx_12"
+            inject "otel_ctx_13" use-headers use-vars
+        finish "TCP RDP cookie request" "otel_ctx_12"
+        otel-event on-process-sticking-rules-request
+
+    otel-scope client_session_end
+        finish "Client session" "otel_ctx_2"
+        otel-event on-client-session-end
+
+    otel-scope server_unavailable
+        finish *
+        otel-event on-server-unavailable
+
+    otel-scope server_session_start
+        span "Server session" parent "otel_ctx_1"
+            inject "otel_ctx_14" use-vars
+        extract "otel_ctx_13" use-vars
+        finish "Process sticking rules request" "otel_ctx_13"
+        otel-event on-server-session-start
+
+    otel-scope tcp_response
+        extract "otel_ctx_14" use-vars
+        span "TCP response" parent "otel_ctx_14"
+            inject "otel_ctx_15" use-vars
+        otel-event on-tcp-response
+
+    otel-scope http_wait_response
+        extract "otel_ctx_15" use-vars
+        span "HTTP wait response" parent "otel_ctx_15"
+            inject "otel_ctx_16" use-headers use-vars
+        finish "TCP response" "otel_ctx_15"
+        otel-event on-http-wait-response
+
+    otel-scope process_store_rules_response
+        extract "otel_ctx_16" use-vars
+        span "Process store rules response" parent "otel_ctx_16"
+            inject "otel_ctx_17" use-headers use-vars
+        finish "HTTP wait response" "otel_ctx_16"
+        otel-event on-process-store-rules-response
+
+    otel-scope http_response
+        extract "otel_ctx_17" use-vars
+        span "HTTP response" parent "otel_ctx_17"
+            attribute "http.status_code" status
+        finish "Process store rules response" "otel_ctx_17"
+        otel-event on-http-response
+
+    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 *
+        otel-event on-server-session-end
diff --git a/addons/otel/test/ctx/otel.yml b/addons/otel/test/ctx/otel.yml
new file mode 100644 (file)
index 0000000..56b96f3
--- /dev/null
@@ -0,0 +1,246 @@
+exporters:
+  exporter_traces_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file trace"
+    file_pattern:   "__ctx_traces_log-%F-%N"
+    alias_pattern:  "__traces_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_traces_otlp_grpc:
+    type:                             otlp_grpc
+    thread_name:                      "OTLP/gRPC trace"
+    endpoint:                         "http://localhost:4317/v1/traces"
+    use_ssl_credentials:              false
+#   ssl_credentials_cacert_path:      ""
+#   ssl_credentials_cacert_as_string: ""
+#   ssl_client_key_path:              ""
+#   ssl_client_key_string:            ""
+#   ssl_client_cert_path:             ""
+#   ssl_client_cert_string:           ""
+#   timeout:                          10
+#   user_agent:                       ""
+#   max_threads:                      0
+#   compression:                      ""
+#   max_concurrent_requests:          0
+
+  exporter_traces_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP trace"
+    endpoint:                    "http://localhost:4318/v1/traces"
+    content_type:                json
+    json_bytes_mapping:          hexid
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP traces test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP traces test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+#   ssl_ca_cert_path:            ""
+#   ssl_ca_cert_string:          ""
+#   ssl_client_key_path:         ""
+#   ssl_client_key_string:       ""
+#   ssl_client_cert_path:        ""
+#   ssl_client_cert_string:      ""
+#   ssl_min_tls:                 ""
+#   ssl_max_tls:                 ""
+#   ssl_cipher:                  ""
+#   ssl_cipher_suite:            ""
+#   compression:                 ""
+
+  exporter_traces_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_traces_ostream:
+    type:     ostream
+    filename: __ctx_traces
+
+  exporter_traces_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_traces_zipkin:
+    type:           zipkin
+    endpoint:       "http://localhost:9411/api/v2/spans"
+    format:         json
+    service_name:   "zipkin-service"
+#   ipv4:           ""
+#   ipv6:           ""
+
+  exporter_metrics_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file metr"
+    file_pattern:   "__ctx_metrics_log-%F-%N"
+    alias_pattern:  "__metrics_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_metrics_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC metr"
+    endpoint:            "http://localhost:4317/v1/metrics"
+    use_ssl_credentials: false
+
+  exporter_metrics_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP metr"
+    endpoint:                    "http://localhost:4318/v1/metrics"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP metrics test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP metrics test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_metrics_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_metrics_ostream:
+    type:     ostream
+    filename: __ctx_metrics
+
+  exporter_metrics_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_logs_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file logs"
+    file_pattern:   "__ctx_logs_log-%F-%N"
+    alias_pattern:  "__logs_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_logs_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC logs"
+    endpoint:            "http://localhost:4317/v1/logs"
+    use_ssl_credentials: false
+
+  exporter_logs_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP logs"
+    endpoint:                    "http://localhost:4318/v1/logs"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP logs test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP logs test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_logs_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_logs_ostream:
+    type:     ostream
+    filename: __ctx_logs
+
+  exporter_logs_elasticsearch:
+    type:             elasticsearch
+    host:             localhost
+    port:             9200
+    index:            logs
+    response_timeout: 30
+    debug:            false
+    http_headers:
+      - X-OTel-Header-1: "Elasticsearch logs test header #1"
+      - X-OTel-Header-2: "Elasticsearch logs test header #2"
+
+readers:
+  reader_metrics:
+    thread_name:     "reader metr"
+    export_interval: 10000
+    export_timeout:  5000
+
+samplers:
+  sampler_traces:
+#   type:  always_on
+#   type:  always_off
+#   type:  trace_id_ratio_based
+#   ratio: 1.0
+    type:     parent_based
+    delegate: always_on
+
+processors:
+  processor_traces_batch:
+    type:                  batch
+    thread_name:           "proc/batch trac"
+    # Note: when the queue is half full, a preemptive notification is sent
+    # to start the export call.
+    max_queue_size:        2048
+    # Time interval (in ms) between two consecutive exports
+    schedule_delay:        5000
+    # Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
+    max_export_batch_size: 512
+
+  processor_traces_single:
+    type:                  single
+
+  processor_logs_batch:
+    type:                  batch
+    thread_name:           "proc/batch logs"
+    max_queue_size:        2048
+    schedule_delay:        5000
+    max_export_batch_size: 512
+
+  processor_logs_single:
+    type:                  single
+
+providers:
+  provider_traces:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-ctx"
+      - service.name:        "ctx"
+      - service.namespace:   "HAProxy traces test"
+
+  provider_metrics:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-ctx"
+      - service.name:        "ctx"
+      - service.namespace:   "HAProxy metrics test"
+
+  provider_logs:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-ctx"
+      - service.name:        "ctx"
+      - service.namespace:   "HAProxy logs test"
+
+signals:
+  traces:
+    scope_name: "HAProxy OTEL - traces"
+    exporters:  exporter_traces_otlp_http
+    samplers:   sampler_traces
+    processors: processor_traces_batch
+    providers:  provider_traces
+
+  metrics:
+    scope_name: "HAProxy OTEL - metrics"
+    exporters:  exporter_metrics_otlp_http
+    readers:    reader_metrics
+    providers:  provider_metrics
+
+  logs:
+    scope_name: "HAProxy OTEL - logs"
+    exporters:  exporter_logs_otlp_http
+    processors: processor_logs_batch
+    providers:  provider_logs
diff --git a/addons/otel/test/empty/haproxy.cfg b/addons/otel/test/empty/haproxy.cfg
new file mode 100644 (file)
index 0000000..a714136
--- /dev/null
@@ -0,0 +1,19 @@
+global
+    stats socket /tmp/haproxy.sock mode 666 level admin
+
+listen stats
+    mode http
+    bind *:8001
+    stats uri /
+    stats admin if TRUE
+    stats refresh 10s
+
+frontend otel-test-empty
+    bind *:10080
+    default_backend servers-backend
+
+    # OTel filter
+    filter opentelemetry id otel-test-empty config empty/otel.cfg
+
+backend servers-backend
+    server server-1 127.0.0.1:8000
diff --git a/addons/otel/test/empty/otel.cfg b/addons/otel/test/empty/otel.cfg
new file mode 100644 (file)
index 0000000..3508936
--- /dev/null
@@ -0,0 +1,2 @@
+otel-instrumentation otel-test-instrumentation
+    config empty/otel.yml
diff --git a/addons/otel/test/empty/otel.yml b/addons/otel/test/empty/otel.yml
new file mode 100644 (file)
index 0000000..1c8281f
--- /dev/null
@@ -0,0 +1,246 @@
+exporters:
+  exporter_traces_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file trace"
+    file_pattern:   "__empty_traces_log-%F-%N"
+    alias_pattern:  "__traces_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_traces_otlp_grpc:
+    type:                             otlp_grpc
+    thread_name:                      "OTLP/gRPC trace"
+    endpoint:                         "http://localhost:4317/v1/traces"
+    use_ssl_credentials:              false
+#   ssl_credentials_cacert_path:      ""
+#   ssl_credentials_cacert_as_string: ""
+#   ssl_client_key_path:              ""
+#   ssl_client_key_string:            ""
+#   ssl_client_cert_path:             ""
+#   ssl_client_cert_string:           ""
+#   timeout:                          10
+#   user_agent:                       ""
+#   max_threads:                      0
+#   compression:                      ""
+#   max_concurrent_requests:          0
+
+  exporter_traces_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP trace"
+    endpoint:                    "http://localhost:4318/v1/traces"
+    content_type:                json
+    json_bytes_mapping:          hexid
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP traces test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP traces test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+#   ssl_ca_cert_path:            ""
+#   ssl_ca_cert_string:          ""
+#   ssl_client_key_path:         ""
+#   ssl_client_key_string:       ""
+#   ssl_client_cert_path:        ""
+#   ssl_client_cert_string:      ""
+#   ssl_min_tls:                 ""
+#   ssl_max_tls:                 ""
+#   ssl_cipher:                  ""
+#   ssl_cipher_suite:            ""
+#   compression:                 ""
+
+  exporter_traces_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_traces_ostream:
+    type:     ostream
+    filename: __empty_traces
+
+  exporter_traces_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_traces_zipkin:
+    type:           zipkin
+    endpoint:       "http://localhost:9411/api/v2/spans"
+    format:         json
+    service_name:   "zipkin-service"
+#   ipv4:           ""
+#   ipv6:           ""
+
+  exporter_metrics_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file metr"
+    file_pattern:   "__empty_metrics_log-%F-%N"
+    alias_pattern:  "__metrics_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_metrics_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC metr"
+    endpoint:            "http://localhost:4317/v1/metrics"
+    use_ssl_credentials: false
+
+  exporter_metrics_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP metr"
+    endpoint:                    "http://localhost:4318/v1/metrics"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP metrics test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP metrics test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_metrics_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_metrics_ostream:
+    type:     ostream
+    filename: __empty_metrics
+
+  exporter_metrics_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_logs_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file logs"
+    file_pattern:   "__empty_logs_log-%F-%N"
+    alias_pattern:  "__logs_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_logs_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC logs"
+    endpoint:            "http://localhost:4317/v1/logs"
+    use_ssl_credentials: false
+
+  exporter_logs_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP logs"
+    endpoint:                    "http://localhost:4318/v1/logs"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP logs test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP logs test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_logs_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_logs_ostream:
+    type:     ostream
+    filename: __empty_logs
+
+  exporter_logs_elasticsearch:
+    type:             elasticsearch
+    host:             localhost
+    port:             9200
+    index:            logs
+    response_timeout: 30
+    debug:            false
+    http_headers:
+      - X-OTel-Header-1: "Elasticsearch logs test header #1"
+      - X-OTel-Header-2: "Elasticsearch logs test header #2"
+
+readers:
+  reader_metrics:
+    thread_name:     "reader metr"
+    export_interval: 10000
+    export_timeout:  5000
+
+samplers:
+  sampler_traces:
+#   type:  always_on
+#   type:  always_off
+#   type:  trace_id_ratio_based
+#   ratio: 1.0
+    type:     parent_based
+    delegate: always_on
+
+processors:
+  processor_traces_batch:
+    type:                  batch
+    thread_name:           "proc/batch trac"
+    # Note: when the queue is half full, a preemptive notification is sent
+    # to start the export call.
+    max_queue_size:        2048
+    # Time interval (in ms) between two consecutive exports
+    schedule_delay:        5000
+    # Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
+    max_export_batch_size: 512
+
+  processor_traces_single:
+    type:                  single
+
+  processor_logs_batch:
+    type:                  batch
+    thread_name:           "proc/batch logs"
+    max_queue_size:        2048
+    schedule_delay:        5000
+    max_export_batch_size: 512
+
+  processor_logs_single:
+    type:                  single
+
+providers:
+  provider_traces:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-empty"
+      - service.name:        "empty"
+      - service.namespace:   "HAProxy traces test"
+
+  provider_metrics:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-empty"
+      - service.name:        "empty"
+      - service.namespace:   "HAProxy metrics test"
+
+  provider_logs:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-empty"
+      - service.name:        "empty"
+      - service.namespace:   "HAProxy logs test"
+
+signals:
+  traces:
+    scope_name: "HAProxy OTEL - traces"
+    exporters:  exporter_traces_otlp_http
+    samplers:   sampler_traces
+    processors: processor_traces_batch
+    providers:  provider_traces
+
+  metrics:
+    scope_name: "HAProxy OTEL - metrics"
+    exporters:  exporter_metrics_otlp_http
+    readers:    reader_metrics
+    providers:  provider_metrics
+
+  logs:
+    scope_name: "HAProxy OTEL - logs"
+    exporters:  exporter_logs_otlp_http
+    processors: processor_logs_batch
+    providers:  provider_logs
diff --git a/addons/otel/test/fe/haproxy.cfg b/addons/otel/test/fe/haproxy.cfg
new file mode 100644 (file)
index 0000000..1d25db1
--- /dev/null
@@ -0,0 +1,19 @@
+global
+    stats socket /tmp/haproxy-fe.sock mode 666 level admin
+
+listen stats
+    mode http
+    bind *:8001
+    stats uri /
+    stats admin if TRUE
+    stats refresh 10s
+
+frontend otel-test-fe-frontend
+    bind *:10080
+    default_backend servers-backend
+
+    # OTel filter
+    filter opentelemetry id otel-test-fe config fe/otel.cfg
+
+backend servers-backend
+    server server-1 127.0.0.1:11080
diff --git a/addons/otel/test/fe/otel.cfg b/addons/otel/test/fe/otel.cfg
new file mode 100644 (file)
index 0000000..7eb1a56
--- /dev/null
@@ -0,0 +1,73 @@
+[otel-test-fe]
+    otel-instrumentation otel-test-instrumentation
+        config fe/otel.yml
+#       log localhost:514 local7 debug
+        option dontlog-normal
+        option hard-errors
+        no option disabled
+        rate-limit 100.0
+
+        scopes client_session_start
+        scopes frontend_tcp_request
+        scopes frontend_http_request
+        scopes backend_tcp_request
+        scopes backend_http_request
+        scopes client_session_end
+
+        scopes server_session_start
+        scopes tcp_response
+        scopes http_response
+        scopes server_session_end
+
+    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 frontend_tcp_request
+        span "Frontend TCP request" parent "Client session"
+        otel-event on-frontend-tcp-request
+
+    otel-scope frontend_http_request
+        span "Frontend HTTP request" parent "Frontend TCP request"
+            attribute "http.method" method
+            attribute "http.url" url
+            attribute "http.version" str("HTTP/") req.ver
+        finish "Frontend TCP request"
+        otel-event on-frontend-http-request
+
+    otel-scope backend_tcp_request
+        span "Backend TCP request" parent "Frontend HTTP request"
+        finish "Frontend HTTP request"
+        otel-event on-backend-tcp-request
+
+    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
+
+    otel-scope client_session_end
+        finish "Client session"
+        otel-event on-client-session-end
+
+    otel-scope server_session_start
+        span "Server session" parent "HAProxy session"
+        finish "Backend HTTP request"
+        otel-event on-server-session-start
+
+    otel-scope tcp_response
+        span "TCP response" parent "Server session"
+        otel-event on-tcp-response
+
+    otel-scope http_response
+        span "HTTP response" parent "TCP response"
+            attribute "http.status_code" status
+        finish "TCP response"
+        otel-event on-http-response
+
+    otel-scope server_session_end
+        finish *
+        otel-event on-server-session-end
diff --git a/addons/otel/test/fe/otel.yml b/addons/otel/test/fe/otel.yml
new file mode 100644 (file)
index 0000000..89e25f7
--- /dev/null
@@ -0,0 +1,246 @@
+exporters:
+  exporter_traces_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file trace"
+    file_pattern:   "__fe_traces_log-%F-%N"
+    alias_pattern:  "__traces_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_traces_otlp_grpc:
+    type:                             otlp_grpc
+    thread_name:                      "OTLP/gRPC trace"
+    endpoint:                         "http://localhost:4317/v1/traces"
+    use_ssl_credentials:              false
+#   ssl_credentials_cacert_path:      ""
+#   ssl_credentials_cacert_as_string: ""
+#   ssl_client_key_path:              ""
+#   ssl_client_key_string:            ""
+#   ssl_client_cert_path:             ""
+#   ssl_client_cert_string:           ""
+#   timeout:                          10
+#   user_agent:                       ""
+#   max_threads:                      0
+#   compression:                      ""
+#   max_concurrent_requests:          0
+
+  exporter_traces_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP trace"
+    endpoint:                    "http://localhost:4318/v1/traces"
+    content_type:                json
+    json_bytes_mapping:          hexid
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP traces test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP traces test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+#   ssl_ca_cert_path:            ""
+#   ssl_ca_cert_string:          ""
+#   ssl_client_key_path:         ""
+#   ssl_client_key_string:       ""
+#   ssl_client_cert_path:        ""
+#   ssl_client_cert_string:      ""
+#   ssl_min_tls:                 ""
+#   ssl_max_tls:                 ""
+#   ssl_cipher:                  ""
+#   ssl_cipher_suite:            ""
+#   compression:                 ""
+
+  exporter_traces_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_traces_ostream:
+    type:     ostream
+    filename: __fe_traces
+
+  exporter_traces_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_traces_zipkin:
+    type:           zipkin
+    endpoint:       "http://localhost:9411/api/v2/spans"
+    format:         json
+    service_name:   "zipkin-service"
+#   ipv4:           ""
+#   ipv6:           ""
+
+  exporter_metrics_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file metr"
+    file_pattern:   "__fe_metrics_log-%F-%N"
+    alias_pattern:  "__metrics_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_metrics_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC metr"
+    endpoint:            "http://localhost:4317/v1/metrics"
+    use_ssl_credentials: false
+
+  exporter_metrics_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP metr"
+    endpoint:                    "http://localhost:4318/v1/metrics"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP metrics test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP metrics test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_metrics_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_metrics_ostream:
+    type:     ostream
+    filename: __fe_metrics
+
+  exporter_metrics_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_logs_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file logs"
+    file_pattern:   "__fe_logs_log-%F-%N"
+    alias_pattern:  "__logs_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_logs_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC logs"
+    endpoint:            "http://localhost:4317/v1/logs"
+    use_ssl_credentials: false
+
+  exporter_logs_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP logs"
+    endpoint:                    "http://localhost:4318/v1/logs"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP logs test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP logs test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_logs_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_logs_ostream:
+    type:     ostream
+    filename: __fe_logs
+
+  exporter_logs_elasticsearch:
+    type:             elasticsearch
+    host:             localhost
+    port:             9200
+    index:            logs
+    response_timeout: 30
+    debug:            false
+    http_headers:
+      - X-OTel-Header-1: "Elasticsearch logs test header #1"
+      - X-OTel-Header-2: "Elasticsearch logs test header #2"
+
+readers:
+  reader_metrics:
+    thread_name:     "reader metr"
+    export_interval: 10000
+    export_timeout:  5000
+
+samplers:
+  sampler_traces:
+#   type:  always_on
+#   type:  always_off
+#   type:  trace_id_ratio_based
+#   ratio: 1.0
+    type:     parent_based
+    delegate: always_on
+
+processors:
+  processor_traces_batch:
+    type:                  batch
+    thread_name:           "proc/batch trac"
+    # Note: when the queue is half full, a preemptive notification is sent
+    # to start the export call.
+    max_queue_size:        2048
+    # Time interval (in ms) between two consecutive exports
+    schedule_delay:        5000
+    # Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
+    max_export_batch_size: 512
+
+  processor_traces_single:
+    type:                  single
+
+  processor_logs_batch:
+    type:                  batch
+    thread_name:           "proc/batch logs"
+    max_queue_size:        2048
+    schedule_delay:        5000
+    max_export_batch_size: 512
+
+  processor_logs_single:
+    type:                  single
+
+providers:
+  provider_traces:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-fe"
+      - service.name:        "fe"
+      - service.namespace:   "HAProxy traces test"
+
+  provider_metrics:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-fe"
+      - service.name:        "fe"
+      - service.namespace:   "HAProxy metrics test"
+
+  provider_logs:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-fe"
+      - service.name:        "fe"
+      - service.namespace:   "HAProxy logs test"
+
+signals:
+  traces:
+    scope_name: "HAProxy OTEL - traces"
+    exporters:  exporter_traces_otlp_http
+    samplers:   sampler_traces
+    processors: processor_traces_batch
+    providers:  provider_traces
+
+  metrics:
+    scope_name: "HAProxy OTEL - metrics"
+    exporters:  exporter_metrics_otlp_http
+    readers:    reader_metrics
+    providers:  provider_metrics
+
+  logs:
+    scope_name: "HAProxy OTEL - logs"
+    exporters:  exporter_logs_otlp_http
+    processors: processor_logs_batch
+    providers:  provider_logs
diff --git a/addons/otel/test/haproxy-common.cfg b/addons/otel/test/haproxy-common.cfg
new file mode 100644 (file)
index 0000000..d66d734
--- /dev/null
@@ -0,0 +1,19 @@
+global
+#   nbthread 1
+    insecure-fork-wanted
+    maxconn 5000
+    hard-stop-after 10s
+#   log localhost:514 local7 debug
+#   debug
+
+defaults
+#   log     global
+#   option  httplog
+#   option  dontlognull
+#   option  httpclose
+    mode    http
+    retries 3
+    maxconn 4000
+    timeout connect 5000
+    timeout client  50000
+    timeout server  50000
diff --git a/addons/otel/test/index.html b/addons/otel/test/index.html
new file mode 100644 (file)
index 0000000..09ed6fa
--- /dev/null
@@ -0,0 +1 @@
+<html><body><p>Did I err?</p></body></html>
diff --git a/addons/otel/test/run-cmp.sh b/addons/otel/test/run-cmp.sh
new file mode 100755 (executable)
index 0000000..9815b09
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh -u
+#
+# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
+#
+SH_ARG_HAPROXY="${1:-$(realpath -L ${PWD}/../../../haproxy)}"
+SH_ARG_PIDFILE="${2:-haproxy.pid}"
+       SH_ARGS="-f haproxy-common.cfg -f cmp/haproxy.cfg -p "${SH_ARG_PIDFILE}""
+    SH_LOG_DIR="_logs"
+        SH_LOG="${SH_LOG_DIR}/_log-$(basename "${0}" .sh)-$(date +%s)"
+
+
+test -x "${SH_ARG_HAPROXY}" || exit 1
+mkdir -p "${SH_LOG_DIR}"    || exit 2
+
+echo "executing: ${SH_ARG_HAPROXY} ${SH_ARGS}" >${SH_LOG}
+"${SH_ARG_HAPROXY}" ${SH_ARGS} >>"${SH_LOG}" 2>&1
diff --git a/addons/otel/test/run-ctx.sh b/addons/otel/test/run-ctx.sh
new file mode 100755 (executable)
index 0000000..70cb87a
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh -u
+#
+# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
+#
+SH_ARG_HAPROXY="${1:-$(realpath -L ${PWD}/../../../haproxy)}"
+SH_ARG_PIDFILE="${2:-haproxy.pid}"
+       SH_ARGS="-f haproxy-common.cfg -f ctx/haproxy.cfg -p "${SH_ARG_PIDFILE}""
+    SH_LOG_DIR="_logs"
+        SH_LOG="${SH_LOG_DIR}/_log-$(basename "${0}" .sh)-$(date +%s)"
+
+
+test -x "${SH_ARG_HAPROXY}" || exit 1
+mkdir -p "${SH_LOG_DIR}"    || exit 2
+
+echo "executing: ${SH_ARG_HAPROXY} ${SH_ARGS}" >${SH_LOG}
+"${SH_ARG_HAPROXY}" ${SH_ARGS} >>"${SH_LOG}" 2>&1
diff --git a/addons/otel/test/run-fe-be.sh b/addons/otel/test/run-fe-be.sh
new file mode 100755 (executable)
index 0000000..1248338
--- /dev/null
@@ -0,0 +1,50 @@
+#!/bin/sh -u
+#
+# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
+#
+SH_ARG_HAPROXY="${1:-$(realpath -L ${PWD}/../../../haproxy)}"
+SH_ARG_PIDFILE="${2:-haproxy.pid}"
+    SH_ARGS_FE="-f haproxy-common.cfg -f fe/haproxy.cfg -p "${SH_ARG_PIDFILE}""
+    SH_ARGS_BE="-f haproxy-common.cfg -f be/haproxy.cfg -p "${SH_ARG_PIDFILE}""
+       SH_TIME="$(date +%s)"
+    SH_LOG_DIR="_logs"
+     SH_LOG_FE="${SH_LOG_DIR}/_log-$(basename "${0}" fe-be.sh)fe-${SH_TIME}"
+     SH_LOG_BE="${SH_LOG_DIR}/_log-$(basename "${0}" fe-be.sh)be-${SH_TIME}"
+
+
+__exit ()
+{
+       test -z "${2}" && {
+               echo
+               echo "Script killed!"
+
+               echo "Waiting for jobs to complete..."
+               pkill --signal SIGUSR1 haproxy
+               wait
+       }
+
+       test -n "${1}" && {
+               echo
+               echo "${1}"
+               echo
+       }
+
+       exit ${2:-100}
+}
+
+
+trap __exit INT TERM
+
+test -x "${SH_ARG_HAPROXY}" || __exit "${SH_ARG_HAPROXY}: executable does not exist" 1
+mkdir -p "${SH_LOG_DIR}"    || __exit "${SH_ARG_HAPROXY}: cannot create log directory" 2
+
+echo "\n------------------------------------------------------------------------"
+echo "--- executing: ${SH_ARG_HAPROXY} ${SH_ARGS_BE}" >${SH_LOG_BE}
+"${SH_ARG_HAPROXY}" ${SH_ARGS_BE} >>"${SH_LOG_BE}" 2>&1 &
+
+echo "--- executing: ${SH_ARG_HAPROXY} ${SH_ARGS_FE}" >${SH_LOG_FE}
+"${SH_ARG_HAPROXY}" ${SH_ARGS_FE} >>"${SH_LOG_FE}" 2>&1 &
+echo "------------------------------------------------------------------------\n"
+
+echo "Press CTRL-C to quit..."
+wait
diff --git a/addons/otel/test/run-sa.sh b/addons/otel/test/run-sa.sh
new file mode 100755 (executable)
index 0000000..27849c6
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/sh -u
+#
+# Copyright 2026 HAProxy Technologies, Miroslav Zagorac <mzagorac@haproxy.com>
+#
+SH_ARG_HAPROXY="${1:-$(realpath -L ${PWD}/../../../haproxy)}"
+SH_ARG_PIDFILE="${2:-haproxy.pid}"
+       SH_ARGS="-f haproxy-common.cfg -f sa/haproxy.cfg -p "${SH_ARG_PIDFILE}""
+    SH_LOG_DIR="_logs"
+        SH_LOG="${SH_LOG_DIR}/_log-$(basename "${0}" .sh)-$(date +%s)"
+
+
+test -x "${SH_ARG_HAPROXY}" || exit 1
+mkdir -p "${SH_LOG_DIR}"    || exit 2
+
+echo "executing: ${SH_ARG_HAPROXY} ${SH_ARGS}" >${SH_LOG}
+"${SH_ARG_HAPROXY}" ${SH_ARGS} >>"${SH_LOG}" 2>&1
diff --git a/addons/otel/test/sa/haproxy.cfg b/addons/otel/test/sa/haproxy.cfg
new file mode 100644 (file)
index 0000000..8b62b7b
--- /dev/null
@@ -0,0 +1,28 @@
+global
+    stats socket /tmp/haproxy.sock mode 666 level admin
+
+listen stats
+    mode http
+    bind *:8001
+    stats uri /
+    stats admin if TRUE
+    stats refresh 10s
+
+frontend otel-test-sa-frontend
+    bind *:10080
+    default_backend servers-backend
+
+    # ACL used to distinguish successful from error responses
+    acl acl-http-status-ok status 100:399
+
+    # OTel filter
+    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
+
+backend servers-backend
+    server server-1 127.0.0.1:8000
diff --git a/addons/otel/test/sa/otel.cfg b/addons/otel/test/sa/otel.cfg
new file mode 100644 (file)
index 0000000..147222b
--- /dev/null
@@ -0,0 +1,180 @@
+[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 on_stream_start
+        scopes on_stream_stop
+        scopes on_idle_timeout
+
+        scopes client_session_start
+        scopes frontend_tcp_request
+        scopes http_wait_request
+        scopes http_body_request
+        scopes frontend_http_request
+        scopes switching_rules_request
+        scopes backend_tcp_request
+        scopes backend_http_request
+        scopes process_server_rules_request
+        scopes http_process_request
+        scopes tcp_rdp_cookie_request
+        scopes process_sticking_rules_request
+        scopes client_session_end
+        scopes server_unavailable
+
+        scopes server_session_start
+        scopes tcp_response
+        scopes http_wait_response
+        scopes process_store_rules_response
+        scopes http_response http_response-error
+        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 http_response_2
+        span "HTTP response"
+            event "event_date" "hdr.date" res.hdr("date") str(" / ") res.hdr("last-modified")
+
+    otel-group http_after_response_group
+        scopes http_after_response
+
+    otel-scope http_after_response
+        span "HAProxy response" parent "HAProxy session"
+            status "error" str("http.status_code: ") status
+
+    otel-scope on_stream_start
+        span "HAProxy session" root
+            baggage "haproxy_id" var(sess.otel.uuid)
+            event "event_ip" "src" src str(":") src_port
+            event "event_be" "be"  be_id str(" ") be_name
+            event "event_ip" "dst" dst str(":") dst_port
+            event "event_fe" "fe"  fe_id str(" ") fe_name
+        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 on_idle_timeout
+        idle-timeout 1s
+        span "heartbeat" parent "HAProxy session"
+            attribute "idle.elapsed" str("idle-check")
+        otel-event on-idle-timeout
+
+    otel-scope client_session_start
+        span "Client session" parent "HAProxy session"
+        otel-event on-client-session-start
+
+    otel-scope frontend_tcp_request
+        span "Frontend TCP request" parent "Client session"
+        otel-event on-frontend-tcp-request
+
+    otel-scope http_wait_request
+        span "HTTP wait request" parent "Frontend TCP request"
+        finish "Frontend TCP request"
+        otel-event on-http-wait-request
+
+    otel-scope http_body_request
+        span "HTTP body request" parent "HTTP wait request"
+        finish "HTTP wait request"
+        otel-event on-http-body-request
+
+    otel-scope frontend_http_request
+        span "Frontend HTTP request" parent "HTTP body request"
+            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 switching_rules_request
+        span "Switching rules request" parent "Frontend HTTP request"
+        finish "Frontend HTTP request"
+        otel-event on-switching-rules-request
+
+    otel-scope backend_tcp_request
+        span "Backend TCP request" parent "Switching rules request"
+        finish "Switching rules request"
+        otel-event on-backend-tcp-request
+
+    otel-scope backend_http_request
+        span "Backend HTTP request" parent "Backend TCP request"
+        finish "Backend TCP request"
+        otel-event on-backend-http-request
+
+    otel-scope process_server_rules_request
+        span "Process server rules request" parent "Backend HTTP request"
+        finish "Backend HTTP request"
+        otel-event on-process-server-rules-request
+
+    otel-scope http_process_request
+        span "HTTP process request" parent "Process server rules request"
+        finish "Process server rules request"
+        otel-event on-http-process-request
+
+    otel-scope tcp_rdp_cookie_request
+        span "TCP RDP cookie request" parent "HTTP process request"
+        finish "HTTP process request"
+        otel-event on-tcp-rdp-cookie-request
+
+    otel-scope process_sticking_rules_request
+        span "Process sticking rules request" parent "TCP RDP cookie request"
+        finish "TCP RDP cookie request"
+        otel-event on-process-sticking-rules-request
+
+    otel-scope client_session_end
+        finish "*req*"
+        otel-event on-client-session-end
+
+    otel-scope server_unavailable
+        finish "*req*" "*res*"
+        otel-event on-server-unavailable
+
+    otel-scope server_session_start
+        span "Server session" parent "HAProxy session"
+        finish "Process sticking rules request"
+        otel-event on-server-session-start
+
+    otel-scope tcp_response
+        span "TCP response" parent "Server session"
+        otel-event on-tcp-response
+
+    otel-scope http_wait_response
+        span "HTTP wait response" parent "TCP response"
+        finish "TCP response"
+        otel-event on-http-wait-response
+
+    otel-scope process_store_rules_response
+        span "Process store rules response" parent "HTTP wait response"
+        finish "HTTP wait response"
+        otel-event on-process-store-rules-response
+
+    otel-scope http_response
+        span "HTTP response" parent "Process store rules response"
+            attribute "http.status_code" status
+        finish "Process store rules response"
+        otel-event on-http-response
+
+    otel-scope http_response-error
+        span "HTTP response"
+            status "error" str("http.status_code: ") status
+        otel-event on-http-response if !acl-http-status-ok
+
+    otel-scope server_session_end
+        finish "*res*"
+        otel-event on-server-session-end
diff --git a/addons/otel/test/sa/otel.yml b/addons/otel/test/sa/otel.yml
new file mode 100644 (file)
index 0000000..1320b83
--- /dev/null
@@ -0,0 +1,246 @@
+exporters:
+  exporter_traces_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file trace"
+    file_pattern:   "__sa_traces_log-%F-%N"
+    alias_pattern:  "__traces_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_traces_otlp_grpc:
+    type:                             otlp_grpc
+    thread_name:                      "OTLP/gRPC trace"
+    endpoint:                         "http://localhost:4317/v1/traces"
+    use_ssl_credentials:              false
+#   ssl_credentials_cacert_path:      ""
+#   ssl_credentials_cacert_as_string: ""
+#   ssl_client_key_path:              ""
+#   ssl_client_key_string:            ""
+#   ssl_client_cert_path:             ""
+#   ssl_client_cert_string:           ""
+#   timeout:                          10
+#   user_agent:                       ""
+#   max_threads:                      0
+#   compression:                      ""
+#   max_concurrent_requests:          0
+
+  exporter_traces_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP trace"
+    endpoint:                    "http://localhost:4318/v1/traces"
+    content_type:                json
+    json_bytes_mapping:          hexid
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP traces test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP traces test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+#   ssl_ca_cert_path:            ""
+#   ssl_ca_cert_string:          ""
+#   ssl_client_key_path:         ""
+#   ssl_client_key_string:       ""
+#   ssl_client_cert_path:        ""
+#   ssl_client_cert_string:      ""
+#   ssl_min_tls:                 ""
+#   ssl_max_tls:                 ""
+#   ssl_cipher:                  ""
+#   ssl_cipher_suite:            ""
+#   compression:                 ""
+
+  exporter_traces_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_traces_ostream:
+    type:     ostream
+    filename: __sa_traces
+
+  exporter_traces_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_traces_zipkin:
+    type:           zipkin
+    endpoint:       "http://localhost:9411/api/v2/spans"
+    format:         json
+    service_name:   "zipkin-service"
+#   ipv4:           ""
+#   ipv6:           ""
+
+  exporter_metrics_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file metr"
+    file_pattern:   "__sa_metrics_log-%F-%N"
+    alias_pattern:  "__metrics_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_metrics_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC metr"
+    endpoint:            "http://localhost:4317/v1/metrics"
+    use_ssl_credentials: false
+
+  exporter_metrics_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP metr"
+    endpoint:                    "http://localhost:4318/v1/metrics"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP metrics test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP metrics test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_metrics_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_metrics_ostream:
+    type:     ostream
+    filename: __sa_metrics
+
+  exporter_metrics_memory:
+    type:        memory
+    buffer_size: 256
+
+  exporter_logs_otlp_file:
+    type:           otlp_file
+    thread_name:    "OTLP/file logs"
+    file_pattern:   "__sa_logs_log-%F-%N"
+    alias_pattern:  "__logs_log-latest"
+    flush_interval: 30000000
+    flush_count:    256
+    file_size:      134217728
+    rotate_size:    5
+
+  exporter_logs_otlp_grpc:
+    type:                otlp_grpc
+    thread_name:         "OTLP/gRPC logs"
+    endpoint:            "http://localhost:4317/v1/logs"
+    use_ssl_credentials: false
+
+  exporter_logs_otlp_http:
+    type:                        otlp_http
+    thread_name:                 "OTLP/HTTP logs"
+    endpoint:                    "http://localhost:4318/v1/logs"
+    content_type:                json
+    debug:                       false
+    timeout:                     10
+    http_headers:
+      - X-OTel-Header-1:         "OTLP HTTP logs test header #1"
+      - X-OTel-Header-2:         "OTLP HTTP logs test header #2"
+    max_concurrent_requests:     64
+    max_requests_per_connection: 8
+    ssl_insecure_skip_verify:    true
+
+  exporter_logs_dev_null:
+    type:     ostream
+    filename: /dev/null
+
+  exporter_logs_ostream:
+    type:     ostream
+    filename: __sa_logs
+
+  exporter_logs_elasticsearch:
+    type:             elasticsearch
+    host:             localhost
+    port:             9200
+    index:            logs
+    response_timeout: 30
+    debug:            false
+    http_headers:
+      - X-OTel-Header-1: "Elasticsearch logs test header #1"
+      - X-OTel-Header-2: "Elasticsearch logs test header #2"
+
+readers:
+  reader_metrics:
+    thread_name:     "reader metr"
+    export_interval: 10000
+    export_timeout:  5000
+
+samplers:
+  sampler_traces:
+#   type:  always_on
+#   type:  always_off
+#   type:  trace_id_ratio_based
+#   ratio: 1.0
+    type:     parent_based
+    delegate: always_on
+
+processors:
+  processor_traces_batch:
+    type:                  batch
+    thread_name:           "proc/batch trac"
+    # Note: when the queue is half full, a preemptive notification is sent
+    # to start the export call.
+    max_queue_size:        2048
+    # Time interval (in ms) between two consecutive exports
+    schedule_delay:        5000
+    # Export 'max_export_batch_size' after every `schedule_delay' milliseconds.
+    max_export_batch_size: 512
+
+  processor_traces_single:
+    type:                  single
+
+  processor_logs_batch:
+    type:                  batch
+    thread_name:           "proc/batch logs"
+    max_queue_size:        2048
+    schedule_delay:        5000
+    max_export_batch_size: 512
+
+  processor_logs_single:
+    type:                  single
+
+providers:
+  provider_traces:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-sa"
+      - service.name:        "sa"
+      - service.namespace:   "HAProxy traces test"
+
+  provider_metrics:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-sa"
+      - service.name:        "sa"
+      - service.namespace:   "HAProxy metrics test"
+
+  provider_logs:
+    resources:
+      - service.version:     "1.0.0"
+      - service.instance.id: "id-sa"
+      - service.name:        "sa"
+      - service.namespace:   "HAProxy logs test"
+
+signals:
+  traces:
+    scope_name: "HAProxy OTEL - traces"
+    exporters:  exporter_traces_otlp_http
+    samplers:   sampler_traces
+    processors: processor_traces_batch
+    providers:  provider_traces
+
+  metrics:
+    scope_name: "HAProxy OTEL - metrics"
+    exporters:  exporter_metrics_otlp_http
+    readers:    reader_metrics
+    providers:  provider_metrics
+
+  logs:
+    scope_name: "HAProxy OTEL - logs"
+    exporters:  exporter_logs_otlp_http
+    processors: processor_logs_batch
+    providers:  provider_logs
diff --git a/addons/otel/test/test-speed.sh b/addons/otel/test/test-speed.sh
new file mode 100755 (executable)
index 0000000..f71300d
--- /dev/null
@@ -0,0 +1,157 @@
+#!/bin/sh -u
+#
+        SH_ARG_CFG="${1:-}"
+        SH_ARG_DIR="${2:-${SH_ARG_CFG}}"
+        SH_LOG_DIR="_logs"
+SH_HAPROXY_PIDFILE="${SH_LOG_DIR}/haproxy.pid"
+  SH_HTTPD_PIDFILE="${SH_LOG_DIR}/thttpd.pid"
+      SH_USAGE_MSG="usage: $(basename "${0}") cfg [dir]"
+
+
+sh_exit ()
+{
+       sh_backup_clean "${SH_ARG_DIR}"
+
+       test -z "${2:-}" && {
+               echo
+               echo "Script killed!"
+       }
+
+       test -n "${1:-}" && {
+               echo
+               echo "${1}"
+               echo
+       }
+
+       exit ${2:-64}
+}
+
+sh_backup_make()
+{
+       _arg_dir="${1}"
+       _var_file=
+
+       for _var_file in haproxy.cfg otel.cfg otel.yml; do
+               test -e "${_arg_dir}/${_var_file}.orig" || cp -af "${_arg_dir}/${_var_file}" "${_arg_dir}/${_var_file}.orig"
+       done
+
+       test "${_arg_dir}" = "fe" && sh_backup_make "be"
+}
+
+sh_backup_clean()
+{
+       _arg_dir="${1}"
+       _var_file=
+
+       for _var_file in haproxy.cfg otel.cfg otel.yml; do
+               test -e "${_arg_dir}/${_var_file}.orig" && mv "${_arg_dir}/${_var_file}.orig" "${_arg_dir}/${_var_file}"
+       done
+
+       test "${_arg_dir}" = "fe" && sh_backup_clean "be"
+}
+
+sh_httpd_run ()
+{
+
+       test -e "${SH_HTTPD_PIDFILE}" && return
+
+       thttpd -p 8000 -d . -nos -nov -l /dev/null -i "${SH_HTTPD_PIDFILE}"
+}
+
+sh_httpd_stop ()
+{
+       test -e "${SH_HTTPD_PIDFILE}" || return
+
+       kill -TERM "$(cat ${SH_HTTPD_PIDFILE})"
+       rm "${SH_HTTPD_PIDFILE}"
+}
+
+sh_haproxy_run ()
+{
+       _arg_cfg="${1}"
+       _arg_dir="${2}"
+       _arg_ratio="${3}"
+       _var_sed_haproxy=
+       _var_sed_otel=
+       _var_sed_yml="s/\(exporters: *exporter_[a-z]*_\).*/\1dev_null/g"
+
+       if test "${_arg_ratio}" = "disabled"; then
+               _var_sed_otel="s/no \(option disabled\)/\1/"
+       elif test "${_arg_ratio}" = "off"; then
+               _var_sed_haproxy="s/^\(.* filter opentelemetry .*\)/#\1/g; s/^\(.* otel-group .*\)/#\1/g"
+       else
+               _var_sed_otel="s/\(rate-limit\) 100.0/\1 ${_arg_ratio}/"
+       fi
+
+       sed "${_var_sed_haproxy}" "${_arg_dir}/haproxy.cfg.orig" > "${_arg_dir}/haproxy.cfg"
+       sed "${_var_sed_otel}"    "${_arg_dir}/otel.cfg.orig"    > "${_arg_dir}/otel.cfg"
+       sed "${_var_sed_yml}"     "${_arg_dir}/otel.yml.orig"    > "${_arg_dir}/otel.yml"
+
+       if test "${_arg_dir}" = "fe"; then
+               sed "${_var_sed_yml}" "be/otel.yml.orig" > "be/otel.yml"
+
+               if test "${_arg_ratio}" = "disabled" -o "${_arg_ratio}" = "off"; then
+                       sed "${_var_sed_haproxy}" "be/haproxy.cfg.orig" > "be/haproxy.cfg"
+                       sed "${_var_sed_otel}"    "be/otel.cfg.orig"    > "be/otel.cfg"
+               fi
+       fi
+
+       ./run-${_arg_cfg}.sh "" "${SH_HAPROXY_PIDFILE}" &
+       sleep 5
+}
+
+sh_haproxy_stop ()
+{
+       # HAProxy does not create a pidfile if it is not running in daemon mode,
+       # this is not used but is left regardless.
+       #
+       if test -e "${SH_HAPROXY_PIDFILE}"; then
+               kill -TERM "$(cat ${SH_HAPROXY_PIDFILE})"
+               rm "${SH_HAPROXY_PIDFILE}"
+       fi
+
+       pkill --signal SIGUSR1 haproxy
+       wait
+}
+
+sh_wrk_run ()
+{
+       _arg_ratio="${1}"
+
+       echo "--- rate-limit ${_arg_ratio} --------------------------------------------------"
+       wrk -c8 -d300 -t8 --latency http://localhost:10080/index.html
+       echo "----------------------------------------------------------------------"
+       echo
+
+       sleep 10
+}
+
+
+command -v thttpd >/dev/null 2>&1 || sh_exit "thttpd: command not found" 5
+command -v wrk >/dev/null 2>&1    || sh_exit "wrk: command not found" 6
+
+mkdir -p "${SH_LOG_DIR}" || sh_exit "${SH_LOG_DIR}: Cannot create log directory" 1
+
+if test "${SH_ARG_CFG}" = "all"; then
+       "${0}" sa sa    > "${SH_LOG_DIR}/README-speed-sa"
+       "${0}" cmp cmp  > "${SH_LOG_DIR}/README-speed-cmp"
+       "${0}" ctx ctx  > "${SH_LOG_DIR}/README-speed-ctx"
+       "${0}" fe-be fe > "${SH_LOG_DIR}/README-speed-fe-be"
+       exit 0
+fi
+
+test -z "${SH_ARG_CFG}" -o -z "${SH_ARG_DIR}" && sh_exit "${SH_USAGE_MSG}" 4
+test -f "run-${SH_ARG_CFG}.sh"                || sh_exit "run-${SH_ARG_CFG}.sh: No such configuration script" 2
+test -d "${SH_ARG_DIR}"                       || sh_exit "${SH_ARG_DIR}: No such directory" 3
+
+trap sh_exit INT TERM
+
+sh_backup_make "${SH_ARG_DIR}"
+sh_httpd_run
+for _var_ratio in 100.0 75.0 50.0 25.0 10.0 2.5 0.0 disabled off; do
+       sh_haproxy_run "${SH_ARG_CFG}" "${SH_ARG_DIR}" "${_var_ratio}"
+       sh_wrk_run "${_var_ratio}"
+       sh_haproxy_stop
+done
+sh_httpd_stop
+sh_exit "" 0