spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.44k stars 3.27k forks source link

Routing latency collection #2045

Open Owenxh opened 3 years ago

Owenxh commented 3 years ago

GatewayMetricsFilter has collect the total latency of a request. We want to collect the latency of the API provider server. NettyRoutingFilter doesn't collect the routing latency. Adding the corresponding latency collect feature in NettyRoutingFilter and then put as an attribute to ServerWebExchange. Then we cat get the routing latency.

tony-clarke-amdocs commented 3 years ago

I think that would be a useful addition. 99% of the time should be in the API provider. This new metric might help to flush out issues with filters / predicates.

Owenxh commented 3 years ago

In order to resolve the problem, I enhanced the NettyRoutingFilter with cglib. It's a temporey method. Here is the code.


public class RoutingLatencyInterceptor implements MethodInterceptor {

    private static final String FILTER_METHOD = "filter";

    @SuppressWarnings("unchecked")
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = (invocation.getThis() != null ?
                AopUtils.getTargetClass(invocation.getThis()) : null);

        Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
        final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);

        if (isInterceptorMethod(userDeclaredMethod)) {
            final long start = System.currentTimeMillis();
            return ((Mono<Void>) invoke0(invocation))
                    .doOnSuccess(aVoid -> calculateRoutingLatency(invocation, start))
                    .doOnError(throwable -> calculateRoutingLatency(invocation, start));
        }

        return invoke0(invocation);
    }

    public boolean isInterceptorMethod(Method method) {
        if (!FILTER_METHOD.equals(method.getName())) {
            return false;
        }

        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length == 2) {
            return ServerWebExchange.class.equals(parameterTypes[0])
                    && GatewayFilterChain.class.equals(parameterTypes[1]);
        }
        return false;
    }

    private Object invoke0(MethodInvocation invocation) throws Throwable {
        return invocation.proceed();
    }

    private void calculateRoutingLatency(MethodInvocation invocation, long start) {
        Object[] args = invocation.getArguments();

        if (args == null || args.length != 2) {
            return;
        }

        ServerWebExchange exchange = (ServerWebExchange) args[0];
        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            calculateRoutingLatency0(exchange, start);
        }
        else {
            response.beforeCommit(() -> {
                calculateRoutingLatency0(exchange, start);
                return Mono.empty();
            });
        }
    }

    private void calculateRoutingLatency0(ServerWebExchange exchange, long start) {
        long routingLatency = System.currentTimeMillis() - start;
        exchange.getAttributes().put("routingLatency", Math.max(routingLatency, 0L));
    }
}

public class RoutingPostProcessor implements BeanPostProcessor, BeanClassLoaderAware {

    private ClassLoader classLoader;

    private final MethodInterceptor methodInterceptor;

    public RoutingPostProcessor(MethodInterceptor methodInterceptor) {
        Objects.requireNonNull(methodInterceptor);
        this.methodInterceptor = methodInterceptor;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof NettyRoutingFilter) {
            ProxyFactory proxyFactory = new ProxyFactory(bean);
            proxyFactory.addAdvice(methodInterceptor);
            proxyFactory.setProxyTargetClass(true);
            return proxyFactory.getProxy(classLoader);
        }

        return bean;
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }
}