From 2dda606ed86ab2c3f8066e59adaecb536609f50e Mon Sep 17 00:00:00 2001 From: Nimesh Kulkarni <167794819+GitNimay@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:54:53 +0530 Subject: [PATCH] feat(opa): propagate traceparent to the OPA server The request to OPA only carried `Content-Type`, so when distributed tracing was enabled OPA could not continue the request trace and its spans started a separate trace. The incoming `traceparent` was only visible inside the JSON input body, which OPA does not use for tracing. Forward the current `traceparent` header on the OPA request. When the opentelemetry plugin is enabled it has already injected the APISIX span into that header, so OPA's spans become children of the APISIX span. Fixes #14051 --- apisix/plugins/opa.lua | 3 + docs/en/latest/plugins/opa.md | 2 + docs/zh/latest/plugins/opa.md | 2 + t/plugin/opa4.t | 174 ++++++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 t/plugin/opa4.t diff --git a/apisix/plugins/opa.lua b/apisix/plugins/opa.lua index 790ef5f1a9d2..88c3f2328a45 100644 --- a/apisix/plugins/opa.lua +++ b/apisix/plugins/opa.lua @@ -80,6 +80,9 @@ function _M.access(conf, ctx) body = core.json.encode(body), headers = { ["Content-Type"] = "application/json", + -- propagate the trace context (injected by the tracing plugins + -- if enabled) so OPA spans join the request trace + ["traceparent"] = core.request.header(ctx, "traceparent"), }, keepalive = conf.keepalive, ssl_verify = conf.ssl_verify diff --git a/docs/en/latest/plugins/opa.md b/docs/en/latest/plugins/opa.md index fc702355ee33..195c02a9775a 100644 --- a/docs/en/latest/plugins/opa.md +++ b/docs/en/latest/plugins/opa.md @@ -41,6 +41,8 @@ The `opa` Plugin supports the integration with [Open Policy Agent (OPA)](https:/ Once configured, the OPA engine will evaluate the client request to a protected Route to determine whether the request should have access to the Upstream resource based on the defined policies. +If the request has a W3C `traceparent` header, for example one injected by the [`opentelemetry`](./opentelemetry.md) Plugin, the Plugin forwards it to OPA so that OPA's spans join the same trace. + ## Attributes | Name | Type | Required | Default | Valid values | Description | diff --git a/docs/zh/latest/plugins/opa.md b/docs/zh/latest/plugins/opa.md index 0e9c1e0d569e..84e2ad8ff9b8 100644 --- a/docs/zh/latest/plugins/opa.md +++ b/docs/zh/latest/plugins/opa.md @@ -41,6 +41,8 @@ import TabItem from '@theme/TabItem'; 配置后,OPA 引擎将根据定义的策略评估客户端对受保护路由的请求,以决定是否允许其访问上游资源。 +如果请求携带 W3C `traceparent` 请求头(例如由 [`opentelemetry`](./opentelemetry.md) 插件注入),插件会将其转发给 OPA,使 OPA 的 span 加入同一条链路。 + ## 属性 | 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | diff --git a/t/plugin/opa4.t b/t/plugin/opa4.t new file mode 100644 index 000000000000..43bd67fb8152 --- /dev/null +++ b/t/plugin/opa4.t @@ -0,0 +1,174 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $extra_yaml_config = <<_EOC_; +plugins: + - opa + - opentelemetry +_EOC_ + $block->set_value("extra_yaml_config", $extra_yaml_config); + + # mock OPA: deny and echo back the traceparent header it received + my $config = $block->config // ""; + $config .= <<_EOC_; + location /v1/data/mock { + content_by_lua_block { + local core = require("apisix.core") + ngx.say(core.json.encode({result = { + allow = false, + reason = ngx.var.http_traceparent or "none", + }})) + } + } +_EOC_ + $block->set_value("config", $config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: setup route with opa +--- 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, + [[{ + "plugins": { + "opa": { + "host": "http://127.0.0.1:1984", + "policy": "mock" + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: client traceparent is forwarded to OPA +--- request +GET /hello +--- more_headers +traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 +--- error_code: 403 +--- response_body chomp +00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 + + + +=== TEST 3: no traceparent header is sent when there is none +--- request +GET /hello +--- error_code: 403 +--- response_body chomp +none + + + +=== TEST 4: setup route with opa and opentelemetry +--- 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, + [[{ + "collector": { + "address": "127.0.0.1:4318" + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "plugins": { + "opa": { + "host": "http://127.0.0.1:1984", + "policy": "mock" + }, + "opentelemetry": { + "sampler": { + "name": "always_off" + } + } + }, + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello" + }]] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: OPA receives the APISIX span as parent, not the client one +--- request +GET /hello +--- more_headers +traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01 +--- error_code: 403 +--- response_body_like eval +qr/^00-0af7651916cd43dd8448eb211c80319c-(?!b7ad6b7169203331)[0-9a-f]{16}-0[01]$/