This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-java.git
The following commit(s) were added to refs/heads/main by this push:
new 64035d609a Add Jetty 12 and Struts 7 (Jakarta) agent plugins (#813)
64035d609a is described below
commit 64035d609a9f5d3bf0ff74eac190a74c0b5cd26d
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Tue Jul 7 11:21:52 2026 +0800
Add Jetty 12 and Struts 7 (Jakarta) agent plugins (#813)
Jetty 12 removed the concrete HttpChannel whose handle() the merged
jetty-server
plugin enhances, and moved request handling to the async, namespace-neutral
core
API Server#handle(Request, Response, Callback). Add a separate
jetty-server-12.x
plugin that enhances it and records an async entry span (finished with the
response status when the wrapped Callback completes).
Struts 7 relocated DefaultActionInvocation from com.opensymphony.xwork2 to
org.apache.struts2 and switched to jakarta.servlet. Add a struts2-7.x plugin
enhancing the new class (a distinct package from struts2-2.x avoids
collision).
Both reuse existing components (JettyServer, Struts2). Add plugin test
scenarios
covering the versions (Jetty 12.0.x/12.1.x; Struts 7.0.x/7.1.x/7.2.x) and
update
the supported list, plugin list and CHANGES. Each scenario drives the full
RPC
validation path in one trace - a server entry span, a client exit span, and
a
second entry span on a nested internal call carrying a cross-process ref -
so
both the entry and the context-extract sides of the plugin are proven, not
just
a lone entry span. The struts scenario uses a lightweight httpheader result
instead of a JSP view to keep the cold first request under the plugin-test
framework's 3s entry-service timeout.
Add test/plugin/CLAUDE.md (plugin-test-first approach, the RPC/client
validation
paths, the one-version-per-minor rule, how the jvm/tomcat containers and
GitHub
Actions lanes pin their JDK/Tomcat base image, and the cold-start timeout
gotcha)
and document the supported jvm/tomcat containers in Plugin-test.md.
---
.github/workflows/plugins-jdk17-test.0.yaml | 1 +
.github/workflows/plugins-jdk17-test.1.yaml | 1 +
.gitignore | 5 +
CHANGES.md | 2 +
apm-sniffer/apm-sdk-plugin/CLAUDE.md | 2 +
.../{ => jetty-server-12.x-plugin}/pom.xml | 36 +++---
.../plugin/jetty/v12/server/HandleInterceptor.java | 100 +++++++++++++++
.../apm/plugin/jetty/v12/server/SwCallback.java | 98 +++++++++++++++
.../define/Jetty12ServerInstrumentation.java | 89 ++++++++++++++
.../src/main/resources/skywalking-plugin.def | 18 +++
apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml | 1 +
apm-sniffer/apm-sdk-plugin/pom.xml | 1 +
.../{jetty-plugin => struts2-7.x-plugin}/pom.xml | 40 +++---
.../apm/plugin/struts2/v7/Struts2Interceptor.java | 82 +++++++++++++
.../struts2/v7/define/Struts2Instrumentation.java | 75 ++++++++++++
.../src/main/resources/skywalking-plugin.def | 18 +++
.../setup/service-agent/java-agent/Plugin-list.md | 2 +
.../setup/service-agent/java-agent/Plugin-test.md | 8 +-
.../service-agent/java-agent/Supported-list.md | 6 +-
test/plugin/CLAUDE.md | 134 +++++++++++++++++++++
.../scenarios/jetty-12.x-scenario/bin/startup.sh | 21 ++++
.../jetty-12.x-scenario/config/expectedData.yaml | 86 +++++++++++++
.../jetty-12.x-scenario/configuration.yml | 22 ++++
test/plugin/scenarios/jetty-12.x-scenario/pom.xml | 128 ++++++++++++++++++++
.../src/main/assembly/assembly.xml | 41 +++++++
.../apm/testcase/jetty12xserver/Application.java | 37 ++++++
.../jetty12xserver/handler/CaseHandler.java | 67 +++++++++++
.../src/main/resources/log4j2.xml | 30 +++++
.../jetty-12.x-scenario/support-version.list | 18 +++
.../struts2.7-scenario/config/expectedData.yaml | 72 +++++++++++
.../scenarios/struts2.7-scenario/configuration.yml | 21 ++++
test/plugin/scenarios/struts2.7-scenario/pom.xml | 96 +++++++++++++++
.../apm/testcase/struts/Case1Action.java | 28 +++++
.../skywalking/apm/testcase/struts/CaseAction.java | 47 ++++++++
.../apm/testcase/struts/HealthCheckServlet.java | 41 +++++++
.../src/main/resources/log4j2.xml | 30 +++++
.../src/main/resources/struts.xml | 46 +++++++
.../src/main/webapp/WEB-INF/web.xml | 44 +++++++
.../struts2.7-scenario/support-version.list | 19 +++
39 files changed, 1578 insertions(+), 35 deletions(-)
diff --git a/.github/workflows/plugins-jdk17-test.0.yaml
b/.github/workflows/plugins-jdk17-test.0.yaml
index 49b3675c01..b62166b155 100644
--- a/.github/workflows/plugins-jdk17-test.0.yaml
+++ b/.github/workflows/plugins-jdk17-test.0.yaml
@@ -79,6 +79,7 @@ jobs:
- grizzly-2.3.x-4.x-workthreadpool-scenario
- jetty-11.x-scenario
- jetty-10.x-scenario
+ - jetty-12.x-scenario
- spring-ai-1.x-scenario
- spring-rabbitmq-scenario
- graphql-20plus-scenario
diff --git a/.github/workflows/plugins-jdk17-test.1.yaml
b/.github/workflows/plugins-jdk17-test.1.yaml
index 6be997dc0e..e4f7144dfd 100644
--- a/.github/workflows/plugins-jdk17-test.1.yaml
+++ b/.github/workflows/plugins-jdk17-test.1.yaml
@@ -72,6 +72,7 @@ jobs:
case:
- spring-6.x-scenario
- spring-boot-3.x-scenario
+ - struts2.7-scenario
- resteasy-6.x-scenario
- gateway-4.x-scenario
- gateway-4.1.2.x-scenario
diff --git a/.gitignore b/.gitignore
index da77f29d42..1efb481266 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,8 @@ packages/
/test/jacoco/*.exec
test/jacoco
.claude/settings.local.json
+
+# Generated by tools/plugin/check-javaagent-plugin-list.sh (not source)
+/javaagent-position.txt
+/tools/plugin/genernate-javaagent-plugin-list.txt
+/tools/plugin/md-javaagent-plugin-list.txt
diff --git a/CHANGES.md b/CHANGES.md
index 9f994787ff..48310efdab 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,8 @@ Release Notes.
* Fix `plugin.http.include_http_headers` not working on Spring Boot 3.x /
Jakarta EE (apache/skywalking#13938).
* Unify the Tomcat plugins into a single `tomcat` plugin (Tomcat 7 - 10) and
the Jetty server plugins into a single `jetty-server` plugin (Jetty 9 - 11),
each supporting both `javax.servlet` and `jakarta.servlet`. Breaking change:
plugin names `tomcat-7.x/8.x` and `tomcat-10.x` are replaced by `tomcat`, and
`jetty-server-9.x` and `jetty-server-11.x` by `jetty-server`; update
`plugin.exclude_plugins` if you reference the old names.
+* Add a Jetty 12 server plugin (`jetty-server-12.x`). Jetty 12 removed the
`HttpChannel` handle target and moved request handling to the async
`Server#handle(Request, Response, Callback)` core API, so it needs a separate
plugin from the merged `jetty-server`.
+* Add a Struts 7 plugin (`struts2-7.x`) for Jakarta Struts, whose
`DefaultActionInvocation` moved to `org.apache.struts2`.
* Added support for Lettuce reactive Redis commands.
* Add Spring AI 1.x plugin and GenAI layer.
* Fix httpclient-5.x plugin injecting sw8 propagation headers into ClickHouse
HTTP requests (port 8123), causing HTTP 400. Add `PROPAGATION_EXCLUDE_PORTS`
config to skip tracing (including header injection) for specified ports in the
classic client interceptor.
diff --git a/apm-sniffer/apm-sdk-plugin/CLAUDE.md
b/apm-sniffer/apm-sdk-plugin/CLAUDE.md
index b839fbe73b..81de9bc670 100644
--- a/apm-sniffer/apm-sdk-plugin/CLAUDE.md
+++ b/apm-sniffer/apm-sdk-plugin/CLAUDE.md
@@ -251,6 +251,8 @@ histogram.addValue(3);
The plugin test framework verifies plugin functionality using Docker
containers with real services and a mock OAP backend.
+> See `test/plugin/CLAUDE.md` for the testing philosophy: a plugin's scenario
(with one-version-per-minor coverage) is the **key** test — it proves the
interceptor's cast/fetch-data logic and version compatibility by running the
real framework. Do **not** over-engineer with many mock-based unit tests for a
plugin.
+
### Environment Requirements
- MacOS/Linux
- JDK 8+
diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml
similarity index 55%
copy from apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
copy to apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml
index 1ac21fb14d..dfc04b5933 100644
--- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
@@ -18,27 +17,36 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
<parent>
+ <artifactId>jetty-plugins</artifactId>
<groupId>org.apache.skywalking</groupId>
- <artifactId>apm-sdk-plugin</artifactId>
<version>9.7.0-SNAPSHOT</version>
</parent>
+ <modelVersion>4.0.0</modelVersion>
- <artifactId>jetty-plugins</artifactId>
- <modules>
- <module>jetty-client-9.x-plugin</module>
- <module>jetty-client-9.0-plugin</module>
- <module>jetty-server-plugin</module>
- <module>jetty-client-11.x-plugin</module>
- </modules>
- <packaging>pom</packaging>
+ <artifactId>apm-jetty-server-12.x-plugin</artifactId>
+ <packaging>jar</packaging>
- <name>jetty-plugin</name>
+ <name>jetty-server-12.x-plugin</name>
<url>http://maven.apache.org</url>
+ <!--
+ Jetty 12 removed the concrete org.eclipse.jetty.server.HttpChannel class
that the merged
+ jetty-server plugin enhances, and redesigned request handling around the
namespace-neutral
+ core API org.eclipse.jetty.server.Server#handle(Request, Response,
Callback), which is async
+ (the response status is only known when the Callback completes). Hence a
separate plugin that
+ does not use the servlet-commons wrapper. Covers Jetty 12.0.x and 12.1.x
(same signature).
+ -->
<properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <jetty-server.version>12.0.36</jetty-server.version>
</properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${jetty-server.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
</project>
diff --git
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java
new file mode 100644
index 0000000000..74e08614dd
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.plugin.jetty.v12.server;
+
+import java.lang.reflect.Method;
+import org.apache.skywalking.apm.agent.core.context.CarrierItem;
+import org.apache.skywalking.apm.agent.core.context.ContextCarrier;
+import org.apache.skywalking.apm.agent.core.context.ContextManager;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
+import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Enhances the core, namespace-neutral {@code
org.eclipse.jetty.server.Server#handle(Request, Response,
+ * Callback)} — the single top-level entry invoked once per request in Jetty
12. Handling is async, so
+ * the entry span is prepared for async in beforeMethod, its synchronous
segment ends in afterMethod, and
+ * it is finished (with the response status) when the wrapped {@link
SwCallback} completes.
+ */
+public class HandleInterceptor implements InstanceMethodsAroundInterceptor {
+
+ private static final ThreadLocal<SwCallback> HOLDER = new
ThreadLocal<SwCallback>();
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[]
allArguments, Class<?>[] argumentsTypes,
+ MethodInterceptResult result) throws Throwable {
+ Request request = (Request) allArguments[0];
+ Response response = (Response) allArguments[1];
+ Callback callback = (Callback) allArguments[2];
+
+ ContextCarrier contextCarrier = new ContextCarrier();
+ HttpFields headers = request.getHeaders();
+ CarrierItem next = contextCarrier.items();
+ while (next.hasNext()) {
+ next = next.next();
+ next.setHeadValue(headers.get(next.getHeadKey()));
+ }
+
+ HttpURI uri = request.getHttpURI();
+ AbstractSpan span = ContextManager.createEntrySpan(uri.getPath(),
contextCarrier);
+ Tags.URL.set(span, uri.asString());
+ Tags.HTTP.METHOD.set(span, request.getMethod());
+ span.setComponent(ComponentsDefine.JETTY_SERVER);
+ SpanLayer.asHttp(span);
+ span.prepareForAsync();
+
+ SwCallback swCallback = new SwCallback(callback, response, span);
+ allArguments[2] = swCallback;
+ HOLDER.set(swCallback);
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes,
+ Object ret) throws Throwable {
+ SwCallback swCallback = HOLDER.get();
+ HOLDER.remove();
+ ContextManager.stopSpan();
+ // ret == true: handled; the wrapped callback fires on response
completion and finishes the span.
+ // ret == false: not handled — Jetty writes 404 on the original
callback, bypassing ours.
+ // ret == null: handle() threw (see handleMethodException) — Jetty
writes 500.
+ // For the latter two the wrapped callback never fires and the status
is written only after
+ // handle() returns, so finish here with the status Jetty's contract
guarantees.
+ if (swCallback != null && !Boolean.TRUE.equals(ret)) {
+ swCallback.finishWithStatus(ret == null ? 500 : 404);
+ }
+ return ret;
+ }
+
+ @Override
+ public void handleMethodException(EnhancedInstance objInst, Method method,
Object[] allArguments,
+ Class<?>[] argumentsTypes, Throwable t) {
+ AbstractSpan span = ContextManager.activeSpan();
+ span.log(t);
+ span.errorOccurred();
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java
new file mode 100644
index 0000000000..f5193d7c05
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.plugin.jetty.v12.server;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.Invocable;
+
+/**
+ * Wraps the {@link Callback} passed to {@code Server#handle}. Jetty 12
request handling is async — the
+ * response status is only known when the callback completes on the (possibly
different) response thread.
+ * On completion this reads the status, records it on the entry span and
finishes the async span. It
+ * delegates every method to the original callback so Jetty's threading model
is preserved.
+ */
+public class SwCallback implements Callback {
+
+ private final Callback delegate;
+ private final Response response;
+ private final AbstractSpan span;
+ private final AtomicBoolean finished = new AtomicBoolean();
+
+ public SwCallback(Callback delegate, Response response, AbstractSpan span)
{
+ this.delegate = delegate;
+ this.response = response;
+ this.span = span;
+ }
+
+ @Override
+ public void succeeded() {
+ finishOnce();
+ delegate.succeeded();
+ }
+
+ @Override
+ public void failed(Throwable x) {
+ if (finished.compareAndSet(false, true)) {
+ span.log(x);
+ span.errorOccurred();
+ Tags.HTTP_RESPONSE_STATUS_CODE.set(span, response.getStatus());
+ span.asyncFinish();
+ }
+ delegate.failed(x);
+ }
+
+ @Override
+ public Invocable.InvocationType getInvocationType() {
+ return delegate.getInvocationType();
+ }
+
+ /**
+ * Records the response status and finishes the async span exactly once.
Safe to call from either the
+ * callback completion or the interceptor's afterMethod fallback
(not-handled / exception paths).
+ */
+ void finishOnce() {
+ if (finished.compareAndSet(false, true)) {
+ int statusCode = response.getStatus();
+ Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode);
+ if (statusCode >= 400) {
+ span.errorOccurred();
+ }
+ span.asyncFinish();
+ }
+ }
+
+ /**
+ * Finishes the async span exactly once with an explicit status. Used for
the not-handled / exception
+ * fallback paths where Jetty writes the error status on the original
callback after {@code handle()}
+ * returns — so the wrapped callback never fires and {@link
Response#getStatus()} is still 0 (uncommitted).
+ */
+ void finishWithStatus(int statusCode) {
+ if (finished.compareAndSet(false, true)) {
+ Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode);
+ if (statusCode >= 400) {
+ span.errorOccurred();
+ }
+ span.asyncFinish();
+ }
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java
new file mode 100644
index 0000000000..8b4d347af9
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.plugin.jetty.v12.server.define;
+
+import java.util.Collections;
+import java.util.List;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.skywalking.apm.agent.core.plugin.WitnessMethod;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
+import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static
org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName;
+
+/**
+ * Enhances {@code org.eclipse.jetty.server.Server#handle(Request, Response,
Callback)} by
+ * {@code HandleInterceptor}. Only Jetty 12+ declares this three-argument,
{@code Callback}-terminated
+ * signature, so the witness method both activates the plugin exclusively on
Jetty 12 and keeps it from
+ * overlapping the merged jetty-server plugin (which enhances the Jetty
9/10/11 {@code HttpChannel},
+ * removed in Jetty 12).
+ */
+public class Jetty12ServerInstrumentation extends
ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS =
"org.eclipse.jetty.server.Server";
+ private static final String ENHANCE_METHOD = "handle";
+ private static final String CALLBACK_CLASS =
"org.eclipse.jetty.util.Callback";
+ private static final String INTERCEPT_CLASS =
"org.apache.skywalking.apm.plugin.jetty.v12.server.HandleInterceptor";
+
+ @Override
+ protected List<WitnessMethod> witnessMethods() {
+ return Collections.singletonList(new WitnessMethod(
+ ENHANCE_CLASS,
+ named(ENHANCE_METHOD).and(takesArgument(2, named(CALLBACK_CLASS)))
+ ));
+ }
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[0];
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints()
{
+ return new InstanceMethodsInterceptPoint[] {
+ new InstanceMethodsInterceptPoint() {
+ @Override
+ public ElementMatcher<MethodDescription> getMethodsMatcher() {
+ return
named(ENHANCE_METHOD).and(takesArguments(3)).and(takesArgument(2,
named(CALLBACK_CLASS)));
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPT_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return true;
+ }
+ }
+ };
+ }
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return byName(ENHANCE_CLASS);
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def
new file mode 100644
index 0000000000..2c466d5f66
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def
@@ -0,0 +1,18 @@
+#
+# 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.
+
+jetty-server-12.x=org.apache.skywalking.apm.plugin.jetty.v12.server.define.Jetty12ServerInstrumentation
diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
index 1ac21fb14d..671cc92f91 100644
--- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
@@ -31,6 +31,7 @@
<module>jetty-client-9.x-plugin</module>
<module>jetty-client-9.0-plugin</module>
<module>jetty-server-plugin</module>
+ <module>jetty-server-12.x-plugin</module>
<module>jetty-client-11.x-plugin</module>
</modules>
<packaging>pom</packaging>
diff --git a/apm-sniffer/apm-sdk-plugin/pom.xml
b/apm-sniffer/apm-sdk-plugin/pom.xml
index 829d63dd84..8bc2e1faaa 100644
--- a/apm-sniffer/apm-sdk-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/pom.xml
@@ -44,6 +44,7 @@
<module>okhttp-common</module>
<module>spring-plugins</module>
<module>struts2-2.x-plugin</module>
+ <module>struts2-7.x-plugin</module>
<module>nutz-plugins</module>
<module>jetty-plugin</module>
<module>spymemcached-2.x-plugin</module>
diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml
similarity index 55%
copy from apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
copy to apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml
index 1ac21fb14d..f314ff5fba 100644
--- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
@@ -18,27 +17,40 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
<parent>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-sdk-plugin</artifactId>
<version>9.7.0-SNAPSHOT</version>
</parent>
+ <modelVersion>4.0.0</modelVersion>
- <artifactId>jetty-plugins</artifactId>
- <modules>
- <module>jetty-client-9.x-plugin</module>
- <module>jetty-client-9.0-plugin</module>
- <module>jetty-server-plugin</module>
- <module>jetty-client-11.x-plugin</module>
- </modules>
- <packaging>pom</packaging>
+ <artifactId>apm-struts2-7.x-plugin</artifactId>
+ <packaging>jar</packaging>
- <name>jetty-plugin</name>
- <url>http://maven.apache.org</url>
+ <name>struts2-7.x-plugin</name>
+ <!--
+ Struts 7.x moved DefaultActionInvocation from com.opensymphony.xwork2 to
org.apache.struts2
+ and switched to the jakarta.servlet namespace (Jakarta Servlet 6.0, JDK
17), so it needs a
+ separate plugin from struts2-2.x-plugin (which stays on the old class +
javax).
+ -->
<properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <struts.version>7.2.1</struts.version>
+ <jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>
</properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.struts</groupId>
+ <artifactId>struts2-core</artifactId>
+ <version>${struts.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <version>${jakarta-servlet-api.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
</project>
diff --git
a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java
new file mode 100644
index 0000000000..80a42b2eaa
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.plugin.struts2.v7;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import org.apache.skywalking.apm.agent.core.context.CarrierItem;
+import org.apache.skywalking.apm.agent.core.context.ContextCarrier;
+import org.apache.skywalking.apm.agent.core.context.ContextManager;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
+import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
+import org.apache.struts2.ServletActionContext;
+
+/**
+ * Enhances {@code org.apache.struts2.DefaultActionInvocation#invokeAction} in
Struts 7.x. Struts 7 is
+ * Jakarta-only (Jakarta Servlet 6.0), so the servlet request/response are
referenced through the
+ * jakarta.servlet namespace directly.
+ */
+public class Struts2Interceptor implements InstanceMethodsAroundInterceptor {
+
+ @Override
+ public void beforeMethod(EnhancedInstance objInst, Method method, Object[]
allArguments, Class<?>[] argumentsTypes,
+ MethodInterceptResult result) throws Throwable {
+ HttpServletRequest request = ServletActionContext.getRequest();
+ ContextCarrier contextCarrier = new ContextCarrier();
+
+ CarrierItem next = contextCarrier.items();
+ while (next.hasNext()) {
+ next = next.next();
+ next.setHeadValue(request.getHeader(next.getHeadKey()));
+ }
+
+ AbstractSpan span =
ContextManager.createEntrySpan(request.getRequestURI(), contextCarrier);
+ Tags.URL.set(span, request.getRequestURL().toString());
+ Tags.HTTP.METHOD.set(span, request.getMethod());
+ span.setComponent(ComponentsDefine.STRUTS2);
+ SpanLayer.asHttp(span);
+ }
+
+ @Override
+ public Object afterMethod(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes,
+ Object ret) throws Throwable {
+ HttpServletResponse response = ServletActionContext.getResponse();
+
+ AbstractSpan span = ContextManager.activeSpan();
+ int statusCode = response.getStatus();
+ Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode);
+ if (statusCode >= 400) {
+ span.errorOccurred();
+ }
+ ContextManager.stopSpan();
+ return ret;
+ }
+
+ @Override
+ public void handleMethodException(EnhancedInstance objInst, Method method,
Object[] allArguments,
+ Class<?>[] argumentsTypes, Throwable t) {
+ ContextManager.activeSpan().log(t);
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java
new file mode 100644
index 0000000000..68302e8c5a
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.plugin.struts2.v7.define;
+
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
+import
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
+import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static
org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName;
+
+/**
+ * {@link Struts2Instrumentation} enhances the <code>invokeAction</code>
method of Struts 7's
+ * <code>org.apache.struts2.DefaultActionInvocation</code> (relocated from
+ * <code>com.opensymphony.xwork2.DefaultActionInvocation</code> in Struts
2.x). The class name is
+ * unique to Struts 7, so it self-witnesses — no witness class is needed, and
there is no overlap
+ * with the struts2-2.x plugin, which enhances the old XWork class.
+ */
+public class Struts2Instrumentation extends
ClassInstanceMethodsEnhancePluginDefine {
+
+ private static final String ENHANCE_CLASS =
"org.apache.struts2.DefaultActionInvocation";
+ private static final String ENHANCE_METHOD = "invokeAction";
+ private static final String INTERCEPT_CLASS =
"org.apache.skywalking.apm.plugin.struts2.v7.Struts2Interceptor";
+
+ @Override
+ public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+ return new ConstructorInterceptPoint[0];
+ }
+
+ @Override
+ public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints()
{
+ return new InstanceMethodsInterceptPoint[] {
+ new InstanceMethodsInterceptPoint() {
+ @Override
+ public ElementMatcher<MethodDescription> getMethodsMatcher() {
+ return named(ENHANCE_METHOD);
+ }
+
+ @Override
+ public String getMethodsInterceptor() {
+ return INTERCEPT_CLASS;
+ }
+
+ @Override
+ public boolean isOverrideArgs() {
+ return false;
+ }
+ }
+ };
+ }
+
+ @Override
+ protected ClassMatch enhanceClass() {
+ return byName(ENHANCE_CLASS);
+ }
+}
diff --git
a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def
new file mode 100644
index 0000000000..a9a45c666b
--- /dev/null
+++
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def
@@ -0,0 +1,18 @@
+#
+# 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.
+
+struts2-7.x=org.apache.skywalking.apm.plugin.struts2.v7.define.Struts2Instrumentation
diff --git a/docs/en/setup/service-agent/java-agent/Plugin-list.md
b/docs/en/setup/service-agent/java-agent/Plugin-list.md
index 2e5ad78336..ac9ea19eeb 100644
--- a/docs/en/setup/service-agent/java-agent/Plugin-list.md
+++ b/docs/en/setup/service-agent/java-agent/Plugin-list.md
@@ -56,6 +56,7 @@
- jetty-client-9.0
- jetty-client-9.x
- jetty-server
+- jetty-server-12.x
- kafka-0.11.x/1.x/2.x
- kafka-3.7.x
- kafka-3.9.x
@@ -132,6 +133,7 @@
- spring-webflux-5.x-webclient
- spymemcached-2.x
- struts2-2.x
+- struts2-7.x
- thrift
- tomcat
- toolkit-counter
diff --git a/docs/en/setup/service-agent/java-agent/Plugin-test.md
b/docs/en/setup/service-agent/java-agent/Plugin-test.md
index a40eb82e35..ca1d6ef2c2 100644
--- a/docs/en/setup/service-agent/java-agent/Plugin-test.md
+++ b/docs/en/setup/service-agent/java-agent/Plugin-test.md
@@ -16,11 +16,11 @@ Every plugin maintained in the main repo requires
corresponding test cases as we
## Case Base Image Introduction
-The test framework provides `JVM-container` and `Tomcat-container` base images
including JDK8 and JDK17. You can choose the best one for your test case. If
both are suitable for your case, **`JVM-container` is preferred**.
+The test framework provides `JVM-container` and `Tomcat-container` base
images. You can choose the best one for your test case. If both are suitable
for your case, **`JVM-container` is preferred**.
### JVM-container Image Introduction
-[JVM-container](../../../../../test/plugin/containers/jvm-container) uses
`eclipse-temurin:8-jdk` as the base image. `JVM-container` supports JDK8 and
JDK17 as well in CI, which inherits `eclipse-temurin:8-jdk` and
`eclipse-temurin:17-jdk`.
+[JVM-container](../../../../../test/plugin/containers/jvm-container) uses
`eclipse-temurin:8-jdk` as the default base image. In CI it is exercised on
**JDK 8, 11, 17, 21 and 25** (via the `eclipse-temurin:{8,11,17,21,25}-jdk`
images).
It is supported to custom the base Java docker image by specify
`base_image_java`.
The test case project must be packaged as `project-name.zip`, including
`startup.sh` and uber jar, by using `mvn clean package`.
@@ -31,8 +31,8 @@ Take the following test projects as examples:
### Tomcat-container Image Introduction
-[Tomcat-container](../../../../../test/plugin/containers/tomcat-container)
uses `tomcat:8.5-jdk8-openjdk`, `tomcat:8.5-jdk17-openjdk` as the base image.
-It is supported to custom the base Tomcat docker image by specify
`base_image_tomcat`.
+[Tomcat-container](../../../../../test/plugin/containers/tomcat-container)
uses `tomcat:8.5-jdk8-openjdk` as the default base image. In CI it is exercised
on **Tomcat 8.5, 9.0, 10.0 and 10.1** (e.g. `tomcat:8.5-jdk8-openjdk`,
`tomcat:9.0.71-jdk8`, `tomcat:10.0.22-jdk8`, `tomcat:8.5-jdk17-openjdk`,
`tomcat:10.1-jdk17-temurin`, `tomcat:10.1-jdk21-temurin`). Tomcat 8.5/9.0 are
`javax.servlet`; Tomcat 10.0/10.1 are `jakarta.servlet`.
+It is supported to custom the base Tomcat docker image by specify
`base_image_tomcat`. The base image must provide `curl` (used by the
container's health check); if an official image lacks it, build a small custom
image that adds `curl` first — as the JDK 25 lane does with
`tomcat-curl:10.1.50-jdk25-temurin`.
The test case project must be packaged as `project-name.war` by using `mvn
package`.
Take the following test project as an example
diff --git a/docs/en/setup/service-agent/java-agent/Supported-list.md
b/docs/en/setup/service-agent/java-agent/Supported-list.md
index ca6f481f06..1416ec3471 100644
--- a/docs/en/setup/service-agent/java-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/java-agent/Supported-list.md
@@ -11,10 +11,10 @@ metrics based on the tracing data.
* Spring MVC 3.x, 4.x 5.x with servlet 3.x
* Spring MVC 6.x (Optional²)
* [Nutz Web Framework](https://github.com/nutzam/nutz) 1.x
- * [Struts2 MVC](http://struts.apache.org/) 2.3.x -> 2.5.x
+ * [Struts2 MVC](http://struts.apache.org/) 2.3.x -> 2.5.x, 7.x (Jakarta,
JDK17)
* Resin 3 (Optional¹), See [SkySPM Plugin
Repository](https://github.com/SkyAPM/java-plugin-extensions)
* Resin 4 (Optional¹), See [SkySPM Plugin
Repository](https://github.com/SkyAPM/java-plugin-extensions)
- * [Jetty Server](http://www.eclipse.org/jetty/) 9.x -> 11.x
+ * [Jetty Server](http://www.eclipse.org/jetty/) 9.x -> 12.x
* [Spring
WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html)
5.x (Optional²) -> 6.x (Optional²)
* [Undertow](http://undertow.io/) 1.3.0.Final -> 2.3.18.Final
* [RESTEasy](https://resteasy.dev/) 3.1.0.Final -> 6.2.4.Final
@@ -172,7 +172,7 @@ The meter plugin provides the advanced metrics collections,
which are not a part
* [Undertow](https://github.com/undertow-io/undertow) 2.1.x -> 2.6.x
* [Tomcat](https://github.com/apache/tomcat) 7.0.x -> 10.0.x
* [Dubbo](https://github.com/apache/dubbo) 2.5.x -> 2.7.x
- * [Jetty](https://github.com/eclipse/jetty.project) 9.1.x -> 11.x
+ * [Jetty](https://github.com/eclipse/jetty.project) 9.1.x -> 12.x
* [Grizzly](https://github.com/eclipse-ee4j/grizzly) 2.3.x -> 4.x
* Connection Pool
* Supported JDBC drviers
diff --git a/test/plugin/CLAUDE.md b/test/plugin/CLAUDE.md
new file mode 100644
index 0000000000..6b73f93540
--- /dev/null
+++ b/test/plugin/CLAUDE.md
@@ -0,0 +1,134 @@
+# CLAUDE.md - Plugin Test Guide
+
+This file guides AI assistants writing and changing **plugin tests** under
`test/plugin/scenarios/`.
+For the full test-framework mechanics (scenario layout, `configuration.yml`,
`expectedData.yaml`,
+containers, running locally) see the **"Plugin Test Framework"** section of
+`apm-sniffer/apm-sdk-plugin/CLAUDE.md`, and `docs/.../Plugin-test.md`.
+
+## Testing philosophy: plugin scenarios are the key test — not unit tests
+
+For an agent plugin, the **plugin test scenario is the primary and sufficient
test**. Do **not**
+over-engineer by adding many unit tests for a plugin.
+
+A plugin's real risk is two things, and a scenario proves both at once by
running the **real
+framework**:
+
+1. **Cast / fetch-data logic** — the interceptor casting the intercepted
arguments to the framework
+ types and reading request/response fields off them (headers, method, URI,
status, params). Mocks
+ can't validate that the real framework class actually exposes those methods
the way you assumed.
+2. **Version compatibility** — the same interceptor must work across the
supported version range.
+
+So: **when adding or changing a plugin, add or extend its scenario**, not a
pile of mock-based unit
+tests. Reserve unit tests for genuinely logic/namespace-critical **shared**
code where a deterministic
+in-JVM assertion earns its keep (e.g. `servlet-commons` `wrap()` resolving
javax vs jakarta with both
+APIs on the test classpath) — never for per-plugin interceptor mocking.
+
+## Validation path: assert the full span shape your plugin produces
+
+A scenario proves a plugin by asserting the **exact spans** the real framework
makes the agent
+produce. Pick the shape by what the framework actually does — and don't settle
for a single entry
+span when the plugin also does propagation, or you silently skip the
inject/extract path.
+
+- **RPC / web frameworks that have both a server and a client side** (Struts,
Spring MVC, gRPC,
+ Dubbo, HTTP servers …): drive the full round-trip —
+ `curl → server → its own client call → another service's endpoint`. The
trace then contains
+ **2 entry spans + 1 exit span**:
+ 1. the **server entry** span for the request `curl` hit (proves the server
side: extract + entry),
+ 2. the **client exit** span for the outbound call (proves inject + exit),
+ 3. a **second entry** span on the other endpoint carrying a **cross-process
ref** back to the
+ first (proves propagation across the wire — the receiving/extract side).
+
+ **The ref itself must be asserted** — for RPC the cross-process `refs:`
block is the whole point,
+ so `expectedData.yaml` must include it on that second entry span with
`refType: CrossProcess`,
+ `parentEndpoint`, `parentSpanId` (pointing at the exit span),
`parentTraceSegmentId: not null`,
+ `parentService`, and `traceId: not null`. A scenario that asserts spans but
not the ref does not
+ prove propagation.
+
+ The struts2.7 scenario is exactly this: `case.action` (entry) → HttpClient
(exit) → `case1.action`
+ (entry + ref); jetty-12 uses the same shape via a JDK-HttpURLConnection
self-call. Keep that nested
+ self-call — it is the only thing that proves inject / extract / propagation;
a lone entry span does not.
+
+- **Client-only plugins** (JDBC/PostgreSQL, Redis, MongoDB, an HTTP client, a
message producer): the
+ peer is a database/broker, not another instrumented SkyWalking service, so
there is **no second
+ entry span**. Assert just the **client exit** span (component, `peer`, and
the `db.*` / `http.*`
+ tags). You do NOT need to stand up another service endpoint.
+
+## Version coverage: one version per minor (latest patch)
+
+`support-version.list` must cover the framework's supported range, but keep
**one version per minor
+version — the latest patch**, not every patch. E.g. Jetty `12.0.36` +
`12.1.10`; Struts `7.0.3`,
+`7.1.1`, `7.2.1`; Spring `6.0.4`, `6.1.1`, `6.2.19`. Verify each version
resolves on Maven Central.
+This is the compatibility proof; CI runs the scenario against each listed
version.
+
+## How the test containers work (and where the JDK/Tomcat version really comes
from)
+
+A scenario runs inside one of two prebuilt container images, selected by
`type:` in
+`configuration.yml`:
+
+- **`type: jvm`** → the **`agent-test-jvm`** container. Your app is a fat-jar
started by
+ `bin/startup.sh` with `${agent_opts}` (the `-javaagent:` line — you MUST
include it in
+ `startup.sh`). Built `FROM ${base_image_java}`.
+- **`type: tomcat`** → the **`agent-test-tomcat`** container. Your app is a
`*.war` dropped into
+ `/usr/local/tomcat/webapps/`; the agent is wired in via the container's
patched `catalina.sh`.
+ Built `FROM ${base_image_tomcat}`.
+
+**The version pin is in the CI lane, not `configuration.yml`.**
`configuration.yml` only says
+`type: tomcat` — it does **not** choose a Tomcat version. The concrete JDK and
Tomcat versions come
+from the `base_image_java` / `base_image_tomcat` inputs passed to
`./.github/actions/build` in the
+lane's `Build` job, and **every scenario in that lane shares that one image**.
So:
+
+- `type: tomcat` on the `plugins-jdk8-*` lane → `tomcat:8.5-jdk8` (javax);
+ on `plugins-jdk11-*` → `tomcat:9.0-jdk11` (javax);
+ on `plugins-jdk17-*` → `tomcat:10.1-jdk17` (**jakarta**).
+- That is why a jakarta/Servlet-6 framework (Struts 7, Jetty 12, Spring 6)
MUST be registered on a
+ JDK-17 lane — it needs the Tomcat-10.1 base — and a javax one on an older
lane. Putting a jakarta
+ WAR on a Tomcat-8.5 lane just won't deploy.
+
+To run locally you pass the same two knobs explicitly (the defaults are the
javax/JDK-8 pair):
+`bash test/plugin/run.sh --base_image_java eclipse-temurin:17-jdk
--base_image_tomcat tomcat:10.1-jdk17-temurin <scenario>`.
+Some newer bases lack `curl` (the container health check needs it) — the
JDK-25 lane bakes it in
+via `.github/workflows/Dockerfile-tomcat-jdk25-withCurl`.
+
+## How the GitHub Actions lanes work
+
+Scenarios are load-balanced across
`.github/workflows/plugins-<jdk>-test.<group>.yaml` files. Each
+file is one lane: its `Build` job pins `base_image_java` +
`base_image_tomcat`, and its `test` job's
+`matrix.case:` list names the scenarios that run on that image. Lanes exist
per JDK
+(`jdk8`/`jdk11`/`jdk17`/`jdk21`/`jdk25`); the trailing `.0`/`.1`/… is just a
bucket to spread load.
+Register a new scenario by adding its directory name to the `matrix.case:`
list of the lane whose
+base image matches the framework's needs — use `python3 tools/select-group.py`
to pick the
+least-loaded bucket in that JDK lane.
+
+## Cold-start and the 3s entry-service timeout (a real gotcha)
+
+The container's `run.sh` runs under `set -e` and hits the entry service with
+`curl -s --max-time 3 ${SCENARIO_ENTRY_SERVICE}` — **no warm-up first**. So
the very first (cold)
+entry request must finish within **3 seconds** or `curl` exits 28 and the
whole scenario fails. That
+first request pays a lot of one-time cost at once: the agent's ByteBuddy
enhancement of every class
+it touches on that path, JIT, and — for WARs — a cold Jasper **JSP compile**,
all under the
+container's tight `-Xmx256m` heap and whatever CPU the runner gives, with
**jacoco** simultaneously
+instrumenting the `org.apache.skywalking.*` classes for coverage.
+
+Keep the entry path's cold cost low:
+
+- Don't render a **JSP** from the entry action if a lightweight result works
(e.g. Struts
+ `httpheader`, a plain-text/stream result). The view is irrelevant to what a
plugin scenario
+ verifies (entry/exit spans, tags, propagation), and a cold Jasper compile is
expensive.
+- Be extra careful when the entry action makes a **nested internal call** (for
cross-process
+ propagation coverage): if that nested request also does cold work (a JSP
compile) *while* the
+ outer request is still cold-enhancing Struts/HttpClient, the two cold-start
storms contend for the
+ limited CPU and heap and blow up **super-linearly** (observed ~0.5s each in
isolation but ~3–20s
+ nested under throttle). This is a slow first request, not a plugin bug —
verify with a
+ CPU-throttled container (`docker run --cpus=0.5 …`) before assuming a hang.
+
+## Practical notes
+
+- One scenario per plugin (or per javax/jakarta era). Mirror an existing
sibling scenario as a
+ template and change only what differs (framework version, namespace,
deployment model).
+- Match `type:` to the deployment: `jvm` (fat-jar started by `bin/startup.sh`,
`${agent_opts}`
+ required) or `tomcat` (WAR on a container). Jakarta/newer frameworks go in a
JDK-17 CI group.
+- Assert the entry span shape that exercises the plugin's data logic:
`componentId`, `spanType: Entry`,
+ `spanLayer: Http`, and the `url` / `http.method` / `http.status_code` (and
`http.params` /
+ `http.headers` where the plugin collects them) tags, plus cross-process refs.
+- Register the scenario in the right `.github/workflows/plugins-*.yaml` matrix
(use
+ `python3 tools/select-group.py` to pick the least-loaded group for its JDK
lane).
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh
b/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh
new file mode 100644
index 0000000000..ea5bec3c39
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# 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.
+
+home="$(cd "$(dirname $0)"; pwd)"
+
+java -jar ${agent_opts} ${home}/../libs/jetty-12.x-scenario.jar &
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml
b/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml
new file mode 100644
index 0000000000..b3e44c9df3
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml
@@ -0,0 +1,86 @@
+# 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.
+segmentItems:
+ - serviceName: jetty-12.x-scenario
+ segmentSize: gt 2
+ segments:
+ - segmentId: not null
+ spans:
+ - operationName: /jetty-12.x-scenario/case/case1
+ parentSpanId: '-1'
+ spanId: '0'
+ spanLayer: Http
+ startTime: gt 0
+ endTime: gt 0
+ componentId: '19'
+ isError: 'false'
+ spanType: Entry
+ peer: ''
+ skipAnalysis: 'false'
+ tags:
+ - key: url
+ value: http://localhost:18080/jetty-12.x-scenario/case/case1
+ - key: http.method
+ value: GET
+ - key: http.status_code
+ value: '200'
+ refs:
+ - parentEndpoint: /jetty-12.x-scenario/case/jetty-12.x-scenario
+ networkAddress: localhost:18080
+ refType: CrossProcess
+ parentSpanId: '1'
+ parentTraceSegmentId: not null
+ parentServiceInstance: not null
+ parentService: jetty-12.x-scenario
+ traceId: not null
+ - segmentId: not null
+ spans:
+ - operationName: /jetty-12.x-scenario/case/case1
+ parentSpanId: '0'
+ spanId: '1'
+ spanLayer: Http
+ startTime: gt 0
+ endTime: gt 0
+ componentId: '2'
+ isError: 'false'
+ spanType: Exit
+ peer: localhost:18080
+ skipAnalysis: 'false'
+ tags:
+ - key: url
+ value: http://localhost:18080/jetty-12.x-scenario/case/case1
+ - key: http.method
+ value: GET
+ - key: http.status_code
+ value: '200'
+ - operationName: /jetty-12.x-scenario/case/jetty-12.x-scenario
+ parentSpanId: '-1'
+ spanId: '0'
+ spanLayer: Http
+ startTime: gt 0
+ endTime: gt 0
+ componentId: '19'
+ isError: 'false'
+ spanType: Entry
+ peer: ''
+ skipAnalysis: 'false'
+ tags:
+ - key: url
+ value:
http://localhost:18080/jetty-12.x-scenario/case/jetty-12.x-scenario
+ - key: http.method
+ value: GET
+ - key: http.status_code
+ value: '200'
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml
b/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml
new file mode 100644
index 0000000000..2db90d0c99
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml
@@ -0,0 +1,22 @@
+# 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.
+
+type: jvm
+entryService:
http://localhost:18080/jetty-12.x-scenario/case/jetty-12.x-scenario
+healthCheck: http://localhost:18080/jetty-12.x-scenario/case/healthCheck
+startScript: ./bin/startup.sh
+environment:
+dependencies:
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/pom.xml
b/test/plugin/scenarios/jetty-12.x-scenario/pom.xml
new file mode 100644
index 0000000000..6a28e254bf
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/pom.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <groupId>org.apache.skywalking.apm.testcase</groupId>
+ <artifactId>jetty-12.x-scenario</artifactId>
+ <version>1.0.0</version>
+ <packaging>jar</packaging>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <compiler.version>17</compiler.version>
+ <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+
+ <test.framework.version>12.0.36</test.framework.version>
+
+ <log4j.version>2.6.2</log4j.version>
+ </properties>
+
+ <name>skywalking-jetty-12.x-scenario</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <version>${test.framework.version}</version>
+ </dependency>
+ <!--
+ Used only by the entry handler to make the nested internal call that
proves cross-process
+ propagation. Apache HttpClient 4.x is instrumented by a normal
(non-bootstrap) plugin, so
+ the exit span + sw8 injection are reliable across JDKs (the JDK
HttpURLConnection bootstrap
+ plugin does not enhance sun.net.www.protocol.http on JDK 17 in this
container).
+ -->
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.3</version>
+ </dependency>
+ </dependencies>
+
+
+ <build>
+ <finalName>jetty-12.x-scenario</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>3.1.0</version>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <transformers>
+ <transformer
+
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <mainClass>
+
org.apache.skywalking.apm.testcase.jetty12xserver.Application
+ </mainClass>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>${maven-compiler-plugin.version}</version>
+ <configuration>
+ <source>${compiler.version}</source>
+ <target>${compiler.version}</target>
+ <encoding>${project.build.sourceEncoding}</encoding>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>assemble</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ <configuration>
+ <descriptors>
+
<descriptor>src/main/assembly/assembly.xml</descriptor>
+ </descriptors>
+ <outputDirectory>./target/</outputDirectory>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml
new file mode 100644
index 0000000000..4be83c4cb7
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+<assembly
+
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2
http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+ <formats>
+ <format>zip</format>
+ </formats>
+
+ <fileSets>
+ <fileSet>
+ <directory>./bin</directory>
+ <fileMode>0775</fileMode>
+ </fileSet>
+ </fileSets>
+
+ <files>
+ <file>
+ <source>${project.build.directory}/jetty-12.x-scenario.jar</source>
+ <outputDirectory>./libs</outputDirectory>
+ <fileMode>0775</fileMode>
+ </file>
+ </files>
+</assembly>
diff --git
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java
new file mode 100644
index 0000000000..aecb666125
--- /dev/null
+++
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.testcase.jetty12xserver;
+
+import org.apache.skywalking.apm.testcase.jetty12xserver.handler.CaseHandler;
+import org.eclipse.jetty.server.Server;
+
+/**
+ * Embedded Jetty 12 server. Jetty 12 handling routes every request through
+ * {@code org.eclipse.jetty.server.Server#handle(Request, Response, Callback)}
— the method the
+ * jetty-server-12.x plugin enhances — before delegating to the top-level
{@link CaseHandler}.
+ */
+public class Application {
+
+ public static void main(String[] args) throws Exception {
+ Server server = new Server(18080);
+ server.setHandler(new CaseHandler());
+ server.start();
+ server.join();
+ }
+}
diff --git
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java
new file mode 100644
index 0000000000..201ab06792
--- /dev/null
+++
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.testcase.jetty12xserver.handler;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Top-level Jetty 12 core handler.
+ *
+ * The entry service {@code /jetty-12.x-scenario/case/jetty-12.x-scenario}
makes a nested internal
+ * call to {@code /jetty-12.x-scenario/case/case1} on the same server. This
exercises both sides of
+ * the jetty-server-12.x plugin in one trace: the entry span for the inbound
request, the HttpClient
+ * exit span for the outbound call (inject), and a second entry span on {@code
case1} that extracts
+ * the propagated context into a cross-process ref (the receiving/extract
side). The health check
+ * {@code /case/healthCheck} and the nested {@code /case/case1} endpoint
simply return 200.
+ */
+public class CaseHandler extends Handler.Abstract {
+
+ @Override
+ public boolean handle(Request request, Response response, Callback
callback) throws Exception {
+ String path = request.getHttpURI().getPath();
+ if (path != null && path.endsWith("/case/jetty-12.x-scenario")) {
+ visit("http://localhost:18080/jetty-12.x-scenario/case/case1");
+ }
+ response.setStatus(200);
+ response.write(true,
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), callback);
+ return true;
+ }
+
+ private static void visit(String url) throws Exception {
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+ HttpGet httpget = new HttpGet(url);
+ ResponseHandler<String> responseHandler = response -> {
+ HttpEntity entity = response.getEntity();
+ return entity != null ? EntityUtils.toString(entity) : null;
+ };
+ httpClient.execute(httpget, responseHandler);
+ }
+ }
+}
diff --git
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..b5cda5ae8a
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+<Configuration status="WARN">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_ERR">
+ <PatternLayout charset="UTF-8" pattern="[%d{yyyy-MM-dd
HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="WARN">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/support-version.list
b/test/plugin/scenarios/jetty-12.x-scenario/support-version.list
new file mode 100644
index 0000000000..6c62f269a7
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/support-version.list
@@ -0,0 +1,18 @@
+# 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.
+
+12.0.36
+12.1.10
diff --git a/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml
b/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml
new file mode 100644
index 0000000000..02d7206ab2
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml
@@ -0,0 +1,72 @@
+# 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.
+segmentItems:
+- serviceName: struts2.7-scenario
+ segmentSize: gt 2
+ segments:
+ - segmentId: not null
+ spans:
+ - operationName: /struts2.7-scenario/case/case1.action
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 15
+ isError: false
+ spanType: Entry
+ peer: ''
+ tags:
+ - {key: url, value:
'http://localhost:8080/struts2.7-scenario/case/case1.action'}
+ - {key: http.method, value: GET}
+ - {key: http.status_code, value: '200'}
+ refs:
+ - {parentEndpoint: /struts2.7-scenario/case/case.action, networkAddress:
'localhost:8080',
+ refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not
null, parentServiceInstance: not
+ null, parentService: struts2.7-scenario, traceId: not null}
+ skipAnalysis: 'false'
+ - segmentId: not null
+ spans:
+ - operationName: /struts2.7-scenario/case/case1.action
+ parentSpanId: 0
+ spanId: 1
+ spanLayer: Http
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 2
+ isError: false
+ spanType: Exit
+ peer: localhost:8080
+ tags:
+ - {key: url, value:
'http://localhost:8080/struts2.7-scenario/case/case1.action'}
+ - {key: http.method, value: GET}
+ - {key: http.status_code, value: '200'}
+ skipAnalysis: 'false'
+ - operationName: /struts2.7-scenario/case/case.action
+ parentSpanId: -1
+ spanId: 0
+ spanLayer: Http
+ startTime: gt 0
+ endTime: gt 0
+ componentId: 15
+ isError: false
+ spanType: Entry
+ peer: ''
+ tags:
+ - {key: url, value:
'http://localhost:8080/struts2.7-scenario/case/case.action'}
+ - {key: http.method, value: GET}
+ - {key: http.status_code, value: '200'}
+ skipAnalysis: 'false'
diff --git a/test/plugin/scenarios/struts2.7-scenario/configuration.yml
b/test/plugin/scenarios/struts2.7-scenario/configuration.yml
new file mode 100644
index 0000000000..b16e56768e
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/configuration.yml
@@ -0,0 +1,21 @@
+# 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.
+
+type: tomcat
+entryService: http://localhost:8080/struts2.7-scenario/case/case.action
+healthCheck: http://localhost:8080/struts2.7-scenario/healthCheck
+environment:
+dependencies:
diff --git a/test/plugin/scenarios/struts2.7-scenario/pom.xml
b/test/plugin/scenarios/struts2.7-scenario/pom.xml
new file mode 100644
index 0000000000..beede09526
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/pom.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <groupId>org.apache.skywalking.apm.testcase</groupId>
+ <artifactId>struts2.7-scenario</artifactId>
+ <version>1.0.0</version>
+ <packaging>war</packaging>
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <name>skywalking-struts2.7-scenario</name>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <!-- Struts 7 is Jakarta-only (Jakarta Servlet 6.0) and requires JDK
17+. -->
+ <compiler.version>17</compiler.version>
+ <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+ <test.framework.version>7.2.1</test.framework.version>
+ <jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>
+ <lombok.version>1.18.42</lombok.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ <version>${jakarta-servlet-api.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ <version>2.8.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ <version>2.8.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <version>${lombok.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.struts</groupId>
+ <artifactId>struts2-core</artifactId>
+ <version>${test.framework.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.3</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>struts2.7-scenario</finalName>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>${maven-compiler-plugin.version}</version>
+ <configuration>
+ <source>${compiler.version}</source>
+ <target>${compiler.version}</target>
+ <encoding>${project.build.sourceEncoding}</encoding>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-war-plugin</artifactId>
+ <version>3.3.1</version>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git
a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java
new file mode 100644
index 0000000000..9f40d9c99f
--- /dev/null
+++
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.testcase.struts;
+
+import java.io.IOException;
+
+public class Case1Action {
+
+ public String execute() throws IOException {
+ return "SUCCESS";
+ }
+}
diff --git
a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java
new file mode 100644
index 0000000000..89a4c70e36
--- /dev/null
+++
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ *
+ */
+
+package org.apache.skywalking.apm.testcase.struts;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+
+public class CaseAction {
+
+ public String execute() throws IOException {
+ visit();
+ return "SUCCESS";
+ }
+
+ private static void visit() throws IOException {
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+ HttpGet httpget = new
HttpGet("http://localhost:8080/struts2.7-scenario/case/case1.action");
+ ResponseHandler<String> responseHandler = response -> {
+ HttpEntity entity = response.getEntity();
+ return entity != null ? EntityUtils.toString(entity) : null;
+ };
+ httpClient.execute(httpget, responseHandler);
+ }
+ }
+}
diff --git
a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java
new file mode 100644
index 0000000000..31458cea43
--- /dev/null
+++
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+package org.apache.skywalking.apm.testcase.struts;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class HealthCheckServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
+ // your codes
+ PrintWriter writer = resp.getWriter();
+ writer.write("Success");
+ writer.flush();
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
+ doGet(req, resp);
+ }
+}
diff --git
a/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml
b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..b5cda5ae8a
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+<Configuration status="WARN">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_ERR">
+ <PatternLayout charset="UTF-8" pattern="[%d{yyyy-MM-dd
HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="WARN">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git
a/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml
b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml
new file mode 100644
index 0000000000..62faf006d4
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ 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.
+ ~
+ -->
+<!DOCTYPE struts PUBLIC
+ "-//Apache Software Foundation//DTD Struts Configuration 6.5//EN"
+ "https://struts.apache.org/dtds/struts-6.5.dtd">
+
+<!--
+ The actions intentionally use the lightweight `httpheader` result rather
than a JSP view.
+ The entry action (case.action) makes a nested internal call (case1.action)
within the same
+ cold request; a JSP result would trigger a cold Jasper compile on that
nested thread at the
+ same time the agent enhances the Struts/HttpClient classes, and the two
cold-start storms
+ contend badly under the plugin-test container's limited CPU/heap, pushing
the first request
+ past the framework's 3s entry-service timeout. The result view is irrelevant
to what this
+ scenario verifies (the Struts entry span, the HttpClient exit span, and
cross-process
+ propagation), so a no-view result keeps the cold path fast without losing
any coverage.
+-->
+<struts>
+ <package name="case" namespace="/case" extends="struts-default">
+ <action name="case"
class="org.apache.skywalking.apm.testcase.struts.CaseAction">
+ <result name="SUCCESS" type="httpheader">
+ <param name="status">200</param>
+ </result>
+ </action>
+ <action name="case1"
class="org.apache.skywalking.apm.testcase.struts.Case1Action">
+ <result name="SUCCESS" type="httpheader">
+ <param name="status">200</param>
+ </result>
+ </action>
+ </package>
+</struts>
diff --git
a/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml
b/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..a43b33cc55
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,44 @@
+<!--
+ ~ 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.
+ ~
+ -->
+<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
+ https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
+ version="6.0">
+ <display-name>skywalking-struts2.7-scenario</display-name>
+
+ <filter>
+ <filter-name>struts2</filter-name>
+
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>struts2</filter-name>
+ <url-pattern>/case/*</url-pattern>
+ </filter-mapping>
+
+ <servlet>
+ <servlet-name>healthCheck</servlet-name>
+
<servlet-class>org.apache.skywalking.apm.testcase.struts.HealthCheckServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>healthCheck</servlet-name>
+ <url-pattern>/healthCheck</url-pattern>
+ </servlet-mapping>
+</web-app>
diff --git a/test/plugin/scenarios/struts2.7-scenario/support-version.list
b/test/plugin/scenarios/struts2.7-scenario/support-version.list
new file mode 100644
index 0000000000..5fcb5f103f
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/support-version.list
@@ -0,0 +1,19 @@
+# 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.
+
+7.0.3
+7.1.1
+7.2.1