SpringBoot2.x整合logback日志框架(2)—layout和MDC机制

JAVA && Spring && SpringBoot2.x — 学习目录

如何根据日志文件快速定位到应用的行为。

1. layout

Layout组件:([累奥特] 布局),将日志事件进行格式化,返回一个字符串

1.1 常用的转换词

转换词 备注
C{length},class{length} 输出发出日志请求调用者的完整类名。注:类名最右边部分不会省略,即使长度超过了设定了length长度。
contextName 日志记录器上下文的名字
d{pattern},date{pattern},d{pattern,timezone},date{pattern,timezone} 日志事件的日期,日期转换词需要一个格式化字符串作为参数,格式化字符串语义与java.text.SimpleDateFormat相同。
F/file 日志请求的java源文件名,一般生成文件信息比较慢,因此一般不使用这个转换词
caller{depth} 输出日志调用者位置信息
L/line 日志请求的行号,由于生成行号信息比较慢,一般不使用这个转换词
m/msg/message 日志事件相关的应用提供信息
M/method 输出与日志事件相关的应用提供的信息
n 输出与平台独立的行分割符,等价于"\n"或者"\r\n"
p/le/level 输出日志级别
r/relative(相对的) 输出日志事件的相对时间
t/thread[斯乱德] 输出产生日志事件的线程名
X{key:-defaultVal} 输出与产生日志事件线程相关的MDC(mapped diagnosis context)。如果mdc转换词后面花括号中有key,那么value就会输出,如果值为空,那么输出默认值;若没有默认值,则输出空字符串。
ex{depth}[带婆斯] 若发生异常时,指定输出异常的行数。
replace(p){r,t} 用't'替换p中正则表达式r的内容,例如:%replace('%m'){'\s',''}会去替换事件消息中包含的所有空格

详细请见:https://blog.csdn.net/lingbomanbu_lyl/article/details/89852037

1.2 自定义layout

  1. 继承自LayoutBase接口即可:
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.LayoutBase;

/**
 * @ClassName MySampleLayout
 * @Description 日志布局器
 * @Date 2019/7/1
 * @Version 1.0
 **/
public class MySampleLayout extends LayoutBase<ILoggingEvent> {
    @Override
    public String doLayout(ILoggingEvent event) {
        StringBuffer sb=new StringBuffer();
        sb.append(event.getTimeStamp()-event.getLoggerContextVO().getBirthTime());
        sb.append(" [").append(event.getThreadName()).append("] ");
        sb.append(event.getLoggerName()).append("-");
        sb.append(event.getFormattedMessage()).append(CoreConstants.LINE_SEPARATOR);
        return sb.toString();
    }
}
  1. 配置文件
<configuration>
    <springProperty scope="context" name="logPath" source="log.out.path" defalutValue="/app/test.log"/>
    <!-- %m输出的信息,%p日志级别,%t线程名,%d日期,%c类的全名,%i索引【从数字0开始递增】,,, -->
    <!-- appender是configuration的子节点,是负责写日志的组件。 -->
    <!-- ConsoleAppender:把日志输出到控制台 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="com.tellme.layout.MySampleLayout"/>
            <!--<pattern>%d %p [%r] [%t]  [%X{traceRootId}] (%file:%line\): %m%n</pattern>-->
            <!--&lt;!&ndash; 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 &ndash;&gt;-->
            <!--<charset>UTF-8</charset>-->
        </encoder>
    </appender>
</configuration>
  1. 输出格式:
1230 [restartedMain] com.MmWebApplication-The following profiles are active: test

若包含自定义参数的layout组件

  1. java实现类
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.LayoutBase;
import org.apache.commons.lang3.StringUtils;

/**
 * @ClassName CustomLayout
 * @Description 自定义Layout
 * @Date 2019/7/1
 * @Version 1.0
 **/
public class CustomLayout extends LayoutBase<ILoggingEvent> {

    private String prefix = null;
    boolean printThreadName = true;

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public void setPrintThreadName(boolean printThreadName) {
        this.printThreadName = printThreadName;
    }

    @Override
    public String doLayout(ILoggingEvent event) {

        StringBuffer sbuf = new StringBuffer(128);
        if (StringUtils.isNotBlank(prefix)) {
            sbuf.append(prefix).append(" ");
        }
        sbuf.append(event.getTimeStamp() - event.getLoggerContextVO().getBirthTime());
        sbuf.append(" ");
        sbuf.append(event.getLevel());
        if (printThreadName) {
            sbuf.append(" [");
            sbuf.append(event.getThreadName());
            sbuf.append("] ");
        } else {
            sbuf.append(" ");
        }
        sbuf.append(event.getLoggerName());
        sbuf.append(" - ");
        sbuf.append(event.getFormattedMessage());
        sbuf.append(CoreConstants.LINE_SEPARATOR);
        return sbuf.toString();
    }
}
  1. 配置文件
