Browse Source

Add Jakarta JSF 3.0+ instrumentation (#7786)

Hi,

I copied existing JSF 1.2-2 instrumentation, updated dependencies and
namespaces related to JSF 3+.
I don't work with Mojjara implementation, but copied by analogy and
verified that package names are unchanged.

I named new packages by anology with `servlet` packages, but I use
`jsf-jakarta-common` when in servlet we have `servlet-javax-common`.
My idea was to avoid touching existing packages, but perhaps to keep
consistency, I can rename old `jsf-common` to `jsf-javax-common`.

Tested with Tomcat and my app, it's working fine with JSF 4 :)
Oleh Astappiev 2 years ago
parent
commit
ae6350bbd3
28 changed files with 990 additions and 0 deletions
  1. 10 0
      instrumentation/jsf/jsf-jakarta-common/javaagent/build.gradle.kts
  2. 19 0
      instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfErrorCauseExtractor.java
  3. 45 0
      instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfRequest.java
  4. 38 0
      instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfServerSpanNaming.java
  5. 23 0
      instrumentation/jsf/jsf-jakarta-common/testing/build.gradle.kts
  6. 263 0
      instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy
  7. 38 0
      instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy
  8. 29 0
      instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy
  9. 12 0
      instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/faces-config.xml
  10. 15 0
      instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/web.xml
  11. 23 0
      instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/greeting.xhtml
  12. 8 0
      instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/hello.xhtml
  13. 35 0
      instrumentation/jsf/jsf-mojarra-3.0/javaagent/build.gradle.kts
  14. 69 0
      instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/ActionListenerImplInstrumentation.java
  15. 25 0
      instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/MojarraInstrumentationModule.java
  16. 33 0
      instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/MojarraSingletons.java
  17. 42 0
      instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/RestoreViewPhaseInstrumentation.java
  18. 7 0
      instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/groovy/Mojarra3Test.groovy
  19. 10 0
      instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/resources/test-app-extra/META-INF/web-fragment.xml
  20. 35 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts
  21. 69 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/ActionListenerImplInstrumentation.java
  22. 21 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesErrorCauseExtractor.java
  23. 26 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesInstrumentationModule.java
  24. 32 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesSingletons.java
  25. 42 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/RestoreViewExecutorInstrumentation.java
  26. 7 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/groovy/Myfaces3Test.groovy
  27. 10 0
      instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/resources/test-app-extra/META-INF/web-fragment.xml
  28. 4 0
      settings.gradle.kts

+ 10 - 0
instrumentation/jsf/jsf-jakarta-common/javaagent/build.gradle.kts

@@ -0,0 +1,10 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+dependencies {
+  compileOnly(project(":instrumentation:servlet:servlet-common:bootstrap"))
+
+  compileOnly("jakarta.faces:jakarta.faces-api:3.0.0")
+  compileOnly("jakarta.el:jakarta.el-api:4.0.0")
+}

+ 19 - 0
instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfErrorCauseExtractor.java

@@ -0,0 +1,19 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jsf.jakarta;
+
+import io.opentelemetry.instrumentation.api.instrumenter.ErrorCauseExtractor;
+import jakarta.faces.FacesException;
+
+public class JsfErrorCauseExtractor implements ErrorCauseExtractor {
+  @Override
+  public Throwable extract(Throwable error) {
+    while (error.getCause() != null && error instanceof FacesException) {
+      error = error.getCause();
+    }
+    return ErrorCauseExtractor.getDefault().extract(error);
+  }
+}

+ 45 - 0
instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfRequest.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jsf.jakarta;
+
+import jakarta.faces.component.ActionSource2;
+import jakarta.faces.event.ActionEvent;
+import java.util.Objects;
+
+public class JsfRequest {
+  private final String spanName;
+
+  public JsfRequest(ActionEvent event) {
+    this.spanName = getSpanName(event);
+  }
+
+  public String spanName() {
+    return Objects.requireNonNull(spanName);
+  }
+
+  public boolean shouldStartSpan() {
+    return spanName != null;
+  }
+
+  private static String getSpanName(ActionEvent event) {
+    // https://jakarta.ee/specifications/faces/2.3/apidocs/index.html?javax/faces/component/ActionSource2.html
+    // ActionSource2 was added in JSF 1.2 and is implemented by components that have an action
+    // attribute such as a button or a link
+    if (event.getComponent() instanceof ActionSource2) {
+      ActionSource2 actionSource = (ActionSource2) event.getComponent();
+      if (actionSource.getActionExpression() != null) {
+        // either an el expression in the form #{bean.method()} or navigation case name
+        String expressionString = actionSource.getActionExpression().getExpressionString();
+        // start span only if expression string is really an expression
+        if (expressionString.startsWith("#{") || expressionString.startsWith("${")) {
+          return expressionString;
+        }
+      }
+    }
+
+    return null;
+  }
+}

+ 38 - 0
instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfServerSpanNaming.java

@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.jsf.jakarta;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.instrumenter.LocalRootSpan;
+import io.opentelemetry.javaagent.bootstrap.servlet.ServletContextPath;
+import jakarta.faces.component.UIViewRoot;
+import jakarta.faces.context.FacesContext;
+
+public final class JsfServerSpanNaming {
+
+  public static void updateViewName(Context context, FacesContext facesContext) {
+    // just update the server span name, without touching the http.route
+    Span serverSpan = LocalRootSpan.fromContextOrNull(context);
+    if (serverSpan == null) {
+      return;
+    }
+
+    UIViewRoot uiViewRoot = facesContext.getViewRoot();
+    if (uiViewRoot == null) {
+      return;
+    }
+
+    // JSF spec 7.6.2
+    // view id is a context relative path to the web application resource that produces the
+    // view, such as a JSP page or a Facelets page.
+    String viewId = uiViewRoot.getViewId();
+    String name = ServletContextPath.prepend(context, viewId);
+    serverSpan.updateName(name);
+  }
+
+  private JsfServerSpanNaming() {}
+}

+ 23 - 0
instrumentation/jsf/jsf-jakarta-common/testing/build.gradle.kts

@@ -0,0 +1,23 @@
+plugins {
+  id("otel.java-conventions")
+}
+
+dependencies {
+  api("ch.qos.logback:logback-classic")
+  api("org.slf4j:log4j-over-slf4j")
+  api("org.slf4j:jcl-over-slf4j")
+  api("org.slf4j:jul-to-slf4j")
+
+  compileOnly("jakarta.faces:jakarta.faces-api:3.0.0")
+  compileOnly("jakarta.el:jakarta.el-api:4.0.0")
+
+  api("org.eclipse.jetty:jetty-webapp:11.0.0")
+  api("org.eclipse.jetty:apache-jstl:11.0.0")
+  api("org.eclipse.jetty:apache-jsp:11.0.0")
+
+  implementation(project(":testing-common"))
+  implementation("org.jsoup:jsoup:1.13.1")
+
+  implementation("org.glassfish:jakarta.el:4.0.2")
+  implementation("jakarta.websocket:jakarta.websocket-api:2.0.0")
+}

+ 263 - 0
instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy

@@ -0,0 +1,263 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import io.opentelemetry.api.trace.SpanKind
+import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
+import io.opentelemetry.instrumentation.test.asserts.TraceAssert
+import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait
+import io.opentelemetry.sdk.trace.data.SpanData
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
+import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpRequest
+import io.opentelemetry.testing.internal.armeria.common.AggregatedHttpResponse
+import io.opentelemetry.testing.internal.armeria.common.HttpData
+import io.opentelemetry.testing.internal.armeria.common.HttpMethod
+import io.opentelemetry.testing.internal.armeria.common.MediaType
+import io.opentelemetry.testing.internal.armeria.common.QueryParams
+import io.opentelemetry.testing.internal.armeria.common.RequestHeaders
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.util.resource.Resource
+import org.eclipse.jetty.webapp.WebAppContext
+import org.jsoup.Jsoup
+import spock.lang.Unroll
+
+import static io.opentelemetry.api.trace.SpanKind.INTERNAL
+import static io.opentelemetry.api.trace.StatusCode.ERROR
+
+abstract class BaseJsfTest extends AgentInstrumentationSpecification implements HttpServerTestTrait<Server> {
+
+  def setupSpec() {
+    setupServer()
+  }
+
+  def cleanupSpec() {
+    cleanupServer()
+  }
+
+  @Override
+  Server startServer(int port) {
+    WebAppContext webAppContext = new WebAppContext()
+    webAppContext.setContextPath(getContextPath())
+    // set up test application
+    webAppContext.setBaseResource(Resource.newSystemResource("test-app"))
+
+    Resource extraResource = Resource.newSystemResource("test-app-extra")
+    if (extraResource != null) {
+      webAppContext.getMetaData().addWebInfResource(extraResource)
+    }
+
+    def jettyServer = new Server(port)
+    jettyServer.connectors.each {
+      it.setHost('localhost')
+    }
+
+    jettyServer.setHandler(webAppContext)
+    jettyServer.start()
+
+    return jettyServer
+  }
+
+  @Override
+  void stopServer(Server server) {
+    server.stop()
+    server.destroy()
+  }
+
+  @Override
+  String getContextPath() {
+    return "/jetty-context"
+  }
+
+  @Unroll
+  def "test #path"() {
+    setup:
+    AggregatedHttpResponse response = client.get(address.resolve(path).toString()).aggregate().join()
+
+    expect:
+    response.status().code() == 200
+    response.contentUtf8().trim() == "Hello"
+
+    and:
+    assertTraces(1) {
+      trace(0, 1) {
+        span(0) {
+          name getContextPath() + "/hello.xhtml"
+          kind SpanKind.SERVER
+          hasNoParent()
+          attributes {
+            "$SemanticAttributes.NET_TRANSPORT" SemanticAttributes.NetTransportValues.IP_TCP
+            "$SemanticAttributes.NET_HOST_NAME" "localhost"
+            "$SemanticAttributes.NET_HOST_PORT" port
+            "$SemanticAttributes.NET_SOCK_PEER_ADDR" "127.0.0.1"
+            "$SemanticAttributes.NET_SOCK_PEER_PORT" Long
+            "$SemanticAttributes.NET_SOCK_HOST_ADDR" "127.0.0.1"
+            "$SemanticAttributes.HTTP_METHOD" "GET"
+            "$SemanticAttributes.HTTP_SCHEME" "http"
+            "$SemanticAttributes.HTTP_TARGET" "/jetty-context/" + path
+            "$SemanticAttributes.HTTP_USER_AGENT" TEST_USER_AGENT
+            "$SemanticAttributes.HTTP_FLAVOR" SemanticAttributes.HttpFlavorValues.HTTP_1_1
+            "$SemanticAttributes.HTTP_STATUS_CODE" 200
+            "$SemanticAttributes.HTTP_ROUTE" "/jetty-context/" + route
+            "$SemanticAttributes.HTTP_CLIENT_IP" { it == null || it == TEST_CLIENT_IP }
+          }
+        }
+      }
+    }
+
+    where:
+    path                | route
+    "hello.xhtml"       | "*.xhtml"
+    "faces/hello.xhtml" | "faces/*"
+  }
+
+  def "test greeting"() {
+    // we need to display the page first before posting data to it
+    setup:
+    AggregatedHttpResponse response = client.get(address.resolve("greeting.xhtml").toString()).aggregate().join()
+    def doc = Jsoup.parse(response.contentUtf8())
+
+    expect:
+    response.status().code() == 200
+    doc.selectFirst("title").text() == "Hello, World!"
+
+    and:
+    assertTraces(1) {
+      trace(0, 1) {
+        span(0) {
+          name getContextPath() + "/greeting.xhtml"
+          kind SpanKind.SERVER
+          hasNoParent()
+        }
+      }
+    }
+    clearExportedData()
+
+    when:
+    // extract parameters needed to post back form
+    def viewState = doc.selectFirst("[name=jakarta.faces.ViewState]")?.val()
+    def formAction = doc.selectFirst("#app-form").attr("action")
+    def jsessionid = formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length())
+
+    then:
+    viewState != null
+    jsessionid != null
+
+    when:
+    // set up form parameter for post
+    QueryParams formBody = QueryParams.builder()
+      .add("app-form", "app-form")
+    // value used for name is returned in app-form:output-message element
+      .add("app-form:name", "test")
+      .add("app-form:submit", "Say hello")
+      .add("app-form_SUBMIT", "1") // MyFaces
+      .add("jakarta.faces.ViewState", viewState)
+      .build()
+    // use the session created for first request
+    def request2 = AggregatedHttpRequest.of(
+      RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.xhtml;jsessionid=" + jsessionid).toString())
+        .contentType(MediaType.FORM_DATA)
+        .build(),
+      HttpData.ofUtf8(formBody.toQueryString()))
+    AggregatedHttpResponse response2 = client.execute(request2).aggregate().join()
+    def responseContent = response2.contentUtf8()
+    def doc2 = Jsoup.parse(responseContent)
+
+    then:
+    response2.status().code() == 200
+    doc2.getElementById("app-form:output-message").text() == "Hello test"
+
+    and:
+    assertTraces(1) {
+      trace(0, 2) {
+        span(0) {
+          name getContextPath() + "/greeting.xhtml"
+          kind SpanKind.SERVER
+          hasNoParent()
+        }
+        handlerSpan(it, 1, span(0), "#{greetingForm.submit()}")
+      }
+    }
+  }
+
+  def "test exception"() {
+    // we need to display the page first before posting data to it
+    setup:
+    AggregatedHttpResponse response = client.get(address.resolve("greeting.xhtml").toString()).aggregate().join()
+    def doc = Jsoup.parse(response.contentUtf8())
+
+    expect:
+    response.status().code() == 200
+    doc.selectFirst("title").text() == "Hello, World!"
+
+    and:
+    assertTraces(1) {
+      trace(0, 1) {
+        span(0) {
+          name getContextPath() + "/greeting.xhtml"
+          kind SpanKind.SERVER
+          hasNoParent()
+        }
+      }
+    }
+    clearExportedData()
+
+    when:
+    // extract parameters needed to post back form
+    def viewState = doc.selectFirst("[name=jakarta.faces.ViewState]").val()
+    def formAction = doc.selectFirst("#app-form").attr("action")
+    def jsessionid = formAction.substring(formAction.indexOf("jsessionid=") + "jsessionid=".length())
+
+    then:
+    viewState != null
+    jsessionid != null
+
+    when:
+    // set up form parameter for post
+    QueryParams formBody = QueryParams.builder()
+      .add("app-form", "app-form")
+    // setting name parameter to "exception" triggers throwing exception in GreetingForm
+      .add("app-form:name", "exception")
+      .add("app-form:submit", "Say hello")
+      .add("app-form_SUBMIT", "1") // MyFaces
+      .add("jakarta.faces.ViewState", viewState)
+      .build()
+    // use the session created for first request
+    def request2 = AggregatedHttpRequest.of(
+      RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.xhtml;jsessionid=" + jsessionid).toString())
+        .contentType(MediaType.FORM_DATA)
+        .build(),
+      HttpData.ofUtf8(formBody.toQueryString()))
+    AggregatedHttpResponse response2 = client.execute(request2).aggregate().join()
+
+    then:
+    response2.status().code() == 500
+    def ex = new Exception("submit exception")
+
+    and:
+    assertTraces(1) {
+      trace(0, 2) {
+        span(0) {
+          name getContextPath() + "/greeting.xhtml"
+          kind SpanKind.SERVER
+          hasNoParent()
+          status ERROR
+          errorEvent(ex.class, ex.message)
+        }
+        handlerSpan(it, 1, span(0), "#{greetingForm.submit()}", ex)
+      }
+    }
+  }
+
+  void handlerSpan(TraceAssert trace, int index, Object parent, String spanName, Exception expectedException = null) {
+    trace.span(index) {
+      name spanName
+      kind INTERNAL
+      if (expectedException != null) {
+        status ERROR
+        errorEvent(expectedException.getClass(), expectedException.getMessage())
+      }
+      childOf((SpanData) parent)
+    }
+  }
+}

