日志那些事儿——Logback源码解析

前言

在上篇文章日志漫谈中谈到,日志在监控报警、查错分析等方面有着非常重要的应用。Logback作为目前最火的日志系统,本文就简单分析一下logback日志打印的过程。

logback背景与配置

Logback是一个开源的日志组件,是log4j的作者开发的用来替代log4j的。

logback由三个部分组成,logback-core, logback-classic, logback-access。其中logback-core是其他两个模块的基础。

logback一般不能单独使用需要和slf4j配合使用,slf4j定义日志接口,具体实现由logback提供。关于slf4j怎么和logback配合本文暂不赘述,本文主要讨论logback的日志打印过程。

要使用logback,除了引入logback的相关jar包之外,还应该在classpath下放置logback.xml(不是唯一的做法),logback.xml配置一般如下:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Logback Configuration. -->
<configuration debug="false">
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">      <target>System.out</target>
      <encoding>${loggingCharset}</encoding>
           <layout class="ch.qos.logback.classic.PatternLayout">
         <pattern><![CDATA[  %d %-4relative [%thread] %-5level %logger{35} - %msg%n ]]>
</pattern>
      </layout>
   </appender>
<appender name="FILE"   class="ch.qos.logback.core.rolling.RollingFileAppender">   <file>${loggingRoot}/some.log</file>
   <encoding>${loggingCharset}</encoding>
     <append>true</append> 
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">      <FileNamePattern>${loggingRoot}/some.%d{yyyy-MM-dd}.log      </FileNamePattern>
      <!-- keep 30 days' worth of history -->      <MaxHistory>30</MaxHistory>
   </rollingPolicy>
   <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>%d %-4relative [%thread] %-5level %logger{35} - %msg%n      </pattern>
   </layout>
</appender>
<logger name="logger1">
   <level value="INFO" />
   <appender-ref ref="FILE" />
</logger>
<root>
   <level value="${loggingLevel}" />
   <appender-ref ref="FILE" />
</root>
</configuration>

配置文件中主要包含了logger、root、appender、rollingPolicy、layout、pattern等元素,这几个元素就是整个logger体系中最最重要的几个组成部分。
其中:
Logger: 日志记录器,把它关联到应用对应的context上后,主要用于存放日志对象,定义日志类型,级别。
Appender: 指定日志输出的目的地,目的地可以是控制台,文件,或者数据库等
Layout: 负责把事件转换成字符串,格式化日志信息的输出

logback日志打印流程

当我们配置了上述logback.xml以后,我们通常通过

Logger logger1 = LoggerFactory.getLogger("logger1");
logger1.info("This is a test for logging,msg:{}","I love xiaojin");

的方式使用,现在我们来看看调用logger1.info(),到底发生了什么事情。下图是logger1.info()的简略流程图:

logback logger流程图.jpg

从图中可以看出,整个过程中起关键作用的几个类为:AppenderAttatchableImpl,OutputStreamAppender、Encoder、Layout、OutputStream。这几类和配置文件中相应元素一一对应。logback通过类的继承和组合层层封装方法,最后通过OutputStream写入到控制台(ConsoleAppender)或者是文件(FileAppender)中。

为了对Appender、encoder、Layout、OutputStream有个整体的认识,先给出一张logback中上述各类的类图。

logback loginfo相关类全景图.jpg

可以看出,Appender、encoder、Layout、OutputStream是其中的核心,上图列出了我们经常使用的一些Appender,例如ConsoleAppender、RollingFileAppender;文件的滚动策略:TimeBasedRollingPolicy;格式化日志输出的PatternLayout,以及相应的pattern“%d %-4relative [%thread] %-5level %logger{35} - %msg%n”。

相关代码分析

上面的两张图列出了日志打印的过程和相关类,下面将通过关键代码分析Logback是如何将上述类串起来的。

关键代码1

public void callAppenders(ILoggingEvent event) { 
 int writes = 0;
  for (Logger l = this; l != null; l = l.parent) {
    writes += l.appendLoopOnAppenders(event);
    if (!l.additive) {
      break;
    }
  } 
 // No appenders in hierarchy
  if (writes == 0) {
    loggerContext.noAppenderDefinedWarning(this); 
 }}
