Search code examples
testingkotlinapache-camelapache-camel-3

Camel 3: How to intercept a route from `onException` using `interceptSendToEndpoint`


Problem:

During the migration from Camel 2 to 3, my error routing tests broke.

The pattern that I follow is to force an exception and assert that the onException() block sends to my metrics route with the appropriate tags.

I'm using uri pattern matching to individually test that each tag is emitted...this strongly influences the test pattern

Note: In both examples below the createRouteBuilder() method is identical

Passing Camel 2 Example

import org.apache.camel.RoutesBuilder
import org.apache.camel.builder.RouteBuilder
import org.apache.camel.test.junit4.CamelTestSupport
import org.junit.Test
import java.util.concurrent.TimeUnit

class Camel2Test : CamelTestSupport() {

    val startUri = "direct:start"
    val baseMetricsUri = "micrometer:counter:errors"
    // Want to use pattern to test each individual tag here
    val fullMetricsUri = "$baseMetricsUri?tags=a=1,b=2"

    override fun isUseAdviceWith(): Boolean {
        return true
    }

    override fun createRouteBuilder(): RoutesBuilder {
        return object : RouteBuilder() {
            override fun configure() {

                onException(Exception::class.java)
                    .to(fullMetricsUri)

                from(startUri)
                    .routeId(startUri)
                    .throwException(Exception())
            }

        }
    }

    @Test
    fun `metric with tag B is emitted`() {
        val exchange = createExchangeWithBody("")

        val mockEndpoint = getMockEndpoint("mock:test")

        context.getRouteDefinition(startUri)
            .adviceWith(context, object : RouteBuilder() {
                override fun configure() {
                    interceptSendToEndpoint("$baseMetricsUri.*b.*2.*") // <-- PATTERN
                        .skipSendToOriginalEndpoint()
                        .to(mockEndpoint)
                }
            })

        context.start()

        mockEndpoint.expectedMessageCount(1)

        template.send(startUri, exchange)

        assertMockEndpointsSatisfied(2, TimeUnit.SECONDS)
    }
}

Failing Camel 3 Example

import org.apache.camel.RoutesBuilder
import org.apache.camel.builder.AdviceWithRouteBuilder
import org.apache.camel.builder.RouteBuilder
import org.apache.camel.test.junit4.CamelTestSupport
import org.junit.Test
import java.util.concurrent.TimeUnit

class Camel3Test : CamelTestSupport() {

    val startUri = "direct:start"
    val baseMetricsUri = "micrometer:counter:errors"
    // Want to use pattern to test each individual tag here
    val fullMetricsUri = "$baseMetricsUri?tags=a=1,b=2"

    override fun isUseAdviceWith(): Boolean {
        return true
    }

    override fun createRouteBuilder(): RoutesBuilder {
        return object : RouteBuilder() {
            override fun configure() {

                onException(Exception::class.java)
                    .to(fullMetricsUri)

                from(startUri)
                    .routeId(startUri)
                    .throwException(Exception())
            }

        }
    }

    @Test
    fun `metric with tag B is emitted`() {
        val exchange = createExchangeWithBody("")

        val mockEndpoint = getMockEndpoint("mock:test")

        AdviceWithRouteBuilder.adviceWith(context, startUri) { routeBuilder ->
            routeBuilder.interceptSendToEndpoint("$baseMetricsUri.*b.*2.*") // <-- PATTERN
                .skipSendToOriginalEndpoint()
                .to(mockEndpoint)
        }

        context.start()

        mockEndpoint.expectedMessageCount(1)

        template.send(startUri, exchange)

        assertMockEndpointsSatisfied(2, TimeUnit.SECONDS)
    }
}

The mockEndpoint isn't receiving the exchange and it is instead still going to the metrics endpoint.

Question:

In Camel 3 how can I intercept a route like I was in Camel 2 using patterns? Manual testing shows that the error routing is behaving as expected in prod, so this seems to be a test configuration issue.

Other details:

  • This unit test from the camel repo demonstrates what I'm trying to do, but by manually intercepting the route rather than using mock: directly in the route.
  • When I don't need pattern matching then this alternate approach works

    override fun isMockEndpointsAndSkip() = myUri
    
    // ... in test
    getMockEndpoint("mock:$myUri").expectedMessageCount(1)
    

Solution

  • This seems to be directly related to the use of onException(). Apparently in Camel 3 you can no longer intercept directly from onException, so moving the business logic out of the exception block into a new route allows the intercept to work.

    In my case this just required saving relevant onException information in exchange properties, which could then be referenced when emitting metrics.

    import org.apache.camel.RoutesBuilder
    import org.apache.camel.builder.AdviceWithRouteBuilder
    import org.apache.camel.builder.RouteBuilder
    import org.apache.camel.test.junit4.CamelTestSupport
    import org.junit.Test
    import java.util.concurrent.TimeUnit
    
    class Camel3ErrorInterceptWorking : CamelTestSupport() {
    
        val startUri = "direct:start"
        val errorUri = "direct:errors"
        val baseMetricsUri = "micrometer:counter:errors"
        val fullMetricsUri = "$baseMetricsUri?tags=a=1,b=2"
    
        override fun isUseAdviceWith(): Boolean {
            return true
        }
    
        override fun createRouteBuilder(): RoutesBuilder {
            return object : RouteBuilder() {
                override fun configure() {
    
                    onException(Exception::class.java)
                        .to(errorUri)
    
                    from(errorUri)
                        .to(fullMetricsUri) // Moved metrics here from `onException`
    
                    from(startUri)
                        .routeId(startUri)
                        .throwException(Exception())
                }
    
            }
        }
    
        @Test
        fun `exception is routed to error logging route`() {
            val exchange = createExchangeWithBody("")
    
            val mockEndpoint = getMockEndpoint("mock:test")
    
            AdviceWithRouteBuilder.adviceWith(context, startUri) { routeBuilder ->
                routeBuilder.interceptSendToEndpoint("$baseMetricsUri.*b.*2.*") // <-- PATTERN
                    .skipSendToOriginalEndpoint()
                    .to(mockEndpoint)
            }
    
            context.start()
    
            mockEndpoint.expectedMessageCount(1)
    
            template.send(startUri, exchange)
    
            assertMockEndpointsSatisfied(2, TimeUnit.SECONDS)
        }
    }