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

I’ve been using Sentry for years in both Python and Java projects for backend error logging and analysis. Super tool!

Now I’m trying to get performance monitoring working in a Grails 4 application. Grails 4.0.x is based on Spring Boot 2.1.8, so I’m using the spring boot integration. As noted above, the error logging with context, tags, breadcrumbs, and user data all works just like it should! I inlcuded

   implementation 'io.sentry:sentry-spring-boot-starter:5.5.2'
    implementation 'io.sentry:sentry-logback:5.5.2'

in build.gradle plus this config in `application.yml’

    dsn: "${SENTRY_DSN}"
    in-app-includes: ""
    servername: "localhost"
        enabled: true
        minimum-event-level: "info"
        minimum-breadcrumb-level: "debug"
    use-git-commit-id-as-release: true
    send-default-pii: true
    environment: "development"
    # traces-sample-rate: 1.0
    # debug: true

and ,boom, as exected, error logging works immediately.

But when I uncomment

  traces-sample-rate: 1.0

I cannot get the automatic instrumention for perf events to work for controllers/methods. I’ve tried it in debug mode too and no events are getting dropped - the codes just isn’t firing to pick them up.

I checked my sentry project and I am not throttled, and I have no inbound filters that would account for this. I tested out the sample app you provided at

and I upgraded the spring-boot-starter-parent to 2.18.RELEASE and upgraded sentry-s-b-starting and sentry-logback to 5.5.2, matching my current setup as best I can.

In that app, I can perf events as well as errors to get sent to Sentry.

I’ve looked looked at Grails 4, and I know that a Grails Controller isn’t really a canonical Spring RestController or Controller, but now I’m stuck. How can or should I drop in some custom code - or annoations - into Grails controllers or services to get things going.

I have tried adding Spring RestController, Component, and Controller annoations bo my controllers without luck. I’ve also tried SentrySpan annoations to work without luck either.

I’m sure the key is that Grails may be based on Spring Boot, but it isn’t Spring Boot. I’m not well-versed enough in Spring MVC or Spring Boot to know how to change the HandlerMapping/RequestMappingInfoHandlerMapping behavior to get the SentryTrackingFilter to catch web requests and instrument them.

I can see from Spring Actuator endpoints that these sentry filters are loaded:

“className”: “io.sentry.spring.SentryUserFilter”
“className”: “io.sentry.spring.SentrySpringFilter”
“className”: “io.sentry.spring.tracing.SentryTracingFilter”

and all are mapped to “/*” urls. Error trapping working, but tracing not so much.

Ironically, visiting the Spring Actuator endpoints does send tracing to Sentry. But no oether endpoints in my Grails app result in tracking.

Many thanks for any tips.

I’m not familiar with Grails so I’m not sure if it’ll work. But did you try @SentryTransaction on your controller? @SentrySpan will only do something of there’s an ongoing transaction first.

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 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() {

    public SentryGrailsTracingFilter(final IHub hub) {
        this.hub = hub;

    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
                throw e;
            } finally {
                // after all filters run, controller and action are available in request attribute
                final String transactionName = provideTransactionName(httpRequest);
                // if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and
                // httpResponse.getStatus() returns 200.
                if (transaction.getStatus() == null) {
        } 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 =
                                name, "http.server", new SentryTraceHeader(sentryTraceHeader));
                return hub.startTransaction(contexts, customSamplingContext, true);
            } catch (InvalidSentryTraceHeaderException e) {
                        .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 = {
	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

Maciej! You are my hero.

I was staring at SentryTracingFilter and knew the whole ‘provide the name mapping via a request attribute after all filters have run’ was not working, but could not figure what do to about it. I had been trying to ‘trick’ Grails into using some of the default Spring MVC machinery by tacking on annoations. Which was quite foolish.


 private String provideTransactionName(HttpServletRequest httpRequest) {

        return httpRequest.getAttribute("org.grails.CONTROLLER_NAME_ATTRIBUTE") + "/" + httpRequest.getAttribute("org.grails.ACTION_NAME_ATTRIBUTE");


is exactly what I needed! Many thanks! As I’m sure is obvious, I am not a Spring or Spring Boot expert. :frowning:

I hope you and yours are safe and well,


1 Like

Thank you @knoxilla for kind words! I am happy I could help.

Take care and all the best!

1 Like

This topic was automatically closed 15 days after the last reply. New replies are no longer allowed.