+ 38 - 0
instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy

@@ -0,0 +1,38 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import jakarta.servlet.Filter
+import jakarta.servlet.FilterChain
+import jakarta.servlet.FilterConfig
+import jakarta.servlet.ServletException
+import jakarta.servlet.ServletRequest
+import jakarta.servlet.ServletResponse
+
+class ExceptionFilter implements Filter {
+  @Override
+  void init(FilterConfig filterConfig) throws ServletException {
+  }
+
+  @Override
+  void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+    try {
+      chain.doFilter(request, response)
+    } catch (Exception exception) {
+      // to ease testing unwrap our exception to root cause
+      Exception tmp = exception
+      while (tmp.getCause() != null) {
+        tmp = tmp.getCause()
+      }
+      if (tmp.getMessage() != null && tmp.getMessage().contains("submit exception")) {
+        throw tmp
+      }
+      throw exception
+    }
+  }
+
+  @Override
+  void destroy() {
+  }
+}

+ 29 - 0
instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy

@@ -0,0 +1,29 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+class GreetingForm {
+
+  String name = ""
+  String message = ""
+
+  String getName() {
+    name
+  }
+
+  void setName(String name) {
+    this.name = name
+  }
+
+  String getMessage() {
+    return message
+  }
+
+  void submit() {
+    message = "Hello " + name
+    if (name == "exception") {
+      throw new Exception("submit exception")
+    }
+  }
+}

