Instrument jsf action calls (#2018)

This commit is contained in:
Lauri Tulmin 2021-01-20 07:34:41 +02:00 committed by GitHub
parent c6cc263c6e
commit 9825ab0afa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1205 additions and 89 deletions

View File

@ -10,6 +10,7 @@ import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
import static io.opentelemetry.api.trace.Span.Kind.SERVER
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import org.apache.camel.CamelContext
@ -18,7 +19,7 @@ import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class RestCamelTest extends AgentTestRunner {
class RestCamelTest extends AgentTestRunner implements RetryOnAddressAlreadyInUseTrait {
@Shared
ConfigurableApplicationContext server

View File

@ -8,6 +8,7 @@ package test
import static io.opentelemetry.api.trace.Span.Kind.SERVER
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait
import io.opentelemetry.instrumentation.test.utils.OkHttpUtils
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
@ -19,7 +20,7 @@ import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class SingleServiceCamelTest extends AgentTestRunner {
class SingleServiceCamelTest extends AgentTestRunner implements RetryOnAddressAlreadyInUseTrait {
@Shared
ConfigurableApplicationContext server

View File

@ -10,6 +10,7 @@ import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
import static io.opentelemetry.api.trace.Span.Kind.SERVER
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import org.apache.camel.CamelContext
@ -20,7 +21,7 @@ import org.springframework.boot.SpringApplication
import org.springframework.context.ConfigurableApplicationContext
import spock.lang.Shared
class TwoServicesWithDirectClientCamelTest extends AgentTestRunner {
class TwoServicesWithDirectClientCamelTest extends AgentTestRunner implements RetryOnAddressAlreadyInUseTrait {
@Shared
int portOne

View File

@ -8,6 +8,7 @@ import static Jms2Test.producerSpan
import com.google.common.io.Files
import io.opentelemetry.instrumentation.test.AgentTestRunner
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import javax.jms.Session
@ -101,7 +102,9 @@ class SpringTemplateJms2Test extends AgentTestRunner {
def "send and receive message generates spans"() {
setup:
AtomicReference<String> msgId = new AtomicReference<>()
CountDownLatch countDownLatch = new CountDownLatch(1)
Thread.start {
countDownLatch.countDown()
TextMessage msg = template.receive(destination)
assert msg.text == messageText
msgId.set(msg.getJMSMessageID())
@ -111,6 +114,8 @@ class SpringTemplateJms2Test extends AgentTestRunner {
session -> template.getMessageConverter().toMessage("responded!", session)
}
}
// wait for thread to start, we expect the first span to be from receive
countDownLatch.await()
TextMessage receivedMessage = template.sendAndReceive(destination) {
session -> template.getMessageConverter().toMessage(messageText, session)
}

View File

@ -0,0 +1,6 @@
apply from: "$rootDir/gradle/instrumentation-library.gradle"
dependencies {
compileOnly group: 'jakarta.faces', name: 'jakarta.faces-api', version: '2.3.2'
compileOnly group: 'jakarta.el', name: 'jakarta.el-api', version: '3.0.3'
}

View File

@ -0,0 +1,65 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.jsf;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.servlet.ServletContextPath;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
import javax.faces.FacesException;
import javax.faces.component.ActionSource2;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
public abstract class JsfTracer extends BaseTracer {
public Span startSpan(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 tracer.spanBuilder(expressionString).startSpan();
}
}
}
return null;
}
public void updateServerSpanName(Context context, FacesContext facesContext) {
Span serverSpan = getCurrentServerSpan();
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();
serverSpan.updateName(ServletContextPath.prepend(context, viewId));
}
@Override
protected Throwable unwrapThrowable(Throwable throwable) {
while (throwable.getCause() != null && throwable instanceof FacesException) {
throwable = throwable.getCause();
}
return super.unwrapThrowable(throwable);
}
}

View File

@ -0,0 +1,19 @@
apply from: "$rootDir/gradle/java.gradle"
dependencies {
api deps.testLogging
compileOnly group: 'jakarta.faces', name: 'jakarta.faces-api', version: '2.3.2'
compileOnly group: 'jakarta.el', name: 'jakarta.el-api', version: '3.0.3'
implementation(project(':testing-common')) {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
}
implementation group: 'org.jsoup', name: 'jsoup', version: '1.13.1'
def jettyVersion = '9.4.35.v20201120'
api group: 'org.eclipse.jetty', name: 'jetty-annotations', version: jettyVersion
implementation group: 'org.eclipse.jetty', name: 'apache-jsp', version: jettyVersion
implementation group: 'org.glassfish', name: 'jakarta.el', version: '3.0.2'
implementation group: 'jakarta.websocket', name: 'jakarta.websocket-api', version: '1.1.1'
}

View File

@ -0,0 +1,229 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.api.trace.Span.Kind.INTERNAL
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.basicSpan
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait
import io.opentelemetry.sdk.trace.data.SpanData
import okhttp3.FormBody
import okhttp3.HttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
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
abstract class BaseJsfTest extends AgentTestRunner implements HttpServerTestTrait<Server> {
@Override
Server startServer(int port) {
String jsfVersion = getJsfVersion()
List<String> 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:
def url = HttpUrl.get(address.resolve("hello.jsf")).newBuilder().build()
def request = request(url, "GET", null).build()
Response response = client.newCall(request).execute()
expect:
response.code() == 200
response.body().string().trim() == "Hello"
and:
assertTraces(1) {
trace(0, 1) {
basicSpan(it, 0, getContextPath() + "/hello.xhtml", null)
}
}
where:
path << ['hello.jsf', 'faces/hello.xhtml']
}
def "test greeting"() {
// we need to display the page first before posting data to it
setup:
def url = HttpUrl.get(address.resolve("greeting.jsf")).newBuilder().build()
def request = request(url, "GET", null).build()
Response response = client.newCall(request).execute()
def doc = Jsoup.parse(response.body().string())
expect:
response.code() == 200
doc.selectFirst("title").text() == "Hello, World!"
and:
assertTraces(1) {
trace(0, 1) {
basicSpan(it, 0, getContextPath() + "/greeting.xhtml", null)
}
}
TEST_WRITER.clear()
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:
// use the session created for first request
def url2 = HttpUrl.get(address.resolve("greeting.jsf;jsessionid=" + jsessionid)).newBuilder().build()
// set up form parameter for post
RequestBody formBody = new FormBody.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()
def request2 = this.request(url2, "POST", formBody).build()
Response response2 = client.newCall(request2).execute()
def responseContent = response2.body().string()
def doc2 = Jsoup.parse(responseContent)
then:
response2.code() == 200
doc2.getElementById("app-form:output-message").text() == "Hello test"
and:
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, getContextPath() + "/greeting.xhtml", null)
handlerSpan(it, 1, span(0), "#{greetingForm.submit()}")
}
}
}
def "test exception"() {
// we need to display the page first before posting data to it
setup:
def url = HttpUrl.get(address.resolve("greeting.jsf")).newBuilder().build()
def request = request(url, "GET", null).build()
Response response = client.newCall(request).execute()
def doc = Jsoup.parse(response.body().string())
expect:
response.code() == 200
doc.selectFirst("title").text() == "Hello, World!"
and:
assertTraces(1) {
trace(0, 1) {
basicSpan(it, 0, getContextPath() + "/greeting.xhtml", null)
}
}
TEST_WRITER.clear()
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:
// use the session created for first request
def url2 = HttpUrl.get(address.resolve("greeting.jsf;jsessionid=" + jsessionid)).newBuilder().build()
// set up form parameter for post
RequestBody formBody = new FormBody.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()
def request2 = this.request(url2, "POST", formBody).build()
Response response2 = client.newCall(request2).execute()
then:
response2.code() == 500
and:
assertTraces(1) {
trace(0, 2) {
basicSpan(it, 0, getContextPath() + "/greeting.xhtml", null, new Exception("submit exception"))
handlerSpan(it, 1, span(0), "#{greetingForm.submit()}", new Exception("submit exception"))
}
}
}
Request.Builder request(HttpUrl url, String method, RequestBody body) {
return new Request.Builder()
.url(url)
.method(method, body)
.header("User-Agent", TEST_USER_AGENT)
.header("X-Forwarded-For", TEST_CLIENT_IP)
}
void handlerSpan(TraceAssert trace, int index, Object parent, String spanName, Exception expectedException = null) {
trace.span(index) {
name spanName
kind INTERNAL
errored expectedException != null
if (expectedException != null) {
errorEvent(expectedException.getClass(), expectedException.getMessage())
}
childOf((SpanData) parent)
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import javax.servlet.Filter
import javax.servlet.FilterChain
import javax.servlet.FilterConfig
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.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() {
}
}

View File

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

View File

@ -0,0 +1,14 @@
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
version="1.2">
<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>
<application>
<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" metadata-complete="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.jsf</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<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>
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.xhtml</param-value>
</context-param>
</web-app>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html">
<head>
<title>Hello, World!</title>
</head>
<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()}">
</h:commandButton>
</p>
<p>
<h:outputText id="output-message" value="#{greetingForm.message}"/>
</p>
</h:form>
</body>
</html>

View File

@ -0,0 +1,6 @@
<f:view xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition>
Hello
</ui:composition>
</f:view>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee" version="2.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>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" metadata-complete="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,79 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
muzzle {
pass {
group = "org.glassfish"
module = "jakarta.faces"
versions = "[2.3.9,3)"
extraDependency "javax.el:el-api:2.2"
}
pass {
group = "org.glassfish"
module = "javax.faces"
versions = "[2.0.7,3)"
extraDependency "javax.el:el-api:2.2"
}
pass {
group = "com.sun.faces"
module = "jsf-impl"
versions = "[2.1,2.2)"
extraDependency "javax.faces:jsf-api:2.1"
extraDependency "javax.el:el-api:1.0"
}
pass {
group = "com.sun.faces"
module = "jsf-impl"
versions = "[2.0,2.1)"
extraDependency "javax.faces:jsf-api:2.0"
extraDependency "javax.el:el-api:1.0"
}
pass {
group = "javax.faces"
module = "jsf-impl"
versions = "[1.2,2)"
extraDependency "javax.faces:jsf-api:1.2"
extraDependency "javax.el:el-api:1.0"
}
fail {
group = "org.glassfish"
module = "jakarta.faces"
versions = "[3.0,)"
extraDependency "javax.el:el-api:2.2"
}
fail {
group = "javax.faces"
module = "jsf-impl"
versions = "[1.1,1.2)"
extraDependency "javax.faces:jsf-api:1.1_02"
extraDependency "javax.el:el-api:1.0"
}
}
testSets {
mojarra12Test
mojarra2Test
latestDepTest {
extendsFrom mojarra2Test
dirName = 'mojarra2LatestTest'
}
}
test.dependsOn mojarra12Test, mojarra2Test
dependencies {
compileOnly group: 'javax.faces', name: 'jsf-api', version: '1.2'
implementation project(':instrumentation:jsf:jsf-common:library')
testImplementation project(':instrumentation:jsf:jsf-testing-common')
testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent')
mojarra12TestImplementation group: 'javax.faces', name: 'jsf-impl', version: '1.2-20'
mojarra12TestImplementation group: 'javax.faces', name: 'jsf-api', version: '1.2'
mojarra12TestImplementation group: 'com.sun.facelets', name: 'jsf-facelets', version: '1.1.14'
mojarra2TestImplementation group: 'org.glassfish', name: 'jakarta.faces', version: '2.3.12'
latestDepTestImplementation group: 'org.glassfish', name: 'jakarta.faces', version: '2+'
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mojarra;
import static io.opentelemetry.javaagent.instrumentation.mojarra.MojarraTracer.tracer;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import javax.faces.event.ActionEvent;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class ActionListenerImplInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("com.sun.faces.application.ActionListenerImpl");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("processAction"),
ActionListenerImplInstrumentation.class.getName() + "$ProcessActionAdvice");
}
public static class ProcessActionAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Argument(0) ActionEvent event,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope) {
span = tracer().startSpan(event);
if (span != null) {
scope = tracer().startScope(span);
}
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Thrown Throwable throwable,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope) {
if (scope != null) {
scope.close();
if (throwable != null) {
tracer().endExceptionally(span, throwable);
} else {
tracer().end(span);
}
}
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mojarra;
import static java.util.Arrays.asList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.List;
@AutoService(InstrumentationModule.class)
public class MojarraInstrumentationModule extends InstrumentationModule {
public MojarraInstrumentationModule() {
super("mojarra", "mojarra-1.2");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(new ActionListenerImplInstrumentation(), new RestoreViewPhaseInstrumentation());
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mojarra;
import io.opentelemetry.instrumentation.jsf.JsfTracer;
public class MojarraTracer extends JsfTracer {
private static final MojarraTracer TRACER = new MojarraTracer();
public static MojarraTracer tracer() {
return TRACER;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.mojarra";
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.mojarra;
import static io.opentelemetry.javaagent.instrumentation.mojarra.MojarraTracer.tracer;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import javax.faces.context.FacesContext;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class RestoreViewPhaseInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("com.sun.faces.lifecycle.RestoreViewPhase");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("execute").and(takesArgument(0, named("javax.faces.context.FacesContext"))),
RestoreViewPhaseInstrumentation.class.getName() + "$ExecuteAdvice");
}
public static class ExecuteAdvice {
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Argument(0) FacesContext facesContext) {
tracer().updateServerSpanName(Java8BytecodeBridge.currentContext(), facesContext);
}
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
class Mojarra12Test extends BaseJsfTest {
@Override
String getJsfVersion() {
"1.2"
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-fragment version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd">
<listener>
<listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>
</web-fragment>

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
class Mojarra2LatestTest extends BaseJsfTest {
@Override
String getJsfVersion() {
"2"
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
class Mojarra2Test extends BaseJsfTest {
@Override
String getJsfVersion() {
"2"
}
}

View File

@ -0,0 +1,42 @@
apply from: "$rootDir/gradle/instrumentation.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
muzzle {
pass {
group = "org.apache.myfaces.core"
module = "myfaces-impl"
versions = "[1.2,3)"
extraDependency "jakarta.el:jakarta.el-api:3.0.3"
assertInverse = true
}
}
testSets {
myfaces12Test
myfaces2Test
latestDepTest {
extendsFrom myfaces2Test
dirName = 'myfaces2LatestTest'
}
}
test.dependsOn myfaces12Test, myfaces2Test
dependencies {
compileOnly group: 'org.apache.myfaces.core', name: 'myfaces-api', version: '1.2.12'
compileOnly group: 'javax.el', name: 'el-api', version: '1.0'
implementation project(':instrumentation:jsf:jsf-common:library')
testImplementation project(':instrumentation:jsf:jsf-testing-common')
testInstrumentation project(':instrumentation:servlet:servlet-3.0:javaagent')
myfaces12TestImplementation group: 'org.apache.myfaces.core', name: 'myfaces-impl', version: '1.2.12'
myfaces12TestImplementation group: 'com.sun.facelets', name: 'jsf-facelets', version: '1.1.14'
myfaces2TestImplementation group: 'org.apache.myfaces.core', name: 'myfaces-impl', version: '2.3.2'
myfaces2TestImplementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.11'
myfaces2TestImplementation group: 'com.sun.xml.bind', name: 'jaxb-impl', version: '2.2.11'
latestDepTestImplementation group: 'org.apache.myfaces.core', name: 'myfaces-impl', version: '2+'
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.myfaces;
import static io.opentelemetry.javaagent.instrumentation.myfaces.MyFacesTracer.tracer;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import javax.faces.event.ActionEvent;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class ActionListenerImplInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("org.apache.myfaces.application.ActionListenerImpl");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("processAction"),
ActionListenerImplInstrumentation.class.getName() + "$ProcessActionAdvice");
}
public static class ProcessActionAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Argument(0) ActionEvent event,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope) {
span = tracer().startSpan(event);
if (span != null) {
scope = tracer().startScope(span);
}
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stopSpan(
@Advice.Thrown Throwable throwable,
@Advice.Local("otelSpan") Span span,
@Advice.Local("otelScope") Scope scope) {
if (scope != null) {
scope.close();
if (throwable != null) {
tracer().endExceptionally(span, throwable);
} else {
tracer().end(span);
}
}
}
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.myfaces;
import static java.util.Arrays.asList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.tooling.InstrumentationModule;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.List;
@AutoService(InstrumentationModule.class)
public class MyFacesInstrumentationModule extends InstrumentationModule {
public MyFacesInstrumentationModule() {
super("myfaces", "myfaces-1.2");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new ActionListenerImplInstrumentation(), new RestoreViewExecutorInstrumentation());
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.myfaces;
import io.opentelemetry.instrumentation.jsf.JsfTracer;
import javax.el.ELException;
public class MyFacesTracer extends JsfTracer {
private static final MyFacesTracer TRACER = new MyFacesTracer();
public static MyFacesTracer tracer() {
return TRACER;
}
@Override
protected Throwable unwrapThrowable(Throwable throwable) {
throwable = super.unwrapThrowable(throwable);
while (throwable instanceof ELException) {
throwable = throwable.getCause();
}
return throwable;
}
@Override
protected String getInstrumentationName() {
return "io.opentelemetry.javaagent.myfaces";
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.myfaces;
import static io.opentelemetry.javaagent.instrumentation.myfaces.MyFacesTracer.tracer;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.Map;
import javax.faces.context.FacesContext;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class RestoreViewExecutorInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return named("org.apache.myfaces.lifecycle.RestoreViewExecutor");
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
named("execute").and(takesArgument(0, named("javax.faces.context.FacesContext"))),
RestoreViewExecutorInstrumentation.class.getName() + "$ExecuteAdvice");
}
public static class ExecuteAdvice {
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Argument(0) FacesContext facesContext) {
tracer().updateServerSpanName(Java8BytecodeBridge.currentContext(), facesContext);
}
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
class Myfaces12Test extends BaseJsfTest {
@Override
String getJsfVersion() {
"1.2"
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-fragment version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd">
<!-- without this response status is 200 even when there is an exception -->
<context-param>
<param-name>org.apache.myfaces.ERROR_HANDLING</param-name>
<param-value>false</param-value>
</context-param>
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
</web-fragment>

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
class Myfaces2LatestTest extends BaseJsfTest {
@Override
String getJsfVersion() {
"2"
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-fragment version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd">
<!-- need to explicitly add ServletContextInitializer as myfaces jars are not inside webapp -->
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
</web-fragment>

View File

@ -0,0 +1,11 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
class Myfaces2Test extends BaseJsfTest {
@Override
String getJsfVersion() {
"2"
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-fragment version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-fragment_3_0.xsd">
<!-- need to explicitly add ServletContextInitializer as myfaces jars are not inside webapp -->
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
</web-fragment>

View File

@ -23,12 +23,12 @@ public class ServletAndFilterInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("javax.servlet.http.HttpServlet");
return hasClassesNamed("javax.servlet.Servlet");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return safeHasSuperType(namedOneOf("javax.servlet.Filter", "javax.servlet.http.HttpServlet"));
return safeHasSuperType(namedOneOf("javax.servlet.Filter", "javax.servlet.Servlet"));
}
@Override

View File

@ -27,7 +27,7 @@ public class ServletAndFilterInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return safeHasSuperType(namedOneOf("javax.servlet.Filter", "javax.servlet.http.HttpServlet"));
return safeHasSuperType(namedOneOf("javax.servlet.Filter", "javax.servlet.Servlet"));
}
@Override

View File

@ -143,7 +143,8 @@ public class GlobalIgnoresMatcher<T extends TypeDescription>
if (name.startsWith("com.sun.")) {
if (name.startsWith("com.sun.messaging.")
|| name.startsWith("com.sun.jersey.api.client")
|| name.startsWith("com.sun.appserv")) {
|| name.startsWith("com.sun.appserv")
|| name.startsWith("com.sun.faces")) {
return false;
}

View File

@ -124,6 +124,10 @@ include ':instrumentation:jedis:jedis-1.4:javaagent'
include ':instrumentation:jedis:jedis-3.0:javaagent'
include ':instrumentation:jetty-8.0:javaagent'
include ':instrumentation:jms-1.1:javaagent'
include ':instrumentation:jsf:jsf-common:library'
include ':instrumentation:jsf:jsf-testing-common'
include ':instrumentation:jsf:mojarra-1.2:javaagent'
include ':instrumentation:jsf:myfaces-1.2:javaagent'
include ':instrumentation:jsp-2.3:javaagent'
include ':instrumentation:kafka-clients-0.11:javaagent'
include ':instrumentation:kafka-streams-0.11:javaagent'

View File

@ -38,8 +38,6 @@ import spock.lang.Specification;
@SpecMetadata(filename = "AgentTestRunner.java", line = 0)
public abstract class AgentTestRunner extends Specification {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(AgentTestRunner.class);
static {
// always run with the thread propagation debugger to help track down sporadic test failures
System.setProperty("otel.threadPropagationDebugger", "true");
@ -102,31 +100,6 @@ public abstract class AgentTestRunner extends Specification {
AgentTestingExporterAccess.reset();
}
/**
* This is used by setupSpec() methods to auto-retry setup that depends on finding and then using
* an available free port, because that kind of setup can fail sporadically if the available port
* gets re-used between when we find the available port and when we use it.
*
* @param closure the groovy closure to run with retry
*/
public static void withRetryOnAddressAlreadyInUse(Closure<?> closure) {
withRetryOnAddressAlreadyInUse(closure, 3);
}
private static void withRetryOnAddressAlreadyInUse(Closure<?> closure, int numRetries) {
try {
closure.call();
} catch (Throwable t) {
// typically this is "java.net.BindException: Address already in use", but also can be
// "io.netty.channel.unix.Errors$NativeIoException: bind() failed: Address already in use"
if (numRetries == 0 || !t.getMessage().contains("Address already in use")) {
throw t;
}
log.debug("retrying due to bind exception: {}", t.getMessage(), t);
withRetryOnAddressAlreadyInUse(closure, numRetries - 1);
}
}
@AfterClass
public static synchronized void agentCleanup() {
// Cleanup before assertion.

View File

@ -0,0 +1,41 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* A trait for retrying operation when it fails with "java.net.BindException: Address already in use"
*/
trait RetryOnAddressAlreadyInUseTrait {
private static final Logger log = LoggerFactory.getLogger(RetryOnAddressAlreadyInUseTrait)
/**
* This is used by setupSpec() methods to auto-retry setup that depends on finding and then using
* an available free port, because that kind of setup can fail sporadically if the available port
* gets re-used between when we find the available port and when we use it.
*
* @param closure the groovy closure to run with retry
*/
static void withRetryOnAddressAlreadyInUse(Closure<?> closure) {
withRetryOnAddressAlreadyInUse(closure, 3)
}
static void withRetryOnAddressAlreadyInUse(Closure<?> closure, int numRetries) {
try {
closure.call()
} catch (Throwable t) {
// typically this is "java.net.BindException: Address already in use", but also can be
// "io.netty.channel.unix.Errors$NativeIoException: bind() failed: Address already in use"
if (numRetries == 0 || !t.getMessage().contains("Address already in use")) {
throw t
}
log.debug("retrying due to bind exception: {}", t.getMessage(), t)
withRetryOnAddressAlreadyInUse(closure, numRetries - 1)
}
}
}

View File

@ -15,74 +15,20 @@ import static io.opentelemetry.instrumentation.test.base.HttpServerTest.ServerEn
import static io.opentelemetry.instrumentation.test.utils.TraceUtils.runUnderTrace
import static org.junit.Assume.assumeTrue
import ch.qos.logback.classic.Level
import io.opentelemetry.api.trace.Span
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
import io.opentelemetry.instrumentation.test.AgentTestRunner
import io.opentelemetry.instrumentation.test.asserts.TraceAssert
import io.opentelemetry.instrumentation.test.utils.OkHttpUtils
import io.opentelemetry.instrumentation.test.utils.PortUtils
import io.opentelemetry.sdk.trace.data.SpanData
import java.util.concurrent.Callable
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import spock.lang.Shared
import spock.lang.Unroll
@Unroll
abstract class HttpServerTest<SERVER> extends AgentTestRunner {
public static final Logger SERVER_LOGGER = LoggerFactory.getLogger("http-server")
static {
((ch.qos.logback.classic.Logger) SERVER_LOGGER).setLevel(Level.DEBUG)
}
protected static final String TEST_CLIENT_IP = "1.1.1.1"
protected static final String TEST_USER_AGENT = "test-user-agent"
@Shared
SERVER server
@Shared
OkHttpClient client = OkHttpUtils.client()
@Shared
int port
@Shared
URI address
def setupSpec() {
withRetryOnAddressAlreadyInUse({
setupSpecUnderRetry()
})
}
def setupSpecUnderRetry() {
port = PortUtils.randomOpenPort()
address = buildAddress()
server = startServer(port)
println getClass().name + " http server started at: http://localhost:$port" + getContextPath()
}
URI buildAddress() {
return new URI("http://localhost:$port" + getContextPath() + "/")
}
abstract SERVER startServer(int port)
def cleanupSpec() {
if (server == null) {
println getClass().name + " can't stop null server"
return
}
stopServer(server)
server = null
println getClass().name + " http server stopped at: http://localhost:$port/"
}
abstract void stopServer(SERVER server)
abstract class HttpServerTest<SERVER> extends AgentTestRunner implements HttpServerTestTrait<SERVER> {
String expectedServerSpanName(ServerEndpoint endpoint) {
return endpoint == PATH_PARAM ? getContextPath() + "/path/:id/param" : endpoint.resolvePath(address).path

View File

@ -0,0 +1,71 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.test.base
import ch.qos.logback.classic.Level
import io.opentelemetry.instrumentation.test.RetryOnAddressAlreadyInUseTrait
import io.opentelemetry.instrumentation.test.utils.OkHttpUtils
import io.opentelemetry.instrumentation.test.utils.PortUtils
import okhttp3.OkHttpClient
import org.junit.AfterClass
import org.junit.BeforeClass
import org.slf4j.Logger
import org.slf4j.LoggerFactory
/**
* A trait for testing requests against http server.
*/
trait HttpServerTestTrait<SERVER> implements RetryOnAddressAlreadyInUseTrait {
static final Logger SERVER_LOGGER = LoggerFactory.getLogger("http-server")
static {
((ch.qos.logback.classic.Logger) SERVER_LOGGER).setLevel(Level.DEBUG)
}
static final String TEST_CLIENT_IP = "1.1.1.1"
static final String TEST_USER_AGENT = "test-user-agent"
// not using SERVER as type because it triggers a bug in groovy and java joint compilation
static Object server
static OkHttpClient client = OkHttpUtils.client()
static int port
static URI address
@BeforeClass
def setupServer() {
withRetryOnAddressAlreadyInUse({
setupSpecUnderRetry()
})
}
def setupSpecUnderRetry() {
port = PortUtils.randomOpenPort()
address = buildAddress()
server = startServer(port)
println getClass().name + " http server started at: http://localhost:$port" + getContextPath()
}
URI buildAddress() {
return new URI("http://localhost:$port" + getContextPath() + "/")
}
abstract SERVER startServer(int port)
@AfterClass
def cleanupServer() {
if (server == null) {
println getClass().name + " can't stop null server"
return
}
stopServer(server)
server = null
println getClass().name + " http server stopped at: http://localhost:$port/"
}
abstract void stopServer(SERVER server)
String getContextPath() {
return ""
}
}