version: "3.8"
services:
etcd:
image: bitnami/etcd:3.5.11
environment:
- ALLOW_NONE_AUTHENTICATION=yes
- ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
volumes: [etcd_data:/etcd-data]
ports: ["2379:2379"]
apisix:
image: apache/apisix:3.9.0-debian
environment:
APISIX_DEPLOYMENT_ETCD_HOST: '["http://etcd:2379"]'
volumes:
- ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
- ./plugins/w3c-trace-inject.lua:/usr/local/apisix/apisix/plugins/w3c-trace-inject.lua:ro
- logs:/usr/local/apisix/logs
command: ["sh","-c","rm -f /usr/local/apisix/logs/worker_events.sock && apisix start"]
ports: ["9080:9080","9443:9443","9180:9180"]
depends_on: [etcd]
dashboard:
image: apache/apisix-dashboard:3.0.0-alpine
environment:
APISIX_LISTEN_ADDRESS=apisix:9180
ETCD_HOST=etcd:2379 # ⚠ 不要带 http://
DEFAULT_ADMIN_PASSWORD=admin
ports: ["9000:9000"]
depends_on: [apisix]
volumes:
etcd_data:
logs:
--
-- 插件名称 : w3c-trace-inject
-- APISIX 3.9
-- 功能 : 为所有请求注入 / 校验 traceparent 与 tracestate,并把 traceparent 回写到响应头
--
local core = require("apisix.core")
local random = require("resty.random")
local str = require("resty.string")
local plugin_name = "w3c-trace-inject"
------------------------------------------------------------------------
-- 插件元信息
------------------------------------------------------------------------
local _M = {
version = 0.3, -- ↑ 0.3:新增 header_filter
priority = 10000,
name = plugin_name,
schema = { type = "object" }, -- 无需额外配置
}
------------------------------------------------------------------------
-- 工具函数
------------------------------------------------------------------------
local function gen_hex(bytes) -- 将随机字节转 16 进制
return str.to_hex(random.bytes(bytes, true))
end
-- traceparent 必须形如 00-32hex-16hex-2hex
local function valid_traceparent(tp)
return tp
and tp:match("^00%-%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%-%x%x$")
end
-- tracestate 基础校验:≤512 字节,逗号分隔的 key=value,key 只能小写字母数字和 `_ - * /`
local function valid_tracestate(ts)
if not ts or #ts > 512 then return false end
for entry in ts:gmatch("[^,]+") do
local key, val = entry:match("^([%l%d%-%_%*/]+)=([%g]+)$")
if not key or not val then
return false
end
end
return true
end
------------------------------------------------------------------------
-- access 阶段:注入 / 校验请求头
------------------------------------------------------------------------
function _M.access(conf, ctx)
local headers = core.request.headers(ctx)
-- 1️⃣ traceparent --------------------------------------------------
local tp = headers["traceparent"]
if not valid_traceparent(tp) then
local trace_id = gen_hex(16) -- 128-bit
local span_id = gen_hex(8) -- 64-bit
tp = string.format("00-%s-%s-01", trace_id, span_id)
core.request.set_header(ctx, "traceparent", tp)
ctx.trace_id = trace_id -- 写入 ctx 供日志等使用
else
ctx.trace_id = tp:sub(4, 35) -- 提取已存在的 trace-id
end
ctx.response_traceparent = tp -- 留给 header_filter
-- 2️⃣ tracestate ---------------------------------------------------
local ts = headers["tracestate"]
if not ts then
ts = "as=1"
core.request.set_header(ctx, "tracestate", ts)
elseif not valid_tracestate(ts) then
core.request.clear_header(ctx, "tracestate")
ts = nil
end
ctx.response_tracestate = ts -- 留给 header_filter
end
------------------------------------------------------------------------
-- header_filter 阶段:把 trace 信息写回响应头
------------------------------------------------------------------------
function _M.header_filter(conf, ctx)
if ctx.response_traceparent then
ngx.header["traceparent"] = ctx.response_traceparent
end
if ctx.response_tracestate then
ngx.header["tracestate"] = ctx.response_tracestate
end
end
return _M
bash-4.2# curl -X PUT http://localhost:9180/apisix/admin/upstreams/1 \ # 添加上游
> -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
> -d '{
> "scheme":"http",
> "type":"roundrobin",
> "pass_host":"rewrite",
> "upstream_host":"10.244.191.219",
> "nodes":{"10.244.191.219:8080":1}
> }'
{"key":"/apisix/upstreams/1","value":{"id":"1","scheme":"http","hash_on":"vars","pass_host":"rewrite","upstream_host":"10.244.191.219","type":"roundrobin","create_time":1747881772,"nodes":{"10.244.191.219:8080":1},"update_time":1747881772}}
-bash-4.2# curl -X PUT http://localhost:9180/apisix/admin/routes/1 \ # 添加路由
> -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' \
> -d '{
> "uri":"/rec/v2/testip",
> "methods":["GET","POST"],
> "upstream_id":1,
> "plugins":{"w3c-trace-inject":{}}
> }'
{"key":"/apisix/routes/1","value":{"upstream_id":1,"id":"1","status":1,"priority":0,"plugins":{"w3c-trace-inject":{}},"create_time":1747881778,"update_time":1747881778,"methods":["GET","POST"],"uri":"/rec/v2/testip"}}
-bash-4.2#
-bash-4.2# curl -i http://localhost:9080/rec/v2/testip #测试接口
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 332
Connection: keep-alive
Date: Thu, 22 May 2025 02:43:12 GMT
Server: APISIX/3.9.0
traceparent: 00-76be9240e9f1b09cd5f8575b998cf82b-ea282e6c58efb57e-01 # 这里是响应头
tracestate: as=1
{"code":0,"data":{"x-real-ip":"192.168.240.1","tracestate":"as=1","x-forwarded-proto":"http","x-forwarded-host":"localhost","traceparent":"00-76be9240e9f1b09cd5f8575b998cf82b-ea282e6c58efb57e-01","clientIP":"192.168.240.1","host":"10.244.191.219","x-forwarded-port":"9080","user-agent":"curl/7.29.0","accept":"*/*"},"msg":"success"}-bash-4.2#
-bash-4.2#
-bash-4.2# curl -i http://localhost:9080/rec/v2/testip
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Content-Length: 332
Connection: keep-alive
Date: Thu, 22 May 2025 02:43:16 GMT
Server: APISIX/3.9.0
traceparent: 00-c968d17a84e4b2391ecd986259551550-44b21d2b9dcc18e5-01 # 这里是响应头
tracestate: as=1
{"code":0,"data":{"x-real-ip":"192.168.240.1","tracestate":"as=1","x-forwarded-proto":"http","x-forwarded-host":"localhost","traceparent":"00-c968d17a84e4b2391ecd986259551550-44b21d2b9dcc18e5-01","clientIP":"192.168.240.1","host":"10.244.191.219","x-forwarded-port":"9080","user-agent":"curl/7.29.0","accept":"*/*"},"msg":"success"}