Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Jakarta and Javax instrumentations #3989

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open

Conversation

marcingrzejszczak
Copy link
Contributor

@marcingrzejszczak marcingrzejszczak commented Jul 20, 2023

to have that working we've added additional convention and contexts around javax and jakarta classes. Added tests for RestEasy that use the instrumentations. Also added ObservedValve that is an instrumentation for Tomcat (we currently support Tomcat up till version 8 - no jakarta imports).

cc @bclozel

@marcingrzejszczak marcingrzejszczak added the enhancement A general enhancement label Jul 20, 2023
@sonatype-lift

This comment was marked as outdated.

@marcingrzejszczak marcingrzejszczak marked this pull request as draft August 9, 2023 14:57
@marcingrzejszczak marcingrzejszczak changed the title Added ObservedValve for Tomcat Added Jakarta and Javax instrumentations Aug 10, 2023
@bclozel
Copy link
Contributor

bclozel commented Aug 17, 2023

Thanks Marcin for working on this!

I'm wondering about the instrumentation for the JAX RS client bits. This is using the ClientRequestFilter and ClientResponseFilter contracts for this. It seems that if the connection somehow fails, or if a filter fails, a javax.ws.rs.client.ResponseProcessingException is thrown directly and this exception does not go through the response filter. This means that observations would not be stopped in those cases and that the instrumentation would miss a bunch of cases: filter errors and I/O errors in general. This reminds me of the work we recently did on the HttpComponents client where we switched to a different approach. Is there a better way to instrument clients here. Maybe through the *Invoker contracts by wrapping? We would need to see some concrete usage with a client to see if that's feasible.

I'm also wondering about the same problems on the server side with the ContainerRequestFilter and ContainerResponseFilter contracts. There, it seems that any exception (thrown by a JAX-RS endpoint or a filter) will be handled by an ExceptionMapper. In this case we might not need a different instrumentation approach but we might need a consistent ordering of that filter in the chain. Should it be ordered so that all other filters had a chance to process the response so that we don't record the wrong information? How should that be enforced?

Now about the Tomcat Valve. I'm not super familiar with this contract, but I remember that the asyncSupported flag is off by default and that this can play a important part since requests might not be instrumented for the async I/O case. I think that for regular async support, you might not need to do something special for async dispatches (like in the Servlet filter case), but I'm not entirely sure and I think this should be checked.

In general, in the Spring Framework instrumentation I tried to provide a lot of flexibility with custom names for observations and custom conventions (by providing constructor variants), but I'm wondering if this doesn't clutter the API in the end. Maybe this would be better handled with other contracts like the observation filter? I think this really depends on the use case. Maybe here JAX RS implementations would like to have a choice to come up with their own custom name?

@marcingrzejszczak
Copy link
Contributor Author

I'm wondering about the instrumentation for the JAX RS client bits. This is using the ClientRequestFilter and ClientResponseFilter contracts for this. It seems that if the connection somehow fails, or if a filter fails, a javax.ws.rs.client.ResponseProcessingException is thrown directly and this exception does not go through the response filter. This means that observations would not be stopped in those cases and that the instrumentation would miss a bunch of cases: filter errors and I/O errors in general. This reminds me of the work we recently did on the HttpComponents client where we switched to a different approach. Is there a better way to instrument clients here. Maybe through the *Invoker contracts by wrapping? We would need to see some concrete usage with a client to see if that's feasible.
I'm also wondering about the same problems on the server side with the ContainerRequestFilter and ContainerResponseFilter contracts. There, it seems that any exception (thrown by a JAX-RS endpoint or a filter) will be handled by an ExceptionMapper. In this case we might not need a different instrumentation approach but we might need a consistent ordering of that filter in the chain. Should it be ordered so that all other filters had a chance to process the response so that we don't record the wrong information? How should that be enforced?

Yup I fully agree. I don't know if these are the right contracts to be used for proper wrapping. I think I'll need some help from people working with Jakarta API directly. I asked about it in the RestEASY tracker https://issues.redhat.com/browse/RESTEASY-3356?focusedId=22781362&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-22781362 and still am waiting for some feedback.

Now about the Tomcat Valve. I'm not super familiar with this contract, but I remember that the asyncSupported flag is off by default and that this can play a important part since requests might not be instrumented for the async I/O case. I think that for regular async support, you might not need to do something special for async dispatches (like in the Servlet filter case), but I'm not entirely sure and I think this should be checked.

We had it turned on for Sleuth for ages and it worked well 🤷

In general, in the Spring Framework instrumentation I tried to provide a lot of flexibility with custom names for observations and custom conventions (by providing constructor variants), but I'm wondering if this doesn't clutter the API in the end. Maybe this would be better handled with other contracts like the observation filter? I think this really depends on the use case. Maybe here JAX RS implementations would like to have a choice to come up with their own custom name?

