This is an automated email from the ASF dual-hosted git repository.

baoyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 5ad1b0ee1 fix: correct span handling in tracing logic (#13008)
5ad1b0ee1 is described below

commit 5ad1b0ee1e179e2d8049d524a5a0ab708e0eade4
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Fri Feb 13 07:29:45 2026 +0545

    fix: correct span handling in tracing logic (#13008)
---
 apisix/plugins/opentelemetry.lua   |   2 +-
 apisix/secret.lua                  |   2 +-
 apisix/tracer.lua                  |   5 +-
 apisix/utils/span.lua              |   2 +-
 ci/pod/otelcol-contrib/config.yaml |   1 +
 t/lib/test_otel.lua                | 135 +++++++++++++++++++++++++++++++++++++
 t/plugin/opentelemetry6.t          |  80 +++++++++++++---------
 7 files changed, 189 insertions(+), 38 deletions(-)

diff --git a/apisix/plugins/opentelemetry.lua b/apisix/plugins/opentelemetry.lua
index 487c14381..3349ed6ec 100644
--- a/apisix/plugins/opentelemetry.lua
+++ b/apisix/plugins/opentelemetry.lua
@@ -337,7 +337,7 @@ function _M.rewrite(conf, api_ctx)
         -- new attributes
         attr.string("http.request.method", vars.method),
         attr.string("url.scheme", vars.scheme),
-        attr.string("uri.path", vars.uri),
+        attr.string("url.path", vars.uri),
         attr.string("user_agent.original", vars.http_user_agent),
     }
 
diff --git a/apisix/secret.lua b/apisix/secret.lua
index 4af7c7dd4..6c224f05e 100644
--- a/apisix/secret.lua
+++ b/apisix/secret.lua
@@ -150,12 +150,12 @@ local function fetch_by_uri_secret(secret_uri)
         return nil, "no secret conf, secret_uri: " .. secret_uri
     end
 
-    local span = tracer.start(ngx.ctx, "fetch_secret", tracer.kind.client)
     local ok, sm = pcall(require, "apisix.secret." .. opts.manager)
     if not ok then
         return nil, "no secret manager: " .. opts.manager
     end
 
+    local span = tracer.start(ngx.ctx, "fetch_secret", tracer.kind.client)
     local value, err = sm.get(conf, opts.key)
     if err then
         span:set_status(tracer.status.ERROR, err)
diff --git a/apisix/tracer.lua b/apisix/tracer.lua
index 8bee81712..ca47730c5 100644
--- a/apisix/tracer.lua
+++ b/apisix/tracer.lua
@@ -31,7 +31,6 @@ end
 local _M = {
     kind = span_kind,
     status = span_status,
-    span_state = {},
 }
 
 function _M.start(ctx, name, kind)
@@ -44,6 +43,8 @@ function _M.start(ctx, name, kind)
         tracing = tablepool.fetch("tracing", 0, 8)
         tracing.spans = tablepool.fetch("tracing_spans", 20, 0)
         ctx.tracing = tracing
+        -- create a dummy root span as the invisible parent of all top-level 
spans
+        span.new(ctx, "root", nil)
     end
     if tracing.skip then
         return noop_span
@@ -56,7 +57,7 @@ end
 
 function _M.finish_all(ctx, code, message)
     local tracing = ctx.tracing
-    if not tracing then
+    if not tracing or not tracing.current_span then
         return
     end
 
diff --git a/apisix/utils/span.lua b/apisix/utils/span.lua
index 11ee50fd9..65427ad1a 100644
--- a/apisix/utils/span.lua
+++ b/apisix/utils/span.lua
@@ -97,7 +97,7 @@ end
 
 function _M.set_attributes(self, ...)
     if not self.attributes then
-        self.attributes = table.new(10, 0)
+        self.attributes = new_tab(10, 0)
     end
     local count = select('#', ...)
     for i = 1, count do
diff --git a/ci/pod/otelcol-contrib/config.yaml 
b/ci/pod/otelcol-contrib/config.yaml
index 6068f4cda..4d8dc9484 100644
--- a/ci/pod/otelcol-contrib/config.yaml
+++ b/ci/pod/otelcol-contrib/config.yaml
@@ -25,6 +25,7 @@ receivers:
 exporters:
   file:
     path: /etc/otelcol-contrib/data-otlp.json
+    append: true
 service:
   pipelines:
     traces:
diff --git a/t/lib/test_otel.lua b/t/lib/test_otel.lua
new file mode 100644
index 000000000..0bbbf1645
--- /dev/null
+++ b/t/lib/test_otel.lua
@@ -0,0 +1,135 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local cjson = require("cjson")
+
+local _M = {}
+
+
+-- Parse a data-otlp.json file (one JSON object per line) into a spans_by_id 
table.
+local function parse_spans(filepath)
+    local file = io.open(filepath, "rb")
+    if not file then
+        return nil, "cannot open " .. filepath
+    end
+
+    local spans_by_id = {}
+    for line in file:lines() do
+        if line and #line > 0 then
+            local ok, data = pcall(cjson.decode, line)
+            if ok and data.resourceSpans then
+                for _, rs in ipairs(data.resourceSpans) do
+                    for _, ss in ipairs(rs.scopeSpans or {}) do
+                        for _, span in ipairs(ss.spans or {}) do
+                            spans_by_id[span.spanId] = span
+                        end
+                    end
+                end
+            end
+        end
+    end
+    file:close()
+
+    return spans_by_id
+end
+
+
+-- Find a child span of the given parent by name.
+local function find_child(spans_by_id, parent_id, child_name)
+    for _, span in pairs(spans_by_id) do
+        if span.parentSpanId == parent_id and span.name == child_name then
+            return span
+        end
+    end
+    return nil
+end
+
+
+-- Convert span.attributes array into a key -> value map.
+local function get_attr_map(span)
+    local map = {}
+    for _, attr in ipairs(span.attributes or {}) do
+        local v = attr.value
+        map[attr.key] = v.stringValue or v.intValue or v.boolValue
+    end
+    return map
+end
+
+
+-- Recursively verify a span tree node against the expected structure.
+local function verify(spans_by_id, expected, actual, path, errors)
+    if not actual then
+        table.insert(errors, path .. ": span not found")
+        return
+    end
+
+    if expected.kind and actual.kind ~= expected.kind then
+        table.insert(errors, string.format(
+            "%s: expected kind=%d, got=%s",
+            path, expected.kind, tostring(actual.kind)))
+    end
+
+    if expected.attributes then
+        local attr_map = get_attr_map(actual)
+        for key, val in pairs(expected.attributes) do
+            if tostring(attr_map[key]) ~= tostring(val) then
+                table.insert(errors, string.format(
+                    "%s: attr '%s' expected '%s', got '%s'",
+                    path, key, tostring(val), tostring(attr_map[key])))
+            end
+        end
+    end
+
+    if expected.children then
+        for _, child_exp in ipairs(expected.children) do
+            local child = find_child(spans_by_id, actual.spanId, 
child_exp.name)
+            verify(spans_by_id, child_exp, child,
+                   path .. " > " .. child_exp.name, errors)
+        end
+    end
+end
+
+
+-- Main entry point: verify a span tree from a data-otlp.json file.
+-- Returns true on success, or (false, error_string) on failure.
+function _M.verify_tree(filepath, expected_tree)
+    local spans_by_id, err = parse_spans(filepath)
+    if not spans_by_id then
+        return false, err
+    end
+
+    -- find root span (no parentSpanId)
+    local root
+    for _, span in pairs(spans_by_id) do
+        if span.name == expected_tree.name
+           and (not span.parentSpanId or span.parentSpanId == "")
+        then
+            root = span
+            break
+        end
+    end
+
+    local errors = {}
+    verify(spans_by_id, expected_tree, root, expected_tree.name, errors)
+
+    if #errors > 0 then
+        return false, table.concat(errors, "\n")
+    end
+    return true
+end
+
+
+return _M
diff --git a/t/plugin/opentelemetry6.t b/t/plugin/opentelemetry6.t
index 003e191f5..7504af34a 100644
--- a/t/plugin/opentelemetry6.t
+++ b/t/plugin/opentelemetry6.t
@@ -201,36 +201,50 @@ opentracing
 
 
 
-=== TEST 6: check sni_radixtree_match span
---- max_size: 1048576
---- exec
-tail -n 18 ci/pod/otelcol-contrib/data-otlp.json
---- response_body eval
-qr/.*sni_radixtree_match.*/
-
-
-
-=== TEST 7: check resolve_dns span
---- max_size: 1048576
---- exec
-tail -n 18 ci/pod/otelcol-contrib/data-otlp.json
---- response_body eval
-qr/.*resolve_dns.*/
-
-
-
-=== TEST 8: check apisix.phase.access span
---- max_size: 1048576
---- exec
-tail -n 18 ci/pod/otelcol-contrib/data-otlp.json
---- response_body eval
-qr/.*apisix.phase.access.*/
-
-
-
-=== TEST 9: check apisix.phase.header_filter span
---- max_size: 1048576
---- exec
-tail -n 18 ci/pod/otelcol-contrib/data-otlp.json
---- response_body eval
-qr/.*apisix.phase.header_filter.*/
+=== TEST 6: verify span tree structure
+--- 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 = {
+                        ["apisix.route_id"] = "1",
+                        ["http.method"] = "GET",
+                        ["http.status_code"] = "200",
+                    },
+                    children = {
+                        {
+                            name = "ssl_client_hello_phase",
+                            kind = 2,
+                            children = {
+                                { name = "sni_radixtree_match", kind = 1 },
+                            }
+                        },
+                        {
+                            name = "apisix.phase.access",
+                            kind = 2,
+                            children = {
+                                { name = "sni_radixtree_match", kind = 1 },
+                                { name = "http_router_match", kind = 1 },
+                            }
+                        },
+                        { name = "resolve_dns", kind = 1 },
+                        { name = "apisix.phase.header_filter", kind = 2 },
+                        { name = "apisix.phase.body_filter", kind = 2 },
+                        { name = "apisix.phase.log.plugins.opentelemetry", 
kind = 1 },
+                    }
+                }
+            )
+
+            if not ok then
+                ngx.say("FAIL:\n" .. err)
+            else
+                ngx.say("passed")
+            end
+        }
+    }

Reply via email to