public int appendLoopOnAppenders(E e) {
  int size = 0;  r.lock();
  try {
    for (Appender<E> appender : appenderList) {
      appender.doAppend(e);
      size++;
    }
  } finally {
    r.unlock();
  }
  return size;
}

上述第一段代码位于Logger.java中,logger会通过parent组成一条链路,通过每个logger都可以拥有多个Appender,上述代码会沿着Logger链路依次调用logger.appendLoopOnAppenders方法,直到logger.additive属性为false。第二段代码位于AppenderAttachableImpl中,logger.appendLoopOnAppenders实际上就是调用了AppenderAttachableImpl.appendLoopOnAppenders,该代码遍历logger中的所有appender调用Appender.doAppend(e)方法。

关键代码2

因为我们目前主要使用的还是FileAppender,所以下面的代码都是以RollingFileAppender为例。

@Override
protected void subAppend(E event) {
  synchronized (triggeringPolicy) {
    if (triggeringPolicy.isTriggeringEvent(currentlyActiveFile, event)) {
      rollover();
    }
  }
  super.subAppend(event);}
  protected void subAppend(E event) {
    if (!isStarted()) {
      return;
    }
    try {  
      if (event instanceof DeferredProcessingAware) {
        ((DeferredProcessingAware) event).prepareForDeferredProcessing();
      }
      synchronized (lock) {
        writeOut(event);
      }
    } catch (IOException ioe) {
      this.started = false;
      addStatus(new ErrorStatus("IO failure in appender", this, ioe));
    }
  }

protected void writeOut(E event) throws IOException { 
 this.encoder.doEncode(event);
}

前面讲到logger会调用到Appender.doAppend(),经过层层封装最后会调用到Appender.subAppend()。第一段代码在RollingFileAppender中,可以看出先进行了一次triggeringPolicy的校验,然后调用了父类的subAppend();下面一个就是RollingFileAppender的父类OutputStreamAppender的subAppender(),而其又调用了writeOut方法,而writeOut调用了Encoder.doEncode方法。

关键代码3

