Server span naming for servlet filters (#2887)

* Server span naming for servlet filters

* wildfly default servlet has empty mappings

* jetty11 requires java11

* try a differnt way to disable jetty11 tests on java8

* Update instrumentation/servlet/servlet-5.0/javaagent/src/test/groovy/TomcatServlet5FilterMappingTest.groovy

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>

* review fix

* rework to use InstrumentationContext

* remove debugging code

* move MappingResolver to avoid ClassCastException on wildfly

* Update instrumentation/servlet/servlet-3.0/javaagent/src/test/groovy/TomcatServlet3FilterMappingTest.groovy

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>

* review fixes

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
This commit is contained in:
Lauri Tulmin 2021-05-06 13:25:17 +03:00 committed by GitHub
parent c9a3793bf8
commit 357140c081
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1078 additions and 213 deletions

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.servlet;
package io.opentelemetry.instrumentation.api.servlet;
import java.util.ArrayList;
import java.util.Collection;
@ -12,7 +12,10 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MappingResolver {
/**
* Helper class for finding a mapping that matches current request from a collection of mappings.
*/
public final class MappingResolver {
private final Set<String> exactMatches;
private final List<WildcardMatcher> wildcardMatchers;
private final boolean hasDefault;

View File

@ -28,6 +28,9 @@ public final class ServerSpanNaming {
}
private volatile Source updatedBySource;
// Length of the currently set name. This is used when setting name from a servlet filter
// to pick the most descriptive (longest) name.
private volatile int nameLength;
private ServerSpanNaming(Source initialSource) {
this.updatedBySource = initialSource;
@ -58,15 +61,24 @@ public final class ServerSpanNaming {
}
return;
}
if (source.order > serverSpanNaming.updatedBySource.order) {
// special case for servlet filters, even when we have a name from previous filter see whether
// the new name is better and if so use it instead
boolean onlyIfBetterName =
!source.useFirst && source.order == serverSpanNaming.updatedBySource.order;
if (source.order > serverSpanNaming.updatedBySource.order || onlyIfBetterName) {
String name = serverSpanName.get();
if (name != null) {
if (name != null && (!onlyIfBetterName || serverSpanNaming.isBetterName(name))) {
serverSpan.updateName(name);
serverSpanNaming.updatedBySource = source;
serverSpanNaming.nameLength = name.length();
}
}
}
private boolean isBetterName(String name) {
return name.length() > nameLength;
}
// TODO (trask) migrate the one usage (ServletHttpServerTracer) to ServerSpanNaming.init() once we
// migrate to new Instrumenters (see
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/2814#discussion_r617351334
@ -82,13 +94,22 @@ public final class ServerSpanNaming {
public enum Source {
CONTAINER(1),
SERVLET(2),
CONTROLLER(3);
// for servlet filters we try to find the best name which isn't necessarily from the first
// filter that is called
FILTER(2, false),
SERVLET(3),
CONTROLLER(4);
private final int order;
private final boolean useFirst;
Source(int order) {
this(order, true);
}
Source(int order, boolean useFirst) {
this.order = order;
this.useFirst = useFirst;
}
}
}

View File

@ -10,9 +10,13 @@ import static io.opentelemetry.instrumentation.servlet.v3_0.Servlet3HttpServerTr
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.servlet.AppServerBridge;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterAdviceHelper;
import javax.servlet.Filter;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
@ -36,12 +40,24 @@ public class Servlet3Advice {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
boolean servlet = servletOrFilter instanceof Servlet;
MappingResolver mappingResolver;
if (servlet) {
mappingResolver =
InstrumentationContext.get(Servlet.class, MappingResolver.class)
.get((Servlet) servletOrFilter);
} else {
mappingResolver =
InstrumentationContext.get(Filter.class, MappingResolver.class)
.get((Filter) servletOrFilter);
}
Context attachedContext = tracer().getServerContext(httpServletRequest);
if (attachedContext != null) {
// We are inside nested servlet/filter/app-server span, don't create new span
if (tracer().needsRescoping(attachedContext)) {
attachedContext =
tracer().updateContext(attachedContext, servletOrFilter, httpServletRequest);
tracer().updateContext(attachedContext, httpServletRequest, mappingResolver, servlet);
scope = attachedContext.makeCurrent();
return;
}
@ -50,7 +66,7 @@ public class Servlet3Advice {
// instrumentation, if needed update span with info from current request.
Context currentContext = Java8BytecodeBridge.currentContext();
Context updatedContext =
tracer().updateContext(currentContext, servletOrFilter, httpServletRequest);
tracer().updateContext(currentContext, httpServletRequest, mappingResolver, servlet);
if (updatedContext != currentContext) {
// runOnceUnderAppServer updated context, need to re-scope
scope = updatedContext.makeCurrent();
@ -65,7 +81,7 @@ public class Servlet3Advice {
// In case it was created by app server integration we need to update it with info from
// current request.
Context updatedContext =
tracer().updateContext(currentContext, servletOrFilter, httpServletRequest);
tracer().updateContext(currentContext, httpServletRequest, mappingResolver, servlet);
if (currentContext != updatedContext) {
// updateContext updated context, need to re-scope
scope = updatedContext.makeCurrent();
@ -73,7 +89,7 @@ public class Servlet3Advice {
return;
}
context = tracer().startSpan(servletOrFilter, httpServletRequest);
context = tracer().startSpan(httpServletRequest, mappingResolver, servlet);
scope = context.makeCurrent();
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v3_0;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import net.bytebuddy.asm.Advice;
public class Servlet3FilterInitAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void filterInit(
@Advice.This Filter filter, @Advice.Argument(0) FilterConfig filterConfig) {
if (filterConfig == null) {
return;
}
InstrumentationContext.get(Filter.class, MappingResolver.class)
.putIfAbsent(filter, new Servlet3FilterMappingResolverFactory(filterConfig));
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v3_0;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.instrumentation.servlet.naming.ServletFilterMappingResolverFactory;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import java.util.Collection;
import javax.servlet.FilterConfig;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
public class Servlet3FilterMappingResolverFactory
extends ServletFilterMappingResolverFactory<FilterRegistration>
implements ContextStore.Factory<MappingResolver> {
private final FilterConfig filterConfig;
public Servlet3FilterMappingResolverFactory(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
@Override
protected FilterRegistration getFilterRegistration() {
String filterName = filterConfig.getFilterName();
ServletContext servletContext = filterConfig.getServletContext();
if (filterName == null || servletContext == null) {
return null;
}
return servletContext.getFilterRegistration(filterName);
}
@Override
protected Collection<String> getUrlPatternMappings(FilterRegistration filterRegistration) {
return filterRegistration.getUrlPatternMappings();
}
@Override
protected Collection<String> getServletNameMappings(FilterRegistration filterRegistration) {
return filterRegistration.getServletNameMappings();
}
@Override
protected Collection<String> getServletMappings(String servletName) {
ServletRegistration servletRegistration =
filterConfig.getServletContext().getServletRegistration(servletName);
if (servletRegistration == null) {
return null;
}
return servletRegistration.getMappings();
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v3_0;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import net.bytebuddy.asm.Advice;
public class Servlet3InitAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void servletInit(
@Advice.This Servlet servlet, @Advice.Argument(0) ServletConfig servletConfig) {
if (servletConfig == null) {
return;
}
InstrumentationContext.get(Servlet.class, MappingResolver.class)
.putIfAbsent(servlet, new Servlet3MappingResolverFactory(servletConfig));
}
}

View File

@ -26,7 +26,11 @@ public class Servlet3InstrumentationModule extends InstrumentationModule {
public List<TypeInstrumentation> typeInstrumentations() {
return asList(
new AsyncContextInstrumentation(BASE_PACKAGE, adviceClassName(".AsyncDispatchAdvice")),
new ServletAndFilterInstrumentation(BASE_PACKAGE, adviceClassName(".Servlet3Advice")));
new ServletAndFilterInstrumentation(
BASE_PACKAGE,
adviceClassName(".Servlet3Advice"),
adviceClassName(".Servlet3InitAdvice"),
adviceClassName(".Servlet3FilterInitAdvice")));
}
private static String adviceClassName(String suffix) {

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v3_0;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.instrumentation.servlet.naming.ServletMappingResolverFactory;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import java.util.Collection;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
public class Servlet3MappingResolverFactory extends ServletMappingResolverFactory
implements ContextStore.Factory<MappingResolver> {
private final ServletConfig servletConfig;
public Servlet3MappingResolverFactory(ServletConfig servletConfig) {
this.servletConfig = servletConfig;
}
public Collection<String> getMappings() {
String servletName = servletConfig.getServletName();
ServletContext servletContext = servletConfig.getServletContext();
if (servletName == null || servletContext == null) {
return null;
}
ServletRegistration servletRegistration = servletContext.getServletRegistration(servletName);
if (servletRegistration == null) {
return null;
}
return servletRegistration.getMappings();
}
}

View File

@ -19,7 +19,7 @@ import okhttp3.RequestBody
import okhttp3.Response
import spock.lang.Unroll
abstract class AbstractServletMappingTest<SERVER, CONTEXT> extends AgentInstrumentationSpecification implements HttpServerTestTrait<SERVER> {
abstract class AbstractServlet3MappingTest<SERVER, CONTEXT> extends AgentInstrumentationSpecification implements HttpServerTestTrait<SERVER> {
abstract void addServlet(CONTEXT context, String path, Class<Servlet> servlet)

View File

@ -11,7 +11,7 @@ import javax.servlet.http.HttpServletResponse
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.ServletContextHandler
class JettyServletMappingTest extends AbstractServletMappingTest<Server, ServletContextHandler> {
class JettyServlet3MappingTest extends AbstractServlet3MappingTest<Server, ServletContextHandler> {
@Override
Server startServer(int port) {

View File

@ -0,0 +1,135 @@
/*
* 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
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import org.apache.catalina.Context
import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.util.descriptor.web.FilterDef
import org.apache.tomcat.util.descriptor.web.FilterMap
abstract class TomcatServlet3FilterMappingTest extends TomcatServlet3MappingTest {
void addFilter(Context servletContext, String path, Class<Filter> filter) {
String name = UUID.randomUUID()
FilterDef filterDef = new FilterDef()
filterDef.setFilter(filter.newInstance())
filterDef.setFilterName(name)
servletContext.addFilterDef(filterDef)
FilterMap filterMap = new FilterMap()
filterMap.setFilterName(name)
filterMap.addURLPattern(path)
servletContext.addFilterMap(filterMap)
}
void addFilterWithServletName(Context servletContext, String servletName, Class<Filter> filter) {
String name = UUID.randomUUID()
FilterDef filterDef = new FilterDef()
filterDef.setFilter(filter.newInstance())
filterDef.setFilterName(name)
servletContext.addFilterDef(filterDef)
FilterMap filterMap = new FilterMap()
filterMap.setFilterName(name)
filterMap.addServletName(servletName)
servletContext.addFilterMap(filterMap)
}
static class TestFilter implements Filter {
@Override
void init(FilterConfig filterConfig) throws ServletException {
}
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest.getAttribute("firstFilterCalled") != null) {
servletRequest.setAttribute("testFilterCalled", Boolean.TRUE)
filterChain.doFilter(servletRequest, servletResponse)
} else {
throw new IllegalStateException("First filter should have been called.")
}
}
@Override
void destroy() {
}
}
static class FirstFilter implements Filter {
@Override
void init(FilterConfig filterConfig) throws ServletException {
}
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setAttribute("firstFilterCalled", Boolean.TRUE)
filterChain.doFilter(servletRequest, servletResponse)
}
@Override
void destroy() {
}
}
static class LastFilter implements Filter {
@Override
void init(FilterConfig filterConfig) throws ServletException {
}
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest.getAttribute("testFilterCalled") != null) {
HttpServletResponse response = (HttpServletResponse) servletResponse
response.getWriter().write("Ok")
response.setStatus(HttpServletResponse.SC_OK)
} else {
filterChain.doFilter(servletRequest, servletResponse)
}
}
@Override
void destroy() {
}
}
static class DefaultServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp) {
throw new IllegalStateException("Servlet should not have been called, filter should have handled the request.")
}
}
}
class TomcatServlet3FilterUrlPatternMappingTest extends TomcatServlet3FilterMappingTest {
@Override
protected void setupServlets(Context context) {
addFilter(context, "/*", FirstFilter)
addFilter(context, "/prefix/*", TestFilter)
addFilter(context, "*.suffix", TestFilter)
addFilter(context, "/*", LastFilter)
}
}
class TomcatServlet3FilterServletNameMappingTest extends TomcatServlet3FilterMappingTest {
@Override
protected void setupServlets(Context context) {
Tomcat.addServlet(context, "prefix-servlet", new DefaultServlet())
context.addServletMappingDecoded("/prefix/*", "prefix-servlet")
Tomcat.addServlet(context, "suffix-servlet", new DefaultServlet())
context.addServletMappingDecoded("*.suffix", "suffix-servlet")
addFilter(context, "/*", FirstFilter)
addFilterWithServletName(context, "prefix-servlet", TestFilter)
addFilterWithServletName(context, "suffix-servlet", TestFilter)
addFilterWithServletName(context, "prefix-servlet", LastFilter)
addFilterWithServletName(context, "suffix-servlet", LastFilter)
}
}

View File

@ -10,7 +10,7 @@ import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.JarScanFilter
import org.apache.tomcat.JarScanType
class TomcatServletMappingTest extends AbstractServletMappingTest<Tomcat, Context> {
class TomcatServlet3MappingTest extends AbstractServlet3MappingTest<Tomcat, Context> {
@Override
Tomcat startServer(int port) {

View File

@ -5,22 +5,21 @@
package io.opentelemetry.instrumentation.servlet.v3_0;
import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.FILTER;
import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.SERVLET;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming;
import io.opentelemetry.instrumentation.servlet.MappingResolver;
import io.opentelemetry.instrumentation.servlet.javax.JavaxServletHttpServerTracer;
import java.util.Collection;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import io.opentelemetry.instrumentation.servlet.naming.ServletSpanNameProvider;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Servlet3HttpServerTracer extends JavaxServletHttpServerTracer<HttpServletResponse> {
private static final Servlet3HttpServerTracer TRACER = new Servlet3HttpServerTracer();
private static final ServletSpanNameProvider<HttpServletRequest> SPAN_NAME_PROVIDER =
new ServletSpanNameProvider<>(Servlet3Accessor.INSTANCE);
protected Servlet3HttpServerTracer() {
super(Servlet3Accessor.INSTANCE);
@ -30,89 +29,20 @@ public class Servlet3HttpServerTracer extends JavaxServletHttpServerTracer<HttpS
return TRACER;
}
public Context startSpan(Object servletOrFilter, HttpServletRequest request) {
return startSpan(
request, getSpanName(servletOrFilter, request), servletOrFilter instanceof Servlet);
}
private String getSpanName(Object servletOrFilter, HttpServletRequest request) {
String spanName = getSpanNameFromPath(servletOrFilter, request);
if (spanName == null) {
String contextPath = request.getContextPath();
if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) {
return "HTTP " + request.getMethod();
}
return contextPath;
}
return spanName;
}
private static String getSpanNameFromPath(Object servletOrFilter, HttpServletRequest request) {
// we are only interested in Servlets
if (!(servletOrFilter instanceof Servlet)) {
return null;
}
Servlet servlet = (Servlet) servletOrFilter;
String mapping = getMapping(servlet, request.getServletPath(), request.getPathInfo());
// mapping was not found
if (mapping == null) {
return null;
}
// prepend context path
String contextPath = request.getContextPath();
if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) {
return mapping;
}
return contextPath + mapping;
}
private static String getMapping(Servlet servlet, String servletPath, String pathInfo) {
ServletConfig servletConfig = servlet.getServletConfig();
if (servletConfig == null) {
return null;
}
String servletName = servletConfig.getServletName();
ServletContext servletContext = servletConfig.getServletContext();
MappingResolver mappingResolver = getMappingResolver(servletContext, servletName);
if (mappingResolver == null) {
return null;
}
return mappingResolver.resolve(servletPath, pathInfo);
}
private static MappingResolver getMappingResolver(
ServletContext servletContext, String servletName) {
if (servletContext == null || servletName == null) {
return null;
}
String key = MappingResolver.class.getName() + "." + servletName;
MappingResolver mappingResolver = (MappingResolver) servletContext.getAttribute(key);
if (mappingResolver != null) {
return mappingResolver;
}
ServletRegistration servletRegistration = servletContext.getServletRegistration(servletName);
if (servletRegistration == null) {
return null;
}
Collection<String> mappings = servletRegistration.getMappings();
if (mappings == null) {
return null;
}
mappingResolver = MappingResolver.build(mappings);
servletContext.setAttribute(key, mappingResolver);
return mappingResolver;
public Context startSpan(
HttpServletRequest request, MappingResolver mappingResolver, boolean servlet) {
return startSpan(request, SPAN_NAME_PROVIDER.getSpanName(mappingResolver, request), servlet);
}
public Context updateContext(
Context context, Object servletOrFilter, HttpServletRequest request) {
Context context,
HttpServletRequest request,
MappingResolver mappingResolver,
boolean servlet) {
ServerSpanNaming.updateServerSpanName(
context, SERVLET, () -> getSpanNameFromPath(servletOrFilter, request));
context,
servlet ? SERVLET : FILTER,
() -> SPAN_NAME_PROVIDER.getSpanNameOrNull(mappingResolver, request));
return updateContext(context, request);
}

View File

@ -13,4 +13,9 @@ dependencies {
api(project(':instrumentation:servlet:servlet-5.0:library'))
implementation(project(':instrumentation:servlet:servlet-common:javaagent'))
compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: '5.0.0'
testLibrary group: 'org.eclipse.jetty', name: 'jetty-server', version: '11.0.0'
testLibrary group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '11.0.0'
testLibrary group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '10.0.0'
testLibrary group: 'org.apache.tomcat.embed', name: 'tomcat-embed-jasper', version: '10.0.0'
}

View File

@ -29,7 +29,10 @@ public class JakartaServletInstrumentationModule extends InstrumentationModule {
new AsyncContextInstrumentation(
BASE_PACKAGE, adviceClassName(".async.AsyncDispatchAdvice")),
new ServletAndFilterInstrumentation(
BASE_PACKAGE, adviceClassName(".service.JakartaServletServiceAdvice")),
BASE_PACKAGE,
adviceClassName(".service.JakartaServletServiceAdvice"),
adviceClassName(".service.JakartaServletInitAdvice"),
adviceClassName(".service.JakartaServletFilterInitAdvice")),
new HttpServletResponseInstrumentation(
BASE_PACKAGE, adviceClassName(".response.ResponseSendAdvice")),
new RequestDispatcherInstrumentation(

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterConfig;
import net.bytebuddy.asm.Advice;
public class JakartaServletFilterInitAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void filterInit(
@Advice.This Filter filter, @Advice.Argument(0) FilterConfig filterConfig) {
if (filterConfig == null) {
return;
}
InstrumentationContext.get(Filter.class, MappingResolver.class)
.putIfAbsent(filter, new JakartaServletFilterMappingResolverFactory(filterConfig));
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.instrumentation.servlet.naming.ServletFilterMappingResolverFactory;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.FilterRegistration;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import java.util.Collection;
public class JakartaServletFilterMappingResolverFactory
extends ServletFilterMappingResolverFactory<FilterRegistration>
implements ContextStore.Factory<MappingResolver> {
private final FilterConfig filterConfig;
public JakartaServletFilterMappingResolverFactory(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
@Override
protected FilterRegistration getFilterRegistration() {
String filterName = filterConfig.getFilterName();
ServletContext servletContext = filterConfig.getServletContext();
if (filterName == null || servletContext == null) {
return null;
}
return servletContext.getFilterRegistration(filterName);
}
@Override
protected Collection<String> getUrlPatternMappings(FilterRegistration filterRegistration) {
return filterRegistration.getUrlPatternMappings();
}
@Override
protected Collection<String> getServletNameMappings(FilterRegistration filterRegistration) {
return filterRegistration.getServletNameMappings();
}
@Override
protected Collection<String> getServletMappings(String servletName) {
ServletRegistration servletRegistration =
filterConfig.getServletContext().getServletRegistration(servletName);
if (servletRegistration == null) {
return null;
}
return servletRegistration.getMappings();
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import net.bytebuddy.asm.Advice;
public class JakartaServletInitAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static void servletInit(
@Advice.This Servlet servlet, @Advice.Argument(0) ServletConfig servletConfig) {
if (servletConfig == null) {
return;
}
InstrumentationContext.get(Servlet.class, MappingResolver.class)
.putIfAbsent(servlet, new JakartaServletMappingResolverFactory(servletConfig));
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.servlet.v5_0.service;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.instrumentation.servlet.naming.ServletMappingResolverFactory;
import io.opentelemetry.javaagent.instrumentation.api.ContextStore;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import java.util.Collection;
public class JakartaServletMappingResolverFactory extends ServletMappingResolverFactory
implements ContextStore.Factory<MappingResolver> {
private final ServletConfig servletConfig;
public JakartaServletMappingResolverFactory(ServletConfig servletConfig) {
this.servletConfig = servletConfig;
}
public Collection<String> getMappings() {
String servletName = servletConfig.getServletName();
ServletContext servletContext = servletConfig.getServletContext();
if (servletName == null || servletContext == null) {
return null;
}
ServletRegistration servletRegistration = servletContext.getServletRegistration(servletName);
if (servletRegistration == null) {
return null;
}
return servletRegistration.getMappings();
}
}

View File

@ -10,9 +10,13 @@ import static io.opentelemetry.instrumentation.servlet.jakarta.v5_0.JakartaServl
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.api.servlet.AppServerBridge;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.javaagent.instrumentation.api.CallDepthThreadLocalMap;
import io.opentelemetry.javaagent.instrumentation.api.InstrumentationContext;
import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge;
import io.opentelemetry.javaagent.instrumentation.servlet.common.service.ServletAndFilterAdviceHelper;
import jakarta.servlet.Filter;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
@ -37,12 +41,24 @@ public class JakartaServletServiceAdvice {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
boolean servlet = servletOrFilter instanceof Servlet;
MappingResolver mappingResolver;
if (servlet) {
mappingResolver =
InstrumentationContext.get(Servlet.class, MappingResolver.class)
.get((Servlet) servletOrFilter);
} else {
mappingResolver =
InstrumentationContext.get(Filter.class, MappingResolver.class)
.get((Filter) servletOrFilter);
}
Context attachedContext = tracer().getServerContext(httpServletRequest);
if (attachedContext != null) {
// We are inside nested servlet/filter/app-server span, don't create new span
if (tracer().needsRescoping(attachedContext)) {
attachedContext =
tracer().updateContext(attachedContext, servletOrFilter, httpServletRequest);
tracer().updateContext(attachedContext, httpServletRequest, mappingResolver, servlet);
scope = attachedContext.makeCurrent();
return;
}
@ -51,7 +67,7 @@ public class JakartaServletServiceAdvice {
// instrumentation, if needed update span with info from current request.
Context currentContext = Java8BytecodeBridge.currentContext();
Context updatedContext =
tracer().updateContext(currentContext, servletOrFilter, httpServletRequest);
tracer().updateContext(currentContext, httpServletRequest, mappingResolver, servlet);
if (updatedContext != currentContext) {
// runOnceUnderAppServer updated context, need to re-scope
scope = updatedContext.makeCurrent();
@ -66,7 +82,7 @@ public class JakartaServletServiceAdvice {
// In case it was created by app server integration we need to update it with info from
// current request.
Context updatedContext =
tracer().updateContext(currentContext, servletOrFilter, httpServletRequest);
tracer().updateContext(currentContext, httpServletRequest, mappingResolver, servlet);
if (currentContext != updatedContext) {
// updateContext updated context, need to re-scope
scope = updatedContext.makeCurrent();
@ -74,7 +90,7 @@ public class JakartaServletServiceAdvice {
return;
}
context = tracer().startSpan(servletOrFilter, httpServletRequest);
context = tracer().startSpan(httpServletRequest, mappingResolver, servlet);
scope = context.makeCurrent();
}

View File

@ -0,0 +1,82 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static io.opentelemetry.api.trace.StatusCode.ERROR
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
import io.opentelemetry.instrumentation.test.base.HttpServerTestTrait
import jakarta.servlet.Servlet
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServlet
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import okhttp3.HttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import spock.lang.Unroll
abstract class AbstractServlet5MappingTest<SERVER, CONTEXT> extends AgentInstrumentationSpecification implements HttpServerTestTrait<SERVER> {
abstract void addServlet(CONTEXT context, String path, Class<Servlet> servlet)
protected void setupServlets(CONTEXT context) {
addServlet(context, "/prefix/*", TestServlet)
addServlet(context, "*.suffix", TestServlet)
}
static class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().write("Ok")
}
}
Request.Builder request(HttpUrl url, String method, RequestBody body) {
return new Request.Builder()
.url(url)
.method(method, body)
}
@Unroll
def "test path #path"() {
setup:
def url = HttpUrl.get(address.resolve(path)).newBuilder().build()
def request = request(url, "GET", null).build()
Response response = client.newCall(request).execute()
expect:
response.code() == success ? 200 : 404
and:
def spanCount = success ? 1 : 2
assertTraces(1) {
trace(0, spanCount) {
span(0) {
name getContextPath() + spanName
kind SpanKind.SERVER
if (!success) {
status ERROR
}
}
if (!success) {
span(1) {
}
}
}
}
where:
path | spanName | success
'prefix' | '/prefix/*' | true
'prefix/' | '/prefix/*' | true
'prefix/a' | '/prefix/*' | true
'prefixa' | '/*' | false
'a.suffix' | '/*.suffix' | true
'.suffix' | '/*.suffix' | true
'suffix' | '/*' | false
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import jakarta.servlet.Servlet
import jakarta.servlet.ServletException
import jakarta.servlet.http.HttpServlet
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.ServletContextHandler
import spock.lang.IgnoreIf
@IgnoreIf({ !jvm.java11Compatible })
class JettyServlet5MappingTest extends AbstractServlet5MappingTest<Object, Object> {
@Override
Object startServer(int port) {
Server server = new Server(port)
ServletContextHandler handler = new ServletContextHandler(null, contextPath)
setupServlets(handler)
server.setHandler(handler)
server.start()
return server
}
@Override
void stopServer(Object serverObject) {
Server server = (Server) serverObject
server.stop()
server.destroy()
}
@Override
protected void setupServlets(Object handlerObject) {
ServletContextHandler handler = (ServletContextHandler) handlerObject
super.setupServlets(handler)
addServlet(handler, "/", DefaultServlet)
}
@Override
void addServlet(Object handlerObject, String path, Class<Servlet> servlet) {
ServletContextHandler handler = (ServletContextHandler) handlerObject
handler.addServlet(servlet, path)
}
static class DefaultServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.sendError(404)
}
}
@Override
String getContextPath() {
"/jetty-context"
}
}

View File

@ -0,0 +1,135 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import jakarta.servlet.Filter
import jakarta.servlet.FilterChain
import jakarta.servlet.FilterConfig
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServlet
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.apache.catalina.Context
import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.util.descriptor.web.FilterDef
import org.apache.tomcat.util.descriptor.web.FilterMap
abstract class TomcatServlet5FilterMappingTest extends TomcatServlet5MappingTest {
void addFilter(Context servletContext, String path, Class<Filter> filter) {
String name = UUID.randomUUID()
FilterDef filterDef = new FilterDef()
filterDef.setFilter(filter.newInstance())
filterDef.setFilterName(name)
servletContext.addFilterDef(filterDef)
FilterMap filterMap = new FilterMap()
filterMap.setFilterName(name)
filterMap.addURLPattern(path)
servletContext.addFilterMap(filterMap)
}
void addFilterWithServletName(Context servletContext, String servletName, Class<Filter> filter) {
String name = UUID.randomUUID()
FilterDef filterDef = new FilterDef()
filterDef.setFilter(filter.newInstance())
filterDef.setFilterName(name)
servletContext.addFilterDef(filterDef)
FilterMap filterMap = new FilterMap()
filterMap.setFilterName(name)
filterMap.addServletName(servletName)
servletContext.addFilterMap(filterMap)
}
static class TestFilter implements Filter {
@Override
void init(FilterConfig filterConfig) throws ServletException {
}
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest.getAttribute("firstFilterCalled") != null) {
servletRequest.setAttribute("testFilterCalled", Boolean.TRUE)
filterChain.doFilter(servletRequest, servletResponse)
} else {
throw new IllegalStateException("First filter should have been called.")
}
}
@Override
void destroy() {
}
}
static class FirstFilter implements Filter {
@Override
void init(FilterConfig filterConfig) throws ServletException {
}
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setAttribute("firstFilterCalled", Boolean.TRUE)
filterChain.doFilter(servletRequest, servletResponse)
}
@Override
void destroy() {
}
}
static class LastFilter implements Filter {
@Override
void init(FilterConfig filterConfig) throws ServletException {
}
@Override
void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest.getAttribute("testFilterCalled") != null) {
HttpServletResponse response = (HttpServletResponse) servletResponse
response.getWriter().write("Ok")
response.setStatus(HttpServletResponse.SC_OK)
} else {
filterChain.doFilter(servletRequest, servletResponse)
}
}
@Override
void destroy() {
}
}
static class DefaultServlet extends HttpServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp) {
throw new IllegalStateException("Servlet should not have been called, filter should have handled the request.")
}
}
}
class TomcatServlet5FilterUrlPatternMappingTest extends TomcatServlet5FilterMappingTest {
@Override
protected void setupServlets(Context context) {
addFilter(context, "/*", FirstFilter)
addFilter(context, "/prefix/*", TestFilter)
addFilter(context, "*.suffix", TestFilter)
addFilter(context, "/*", LastFilter)
}
}
class TomcatServlet5FilterServletNameMappingTest extends TomcatServlet5FilterMappingTest {
@Override
protected void setupServlets(Context context) {
Tomcat.addServlet(context, "prefix-servlet", DefaultServlet.newInstance())
context.addServletMappingDecoded("/prefix/*", "prefix-servlet")
Tomcat.addServlet(context, "suffix-servlet", DefaultServlet.newInstance())
context.addServletMappingDecoded("*.suffix", "suffix-servlet")
addFilter(context, "/*", FirstFilter)
addFilterWithServletName(context, "prefix-servlet", TestFilter)
addFilterWithServletName(context, "suffix-servlet", TestFilter)
addFilterWithServletName(context, "prefix-servlet", LastFilter)
addFilterWithServletName(context, "suffix-servlet", LastFilter)
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import jakarta.servlet.Servlet
import java.nio.file.Files
import org.apache.catalina.Context
import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.JarScanFilter
import org.apache.tomcat.JarScanType
class TomcatServlet5MappingTest extends AbstractServlet5MappingTest<Tomcat, Context> {
@Override
Tomcat startServer(int port) {
def tomcatServer = new Tomcat()
def baseDir = Files.createTempDirectory("tomcat").toFile()
baseDir.deleteOnExit()
tomcatServer.setBaseDir(baseDir.getAbsolutePath())
tomcatServer.setPort(port)
tomcatServer.getConnector().enableLookups = true // get localhost instead of 127.0.0.1
File applicationDir = new File(baseDir, "/webapps/ROOT")
if (!applicationDir.exists()) {
applicationDir.mkdirs()
applicationDir.deleteOnExit()
}
Context servletContext = tomcatServer.addWebapp(contextPath, applicationDir.getAbsolutePath())
// Speed up startup by disabling jar scanning:
servletContext.getJarScanner().setJarScanFilter(new JarScanFilter() {
@Override
boolean check(JarScanType jarScanType, String jarName) {
return false
}
})
setupServlets(servletContext)
tomcatServer.start()
return tomcatServer
}
@Override
void stopServer(Tomcat server) {
server.stop()
server.destroy()
}
@Override
void addServlet(Context servletContext, String path, Class<Servlet> servlet) {
String name = UUID.randomUUID()
Tomcat.addServlet(servletContext, name, servlet.newInstance())
servletContext.addServletMappingDecoded(path, name)
}
@Override
String getContextPath() {
return "/tomcat-context"
}
}

View File

@ -85,6 +85,11 @@ public class JakartaServletAccessor
return request.getServletPath();
}
@Override
public String getRequestPathInfo(HttpServletRequest request) {
return request.getPathInfo();
}
@Override
public Principal getRequestUserPrincipal(HttpServletRequest request) {
return request.getUserPrincipal();

View File

@ -5,25 +5,24 @@
package io.opentelemetry.instrumentation.servlet.jakarta.v5_0;
import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.FILTER;
import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.SERVLET;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming;
import io.opentelemetry.instrumentation.servlet.MappingResolver;
import io.opentelemetry.instrumentation.servlet.ServletHttpServerTracer;
import io.opentelemetry.instrumentation.servlet.naming.ServletSpanNameProvider;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Collection;
public class JakartaServletHttpServerTracer
extends ServletHttpServerTracer<HttpServletRequest, HttpServletResponse> {
private static final JakartaServletHttpServerTracer TRACER = new JakartaServletHttpServerTracer();
private static final ServletSpanNameProvider<HttpServletRequest> SPAN_NAME_PROVIDER =
new ServletSpanNameProvider<>(JakartaServletAccessor.INSTANCE);
public JakartaServletHttpServerTracer() {
super(JakartaServletAccessor.INSTANCE);
@ -33,89 +32,20 @@ public class JakartaServletHttpServerTracer
return TRACER;
}
public Context startSpan(Object servletOrFilter, HttpServletRequest request) {
return startSpan(
request, getSpanName(servletOrFilter, request), servletOrFilter instanceof Servlet);
}
private String getSpanName(Object servletOrFilter, HttpServletRequest request) {
String spanName = getSpanNameFromPath(servletOrFilter, request);
if (spanName == null) {
String contextPath = request.getContextPath();
if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) {
return "HTTP " + request.getMethod();
}
return contextPath;
}
return spanName;
}
private static String getSpanNameFromPath(Object servletOrFilter, HttpServletRequest request) {
// we are only interested in Servlets
if (!(servletOrFilter instanceof Servlet)) {
return null;
}
Servlet servlet = (Servlet) servletOrFilter;
String mapping = getMapping(servlet, request.getServletPath(), request.getPathInfo());
// mapping was not found
if (mapping == null) {
return null;
}
// prepend context path
String contextPath = request.getContextPath();
if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) {
return mapping;
}
return contextPath + mapping;
}
private static String getMapping(Servlet servlet, String servletPath, String pathInfo) {
ServletConfig servletConfig = servlet.getServletConfig();
if (servletConfig == null) {
return null;
}
String servletName = servletConfig.getServletName();
ServletContext servletContext = servletConfig.getServletContext();
MappingResolver mappingResolver = getMappingResolver(servletContext, servletName);
if (mappingResolver == null) {
return null;
}
return mappingResolver.resolve(servletPath, pathInfo);
}
private static MappingResolver getMappingResolver(
ServletContext servletContext, String servletName) {
if (servletContext == null || servletName == null) {
return null;
}
String key = MappingResolver.class.getName() + "." + servletName;
MappingResolver mappingResolver = (MappingResolver) servletContext.getAttribute(key);
if (mappingResolver != null) {
return mappingResolver;
}
ServletRegistration servletRegistration = servletContext.getServletRegistration(servletName);
if (servletRegistration == null) {
return null;
}
Collection<String> mappings = servletRegistration.getMappings();
if (mappings == null) {
return null;
}
mappingResolver = MappingResolver.build(mappings);
servletContext.setAttribute(key, mappingResolver);
return mappingResolver;
public Context startSpan(
HttpServletRequest request, MappingResolver mappingResolver, boolean servlet) {
return startSpan(request, SPAN_NAME_PROVIDER.getSpanName(mappingResolver, request), servlet);
}
public Context updateContext(
Context context, Object servletOrFilter, HttpServletRequest request) {
Context context,
HttpServletRequest request,
MappingResolver mappingResolver,
boolean servlet) {
ServerSpanNaming.updateServerSpanName(
context, SERVLET, () -> getSpanNameFromPath(servletOrFilter, request));
context,
servlet ? SERVLET : FILTER,
() -> SPAN_NAME_PROVIDER.getSpanNameOrNull(mappingResolver, request));
return updateContext(context, request);
}

View File

@ -8,12 +8,12 @@ package io.opentelemetry.javaagent.instrumentation.servlet.common.service;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.AgentElementMatchers.safeHasSuperType;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.ClassLoaderMatcher.hasClassesNamed;
import static io.opentelemetry.javaagent.tooling.bytebuddy.matcher.NameMatchers.namedOneOf;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import io.opentelemetry.javaagent.tooling.TypeInstrumentation;
import java.util.HashMap;
import java.util.Map;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
@ -22,10 +22,22 @@ import net.bytebuddy.matcher.ElementMatcher;
public class ServletAndFilterInstrumentation implements TypeInstrumentation {
private final String basePackageName;
private final String adviceClassName;
private final String servletInitAdviceClassName;
private final String filterInitAdviceClassName;
public ServletAndFilterInstrumentation(String basePackageName, String adviceClassName) {
public ServletAndFilterInstrumentation(
String basePackageName,
String adviceClassName,
String servletInitAdviceClassName,
String filterInitAdviceClassName) {
this.basePackageName = basePackageName;
this.adviceClassName = adviceClassName;
this.servletInitAdviceClassName = servletInitAdviceClassName;
this.filterInitAdviceClassName = filterInitAdviceClassName;
}
public ServletAndFilterInstrumentation(String basePackageName, String adviceClassName) {
this(basePackageName, adviceClassName, null, null);
}
@Override
@ -40,11 +52,23 @@ public class ServletAndFilterInstrumentation implements TypeInstrumentation {
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap(
Map<ElementMatcher<MethodDescription>, String> transformers = new HashMap<>();
transformers.put(
namedOneOf("doFilter", "service")
.and(takesArgument(0, named(basePackageName + ".ServletRequest")))
.and(takesArgument(1, named(basePackageName + ".ServletResponse")))
.and(isPublic()),
adviceClassName);
if (servletInitAdviceClassName != null) {
transformers.put(
named("init").and(takesArgument(0, named(basePackageName + ".ServletConfig"))),
servletInitAdviceClassName);
}
if (filterInitAdviceClassName != null) {
transformers.put(
named("init").and(takesArgument(0, named(basePackageName + ".FilterConfig"))),
filterInitAdviceClassName);
}
return transformers;
}
}

View File

@ -8,10 +8,11 @@ package io.opentelemetry.instrumentation.servlet;
import java.security.Principal;
/**
* This interface is used to access methods of HttpServletRequest and HttpServletResponse classes in
* shared code that is used for both jakarta.servlet and javax.servlet versions of those classes. A
* wrapper class with extra information attached may be used as well in cases where the class itself
* does not provide some field (such as response status for Servlet API 2.2).
* This interface is used to access methods of ServletContext, HttpServletRequest and
* HttpServletResponse classes in shared code that is used for both jakarta.servlet and
* javax.servlet versions of those classes. A wrapper class with extra information attached may be
* used as well in cases where the class itself does not provide some field (such as response status
* for Servlet API 2.2).
*
* @param <REQUEST> HttpServletRequest class (or a wrapper)
* @param <RESPONSE> HttpServletResponse class (or a wrapper)
@ -43,6 +44,8 @@ public interface ServletAccessor<REQUEST, RESPONSE> {
String getRequestServletPath(REQUEST request);
String getRequestPathInfo(REQUEST request);
Principal getRequestUserPrincipal(REQUEST request);
Integer getRequestRemotePort(REQUEST request);

View File

@ -6,6 +6,7 @@
package io.opentelemetry.instrumentation.servlet;
import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.CONTAINER;
import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.FILTER;
import static io.opentelemetry.instrumentation.api.servlet.ServerSpanNaming.Source.SERVLET;
import io.opentelemetry.api.trace.Span;
@ -48,11 +49,10 @@ public abstract class ServletHttpServerTracer<REQUEST, RESPONSE>
accessor.setRequestAttribute(request, "trace_id", spanContext.getTraceId());
accessor.setRequestAttribute(request, "span_id", spanContext.getSpanId());
if (servlet) {
// server span name shouldn't be updated when server span was created from a call to Servlet
// (if created from a call to Filter then name may be updated from updateContext)
ServerSpanNaming.updateSource(context, SERVLET);
}
// server span name shouldn't be updated when server span was created from a call to Servlet
// (if created from a call to Filter then name may be updated from updateContext)
ServerSpanNaming.updateSource(context, servlet ? SERVLET : FILTER);
return addServletContextPath(context, request);
}

View File

@ -0,0 +1,67 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.servlet.naming;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public abstract class ServletFilterMappingResolverFactory<FILTERREGISTRATION> {
protected abstract FILTERREGISTRATION getFilterRegistration();
protected abstract Collection<String> getUrlPatternMappings(
FILTERREGISTRATION filterRegistration);
protected abstract Collection<String> getServletNameMappings(
FILTERREGISTRATION filterRegistration);
protected abstract Collection<String> getServletMappings(String servletName);
private Collection<String> getMappings() {
FILTERREGISTRATION filterRegistration = getFilterRegistration();
if (filterRegistration == null) {
return null;
}
Set<String> mappings = new HashSet<>();
Collection<String> urlPatternMappings = getUrlPatternMappings(filterRegistration);
if (urlPatternMappings != null) {
mappings.addAll(urlPatternMappings);
}
Collection<String> servletNameMappings = getServletNameMappings(filterRegistration);
if (servletNameMappings != null) {
for (String servletName : servletNameMappings) {
Collection<String> servletMappings = getServletMappings(servletName);
if (servletMappings != null) {
mappings.addAll(servletMappings);
}
}
}
if (mappings.isEmpty()) {
return null;
}
List<String> mappingsList = new ArrayList<>(mappings);
// sort longest mapping first
Collections.sort(mappingsList, (s1, s2) -> s2.length() - s1.length());
return mappingsList;
}
public final MappingResolver create() {
Collection<String> mappings = getMappings();
if (mappings == null) {
return null;
}
return MappingResolver.build(mappings);
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.servlet.naming;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import java.util.Collection;
public abstract class ServletMappingResolverFactory {
protected abstract Collection<String> getMappings();
public final MappingResolver create() {
Collection<String> mappings = getMappings();
if (mappings == null) {
return null;
}
return MappingResolver.build(mappings);
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.servlet.naming;
import io.opentelemetry.instrumentation.api.servlet.MappingResolver;
import io.opentelemetry.instrumentation.servlet.ServletAccessor;
/** Helper class for constructing span name for given servlet/filter mapping and request. */
public class ServletSpanNameProvider<REQUEST> {
private final ServletAccessor<REQUEST, ?> servletAccessor;
public ServletSpanNameProvider(ServletAccessor<REQUEST, ?> servletAccessor) {
this.servletAccessor = servletAccessor;
}
public String getSpanName(MappingResolver mappingResolver, REQUEST request) {
String spanName = getSpanNameOrNull(mappingResolver, request);
if (spanName == null) {
String contextPath = servletAccessor.getRequestContextPath(request);
if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) {
return "HTTP " + servletAccessor.getRequestMethod(request);
}
return contextPath + "/*";
}
return spanName;
}
public String getSpanNameOrNull(MappingResolver mappingResolver, REQUEST request) {
if (mappingResolver == null) {
return null;
}
String servletPath = servletAccessor.getRequestServletPath(request);
String pathInfo = servletAccessor.getRequestPathInfo(request);
String mapping = mappingResolver.resolve(servletPath, pathInfo);
// mapping was not found
if (mapping == null) {
return null;
}
// prepend context path
String contextPath = servletAccessor.getRequestContextPath(request);
if (contextPath == null || contextPath.isEmpty() || contextPath.equals("/")) {
return mapping;
}
return contextPath + mapping;
}
}

View File

@ -76,6 +76,11 @@ public abstract class JavaxServletAccessor<R> implements ServletAccessor<HttpSer
return request.getServletPath();
}
@Override
public String getRequestPathInfo(HttpServletRequest request) {
return request.getPathInfo();
}
@Override
public Principal getRequestUserPrincipal(HttpServletRequest request) {
return request.getUserPrincipal();

View File

@ -87,7 +87,7 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
case NOT_FOUND:
return getContextPath() + "/**"
case LOGIN:
return "HTTP POST"
return getContextPath() + "/*"
default:
return super.expectedServerSpanName(endpoint)
}

View File

@ -68,7 +68,7 @@ class Struts2ActionSpanTest extends HttpServerTest<Server> implements AgentTestT
case PATH_PARAM:
return getContextPath() + "/path/{id}/param"
case NOT_FOUND:
return "HTTP GET"
return getContextPath() + "/*"
default:
return endpoint.resolvePath(address).path
}

View File

@ -16,4 +16,13 @@ class LibertyServletOnlySmokeTest extends LibertySmokeTest {
return ["liberty-servlet.xml": "/config/server.xml"]
}
@Override
protected String getSpanName(String path) {
switch (path) {
case "/app/hello.txt":
case "/app/file-that-does-not-exist":
return "HTTP GET"
}
return super.getSpanName(path)
}
}

View File

@ -21,14 +21,4 @@ class LibertySmokeTest extends AppServerTest {
protected TargetWaitStrategy getWaitStrategy() {
return new TargetWaitStrategy.Log(Duration.ofMinutes(3), ".*server is ready to run a smarter planet.*")
}
@Override
protected String getSpanName(String path) {
switch (path) {
case "/app/hello.txt":
case "/app/file-that-does-not-exist":
return "HTTP GET"
}
return super.getSpanName(path)
}
}