-
Notifications
You must be signed in to change notification settings - Fork 336
Add instrumentation to detect the route at the beginning of the spring request #1360
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,23 +5,31 @@ | |
| import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan; | ||
| import static datadog.trace.instrumentation.springweb.SpringWebHttpServerDecorator.DECORATE; | ||
| import static datadog.trace.instrumentation.springweb.SpringWebHttpServerDecorator.DECORATE_RENDER; | ||
| import static java.util.Collections.singletonMap; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isMethod; | ||
| import static net.bytebuddy.matcher.ElementMatchers.isProtected; | ||
| import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; | ||
| import static net.bytebuddy.matcher.ElementMatchers.named; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArgument; | ||
| import static net.bytebuddy.matcher.ElementMatchers.takesArguments; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.bootstrap.ContextStore; | ||
| import datadog.trace.bootstrap.InstrumentationContext; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentScope; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import javax.servlet.ServletContext; | ||
| import net.bytebuddy.asm.Advice; | ||
| import net.bytebuddy.description.method.MethodDescription; | ||
| import net.bytebuddy.description.type.TypeDescription; | ||
| import net.bytebuddy.matcher.ElementMatcher; | ||
| import org.springframework.web.method.HandlerMethod; | ||
| import org.springframework.context.ApplicationContext; | ||
| import org.springframework.web.servlet.DispatcherServlet; | ||
| import org.springframework.web.servlet.HandlerMapping; | ||
| import org.springframework.web.servlet.ModelAndView; | ||
|
|
||
| @AutoService(Instrumenter.class) | ||
|
|
@@ -36,23 +44,38 @@ public ElementMatcher<TypeDescription> typeMatcher() { | |
| return named("org.springframework.web.servlet.DispatcherServlet"); | ||
| } | ||
|
|
||
| @Override | ||
| public Map<String, String> contextStore() { | ||
| return singletonMap( | ||
| "org.springframework.web.servlet.DispatcherServlet", | ||
| packageName + ".HandlerMappingResourceNameFilter"); | ||
| } | ||
|
|
||
| @Override | ||
| public String[] helperClassNames() { | ||
| return new String[] { | ||
| packageName + ".SpringWebHttpServerDecorator", | ||
| packageName + ".SpringWebHttpServerDecorator$1", | ||
| packageName + ".HandlerMappingResourceNameFilter", | ||
| }; | ||
| } | ||
|
|
||
| @Override | ||
| public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() { | ||
| final Map<ElementMatcher<? super MethodDescription>, String> transformers = new HashMap<>(); | ||
| transformers.put( | ||
| isMethod() | ||
| .and(isProtected()) | ||
| .and(named("onRefresh")) | ||
| .and(takesArgument(0, named("org.springframework.context.ApplicationContext"))) | ||
| .and(takesArguments(1)), | ||
| DispatcherServletInstrumentation.class.getName() + "$HandlerMappingAdvice"); | ||
| transformers.put( | ||
| isMethod() | ||
| .and(isProtected()) | ||
| .and(named("render")) | ||
| .and(takesArgument(0, named("org.springframework.web.servlet.ModelAndView"))), | ||
| DispatcherServletInstrumentation.class.getName() + "$DispatcherAdvice"); | ||
| DispatcherServletInstrumentation.class.getName() + "$RenderAdvice"); | ||
| transformers.put( | ||
| isMethod() | ||
| .and(isProtected()) | ||
|
|
@@ -62,7 +85,37 @@ public Map<? extends ElementMatcher<? super MethodDescription>, String> transfor | |
| return transformers; | ||
| } | ||
|
|
||
| public static class DispatcherAdvice { | ||
| /** | ||
| * This advice creates a filter that has reference to the handlerMappings from DispatcherServlet | ||
| * which allows the mappings to be evaluated at the beginning of the filter chain. This evaluation | ||
| * is done inside the Servlet3Decorator.onContext method. | ||
| */ | ||
| public static class HandlerMappingAdvice { | ||
|
|
||
| @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) | ||
| public static void afterRefresh( | ||
| @Advice.This final DispatcherServlet dispatcher, | ||
| @Advice.Argument(0) final ApplicationContext springCtx, | ||
| @Advice.FieldValue("handlerMappings") final List<HandlerMapping> handlerMappings, | ||
| @Advice.Thrown final Throwable throwable) { | ||
| final ServletContext servletContext = springCtx.getBean(ServletContext.class); | ||
| if (handlerMappings != null && servletContext != null) { | ||
| final ContextStore<DispatcherServlet, HandlerMappingResourceNameFilter> contextStore = | ||
| InstrumentationContext.get( | ||
| DispatcherServlet.class, HandlerMappingResourceNameFilter.class); | ||
| HandlerMappingResourceNameFilter filter = contextStore.get(dispatcher); | ||
| if (filter == null) { | ||
| filter = new HandlerMappingResourceNameFilter(); | ||
| contextStore.put(dispatcher, filter); | ||
| } | ||
| filter.setHandlerMappings(handlerMappings); | ||
| servletContext.setAttribute( | ||
| "dd.dispatcher-filter", filter); // used by Servlet3Decorator.onContext | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static class RenderAdvice { | ||
|
|
||
| @Advice.OnMethodEnter(suppress = Throwable.class) | ||
| public static AgentScope onEnter(@Advice.Argument(0) final ModelAndView mv) { | ||
|
|
@@ -79,11 +132,6 @@ public static void stopSpan( | |
| DECORATE_RENDER.beforeFinish(scope); | ||
| scope.close(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realize this didn't change in this PR, but I'll ask anyway. I'd expect scope.close to be inside of a finally block, but usually, it is not. Maybe, the first two lines don't typically raise exceptions; however, we might not know because of the suppress=Throwable. Basically, I'm concerned that this code is not obviously correct. From looking around, this seems to be a general problem with our resource handling.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's true that exceptions thrown from our decorators could cause problems in many places... That should probably be addressed as a separate issue. |
||
| } | ||
|
|
||
| // Make this advice match consistently with HandlerAdapterInstrumentation | ||
| private void muzzleCheck(final HandlerMethod method) { | ||
| method.getMethod(); | ||
| } | ||
| } | ||
|
|
||
| public static class ErrorHandlerAdvice { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package datadog.trace.instrumentation.springweb; | ||
|
|
||
| import static datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator.DD_SPAN_ATTRIBUTE; | ||
| import static datadog.trace.instrumentation.springweb.SpringWebHttpServerDecorator.DECORATE; | ||
|
|
||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import java.util.List; | ||
| import javax.servlet.Filter; | ||
| import javax.servlet.FilterChain; | ||
| import javax.servlet.FilterConfig; | ||
| import javax.servlet.ServletRequest; | ||
| import javax.servlet.ServletResponse; | ||
| import javax.servlet.http.HttpServletRequest; | ||
| import org.springframework.web.servlet.HandlerExecutionChain; | ||
| import org.springframework.web.servlet.HandlerMapping; | ||
|
|
||
| public class HandlerMappingResourceNameFilter implements Filter { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this doesn't have to implement Filter since it's not being used as one, but since we don't have access to the functional interfaces there doesn't really seem to be a better type in my mind
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that was my thought also... If there's a better generic interface that takes a single argument I'd be happy to use that instead. |
||
| private volatile List<HandlerMapping> handlerMappings; | ||
|
|
||
| @Override | ||
| public void init(final FilterConfig filterConfig) {} | ||
|
|
||
| @Override | ||
| public void doFilter( | ||
| final ServletRequest servletRequest, | ||
| final ServletResponse servletResponse, | ||
| final FilterChain filterChain) { | ||
| if (servletRequest instanceof HttpServletRequest && handlerMappings != null) { | ||
| final HttpServletRequest request = (HttpServletRequest) servletRequest; | ||
| try { | ||
| if (findMapping(request)) { | ||
| // Name the parent span based on the matching pattern | ||
| final Object parentSpan = request.getAttribute(DD_SPAN_ATTRIBUTE); | ||
| if (parentSpan instanceof AgentSpan) { | ||
| // Let the parent span resource name be set with the attribute set in findMapping. | ||
| DECORATE.onRequest((AgentSpan) parentSpan, request); | ||
| } | ||
| } | ||
| } catch (final Exception e) { | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void destroy() {} | ||
|
|
||
| /** | ||
| * When a HandlerMapping matches a request, it sets HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE | ||
| * as an attribute on the request. This attribute is read by | ||
| * SpringWebHttpServerDecorator.onRequest and set as the resource name. | ||
| */ | ||
| private boolean findMapping(final HttpServletRequest request) throws Exception { | ||
| for (final HandlerMapping mapping : handlerMappings) { | ||
| final HandlerExecutionChain handler = mapping.getHandler(request); | ||
| if (handler != null) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| public void setHandlerMappings(final List<HandlerMapping> handlerMappings) { | ||
| this.handlerMappings = handlerMappings; | ||
| } | ||
| } | ||
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.
Do we want log here? Add a health metric?
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 added a log statement, even though the exception is never thrown. If we had a better shared interface we could avoid the try/catch (
filterdoesn't actually need to implement theFilterinterface).