Skip to content

Commit

Permalink
Add instrumentation for jaxws metro 3.0+
Browse files Browse the repository at this point in the history
Previously, only jaxws metro 2.2 was instrumented, which only worked with Java EE (javax namespace).
Now, jaxws metro 3.0+ is also instrumented, which works with Jakarta EE (jakarta namespace).

Rather than copy/pasting the metro 2.2 instrumentation implementation to a new metro 3.0 module,
I made the existing module able to work with both Java EE and Jakarta EE classes.

More specifically, I
* moved the instrumentation implementation from `jaxws-2.0-metro-2.2` to `jaxws-metro-2.2` since "jaxws-2.0" is specific to Java EE.
  * Renamed `MetroServerSpanNaming` to `MetroServerSpanNameUpdater`, and made it work with both Java EE and Jakarta EE based on what is available on the runtime classpath.
* moved the Java EE specific tests from `jaxws-2.0-metro-2.2` to `jaxws-2.0-metro-2.2-testing`
* added new Jakarta EE specific tests in `jaxws-3.0-metro-2.2-testing`.
  This is basically a copy/paste of the tests from `jaxws-2.0-metro-2.2-testing`, but using Jakarta EE namespacing.
* added the `jaxws-3.0-common-testing` module for reusable Jakarta EE tests for other future jaxws instrumentation implementations.
  This is basically a copy/paste of `jaxws-2.0-common-testing`, but using Jakarta EE namespacing.

Regarding the implementation of `MetroServerSpanNameUpdater`, I originally added compile time dependencies on both `jakarta.servlet-api` and `javax.servlet-api`, and referenced both of their classes directly in the code, although guarded to make sure things would still work if they weren't on the runtime classpath.  Unfortunately, muzzle would disable the instrumentation if either was not found at runtime.  Therefore, I had to refactor the code a bit to only use reflection to access the classes.  This way muzzle won't disable the instrumentation if either is not found.

This is related to open-telemetry#9569, but specific to the metro runtime, rather than at the annotated `@WebService` level.
  • Loading branch information
