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
浏览
次