+ 12 - 0
instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/faces-config.xml

@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<faces-config 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-facesconfig_3_0.xsd"
+  version="3.0">
+
+  <managed-bean>
+    <managed-bean-name>greetingForm</managed-bean-name>
+    <managed-bean-class>GreetingForm</managed-bean-class>
+    <managed-bean-scope>request</managed-bean-scope>
+  </managed-bean>
+
+</faces-config>

+ 15 - 0
instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/WEB-INF/web.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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_5_0.xsd"
+  version="5.0">
+
+  <filter>
+    <filter-name>ExceptionFilter</filter-name>
+    <filter-class>ExceptionFilter</filter-class>
+  </filter>
+  <filter-mapping>
+    <filter-name>ExceptionFilter</filter-name>
+    <url-pattern>/*</url-pattern>
+  </filter-mapping>
+
+</web-app>

+ 23 - 0
instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/greeting.xhtml

@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html lang="en"
+      xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:h="http://xmlns.jcp.org/jsf/html">
+  <h:head>
+    <title>Hello, World!</title>
+  </h:head>
+  <h:body>
+    <h:form id="app-form">
+      <p>
+        <h:outputLabel for="name" value="Enter your name" required="true"/>
+        <h:inputText id="name" value="#{greetingForm.name}"/>
+        <h:message for="name"/>
+      </p>
+      <p>
+        <h:commandButton id="submit" value="Say hello" action="#{greetingForm.submit()}"/>
+      </p>
+      <p>
+        <h:outputText id="output-message" value="#{greetingForm.message}"/>
+      </p>
+    </h:form>
+  </h:body>
+</html>

+ 8 - 0
instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/test-app/hello.xhtml

@@ -0,0 +1,8 @@
+<f:view xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
+        xmlns:f="http://xmlns.jcp.org/jsf/core"
+        contentType="text/html"
+        encoding="UTF-8">
+  <ui:composition>
+    Hello
+  </ui:composition>
+</f:view>

+ 35 - 0
instrumentation/jsf/jsf-mojarra-3.0/javaagent/build.gradle.kts

@@ -0,0 +1,35 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+  pass {
+    group.set("org.glassfish")
+    module.set("jakarta.faces")
+    versions.set("[3,)")
+    extraDependency("jakarta.el:jakarta.el-api:4.0.0")
+    assertInverse.set(true)
+  }
+}
+
+otelJava {
+  minJavaVersionSupported.set(JavaVersion.VERSION_11)
+}
+
+dependencies {
+  library("jakarta.el:jakarta.el-api:4.0.0")
+  library("jakarta.faces:jakarta.faces-api:3.0.0")
+  testLibrary("org.glassfish:jakarta.faces:3.0.4")
+
+  implementation(project(":instrumentation:jsf:jsf-jakarta-common:javaagent"))
+  testImplementation(project(":instrumentation:jsf:jsf-jakarta-common:testing"))
+
+  testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent"))
+  testInstrumentation(project(":instrumentation:servlet:servlet-common:javaagent"))
+
+  latestDepTestLibrary("jakarta.el:jakarta.el-api:4.+")
+  latestDepTestLibrary("jakarta.faces:jakarta.faces-api:3.+")
+  latestDepTestLibrary("org.glassfish:jakarta.faces:3.+")
+  // JSF 4+ requires CDI instead of BeanManager, the test should be upgraded first
+  // latestDepTestLibrary("org.glassfish:jakarta.faces:4.+")
+}

+ 69 - 0
instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/ActionListenerImplInstrumentation.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.mojarra.v3_0;
+
+import static io.opentelemetry.javaagent.instrumentation.mojarra.v3_0.MojarraSingletons.instrumenter;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfRequest;
+import jakarta.faces.event.ActionEvent;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class ActionListenerImplInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("com.sun.faces.application.ActionListenerImpl");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("processAction"),
+        ActionListenerImplInstrumentation.class.getName() + "$ProcessActionAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class ProcessActionAdvice {
+
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Argument(0) ActionEvent event,
+        @Advice.Local("otelRequest") JsfRequest request,
+        @Advice.Local("otelContext") Context context,
+        @Advice.Local("otelScope") Scope scope) {
+      Context parentContext = Java8BytecodeBridge.currentContext();
+
+      request = new JsfRequest(event);
+      if (!request.shouldStartSpan() || !instrumenter().shouldStart(parentContext, request)) {
+        return;
+      }
+
+      context = instrumenter().start(parentContext, request);
+      scope = context.makeCurrent();
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void stopSpan(
+        @Advice.Thrown Throwable throwable,
+        @Advice.Local("otelRequest") JsfRequest request,
+        @Advice.Local("otelContext") Context context,
+        @Advice.Local("otelScope") Scope scope) {
+      if (scope == null) {
+        return;
+      }
+      scope.close();
+      instrumenter().end(context, request, null, throwable);
+    }
+  }
+}

+ 25 - 0
instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/MojarraInstrumentationModule.java

@@ -0,0 +1,25 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.mojarra.v3_0;
+
+import static java.util.Arrays.asList;
+
+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 MojarraInstrumentationModule extends InstrumentationModule {
+  public MojarraInstrumentationModule() {
+    super("mojarra", "mojarra-3.0");
+  }
+
+  @Override
+  public List<TypeInstrumentation> typeInstrumentations() {
+    return asList(new ActionListenerImplInstrumentation(), new RestoreViewPhaseInstrumentation());
+  }
+}

+ 33 - 0
instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/MojarraSingletons.java

@@ -0,0 +1,33 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.mojarra.v3_0;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig;
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfErrorCauseExtractor;
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfRequest;
+
+public class MojarraSingletons {
+  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jsf-mojarra-3.0";
+
+  private static final Instrumenter<JsfRequest, Void> INSTRUMENTER;
+
+  static {
+    INSTRUMENTER =
+        Instrumenter.<JsfRequest, Void>builder(
+                GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, JsfRequest::spanName)
+            .setErrorCauseExtractor(new JsfErrorCauseExtractor())
+            .setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled())
+            .buildInstrumenter();
+  }
+
+  public static Instrumenter<JsfRequest, Void> instrumenter() {
+    return INSTRUMENTER;
+  }
+
+  private MojarraSingletons() {}
+}

+ 42 - 0
instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/mojarra/v3_0/RestoreViewPhaseInstrumentation.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.mojarra.v3_0;
+
+import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfServerSpanNaming;
+import jakarta.faces.context.FacesContext;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class RestoreViewPhaseInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("com.sun.faces.lifecycle.RestoreViewPhase");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("execute").and(takesArgument(0, named("jakarta.faces.context.FacesContext"))),
+        RestoreViewPhaseInstrumentation.class.getName() + "$ExecuteAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class ExecuteAdvice {
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(@Advice.Argument(0) FacesContext facesContext) {
+      JsfServerSpanNaming.updateViewName(currentContext(), facesContext);
+    }
+  }
+}

+ 7 - 0
instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/groovy/Mojarra3Test.groovy

@@ -0,0 +1,7 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+class Mojarra3Test extends BaseJsfTest {
+}

+ 10 - 0
instrumentation/jsf/jsf-mojarra-3.0/javaagent/src/test/resources/test-app-extra/META-INF/web-fragment.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-fragment 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-fragment_5_0.xsd"
+  version="5.0">
+
+  <listener>
+    <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
+  </listener>
+
+</web-fragment>

+ 35 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts

@@ -0,0 +1,35 @@
+plugins {
+  id("otel.javaagent-instrumentation")
+}
+
+muzzle {
+  pass {
+    group.set("org.apache.myfaces.core")
+    module.set("myfaces-impl")
+    versions.set("[3,)")
+    extraDependency("jakarta.el:jakarta.el-api:4.0.0")
+    assertInverse.set(true)
+  }
+}
+
+otelJava {
+  minJavaVersionSupported.set(JavaVersion.VERSION_11)
+}
+
+dependencies {
+  library("jakarta.el:jakarta.el-api:4.0.0")
+  library("org.apache.myfaces.core:myfaces-api:3.0.2")
+  testLibrary("org.apache.myfaces.core:myfaces-impl:3.0.2")
+
+  implementation(project(":instrumentation:jsf:jsf-jakarta-common:javaagent"))
+  testImplementation(project(":instrumentation:jsf:jsf-jakarta-common:testing"))
+
+  testInstrumentation(project(":instrumentation:servlet:servlet-5.0:javaagent"))
+  testInstrumentation(project(":instrumentation:servlet:servlet-common:javaagent"))
+
+  latestDepTestLibrary("jakarta.el:jakarta.el-api:4.+")
+  latestDepTestLibrary("org.apache.myfaces.core:myfaces-api:3.+")
+  latestDepTestLibrary("org.apache.myfaces.core:myfaces-impl:3.+")
+  // JSF 4+ requires CDI instead of BeanManager, the test should be upgraded first
+  // latestDepTestLibrary("org.apache.myfaces.core:myfaces-impl:4.+")
+}

+ 69 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/ActionListenerImplInstrumentation.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.myfaces.v3_0;
+
+import static io.opentelemetry.javaagent.instrumentation.myfaces.v3_0.MyFacesSingletons.instrumenter;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import io.opentelemetry.context.Context;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfRequest;
+import jakarta.faces.event.ActionEvent;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class ActionListenerImplInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("org.apache.myfaces.application.ActionListenerImpl");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("processAction"),
+        ActionListenerImplInstrumentation.class.getName() + "$ProcessActionAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class ProcessActionAdvice {
+
+    @Advice.OnMethodEnter(suppress = Throwable.class)
+    public static void onEnter(
+        @Advice.Argument(0) ActionEvent event,
+        @Advice.Local("otelRequest") JsfRequest request,
+        @Advice.Local("otelContext") Context context,
+        @Advice.Local("otelScope") Scope scope) {
+      Context parentContext = Java8BytecodeBridge.currentContext();
+
+      request = new JsfRequest(event);
+      if (!request.shouldStartSpan() || !instrumenter().shouldStart(parentContext, request)) {
+        return;
+      }
+
+      context = instrumenter().start(parentContext, request);
+      scope = context.makeCurrent();
+    }
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void stopSpan(
+        @Advice.Thrown Throwable throwable,
+        @Advice.Local("otelRequest") JsfRequest request,
+        @Advice.Local("otelContext") Context context,
+        @Advice.Local("otelScope") Scope scope) {
+      if (scope == null) {
+        return;
+      }
+      scope.close();
+      instrumenter().end(context, request, null, throwable);
+    }
+  }
+}

+ 21 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesErrorCauseExtractor.java

@@ -0,0 +1,21 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.myfaces.v3_0;
+
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfErrorCauseExtractor;
+import jakarta.el.ELException;
+
+public class MyFacesErrorCauseExtractor extends JsfErrorCauseExtractor {
+
+  @Override
+  public Throwable extract(Throwable error) {
+    error = super.extract(error);
+    while (error.getCause() != null && error instanceof ELException) {
+      error = error.getCause();
+    }
+    return error;
+  }
+}

+ 26 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesInstrumentationModule.java

@@ -0,0 +1,26 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.myfaces.v3_0;
+
+import static java.util.Arrays.asList;
+
+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 MyFacesInstrumentationModule extends InstrumentationModule {
+  public MyFacesInstrumentationModule() {
+    super("myfaces", "myfaces-3.0");
+  }
+
+  @Override
+  public List<TypeInstrumentation> typeInstrumentations() {
+    return asList(
+        new ActionListenerImplInstrumentation(), new RestoreViewExecutorInstrumentation());
+  }
+}

+ 32 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesSingletons.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.myfaces.v3_0;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.javaagent.bootstrap.internal.ExperimentalConfig;
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfRequest;
+
+public class MyFacesSingletons {
+  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.jsf-myfaces-3.0";
+
+  private static final Instrumenter<JsfRequest, Void> INSTRUMENTER;
+
+  static {
+    INSTRUMENTER =
+        Instrumenter.<JsfRequest, Void>builder(
+                GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, JsfRequest::spanName)
+            .setErrorCauseExtractor(new MyFacesErrorCauseExtractor())
+            .setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled())
+            .buildInstrumenter();
+  }
+
+  public static Instrumenter<JsfRequest, Void> instrumenter() {
+    return INSTRUMENTER;
+  }
+
+  private MyFacesSingletons() {}
+}

+ 42 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/RestoreViewExecutorInstrumentation.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.myfaces.v3_0;
+
+import static io.opentelemetry.javaagent.bootstrap.Java8BytecodeBridge.currentContext;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
+import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
+import io.opentelemetry.javaagent.instrumentation.jsf.jakarta.JsfServerSpanNaming;
+import jakarta.faces.context.FacesContext;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public class RestoreViewExecutorInstrumentation implements TypeInstrumentation {
+
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("org.apache.myfaces.lifecycle.RestoreViewExecutor");
+  }
+
+  @Override
+  public void transform(TypeTransformer transformer) {
+    transformer.applyAdviceToMethod(
+        named("execute").and(takesArgument(0, named("jakarta.faces.context.FacesContext"))),
+        RestoreViewExecutorInstrumentation.class.getName() + "$ExecuteAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class ExecuteAdvice {
+
+    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+    public static void onExit(@Advice.Argument(0) FacesContext facesContext) {
+      JsfServerSpanNaming.updateViewName(currentContext(), facesContext);
+    }
+  }
+}

+ 7 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/groovy/Myfaces3Test.groovy

@@ -0,0 +1,7 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+class Myfaces3Test extends BaseJsfTest {
+}

+ 10 - 0
instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/test/resources/test-app-extra/META-INF/web-fragment.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-fragment 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-fragment_5_0.xsd"
+  version="5.0">
+
+  <listener>
+    <listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
+  </listener>
+
+</web-fragment>

+ 4 - 0
settings.gradle.kts

@@ -302,8 +302,12 @@ hideFromDependabot(":instrumentation:jmx-metrics:javaagent")
 hideFromDependabot(":instrumentation:jmx-metrics:library")
 hideFromDependabot(":instrumentation:jsf:jsf-common:javaagent")
 hideFromDependabot(":instrumentation:jsf:jsf-common:testing")
+hideFromDependabot(":instrumentation:jsf:jsf-jakarta-common:javaagent")
+hideFromDependabot(":instrumentation:jsf:jsf-jakarta-common:testing")
 hideFromDependabot(":instrumentation:jsf:jsf-mojarra-1.2:javaagent")
+hideFromDependabot(":instrumentation:jsf:jsf-mojarra-3.0:javaagent")
 hideFromDependabot(":instrumentation:jsf:jsf-myfaces-1.2:javaagent")
+hideFromDependabot(":instrumentation:jsf:jsf-myfaces-3.0:javaagent")
 hideFromDependabot(":instrumentation:jsp-2.3:javaagent")
 hideFromDependabot(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:bootstrap")
 hideFromDependabot(":instrumentation:kafka:kafka-clients:kafka-clients-0.11:javaagent")