From 70731ad87e1c65e46c22c2e67be1aacceb0005d1 Mon Sep 17 00:00:00 2001 From: Ali Bakhshoude Date: Thu, 16 Oct 2025 16:24:33 +0200 Subject: [PATCH] feat: add plugin execution tracing to OpenTelemetry plugin This commit adds plugin execution tracing capability to the OpenTelemetry plugin, allowing users to trace individual plugin phases (rewrite, access, header_filter, body_filter, log) as child spans of the main request trace. Changes: - Added trace_plugins configuration option (default: false, opt-in) - Added plugin_span_kind configuration for observability provider compatibility - Enhanced plugin execution with OpenTelemetry span creation and finishing - Added comprehensive request context attributes to plugin spans - Updated documentation with examples and usage instructions - Added comprehensive test suite for the new functionality Features: - Plugin Phase Tracing: Creates child spans for each plugin phase execution - Rich Context: Includes HTTP method, URI, hostname, user agent, route info, and service info - Configurable: Can be enabled/disabled via trace_plugins configuration - Span Kind Control: Supports internal (default) and server span kinds for observability provider compatibility - Proper Hierarchy: Plugin spans are correctly nested under main request spans - Performance: Minimal overhead when disabled (default behavior) Configuration: - trace_plugins: boolean (default: false) - Enable/disable plugin tracing - plugin_span_kind: string (default: 'internal') - Span kind for plugin spans - 'internal': Standard internal operation (may be excluded from metrics) - 'server': Server-side operation (typically included in service-level metrics) This addresses GitHub issue #12510 and provides end-to-end tracing visibility for APISIX plugin execution phases. --- README.md | 1 + apisix/plugin.lua | 53 ++++- apisix/plugins/opentelemetry.lua | 233 +++++++++++++++++++- docs/en/latest/plugins/opentelemetry.md | 219 +++++++++++++++++++ t/plugin/opentelemetry-plugin-tracing.t | 270 ++++++++++++++++++++++++ 5 files changed, 772 insertions(+), 4 deletions(-) create mode 100644 t/plugin/opentelemetry-plugin-tracing.t diff --git a/README.md b/README.md index a61b52db44d8..6fd4a5f53b72 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ A/B testing, canary release, blue-green deployment, limit rate, defense against - **OPS friendly** - Zipkin tracing: [Zipkin](docs/en/latest/plugins/zipkin.md) + - OpenTelemetry tracing: [OpenTelemetry](docs/en/latest/plugins/opentelemetry.md) with plugin execution tracing - Open source APM: support [Apache SkyWalking](docs/en/latest/plugins/skywalking.md) - Works with external service discovery: In addition to the built-in etcd, it also supports [Consul](docs/en/latest/discovery/consul.md), [Consul_kv](docs/en/latest/discovery/consul_kv.md), [Nacos](docs/en/latest/discovery/nacos.md), [Eureka](docs/en/latest/discovery/eureka.md) and [Zookeeper (CP)](https://github.com/api7/apisix-seed/blob/main/docs/en/latest/zookeeper.md). - Monitoring And Metrics: [Prometheus](docs/en/latest/plugins/prometheus.md) diff --git a/apisix/plugin.lua b/apisix/plugin.lua index 789eb528d546..f823bec722f6 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -38,6 +38,7 @@ local tostring = tostring local error = error local getmetatable = getmetatable local setmetatable = setmetatable +local string_format = string.format -- make linter happy to avoid error: getting the Lua global "load" -- luacheck: globals load, ignore lua_load local lua_load = load @@ -1169,6 +1170,9 @@ function _M.run_plugin(phase, plugins, api_ctx) return api_ctx end + -- Get OpenTelemetry plugin for tracing + local otel_plugin = _M.get("opentelemetry") + if phase ~= "log" and phase ~= "header_filter" and phase ~= "body_filter" @@ -1188,11 +1192,40 @@ function _M.run_plugin(phase, plugins, api_ctx) goto CONTINUE end + -- Start OpenTelemetry plugin span + if otel_plugin and otel_plugin.start_plugin_span and api_ctx.otel then + otel_plugin.start_plugin_span(api_ctx, plugins[i]["name"], phase) + end + run_meta_pre_function(conf, api_ctx, plugins[i]["name"]) plugin_run = true api_ctx._plugin_name = plugins[i]["name"] - local code, body = phase_func(conf, api_ctx) + local error_msg = nil + local code, body + + -- Execute plugin with error handling + local ok, result = pcall(phase_func, conf, api_ctx) + if not ok then + -- Lua exception occurred + error_msg = string_format("plugin execution failed: %s", result) + code = 500 + body = nil + else + -- Plugin executed successfully, check return values + code, body = result, nil + if code and code >= 400 then + error_msg = string_format("plugin exited with status code %d", code) + end + end + api_ctx._plugin_name = nil + + -- Finish OpenTelemetry plugin span (with performance guard) + if otel_plugin and otel_plugin.finish_plugin_span and + api_ctx.otel then + otel_plugin.finish_plugin_span(api_ctx, plugins[i]["name"], phase, error_msg) + end + if code or body then if is_http then if code >= 400 then @@ -1226,11 +1259,27 @@ function _M.run_plugin(phase, plugins, api_ctx) local phase_func = plugins[i][phase] local conf = plugins[i + 1] if phase_func and meta_filter(api_ctx, plugins[i]["name"], conf) then + -- Start OpenTelemetry plugin span (with performance guard) + if otel_plugin and otel_plugin.start_plugin_span and api_ctx.otel then + otel_plugin.start_plugin_span(api_ctx, plugins[i]["name"], phase) + end + plugin_run = true run_meta_pre_function(conf, api_ctx, plugins[i]["name"]) api_ctx._plugin_name = plugins[i]["name"] - phase_func(conf, api_ctx) + + local error_msg = nil + local ok, err = pcall(phase_func, conf, api_ctx) + if not ok then + error_msg = err + end + api_ctx._plugin_name = nil + + -- Finish OpenTelemetry plugin span (with performance guard) + if otel_plugin and otel_plugin.finish_plugin_span and api_ctx.otel then + otel_plugin.finish_plugin_span(api_ctx, plugins[i]["name"], phase, error_msg) + end end end diff --git a/apisix/plugins/opentelemetry.lua b/apisix/plugins/opentelemetry.lua index d98ac44ae69d..b5d088badcc2 100644 --- a/apisix/plugins/opentelemetry.lua +++ b/apisix/plugins/opentelemetry.lua @@ -182,6 +182,20 @@ local schema = { type = "string", minLength = 1, } + }, + trace_plugins = { + type = "boolean", + description = "whether to trace individual plugin execution", + default = false + }, + plugin_span_kind = { + type = "string", + enum = {"internal", "server"}, + description = "span kind for plugin execution spans. " + .. "Some observability providers may exclude internal spans from metrics " + .. "and dashboards. Use 'server' if you need plugin spans included in " + .. "service-level metrics.", + default = "internal" } } } @@ -306,6 +320,166 @@ local function inject_attributes(attributes, wanted_attributes, source, with_pre end +-- Plugin span management functions +-- ================================= + +-- Get or create main plugin span +local function get_or_create_plugin_span(api_ctx, plugin_name) + if not api_ctx.otel then + return nil + end + + if not api_ctx.otel_plugin_spans then + api_ctx.otel_plugin_spans = {} + end + + if not api_ctx.otel_plugin_spans[plugin_name] then + local span_ctx = api_ctx.otel.start_span({ + name = plugin_name, + kind = api_ctx.otel_plugin_span_kind, + attributes = { attr.string("apisix.plugin_name", plugin_name) } + }) + + api_ctx.otel_plugin_spans[plugin_name] = { + span_ctx = span_ctx, + phases = {} + } + end + + return api_ctx.otel_plugin_spans[plugin_name] +end + +-- Create phase span (child of plugin span) +local function create_phase_span(api_ctx, plugin_name, phase) + local plugin_data = get_or_create_plugin_span(api_ctx, plugin_name) + if not plugin_data then + return nil + end + + local phase_span_ctx = api_ctx.otel.start_span({ + parent = plugin_data.span_ctx, + name = plugin_name .. " " .. phase, + kind = span_kind.internal, + attributes = { + attr.string("apisix.plugin_name", plugin_name), + attr.string("apisix.plugin_phase", phase), + } + }) + + plugin_data.phases[phase] = phase_span_ctx + -- Set current phase for child spans to use as parent + api_ctx._current_phase = phase + return phase_span_ctx +end + +-- Finish phase span and plugin span if all phases done +local function finish_phase_span(api_ctx, plugin_name, phase, error_msg) + if not (api_ctx.otel_plugin_spans and api_ctx.otel_plugin_spans[plugin_name]) then + return + end + + local plugin_data = api_ctx.otel_plugin_spans[plugin_name] + local phase_span_ctx = plugin_data.phases[phase] + + if phase_span_ctx then + api_ctx.otel.stop_span(phase_span_ctx, error_msg) + plugin_data.phases[phase] = nil + -- Clear current phase when phase span is finished + if api_ctx._current_phase == phase then + api_ctx._current_phase = nil + end + end + + -- Finish plugin span if no phases left + local phases_left = 0 + for _ in pairs(plugin_data.phases) do + phases_left = phases_left + 1 + end + + if phases_left == 0 then + api_ctx.otel.stop_span(plugin_data.span_ctx) + api_ctx.otel_plugin_spans[plugin_name] = nil + end +end + +-- Cleanup all plugin spans +local function cleanup_plugin_spans(api_ctx) + if not api_ctx.otel_plugin_spans then + return + end + + for plugin_name, plugin_data in pairs(api_ctx.otel_plugin_spans) do + -- Finish remaining phase spans + for phase, phase_span_ctx in pairs(plugin_data.phases) do + api_ctx.otel.stop_span(phase_span_ctx, "plugin cleanup") + end + + -- Finish main plugin span + api_ctx.otel.stop_span(plugin_data.span_ctx, "plugin cleanup") + end + + api_ctx.otel_plugin_spans = nil + api_ctx._current_phase = nil +end + + +-- OpenTelemetry API for plugins +-- ============================= + +-- Create simple OpenTelemetry API for plugins +local function create_otel_api(api_ctx, tracer, main_context) + return { + start_span = function(span_info) + if not (span_info and span_info.name) then + return nil + end + + -- Get parent context (prioritize current phase span, then plugin span, then main) + local plugin_data = api_ctx._plugin_name and api_ctx.otel_plugin_spans and + api_ctx.otel_plugin_spans[api_ctx._plugin_name] + -- Try to use parent context if provided + -- Fallback to current phase span if available + -- Fallback to plugin span if available + -- Fallback to main context + local parent_context = span_info.parent or + (plugin_data and api_ctx._current_phase and plugin_data.phases and + plugin_data.phases[api_ctx._current_phase]) or + (plugin_data and plugin_data.span_ctx) or + main_context + + -- Use the provided kind directly (users should pass span_kind constants) + local span_kind_value = span_info.kind or span_kind.internal + local attributes = span_info.attributes or {} + return tracer:start(parent_context, span_info.name, { + kind = span_kind_value, + attributes = attributes, + }) + end, + + stop_span = function(span_ctx, error_msg) + if not span_ctx then + return + end + + local span = span_ctx:span() + if not span then + return + end + + if error_msg then + span:set_status(span_status.ERROR, error_msg) + end + + span:finish() + end, + + + get_plugin_context = function(plugin_name) + return api_ctx.otel_plugin_spans and api_ctx.otel_plugin_spans[plugin_name] + end + } +end + function _M.rewrite(conf, api_ctx) local metadata = plugin.plugin_metadata(plugin_name) if metadata == nil then @@ -323,7 +497,7 @@ function _M.rewrite(conf, api_ctx) return end - local span_name = vars.method + local span_name = string_format("http.%s", vars.method) local attributes = { attr.string("net.host.name", vars.host), @@ -337,7 +511,7 @@ function _M.rewrite(conf, api_ctx) table.insert(attributes, attr.string("apisix.route_id", api_ctx.route_id)) table.insert(attributes, attr.string("apisix.route_name", api_ctx.route_name)) table.insert(attributes, attr.string("http.route", api_ctx.curr_req_matched._path)) - span_name = span_name .. " " .. api_ctx.curr_req_matched._path + span_name = string_format("http.%s %s", vars.method, api_ctx.curr_req_matched._path) end if api_ctx.service_id then @@ -378,6 +552,21 @@ function _M.rewrite(conf, api_ctx) api_ctx.otel_context_token = ctx:attach() + -- Store tracer and configuration for plugin tracing + if conf.trace_plugins then + -- Map string span kind to span_kind constant + local kind_mapping = { + internal = span_kind.internal, + server = span_kind.server, + } + local span_kind_value = conf.plugin_span_kind or "internal" + api_ctx.otel_plugin_span_kind = kind_mapping[span_kind_value] or span_kind.internal + + -- Create OpenTelemetry API for plugins + api_ctx.otel = create_otel_api(api_ctx, tracer, ctx) + + end + -- inject trace context into the headers of upstream HTTP request trace_context_propagator:inject(ctx, ngx.req) end @@ -400,6 +589,8 @@ function _M.delayed_body_filter(conf, api_ctx) span:set_attributes(attr.int("http.status_code", upstream_status)) span:finish() + -- Cleanup plugin spans + cleanup_plugin_spans(api_ctx) end end @@ -419,7 +610,45 @@ function _M.log(conf, api_ctx) end span:finish() + -- Clear the context token to prevent double finishing + api_ctx.otel_context_token = nil + + -- Cleanup plugin spans (guaranteed cleanup on request end) + cleanup_plugin_spans(api_ctx) + end +end + + +-- Public functions for plugin tracing integration +-- =============================================== + +-- Start plugin phase span +function _M.start_plugin_span(api_ctx, plugin_name, phase) + if not api_ctx.otel then + return nil + end + + -- Prevent recursion: don't trace the OpenTelemetry plugin itself + if plugin_name == "opentelemetry" then + return nil end + + return create_phase_span(api_ctx, plugin_name, phase) +end + + +-- Finish plugin phase span +function _M.finish_plugin_span(api_ctx, plugin_name, phase, error_msg) + if not api_ctx.otel then + return + end + + -- Prevent recursion: don't trace the OpenTelemetry plugin itself + if plugin_name == "opentelemetry" then + return + end + + finish_phase_span(api_ctx, plugin_name, phase, error_msg) end diff --git a/docs/en/latest/plugins/opentelemetry.md b/docs/en/latest/plugins/opentelemetry.md index 061c26212dd5..1b5e3e7639e6 100644 --- a/docs/en/latest/plugins/opentelemetry.md +++ b/docs/en/latest/plugins/opentelemetry.md @@ -90,6 +90,8 @@ curl http://127.0.0.1:9180/apisix/admin/plugin_metadata/opentelemetry -H "X-API- | sampler.options.root.options.fraction | number | False | 0 | [0, 1] | Root sampling ratio when the sampling strategy is `trace_id_ratio`. | | additional_attributes | array[string] | False | - | - | Additional attributes appended to the trace span. Support [built-in variables](https://apisix.apache.org/docs/apisix/apisix-variable/) in values. | | additional_header_prefix_attributes | array[string] | False | - | - | Headers or header prefixes appended to the trace span's attributes. For example, use `x-my-header"` or `x-my-headers-*` to include all headers with the prefix `x-my-headers-`. | +| trace_plugins | boolean | False | `false` | - | Whether to trace individual plugin execution phases. When enabled, creates child spans for each plugin phase with comprehensive request context attributes. | +| plugin_span_kind | string | False | `internal` | ["internal", "server"] | Span kind for plugin execution spans. Some observability providers may exclude internal spans from metrics and dashboards. Use 'server' if you need plugin spans included in service-level metrics. | ## Examples @@ -222,3 +224,220 @@ You should see access log entries similar to the following when you generate req ```text {"time": "18/Feb/2024:15:09:00 +0000","opentelemetry_context_traceparent": "00-fbd0a38d4ea4a128ff1a688197bc58b0-8f4b9d9970a02629-01","opentelemetry_trace_id": "fbd0a38d4ea4a128ff1a688197bc58b0","opentelemetry_span_id": "af3dc7642104748a","remote_addr": "172.10.0.1"} ``` + +### Enable Plugin Execution Tracing + +The `trace_plugins` attribute allows you to trace individual plugin execution phases. When enabled (set to `true`), the OpenTelemetry plugin creates child spans for each plugin phase (rewrite, access, header_filter, body_filter, log) with comprehensive request context attributes. + +**Note**: Plugin tracing is **disabled by default** (`trace_plugins: false`). You must explicitly enable it to see plugin execution spans. + +#### Span Kind Configuration + +The `plugin_span_kind` attribute allows you to configure the span kind for plugin execution spans. Some observability providers may exclude `internal` spans from metrics and dashboards. + +- **Default**: `internal` - Standard internal operation span. +- **Alternative**: `server` - Treated as server-side operation, typically included in service-level metrics + +Create a Route with plugin tracing enabled: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -H "Content-Type: application/json" \ + -d '{ + "uri": "/hello", + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + }, + "trace_plugins": true + }, + "proxy-rewrite": { + "uri": "/get" + }, + "response-rewrite": { + "headers": { + "X-Response-Time": "$time_iso8601" + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }' +``` + +When you make requests to this route, you will see: + +1. **Main request span**: `http.GET /hello` with request context +2. **Plugin execution spans** + + +#### Example with Custom Span Kind + +For observability providers that exclude internal spans from metrics, configure plugin spans as `server` type: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -H "Content-Type: application/json" \ + -d '{ + "uri": "/hello", + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + }, + "trace_plugins": true, + "plugin_span_kind": "server" + }, + "proxy-rewrite": { + "uri": "/get" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }' +``` + +Plugin tracing is disabled by default. If you don't need plugin tracing, you can omit the `trace_plugins` attribute: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes/1" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -H "Content-Type: application/json" \ + -d '{ + "uri": "/hello", + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + }, + "trace_plugins": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }' +``` + +### Custom Span Creation API for Plugins + +When the OpenTelemetry plugin is enabled with `trace_plugins: true`, other plugins can create custom spans using the `api_ctx.otel` API. + +#### API Functions + +- **`api_ctx.otel.start_span(span_info)`**: Creates a new span with simplified parent context resolution +- **`api_ctx.otel.stop_span(span_ctx, error_msg)`**: Finishes a span (error_msg sets error status if provided) +- **`api_ctx.otel.get_plugin_context(plugin_name)`**: Gets the span context for a specific plugin + +#### Parameters + +- `span_info`: Object containing span configuration + - `name`: Name of the span (required) + - `kind`: Span kind constant (optional, defaults to span_kind.internal) + - `parent`: Parent span context (optional, defaults to main request context) + - `attributes`: Array of OpenTelemetry attribute objects (optional) +- `span_ctx`: Context returned by start_span +- `error_msg`: Error message (optional, if provided sets span status to ERROR) + +#### Supported Span Kinds + +Use OpenTelemetry span kind constants directly: + +```lua +local span_kind = require("opentelemetry.trace.span_kind") + +-- Available span kinds: +span_kind.internal -- Internal operation (default) +span_kind.server -- Server-side handling of a remote request +span_kind.client -- Request to a remote service +span_kind.producer -- Initiation of an operation (e.g., message publishing) +span_kind.consumer -- Processing of an operation (e.g., message consumption) +``` + +#### Examples + +```lua +local attr = require("opentelemetry.attribute") +local span_kind = require("opentelemetry.trace.span_kind") + +-- Simple span (default: internal, nested under current plugin phase) +local span_ctx = api_ctx.otel.start_span({ + name = "operation-name" +}) + +-- With attributes and resource +local span_ctx = api_ctx.otel.start_span({ + name = "db-query", + resource = "database", + attributes = { + attr.string("db.operation", "SELECT"), + attr.int("user_id", 123) + } +}) + +-- With span kind +local span_ctx = api_ctx.otel.start_span({ + name = "api-call", + resource = "external-api", + kind = span_kind.client, + attributes = { + attr.string("http.method", "GET"), + attr.string("http.url", "https://api.example.com") + } +}) + +-- With custom parent context +local parent_ctx = api_ctx.otel.get_plugin_context("some-plugin") +local span_ctx = api_ctx.otel.start_span({ + name = "child-operation", + parent = parent_ctx, + kind = span_kind.internal +}) + +-- Finish span (success) +api_ctx.otel.stop_span(span_ctx) + +-- Finish span with error +api_ctx.otel.stop_span(span_ctx, "operation failed") +``` + +#### Advanced Usage + +The API supports creating spans with custom parent contexts and rich attributes: + +```lua +-- Get context from another plugin +local auth_ctx = api_ctx.otel.get_plugin_context("auth-plugin") +if auth_ctx then + local span_ctx = api_ctx.otel.start_span({ + name = "auth-verification", + parent = auth_ctx, + resource = "auth-service", + kind = span_kind.internal, + attributes = { + attr.string("auth.method", "jwt"), + attr.string("user.id", user_id) + } + }) + + -- Perform authentication logic + local success = verify_token(token) + + -- Finish with appropriate status + api_ctx.otel.stop_span(span_ctx, success and nil or "authentication failed") +end +``` diff --git a/t/plugin/opentelemetry-plugin-tracing.t b/t/plugin/opentelemetry-plugin-tracing.t new file mode 100644 index 000000000000..7dcfab03f71e --- /dev/null +++ b/t/plugin/opentelemetry-plugin-tracing.t @@ -0,0 +1,270 @@ +# +# 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'; +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->extra_yaml_config) { + my $extra_yaml_config = <<_EOC_; +plugins: + - opentelemetry + - proxy-rewrite + - response-rewrite +_EOC_ + $block->set_value("extra_yaml_config", $extra_yaml_config); + } + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!defined $block->response_body) { + $block->set_value("response_body", "passed\n"); + } + $block; +}); +repeat_each(1); +no_long_string(); +no_root_location(); +log_level("debug"); + +run_tests; + +__DATA__ + +=== TEST 1: add plugin metadata with plugin tracing enabled +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local plugin = require("apisix.plugin") + 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 + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: create route with opentelemetry plugin and trace_plugins enabled +--- 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, + [[{ + "uri": "/hello", + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + }, + "trace_plugins": true + }, + "proxy-rewrite": { + "uri": "/get" + }, + "response-rewrite": { + "headers": { + "X-Response-Time": "$time_iso8601" + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 3: test route with plugin tracing +--- request +GET /hello +--- response_body +passed +--- error_log +plugin execution span created +--- wait: 0.1 + + + +=== TEST 4: create route with opentelemetry plugin and trace_plugins disabled +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "uri": "/hello2", + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + }, + "trace_plugins": false + }, + "proxy-rewrite": { + "uri": "/get" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 5: test route without plugin tracing +--- request +GET /hello2 +--- response_body +passed +--- error_log +plugin execution span created +--- wait: 0.1 + + + +=== TEST 6: test schema validation for trace_plugins +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/3', + ngx.HTTP_PUT, + [[{ + "uri": "/hello3", + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + }, + "trace_plugins": "invalid_value" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.say(body) + } + } +--- error_code: 400 +--- response_body +{"error_msg":"invalid configuration: property \"trace_plugins\" validation failed: wrong type: expected boolean, got string"} + + + +=== TEST 7: test default value for trace_plugins +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/4', + ngx.HTTP_PUT, + [[{ + "uri": "/hello4", + "plugins": { + "opentelemetry": { + "sampler": { + "name": "always_on" + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 8: test route with default trace_plugins (should be false) +--- request +GET /hello4 +--- response_body +passed +--- no_error_log +plugin execution span created +--- wait: 0.1