概述
没有调试过线上bug的人学不会打log
1. Object… arguments
从slf4j-1.6.0开始,public void error(String format, Object... arguments);
中arguments的最后一个参数如果是throwable对象,将会被作为异常信息进行打印。
slf4j-1.6.0以前,只能通过public void error(String msg, Throwable t);
打印异常信息,缺点是必须通过拼接字符串的形式把arguments组装为msg。
2. MDC的使用
本节内容摘取自:Slf4j MDC 使用和 基于 Logback 的实现分析(感谢原作者)
有了日志之后,我们就可以追踪各种线上问题。但是,在分布式系统中,各种无关日志穿行其中,导致我们可能无法直接定位整个操作流程。因此,我们可能需要对一个用户的操作流程进行归类标记,比如使用线程+时间戳,或者用户身份标识等;如此,我们可以从大量日志信息中grep出某个用户的操作流程,或者某个时间的流转记录。
MDC ( Mapped Diagnostic Contexts ),顾名思义,其目的是为了便于我们诊断线上问题而出现的方法工具类。虽然,Slf4j 是用来适配其他的日志具体实现包的,但是针对 MDC功能,目前只有logback 以及 log4j 支持。
在日志模板中,使用 %X{ }来占位,替换到对应的 MDC 中 key 的值。
看一个MDC使用的简单示例:
public class LogTest {
private static final Logger logger = LoggerFactory.getLogger(LogTest.class);
public static void main(String[] args) {
MDC.put("THREAD_ID", String.valueOf(Thread.currentThread().getId()));
logger.info("纯字符串信息的info级别日志");
}
}
logback的输出模板配置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.base" value="${catalina.base}/logs" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>[%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5p) %logger.%M(%F:%L)] %X{THREAD_ID} %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console" />
</root>
</configuration>
于是,就有了输出:
[2015-04-30 15:34:35 INFO io.github.ketao1989.log4j.LogTest.main(LogTest.java:29)] 1 纯字符串信息的info级别日志
3. 日志文件的分类
STDOUT:控制台输出日志
RollingFile:完整的日志文件
ErrorFile:保存系统报错
MainFile:保存系统一些关键日志,便于搜索
4. 常用配置
工具类:
package com.example;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
/**
* 日志工具类
*
* @author frcoder
*/
public class LogUtil {
/**
* 记录用户行为
*/
public static Logger userLog = LoggerFactory.getLogger("@USER");
public static void newUserLog(String somethings) {
MDC.put("USER", "");
MDC.put("DO", somethings);
userLog.debug("...");
}
public static void newUserLog(Object userId, String somethings) {
MDC.put("USER", userId.toString());
MDC.put("DO", somethings);
userLog.debug("...");
}
public static void newUserLog(Object userId, Object role, String somethings) {
MDC.put("USER", UserRole.toRoleString((Integer) role) + ":" + userId.toString());
MDC.put("DO", somethings);
userLog.debug("...");
}
public static void userQuit() {
MDC.clear();
}
}
配置:
Configuration:
# Internal Log4j events level
status: warn
# Automatic Reconfiguration, unit: second
monitorInterval: 300
dest: err
name: YAMLConfig
properties:
property:
-
name: projectName
value: me
-
name: logHome
value: /tmp/logs
thresholdFilter:
level: debug
appenders:
## Console appender
Console:
name: STDOUT
PatternLayout:
Pattern: "%d %-5p %c [%L] [%t] [%X{USER}:%X{DO}] - %m%n"
## RollingFile appender
RollingRandomAccessFile:
-
name: RollingFile
filename: "${logHome}/${projectName}.log"
filePattern: "${logHome}/${projectName}.%d{yyyy-MM-dd}-%i.log.gz"
PatternLayout:
Pattern: "%d %-5p %c [%L] [%t] [%X{USER}:%X{DO}] - %m%n"
Policies:
SizeBasedTriggeringPolicy:
size: 20MB
DefaultRollOverStrategy:
max: 5
Delete:
basePath: "/tmp/logs"
maxDepth: 1
IfFileName:
glob: "epg*.log.*"
IfLastModified:
age: 5d
-
name: ErrorFile
filename: "${logHome}/${projectName}-error.log"
filePattern: "${logHome}/${projectName}-error.%d{yyyy-MM-dd}-%i.log.gz"
PatternLayout:
Pattern: "%d %-5p %c [%L] [%t] [%X{USER}:%X{DO}] - %m%n"
Policies:
SizeBasedTriggeringPolicy:
size: 20MB
DefaultRollOverStrategy:
max: 5
-
name: MainFile
filename: "${logHome}/${projectName}-main.log"
filePattern: "${logHome}/${projectName}-main.%d{yyyy-MM-dd}-%i.log.gz"
thresholdFilter:
level: debug
PatternLayout:
Pattern: "%d %-5p %c [%L] [%t] [%X{USER}:%X{DO}] - %m%n"
Policies:
SizeBasedTriggeringPolicy:
size: 20MB
DefaultRollOverStrategy:
max: 5
Loggers:
logger:
-
name: com.example
level: debug
additivity: false
AppenderRef:
- ref: MainFile
- ref: STDOUT
-
name: "@USER"
level: debug
additivity: false
AppenderRef:
- ref: MainFile
- ref: STDOUT
-
name: org.hibernate.SQL
level: warn
Root:
level: info
AppenderRef:
- ref: STDOUT
level: error
- ref: RollingFile
level: info
- ref: ErrorFile
level: error
注意:上面代码中的additivity属性(false:只在本logger中输出,不要传递给上级logger;true:不仅在本logger中输出,也会传递给上级,如果本logger和上级logger都指向同一个日志文件,则日志可能会在该文件中打印2次。)
特别提示:各个level的优先级
thresholdFilter:总开关,低于这个级别的日志都不会显示
logger下:logger.level和logger.AppenderRef.level的级别取最低值
5. 日志与行为
一般在行为完成之后才打日志,看到日志就表示该行为已完成。
6. Response模板类
package com.example;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
/**
* Response模板类
*
* @author frcoder
*/
@ApiModel(value = "Response", description = "接口响应对象")
public class Response<T> {
@ApiModelProperty(value = "编码")
@JsonProperty("code")
private int code;
@ApiModelProperty(value = "消息")
@JsonProperty("message")
private String message;
@ApiModelProperty(value = "数据")
@JsonProperty("data")
private T data;
@JsonIgnore
private Exception exception;
public static <T> Response<T> ok() {
return ok(null, "success");
}
public static <T> Response<T> ok(T data) {
return ok(data, "success");
}
public static <T> Response<T> ok(T data, String message) {
return ok(0, data, message);
}
public static <T> Response<T> ok(Integer code, T data, String message) {
return new Response(code, message, data);
}
public static <T> Response<T> fail(String message) {
return fail(99, message);
}
public static <T> Response<T> fail(Integer code, String message) {
return new Response(code, message);
}
public static <T> Response<T> failParam(String message) {
return fail(400, message);
}
public static <T> Response<T> error(String message, Exception e) {
return error(99, message, e);
}
public static <T> Response<T> error(Integer code, String message, Exception e) {
return new Response(code, message).exception(e);
}
public Response() {
}
public Response(int code) {
this.code = code;
}
public Response(int code, String message) {
this.code = code;
this.message = message;
}
public Response(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public Response code(int code) {
this.code = code;
return this;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Response message(String message) {
this.message = message;
return this;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Response data(T data) {
this.data = data;
return this;
}
public Exception getException() {
return exception;
}
public void setException(Exception exception) {
this.exception = exception;
}
public Response exception(Exception exception) {
this.exception = exception;
return this;
}
}
7. 注解与切面在日志中的应用
1. 在gradle中引入jar包
compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"
2. 编写注解类
package com.example;
import java.lang.annotation.*;
/**
* @Log注解类
*
* @author frcoder
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String value() default "";
}
package com.example;
import com.example.Response;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import static com.example.LogUtil.userLog;
/**
* LogAop日志切面类
*
* @author frcoder
*/
@Aspect
@Component
public class LogAop {
private static Logger logger = LoggerFactory.getLogger(LogAop.class);
/**
* api.impl包下的函数,如果参数列表是以userId, role开头的会被记录用户日志
*/
@Pointcut("execution(public * com..api.impl..*.*(..)) || @annotation(Log))")
public void log() {
}
@Before(value = "log()")
public void doBeforeLog(JoinPoint joinPoint) {
try {
String methodName = joinPoint.getSignature().getName();
Map args = AOPUtil.getArgs(joinPoint);
if (args.containsKey("userId")) {
if (args.containsKey("role")) {
LogUtil.newUserLog(args.get("userId"), args.get("role"), methodName);
} else {
LogUtil.newUserLog(args.get("userId"), methodName);
}
} else {
LogUtil.newUserLog(methodName);
}
} catch (Exception e) {
logger.debug("LogAop doBefore new Log is wrong", e);
}
}
@AfterReturning(value = "log()", returning = "ret")
public void doAfterReturningLog(Object ret) {
try {
Response response = (Response) ret;
userLog.info("[{}]: {}", response.getCode(), response.getMessage());
if (response.getData() != null) {
userLog.debug(StringUtil.Obj2JsonStr(response.getData()));
}
if (response.getException() != null) {
userLog.error(response.getException().toString(), response.getException());
}
} catch (Exception e) {
logger.debug("LogAop doAfterReturning is wrong", e);
} finally {
LogUtil.userQuit();
}
}
}
package com.example;
import org.aspectj.lang.JoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* AOP工具类
*
* @author frcoder
*/
public class AOPUtil {
private static Logger logger = LoggerFactory.getLogger(AOPUtil.class);
/**
* 用于提取切入点参数
*/
public static Map getArgs(JoinPoint joinPoint) {
try {
String classType = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
// 获取参数值
Object[] args = joinPoint.getArgs();
Class<?>[] classes = new Class[args.length];
for (int k = 0; k < args.length; k++) {
if (!args[k].getClass().isPrimitive()) {
// 获取的是封装类型而不是基础类型
String result = args[k].getClass().getName();
Class s = map.get(result);
classes[k] = s == null ? args[k].getClass() : s;
}
}
// 获取方法(第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型)
Method method = Class.forName(classType).getMethod(methodName, classes);
// 获取参数名
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
String[] parameterNames = pnd.getParameterNames(method);
// 通过map封装参数名和参数值
HashMap<String, Object> paramMap = new HashMap();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
return paramMap;
} catch (Exception e) {
logger.error("提取切入点参数出错", e);
}
return Collections.EMPTY_MAP;
}
private static HashMap<String, Class> map = new HashMap<String, Class>() {
{
put("java.lang.Integer", Integer.class);
put("java.lang.Double", Double.class);
put("java.lang.Float", Float.class);
put("java.lang.Long", Long.class);
put("java.lang.Short", Short.class);
put("java.lang.Boolean", Boolean.class);
put("java.lang.Char", Character.class);
}
};
}
最后
以上就是慈祥棉花糖为你收集整理的关于日志(slf4j的使用心得)的全部内容,希望文章能够帮你解决关于日志(slf4j的使用心得)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复