I see. I guess it would be good for someone using JAX RS to chime in and give us feedback. Do you know anyone we can mention here? :)

@bclozel
Copy link
Contributor

bclozel commented Aug 17, 2023

Now about the Tomcat Valve. I'm not super familiar with this contract, but I remember that the asyncSupported flag is off by default and that this can play a important part since requests might not be instrumented for the async I/O case. I think that for regular async support, you might not need to do something special for async dispatches (like in the Servlet filter case), but I'm not entirely sure and I think this should be checked.

We had it turned on for Sleuth for ages and it worked well 🤷

My bad, I missed that it was calling setAsyncSupported(true) already in the constructor.

In general, in the Spring Framework instrumentation I tried to provide a lot of flexibility with custom names for observations and custom conventions (by providing constructor variants), but I'm wondering if this doesn't clutter the API in the end. Maybe this would be better handled with other contracts like the observation filter? I think this really depends on the use case. Maybe here JAX RS implementations would like to have a choice to come up with their own custom name?

I see. I guess it would be good for someone using JAX RS to chime in and give us feedback. Do you know anyone we can mention here? :)

Not really unfortunately. I think Helidon is integrating with tracing libraries directly. Maybe something there that we can take a look at?

@marcingrzejszczak
Copy link
Contributor Author

So I think we're using the proper APIs here. We just need to add support for error cases https://github.com/helidon-io/helidon/blob/helidon-3.x/tracing/jersey/src/main/java/io/helidon/tracing/jersey/AbstractTracingFilter.java#L125C9-L139

@marcingrzejszczak
Copy link
Contributor Author

config/checkstyle/checkstyle-suppressions.xml Outdated Show resolved Hide resolved
dependencies.gradle Outdated Show resolved Hide resolved
micrometer-core/build.gradle Outdated Show resolved Hide resolved
}

