blob: 577c12c22408878fece9f5ac617ebca2830630df [file] [log] [blame]
package org.apache.geronimo.microprofile.opentracing.microprofile.server;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
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.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.geronimo.microprofile.opentracing.config.GeronimoOpenTracingConfig;
import org.apache.geronimo.microprofile.opentracing.impl.ScopeManagerImpl;
import org.apache.geronimo.microprofile.opentracing.impl.ServletHeaderTextMap;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
public class OpenTracingFilter implements Filter {
@Inject
private Tracer tracer;
@Inject
private GeronimoOpenTracingConfig config;
@Inject
private ScopeManagerImpl manager;
private Collection<Predicate<String>> forcedUrls;
private List<Predicate<String>> skipUrls;
private boolean skipDefaultTags;
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
skipDefaultTags = Boolean.parseBoolean(config.read("filter.forcedTracing.skipDefaultTags", "false"));
forcedUrls = ofNullable(config.read("filter.forcedTracing.urls", null))
.map(String::trim).filter(v -> !v.isEmpty())
.map(v -> toMatchingPredicates(v, "forcedTracing"))
.orElse(null);
skipUrls = ofNullable(config.read("filter.skippedTracing.urls", null))
.map(String::trim).filter(v -> !v.isEmpty())
.map(v -> toMatchingPredicates(v, "skippedTracing"))
.orElse(null);
}
private List<Predicate<String>> toMatchingPredicates(final String v, final String keyMarker) {
final String matchingType = config.read("filter." + keyMarker + ".matcherType", "prefix");
final Function<String, Predicate<String>> matcherFactory;
switch (matchingType) {
case "regex":
matcherFactory = from -> {
final Pattern compiled = Pattern.compile(from);
return url -> compiled.matcher(url).matches();
};
break;
case "prefix":
default:
matcherFactory = from -> url -> url.startsWith(from);
}
return Stream.of(v.split(",")).map(String::trim).filter(it -> !it.isEmpty()).map(matcherFactory)
.collect(toList());
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
if (!HttpServletRequest.class.isInstance(request)) {
chain.doFilter(request, response);
return;
}
if (forcedUrls != null && !forcedUrls.isEmpty()) {
final HttpServletRequest req = HttpServletRequest.class.cast(request);
final String matching = req.getRequestURI().substring(req.getContextPath().length());
if (forcedUrls.stream().anyMatch(p -> p.test(matching))) {
final Tracer.SpanBuilder builder = tracer.buildSpan(buildServletOperationName(req));
builder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER);
builder.withTag("component", "servlet");
ofNullable(ofNullable(tracer.activeSpan()).map(Span::context)
.orElseGet(() -> tracer.extract(Format.Builtin.HTTP_HEADERS,
new ServletHeaderTextMap(req, HttpServletResponse.class.cast(response)))))
.ifPresent(builder::asChildOf);
final Scope scope = builder.startActive(true);
final Span span = scope.span();
if (!skipDefaultTags) {
Tags.HTTP_METHOD.set(span, req.getMethod());
Tags.HTTP_URL.set(span, req.getRequestURL().toString());
}
request.setAttribute(OpenTracingFilter.class.getName(), scope);
}
}
if (skipUrls != null && !skipUrls.isEmpty()) {
final HttpServletRequest req = HttpServletRequest.class.cast(request);
final String matching = req.getRequestURI().substring(req.getContextPath().length());
if (forcedUrls.stream().anyMatch(p -> p.test(matching))) {
chain.doFilter(request, response);
return;
}
}
try {
chain.doFilter(request, response);
} catch (final Exception ex) {
getCurrentScope(request).ifPresent(scope -> {
final int status = HttpServletResponse.class.cast(response).getStatus();
final Span span = scope.span();
Tags.HTTP_STATUS.set(span,
status == HttpServletResponse.SC_OK ? HttpServletResponse.SC_INTERNAL_SERVER_ERROR : status);
Tags.ERROR.set(span, true);
span.log(new HashMap<String, Object>() {
{
put("event", Tags.ERROR.getKey());
put("event.object", ex);
}
});
});
throw ex;
} finally {
getCurrentScope(request).ifPresent(scope -> {
if (request.isAsyncStarted()) {
request.getAsyncContext().addListener(new AsyncListener() {
@Override
public void onComplete(final AsyncEvent event) {
scope.close();
}
@Override
public void onTimeout(final AsyncEvent event) {
// no-op
}
@Override
public void onError(final AsyncEvent event) {
// no-op
}
@Override
public void onStartAsync(final AsyncEvent event) {
// no-op
}
});
manager.clear();
} else {
scope.close();
}
});
}
}
private Optional<Scope> getCurrentScope(final ServletRequest request) {
return ofNullable(ofNullable(request.getAttribute(OpenTracingFilter.class.getName()))
.orElseGet(() -> tracer.scopeManager().active())).map(Scope.class::cast);
}
protected String buildServletOperationName(final HttpServletRequest req) {
return req.getMethod() + ":" + req.getRequestURL();
}
}