-
Notifications
You must be signed in to change notification settings - Fork 905
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
Fix ServletContextPath.prepend for app server spans #2089
Conversation
Context context = | ||
ctx.with(AppServerBridge.CONTEXT_KEY, new AppServerBridge(shouldRecordException)); | ||
// Add context for storing servlet context path. Servlet context path is updated from servlet | ||
// integration. | ||
if (context.get(ServletContextPath.CONTEXT_KEY) == null) { | ||
context = context.with(ServletContextPath.CONTEXT_KEY, new AtomicReference<>(null)); | ||
} | ||
|
||
return context; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of adding an AtomicReference so that we can set the contextPath
into the Context without having to create a new scope, would it work to modify the Servlet instrumentation so that it creates a new Scope with the contextPath
as needed in the case where normal Servlet span is suppressed due to upstream app server span?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could add it in a separate advice
public static class ServletContextPathAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void onEnter(
@Advice.Argument(0) ServletRequest request,
@Advice.Local("otelScope") Scope scope) {
if (!(request instanceof HttpServletRequest)) {
return;
}
int callDepth = CallDepthThreadLocalMap.incrementCallDepth(ServletContextPath.class);
if (callDepth > 0) {
return;
}
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String contextPath = httpServletRequest.getContextPath();
if (contextPath != null && !contextPath.isEmpty() && !contextPath.equals("/")) {
Context context = Java8BytecodeBridge.currentContext().with(ServletContextPath.CONTEXT_KEY, contextPath);
scope = context.makeCurrent();
}
}
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void stop(
@Advice.Local("otelScope") Scope scope) {
if (scope != null) {
CallDepthThreadLocalMap.reset(ServletContextPath.class);
scope.close();
}
}
}
and apply it to same methods as Servlet3Advice/Servlet2Advice. But now the question is where should I place this advice class. Easiest would be to copy paste it to both servlet2 & servlet3 but I doubt that you'll accept that. If I add it to servlet-common with a separate instrumentation module then tests that have testInstrumentation project(":instrumentation:servlet:servlet-3.0:javaagent")
wouldn't use it because servlet-3.0 doesn't depend on servlet-common. I could either make servlet-3.0 depend on servlet-common or add servlet-common to tests that use ServletContextPath.prepend
but that would create another issue. Dispatcher and response instrumentation in servlet-common create extra spans that would need handling in tests. Advice appreciated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally think that both servlet-2.2
and servlet-3.0
may depend on servlet-common
. And in any case we should use all 2 (or even all 3) of them in tests. To be sure that they interplay correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mother of dragons... Our tests are becoming something...
@open-telemetry/java-instrumentation-approvers Anybody else wants to chime in? |
@laurit can you summarize the changes in this PR? there's a lot of new stuff since I first reviewed it that don't seem related to the original description above. thanks! |
@trask sure.
|
@laurit would it make sense to re-use the existing instrumentation points (Servlet2Advice/Servlet3Advice), instead of creating a new instrumentation point (ServletContextPathAdvice) that needs to be applied? If we do keep the new instrumentation point, does it matter which order the instrumentation is run in (relative to Servlet2Advice/Servlet3Advice)? if so, we should probably use InstrumentationModule.getOrder() to make the ordering explicit |
@trask i believe ordering is not important, neither advice depends on the other. |
Thanks, this makes sense.
We do something surprising in the servlet advice, if it extracts the context from the request (e.g. no app server already extracts the context), then it uses Context.root(), so it won't inherit the contextPath if the ServletContextPathAdvice runs first. Lines 193 to 195 in 121a9bf
|
Btw, just noticed @iNikem also ran into this muzzle issue when adding dependency on another javaagent instrumentation: #2156 (comment), he had solution there to extract library instrumentation for the shared code |
9a11e4a
to
dc2a241
Compare
@trask Thank, now context path is added after servlet integration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey @laurit, sorry, thinking about this some more, from a performance perspective (since servlet is such a critical instrumentation), I think it's worth re-visiting if we can integrate this new instrumentation point into the existing Servlet2Advice/Servlet3Advice methods, so that we don't need to double the number of CallDepth
thread local checks (and especially because these CallDepth checks need to be done inside of every doFilter, so there can be many in the filter chain).
If you would prefer to revisit as a separate PR, I'm good merging this as-is.
I sent the beginning of a PR to your PR: laurit#2. Let me know if you see problems with that approach. |
@trask that should work. With this adding servlet-common dependency and changes to tests shouldn't be needed any more, should I keep them or get rid of them? |
I think keep the servlet-common instrumentation addition to the tests. We want more realistic tests anyways. |
…rom app server integration
…let2&3 depend on servlet-common so that it would be used in tests that depend on servlet3
@trask If it is Ok with you, can you approve and merge? |
d2e12f8
to
d03281e
Compare
@trask could you review this again |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks @laurit
do you know why our smoke tests were passing prior to this PR? it looks like they are validating span names start with the context path /app
mostly wondering if we are missing some smoke test coverage
if (serverSpan != null) { | ||
// Name the parent span based on the matching pattern | ||
tracer().onRequest(context, serverSpan, request); | ||
// Now create a span for handler/controller execution. | ||
span = tracer().startHandlerSpan(handler); | ||
scope = context.with(span).makeCurrent(); | ||
} | ||
// Now create a span for handler/controller execution. | ||
span = tracer().startHandlerSpan(handler); | ||
scope = context.with(span).makeCurrent(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this change related to this PR? if not, can you send as a separate PR? since it's a behavior change, just want it to be more visible for others to review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appserver tests only have a servlet, there the span name is set in servlet instrumentation and does not use ServletContextPath.prepend
. SpringBootSmokeTest
could possibly fail (din't really try), but it uses an empty context path so ServletContextPath.prepend
doesn't do anything there anyway.
Probably should have mentioned this change as it should be the only non test change that was needed and there is a good chance that this is not the correct way to handle this case. This is needed for spring mvc SpringBootBasedTest "test spans with auth error"
and a coupe of other spring mvc tests where the common theme is that these tests use an error page. This tests sets status to 401 Unauthorized
after which servlet code exits and closes server span, after that tomcat forwards request to error page with request dispatcher. This error page is also implemented with spring so it hits servlet integration again where it is handled in Servlet3HttpServerTracer.needsRescoping(attachedContext)
branch that restores the scope of the original request. Now when code reaches ControllerAdvice
BaseTracer.getCurrentServerSpan(context);
returns the already ended server span that was created in first servlet call and all is fine.
When servlet common instrumentation is enable then RequestDispatcherAdvice
sets up a new scope, this makes servlet integration not take the Servlet3HttpServerTracer.needsRescoping(attachedContext)
branch and eventually when the request reaches ControllerAdvice
BaseTracer.getCurrentServerSpan(context);
is null and creating handler span is skipped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reverted this changed and instead made springmvc tests run with tomcat instrumentation. With tomcat instrumentation RequestDispatcherAdvice
does not cause loosing server span. Also fixed peer port and peer ip reporting in TomcatTracer
. There is a slight side effect from using tomcat instrumentation, perviously showing error page did not update server span name as server span had already ended, now with tomcat instrumentation server span has not ended and error page updates server span name to /error
.
hey @laurit, I remembered a hacky workaround in
|
@trask removed jetty hack |
Had to add it back. Turns out servlet-3.0 depends on jetty and removing it breaks async tests. When jetty integration applies to all handlers then jetty tests under servlet-3.0 would not be testing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When jetty integration applies to all handlers then jetty tests under servlet-3.0 would not be testing Servlet3Advice but rather jetty integration (would that be ok?)
Ya, I think it will work out well, to have JettyServlet3Test
test the AppServerBridge path, and have TomcatServlet3Test
test the non-AppServerBridge path.
Might be better to address this in a separate pr.
Good idea 😅 👍
ServletContextPath.prepend doesn't work when server span is created from app server integration because it uses a context that is only set up from servlet integration. Make app server integrations create this context and update it from servlet integration similarly to how span name is update. Modify struts tests that already declare a dependency on jetty integration to actually use jetty integration to verify that ServletContextPath.prepend actually prepends context path on jetty.