Zuul 网关记录请求全生命周期日志

微服务架构中网关是所有对外提供服务的入口,一个常见的需求是对每个请求记录详细日志,包括url、method、header、request body、response status、response body等,方便问题排查而不用每个内部服务再做一套日志了。

以下为zuul框架搭建的网关记录全链路日志的方法。

思路介绍

zuul有个处理代理请求工具类ProxyRequestHelper,用在关键的请求转发处理上。并且官方带一个子类TraceProxyRequestHelper,其中记录了不少debug信息和一些关键的钩子方法,包括我们需要的请求数据、转发模式等。因此继承这个子类,重写一些方法并把需要的信息,导出来存成日志即ok。

步骤源码

1、如上面思路介绍,新建子类并继承TraceProxyRequestHelper,具体为什么这么改可以参见注释

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.util.HTTPRequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.TraceProxyRequestHelper;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.springframework.http.HttpHeaders.CONTENT_ENCODING;
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;

/**
 * 继承zuul的实现,添加调试日志
 */
@Slf4j
public class MyTraceProxyRequestHelper extends TraceProxyRequestHelper {

    public static final String REQUEST_TRACE_DATA_KEY = "MyRequestTraceData";

    public static final String RESPONSE_TRACE_DATA_KEY = "MyResponseTraceData";

    /**
     * 在这里保存 request 的相关信息供之后导出来
     */
    @Override
    public Map<String, Object> debug(String verb, String uri, MultiValueMap<String, String> headers, MultiValueMap<String, String> params, InputStream requestEntity) throws IOException {
        Map<String, Object> result = super.debug(verb, uri, headers, params, requestEntity);
        RequestContext.getCurrentContext().getRequest().setAttribute(REQUEST_TRACE_DATA_KEY, result);
        return result;
    }

    /**
     * 在这里保存 response 的相关信息供之后导出来
     * 大部分代码复制自父类
     */
    @Override
    public void setResponse(int status, InputStream entity, MultiValueMap<String, String> headers) throws IOException {
        RequestContext context = RequestContext.getCurrentContext();
        context.setResponseStatusCode(status);

        // 记录response日志
        debugResponse(status, entity, headers);

        HttpHeaders httpHeaders = new HttpHeaders();
        for (Map.Entry<String, List<String>> header : headers.entrySet()) {
            List<String> values = header.getValue();
            for (String value : values) {
                httpHeaders.add(header.getKey(), value);
            }
        }
        boolean isOriginResponseGzipped = false;
        if (httpHeaders.containsKey(CONTENT_ENCODING)) {
            List<String> collection = httpHeaders.get(CONTENT_ENCODING);
            for (String header : collection) {
                if (HTTPRequestUtils.getInstance().isGzipped(header)) {
                    isOriginResponseGzipped = true;
                    break;
                }
            }
        }
        context.setResponseGZipped(isOriginResponseGzipped);

        for (Map.Entry<String, List<String>> header : headers.entrySet()) {
            String name = header.getKey();
            for (String value : header.getValue()) {
                context.addOriginResponseHeader(name, value);
                if (name.equalsIgnoreCase(CONTENT_LENGTH)) {
                    context.setOriginContentLength(value);
                }
                if (isIncludedHeader(name)) {
                    context.addZuulResponseHeader(name, value);
                }
            }
        }
    }

    /**
     * 新增的辅助方法,只有json等返回类型时才记录response日志
     * 判断response content-type是否是json、xml等正常接口返回类型
     */
    private boolean matchesMimeType(MultiValueMap<String, String> headers) {
        if (CollectionUtils.isEmpty(headers)) {
            return false;
        }
        String contentType = null;
        for (Map.Entry entry : headers.entrySet()) {
            if (entry.getKey().toString().equalsIgnoreCase(HttpHeaders.CONTENT_TYPE)) {
                List list = (ArrayList) entry.getValue();
                contentType = (String) list.get(0);
                break;
            }
        }
        if (StringUtils.isEmpty(contentType)) {
            return false;
        }
        String[] mimeTypes = new String[]{"text/html", "text/xml", "application/xml", "application/json"};
        for (String mimeType : mimeTypes) {
            if (contentType.contains(mimeType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 辅助方法
     * 实际记录response返回日志
     */
    private void debugResponse(int status, InputStream entity, MultiValueMap<String, String> headers) throws IOException {
        if (entity == null) {
            return;
        }

        // 非需要解析的content-type,不做处理
        RequestContext context = RequestContext.getCurrentContext();
        if (!matchesMimeType(headers)) {
            context.setResponseDataStream(entity);
            return;
        }

        byte[] entityByteArray = StreamUtils.copyToByteArray(entity);
        InputStream wrappedEntity = new ByteArrayInputStream(entityByteArray);

        //记录response
        char[] buffer = new char[4096];
        int count;
        boolean isSmallBody = true;
        try (InputStreamReader input = new InputStreamReader(wrappedEntity, StandardCharsets.UTF_8)) {
            count = input.read(buffer, 0, buffer.length);
            if (input.read() != -1) {
                isSmallBody = false;
            }
        }
        if (count > 0) {
            String resp = new String(buffer).substring(0, count);
            context.getRequest().setAttribute(RESPONSE_TRACE_DATA_KEY, (isSmallBody ? resp : resp + "<truncated>"));
        }
        wrappedEntity.reset();
        context.setResponseDataStream(wrappedEntity);
    }

}

2、启用新建好的子类。新建个配置项,将自定义的子类,设置为proxyRequestHelper这个bean的默认实现(@Primary)

import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class MyZuulAutoConfiguration extends ZuulProxyAutoConfiguration {
    @Bean
    @Primary
    public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) {
        MyTraceProxyRequestHelper helper = new MyTraceProxyRequestHelper();
        helper.setTraces(new InMemoryTraceRepository());
        helper.setIgnoredHeaders(zuulProperties.getIgnoredHeaders());
        helper.setTraceRequestBody(zuulProperties.isTraceRequestBody());
        return helper;
    }
}


3、获取日志内容并实际写入日志。这儿可以建一个spring的请求拦截器OncePerRequestFilter,记录请求耗时并在处理完毕后写入日志。

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 添加调试日志信息
 */
@Configuration
@Slf4j
public class LogFilter extends OncePerRequestFilter {

    public static final String REQUEST_BEGIN_TIME = "REQUEST_BEGIN_TIME";

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        httpServletRequest.setAttribute(REQUEST_BEGIN_TIME, System.currentTimeMillis());
        filterChain.doFilter(httpServletRequest, httpServletResponse);
        traceLog(httpServletRequest);
    }

    /**
     * 记录日志
     */
    private void traceLog(HttpServletRequest request) {
        Long begin = (Long) request.getAttribute(REQUEST_BEGIN_TIME);
        if (begin == null || begin == 0L) {
            begin = System.currentTimeMillis();
        }

        Map<String, Object> logData = new LinkedHashMap<>();
        Map<String, Object> requestMap = (Map<String, Object>) request.getAttribute(REQUEST_TRACE_DATA_KEY);
        logData.put("url", request.getRequestURL());
        logData.put("uri", request.getRequestURI());
        logData.put("timeCost", System.currentTimeMillis() - begin);
        if (requestMap != null) {
            logData.putAll(requestMap);
        }
        logData.put("responseBody", StringUtils.abbreviate(String.valueOf(request.getAttribute(RESPONSE_TRACE_DATA_KEY)), 128));

        log.info("{}", logData);
    }
}


ok了,至此大功告成!


发布于 2020/06/05 浏览