protected KeyValues getHighCardinalityKeyValues(String requestUri) {
return KeyValues.of(httpUrl(requestUri));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we add the user-agent too just like we did with the client?

micrometer-jakarta/build.gradle Outdated Show resolved Hide resolved
micrometer-core/build.gradle Outdated Show resolved Hide resolved
public String getContextualName(HttpJakartaClientRequestObservationContext context) {
String method = context.getCarrier() != null
? (context.getCarrier().getMethod() != null ? context.getCarrier().getMethod() : null) : null;
return getContextualName(method);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method might be null here, which would result in http null as the contextual name. Is that what we want?

* @author Marcin Grzejszczak
* @since 1.12.0
*/
public class HttpJakartaServerRequestObservationContext
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the client instrumentation, I think this should be named more specific to JAX-RS, since it requires JAX-RS classes. Perhaps JaxRsServerObservationContext, and other classes that use this should be similarly renamed. The JavaDoc should also be updated to not mention Servlet but JAX-RS instead. We should also guide users on instrumenting at the right level. If they are using a framework, they should use the instrumentation there. If they are not using a framework or it doesn't have Micrometer instrumentation, but they are using JAX-RS, they should instrument at the JAX-RS level. If they are not using JAX-RS but are using Servlet, they should instrument at the Servlet level.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the naming of all related classes. The JavaDocs still need an update.

micrometer-jakarta/build.gradle Outdated Show resolved Hide resolved
* Binds observation into Jersey.
*/
@ConstrainedTo(RuntimeType.CLIENT)
public class ObservationAutoDiscoverable implements AutoDiscoverable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with how libraries provide these AutoDiscoverable implementations, but the JavaDoc mentions declaring the implementation in a file in META-INF/services named org.glassfish.jersey.internal.spi.AutoDiscoverable. Otherwise I guess this won't be auto-discovered, but as I said, I'm not familiar with this.

Looking at the documentation, it says this is internal, so I wonder if we should be providing this implementation at all.

Auto discovery functionality is in Jersey supported by implementing an internal AutoDiscoverable Jersey SPI. This interface is not public at the moment, and is subject to change in the future, so be careful when trying to use it.

* @author Marcin Grzejszczak
* @since 1.12.0
*/
public class ObservationHttpJakartaInterceptor implements PostInvocationInterceptor {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be named more specifically to Jersey, since it implements a Jersey SPI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this would be a problem in an environment that is using a JAX-RS implementation not including Jersey. I don't know how much of a problem that is, but as far as I understand, there will be issues with the filter linked in the JavaDoc if there are exceptions and an interceptor like this isn't configured.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately, this is a blocker for providing JAX-RS client instrumentation. This was ran into by the OTel Java instrumentation as well in open-telemetry/opentelemetry-java-instrumentation#5430. There's more information about the limitation in JAX-RS itself in jakartaee/rest#684. So I don't know that we can provide JAX-RS client instrumentation without it being buggy. For now, we should not merge this code until/unless we can find an alternative solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcingrzejszczak The existing tests do not demonstrate the issue mentioned in the linked issues. Running the tests against a WireMock endpoint stubbed as follows demonstrates the issue:

wmRuntimeInfo.getWireMock().register(WireMock.get("/connectionReset").willReturn(WireMock.aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));

The observation will be started but never stopped in this case. The Jersey specific ObservationJerseyClientInterceptor doesn't have any effect in the ObservationJaxRsHttpClientFilterTests because RESTEasy is used. This demonstrates why I don't think we can offer JAX-RS client instrumentation: it leaks observations when there is no response from the server. Rather we can add Jersey client instrumentation to jersey-micrometer, where with the Jersey interceptor we can ensure no leaks. It's unfortunate we can't instrument at the JAX-RS level for the client side, but we don't want to put out instrumentation with a known leak.

Full test code and failure

Test method added to ObservationJaxRsHttpClientFilterTests

@Test
void clientFilterShouldWorkWithJakartaHttpClientForExceptionsWithoutResponse(WireMockRuntimeInfo wmRuntimeInfo) {
    wmRuntimeInfo.getWireMock().register(WireMock.get("/connectionReset").willReturn(WireMock.aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));

    try (Client client = ClientBuilder.newClient()) {
        client.register(new ObservationJaxRsHttpClientFilter(observationRegistry, null));
        final WebTarget target = client
            .target("http://localhost:" + wmRuntimeInfo.getHttpPort() + "/connectionReset");
        Exception exception = null;
        try (Response response = target.request().get()) {
            BDDAssertions.fail("Response should not be returned");
        } catch (Exception e) {
            exception = e;
        }
        BDDAssertions.then(exception).isNotNull().isInstanceOf(ProcessingException.class);
    }

    wmRuntimeInfo.getWireMock()
        .verifyThat(WireMock.getRequestedFor(WireMock.urlEqualTo("/connectionReset"))
            .withHeader("foo", WireMock.equalTo("bar")));

    TestObservationRegistryAssert.then(observationRegistry)
        .hasSingleObservationThat()
        .hasBeenStarted()
        .hasBeenStopped()
        .hasError();
}

Test outcome

java.lang.AssertionError: Observation is not stopped

	at io.micrometer.jakarta9.instrument.binder.http.jaxrs.client.ObservationJaxRsHttpClientFilterTests.clientFilterShouldWorkWithJakartaHttpClientForExceptionsWithoutResponses(ObservationJaxRsHttpClientFilterTests.java:112)
	at java.base/java.lang.reflect.Method.invoke(Method.java:578)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)

micrometer-jakarta/build.gradle Outdated Show resolved Hide resolved
micrometer-jakarta/build.gradle Outdated Show resolved Hide resolved
@@ -67,6 +67,8 @@ jar {
dependencies {
api project(":micrometer-commons")
api project(":micrometer-observation")
optionalApi 'javax.ws.rs:javax.ws.rs-api' // javax
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why this is needed now but it wasn't prior to this pull request. I get a compilation error in JerseyTags when I remove this, but that class in micrometer-core is unchanged.

BDDAssertions.then(response.getStatus()).isEqualTo(200);
BDDAssertions.then(response.getHeaders()).isNotEmpty();
List<Object> header = response.getHeaders().get("baz");
then(header).hasSize(1).containsOnly("bar");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no assertions on the observation produced by the instrumentation. I was looking to add an implementation of our HttpServerTimingInstrumentationVerificationTests for this instrumentation, but it looks like the uri tag always has the value UNKNOWN because setPathPattern is never called on JaxRsContainerObservationContext

to have that working we've added additional convention and contexts around javax and jakarta classes
modified the jakarta imports to have both jakarta and javax imports on the classpath. Added filters for client and server side that use jakarta imports and added RestEasy tests for client and server
created a separate micrometer-jakarta module and put all the latest deps there. That way we could instrument latest Tomcat, Jersey etc. without any classpath issues
marcingrzejszczak and others added 10 commits January 16, 2024 11:36
Remove unused dependencies, plugins, tasks, bundle imports.
Renames classes for the specific API/implementation they target for clarity. Re-organizes the classes into more specific packages to more easily identify related classes.
without this change JaxRs doesn't give an option to close the scope and stop the observation in a case of an exception
with this change we're adding a proxy around invocations. Users need to use that proxying mechanism manually until a better solution is provided by JaxRs
@mkjensen
Copy link

mkjensen commented Apr 11, 2024

Hi,

What is the status of this?

It looks pretty interesting to us. We have a product using Spring Boot 3.2 consisting of both legacy code using RESTEasy (via RESTEasy Spring Boot Starter) and newer code using Spring Web MVC.

It would be nice if we could easily get the same metrics for both, specifically the http.server.requests metric.

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Observed Tomcat Valve
5 participants