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
+ }
+ }