<configuration>
    <springProperty scope="context" name="logPath" source="log.out.path" defalutValue="/app/test.log"/>
    <!-- %m输出的信息,%p日志级别,%t线程名,%d日期,%c类的全名,%i索引【从数字0开始递增】,,, -->
    <!-- appender是configuration的子节点,是负责写日志的组件。 -->
    <!-- ConsoleAppender:把日志输出到控制台 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="com.tellme.layout.CustomLayout">
                <prefix>MyPrefix</prefix>
                <printThreadName>false</printThreadName>
            </layout>
            <!--<pattern>%d %p [%r] [%t]  [%X{traceRootId}] (%file:%line\): %m%n</pattern>-->
            <!--&lt;!&ndash; 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 &ndash;&gt;-->
            <!--<charset>UTF-8</charset>-->
        </encoder>
    </appender>
</configuration>
  1. 输出格式
MyPrefix 1405 INFO com.MmWebApplication - The following profiles are active: test

2. MDC

logback内置的日志字段还是比较少的,如果我们需要打印有关业务的更多内容,包括自定义的一些数据,需要借助logback的MDC机制,(映射诊断上下文),即将一些运行时的上下文数据通过logback打印出来;此时我们需要借助org.sl4j.MDC类。

MDC类的原理:其内部持有一个InheritableThreadLocal [因海瑞特包 遗传]实例,用于保存context数据,MDC提供了put/get/clear等几个接口,用来操作ThreadLocal中的数据;ThreadLocal中的K-V,可以在logback.xml中声明,最终会打印在日志中。

MDC.put("userId",1000);

那么在logback.xml中,即可在layout中通过"%X{userId}"来打印此信息。

注意:因为使用的是ThreadLocal,在需要线程退出之前,需要清除(clear)MDC里面的数据;在线程池中使用MDC时,在子线程退出之前调用MDC.clear()方法。

2.1 MDC实践

public class LogMDCUtil {
    //接口名
    private final static String SVR_ID = "SVR_ID";

    private final static Logger logger = LoggerFactory.getLogger(LogMDCUtil.class);
/**
     * 为日志设置服务名
     *
     * @param serviceId
     */
    public static void setServiceNameToLog(String serviceId) {
        try {
            
            MDC.put(SVR_ID, serviceId);
        } catch (Exception e) {
            logger.error("【Logback设置MDC】-设置服务名失败", e);
        }
    }

    /**
     * 删除ThreadLocal中关于ServiceName的数据,防止内存溢出
     */
    public static void removeServiceNameToLog() {
        //线程执行完毕,需要删除ServiceName
        try {
            MDC.remove(SVR_ID);
        } catch (Exception e) {
            logger.error("【Logback设置MDC】-删除服务名失败", e);
        }
    }
}

配置文件:

 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d %p [%t %X{SVR_ID}] (%C{0}:%line\): %m%n</pattern>
            <!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

2.2 MDC实战—为请求生成唯一编号

如何为每一请求生成唯一编号,并在日志中打印出来呢?

  1. 借助Redis自增命令,获取唯一编号
@Service
public class GainIdService {

    private static Logger logger = LoggerFactory.getLogger(GainIdService.class);

    @Resource
    private StringRedisTemplate template;

    //前缀 不同系统的前缀可以不相同
    public String gainId(String prefix) {
        Long seq = template.opsForValue().increment("system:idseq:" + pre.getPrefix());
        DecimalFormat df = new DecimalFormat("0000");
        String str = df.format(seq);
        //substring(int beginIndex),[beginIndex,endIndex]的子串,即获取后四位编号
        str = str.substring(str.length() - 4);
        SimpleDateFormat sf=new SimpleDateFormat("yyyyMMddHHmmss");
        String timeStr = sf.format(new Date());
        return pre.getPrefix() + timeStr  + str;
    }
}
  1. 在SpringBoot拦截器中放入MDC中

springBoot—拦截器详解

@Component
public class LogInterceptor implements HandlerInterceptor {

    private final static String REQ_ID = "REQ_ID";

    @Autowired
    GainIdService gainIdService;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        String traceId = gainIdService.gainId(IdPrefixEnum.REQ_ID);
        MDC.put(REQ_ID, traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        MDC.remove(REQ_ID);
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}
  1. 将拦截器配置到系统中
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {

    @Autowired
    private LogInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor);
    }
}

参考文档

(译)(五)Logback中的Layout

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容