I am not a Grails expert, but this is how I managed to get Sentry Spring Boot Starter working with Grails.
Grails uses different from Spring MVC way to map urls to controllers, as a result, our integration is not able to figure out the transaction name. To get it working with Grails, create a custom filter based on SentryTracingFilter
:
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import io.sentry.CustomSamplingContext;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import io.sentry.ITransaction;
import io.sentry.SentryLevel;
import io.sentry.SentryTraceHeader;
import io.sentry.SpanStatus;
import io.sentry.TransactionContext;
import io.sentry.exception.InvalidSentryTraceHeaderException;
import org.springframework.web.filter.OncePerRequestFilter;
public class SentryGrailsTracingFilter extends OncePerRequestFilter {
private static final String TRANSACTION_OP = "http.server";
private final IHub hub;
public SentryGrailsTracingFilter() {
this(HubAdapter.getInstance());
}
public SentryGrailsTracingFilter(final IHub hub) {
this.hub = hub;
}
@Override
protected void doFilterInternal(
final HttpServletRequest httpRequest,
final HttpServletResponse httpResponse,
final FilterChain filterChain)
throws ServletException, IOException {
if (hub.isEnabled()) {
final String sentryTraceHeader = httpRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER);
// at this stage we are not able to get real transaction name
final ITransaction transaction = startTransaction(httpRequest, sentryTraceHeader);
try {
filterChain.doFilter(httpRequest, httpResponse);
} catch (Throwable e) {
// exceptions that are not handled by Spring
transaction.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
// after all filters run, controller and action are available in request attribute
final String transactionName = provideTransactionName(httpRequest);
transaction.setName(transactionName);
transaction.setOperation(TRANSACTION_OP);
// if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and
// httpResponse.getStatus() returns 200.
if (transaction.getStatus() == null) {
transaction.setStatus(SpanStatus.fromHttpStatusCode(httpResponse.getStatus()));
}
transaction.finish();
}
} else {
filterChain.doFilter(httpRequest, httpResponse);
}
}
private String provideTransactionName(HttpServletRequest httpRequest) {
return httpRequest.getAttribute("org.grails.CONTROLLER_NAME_ATTRIBUTE") + "/" + httpRequest.getAttribute("org.grails.ACTION_NAME_ATTRIBUTE");
}
private ITransaction startTransaction(
final HttpServletRequest request, final String sentryTraceHeader) {
final String name = request.getMethod() + " " + request.getRequestURI();
final CustomSamplingContext customSamplingContext = new CustomSamplingContext();
customSamplingContext.set("request", request);
if (sentryTraceHeader != null) {
try {
final TransactionContext contexts =
TransactionContext.fromSentryTrace(
name, "http.server", new SentryTraceHeader(sentryTraceHeader));
return hub.startTransaction(contexts, customSamplingContext, true);
} catch (InvalidSentryTraceHeaderException e) {
hub.getOptions()
.getLogger()
.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage());
}
}
return hub.startTransaction(name, "http.server", customSamplingContext, true);
}
}
Then it must be registered it in resources.groovy
with a name “sentryTracingFilter” (to tell auto-configuration to not create the default bean):
beans = {
sentryTracingFilter(SentryGrailsTracingFilter)
sentryTracingFilterRegistration(FilterRegistrationBean) {
filter = sentryTracingFilter
urlPatterns = ['/*']
order = Ordered.HIGHEST_PRECEDENCE + 1
}
}
To simplify it, we could make it possible to inject different TransactionNameProvider
to SentryTracingFilter
, so that instead of copy & pasting whole filter class just to modify 3 lines, providing transaction name can be overwritten by a user. cc @bruno-garcia