Sentry-java, spring boot, and grails: performance tracing not working

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

1 Like