This is an automated email from the ASF dual-hosted git repository.
AlinsRan 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 cbd264a31 fix(opentelemetry): validate x-request-id before using it as
trace_id (#12990)
cbd264a31 is described below
commit cbd264a310732b3a773b7bc2c026d066c60ab6cb
Author: prasun srivastav <[email protected]>
AuthorDate: Fri Jul 10 12:08:13 2026 +0530
fix(opentelemetry): validate x-request-id before using it as trace_id
(#12990)
---
apisix/plugins/opentelemetry.lua | 46 +++++++-
t/plugin/opentelemetry.t | 245 +++++++++++++++++++++++++++++++++++++++
2 files changed, 289 insertions(+), 2 deletions(-)
diff --git a/apisix/plugins/opentelemetry.lua b/apisix/plugins/opentelemetry.lua
index 7ec44edd0..cecf8ba84 100644
--- a/apisix/plugins/opentelemetry.lua
+++ b/apisix/plugins/opentelemetry.lua
@@ -48,6 +48,7 @@ local pairs = pairs
local ipairs = ipairs
local unpack = unpack
local string_format = string.format
+local string_lower = string.lower
local update_time = ngx.update_time
local tostring = tostring
@@ -57,6 +58,29 @@ local lrucache = core.lrucache.new({
local asterisk = string.byte("*", 1)
+-- capture the library's default (random) generator once, so wrapping it below
+-- stays idempotent even when create_tracer_obj re-runs after cache expiry
+local original_new_ids = id_generator.new_ids
+
+
+-- expects an already-lowercased string
+local function is_valid_trace_id(trace_id)
+ if not trace_id or #trace_id ~= 32 then
+ return false
+ end
+
+ if not trace_id:match("^[0-9a-f]+$") then
+ return false
+ end
+
+ -- W3C Trace Context: all-zero trace_id is invalid
+ if trace_id == "00000000000000000000000000000000" then
+ return false
+ end
+
+ return true
+end
+
local metadata_schema = {
type = "object",
properties = {
@@ -231,10 +255,28 @@ end
local function create_tracer_obj(conf, plugin_info)
+ -- id_generator is a shared module, so restore the default before applying
+ -- the override: without this, switching trace_id_source back to "random"
+ -- would keep honoring X-Request-Id until the worker restarts
+ id_generator.new_ids = original_new_ids
+
if plugin_info.trace_id_source == "x-request-id" then
id_generator.new_ids = function()
- local trace_id = core.request.headers()["x-request-id"] or
ngx_var.request_id
- return trace_id, id_generator.new_span_id()
+ local trace_id = core.request.headers()["x-request-id"]
+ or ngx_var.request_id
+
+ -- a duplicated X-Request-Id makes get_headers() return a table;
+ -- only a plain, valid 32-hex string can be used as a trace_id,
+ -- anything else (UUID, table, empty, ...) falls back to the
+ -- default generator instead of crashing the otlp encoder
+ if type(trace_id) == "string" then
+ trace_id = string_lower(trace_id)
+ if is_valid_trace_id(trace_id) then
+ return trace_id, id_generator.new_span_id()
+ end
+ end
+
+ return original_new_ids()
end
end
-- create exporter
diff --git a/t/plugin/opentelemetry.t b/t/plugin/opentelemetry.t
index 8287a99ea..0e2687392 100644
--- a/t/plugin/opentelemetry.t
+++ b/t/plugin/opentelemetry.t
@@ -734,3 +734,248 @@ plugins:
grep apisix.phase.access ci/pod/otelcol-contrib/data-otlp.json | tail -n 1
--- response_body eval
qr/otel-meta-change-second/
+
+
+
+=== TEST 31: reset metadata trace_id_source = x-request-id
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',
+ ngx.HTTP_PUT,
+ [[{
+ "batch_span_processor": {
+ "max_export_batch_size": 1,
+ "inactive_timeout": 0.5
+ },
+ "trace_id_source": "x-request-id",
+ "resource": {
+ "service.name": "APISIX"
+ },
+ "collector": {
+ "address": "127.0.0.1:4318",
+ "request_timeout": 3
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 32: reset route for x-request-id validation
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "name": "route_name",
+ "plugins": {
+ "opentelemetry": {
+ "sampler": {
+ "name": "always_on"
+ }
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin"
+ },
+ "uri": "/opentracing"
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 33: invalid (UUID) x-request-id should not crash
+--- request
+GET /opentracing
+--- more_headers
+X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
+--- wait: 2
+--- response_body
+opentracing
+--- no_error_log
+[error]
+
+
+
+=== TEST 34: invalid x-request-id still exports a valid random trace id
+--- exec
+tail -n 1 ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr/"traceId"\s*:\s*"[0-9a-f]{32}"/
+
+
+
+=== TEST 35: all-zero x-request-id must not be used as trace id
+--- request
+GET /opentracing
+--- more_headers
+X-Request-Id: 00000000000000000000000000000000
+--- wait: 2
+--- response_body
+opentracing
+
+
+
+=== TEST 36: all-zero id is replaced by a non-zero random trace id
+--- exec
+tail -n 1 ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr/"traceId"\s*:\s*"(?!0{32})[0-9a-f]{32}"/
+
+
+
+=== TEST 37: uppercase 32-hex x-request-id is used
+--- request
+GET /opentracing
+--- more_headers
+X-Request-Id: 550E8400E29B41D4A716446655440000
+--- wait: 2
+--- response_body
+opentracing
+
+
+
+=== TEST 38: uppercase 32-hex is lowercased and used as trace id
+--- exec
+tail -n 1 ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr/"traceId"\s*:\s*"550e8400e29b41d4a716446655440000"/
+
+
+
+=== TEST 39: duplicated x-request-id header should not crash
+--- request
+GET /opentracing
+--- more_headers
+X-Request-Id: 550e8400e29b41d4a716446655440000
+X-Request-Id: aabbccddeeff00112233445566778899
+--- wait: 2
+--- response_body
+opentracing
+--- no_error_log
+[error]
+
+
+
+=== TEST 40: missing x-request-id falls back to default generator
+--- request
+GET /opentracing
+--- wait: 2
+--- response_body
+opentracing
+
+
+
+=== TEST 41: missing x-request-id still exports a valid trace id
+--- exec
+tail -n 1 ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr/"traceId"\s*:\s*"[0-9a-f]{32}"/
+
+
+
+=== TEST 42: non-hex x-request-id falls back to default generator
+--- request
+GET /opentracing
+--- more_headers
+X-Request-Id: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+--- wait: 2
+--- response_body
+opentracing
+
+
+
+=== TEST 43: non-hex x-request-id still exports a valid trace id
+--- exec
+tail -n 1 ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr/"traceId"\s*:\s*"[0-9a-f]{32}"/
+
+
+
+=== TEST 44: empty x-request-id falls back to default generator
+--- request
+GET /opentracing
+--- more_headers
+X-Request-Id:
+--- wait: 2
+--- response_body
+opentracing
+--- no_error_log
+[error]
+
+
+
+=== TEST 45: empty x-request-id still exports a valid trace id
+--- exec
+tail -n 1 ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr/"traceId"\s*:\s*"[0-9a-f]{32}"/
+
+
+
+=== TEST 46: switch metadata trace_id_source back to random
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/plugin_metadata/opentelemetry',
+ ngx.HTTP_PUT,
+ [[{
+ "batch_span_processor": {
+ "max_export_batch_size": 1,
+ "inactive_timeout": 0.5
+ },
+ "trace_id_source": "random",
+ "resource": {
+ "service.name": "APISIX"
+ },
+ "collector": {
+ "address": "127.0.0.1:4318",
+ "request_timeout": 3
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+
+
+
+=== TEST 47: after switching to random, send a valid 32-hex x-request-id
+--- request
+GET /opentracing
+--- more_headers
+X-Request-Id: 550e8400e29b41d4a716446655440000
+--- wait: 2
+--- response_body
+opentracing
+
+
+
+=== TEST 48: x-request-id must be ignored once trace_id_source is random
+--- exec
+tail -n 1 ci/pod/otelcol-contrib/data-otlp.json
+--- response_body eval
+qr/"traceId"\s*:\s*"(?!550e8400e29b41d4a716446655440000)[0-9a-f]{32}"/