diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md
index 74bea2ded278..12ebadfb9b5d 100644
--- a/docs/supported-libraries.md
+++ b/docs/supported-libraries.md
@@ -39,7 +39,7 @@ These are the supported libraries and frameworks:
| [Apache Pulsar](https://pulsar.apache.org/) | 2.8+ | N/A | [Messaging Spans] |
| [Apache RocketMQ gRPC/Protobuf-based Client](https://rocketmq.apache.org/) | 5.0+ | N/A | [Messaging Spans] |
| [Apache RocketMQ Remoting-based Client](https://rocketmq.apache.org/) | 4.8+ | [opentelemetry-rocketmq-client-4.8](../instrumentation/rocketmq/rocketmq-client/rocketmq-client-4.8/library) | [Messaging Spans] |
-| [Apache Struts 2](https://github.com/apache/struts) | 2.3+ | N/A | Provides `http.route` [2], Controller Spans [3] |
+| [Apache Struts](https://github.com/apache/struts) | 2.3+ | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Apache Tapestry](https://tapestry.apache.org/) | 5.4+ | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Apache Wicket](https://wicket.apache.org/) | 8.0+ | N/A | Provides `http.route` [2] |
| [Armeria](https://armeria.dev) | 1.3+ | [opentelemetry-armeria-1.3](../instrumentation/armeria/armeria-1.3/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] |
diff --git a/instrumentation/struts-2.3/javaagent/build.gradle.kts b/instrumentation/struts/struts-2.3/javaagent/build.gradle.kts
similarity index 88%
rename from instrumentation/struts-2.3/javaagent/build.gradle.kts
rename to instrumentation/struts/struts-2.3/javaagent/build.gradle.kts
index bdb8a442dda5..4dabe129d9a3 100644
--- a/instrumentation/struts-2.3/javaagent/build.gradle.kts
+++ b/instrumentation/struts/struts-2.3/javaagent/build.gradle.kts
@@ -6,7 +6,8 @@ muzzle {
pass {
group.set("org.apache.struts")
module.set("struts2-core")
- versions.set("[2.3.1,)")
+ versions.set("[2.1.0,7)")
+ assertInverse.set(true)
}
}
@@ -24,6 +25,7 @@ dependencies {
testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent"))
testInstrumentation(project(":instrumentation:servlet:servlet-javax-common:javaagent"))
testInstrumentation(project(":instrumentation:jetty:jetty-8.0:javaagent"))
+ testInstrumentation(project(":instrumentation:struts:struts-7.0:javaagent"))
latestDepTestLibrary("org.apache.struts:struts2-core:6.0.+")
}
diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/ActionInvocationInstrumentation.java
similarity index 94%
rename from instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java
rename to instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/ActionInvocationInstrumentation.java
index e8ed9f464c47..8c831dfd2394 100644
--- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/ActionInvocationInstrumentation.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/ActionInvocationInstrumentation.java
@@ -3,12 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
-import static io.opentelemetry.javaagent.instrumentation.struts2.StrutsSingletons.instrumenter;
+import static io.opentelemetry.javaagent.instrumentation.struts.v2_3.StrutsSingletons.instrumenter;
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2InstrumentationModule.java b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/Struts2InstrumentationModule.java
similarity index 91%
rename from instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2InstrumentationModule.java
rename to instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/Struts2InstrumentationModule.java
index 82d00204fc99..d44b85d81229 100644
--- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2InstrumentationModule.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/Struts2InstrumentationModule.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import static java.util.Collections.singletonList;
diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsCodeAttributesGetter.java b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsCodeAttributesGetter.java
similarity index 90%
rename from instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsCodeAttributesGetter.java
rename to instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsCodeAttributesGetter.java
index 669682173074..a6f21b3f3b7a 100644
--- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsCodeAttributesGetter.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsCodeAttributesGetter.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import com.opensymphony.xwork2.ActionInvocation;
import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter;
diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsServerSpanNaming.java b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsServerSpanNaming.java
similarity index 94%
rename from instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsServerSpanNaming.java
rename to instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsServerSpanNaming.java
index 5bf7240bf31e..1d500ef201cf 100644
--- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsServerSpanNaming.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsServerSpanNaming.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import com.opensymphony.xwork2.ActionProxy;
import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter;
diff --git a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsSingletons.java b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsSingletons.java
similarity index 95%
rename from instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsSingletons.java
rename to instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsSingletons.java
index fc1afa0472b0..913db3c99bc1 100644
--- a/instrumentation/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts2/StrutsSingletons.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/StrutsSingletons.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import com.opensymphony.xwork2.ActionInvocation;
import io.opentelemetry.api.GlobalOpenTelemetry;
diff --git a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingAction.java b/instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/GreetingAction.java
similarity index 97%
rename from instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingAction.java
rename to instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/GreetingAction.java
index 9741ab90b555..21abf1d876e0 100644
--- a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingAction.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/GreetingAction.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest.controller;
diff --git a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingServlet.java b/instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/GreetingServlet.java
similarity index 87%
rename from instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingServlet.java
rename to instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/GreetingServlet.java
index 2f480affbe7e..d82474b4f65b 100644
--- a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/GreetingServlet.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/GreetingServlet.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
diff --git a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2ActionSpanTest.java b/instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/Struts2ActionSpanTest.java
similarity index 99%
rename from instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2ActionSpanTest.java
rename to instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/Struts2ActionSpanTest.java
index f7d91eed52b1..e4c756910a2c 100644
--- a/instrumentation/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts2/Struts2ActionSpanTest.java
+++ b/instrumentation/struts/struts-2.3/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v2_3/Struts2ActionSpanTest.java
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-package io.opentelemetry.javaagent.instrumentation.struts2;
+package io.opentelemetry.javaagent.instrumentation.struts.v2_3;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR;
import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION;
diff --git a/instrumentation/struts-2.3/javaagent/src/test/resources/greeting.ftl b/instrumentation/struts/struts-2.3/javaagent/src/test/resources/greeting.ftl
similarity index 100%
rename from instrumentation/struts-2.3/javaagent/src/test/resources/greeting.ftl
rename to instrumentation/struts/struts-2.3/javaagent/src/test/resources/greeting.ftl
diff --git a/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml b/instrumentation/struts/struts-2.3/javaagent/src/test/resources/struts.xml
similarity index 74%
rename from instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml
rename to instrumentation/struts/struts-2.3/javaagent/src/test/resources/struts.xml
index 3cf78125c183..33eeceebd183 100644
--- a/instrumentation/struts-2.3/javaagent/src/test/resources/struts.xml
+++ b/instrumentation/struts/struts-2.3/javaagent/src/test/resources/struts.xml
@@ -26,16 +26,16 @@
-
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
diff --git a/instrumentation/struts/struts-7.0/javaagent/build.gradle.kts b/instrumentation/struts/struts-7.0/javaagent/build.gradle.kts
new file mode 100644
index 000000000000..6d0d99e84258
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/build.gradle.kts
@@ -0,0 +1,37 @@
+plugins {
+ id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+ pass {
+ group.set("org.apache.struts")
+ module.set("struts2-core")
+ versions.set("[7.0.0,)")
+ assertInverse.set(true)
+ }
+}
+
+// struts 7 requires java 17
+otelJava {
+ minJavaVersionSupported.set(JavaVersion.VERSION_17)
+}
+
+dependencies {
+ bootstrap(project(":instrumentation:servlet:servlet-common:bootstrap"))
+
+ library("org.apache.struts:struts2-core:7.0.0")
+
+ testImplementation(project(":testing-common"))
+ testImplementation("org.eclipse.jetty:jetty-server:11.0.0")
+ testImplementation("org.eclipse.jetty:jetty-servlet:11.0.0")
+ testImplementation("jakarta.servlet:jakarta.servlet-api:5.0.0")
+ testImplementation("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.0.0")
+
+ testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent"))
+ testInstrumentation(project(":instrumentation:jetty:jetty-11.0:javaagent"))
+ testInstrumentation(project(":instrumentation:struts:struts-2.3:javaagent"))
+}
+
+tasks.withType().configureEach {
+ jvmArgs("-Dotel.instrumentation.common.experimental.controller-telemetry.enabled=true")
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/ActionInvocationInstrumentation.java b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/ActionInvocationInstrumentation.java
new file mode 100644
index 000000000000..6e751570538e
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/ActionInvocationInstrumentation.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import static io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteSource.CONTROLLER;
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
+import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
+import static io.opentelemetry.javaagent.instrumentation.struts.v7_0.StrutsSingletons.instrumenter;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRoute;
+import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.struts2.ActionInvocation;
+
+public class ActionInvocationInstrumentation implements TypeInstrumentation {
+
+ @Override
+ public ElementMatcher classLoaderOptimization() {
+ return hasClassesNamed("org.apache.struts2.ActionInvocation");
+ }
+
+ @Override
+ public ElementMatcher typeMatcher() {
+ return implementsInterface(named("org.apache.struts2.ActionInvocation"));
+ }
+
+ @Override
+ public void transform(TypeTransformer transformer) {
+ transformer.applyAdviceToMethod(
+ isMethod().and(isPublic()).and(named("invokeActionOnly")),
+ this.getClass().getName() + "$InvokeActionOnlyAdvice");
+ }
+
+ @SuppressWarnings("unused")
+ public static class InvokeActionOnlyAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void onEnter(
+ @Advice.This ActionInvocation actionInvocation,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ Context parentContext = Java8BytecodeBridge.currentContext();
+
+ HttpServerRoute.update(
+ parentContext,
+ CONTROLLER,
+ StrutsServerSpanNaming.SERVER_SPAN_NAME,
+ actionInvocation.getProxy());
+
+ if (!instrumenter().shouldStart(parentContext, actionInvocation)) {
+ return;
+ }
+
+ context = instrumenter().start(parentContext, actionInvocation);
+ scope = context.makeCurrent();
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopSpan(
+ @Advice.Thrown Throwable throwable,
+ @Advice.This ActionInvocation actionInvocation,
+ @Advice.Local("otelContext") Context context,
+ @Advice.Local("otelScope") Scope scope) {
+ if (scope == null) {
+ return;
+ }
+ scope.close();
+
+ instrumenter().end(context, actionInvocation, null, throwable);
+ }
+ }
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/Struts2InstrumentationModule.java b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/Struts2InstrumentationModule.java
new file mode 100644
index 000000000000..a2ca8143df7f
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/Struts2InstrumentationModule.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import static java.util.Collections.singletonList;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import java.util.List;
+
+@AutoService(InstrumentationModule.class)
+public class Struts2InstrumentationModule extends InstrumentationModule {
+
+ public Struts2InstrumentationModule() {
+ super("struts", "struts-7.0");
+ }
+
+ @Override
+ public List typeInstrumentations() {
+ return singletonList(new ActionInvocationInstrumentation());
+ }
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsCodeAttributesGetter.java b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsCodeAttributesGetter.java
new file mode 100644
index 000000000000..a902faf6cfbb
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsCodeAttributesGetter.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesGetter;
+import org.apache.struts2.ActionInvocation;
+
+public class StrutsCodeAttributesGetter implements CodeAttributesGetter {
+
+ @Override
+ public Class> getCodeClass(ActionInvocation actionInvocation) {
+ return actionInvocation.getAction().getClass();
+ }
+
+ @Override
+ public String getMethodName(ActionInvocation actionInvocation) {
+ return actionInvocation.getProxy().getMethod();
+ }
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsServerSpanNaming.java b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsServerSpanNaming.java
new file mode 100644
index 000000000000..112ffaf646f3
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsServerSpanNaming.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import io.opentelemetry.instrumentation.api.semconv.http.HttpServerRouteGetter;
+import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath;
+import org.apache.struts2.ActionProxy;
+
+public class StrutsServerSpanNaming {
+
+ public static final HttpServerRouteGetter SERVER_SPAN_NAME =
+ (context, actionProxy) -> {
+ // We take name from the config, because it contains the path pattern from the
+ // configuration.
+ String result = actionProxy.getConfig().getName();
+
+ String actionNamespace = actionProxy.getNamespace();
+ if (actionNamespace != null && !actionNamespace.isEmpty()) {
+ if (actionNamespace.endsWith("/") || result.startsWith("/")) {
+ result = actionNamespace + result;
+ } else {
+ result = actionNamespace + "/" + result;
+ }
+ }
+
+ if (!result.startsWith("/")) {
+ result = "/" + result;
+ }
+
+ return ServletContextPath.prepend(context, result);
+ };
+
+ private StrutsServerSpanNaming() {}
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsSingletons.java b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsSingletons.java
new file mode 100644
index 000000000000..b638dd646223
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/StrutsSingletons.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeAttributesExtractor;
+import io.opentelemetry.instrumentation.api.incubator.semconv.code.CodeSpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig;
+import org.apache.struts2.ActionInvocation;
+
+public class StrutsSingletons {
+ private static final String INSTRUMENTATION_NAME = "io.opentelemetry.struts-7.0";
+
+ private static final Instrumenter INSTRUMENTER;
+
+ static {
+ StrutsCodeAttributesGetter codeAttributesGetter = new StrutsCodeAttributesGetter();
+
+ INSTRUMENTER =
+ Instrumenter.builder(
+ GlobalOpenTelemetry.get(),
+ INSTRUMENTATION_NAME,
+ CodeSpanNameExtractor.create(codeAttributesGetter))
+ .addAttributesExtractor(CodeAttributesExtractor.create(codeAttributesGetter))
+ .setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled())
+ .buildInstrumenter();
+ }
+
+ public static Instrumenter instrumenter() {
+ return INSTRUMENTER;
+ }
+
+ private StrutsSingletons() {}
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/GreetingAction.java b/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/GreetingAction.java
new file mode 100644
index 000000000000..4b3631be08c6
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/GreetingAction.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import static io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest.controller;
+
+import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.struts2.ActionSupport;
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.interceptor.parameter.StrutsParameter;
+
+public class GreetingAction extends ActionSupport {
+
+ String responseBody = "default";
+
+ public String success() {
+ responseBody = controller(ServerEndpoint.SUCCESS, ServerEndpoint.SUCCESS::getBody);
+
+ return "greeting";
+ }
+
+ public String redirect() {
+ responseBody = controller(ServerEndpoint.REDIRECT, ServerEndpoint.REDIRECT::getBody);
+ return "redirect";
+ }
+
+ public String query_param() {
+ responseBody = controller(ServerEndpoint.QUERY_PARAM, ServerEndpoint.QUERY_PARAM::getBody);
+ return "greeting";
+ }
+
+ public String error() {
+ controller(ServerEndpoint.ERROR, ServerEndpoint.ERROR::getBody);
+ return "error";
+ }
+
+ public String exception() {
+ controller(
+ ServerEndpoint.EXCEPTION,
+ () -> {
+ throw new IllegalStateException(ServerEndpoint.EXCEPTION.getBody());
+ });
+ throw new AssertionError(); // should not reach here
+ }
+
+ public String path_param() {
+ controller(
+ ServerEndpoint.PATH_PARAM,
+ () ->
+ "this does nothing, as responseBody is set in setId, but we need this controller span nevertheless");
+ return "greeting";
+ }
+
+ public String indexed_child() {
+ responseBody =
+ controller(
+ ServerEndpoint.INDEXED_CHILD,
+ () -> {
+ ServerEndpoint.INDEXED_CHILD.collectSpanAttributes(
+ (name) -> ServletActionContext.getRequest().getParameter(name));
+ return ServerEndpoint.INDEXED_CHILD.getBody();
+ });
+ return "greeting";
+ }
+
+ public String capture_headers() {
+ HttpServletRequest request = ServletActionContext.getRequest();
+ HttpServletResponse response = ServletActionContext.getResponse();
+ response.setHeader("X-Test-Response", request.getHeader("X-Test-Request"));
+ responseBody =
+ controller(ServerEndpoint.CAPTURE_HEADERS, ServerEndpoint.CAPTURE_HEADERS::getBody);
+ return "greeting";
+ }
+
+ public String dispatch_servlet() {
+ return "greetingServlet";
+ }
+
+ @StrutsParameter
+ public void setId(String id) {
+ responseBody = id;
+ }
+
+ public String getResponseBody() {
+ return responseBody;
+ }
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/GreetingServlet.java b/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/GreetingServlet.java
new file mode 100644
index 000000000000..13215454dff8
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/GreetingServlet.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class GreetingServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ resp.getWriter().write("greeting");
+ }
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/Struts2ActionSpanTest.java b/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/Struts2ActionSpanTest.java
new file mode 100644
index 000000000000..fb5bb758b910
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/struts/v7_0/Struts2ActionSpanTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.struts.v7_0;
+
+import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.ERROR;
+import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.EXCEPTION;
+import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.NOT_FOUND;
+import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.PATH_PARAM;
+import static io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint.REDIRECT;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
+import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_FUNCTION;
+import static io.opentelemetry.semconv.incubating.CodeIncubatingAttributes.CODE_NAMESPACE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.instrumentation.api.internal.HttpConstants;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerTest;
+import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.http.HttpServerTestOptions;
+import io.opentelemetry.instrumentation.testing.junit.http.ServerEndpoint;
+import io.opentelemetry.sdk.testing.assertj.SpanDataAssert;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.data.StatusData;
+import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse;
+import jakarta.servlet.DispatcherType;
+import java.net.InetSocketAddress;
+import java.util.EnumSet;
+import java.util.Locale;
+import org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class Struts2ActionSpanTest extends AbstractHttpServerTest {
+
+ @RegisterExtension
+ public static final InstrumentationExtension testing =
+ HttpServerInstrumentationExtension.forAgent();
+
+ @Override
+ protected Server setupServer() throws Exception {
+ Server server = new Server(new InetSocketAddress("localhost", port));
+
+ ServletContextHandler context = new ServletContextHandler(null, getContextPath());
+
+ context.addServlet(DefaultServlet.class, "/");
+ context.addServlet(GreetingServlet.class, "/greetingServlet");
+ context.addFilter(
+ StrutsPrepareAndExecuteFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+
+ server.setHandler(context);
+
+ server.start();
+ return server;
+ }
+
+ @Override
+ protected void stopServer(Server server) throws Exception {
+ server.stop();
+ server.destroy();
+ }
+
+ @Override
+ protected void configure(HttpServerTestOptions options) {
+ options.setContextPath("/context");
+ options.setTestPathParam(true);
+ options.setTestErrorBody(false);
+ options.setTestPathParam(false);
+ options.setHasHandlerSpan(endpoint -> !endpoint.equals(NOT_FOUND));
+ options.setHasResponseSpan(
+ endpoint ->
+ endpoint == REDIRECT
+ || endpoint == ERROR
+ || endpoint == EXCEPTION
+ || endpoint == NOT_FOUND);
+
+ options.setExpectedHttpRoute(
+ (ServerEndpoint endpoint, String method) -> {
+ if (method.equals(HttpConstants._OTHER)) {
+ return getContextPath() + endpoint.getPath();
+ }
+ if (endpoint.equals(PATH_PARAM)) {
+ return getContextPath() + "/path/{id}/param";
+ } else if (endpoint.equals(NOT_FOUND)) {
+ return getContextPath() + "/*";
+ } else {
+ return super.expectedHttpRoute(endpoint, method);
+ }
+ });
+ }
+
+ @Override
+ protected SpanDataAssert assertResponseSpan(
+ SpanDataAssert span, SpanData parentSpan, String method, ServerEndpoint endpoint) {
+ if (endpoint.equals(REDIRECT)) {
+ span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendRedirect"));
+ } else if (endpoint.equals(NOT_FOUND)) {
+ span.satisfies(spanData -> assertThat(spanData.getName()).endsWith(".sendError"))
+ .hasParent(parentSpan);
+ }
+
+ span.hasKind(SpanKind.INTERNAL);
+ return span;
+ }
+
+ @Override
+ protected SpanDataAssert assertHandlerSpan(
+ SpanDataAssert span, String method, ServerEndpoint endpoint) {
+ span.hasName("GreetingAction." + endpoint.name().toLowerCase(Locale.ROOT))
+ .hasKind(SpanKind.INTERNAL);
+
+ if (endpoint.equals(EXCEPTION)) {
+ span.hasStatus(StatusData.error())
+ .hasException(new IllegalStateException(EXCEPTION.getBody()));
+ }
+
+ span.hasAttributesSatisfyingExactly(
+ equalTo(CODE_NAMESPACE, GreetingAction.class.getName()),
+ equalTo(CODE_FUNCTION, endpoint.name().toLowerCase(Locale.ROOT)));
+ return span;
+ }
+
+ // Struts runs from a servlet filter. Test that dispatching from struts action to a servlet
+ // does not overwrite server span name given by struts instrumentation.
+ @Test
+ void testDispatchToServlet() {
+ AggregatedHttpResponse response =
+ client.get(address.resolve("dispatch").toString()).aggregate().join();
+
+ assertThat(response.status().code()).isEqualTo(200);
+ assertThat(response.contentUtf8()).isEqualTo("greeting");
+
+ testing.waitAndAssertTraces(
+ trace ->
+ trace.hasSpansSatisfyingExactly(
+ span ->
+ span.hasName("GET " + getContextPath() + "/dispatch")
+ .hasKind(SpanKind.SERVER)
+ .hasNoParent(),
+ span ->
+ span.hasName("GreetingAction.dispatch_servlet")
+ .hasKind(SpanKind.INTERNAL)
+ .hasParent(trace.getSpan(0))));
+ }
+}
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/test/resources/greeting.ftl b/instrumentation/struts/struts-7.0/javaagent/src/test/resources/greeting.ftl
new file mode 100644
index 000000000000..b833223d9494
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/test/resources/greeting.ftl
@@ -0,0 +1,2 @@
+<#-- @ftlvariable name="responseBody" type="java.lang.String" -->
+${responseBody}
\ No newline at end of file
diff --git a/instrumentation/struts/struts-7.0/javaagent/src/test/resources/struts.xml b/instrumentation/struts/struts-7.0/javaagent/src/test/resources/struts.xml
new file mode 100644
index 000000000000..362b7acb7bfd
--- /dev/null
+++ b/instrumentation/struts/struts-7.0/javaagent/src/test/resources/struts.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+ /redirected
+ false
+
+
+ 500
+
+ /greeting.ftl
+ /greetingServlet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 26e8a24ef38a..0e4cda313a8d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -571,7 +571,8 @@ include(":instrumentation:spring:spring-ws-2.0:javaagent")
include(":instrumentation:spring:starters:spring-boot-starter")
include(":instrumentation:spring:starters:zipkin-spring-boot-starter")
include(":instrumentation:spymemcached-2.12:javaagent")
-include(":instrumentation:struts-2.3:javaagent")
+include(":instrumentation:struts:struts-2.3:javaagent")
+include(":instrumentation:struts:struts-7.0:javaagent")
include(":instrumentation:tapestry-5.4:javaagent")
include(":instrumentation:tomcat:tomcat-7.0:javaagent")
include(":instrumentation:tomcat:tomcat-10.0:javaagent")