一、 一般微服务中日志记录
1 AOP切面记录
package com.xsy.aop;
import java.io.IOException;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.xsy.entity.LoggerEntity;
/**
* 日志AOP
*
*
*/
@Aspect
@Component
public class WebLogAspect {
private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
public static final String X_USERID = "X-UserId";
public static final String X_REQUEST_ID = "X-Request-Id";
ThreadLocal<LoggerEntity> loggerEntityThreadLocal = new ThreadLocal<LoggerEntity>();
@Pointcut("execution(* com.xsy.controller..*.*(..)))")
public void webLog() {
}
/**
* 前置通知,方法调用前被调用
*
* @param joinPoint
*/
@Before(value = "webLog()")
public void doBefore(JoinPoint joinPoint) throws IOException {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 创建日志实体
LoggerEntity loggerEntity = new LoggerEntity();
// 请求开始时间
loggerEntity.setStartTime(System.currentTimeMillis());
String userId = request.getHeader(X_USERID);
loggerEntity.setUserId(userId);
String uuid = request.getHeader(X_REQUEST_ID);
loggerEntity.setUuid(uuid);
// 请求路径
String url = request.getRequestURI();
// 获取请求参数信息
String paramData = JSON.toJSONString(request.getParameterMap(),
SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue);
loggerEntity.setParamData(paramData);
// 设置客户端ip
loggerEntity.setClientIp(getCliectIp(request));
// 设置请求方法 get post
loggerEntity.setRequestMethodType(request.getMethod());
// 设置请求类型(json|普通请求)
loggerEntity.setType(getRequestType(request));
// 设置请求地址
loggerEntity.setUri(url);
String sessionId = request.getRequestedSessionId();
//设置sessionId
loggerEntity.setSessionId(sessionId);
// 请求的类及名称
loggerEntity.setClassMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
// 设置请求方法
loggerEntity.setMethod(joinPoint.getSignature().getName());
loggerEntityThreadLocal.set(loggerEntity);
// 获取形参的名称
Method method = ((MethodSignature) (joinPoint.getSignature())).getMethod();
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
Object[] arguments = joinPoint.getArgs();
for (int i = 0; i < arguments.length; i++) {
try {
logger.info(JSON.toJSONString(parameterNames[i]) + ": " + JSON.toJSONString(arguments[i]));
} catch (Exception e) {
logger.info("未知传入参数");
}
}
}
/**
* 后置返回通知 这里需要注意的是: 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数 returning
* 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
*
* @param joinPoint
* @param returnData
*/
@AfterReturning(value = "webLog()", returning = "returnData")
public void doAfterReturning(JoinPoint joinPoint, Object returnData) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
// 获取请求错误码
int status = response.getStatus();
// 获取本次请求日志实体
LoggerEntity loggerEntity = loggerEntityThreadLocal.get();
// 请求结束时间
loggerEntity.setEndTime(System.currentTimeMillis());
// 设置请求时间差
loggerEntity.setCostTime(Integer.valueOf((loggerEntity.getEndTime() - loggerEntity.getStartTime()) + ""));
// 设置返回错误码
loggerEntity.setHttpStatusCode(status + "");
// 设置返回值
loggerEntity.setReturnData(JSON.toJSONString(returnData, SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteMapNullValue));
logger.info("Common Logger Entity: " + JSON.toJSONString(loggerEntity));
}
/**
* 后置异常通知 定义一个名字,该名字用于匹配通知实现方法的一个参数名,当目标方法抛出异常返回后,将把目标方法抛出的异常传给通知方法;
* throwing 限定了只有目标方法抛出的异常与通知方法相应参数异常类型时才能执行后置异常通知,否则不执行,
* 对于throwing对应的通知方法参数为Throwable类型将匹配任何异常。
*
* @param joinPoint
* @param exception
*/
@AfterThrowing(value = "webLog()", throwing = "exception")
public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
// 目标方法名:
LoggerEntity loggerEntity = loggerEntityThreadLocal.get();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
// 请求结束时间
loggerEntity.setEndTime(System.currentTimeMillis());
// 设置请求时间差
loggerEntity.setCostTime(Integer.valueOf((loggerEntity.getEndTime() - loggerEntity.getStartTime()) + ""));
// 设置返回错误码
int status = response.getStatus();
loggerEntity.setHttpStatusCode(status + "");
//异常信息
loggerEntity.setExceptionMessage(exception.getMessage());
logger.info("Exception Logger Entity: " + JSON.toJSONString(loggerEntity));
}
@Around(value = "webLog()")
public Object doAroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = joinPoint.proceed();// 调用执行目标方法
return obj;
}
/**
* 获取客户端ip地址
* @param request
* @return
*/
public static String getCliectIp(HttpServletRequest request)
{
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个路由时,取第一个非unknown的ip
final String[] arr = ip.split(",");
for (final String str : arr) {
if (!"unknown".equalsIgnoreCase(str)) {
ip = str;
break;
}
}
return ip;
}
/**
* 判断是否为ajax请求
* @param request
* @return
*/
public static String getRequestType(HttpServletRequest request) {
return request.getHeader("X-Requested-With");
}
}
2 controller异常拦截
package com.xsy.kyz.aop;
import java.util.ArrayList;
import java.util.List;
import org.jboss.logging.Logger;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.xsy.common.constant.ResultCodeEnum;
import com.xsy.exception.XsyException;
import com.xsy.model.dto.ResultDTO;
import com.xsy.model.dto.ValidationResult;
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger log = Logger.getLogger(ControllerExceptionHandler.class);
/**
* 拦截未知的运行时异常
*
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ResultDTO<String> notFound(RuntimeException e) {
log.error("运行时异常:", e);
ResultDTO<String> dto = new ResultDTO<>();
dto.setMessage(ResultCodeEnum.FAIL.msg() + ", " + e.getMessage());
dto.setCode(ResultCodeEnum.FAIL.code());
dto.setSuccess(false);
return dto;
}
/**
* 拦截业务异常
*
* @param e
* @return
*/
@ExceptionHandler(XsyException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ResultDTO<String> notFount(XsyExceptione) {
log.error("业务异常:" + e.getMessage());
return ResultDTO.factory(e);
}
/**
* 字段校驗返回处理
*
* @param exception
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public Object MethodArgumentNotValidHandler(MethodArgumentNotValidException exception) {
ResultDTO<List<ValidationResult>> dto = new ResultDTO<>();
dto.setMessage(ResultCodeEnum.VALIDATION.msg());
dto.setCode(ResultCodeEnum.VALIDATION.code());
dto.setSuccess(false);
List<ValidationResult> list = new ArrayList<>();
for (FieldError error : exception.getBindingResult().getFieldErrors()) {
ValidationResult result = new ValidationResult();
result.setMessage(error.getDefaultMessage());
result.setParam(error.getField());
list.add(result);
}
dto.setData(list);
return dto;
}
}
package com.pingan.kyz.model.dto.kyz;
import com.xsy.common.constant.OperationEnum;
import com.xsy.common.constant.ResultCodeEnum;
import com.xsy.exception.XsyException;
import java.io.Serializable;
public class ResultDTO<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 标志
*/
private boolean success;
/**
* 状态码
*/
private Integer code;
/**
* message
*/
private String message;
/**
* 数据
*/
private T data;
public ResultDTO() {
}
public ResultDTO(boolean success) {
this(success, null);
}
public ResultDTO(String message) {
this(true, null, message);
}
public ResultDTO(boolean success, T data) {
this(success, data, null);
}
public ResultDTO(boolean success, T data, String message) {
this.success = success;
this.code = success ? ResultCodeEnum.SUCCESS.code() : ResultCodeEnum.FAIL.code();
this.data = data;
this.message = message == null ? success ? ResultCodeEnum.SUCCESS.msg() : ResultCodeEnum.FAIL.msg() : message;
}
/**
* 静态工厂
*
* @param operation
* @param <T>
* @return
*/
public static <T> ResultDTO<T> resultFactory(OperationEnum operation) {
ResultDTO<T> dto = null;
switch (operation) {
case DELETE_SUCCESS:
case INSERT_SUCCESS:
case UPDATE_SUCCESS:
case OPERATION_SUCCESS:
dto = new ResultDTO<>(true, null, operation.getMessage());
break;
case INSERT_ERROR:
case DELETE_ERROR:
case UPDATE_ERROR:
case OPERATION_ERROR:
dto = new ResultDTO<>(false, null, operation.getMessage());
break;
default:
new ResultDTO<>(true, null);
break;
}
return dto;
}
/**
* 静态工厂
*
* @param codeEnum
* @param <T>
* @return
*/
public static <T> ResultDTO<T> factory(ResultCodeEnum codeEnum) {
ResultDTO<T> dto = new ResultDTO<T>();
if (codeEnum == ResultCodeEnum.SUCCESS) {
dto.setSuccess(true);
} else {
dto.setSuccess(false);
}
dto.setCode(codeEnum.code());
dto.setMessage(codeEnum.msg());
return dto;
}
public static <T> ResultDTO<T> factory(IcasException exception) {
ResultDTO<T> dto = new ResultDTO<T>();
if (exception.getCode() == 200) {
dto.setSuccess(true);
} else {
dto.setSuccess(false);
}
dto.setCode(exception.getCode());
dto.setMessage(exception.getMessage());
return dto;
}
public static <T> ResultDTO<T> factory(T t) {
return new ResultDTO<>(true, t);
}
/**
* 成功
*
* @param message
* @param <T>
* @return
*/
public static <T> ResultDTO<T> success(String message) {
return new ResultDTO<T>(true, null, message);
}
/**
* 失败
*
* @param message
* @param <T>
* @return
*/
public static <T> ResultDTO<T> fail(String message) {
return new ResultDTO<T>(false, null, message);
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
二、网关zuul微服务日志处理
1 熔断情况下日志记录
package com.xsy.zuul.controller;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.jboss.logging.Logger;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.context.RequestContext;
@Component
public class SrpingCLoudFallBack implements FallbackProvider {
private static final Logger log = Logger.getLogger(SrpingCLoudFallBack.class);
public static final String X_USERID = "X-UserId";
public static final String X_REQUEST_ID = "X-Request-Id";
@Override
public String getRoute() {
//api服务id,如果需要所有调用都支持回退,则return "*"或return null
return "service-name";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LoggerEntity loggerEntity = new LoggerEntity();
String requestMethodName = request.getRequestURI();// /service-name/hi3
String uri = request.getRequestURI();
String paramData = JSON.toJSONString(request.getParameterMap());
String clientIp = getCliectIp(request);
String userId = request.getHeader(X_USERID);
String uuid = request.getHeader(X_REQUEST_ID);
loggerEntity.setServiceName(route);
loggerEntity.setClientIp(clientIp);
loggerEntity.setMethod(requestMethodName);
loggerEntity.setParamData(paramData);
loggerEntity.setUri(uri);
loggerEntity.setUserId(userId);
loggerEntity.setUuid(uuid);
String exceptionMessage = cause.getCause().getMessage();
loggerEntity.setExceptionMessage(exceptionMessage);
log.info("Zuul Logger Entity: " + JSON.toJSONString(loggerEntity));
return new ClientHttpResponse() {
/**
* 网关向api服务请求是失败了,但是消费者客户端向网关发起的请求是OK的, 不应该把api 的404,500等问题抛给客户端
* 网关和api服务集群对于客户端来说是黑盒子
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
@Override
public void close() {
}
/**
* 当 service-name 微服务出现宕机后,客户端再请求时候就会返回 fallback
* 等字样的字符串提示;
*
* 但对于复杂一点的微服务,我们这里就得好好琢磨该怎么友好提示给用户了;
*
* 如果请求用户服务失败,返回什么信息给消费者客户端
*
* @return
* @throws IOException
*/
@Override
public InputStream getBody() throws IOException {
JSONObject r = new JSONObject();
try {
r.put("state", "9999");
r.put("msg", "路由请求失败,请查看日志,这是Fallback提示,");
} catch (JSONException e) {
e.printStackTrace();
}
return new ByteArrayInputStream(r.toString().getBytes("UTF-8"));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
// 和body中的内容编码一致,否则容易乱码
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
};
}
public static String getCliectIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个路由时,取第一个非unknown的ip
final String[] arr = ip.split(",");
for (final String str : arr) {
if (!"unknown".equalsIgnoreCase(str)) {
ip = str;
break;
}
}
return ip;
}
}
2 通过filter记录异常情况下及一般情况下的日志信息
package com.xsy.zuul.Filter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.util.StreamUtils;
import com.xsy.zuul.controller.LoggerEntity;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
public class PostFilter extends ZuulFilter {
private Logger log = LoggerFactory.getLogger(PostFilter.class);
public static final String DEFAULT_ERR_MSG = "系统繁忙,请稍后再试";
public static final String X_USERID = "X-UserId";
public static final String X_REQUEST_ID = "X-Request-Id";
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
// RequestContext ctx = RequestContext.getCurrentContext();
// return !"200".equals(ctx.get("responseStatusCode"));
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
LoggerEntity loggerEntity = new LoggerEntity();
String requestMethodName = request.getRequestURI();// /service-name/hi3
String uri = request.getRequestURI();
String paramData = JSON.toJSONString(request.getParameterMap());
String clientIp = getCliectIp(request);
String userId = request.getHeader(X_USERID);
String uuid = request.getHeader(X_REQUEST_ID);
loggerEntity.setClientIp(clientIp);
loggerEntity.setMethod(requestMethodName);
loggerEntity.setParamData(paramData);
loggerEntity.setUri(uri);
loggerEntity.setUserId(userId);
loggerEntity.setUuid(uuid);
try {
InputStream out = ctx.getResponseDataStream();
String outBody = StreamUtils.copyToString(out, Charset.forName("UTF-8"));
if (outBody != null) {
log.info("response body:\t" + outBody);
int status = ctx.getResponse().getStatus();
if (status != 200) {
HashMap<String, String> excMap = new HashMap<String, String>();
excMap = getExceptionMsg(outBody);
loggerEntity.setExceptionMessage(excMap.get("msg"));
loggerEntity.setServiceName(excMap.get("serviceName"));
} else {
loggerEntity.setReturnData(outBody);
}
ctx.setResponseBody(outBody);
}
} catch (IOException e2) {
e2.printStackTrace();
}
log.info("Zuul Logger Entity: " + JSON.toJSONString(loggerEntity));
return null;
}
public static HashMap<String, String> getExceptionMsg(String htmlBody) {
// String htmlBody = "<html><body><h1>Whitelabel Error Page</h1><p>This
// application has no explicit mapping for /error, so you are seeing
// this as a fallback.</p><div id='created'>Thu Feb 21 11:11:05 CST
// 2019</div><div>There was an unexpected error (type=Internal Server
// Error, status=500).</div><div>Exception type is
// RuntimeException.</div></body></html>";
HashMap<String, String> excMap = new HashMap<String, String>();
Document doc = Jsoup.parse(htmlBody);
Elements divs = doc.getElementsByTag("div");
Element excep = divs.last();
if (excep != null) {
String msg = excep.html();
excMap = JSON.parseObject(msg, HashMap.class);
return excMap;
} else {
excMap.put("msg", htmlBody);
}
return excMap;
}
public static String getCliectIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.trim() == "" || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个路由时,取第一个非unknown的ip
final String[] arr = ip.split(",");
for (final String str : arr) {
if (!"unknown".equalsIgnoreCase(str)) {
ip = str;
break;
}
}
return ip;
}
}
package com.xsy.zuul.Filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
public class ErrorFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(ErrorFilter.class);
@Override
public String filterType() {
//异常过滤器
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
//优先级,数字越大,优先级越低
return 0;
}
@Override
public boolean shouldFilter() {
//是否执行该过滤器,true代表需要过滤
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
log.info("进入异常过滤器");
System.out.println(ctx.getResponseBody());
ctx.setResponseBody("出现异常");
return null;
}
}