Use case
I'm trying to call a service, using a micronaut declarative client. The service is actually many services all the same, but hosted on a different host for each tenet in our system. e.g.
tenetA.example.com/api
tenetB.example.com/api
From micronaut, I would like to use a request header X-tenetID
, and make calls to the correct service based on it. Sounds simple enough right?
1st attempt: Using a filter
The first thing I tried was using a filter on the client
@FilterMatcher
@Documented
@Retention(RUNTIME)
@Target({TYPE, PARAMETER})
public @interface MyFilterAnnotation
{
}
@MyFilterAnnotation
@Client("http://replaceme.example.com/")
public interface MyClient
{
@Post(uri = "some/endpoint")
AuthResponse auth(@Body CustomCredentials credentials, @RequestAttribute(name = "tenet-id") String tenetID);
}
@MyFilterAnnotation
@Singleton
public class MyFilter implements HttpClientFilter
{
@Override
public int getOrder()
{
return -10; // Tried playing with the order here to no avail
}
@Override
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain)
{
String tenetID = request.getAttribute("tenet-id", String.class).orElse(null);
UriBuilder builder = UriBuilder.of(request.getUri());
builder.host(tenetID + ".example.com");
request.uri(builder.build());
return chain.proceed(request);
}
}
I've confirmed my filter IS getting called, and the request uri IS getting set, but the request uri is not being honored further down the chain. Overriding the order doesn't seem to have any effect either. It's still sending traffic to replaceme.example.com
. Am I doing something wrong here?
I found some forum posts that said this approach might not work, because the host that gets chosen is done early in the process to account for the LoadBalancer mechanism. Which takes me to my second attempt:
2nd attempt: Using client side LoadBalancer
Since it seems like the host of the url can't be changed in a filter, I tried writing my own DiscoveryClientLoadBalancerFactory
@Replaces(DiscoveryClientLoadBalancerFactory.class)
public class MyLoadBalancer extends DiscoveryClientLoadBalancerFactory
{
/**
* @param discoveryClient The discover client
*/
public MyLoadBalancer(final DiscoveryClient discoveryClient)
{
super(discoveryClient);
}
@Override
public LoadBalancer create(final String serviceID)
{
return discriminator ->
{
// discriminator always seems to be null here.
return Publishers.just(ServiceInstance.of("myService", discriminator + "example.com",8080));
};
}
}
I'm stuck here though, because I don't know how to tell the declarative client to use my tenetID as the discriminator. I've only ever seen the @Inject
annotation create a DefaultHttpClient
, which only ever calls loadBalancer.select(getLoadBalancerDiscriminator())
and getLoadBalancerDiscriminator()
always returns null
. Is there a way I can get the discriminator to be set based off a request header?
What isn't viable: application configuration
Some example documentation that uses bitbucket suggests that the url comes from configuration. We have tenets come and go, and each service is going to have to be authenticated with their own credentials that are stored in a cloud vault. So storing some kind of map in application.conf or similar isn't very useful here because we don't want to have to restart everytime we add a new tenet or rotate keys.
Help
Between the two approaches, the LoadBalancer route seems hackier because what I'm doing is request routing or url rewriting, and not load balancing. Should one of these approaches work? Is there a better way of doing what I want?