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")