一 引言
最近項(xiàng)目在線上運(yùn)行出現(xiàn)了一些難以復(fù)現(xiàn)的bug需要定位相應(yīng)api的日志,通過nginx提供的api請求日志難以實(shí)現(xiàn),于是在gateway通過全局過濾器記錄api請求日志。
二 實(shí)現(xiàn)邏輯
- 接受到用戶請求后,經(jīng)過全局過濾器,檢驗(yàn)是否開啟相應(yīng)的日志配置及相應(yīng)的黑白名單配置
- 在gateway前置處理如記錄當(dāng)前請求開始時間,請求url,請求方法等
- 在gateway后置處理中獲得相應(yīng)的請求結(jié)果,響應(yīng)狀態(tài)碼
- 記錄當(dāng)前請求日志,根據(jù)實(shí)際需求,通過mq異步處理工具持久化相應(yīng)的日志(本案列作處理)
- 診斷請求結(jié)果,對請求異常,慢api等推送相應(yīng)的消息給研發(fā)人人員
三 代碼實(shí)現(xiàn)
定義相應(yīng)的配置類
@Data
@RefreshScope
@Component
@ConfigurationProperties(prefix = LogProperties.PREFIX)
public class LogProperties {
public static final String PREFIX = "config.gateway.log.access";
/**
* 是否開啟日志打印
*/
private Boolean enabled = true;
/**
* 忽略的pattern
*/
private List<String> ignoredPatterns;
private ApiAlarmConfiguration fail = new ApiAlarmConfiguration();
private SlowApiAlarmConfiguration slow = new SlowApiAlarmConfiguration();
/**
* 慢API報警配置
*/
@Data
public static class SlowApiAlarmConfiguration {
/**
* 是否開啟API慢日志打印
*/
private boolean alarm = true;
/**
* 報警閾值 (單位:毫秒)
*/
private long threshold = 500;
}
/**
* API異常報警(根據(jù)http狀態(tài)碼判定)
*/
@Data
public static class ApiAlarmConfiguration {
/**
* 是否開啟異常報警 默認(rèn)關(guān)閉
*/
private boolean alarm = false;
/**
* 排除狀態(tài)碼
*/
private List<Integer> exclusion;
}
}
定義log實(shí)體
@Data
public class GatewayLog implements Serializable {
private static final long serialVersionUID = -3205904134722576668L;
/**
* 訪問實(shí)例
*/
private String targetServer;
/**
* 請求路徑
*/
private String requestPath;
/**
* 請求與方法
*/
private String method;
/**
* 請求協(xié)議
*/
private String schema;
/**
* 請求ip
*/
private String ip;
/**
* 請求時間
*/
private Date requestTime;
/**
* 請求參數(shù)
*/
private Map<String,String> queryParams;
/**
* 請求體
*/
private String requestBody;
/**
* 請求執(zhí)行時間
*/
private Long executeTime;
/**
* 請求類型
*/
private String requestContentType;
/**
* 相應(yīng)狀態(tài)碼
*/
private int code;
}
定義相應(yīng)日志工廠及常量文章來源:http://www.zghlxwxcb.cn/news/detail-506659.html
public interface GatewayLogType {
/**
* 常規(guī)輸出
*/
String APPLICATION_JSON_REQUEST = "applicationJsonRequest";
String FORM_DATA_REQUEST = "formDataRequest";
String BASIC_REQUEST = "basicRequest";
String NORMAL_REQUEST = "normalRequest";
/**
* 慢查詢
*/
String SLOW = "slow";
/**
* 非200響應(yīng)
*/
String FAIL = "fail";
}
@Slf4j
public class GatewayLogInfoFactory {
public static void log(String type, GatewayLog gatewayLog){
switch (type){
case GatewayLogType.APPLICATION_JSON_REQUEST:
log.info("[{}] {} {},route: {},status: {},excute: {} mills,requestBody: {}"
,gatewayLog.getIp()
,gatewayLog.getMethod()
,gatewayLog.getRequestPath()
,gatewayLog.getTargetServer()
,gatewayLog.getCode()
,gatewayLog.getExecuteTime()
,StrUtil.replace(gatewayLog.getRequestBody(), StrPool.LF,"")
);
break;
case GatewayLogType.FORM_DATA_REQUEST:
log.info("[{}] {} {},route: {},status: {},excute: {} mills,requestBody: {}"
,gatewayLog.getIp()
,gatewayLog.getMethod()
,gatewayLog.getRequestPath()
,gatewayLog.getTargetServer()
,gatewayLog.getCode()
,gatewayLog.getExecuteTime()
,StrUtil.replace(gatewayLog.getRequestBody(), StrPool.LF,"")
);
break;
case GatewayLogType.BASIC_REQUEST:
log.info("[{}] {} {},route: {},status: {},excute: {} mills,requestBody: {}"
,gatewayLog.getIp()
,gatewayLog.getMethod()
,gatewayLog.getRequestPath()
,gatewayLog.getTargetServer()
,gatewayLog.getCode()
,gatewayLog.getExecuteTime()
,StrUtil.replace(gatewayLog.getRequestBody(), StrPool.LF,"")
);
break;
case GatewayLogType.NORMAL_REQUEST:
log.info("[{}] {} {},route: {},status: {},excute: {} mills,queryParams: {}"
,gatewayLog.getIp()
,gatewayLog.getMethod()
,gatewayLog.getRequestPath()
,gatewayLog.getTargetServer()
,gatewayLog.getCode()
,gatewayLog.getExecuteTime()
,gatewayLog.getQueryParams()
);
break;
default:
break;
}
}
}
定義日志全局過濾器文章來源地址http://www.zghlxwxcb.cn/news/detail-506659.html
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.StrUtil;
import com.zkjc.xlcp.common.notifier.core.Notifier;
import com.zkjc.xlcp.gateway.log.GatewayLogInfoFactory;
import com.zkjc.xlcp.gateway.log.LogProperties;
import com.zkjc.xlcp.gateway.log.constant.GatewayLogType;
import com.zkjc.xlcp.gateway.log.entity.GatewayLog;
import com.zkjc.xlcp.gateway.util.IpUtil;
import io.netty.util.internal.StringUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.multipart.FormFieldPart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.*;
/**
* @author likun
* @date 2022年10月24日 9:52
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class AccessLogFilter implements GlobalFilter, Ordered {
private final LogProperties logProperties;
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
/**
* default HttpMessageReader.
*/
private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final Notifier notifier;
/*
* 在CncloudRequestGlobalFilter后面執(zhí)行 先清洗url在進(jìn)行路徑的日志的打印
* */
@Override
public int getOrder() {
return -100;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 判斷是否打開相應(yīng)是日志配置 ingore配置校驗(yàn)
if (!logProperties.getEnabled()||hasIgnoredFlag(exchange,logProperties)){
return chain.filter(exchange);
}
// 獲得請求上下文
GatewayLog gatewayLog = parseGateway(exchange);
ServerHttpRequest request = exchange.getRequest();
MediaType mediaType = request.getHeaders().getContentType();
if (Objects.isNull(mediaType)){
return writeNormalLog(exchange,chain,gatewayLog);
}
gatewayLog.setRequestContentType(mediaType.getType() + "/" + mediaType.getSubtype());
// 對不同的請求類型做相應(yīng)的處理
if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){
return writeBodyLog(exchange,chain,gatewayLog);
}else if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(mediaType) || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)){
return readFormData(exchange,chain,gatewayLog);
}else {
return writeBasicLog(exchange,chain,gatewayLog);
}
}
/**
* 校驗(yàn)白名單
* @param exchange
* @param logProperties
* @return
*/
private Boolean hasIgnoredFlag(ServerWebExchange exchange,LogProperties logProperties){
List<String> ignoredPatterns = logProperties.getIgnoredPatterns();
if (CollectionUtil.isEmpty(ignoredPatterns)){
return Boolean.FALSE;
}
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
for (String pattern : ignoredPatterns) {
if (antPathMatcher.match(pattern,uri.getPath())){
return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
/**
* 生成相應(yīng)的報告并推送qq郵箱消息
*/
private void report(GatewayLog gatewayLog){
if (notifier==null){
return;
}
boolean reported = exceptionReport(gatewayLog);
if (!reported){
slowApiReport(gatewayLog);
}
}
/**
* 異常報警
* @param gatewayLog
* @return
*/
private Boolean exceptionReport(GatewayLog gatewayLog){
int code = gatewayLog.getCode();
if (code==HttpStatus.OK.value()){
return Boolean.FALSE;
}
LogProperties.ApiAlarmConfiguration apiAlarmConfiguration = logProperties.getFail();
if (!apiAlarmConfiguration.isAlarm()){
log.debug("api exception alarm disabled.");
return Boolean.FALSE;
}
if (!CollectionUtils.isEmpty(apiAlarmConfiguration.getExclusion()) && apiAlarmConfiguration.getExclusion().contains(code)) {
log.debug("status [{}] excluded.", code);
return Boolean.FALSE;
}
String alarmContent = String.format("【API異常】 請求ip:[{%s}],請求路由:[{%s}],請求地址:[{%s}],返回狀態(tài)碼:[{%d}],執(zhí)行時間:%d ms",gatewayLog.getIp(),gatewayLog.getTargetServer(),gatewayLog.getRequestPath(),code,gatewayLog.getExecuteTime());
notifier.notify(alarmContent);
return Boolean.TRUE;
}
private Boolean slowApiReport(GatewayLog gatewayLog){
LogProperties.SlowApiAlarmConfiguration slowApiAlarmConfiguration = logProperties.getSlow();
long threshold = slowApiAlarmConfiguration.getThreshold();
if (gatewayLog.getExecuteTime()<threshold){
return Boolean.FALSE;
}
if (!slowApiAlarmConfiguration.isAlarm()) {
log.debug("slow api alarm disabled.");
return Boolean.FALSE;
}
String slowContent = String.format("【API執(zhí)行時間過長,超過設(shè)定閾值】 請求ip:[{%s}],請求路由:[{%s}],請求地址:[{%s}],執(zhí)行時間:%d ms",gatewayLog.getIp(),gatewayLog.getTargetServer(),gatewayLog.getRequestPath(),gatewayLog.getExecuteTime());
notifier.notify(slowContent);
return Boolean.TRUE;
}
/**
* 獲得當(dāng)前請求分發(fā)的路由
* @param exchange
* @return
*/
private Route getGatewayRoute(ServerWebExchange exchange) {
return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
}
private GatewayLog parseGateway(ServerWebExchange exchange){
ServerHttpRequest request = exchange.getRequest();
String requestPath = request.getPath().pathWithinApplication().value();
Route route = getGatewayRoute(exchange);
String ip = IpUtil.getIpAddress(request);
GatewayLog gatewayLog = new GatewayLog();
gatewayLog.setSchema(request.getURI().getScheme());
gatewayLog.setMethod(request.getMethodValue());
gatewayLog.setRequestPath(requestPath);
gatewayLog.setTargetServer(route.getId());
gatewayLog.setIp(ip);
gatewayLog.setRequestTime(new Date());
return gatewayLog;
}
private Mono writeNormalLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog){
return chain.filter(exchange).then(Mono.fromRunnable(()->{
ServerHttpResponse response = exchange.getResponse();
int value = response.getStatusCode().value();
gatewayLog.setCode(value);
long executeTime = DateUtil.between(gatewayLog.getRequestTime(), new Date(), DateUnit.MS);
gatewayLog.setExecuteTime(executeTime);
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
Map<String, String> paramsMap = new HashMap<>();
if (CollectionUtil.isNotEmpty(queryParams)) {
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
paramsMap.put(entry.getKey(), StrUtil.join(StrPool.COMMA,entry.getValue()));
}
}
gatewayLog.setQueryParams(paramsMap);
GatewayLogInfoFactory.log(GatewayLogType.NORMAL_REQUEST,gatewayLog);
// 推送相應(yīng)的報告
report(gatewayLog);
}));
}
/**
* 解決 request body 只能讀取一次問題,
* 參考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
* @param exchange
* @param chain
* @param gatewayLog
* @return
*/
@SuppressWarnings("unchecked")
private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
gatewayLog.setRequestBody(body);
return Mono.just(body);
});
// 通過 BodyInserter 插入 body(支持修改body), 避免 request body 只能獲取一次
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> { // 重新封裝請求
ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage); // 記錄響應(yīng)日志
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog); // 記錄普通的
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> { // 打印日志
GatewayLogInfoFactory.log(GatewayLogType.APPLICATION_JSON_REQUEST,gatewayLog);
// 推送相應(yīng)的報告
report(gatewayLog);
}));
}));
}
/**
* 讀取form-data數(shù)據(jù)
* @param exchange
* @param chain
* @param accessLog
* @return
*/
private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
@Override
public MultiValueMap<String, String> getQueryParams() {
return UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build().getQueryParams();
}
};
final HttpHeaders headers = exchange.getRequest().getHeaders();
if (headers.getContentLength() == 0) {
return chain.filter(exchange);
}
ResolvableType resolvableType;
if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(headers.getContentType())) {
resolvableType = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);
} else {
//解析 application/x-www-form-urlencoded
resolvableType = ResolvableType.forClass(String.class);
}
return MESSAGE_READERS.stream().filter(reader -> reader.canRead(resolvableType, mutatedRequest.getHeaders().getContentType())).findFirst().orElseThrow(() -> new IllegalStateException("no suitable HttpMessageReader.")).readMono(resolvableType, mutatedRequest, Collections.emptyMap()).flatMap(resolvedBody -> {
if (resolvedBody instanceof MultiValueMap) {
LinkedMultiValueMap map = (LinkedMultiValueMap) resolvedBody;
if (CollectionUtil.isNotEmpty(map)) {
StringBuilder builder = new StringBuilder();
final Part bodyPartInfo = (Part) ((MultiValueMap) resolvedBody).getFirst("body");
if (bodyPartInfo instanceof FormFieldPart) {
String body = ((FormFieldPart) bodyPartInfo).value();
builder.append("body=").append(body);
}
accessLog.setRequestBody(builder.toString());
}
} else {
accessLog.setRequestBody((String) resolvedBody);
}
//獲取響應(yīng)體
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
return chain.filter(exchange.mutate().request(mutatedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> { // 打印日志
// 打印響應(yīng)的日志
GatewayLogInfoFactory.log(GatewayLogType.FORM_DATA_REQUEST,accessLog);
// 推送相應(yīng)的報告
report(accessLog);
}));
});
});
}
private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
DataBufferUtils.retain(dataBuffer);
final Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
final ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
@Override
public MultiValueMap<String, String> getQueryParams() {
return UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).build().getQueryParams();
}
};
StringBuilder builder = new StringBuilder();
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
if (CollectionUtil.isNotEmpty(queryParams)) {
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
builder.append(entry.getKey()).append("=").append(entry.getValue()).append(StrPool.COMMA);
}
}
accessLog.setRequestBody(builder.toString()); //獲取響應(yīng)體
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
return chain.filter(exchange.mutate().request(mutatedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> { // 打印日志
GatewayLogInfoFactory.log(GatewayLogType.BASIC_REQUEST,accessLog);
// 推送相應(yīng)的報告
report(accessLog);
}));
});
}
/**
* 請求裝飾器,重新計算 headers
* @param exchange
* @param headers
* @param outputMessage
* @return
*/
private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
/**
* 記錄響應(yīng)日志
* 通過 DataBufferFactory 解決響應(yīng)體分段傳輸問題。
*/
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
return new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
// 計算執(zhí)行時間
long executeTime = DateUtil.between(gatewayLog.getRequestTime(), new Date(), DateUnit.MS);
gatewayLog.setExecuteTime(executeTime);
// 獲取響應(yīng)類型,如果是 json 就打印
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);//
gatewayLog.setCode(this.getStatusCode().value());
//
if (Objects.equals(this.getStatusCode(), HttpStatus.OK)
&& !StringUtil.isNullOrEmpty(originalResponseContentType)
&& originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 合并多個流集合,解決返回體分段傳輸
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
// 釋放掉內(nèi)存
join.read(content);
DataBufferUtils.release(join);
return bufferFactory.wrap(content);
}));
}else {
}
}
return super.writeWith(body);
}
};
}
}
到了這里,關(guān)于基于Spring-cloud-gateway實(shí)現(xiàn)全局日志記錄的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!