From a36b73cc33bddf743551c701a5dbbf2e7278acca Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Mon, 15 Jun 2026 14:22:53 -0500 Subject: [PATCH 1/8] Add per-plugin workload counters Add per-plugin metrics that allow us to track how much work each plugin is doing by counting their invocations, intercept bytes, and intercept transfers. Add proxy.process.plugin..{invocations,bytes, transfers}, keyed by the plugin DSO basename and bounded by the number of loaded plugins. Global plugins load via raw dlopen and previously carried no identity, so they are given a PluginThreadContext around TSPluginInit; the continuations they create then carry plugin identity the same way remap plugins already do. Co-Authored-By: Claude Opus 4.8 --- include/proxy/PluginVC.h | 6 ++ include/proxy/http/remap/PluginDso.h | 26 ++++- src/api/InkContInternal.cc | 9 +- src/proxy/Plugin.cc | 38 +++++++ src/proxy/PluginVC.cc | 16 +++ src/proxy/http/remap/PluginDso.cc | 45 ++++++++ src/proxy/http/remap/RemapPlugins.cc | 2 + .../per_plugin_metrics.test.py | 100 ++++++++++++++++++ .../per_plugin_metrics/rules/global.conf | 20 ++++ 9 files changed, 257 insertions(+), 5 deletions(-) create mode 100644 tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py create mode 100644 tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf diff --git a/include/proxy/PluginVC.h b/include/proxy/PluginVC.h index bfa8b1aa5b8..ba016754e14 100644 --- a/include/proxy/PluginVC.h +++ b/include/proxy/PluginVC.h @@ -38,6 +38,7 @@ #include "proxy/Plugin.h" #include "iocore/net/NetVConnection.h" #include "tscore/ink_atomic.h" +#include "tsutil/Metrics.h" class PluginVCCore; @@ -253,6 +254,11 @@ class PluginVCCore : public Continuation Continuation *connect_to = nullptr; bool connected = false; + // Transport counters of the plugin that created this intercept, captured at alloc(). Registry-owned + // (process-lifetime), so safe to hold raw. Null for core-internal PluginVCs. + ts::Metrics::Counter::AtomicType *_bytes = nullptr; + ts::Metrics::Counter::AtomicType *_transfers = nullptr; + IpEndpoint passive_addr_struct; IpEndpoint active_addr_struct; diff --git a/include/proxy/http/remap/PluginDso.h b/include/proxy/http/remap/PluginDso.h index d3ea8087a2b..5176754878e 100644 --- a/include/proxy/http/remap/PluginDso.h +++ b/include/proxy/http/remap/PluginDso.h @@ -46,16 +46,36 @@ namespace fs = swoc::file; #include "tscore/Ptr.h" +#include "tsutil/Metrics.h" #include "iocore/eventsystem/EventSystem.h" #include "proxy/Plugin.h" +#include + class PluginThreadContext : public RefCountObjInHeap { public: - virtual void acquire() = 0; - virtual void release() = 0; - static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */ + virtual void acquire() = 0; + virtual void release() = 0; + + /** @brief Register this plugin's proxy.process.plugin..* metrics. @a plugin_name is the DSO + * path; only its basename stem is used as . */ + void registerPluginMetrics(std::string_view plugin_name); + + void + countInvocation() + { + if (_invocations != nullptr) { + _invocations->increment(1); + } + } + + ts::Metrics::Counter::AtomicType *_invocations = nullptr; + ts::Metrics::Counter::AtomicType *_bytes = nullptr; + ts::Metrics::Counter::AtomicType *_transfers = nullptr; + + static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */ }; class PluginDso : public PluginThreadContext diff --git a/src/api/InkContInternal.cc b/src/api/InkContInternal.cc index 5d5ac2cdc61..6c2da59d7a2 100644 --- a/src/api/InkContInternal.cc +++ b/src/api/InkContInternal.cc @@ -157,8 +157,13 @@ INKContInternal::handle_event(int event, void *edata) /* set the plugin context */ auto *previousContext = pluginThreadContext; pluginThreadContext = reinterpret_cast(m_context); - int retval = m_event_func((TSCont)this, (TSEvent)event, edata); - pluginThreadContext = previousContext; + // Every TSCont (continuation) callback dispatch flows through here; count it against the owning + // plugin. (Remap doRemap dispatch does not, and is counted in RemapPlugins::run_plugin instead.) + if (pluginThreadContext != nullptr) { + pluginThreadContext->countInvocation(); + } + int retval = m_event_func((TSCont)this, (TSEvent)event, edata); + pluginThreadContext = previousContext; if (edata && event == EVENT_INTERVAL) { Event *e = reinterpret_cast(edata); if (e->period != 0) { diff --git a/src/proxy/Plugin.cc b/src/proxy/Plugin.cc index 0cdb620460d..d6ebc46b483 100644 --- a/src/proxy/Plugin.cc +++ b/src/proxy/Plugin.cc @@ -25,12 +25,15 @@ #include #include #include +#include +#include #include "tscore/ink_platform.h" #include "tscore/ink_file.h" #include "tscore/ParseRules.h" #include "records/RecCore.h" #include "tscore/Layout.h" #include "proxy/Plugin.h" +#include "proxy/http/remap/RemapPluginInfo.h" #include "tscore/ink_cap.h" #include "tscore/Filenames.h" #include @@ -94,6 +97,31 @@ plugin_dir_init() using init_func_t = void (*)(int, char **); +namespace +{ +/** Plugin context for global plugins, which load via raw dlopen() rather than the + * PluginFactory/PluginDso path and so would otherwise have no PluginThreadContext to carry their + * identity. Installed as the thread-local pluginThreadContext around TSPluginInit so the plugin's + * continuations are stamped with it. Global plugins are never unloaded, so acquire()/release() are + * no-ops and instances live for the process lifetime. */ +class GlobalPluginContext : public PluginThreadContext +{ +public: + explicit GlobalPluginContext(std::string_view name) { registerPluginMetrics(name); } + void + acquire() override + { + } + void + release() override + { + } +}; + +// Keeps global-plugin contexts reachable for the process lifetime; mutated single-threaded at startup. +std::vector g_global_plugin_contexts; +} // namespace + static PluginLoadSummary s_plugin_load_summary; const PluginLoadSummary & @@ -215,7 +243,17 @@ single_plugin_init(int argc, char *argv[], bool validateOnly) #endif opterr = 0; optarg = nullptr; + + // Install this plugin's context around TSPluginInit so the continuations it creates carry its + // identity (see GlobalPluginContext). + auto *global_context = new GlobalPluginContext(path); + g_global_plugin_contexts.push_back(global_context); + auto *prev_plugin_context = pluginThreadContext; + pluginThreadContext = global_context; + init(argc, argv); + + pluginThreadContext = prev_plugin_context; } // done elevating access if (plugin_reg_current->plugin_registered) { diff --git a/src/proxy/PluginVC.cc b/src/proxy/PluginVC.cc index 3caa8dc673f..c4f44d4d9cf 100644 --- a/src/proxy/PluginVC.cc +++ b/src/proxy/PluginVC.cc @@ -72,6 +72,7 @@ ****************************************************************************/ #include "proxy/PluginVC.h" +#include "proxy/http/remap/RemapPluginInfo.h" #include "../iocore/net/P_Net.h" #include "tscore/Regression.h" #if TS_HAS_TESTS @@ -466,6 +467,11 @@ PluginVC::transfer_bytes(MIOBuffer *transfer_to, IOBufferReader *transfer_from, total_added += moved; } + // Attribute the bytes moved across this intercept to the owning plugin. + if (core_obj->_bytes != nullptr && total_added > 0) { + core_obj->_bytes->increment(total_added); + } + return total_added; } @@ -522,6 +528,11 @@ PluginVC::process_write_side() return; } + // Past this point there is data to move: count a write-side pass for the owning plugin + if (core_obj->_transfers != nullptr) { + core_obj->_transfers->increment(1); + } + // Check the state of the other side read buffer as well as ntodo int64_t other_ntodo = other_side->read_state.vio.ntodo(); if (other_ntodo == 0) { @@ -1002,6 +1013,11 @@ PluginVCCore::alloc(Continuation *acceptor, int64_t buffer_index, int64_t buffer PluginVCCore *pvc = new PluginVCCore; pvc->init(buffer_index, buffer_water_mark); pvc->connect_to = acceptor; + // Capture the creating plugin's transport counters (registry-owned) for per-plugin accounting. + if (pluginThreadContext != nullptr) { + pvc->_bytes = pluginThreadContext->_bytes; + pvc->_transfers = pluginThreadContext->_transfers; + } return pvc; } diff --git a/src/proxy/http/remap/PluginDso.cc b/src/proxy/http/remap/PluginDso.cc index 4b75f4e475c..0d26e8bd056 100644 --- a/src/proxy/http/remap/PluginDso.cc +++ b/src/proxy/http/remap/PluginDso.cc @@ -29,6 +29,7 @@ #include "proxy/http/remap/PluginDso.h" #include "iocore/eventsystem/Freer.h" +#include "tsutil/Metrics.h" #ifdef PLUGIN_DSO_TESTS #include "unit-tests/plugin_testing_common.h" #else @@ -37,9 +38,14 @@ #define PluginError Error #endif +#include #include +#include +#include #include +using ts::Metrics; + namespace { @@ -55,8 +61,43 @@ concat_error(std::string &error, const std::string &msg) } } +// Derive a metric-safe token from a plugin path: the basename with the extension removed (at the +// last '.'), then any character outside [A-Za-z0-9_-] -- including any remaining '.' -- replaced by +// '_' (e.g. "/.../header_rewrite.so" -> "header_rewrite", "foo.bar.so" -> "foo_bar"). +std::string +plugin_metric_token(std::string_view name) +{ + if (auto slash = name.find_last_of('/'); slash != std::string_view::npos) { + name.remove_prefix(slash + 1); + } + if (auto dot = name.find_last_of('.'); dot != std::string_view::npos) { + name = name.substr(0, dot); + } + + std::string token{name}; + for (auto &c : token) { + if (!(std::isalnum(static_cast(c)) || c == '_' || c == '-')) { + c = '_'; + } + } + if (token.empty()) { + token = "unknown"; + } + return token; +} + } // namespace +void +PluginThreadContext::registerPluginMetrics(std::string_view plugin_name) +{ + std::string prefix = "proxy.process.plugin." + plugin_metric_token(plugin_name) + "."; + + _invocations = Metrics::Counter::createPtr(prefix + "invocations"); + _bytes = Metrics::Counter::createPtr(prefix + "bytes"); + _transfers = Metrics::Counter::createPtr(prefix + "transfers"); +} + PluginDso::PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath) : _configPath(configPath), _effectivePath(effectivePath), _runtimePath(runtimePath) { @@ -139,6 +180,10 @@ PluginDso::load(std::string &error, const fs::path &compilerPath) } PluginDbg(_dbg_ctl(), "plugin '%s' finished loading DSO", _configPath.c_str()); + if (result) { + registerPluginMetrics(_effectivePath.string()); + } + return result; } diff --git a/src/proxy/http/remap/RemapPlugins.cc b/src/proxy/http/remap/RemapPlugins.cc index 9c55ee26659..54c52c03be9 100644 --- a/src/proxy/http/remap/RemapPlugins.cc +++ b/src/proxy/http/remap/RemapPlugins.cc @@ -59,6 +59,8 @@ RemapPlugins::run_plugin(RemapPluginInst *plugin) _s->os_response_plugin_inst = plugin; } + plugin->_plugin.countInvocation(); + HttpTransact::milestone_start_api_time(_s); plugin_retcode = plugin->doRemap(reinterpret_cast(_s->state_machine), &rri); HttpTransact::milestone_update_api_time(_s); diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py new file mode 100644 index 00000000000..acb5890c683 --- /dev/null +++ b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py @@ -0,0 +1,100 @@ +''' +Verify the per-plugin workload counters proxy.process.plugin..{invocations,bytes,transfers}. +''' +# 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. + +Test.Summary = ''' +Verify the per-plugin workload counters proxy.process.plugin..invocations (global and remap +dispatch), .bytes and .transfers (PluginVC intercept transport). +''' + +Test.ContinueOnFail = True + +ts = Test.MakeATSProcess("ts") +server = Test.MakeOriginServer("server") + +request_header = {"headers": "GET / HTTP/1.1\r\nHost: test.example\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} +server.addResponse("sessionfile.log", request_header, response_header) + +# header_rewrite (global) and conf_remap (remap) exercise the two invocation-counter sites; +# generator serves its body through a PluginVC intercept, exercising the bytes and transfers counters. +ts.Setup.CopyAs('rules/global.conf', Test.RunDirectory) +ts.Disk.plugin_config.AddLine(f'header_rewrite.so {Test.RunDirectory}/global.conf') +ts.Disk.remap_config.AddLine( + f'map http://test.example http://127.0.0.1:{server.Variables.Port} ' + f'@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1') +ts.Disk.remap_config.AddLine('map http://gen.example http://127.0.0.1/ @plugin=generator.so') + +ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'plugin', +}) + +# curl as a forward proxy sends an absolute-URI request so the named remap rule (conf_remap) matches; +# the 200 confirms it was served rather than 404'd. +tr = Test.AddTestRun("Drive traffic through global + remap plugins") +tr.Processes.Default.StartBefore(server) +tr.Processes.Default.StartBefore(ts) +tr.MakeCurlCommand(f'-s -D - -o /dev/null --proxy 127.0.0.1:{ts.Variables.port} "http://test.example/"', ts=ts) +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200 OK', 'request should match the remap rule and be served') +tr.StillRunningAfter = server +tr.StillRunningAfter = ts + +# Drive a generator request: it serves a 4096-byte body through a PluginVC intercept. +tr = Test.AddTestRun("Drive traffic through a PluginVC intercept plugin") +tr.MakeCurlCommand(f'-s -o /dev/null --proxy 127.0.0.1:{ts.Variables.port} "http://gen.example/nocache/4096"', ts=ts) +tr.Processes.Default.ReturnCode = 0 +tr.StillRunningAfter = ts +tr.StillRunningAfter = server + +tr = Test.AddTestRun("Global plugin invocation counter is non-zero") +tr.Processes.Default.Env = ts.Env +tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.header_rewrite.invocations' +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + r'proxy\.process\.plugin\.header_rewrite\.invocations [1-9]', 'global header_rewrite invocations should be counted') +tr.StillRunningAfter = ts +tr.StillRunningAfter = server + +tr = Test.AddTestRun("Remap plugin invocation counter is non-zero") +tr.Processes.Default.Env = ts.Env +tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.conf_remap.invocations' +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + r'proxy\.process\.plugin\.conf_remap\.invocations [1-9]', 'remap conf_remap invocations should be counted') +tr.StillRunningAfter = ts +tr.StillRunningAfter = server + +tr = Test.AddTestRun("PluginVC intercept bytes counter is non-zero") +tr.Processes.Default.Env = ts.Env +tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.generator.bytes' +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + r'proxy\.process\.plugin\.generator\.bytes [1-9]', 'generator PluginVC transport bytes should be counted') +tr.StillRunningAfter = ts +tr.StillRunningAfter = server + +tr = Test.AddTestRun("PluginVC intercept transfers counter is non-zero") +tr.Processes.Default.Env = ts.Env +tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.generator.transfers' +tr.Processes.Default.ReturnCode = 0 +tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + r'proxy\.process\.plugin\.generator\.transfers [1-9]', 'generator PluginVC transfer events should be counted') +tr.StillRunningAfter = ts +tr.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf b/tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf new file mode 100644 index 00000000000..b715700328c --- /dev/null +++ b/tests/gold_tests/pluginTest/per_plugin_metrics/rules/global.conf @@ -0,0 +1,20 @@ +# 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. + +# Fire on every response so the global header_rewrite continuation is dispatched (through +# INKContInternal::handle_event) for each transaction, exercising the per-plugin invocation counter. +cond %{SEND_RESPONSE_HDR_HOOK} +set-header X-Per-Plugin-Metrics "1" From e02f904adcb14150998d2c252c4906810cf20702 Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Thu, 18 Jun 2026 16:55:39 -0500 Subject: [PATCH 2/8] test_proxy: drop redundant IpAllow::subjects stub The per-plugin metrics change pulls the real IPAllow.o into test_proxy (Plugin.o now references PluginDso's registerPluginMetrics), so the stub definition of IpAllow::subjects duplicated the real one and tripped AddressSanitizer's ODR check on the Rocky (ASAN) build. Drop the stub; the linked IPAllow.o now provides the symbol. Co-Authored-By: Claude Opus 4.8 --- src/proxy/unit_tests/stub.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/proxy/unit_tests/stub.cc b/src/proxy/unit_tests/stub.cc index a1a95fe8ccb..ba279051a34 100644 --- a/src/proxy/unit_tests/stub.cc +++ b/src/proxy/unit_tests/stub.cc @@ -22,5 +22,3 @@ */ #include "proxy/IPAllow.h" - -uint8_t IpAllow::subjects[IpAllow::Subject::MAX_SUBJECTS]; From c816f361e2d5bdaea748a2a3c97a9ea77fdf5241 Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Thu, 18 Jun 2026 18:03:28 -0500 Subject: [PATCH 3/8] test: scope plugin metric checks to the plugins' own namespace regex_revalidate and lua gold tests diff `traffic_ctl metric match ` against a fixed set, but the new proxy.process.plugin..* workload counters also match that substring, adding lines and breaking the gold compare. Anchor the match to ^plugin.. so it captures only each plugin's own metrics. Co-Authored-By: Claude Opus 4.8 --- tests/gold_tests/pluginTest/lua/metrics.sh | 3 ++- tests/gold_tests/pluginTest/regex_revalidate/metrics.sh | 3 ++- tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/gold_tests/pluginTest/lua/metrics.sh b/tests/gold_tests/pluginTest/lua/metrics.sh index 6aec5bc03b2..0d4c8d86175 100755 --- a/tests/gold_tests/pluginTest/lua/metrics.sh +++ b/tests/gold_tests/pluginTest/lua/metrics.sh @@ -18,7 +18,8 @@ N=60 while (( N > 0 )) do rm -f metrics.out metrics.txt - traffic_ctl metric match lua > metrics.out + # Anchor to this plugin's own metrics; exclude the proxy.process.plugin.* workload counters. + traffic_ctl metric match '^plugin\.lua\.' > metrics.out sleep 1 sed 's/ [0-9][0-9]*//' metrics.out > metrics.txt if diff metrics.txt ${AUTEST_TEST_DIR}/gold/metrics.gold diff --git a/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh b/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh index 4365a0e6e02..4b5afd2f990 100755 --- a/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh +++ b/tests/gold_tests/pluginTest/regex_revalidate/metrics.sh @@ -18,7 +18,8 @@ N=60 while (( N > 0 )) do rm -f metrics.out - traffic_ctl metric match regex_revalidate > metrics.out + # Anchor to this plugin's own metrics; exclude the proxy.process.plugin.* workload counters. + traffic_ctl metric match '^plugin\.regex_revalidate\.' > metrics.out sleep 1 if diff metrics.out ${AUTEST_TEST_DIR}/gold/metrics.gold then diff --git a/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh b/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh index 164a4e45277..c2b4db8634b 100755 --- a/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh +++ b/tests/gold_tests/pluginTest/regex_revalidate/metrics_miss.sh @@ -18,7 +18,8 @@ N=60 while (( N > 0 )) do rm -f metrics.out - traffic_ctl metric match regex_revalidate > metrics.out + # Anchor to this plugin's own metrics; exclude the proxy.process.plugin.* workload counters. + traffic_ctl metric match '^plugin\.regex_revalidate\.' > metrics.out sleep 1 if diff metrics.out ${AUTEST_TEST_DIR}/gold/metrics_miss.gold then From a6ab8ac9683f28f6c9a8188be857e5f9cb9cd078 Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Thu, 18 Jun 2026 18:42:34 -0500 Subject: [PATCH 4/8] PluginVC: count a transfer only when data is actually moved _transfers was incremented before the other_ntodo / lock-miss / buffer-space early returns, so write-side passes that moved zero bytes were counted. Move the increment after transfer_bytes and gate it on a positive byte count, matching the _bytes counter. Co-Authored-By: Claude Opus 4.8 --- src/proxy/PluginVC.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/proxy/PluginVC.cc b/src/proxy/PluginVC.cc index c4f44d4d9cf..38cecb3f0b5 100644 --- a/src/proxy/PluginVC.cc +++ b/src/proxy/PluginVC.cc @@ -528,11 +528,6 @@ PluginVC::process_write_side() return; } - // Past this point there is data to move: count a write-side pass for the owning plugin - if (core_obj->_transfers != nullptr) { - core_obj->_transfers->increment(1); - } - // Check the state of the other side read buffer as well as ntodo int64_t other_ntodo = other_side->read_state.vio.ntodo(); if (other_ntodo == 0) { @@ -575,6 +570,11 @@ PluginVC::process_write_side() return; } + // Count a write-side pass for the owning plugin only when data actually moved to the peer. + if (core_obj->_transfers != nullptr && added > 0) { + core_obj->_transfers->increment(1); + } + write_state.vio.ndone += added; other_side->read_state.vio.ndone += added; From 714abbbf8c5accf58316ebbd7c5064fd7daea6ac Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Thu, 18 Jun 2026 19:12:39 -0500 Subject: [PATCH 5/8] PluginDso: make registerPluginMetrics header-inline GlobalPluginContext in Plugin.cc (ts::proxy) called the out-of-line PluginThreadContext::registerPluginMetrics() defined in PluginDso.cc (ts::http_remap), adding a ts::proxy -> ts::http_remap link dependency. Move the implementation (and its token helper) into the header so the symbol is resolved locally and the cross-library edge goes away. Co-Authored-By: Claude Opus 4.8 --- include/proxy/http/remap/PluginDso.h | 40 ++++++++++++++++++++++++++-- src/proxy/http/remap/PluginDso.cc | 38 -------------------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/include/proxy/http/remap/PluginDso.h b/include/proxy/http/remap/PluginDso.h index 5176754878e..94baaf296ac 100644 --- a/include/proxy/http/remap/PluginDso.h +++ b/include/proxy/http/remap/PluginDso.h @@ -51,6 +51,8 @@ namespace fs = swoc::file; #include "proxy/Plugin.h" +#include +#include #include class PluginThreadContext : public RefCountObjInHeap @@ -60,8 +62,17 @@ class PluginThreadContext : public RefCountObjInHeap virtual void release() = 0; /** @brief Register this plugin's proxy.process.plugin..* metrics. @a plugin_name is the DSO - * path; only its basename stem is used as . */ - void registerPluginMetrics(std::string_view plugin_name); + * path; only its basename stem is used as . Defined inline so callers in ts::proxy (the + * global-plugin context) take no link dependency on ts::http_remap. */ + void + registerPluginMetrics(std::string_view plugin_name) + { + std::string prefix = "proxy.process.plugin." + _metric_token(plugin_name) + "."; + + _invocations = ts::Metrics::Counter::createPtr(prefix + "invocations"); + _bytes = ts::Metrics::Counter::createPtr(prefix + "bytes"); + _transfers = ts::Metrics::Counter::createPtr(prefix + "transfers"); + } void countInvocation() @@ -76,6 +87,31 @@ class PluginThreadContext : public RefCountObjInHeap ts::Metrics::Counter::AtomicType *_transfers = nullptr; static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */ + +private: + /** Derive a metric-safe token from a plugin path: the basename with the extension removed, then any + * character outside [A-Za-z0-9_-] replaced by '_' (e.g. "/.../header_rewrite.so" -> "header_rewrite"). */ + static std::string + _metric_token(std::string_view name) + { + if (auto slash = name.find_last_of('/'); slash != std::string_view::npos) { + name.remove_prefix(slash + 1); + } + if (auto dot = name.find_last_of('.'); dot != std::string_view::npos) { + name = name.substr(0, dot); + } + + std::string token{name}; + for (auto &c : token) { + if (!(std::isalnum(static_cast(c)) || c == '_' || c == '-')) { + c = '_'; + } + } + if (token.empty()) { + token = "unknown"; + } + return token; + } }; class PluginDso : public PluginThreadContext diff --git a/src/proxy/http/remap/PluginDso.cc b/src/proxy/http/remap/PluginDso.cc index 0d26e8bd056..7c3982358f3 100644 --- a/src/proxy/http/remap/PluginDso.cc +++ b/src/proxy/http/remap/PluginDso.cc @@ -38,14 +38,11 @@ #define PluginError Error #endif -#include #include #include #include #include -using ts::Metrics; - namespace { @@ -61,43 +58,8 @@ concat_error(std::string &error, const std::string &msg) } } -// Derive a metric-safe token from a plugin path: the basename with the extension removed (at the -// last '.'), then any character outside [A-Za-z0-9_-] -- including any remaining '.' -- replaced by -// '_' (e.g. "/.../header_rewrite.so" -> "header_rewrite", "foo.bar.so" -> "foo_bar"). -std::string -plugin_metric_token(std::string_view name) -{ - if (auto slash = name.find_last_of('/'); slash != std::string_view::npos) { - name.remove_prefix(slash + 1); - } - if (auto dot = name.find_last_of('.'); dot != std::string_view::npos) { - name = name.substr(0, dot); - } - - std::string token{name}; - for (auto &c : token) { - if (!(std::isalnum(static_cast(c)) || c == '_' || c == '-')) { - c = '_'; - } - } - if (token.empty()) { - token = "unknown"; - } - return token; -} - } // namespace -void -PluginThreadContext::registerPluginMetrics(std::string_view plugin_name) -{ - std::string prefix = "proxy.process.plugin." + plugin_metric_token(plugin_name) + "."; - - _invocations = Metrics::Counter::createPtr(prefix + "invocations"); - _bytes = Metrics::Counter::createPtr(prefix + "bytes"); - _transfers = Metrics::Counter::createPtr(prefix + "transfers"); -} - PluginDso::PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath) : _configPath(configPath), _effectivePath(effectivePath), _runtimePath(runtimePath) { From becf6595e96f66394bc626b6316ee8e049b54a1a Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Sat, 20 Jun 2026 18:31:41 -0500 Subject: [PATCH 6/8] PluginThreadContext: move into ts::proxy in its own file Addresses review on #13278. PluginThreadContext is shared by both remap plugins (PluginDso, ts::http_remap) and global plugins (GlobalPluginContext, ts::proxy), but the link dependency only runs ts::http_remap -> ts::proxy. Pulling it into its own ts::proxy file lets its metric helpers be defined out-of-line without ts::proxy taking a back-dependency on ts::http_remap, which was why they had been left inline in the header. Also switch the metric-token sanitization to std::replace_if. Co-Authored-By: Claude Opus 4.8 --- include/proxy/PluginThreadContext.h | 61 ++++++++++++++++++++++++++ include/proxy/http/remap/PluginDso.h | 61 +------------------------- src/proxy/CMakeLists.txt | 1 + src/proxy/PluginThreadContext.cc | 65 ++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 60 deletions(-) create mode 100644 include/proxy/PluginThreadContext.h create mode 100644 src/proxy/PluginThreadContext.cc diff --git a/include/proxy/PluginThreadContext.h b/include/proxy/PluginThreadContext.h new file mode 100644 index 00000000000..88db0d825dd --- /dev/null +++ b/include/proxy/PluginThreadContext.h @@ -0,0 +1,61 @@ +/** @file + + Per-plugin identity carried on the continuations a plugin creates. + + @section license License + + 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. + */ + +#pragma once + +#include +#include + +#include "tscore/Ptr.h" +#include "tsutil/Metrics.h" + +/** Carries a plugin's identity on the continuations it creates so that + * proxy.process.plugin..* workload counters can be attributed back to the originating + * plugin DSO. + * + * This lives in ts::proxy rather than ts::http_remap because it is shared by both remap plugins + * (PluginDso) and global plugins (GlobalPluginContext, in Plugin.cc). The library dependency only + * runs ts::http_remap -> ts::proxy, so putting it here lets both paths resolve these symbols. */ +class PluginThreadContext : public RefCountObjInHeap +{ +public: + virtual void acquire() = 0; + virtual void release() = 0; + + /** Register this plugin's proxy.process.plugin..* metrics. @a plugin_name is the DSO path; + * only its basename stem (extension removed) is used as . */ + void registerPluginMetrics(std::string_view plugin_name); + + void countInvocation(); + + ts::Metrics::Counter::AtomicType *_invocations = nullptr; + ts::Metrics::Counter::AtomicType *_bytes = nullptr; + ts::Metrics::Counter::AtomicType *_transfers = nullptr; + + static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */ + +private: + /** Derive a metric-safe token from a plugin path: the basename with the extension removed, then any + * character outside [A-Za-z0-9_-] replaced by '_' (e.g. "/.../header_rewrite.so" -> "header_rewrite"). */ + static std::string _metric_token(std::string_view name); +}; diff --git a/include/proxy/http/remap/PluginDso.h b/include/proxy/http/remap/PluginDso.h index 94baaf296ac..68b01f90588 100644 --- a/include/proxy/http/remap/PluginDso.h +++ b/include/proxy/http/remap/PluginDso.h @@ -50,70 +50,11 @@ namespace fs = swoc::file; #include "iocore/eventsystem/EventSystem.h" #include "proxy/Plugin.h" +#include "proxy/PluginThreadContext.h" -#include #include #include -class PluginThreadContext : public RefCountObjInHeap -{ -public: - virtual void acquire() = 0; - virtual void release() = 0; - - /** @brief Register this plugin's proxy.process.plugin..* metrics. @a plugin_name is the DSO - * path; only its basename stem is used as . Defined inline so callers in ts::proxy (the - * global-plugin context) take no link dependency on ts::http_remap. */ - void - registerPluginMetrics(std::string_view plugin_name) - { - std::string prefix = "proxy.process.plugin." + _metric_token(plugin_name) + "."; - - _invocations = ts::Metrics::Counter::createPtr(prefix + "invocations"); - _bytes = ts::Metrics::Counter::createPtr(prefix + "bytes"); - _transfers = ts::Metrics::Counter::createPtr(prefix + "transfers"); - } - - void - countInvocation() - { - if (_invocations != nullptr) { - _invocations->increment(1); - } - } - - ts::Metrics::Counter::AtomicType *_invocations = nullptr; - ts::Metrics::Counter::AtomicType *_bytes = nullptr; - ts::Metrics::Counter::AtomicType *_transfers = nullptr; - - static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */ - -private: - /** Derive a metric-safe token from a plugin path: the basename with the extension removed, then any - * character outside [A-Za-z0-9_-] replaced by '_' (e.g. "/.../header_rewrite.so" -> "header_rewrite"). */ - static std::string - _metric_token(std::string_view name) - { - if (auto slash = name.find_last_of('/'); slash != std::string_view::npos) { - name.remove_prefix(slash + 1); - } - if (auto dot = name.find_last_of('.'); dot != std::string_view::npos) { - name = name.substr(0, dot); - } - - std::string token{name}; - for (auto &c : token) { - if (!(std::isalnum(static_cast(c)) || c == '_' || c == '-')) { - c = '_'; - } - } - if (token.empty()) { - token = "unknown"; - } - return token; - } -}; - class PluginDso : public PluginThreadContext { friend class PluginFactory; diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index 34874f380f1..c0814f6009f 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -28,6 +28,7 @@ add_library( ParentSelectionStrategy.cc ParentSelection.cc Plugin.cc + PluginThreadContext.cc PluginVC.cc ProtocolProbeSessionAccept.cc ProxySession.cc diff --git a/src/proxy/PluginThreadContext.cc b/src/proxy/PluginThreadContext.cc new file mode 100644 index 00000000000..23575025e18 --- /dev/null +++ b/src/proxy/PluginThreadContext.cc @@ -0,0 +1,65 @@ +/** @file + + Per-plugin identity carried on the continuations a plugin creates. + + @section license License + + 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. + */ + +#include "proxy/PluginThreadContext.h" + +#include +#include +#include +#include + +void +PluginThreadContext::registerPluginMetrics(std::string_view plugin_name) +{ + std::string prefix = "proxy.process.plugin." + _metric_token(plugin_name) + "."; + + _invocations = ts::Metrics::Counter::createPtr(prefix + "invocations"); + _bytes = ts::Metrics::Counter::createPtr(prefix + "bytes"); + _transfers = ts::Metrics::Counter::createPtr(prefix + "transfers"); +} + +void +PluginThreadContext::countInvocation() +{ + if (_invocations != nullptr) { + _invocations->increment(1); + } +} + +std::string +PluginThreadContext::_metric_token(std::string_view name) +{ + if (auto slash = name.find_last_of('/'); slash != std::string_view::npos) { + name.remove_prefix(slash + 1); + } + if (auto dot = name.find_last_of('.'); dot != std::string_view::npos) { + name = name.substr(0, dot); + } + + std::string token{name}; + std::replace_if(token.begin(), token.end(), [](unsigned char c) { return !(std::isalnum(c) || c == '_' || c == '-'); }, '_'); + if (token.empty()) { + token = "unknown"; + } + return token; +} From 40426530d0dcb4c684061ac1117546e45fca76ad Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Sat, 20 Jun 2026 18:31:41 -0500 Subject: [PATCH 7/8] test: reorganize per_plugin_metrics as a test class Addresses review on #13278: wrap the per-plugin workload counter checks in a TestPerPluginWorkloadCounters class, matching the class-based gold test idiom. Co-Authored-By: Claude Opus 4.8 --- .../per_plugin_metrics.test.py | 156 +++++++++--------- 1 file changed, 82 insertions(+), 74 deletions(-) diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py index acb5890c683..b6ad60a3e4c 100644 --- a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py +++ b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py @@ -17,6 +17,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import re + Test.Summary = ''' Verify the per-plugin workload counters proxy.process.plugin..invocations (global and remap dispatch), .bytes and .transfers (PluginVC intercept transport). @@ -24,77 +26,83 @@ Test.ContinueOnFail = True -ts = Test.MakeATSProcess("ts") -server = Test.MakeOriginServer("server") - -request_header = {"headers": "GET / HTTP/1.1\r\nHost: test.example\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} -server.addResponse("sessionfile.log", request_header, response_header) - -# header_rewrite (global) and conf_remap (remap) exercise the two invocation-counter sites; -# generator serves its body through a PluginVC intercept, exercising the bytes and transfers counters. -ts.Setup.CopyAs('rules/global.conf', Test.RunDirectory) -ts.Disk.plugin_config.AddLine(f'header_rewrite.so {Test.RunDirectory}/global.conf') -ts.Disk.remap_config.AddLine( - f'map http://test.example http://127.0.0.1:{server.Variables.Port} ' - f'@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1') -ts.Disk.remap_config.AddLine('map http://gen.example http://127.0.0.1/ @plugin=generator.so') - -ts.Disk.records_config.update({ - 'proxy.config.diags.debug.enabled': 0, - 'proxy.config.diags.debug.tags': 'plugin', -}) - -# curl as a forward proxy sends an absolute-URI request so the named remap rule (conf_remap) matches; -# the 200 confirms it was served rather than 404'd. -tr = Test.AddTestRun("Drive traffic through global + remap plugins") -tr.Processes.Default.StartBefore(server) -tr.Processes.Default.StartBefore(ts) -tr.MakeCurlCommand(f'-s -D - -o /dev/null --proxy 127.0.0.1:{ts.Variables.port} "http://test.example/"', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression('200 OK', 'request should match the remap rule and be served') -tr.StillRunningAfter = server -tr.StillRunningAfter = ts - -# Drive a generator request: it serves a 4096-byte body through a PluginVC intercept. -tr = Test.AddTestRun("Drive traffic through a PluginVC intercept plugin") -tr.MakeCurlCommand(f'-s -o /dev/null --proxy 127.0.0.1:{ts.Variables.port} "http://gen.example/nocache/4096"', ts=ts) -tr.Processes.Default.ReturnCode = 0 -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Global plugin invocation counter is non-zero") -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.header_rewrite.invocations' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - r'proxy\.process\.plugin\.header_rewrite\.invocations [1-9]', 'global header_rewrite invocations should be counted') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("Remap plugin invocation counter is non-zero") -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.conf_remap.invocations' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - r'proxy\.process\.plugin\.conf_remap\.invocations [1-9]', 'remap conf_remap invocations should be counted') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("PluginVC intercept bytes counter is non-zero") -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.generator.bytes' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - r'proxy\.process\.plugin\.generator\.bytes [1-9]', 'generator PluginVC transport bytes should be counted') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server - -tr = Test.AddTestRun("PluginVC intercept transfers counter is non-zero") -tr.Processes.Default.Env = ts.Env -tr.Processes.Default.Command = 'traffic_ctl metric get proxy.process.plugin.generator.transfers' -tr.Processes.Default.ReturnCode = 0 -tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( - r'proxy\.process\.plugin\.generator\.transfers [1-9]', 'generator PluginVC transfer events should be counted') -tr.StillRunningAfter = ts -tr.StillRunningAfter = server + +class TestPerPluginWorkloadCounters: + + def __init__(self): + self.setUpOriginServer() + self.setUpTS() + + def setUpOriginServer(self): + self.server = Test.MakeOriginServer("server") + request_header = {"headers": "GET / HTTP/1.1\r\nHost: test.example\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""} + self.server.addResponse("sessionfile.log", request_header, response_header) + + def setUpTS(self): + self.ts = Test.MakeATSProcess("ts") + + # header_rewrite (global) and conf_remap (remap) exercise the two invocation-counter sites; + # generator serves its body through a PluginVC intercept, exercising the bytes and transfers counters. + self.ts.Setup.CopyAs('rules/global.conf', Test.RunDirectory) + self.ts.Disk.plugin_config.AddLine(f'header_rewrite.so {Test.RunDirectory}/global.conf') + self.ts.Disk.remap_config.AddLine( + f'map http://test.example http://127.0.0.1:{self.server.Variables.Port} ' + f'@plugin=conf_remap.so @pparam=proxy.config.url_remap.pristine_host_hdr=1') + self.ts.Disk.remap_config.AddLine('map http://gen.example http://127.0.0.1/ @plugin=generator.so') + + self.ts.Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 0, + 'proxy.config.diags.debug.tags': 'plugin', + }) + + def driveTraffic(self): + # curl as a forward proxy sends an absolute-URI request so the named remap rule (conf_remap) + # matches; the 200 confirms it was served rather than 404'd. + tr = Test.AddTestRun("Drive traffic through global + remap plugins") + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts) + tr.MakeCurlCommand(f'-s -D - -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://test.example/"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression( + '200 OK', 'request should match the remap rule and be served') + tr.StillRunningAfter = self.server + tr.StillRunningAfter = self.ts + + # Drive a generator request: it serves a 4096-byte body through a PluginVC intercept. + tr = Test.AddTestRun("Drive traffic through a PluginVC intercept plugin") + tr.MakeCurlCommand( + f'-s -o /dev/null --proxy 127.0.0.1:{self.ts.Variables.port} "http://gen.example/nocache/4096"', ts=self.ts) + tr.Processes.Default.ReturnCode = 0 + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + def checkMetric(self, description, metric, message): + tr = Test.AddTestRun(description) + tr.Processes.Default.Env = self.ts.Env + tr.Processes.Default.Command = f'traffic_ctl metric get {metric}' + tr.Processes.Default.ReturnCode = 0 + tr.Processes.Default.Streams.stdout = Testers.ContainsExpression(f'{re.escape(metric)} [1-9]', message) + tr.StillRunningAfter = self.ts + tr.StillRunningAfter = self.server + + def checkMetrics(self): + self.checkMetric( + "Global plugin invocation counter is non-zero", 'proxy.process.plugin.header_rewrite.invocations', + 'global header_rewrite invocations should be counted') + self.checkMetric( + "Remap plugin invocation counter is non-zero", 'proxy.process.plugin.conf_remap.invocations', + 'remap conf_remap invocations should be counted') + self.checkMetric( + "PluginVC intercept bytes counter is non-zero", 'proxy.process.plugin.generator.bytes', + 'generator PluginVC transport bytes should be counted') + self.checkMetric( + "PluginVC intercept transfers counter is non-zero", 'proxy.process.plugin.generator.transfers', + 'generator PluginVC transfer events should be counted') + + def run(self): + self.driveTraffic() + self.checkMetrics() + + +TestPerPluginWorkloadCounters().run() From a033680f9da8f56fab99e4163f4a2c2b4737d9c5 Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Sat, 20 Jun 2026 18:49:37 -0500 Subject: [PATCH 8/8] test: gate per_plugin_metrics on its plugins Addresses Copilot review: skip the test unless header_rewrite.so, conf_remap.so and generator.so are present, matching the convention used by other plugin gold tests. Co-Authored-By: Claude Opus 4.8 --- .../pluginTest/per_plugin_metrics/per_plugin_metrics.test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py index b6ad60a3e4c..202412e6219 100644 --- a/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py +++ b/tests/gold_tests/pluginTest/per_plugin_metrics/per_plugin_metrics.test.py @@ -24,6 +24,9 @@ dispatch), .bytes and .transfers (PluginVC intercept transport). ''' +Test.SkipUnless( + Condition.PluginExists('header_rewrite.so'), Condition.PluginExists('conf_remap.so'), Condition.PluginExists('generator.so')) + Test.ContinueOnFail = True