diff --git a/instrumentation/jsf/jsf-jakarta-common/javaagent/build.gradle.kts b/instrumentation/jsf/jsf-jakarta-common/javaagent/build.gradle.kts new file mode 100644 index 000000000000..d1b13059f5dd --- /dev/null +++ b/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") +} diff --git a/instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfErrorCauseExtractor.java b/instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfErrorCauseExtractor.java new file mode 100644 index 000000000000..ff1c07c60b59 --- /dev/null +++ b/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); + } +} diff --git a/instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfRequest.java b/instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfRequest.java new file mode 100644 index 000000000000..1e486db60f12 --- /dev/null +++ b/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 java.util.Objects; +import jakarta.faces.component.ActionSource2; +import jakarta.faces.event.ActionEvent; + +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; + } +} diff --git a/instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfServerSpanNaming.java b/instrumentation/jsf/jsf-jakarta-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/jsf/jakarta/JsfServerSpanNaming.java new file mode 100644 index 000000000000..c307ccde383d --- /dev/null +++ b/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() {} +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/build.gradle.kts b/instrumentation/jsf/jsf-jakarta-common/testing/build.gradle.kts new file mode 100644 index 000000000000..e223234292c4 --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/build.gradle.kts @@ -0,0 +1,22 @@ +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") + + implementation(project(":testing-common")) + implementation("org.jsoup:jsoup:1.13.1") + + val jettyVersion = "11.0.13" + api("org.eclipse.jetty:jetty-annotations:$jettyVersion") + implementation("org.eclipse.jetty:apache-jsp:$jettyVersion") + implementation("org.glassfish:jakarta.el:4.0.2") + implementation("jakarta.websocket:jakarta.websocket-api:2.0.0") +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy new file mode 100644 index 000000000000..0960b1c2d3be --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/BaseJsfTest.groovy @@ -0,0 +1,274 @@ +/* + * 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.annotations.AnnotationConfiguration +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 { + + def setupSpec() { + setupServer() + } + + def cleanupSpec() { + cleanupServer() + } + + @Override + Server startServer(int port) { + String jsfVersion = getJsfVersion() + + List configurationClasses = new ArrayList<>() + Collections.addAll(configurationClasses, WebAppContext.getDefaultConfigurationClasses()) + configurationClasses.add(AnnotationConfiguration.getName()) + + WebAppContext webAppContext = new WebAppContext() + webAppContext.setContextPath(getContextPath()) + webAppContext.setConfigurationClasses(configurationClasses) + // set up test application + webAppContext.setBaseResource(Resource.newSystemResource("test-app-" + jsfVersion)) + // add additional resources for test app + Resource extraResource = Resource.newSystemResource("test-app-" + jsfVersion + "-extra") + if (extraResource != null) { + webAppContext.getMetaData().addWebInfJar(extraResource) + } + webAppContext.getMetaData().getWebInfClassesDirs().add(Resource.newClassPathResource("/")) + + def jettyServer = new Server(port) + jettyServer.connectors.each { + it.setHost('localhost') + } + + jettyServer.setHandler(webAppContext) + jettyServer.start() + + return jettyServer + } + + abstract String getJsfVersion(); + + @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.jsf" | "*.jsf" + "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.jsf").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=javax.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("javax.faces.ViewState", viewState) + .build() + // use the session created for first request + def request2 = AggregatedHttpRequest.of( + RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.jsf;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.jsf").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=javax.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("javax.faces.ViewState", viewState) + .build() + // use the session created for first request + def request2 = AggregatedHttpRequest.of( + RequestHeaders.builder(HttpMethod.POST, address.resolve("greeting.jsf;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) + } + } +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/ExceptionFilter.groovy new file mode 100644 index 000000000000..40aa7ac24eaa --- /dev/null +++ b/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() { + } +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/groovy/GreetingForm.groovy new file mode 100644 index 000000000000..f8d429f4ee2a --- /dev/null +++ b/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") + } + } +} diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/WEB-INF/faces-config.xml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/WEB-INF/faces-config.xml new file mode 100644 index 000000000000..b076ae885c01 --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/WEB-INF/faces-config.xml @@ -0,0 +1,12 @@ + + + + + greetingForm + GreetingForm + request + + + diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/WEB-INF/web.xml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/WEB-INF/web.xml new file mode 100644 index 000000000000..d6ae379861e8 --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,28 @@ + + + + + Faces Servlet + jakarta.faces.webapp.FacesServlet + 1 + + + Faces Servlet + *.xhtml + + + Faces Servlet + /faces/* + + + + ExceptionFilter + ExceptionFilter + + + ExceptionFilter + /* + + diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/greeting.xhtml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/greeting.xhtml new file mode 100644 index 000000000000..3bc9510abb1e --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/greeting.xhtml @@ -0,0 +1,23 @@ + + + + Hello, World! + + + +

+ + + +

+

+ +

+

+ +

+
+
+ \ No newline at end of file diff --git a/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/hello.xhtml b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/hello.xhtml new file mode 100644 index 000000000000..f98ba1c01799 --- /dev/null +++ b/instrumentation/jsf/jsf-jakarta-common/testing/src/main/resources/hello.xhtml @@ -0,0 +1,8 @@ + + + Hello + + \ No newline at end of file diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..aeb4e8d75473 --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + id("otel.javaagent-instrumentation") + id("org.unbroken-dome.test-sets") +} + +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) + } +} + +testSets { + create("myfaces3Test") + create("myfaces4Test") +} + +tasks { + test { + dependsOn("myfaces3Test") + dependsOn("myfaces4Test") + } +} + +dependencies { + compileOnly("org.apache.myfaces.core:myfaces-api:3.0.2") + compileOnly("jakarta.el:jakarta.el-api:4.0.0") + + 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")) + + add("myfaces3TestImplementation", "org.apache.myfaces.core:myfaces-impl:3.0.2") + + add("myfaces4TestImplementation", "org.apache.myfaces.core:myfaces-impl:4.0.0-RC4") +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/ActionListenerImplInstrumentation.java b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/ActionListenerImplInstrumentation.java new file mode 100644 index 000000000000..978c4e27c349 --- /dev/null +++ b/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 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); + } + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesErrorCauseExtractor.java b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesErrorCauseExtractor.java new file mode 100644 index 000000000000..babdc79a8ab5 --- /dev/null +++ b/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; + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesInstrumentationModule.java b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesInstrumentationModule.java new file mode 100644 index 000000000000..5a1378789e12 --- /dev/null +++ b/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 typeInstrumentations() { + return asList( + new ActionListenerImplInstrumentation(), new RestoreViewExecutorInstrumentation()); + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesSingletons.java b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/MyFacesSingletons.java new file mode 100644 index 000000000000..6f82d5c74efe --- /dev/null +++ b/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 INSTRUMENTER; + + static { + INSTRUMENTER = + Instrumenter.builder( + GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, JsfRequest::spanName) + .setErrorCauseExtractor(new MyFacesErrorCauseExtractor()) + .setEnabled(ExperimentalConfig.get().controllerTelemetryEnabled()) + .buildInstrumenter(); + } + + public static Instrumenter instrumenter() { + return INSTRUMENTER; + } + + private MyFacesSingletons() {} +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/RestoreViewExecutorInstrumentation.java b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/myfaces/v3_0/RestoreViewExecutorInstrumentation.java new file mode 100644 index 000000000000..edb7e4b72790 --- /dev/null +++ b/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 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); + } + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces3Test/groovy/Myfaces3Test.groovy b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces3Test/groovy/Myfaces3Test.groovy new file mode 100644 index 000000000000..f74af3098490 --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces3Test/groovy/Myfaces3Test.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Myfaces3Test extends BaseJsfTest { + @Override + String getJsfVersion() { + "3" + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces3Test/resources/test-app-3-extra/META-INF/web-fragment.xml b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces3Test/resources/test-app-3-extra/META-INF/web-fragment.xml new file mode 100644 index 000000000000..e9df26c2690f --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces3Test/resources/test-app-3-extra/META-INF/web-fragment.xml @@ -0,0 +1,10 @@ + + + + + + org.apache.myfaces.webapp.StartupServletContextListener + + + diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces4Test/groovy/Myfaces4Test.groovy b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces4Test/groovy/Myfaces4Test.groovy new file mode 100644 index 000000000000..298ef9a326ea --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces4Test/groovy/Myfaces4Test.groovy @@ -0,0 +1,11 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +class Myfaces4Test extends BaseJsfTest { + @Override + String getJsfVersion() { + "4" + } +} diff --git a/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces4Test/resources/test-app-4-extra/META-INF/web-fragment.xml b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces4Test/resources/test-app-4-extra/META-INF/web-fragment.xml new file mode 100644 index 000000000000..096d6dabb11a --- /dev/null +++ b/instrumentation/jsf/jsf-myfaces-3.0/javaagent/src/myfaces4Test/resources/test-app-4-extra/META-INF/web-fragment.xml @@ -0,0 +1,10 @@ + + + + + + org.apache.myfaces.webapp.StartupServletContextListener + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index b911de42c67c..391b9e1f914d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -300,8 +300,11 @@ 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-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")