From 26948ada885a757bb2e2a036bb45a7cfc76cfe60 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Tue, 29 Sep 2026 09:43:43 -0400 Subject: [PATCH 1/2] fix(tracer): don't mark sni_radixtree_match as an error on an expected alt SNI miss verify_https_client re-runs the SNI router with the Host header as alt_sni and treats a miss as non-fatal, and match_and_set already skips its error log for that case. It still set the span status to ERROR, so any HTTPS request whose Host has no SSL object of its own (e.g. behind a CDN that connects with an origin hostname as SNI) exports a trace containing an error span. Tail samplers that keep traces with errors then keep nearly all of them. The status is now set in the same branch as the error log, so only a caller that passes no alt_sni marks the span. ssl_client_hello_phase, which also passes alt_sni, already marks its own span when the handshake SNI has no certificate. --- apisix/ssl/router/radixtree_sni.lua | 2 +- t/lib/test_otel.lua | 10 +++++ t/plugin/opentelemetry6.t | 66 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index 95e39c0ec4cd..ff223841344f 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -178,8 +178,8 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) -- it is expected that alternative SNI doesn't have a SSL certificate associated -- with it sometimes core.log.error("failed to find any SSL certificate by SNI: ", sni) + span:set_status(tracer.status.ERROR, "failed match SNI") end - span:set_status(tracer.status.ERROR, "failed match SNI") span:finish(api_ctx.ngx_ctx) return false end diff --git a/t/lib/test_otel.lua b/t/lib/test_otel.lua index fb897cc84896..5e8f77812a2f 100644 --- a/t/lib/test_otel.lua +++ b/t/lib/test_otel.lua @@ -82,6 +82,16 @@ local function verify(spans_by_id, expected, actual, path, errors) path, expected.kind, tostring(actual.kind))) end + if expected.status_code then + -- OTLP JSON omits the status code when it is UNSET (0) + local code = actual.status and actual.status.code or 0 + if code ~= expected.status_code then + table.insert(errors, string.format( + "%s: expected status_code=%d, got=%s", + path, expected.status_code, tostring(code))) + end + end + if expected.attributes then local attr_map = get_attr_map(actual) for key, val in pairs(expected.attributes) do diff --git a/t/plugin/opentelemetry6.t b/t/plugin/opentelemetry6.t index db3590673779..ffd91908904d 100644 --- a/t/plugin/opentelemetry6.t +++ b/t/plugin/opentelemetry6.t @@ -304,3 +304,69 @@ opentracing end } } + + + +=== TEST 10: clear file +--- exec +echo '' > ci/pod/otelcol-contrib/data-otlp.json +--- response_body eval +qr// + + + +=== TEST 11: request whose Host has no SSL object, over a connection whose SNI does +--- init_by_lua_block + require "resty.core" + apisix = require("apisix") + core = require("apisix.core") + apisix.http_init() + + local utils = require("apisix.core.utils") + utils.dns_parse = function (domain) + if domain == "test1.com" then + return {address = "127.0.0.2"} + end + error("unknown domain: " .. domain) + end +--- exec +curl -sk --resolve "test.com:1994:127.0.0.1" -H "Host: localhost" https://test.com:1994/opentracing +--- wait: 5 +--- response_body +opentracing + + + +=== TEST 12: the Host re-check in the access phase does not mark sni_radixtree_match as an error +--- config + location /t { + content_by_lua_block { + local otel = require("lib.test_otel") + + local ok, err = otel.verify_tree( + "ci/pod/otelcol-contrib/data-otlp.json", + { + name = "GET /opentracing", + kind = 2, + attributes = { + ["http.status_code"] = "200", + }, + children = { + { + name = "apisix.phase.access", + kind = 2, + children = { + { name = "sni_radixtree_match", kind = 1, status_code = 0 }, + } + }, + } + } + ) + + if not ok then + ngx.say("FAIL:\n" .. err) + else + ngx.say("passed") + end + } + } From 1fae379dfdac1e1ca23bbd636207f514a7ce82a8 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Tue, 29 Sep 2026 09:50:19 -0400 Subject: [PATCH 2/2] docs(ssl): say which match_and_set callers own the miss The comment implied alt_sni always means an expected miss. ssl_client_hello_phase also passes the handshake SNI as alt_sni, and it logs and marks its own span when that lookup fails. --- apisix/ssl/router/radixtree_sni.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apisix/ssl/router/radixtree_sni.lua b/apisix/ssl/router/radixtree_sni.lua index ff223841344f..5d6e9642ad0c 100644 --- a/apisix/ssl/router/radixtree_sni.lua +++ b/apisix/ssl/router/radixtree_sni.lua @@ -174,9 +174,10 @@ function _M.match_and_set(api_ctx, match_only, alt_sni) local sni_rev = sni:reverse() local ok = radixtree_router:dispatch(sni_rev, nil, api_ctx) if not ok then + -- callers that pass alt_sni decide whether a miss is a failure: the Host + -- re-check in verify_https_client expects misses, and ssl_client_hello_phase + -- logs and marks its own span when the handshake SNI has no certificate if not alt_sni then - -- it is expected that alternative SNI doesn't have a SSL certificate associated - -- with it sometimes core.log.error("failed to find any SSL certificate by SNI: ", sni) span:set_status(tracer.status.ERROR, "failed match SNI") end