philsttr committed Oct 18, 2023
1 parent 9cb1574 commit b1b9ffd
Show file tree
Hide file tree
Showing 26 changed files with 571 additions and 72 deletions.
2 changes: 1 addition & 1 deletion docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ These are the supported libraries and frameworks:
| [Eclipse Grizzly](https://javaee.github.io/grizzly/httpserverframework.html) | 2.3+ | N/A | [HTTP Server Spans], [HTTP Server Metrics] |
| [Eclipse Jersey](https://eclipse-ee4j.github.io/jersey/) | 2.0+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Eclipse Jetty HTTP Client](https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/client/HttpClient.html) | 9.2+ (not including 10+ yet) | [opentelemetry-jetty-httpclient-9.2](../instrumentation/jetty-httpclient/jetty-httpclient-9.2/library) | [HTTP Client Spans], [HTTP Client Metrics] |
| [Eclipse Metro](https://projects.eclipse.org/projects/ee4j.metro) | 2.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Eclipse Metro](https://projects.eclipse.org/projects/ee4j.metro) | 2.2+ | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Eclipse Mojarra](https://projects.eclipse.org/projects/ee4j.mojarra) | 1.2+ (not including 3.x yet) | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Elasticsearch API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) | 7.16+ and 8.0+ | N/A | [Elasticsearch Client Spans] |
| [Elasticsearch REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html) | 5.0+ | N/A | [Database Client Spans] |
Expand Down
28 changes: 28 additions & 0 deletions instrumentation/jaxws/jaxws-2.0-metro-2.2-testing/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
id("otel.javaagent-testing")
}

dependencies {
testImplementation("javax.servlet:javax.servlet-api:3.0.1")
testImplementation("com.sun.xml.ws:jaxws-rt:2.3.6")

testImplementation(project(":instrumentation:jaxws:jaxws-2.0-common-testing"))

testInstrumentation(project(":instrumentation:jaxws:jaxws-metro-2.2:javaagent"))
testInstrumentation(project(":instrumentation:jaxws:jaxws-2.0:javaagent"))
testInstrumentation(project(":instrumentation:jaxws:jaxws-jws-api-1.1:javaagent"))

testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent"))
testInstrumentation(project(":instrumentation:jetty:jetty-8.0:javaagent"))

latestDepTestLibrary("com.sun.xml.ws:jaxws-rt:2.+")
latestDepTestLibrary("com.sun.xml.stream.buffer:streambuffer:1.+")
}

tasks.withType<Test>().configureEach {
// required on jdk17
jvmArgs("--add-exports=java.xml/com.sun.org.apache.xerces.internal.dom=ALL-UNNAMED")
jvmArgs("--add-exports=java.xml/com.sun.org.apache.xerces.internal.jaxp=ALL-UNNAMED")
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
}

This file was deleted.

28 changes: 28 additions & 0 deletions instrumentation/jaxws/jaxws-3.0-common-testing/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
id("org.unbroken-dome.xjc")
id("otel.java-conventions")
}

tasks {
named<Checkstyle>("checkstyleMain") {
// exclude generated web service classes
exclude("**/hello_web_service/**")
}
}

dependencies {
api("jakarta.xml.ws:jakarta.xml.ws-api:3.0.0")
api("jakarta.jws:jakarta.jws-api:3.0.0")

api("ch.qos.logback:logback-classic")
api("org.slf4j:log4j-over-slf4j")
api("org.slf4j:jcl-over-slf4j")
api("org.slf4j:jul-to-slf4j")
api("org.eclipse.jetty:jetty-webapp:11.0.17")
api("org.springframework.ws:spring-ws-core:4.0.0")

implementation(project(":testing-common"))

xjcTool("com.sun.xml.bind:jaxb-xjc:3.0.2")
xjcTool("com.sun.xml.bind:jaxb-impl:3.0.2")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

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.SemanticAttributes
import io.opentelemetry.test.hello_web_service.Hello2Request
import io.opentelemetry.test.hello_web_service.HelloRequest
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.util.resource.Resource
import org.eclipse.jetty.webapp.WebAppContext
import org.springframework.oxm.jaxb.Jaxb2Marshaller
import org.springframework.util.ClassUtils
import org.springframework.ws.client.core.WebServiceTemplate
import org.springframework.ws.soap.client.SoapFaultClientException
import spock.lang.Shared
import spock.lang.Unroll

import static io.opentelemetry.api.trace.SpanKind.INTERNAL
import static io.opentelemetry.api.trace.SpanKind.SERVER
import static io.opentelemetry.api.trace.StatusCode.ERROR

abstract class AbstractJaxWsTest extends AgentInstrumentationSpecification implements HttpServerTestTrait<Server> {

@Shared
private Jaxb2Marshaller marshaller = new Jaxb2Marshaller()

@Shared
protected WebServiceTemplate webServiceTemplate = new WebServiceTemplate(marshaller)

def setupSpec() {
setupServer()

marshaller.setPackagesToScan(ClassUtils.getPackageName(HelloRequest))
marshaller.afterPropertiesSet()
}

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"))
webAppContext.getMetaData().addWebInfResource(Resource.newClassPathResource("/"))

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

String getServiceAddress(String serviceName) {
return address.resolve("ws/" + serviceName).toString()
}

def makeRequest(methodName, name) {
Object request = null
if ("hello" == methodName) {
request = new HelloRequest(name: name)
} else if ("hello2" == methodName) {
request = new Hello2Request(name: name)
} else {
throw new IllegalArgumentException(methodName)
}

return webServiceTemplate.marshalSendAndReceive(getServiceAddress("HelloService"), request)
}

@Unroll
def "test #methodName"() {
setup:
def response = makeRequest(methodName, "Test")

expect:
response.getMessage() == "Hello Test"

and:
def spanCount = 2
assertTraces(1) {
trace(0, spanCount) {
serverSpan(it, 0, serverSpanName(methodName))
handlerSpan(it, 1, methodName, span(0))
}
}

where:
methodName << ["hello", "hello2"]
}

@Unroll
def "test #methodName exception"() {
when:
makeRequest(methodName, "exception")

then:
def error = thrown(SoapFaultClientException)
error.getMessage() == "hello exception"

and:
def spanCount = 2
def expectedException = new Exception("hello exception")
assertTraces(1) {
trace(0, spanCount) {
serverSpan(it, 0, serverSpanName(methodName), expectedException)
handlerSpan(it, 1, methodName, span(0), expectedException)
}
}

where:
methodName << ["hello", "hello2"]
}

def serverSpanName(String operation) {
return getContextPath() + "/ws/HelloService/" + operation
}

static serverSpan(TraceAssert trace, int index, String operation, Throwable exception = null) {
trace.span(index) {
hasNoParent()
name operation
kind SERVER
if (exception != null) {
status ERROR
}
}
}

static handlerSpan(TraceAssert trace, int index, String operation, Object parentSpan = null, Throwable exception = null) {
trace.span(index) {
if (parentSpan == null) {
hasNoParent()
} else {
childOf((SpanData) parentSpan)
}
name "HelloService/" + operation
kind INTERNAL
if (exception) {
status ERROR
errorEvent(exception.class, exception.message)
}
}
}

static annotationHandlerSpan(TraceAssert trace, int index, String methodName, Object parentSpan = null, Throwable exception = null) {
trace.span(index) {
if (parentSpan == null) {
hasNoParent()
} else {
childOf((SpanData) parentSpan)
}
name "HelloServiceImpl." + methodName
kind INTERNAL
if (exception) {
status ERROR
errorEvent(exception.class, exception.message)
}
attributes {
"$SemanticAttributes.CODE_NAMESPACE" "hello.HelloServiceImpl"
"$SemanticAttributes.CODE_FUNCTION" methodName
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package hello

class BaseHelloService {

String hello2(String name) {
if ("exception" == name) {
throw new Exception("hello exception")
}
return "Hello " + name
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package hello

import jakarta.jws.WebParam
import jakarta.jws.WebResult
import jakarta.jws.WebService
import jakarta.xml.ws.RequestWrapper

@WebService(targetNamespace = "http://opentelemetry.io/test/hello-web-service")
interface HelloService {

@RequestWrapper(localName = "helloRequest")
@WebResult(name = "message")
String hello(@WebParam(name = "name") String name)

@RequestWrapper(localName = "hello2Request")
@WebResult(name = "message")
String hello2(@WebParam(name = "name") String name)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package hello

import jakarta.jws.WebService

@WebService(serviceName = "HelloService", endpointInterface = "hello.HelloService", targetNamespace = "http://opentelemetry.io/test/hello-web-service")
class HelloServiceImpl extends BaseHelloService implements HelloService {

String hello(String name) {
if ("exception" == name) {
throw new Exception("hello exception")
}
return "Hello " + name
}
}
Loading

0 comments on commit b1b9ffd

Please sign in to comment.