上面提到了在RollingFileAppender.subAppender中会进行一次triggeringPolicy的校验。

 public boolean isTriggeringEvent(File activeFile, final E event) {
    long time = getCurrentTime();
    if (time >= nextCheck) {
      Date dateOfElapsedPeriod = dateInCurrentPeriod;
      elapsedPeriodsFileName = tbrp.fileNamePatternWCS
          .convert(dateOfElapsedPeriod);
      setDateInCurrentPeriod(time);
      computeNextCheck();
      return true;
    } else {
      return false;
    }

调用的默认是DefaultTimeBasedFileNamingAndTriggeringPolicy#isTriggeringEvent方法,这个主要是干啥用呢。考虑到我们使用了RollingFileAppender,我们需要日志按天滚动,例如今天是0820,所以今天的日志名应该为some.log,而昨天打的日志将会重命名为some.log.2016-08-19,当然这个是根据配置文件中FileNamePattern元素决定的。而isTriggeringEvent方法就是用来判决当前需不需要对日志名进行rollover(重命名),我们能想到的方法就是去查看日志文件的lastModified的时间与当前时间比较,如果当前时间已经是第二天了,那么应该将some.log重命名为some.log.XXXXXX,XXXX应该是日志修改时间的格式化时间,同时新建一个some.log的文件,并且将日志写入到some.log。logback基本上就是这么做的,但是为了提高效率避免每次都检查,logback会计算出nextChecktime,下次需要check的时间,这个时间有可能是lastModified的第二天凌晨(初始化阶段),有可能是当前时间的第二天凌晨(触发了rollover的时候),这样logback只需要在超过nextChecktime时才需要去rollover。

关键代码3

  public void doEncode(E event) throws IOException {
    String txt = layout.doLayout(event);
    outputStream.write(convertToBytes(txt));
    outputStream.flush();
  }
public String doLayout(ILoggingEvent event) {
    if (!isStarted()) {
      return CoreConstants.EMPTY_STRING;
    }
    return writeLoopOnConverters(event);
  }

protected String writeLoopOnConverters(E event) {
    StringBuilder buf = new StringBuilder(128);
    Converter<E> c = head;
    while (c != null) {
      c.write(buf, event);
      c = c.getNext();
    }
    return buf.toString();
  }

public void start() {
    if(pattern == null || pattern.length() == 0) {
      addError("Empty or null pattern.");
      return;
    }
    try { 
      Parser<E> p = new Parser<E>(pattern);
      if (getContext() != null) {
        p.setContext(getContext());
      }
      Node t = p.parse();
      this.head = p.compile(t, getEffectiveConverterMap());
      if (postCompileProcessor != null) {
        postCompileProcessor.process(head);
      }
      setContextForConverters(head);
      ConverterUtil.startConverters(this.head);
      super.start();
    } catch (ScanException sce) {
      StatusManager sm = getContext().getStatusManager();
      sm.add(new ErrorStatus("Failed to parse pattern \"" + getPattern()
          + "\".", this, sce));
    }
  }
  static {

    defaultConverterMap.put("d", DateConverter.class.getName());
    defaultConverterMap.put("date", DateConverter.class.getName());

    defaultConverterMap.put("r", RelativeTimeConverter.class.getName());
    defaultConverterMap.put("relative", RelativeTimeConverter.class.getName());

    defaultConverterMap.put("level", LevelConverter.class.getName());
    defaultConverterMap.put("le", LevelConverter.class.getName());
    defaultConverterMap.put("p", LevelConverter.class.getName());

    defaultConverterMap.put("t", ThreadConverter.class.getName());
    defaultConverterMap.put("thread", ThreadConverter.class.getName());

    defaultConverterMap.put("lo", LoggerConverter.class.getName());
    defaultConverterMap.put("logger", LoggerConverter.class.getName());
    defaultConverterMap.put("c", LoggerConverter.class.getName());

    defaultConverterMap.put("m", MessageConverter.class.getName());
    defaultConverterMap.put("msg", MessageConverter.class.getName());
    defaultConverterMap.put("message", MessageConverter.class.getName());

    defaultConverterMap.put("C", ClassOfCallerConverter.class.getName());
    defaultConverterMap.put("class", ClassOfCallerConverter.class.getName());

    defaultConverterMap.put("M", MethodOfCallerConverter.class.getName());
    defaultConverterMap.put("method", MethodOfCallerConverter.class.getName());

    defaultConverterMap.put("L", LineOfCallerConverter.class.getName());
    defaultConverterMap.put("line", LineOfCallerConverter.class.getName());

    defaultConverterMap.put("F", FileOfCallerConverter.class.getName());
    defaultConverterMap.put("file", FileOfCallerConverter.class.getName());

    defaultConverterMap.put("X", MDCConverter.class.getName());
    defaultConverterMap.put("mdc", MDCConverter.class.getName());

    defaultConverterMap.put("ex", ThrowableProxyConverter.class.getName());
    defaultConverterMap.put("exception", ThrowableProxyConverter.class
        .getName());
    defaultConverterMap.put("throwable", ThrowableProxyConverter.class
        .getName());

    defaultConverterMap.put("xEx", ExtendedThrowableProxyConverter.class.getName());
    defaultConverterMap.put("xException", ExtendedThrowableProxyConverter.class
        .getName());
    defaultConverterMap.put("xThrowable", ExtendedThrowableProxyConverter.class
        .getName());

    defaultConverterMap.put("nopex", NopThrowableInformationConverter.class
        .getName());
    defaultConverterMap.put("nopexception",
        NopThrowableInformationConverter.class.getName());

    defaultConverterMap.put("cn", ContextNameAction.class.getName());
    defaultConverterMap.put("contextName", ContextNameConverter.class.getName());
    
    defaultConverterMap.put("caller", CallerDataConverter.class.getName());

    defaultConverterMap.put("marker", MarkerConverter.class.getName());

    defaultConverterMap.put("property", PropertyConverter.class.getName());

    
    defaultConverterMap.put("n", LineSeparatorConverter.class.getName());
  }

前面提到输出日志的最终任务会落到Enconder#doEncode身上,第一段代码位于LayoutWrappingEncoder.java中,doEncode会最终调用Layout.doLayout得到格式化的文本,然后使用outputStream输出。第二段代码位于PatternLayout.java中,doLayout会调用writeLoopOnConverters,而writeLoopOnConverters会将日志文件通过Converter链进行格式化。Converter链是怎么产生的呢?请看第三段代码,看来是通过配置的pattern得到的,大体上是通过Parser去解析我们配置的pattern("%d %-4relative [%thread] %-5level %logger{35} - %msg%n"),parser通过识别"%"来判决是关键词还是普通文本,例如[]就是普通文件,relative/msg/n等都是关键字。Node t = p.parse();将pattern转化成Node链,Node有多种例如KeywordNode。通过p.compile(t, getEffectiveConverterMap())得到Convert链,主要是将Node链转化成Convert链,PatternLayout中列出了所有的Convert与pattern中名字的对应关系(最后一段代码),例如d代表DateConverter,level 代码LevelConverter等。

其实我们的日志信息在经过Converter链格式化之前还经过了一次格式化。我们通常会使用logger.info("somemsg:{}",{}),使用"{}"作为占位符,logback会将其替换成后面出现的参数,这个过程是怎么发生的呢?

  public LoggingEvent(String fqcn, Logger logger, Level level, String message,
      Throwable throwable, Object[] argArray) {
    this.fqnOfLoggerClass = fqcn;
    this.loggerName = logger.getName();
    this.loggerContext = logger.getLoggerContext();
    this.loggerContextVO = loggerContext.getLoggerContextRemoteView();
    this.level = level;

    this.message = message;

    FormattingTuple ft = MessageFormatter.arrayFormat(message, argArray);
    formattedMessage = ft.getMessage();

    if (throwable == null) {
      argumentArray = ft.getArgArray();
      throwable = ft.getThrowable();
    } else {
      this.argumentArray = argArray;
    }

    if (throwable != null) {
      this.throwableProxy = new ThrowableProxy(throwable);
      LoggerContext lc = logger.getLoggerContext();
      if (lc.isPackagingDataEnabled()) {
        this.throwableProxy.calculatePackagingData();
      }
    }

    timeStamp = System.currentTimeMillis();

    // ugly but under the circumstances acceptable
    LogbackMDCAdapter logbackMDCAdapter = (LogbackMDCAdapter) MDC
        .getMDCAdapter();
    mdcPropertyMap = logbackMDCAdapter.getPropertyMap();
  }
    FormattingTuple ft = MessageFormatter.arrayFormat(message, argArray);
    formattedMessage = ft.getMessage();

IloggingEvent非常重要,是log信息的携带者,其实现类为LoggingEvent,在实例化LoggingEvent时,使用MessageFormatter和FormattingTuple将{}占位符进行了替换。

后记

已经深夜了,很困!感觉还有很多东西没有提到,思绪又很乱。没有提到Appender/Encoder/Layout的初始化过程,其实上述接口都实现了LifeCycle(非常像tomcat),显式调用start完成初始化过程。初始化过程中做了很多事情上面没有提到,没有讲到logback.xml的解析过程,也没有提到slf4j与logback怎么结合。列个todo在这,有时间再写!

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

推荐阅读更多精彩内容

  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 4,817评论 1 13
  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 4,907评论 0 6
  • Log Java日志:(slf4j、log4j、logback、common-logging ) slf4j 是规...
    年少懵懂丶流年梦阅读 17,276评论 1 11
  • 1 只接受数字输入 2 全选 3 反选 4 单选框标签表示 5 还可输入多少字符提示 6 输入域显示缺省值 7 E...
    毛三十阅读 443评论 1 30
  • 大学的时候,有一次看到网上有一对小情侣,专门拍他们每天吃的一日三餐。当时的我真的很为之心动。于是,心里不断在想:“...
    七公爱吃鸡阅读 3,706评论 4 8