1 日志类型
日志类型 | 说明 | 面向 | 示例 |
---|---|---|---|
系统运行日志 | 系统运行过程中的监控日志。包含所有进出服务器的网络请求(如API请求、数据推送等)、内部模块普通日志 | 开发者、运维 | 如API请求、MQ、email推送、手机推送等 |
用户业务日志 | 需要供用户查询的业务日志 | 用户 | 如登录/登出、关键业务操作等 |
- 按日志类型建立不同的日志存储库(隔离存储):不同性质、存储时长不一样
- traceId贯穿整个链路,可以完整地体现数据流
2 日志流
3 系统运行日志
3.1 字段设计
- 内容字符:根据“模块类型+通讯方式”定义不同的JSON格式
3.2 内容字段设计示例
3.2.1 http
基于http API全部采用JSON格式:
- 仅记录request
Content-Type=application/json
等指定类型的API请求 - 记录response
Content-Type=application/json
等指定类型的响应数据,不记录其他Content-Type
的响应数据(如文件下载)
{
"method":"POST",
"url":"/v1/user/create",
"appId": "",
"userId": "",
"request":{
"headers":{
"<header-name1>":"value1",
"<header-name2>":"value2",
"<header-name3>":[
"value3",
"value4"
]
},
"body":"JSON字符串"
},
"response":{
"headers":{
"<header-name1>":"value1",
"<header-name2>":"value2",
"<header-name3>":[
"value3",
"value4"
]
},
"body":"JSON字符串"
}
}
- 实现示例:使用Spring ContentCachingResponseWrapper
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.taiwii.atthis.app.constant.GlobalConstant;
import com.taiwii.atthis.core.common.util.id.IdUtil;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class HttpLogFilter implements Filter {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/** 需要记录日志的Content-Type */
private static final List<String> MEDIA_TYPES = Arrays.asList(MediaType.TEXT_XML_VALUE,
MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE,
MediaType.APPLICATION_JSON_UTF8_VALUE,
MediaType.TEXT_PLAIN_VALUE);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (null == response.getContentType()) {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
}
MDC.put(GlobalConstant.TRACE_ID, IdUtil.randomUUID());
ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response);
try {
filterChain.doFilter(request, wrapper);
} finally {
saveLog(request, wrapper);
this.copyBodyToResponse(request, wrapper);
MDC.remove(GlobalConstant.TRACE_ID);
}
}
//region common
private void copyBodyToResponse(HttpServletRequest request, ContentCachingResponseWrapper wrapper) throws IOException {
if (request.isAsyncStarted()) { // 支持异步场景
request.getAsyncContext().addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
wrapper.copyBodyToResponse();
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
}
@Override
public void onError(AsyncEvent event) throws IOException {
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
}
});
} else {
wrapper.copyBodyToResponse();
}
}
//endregion
//region saveLog
private void saveLog(HttpServletRequest request, ContentCachingResponseWrapper wrapper) {
if (!this.needLog(request.getContentType())) {
return;
}
HttpLog httpLog = new HttpLog();
httpLog.setMethod(request.getMethod());
httpLog.setUrl(request.getRequestURL().toString());
httpLog.setRequest(this.getRequest(request));
httpLog.setResponse(this.getResponse(request, wrapper));
try {
log.info(OBJECT_MAPPER.writeValueAsString(httpLog));
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
}
}
private boolean needLog(String contentType) {
if (null != contentType) {
contentType = contentType.replaceAll(" ", "");
}
if (null == contentType || !MEDIA_TYPES.contains(contentType)) {
return false;
}
return true;
}
//region Request
private HttpLog.Request getRequest(HttpServletRequest request) {
String reqBody = null;
try {
reqBody = new String(request.getInputStream().readAllBytes(), "utf-8");
} catch (IOException e) {
log.error(e.getMessage(), e);
}
HttpLog.Request req = new HttpLog.Request();
req.setHeaders(this.getRequestHeaders(request));
req.setBody(reqBody);
return req;
}
private Map<String, Object> getRequestHeaders(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
request.getHeaderNames().asIterator().forEachRemaining((name) -> {
Iterator<String> iterator = request.getHeaders(name).asIterator();
List<String> list = new ArrayList<>();
while (iterator.hasNext()) {
list.add(iterator.next());
}
switch (list.size()) {
case 0:
return;
case 1:
map.put(name, list.get(0));
return;
default:
map.put(name, list);
return;
}
});
return map;
}
//endregion
//region Response
private HttpLog.Response getResponse(HttpServletRequest request, ContentCachingResponseWrapper wrapper) {
if (!this.needLog(wrapper.getContentType())) {
return null;
}
String body = null;
try {
body = new String(wrapper.getContentAsByteArray(), "utf-8");
} catch (IOException e) {
log.error(e.getMessage(), e);
}
HttpLog.Response resp = new HttpLog.Response();
resp.setHeaders(this.getResponseHeaders(wrapper));
resp.setBody(body);
return resp;
}
private Map<String, Object> getResponseHeaders(HttpServletResponse response) {
Map<String, Object> map = new HashMap<>();
response.getHeaderNames().iterator().forEachRemaining((name) -> {
Iterator<String> iterator = response.getHeaders(name).iterator();
List<String> list = new ArrayList<>();
while (iterator.hasNext()) {
list.add(iterator.next());
}
switch (list.size()) {
case 0:
return;
case 1:
map.put(name, list.get(0));
return;
default:
map.put(name, list);
return;
}
});
return map;
}
//endregion
@Data
class HttpLog implements Serializable {
private String method;
private String url;
private String appId;
private String userId;
private Request request;
private Response response;
@Data
@NoArgsConstructor
@AllArgsConstructor
static class Request implements Serializable {
private Map<String, Object> headers = Collections.emptyMap();
private String body;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class Response implements Serializable {
private Map<String, Object> headers = Collections.emptyMap();
private String body;
}
}
//endregion
}
3.2.2 MQ
{
"messageId": "",
"topic": "",
"body": ""
}
3.2.3 内部模块
- log4jlogback示例:
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss SSS}][%-5p][%t] - %logger%n%m%n" />
- log4jlogback完整配置(logback-spring.xml):
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true">
<springProperty scope="context" name="LOG_NAME" source="spring.application.name" defaultValue="log"/>
<springProperty scope="context" name="LOG_PATH" source="logging.path" defaultValue="logs"/>
<property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss SSS}][%-5p][%t] - %logger%n%m%n" />
<!-- 同时基于时间和文件大小生成日志文件:每天生成一个文件,且每个文件小于100MB(压缩前),且60天内的日志,总日志大小不超过1GB(压缩后) -->
<appender name="TIME_AND_SIZE_BASE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_NAME}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_NAME}-%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>1GB</totalSizeCap><!-- 日志文件总占用空间大小必须同时满足maxHistory和totalSizeCap -->
</rollingPolicy>
</appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<immediateFlush>true</immediateFlush>
</encoder>
</appender>
<root additivity="false" level="WARN">
<appender-ref ref="TIME_AND_SIZE_BASE_FILE" />
<appender-ref ref="STDOUT" />
</root>
</configuration>
4 用户业务日志
4.1 字段设计
4.2 实现
4.2.1 自定义注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Inherited
@Documented
public @interface BizLogger {
/** 业务类型 */
BizLogType type();
/** 日志内容 */
String content();
}
4.2.2 日志类型
import lombok.Getter;
@Getter
public enum BizLogType {
LOGIN_LOGOUT("登录/登出"),
USER("用户");
private String value;
private BizLogType(String value){
this.value = value;
}
}
4.2.3 Controller
import com.taiwii.atthis.core.bizlog.BizLog;
import com.taiwii.atthis.core.bizlog.BizLogType;
import com.taiwii.atthis.core.bizlog.aspect.BizLogger;
import com.taiwii.atthis.core.common.model.response.BizResponse;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
@RequestMapping("/v1/test")
public class TestControllerV1 {
@GetMapping("/create")
@BizLogger(type = BizLogType.USER, content = "注册新账号【用户名={0},手机号={1}】")
public BizResponse create() {
List<String> bizParams = Arrays.asList("Cooper", "13800138000");
MDC.put(BizLog.MDCKey.PARAMS, "Cooper,13800138000");
return BizResponse.newSuccessInstance("Create SUCCESS");
}
}
4.2.4 AOP处理日志
import com.taiwii.atthis.app.constant.GlobalConstant;
import com.taiwii.atthis.core.bizlog.BizLog;
import com.taiwii.atthis.core.bizlog.BizLogType;
import com.taiwii.atthis.core.bizlog.aspect.BizLogger;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;
import java.text.MessageFormat;
@Slf4j
@Aspect
@Configuration
public class BizLogAOPConfig {
@Pointcut("execution(* com.taiwii.atthis..controller..*.*(..))")
public void pointCutMethod() {
}
@Around("pointCutMethod()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
long start = System.currentTimeMillis();
BizLogger bizLogger = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(BizLogger.class);
try {
result = joinPoint.proceed();
this.saveBizLog(bizLogger, start, null);
} catch (Exception e) {
this.saveBizLog(bizLogger, start, e);
throw e;
}
return result;
}
private void saveBizLog(BizLogger bizLogger, long start, Exception e) {
if (null == bizLogger) {
return;
}
BizLogType type = bizLogger.type();
String content = bizLogger.content();
long cost = System.currentTimeMillis() - start;
String traceId = MDC.get(GlobalConstant.TRACE_ID);
String[] params = MDC.get(BizLog.MDCKey.PARAMS).split(",");
content = MessageFormat.format(content, params);
boolean success = null == e;
// traceId=6138bbd9ce1b4feda7a8136bb166e0af, type=用户, content=注册新账号【用户名=Cooper,手机号=13800138000】, cost=0 ms, success=true
log.info(String.format("traceId=%s, type=%s, content=%s, cost=%s ms, success=%s", traceId, type.getValue(), content, cost, success));
}
}
4.2.5 测试
curl -X GET localhost:8080/v1/test/create