[翻译] logstash中logback的json编码器插件

原文地址为 https://github.com/logstash/logstash-logback-encoder#composite_encoder

logstash-logback-encoder

提供logback的编码器,布局(layouts)和追加器,来输出到json形式的日志。

同时支持日志事件集合(也就是通常意义的logger输出的日志)访问事件集合(也就是通常意义的localhost-access的日志)

logstash原始输入就支持输入成json形式,但是logstash的json形式输出已经发展成更高的可配置性,更加常用化的json形式日志输出器(当然是指输出到elasticsearch和其他接收者)。json输出的结构和json包含的结构是完全可控制的。

目录

  • 在你的项目中使用
  • 用法
    • UDP 追加器
    • TCP 追加器
      • 链接保持
      • 多个目标输出
      • 重连延迟
      • 写入缓冲区的大小
      • SSL链接
    • 异步追加器
    • 编码器和样式布局器
  • 记录事件字段
    • 标准字段
    • MDC字段
    • 上下文文件
    • 请求者信息字段
    • 自定义字段
      • 全局自定义字段
      • 具体事件定义字段
  • 访问事件字段
    • 标准字段
    • 请求头字段
  • 定制标准化的字段名称
  • 定制版本
  • 定制时区
  • 定制JSON输出工厂和启动器
  • 定制日志的名称长度
  • 定制追踪栈
  • 前缀和后缀
  • 复合编码器/样式布局器
    • 提供一个普通日志事件集合
    • 提供一个访问事件日志集合
    • 嵌套式的json提供器
    • 模式化的json提供器
      • 普通日志事件的格式化
      • 访问日志事件的格式化
  • 如何debug

在你的项目中使用

maven引入的格式

<dependency>
  <groupId>net.logstash.logback</groupId>
  <artifactId>logstash-logback-encoder</artifactId>
  <version>4.10</version>
</dependency>

如果你在运行项目时遇到 ClassNotFoundException/NoClassDeffFoundError/NoSuchMethodError的错误,请确认你在pom文件从mavne仓库引入的需要的具体依赖(和适当的版本)存在于运行时的路径中:

  • jackson-databind / jackson-core / jackson-annotations
  • logback-core
  • logback-classic (required for logging LoggingEvents)
  • logback-access (required for logging AccessEvents)
  • slf4j-api
    在pom中的老版本文件也许能用,但是在pom中的文件版本是针对测试的。

用法

为了输出JSON格式日志,你必须配置logback使用下面两者之一:

  • logstash-logback-encoder这个jar包提供的日志追加器,或者
  • 通过logstash-logback-encoder提供的logback(或者其他jar包)的编码器和样式布局器的追加器

下面表格描述了logstash-logback-encoder提供的追加器,编码器和样式布局器:

Format Protocol Function LoggingEvent AccessEvent
Logstash JSON Syslog/UDP Appender n/a
Logstash JSON TCP Appender LogstashTcpSocketAppender LogstashAccessTcpSocketAppender
any any Appender LoggingEventAsyncDisruptorAppender AccessEventAsyncDisruptorAppender
Logstash JSON any Encoder LogstashEncoder LogstashAccessEncoder
Logstash JSON any Layout LogstashLayout LogstashAccessLayout
General JSON any Encoder LoggingEventCompositeJsonEncoder AccessEventCompositeJsonEncoder
General JSON any Layout LoggingEventCompositeJsonLayout AccessEventCompositeJsonLayout

这些编码器和样式布局器通常能用在任何logback的追加器(比如RollingFIleAppender)中。

这些普通复合的JSON编码器和样式布局器通过多种JSON提供器配置,通常被用来输出JSON格式文本和数据。logstash编码和样式布局器事实上是一组预定义的普通复合JSON的输出器和样式布局器。

假如你想用标准logstash版本1格式输出,logstash布局器和样式布局器就非常容易配置。假如你想使用深度定制的输出格式,或者使用logstah的版本0输出,可以使用复合编码器和样式布局器。

AsyncDisruptorAppender是一种除了LMAX Disruptor RingBuffer,一种被用来做队列,和阻塞队列对应,其余部分和logback中的AsyncAppender*差不多的追加器。

UDP追加器

在你的logback.xml中使用LogstashSocketAppender,为普通日志事件输出JSON到syslog/UDP 通道。比如:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="stash" class="net.logstash.logback.appender.LogstashSocketAppender">
    <host>MyAwesomeSyslogServer</host>
    <!-- port is optional (default value shown) -->
    <port>514</port>
  </appender>
  <root level="all">
    <appender-ref ref="stash" />
  </root>
</configuration>

从内部看,LogstashSocketAppender使用LogstashLayout来展示JSON格式。因此,默认地会使用复合logstash来输出。

你能在最后的学习章节中学到,你能通过LogstashLayout或者LogstashEncoder来定制LogstashSocketAppender。不必一定要在logback的配置<appender>的子节点中配置<layout>或者<encoder>。所有LogstashLayoutLogstashEncoder的属性都能在appender的级别中设定。比如你能指定配置全局变量。

<appender name="stash" class="net.logstash.logback.appender.LogstashSocketAppender">
    <host>MyAwesomeSyslogServer</host>
    <!-- port is optional (default value shown) -->
    <port>514</port>
    <customFields>{"appname":"myWebservice"}</customFields>
  </appender>

目前没有办法通过syslog/UDP来传输AccessEvents。

在logstash中接受到syslog/UDP的输入,通过在logsatsh的配置中的json编码器来配置syslog或者udp输入,就像:

input {
  syslog {
    codec => "json"
  }
}

TCP追加器

为了通过TCP输出JSON的日志输出,使用配置了LogstashEncoderLoggingEventCompositeJsonEncoderLogstashTcpSocketAppender

在logback.xml中的日志追加器配置举例:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      <destination>127.0.0.1:4560</destination>
      <!-- encoder is required -->
      <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
  </appender>
  <root level="DEBUG">
      <appender-ref ref="stash" />
  </root>
</configuration>

不同于UDP追加器,TCP追加器必须配置编码器。你能使用一个LogstashEncoder,或一个EventCompositeJsonEncoder,或者任何的logback编码器。所有的输出格式配置都在编码器的级别配置。

在追加器内部,TCP追加是异步的(使用LMAX Disruptor RingBuffer)。所有的编码和TCP链接被委托给一个单独的线程。不需要用另外的异步追加来包装TCP追加器(比如AsyncAppenderLoggingEventAsyncDistruptorAppender)。

异步追加器的所有参数会被TCP追加器验证。比如,waitStrategyType and ringBufferSize

这个TCP追加器永远不会阻塞线程。如果缓冲区是满的(比如网络很慢的情况),这些数据事件会被丢弃。

TCP追加器的连接断开,他会自动重连。然而在断开的情况下,数据事件会丢失。

为了让logstah接受到输入,在logstash配置文件中配置使用json_lines配置logstash的tcp输入,如下:

input {
    tcp {
        port => 4560
        codec => json_lines
    }
}

为了保证TCP追加器输出的日志信息能被处理,你应该保证在你的应用存在的情况下,彻底地关闭logstash。

保持连接

如果事件频繁的输出,在服务器闲置超时的情况下,链接一般会断掉。你能通过配置来保持链接处于激活,就像这样:

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      ...
      <keepAliveDuration>5 minutes</keepAliveDuration>
  </appender>
多个目标输出

TCP追加器能配置尝试连接多个目标输出,如下:

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      <destination>destination1.domain.com:4560</destination>
      <destination>destination2.domain.com:4560</destination>
      <destination>destination3.domain.com:4560</destination>

      ...
  </appender>

或者

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      <destination>
          destination1.domain.com:4560,
          destination2.domain.com:4560,
          destination3.domain.com:4560
      </destination>

      ...
  </appender>

TCP追加器能够使用链接策略决定:

  • 顺序连接目的地
  • 确定的连接应该被重建(通过连接策略链接下一个目的地)。

日志有时只能发送到一个目的地(而不是所有的目的地)。默认的,追加器将会保持连接到目的地直到断开或者应用主动断开。一些连接策略聚焦于链接重连(如下)。如果链接断开,追加器会试图根据链接策略链接下一个目的地。

以下是可用的链接策略:

策略 描述
首要连接 (默认的)第一个目的地选择主要的目的地。每个后附加的目的地是次要的选择。策略更加注重首要目的地,除非他关闭了。追加器试图按照顺序连接每个被配置的目的地。如果一个链接断开,追加器会再次尝试顺序连接首要被配置的目的地。 secondaryConnectionTTL能被配置为在一个具体时间后优雅的关闭到次要目的地的链接。这种策略聚焦在追加器重新尝试连接顺序的目的地。secondaryConnectionTTL的值不能影响首要目的地的链接。比如<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender"><br /><destination>destination1.domain.com:4560</destination><br /><destination>destination2.domain.com:4560</destination><br /><destination>destination3.domain.com:4560</destination><br /><connectionStrategy><br /><preferPrimary><br /><secondaryConnectionTTL>5 minutes</secondaryConnectionTTL><br /></preferPrimary><br /></connectionStrategy><br /></appender><br />
轮询 这个策略尝试轮询连接目的地。如果一个连接失败,会试图下一个。<br /> connectionTTL能被配置为在一个具体时间后优雅的关闭目的地的链接。这种策略聚焦在追加器将会重新连接下一个目的地。<br /> 比如:<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender"><br /> <destination>destination1.domain.com:4560</destination><br /><destination>destination2.domain.com:4560</destination><br /><destination>destination3.domain.com:4560</destination><br /><connectionStrategy><br /><roundRobin><br /><connectionTTL>5 minutes</connectionTTL><br /></roundRobin><br /> </connectionStrategy><br /> </appender><br />
随机 这个策略尝试随机链接目的地。如果一个连接失败,会试图随机连接一个目的地。<br /> connectionTTL能被配置为在一个具体时间后优雅的关闭目的地的链接。 这种策略聚焦在追加器将会重新连接随机的一个目的地。<br /> 比如:<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender"><br /><destination>destination1.domain.com:4560</destination><br /><destination>destination2.domain.com:4560</destination><br /><destination>destination3.domain.com:4560</destination><br /><connectionStrategy><br /><random><br /><connectionTTL>5 minutes</connectionTTL><br /></random><br /></connectionStrategy><br /></appender><br />

你能用你通过实现DestinationConnectionStrategy接口来自己定义的连接策略,然后像如下所示来配置追加器:

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      <destination>destination1.domain.com:4560</destination>
      <destination>destination2.domain.com:4560</destination>
      <destination>destination3.domain.com:4560</destination>
      <connectionStrategy class="your.package.YourDestinationConnectionStrategy">
      </connectionStrategy>
  </appender>
重连延迟

如果所有的配置地址都连接失败,TCP追假器会默认在试图重连前等待30秒。

能通过设定reconnectionDelay字段改变延迟的时间。

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      ...
      <reconnectionDelay>1 second</reconnectionDelay>
  </appender>
写入缓冲区的大小

默认的,一个8192大小大小的缓冲区被Socket用来输出到写入流中。你能通过设定writeBufferSize的值的大小来调整。

<appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      ...
      <writeBufferSize>16384</writeBufferSize>
  </appender>

writeBufferSize设定为0的情况下,缓冲失效。如果缓冲失效,写入线程会减速,但是他会在严苛的网络环境下防止丢弃日志事件。

SSL链接

使用SSL,在LogstashSocketAppender或者LogstashAccessTcpSocketAppender中的<appender>节点的<ssl>自节点。

查看logback manual如何配置SSL,LogstashTcpSocketAppender的SSL能像logback的SSLSocketAppender*一样被配置。

举例,用JVM默认的密钥和证书库(truststore)来使SSL作用,如下所示:

 <appender name="stash" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
      ...
      <!-- Enable SSL using the JVM's default keystore/truststore -->
      <ssl/>
  </appender>

使用不同的证书库(truststore),如下所示:

<appender name="stash" class="net.logstash.logback.appender.LogstashAccessTcpSocketAppender">
      ...
      <!-- Enable SSL and use a different truststore -->
      <ssl>
          <trustStore>
              <location>classpath:server.truststore</location>
              <password>${server.truststore.password}</password>
          </trustStore>
      </ssl>
  </appender>

所有通过Logback*TcpSocketAppender提供的logback自定义配置都被支持(比如配置密码规范,协议,算法,提供者等)。

查看logstash文档TCP的输入中关于如何使用SSL。

异步追加器

AsyncDisruptorAppender追加器和logback的AsyncAppender*追加器很相似,除了LMAX Disruptor RingBuffer被用来作为队列的生成器,作为阻塞队列的对立。这种追加器能转移到任何下游的logback追加器。

比如:

<appender name="async" class="net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender">
    <appender class="ch.qos.logback.core.rolling.RollingFileAppender">
       ...
    </appender>
  </appender>

异步追加器永远不会阻塞日志线程。如果循环buffer区满了(比如网路缓慢),日志事件将会丢弃。

默认的,BlockWaitStrategy被工作线程生产的这个追加器所使用。阻塞线程策略把cup利用率降到最低,但结果是更低的延迟和吞吐率。如果你需要更高的延迟和吞吐率(高CPU利用率),考虑不同方向提供的等待策略,比如SleepingWaitStrategy

在异步追加器中使用waitStrategyType参数来配置等待策略,像这样:

<appender name="async" class="net.logstash.logback.appender.LoggingEventAsyncDisruptorAppender">
    <waitStrategyType>sleeping</waitStrategyType>
    <appender class="ch.qos.logback.core.rolling.RollingFileAppender">
       ...
    </appender>
  </appender>

支持的策略如下:

等待策略 参数 实现类
blocking BlockingWaitStrategy
busySpin BusySpinWaitStrategy
liteBlocking LiteBlockingWaitStrategy
sleeping SleepingWaitStrategy
yielding YieldWaitStrategy
phasedBackoff{spinTime,yieldTime,timeUnit,fallbackStrategy} <br /> 比如phasedBackoff{10,60,seconds,blocking} 1.spinTime-放弃前的等待时间<br /> 2.yieldTim-放弃回落到fallbackStrategy的时间<br /> 3.timeUnit-spinTime和yieldTime的时间单位,时间单位的字符串(比如 second)<br />4.fallbackStrategy-超时回退到等待策略的字符串 PhasedBackoffWaitStrategy
timeoutBlocking{timeout,timeUnit}<br />比如:timeoutBlocking{1,minutes} 1.timeout-抛出异常前的阻塞时间<br />2.时间单位-超时时间的单位(比如:seconds) TimeoutBlockingWaitStrategy

查看AsyncDistruptorAppender的其他配置参数(比如:ringBufferSizeproducerTypethreadNamePrefixdaemon,和droppedWarnFrequency)。

为了保证产生的日志有机会被异步追加器(包含TCP追加器)处理和确保后台线程已经停止,当你的程序退出你需要干净地关闭lobgack。

编码器和样式布局器

你可以使用logstash-logback-encoder库提供的任何编码器和布局器和其他的logback追加器。

比如,输出一个日志事件到文件,可在在你的logabck.xml中使用RollingFileAppenderLogstashEncoder

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="stash" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>info</level>
    </filter>
    <file>/some/path/to/your/file.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>/some/path/to/your/file.log.%d{yyyy-MM-dd}</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder" />
  </appender>
  <root level="all">
    <appender-ref ref="stash" />
  </root>
</configuration>

为了输出访问日志事件到文件,向下面一样配置你的logback-access.xml:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="stash" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>/some/path/to/your/file.log</file>
    <encoder class="net.logstash.logback.encoder.LogstashAccessEncoder" />
  </appender>
  <appender-ref ref="stash" />
</configuration>

能用配置LogstashEncoderLogstashAccessEncoder的方法来配置LogstashLayoutLogstashAccesLayout。本文档中的其他的例子使用的是编码器(encoders),但是布局器(layouts)的配置也相同。

为了在logstash接收到文件输入,要想下面这样配置输入文件:

input {
  file {
    path => "/some/path/to/your/file.log"
    codec => "json"
  }
}

记录事件字段

下面的描述字段包含了被以下构件记录的事件默认的JSON输出:

  • LogstashEncoder
  • LogstashLayout,和
  • logstash追加器

如果你使用了复合的编码器和布局器,那么写入的字段会根据你提供的配置有所不同。

标准字段

如果没有另外的说明,这些字段将会展示在每个日志事件中。这里列出的字段名称是默认的字段名称。这些字段能被自定义(详见下文)

字段 描述
@timestamp 日志事件的时间。(yyyy-MM-dd'T'HH:mm:ss.SSSZZ)详见定制时区
@version logstash格式版本 详见定制版本
message 格式化的日志事件消息
logger_name 记录事件的记录器名称
thread_name 记录事件的线程名称
level 事件等级的字符串名称
level_value 事件等级的数字值
stack_trace (仅在抛出事件中打印)抛出事件的追踪栈。栈由换行分隔。
tags (仅在tags字段被发现时有效)没有明确处理任何标记名称。(比如:MarkerFactory.getMarker中的标记会包含在tags中,而Markers中的不会)。

MDC字段

默认地,每个在映射诊断上下文(the Mapped Diagnostic Context ,MDC,)(org.slf4j.MDC)中的条目将会作为一个日志事件的字段来展示。这可以通过在编码器,布局器和追加器中指定<includeMdc>false</includeMdc>来完全禁用。你也能在MDC中配置指定的项来包含或者排除,像如下所示:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <includeMdcKeyName>key1ToInclude</includeMdcKeyName>
  <includeMdcKeyName>key2ToInclude</includeMdcKeyName>
</encoder>

或者

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <excludeMdcKeyName>key1ToExclude</excludeMdcKeyName>
  <excludeMdcKeyName>key2ToExclude</excludeMdcKeyName>
</encoder>

当key的名称被指定包含,那么其他所有字段都被排除。当key的名称被指定排除,那么其他所有字段都被包含。同时配置包含key的名称和排除key的名称是错误的。

上下文文件

默认地,每个logback上下文属性(cn.qos.logback.core.Context)将会作为一个日志事件的字段来展示。可以在编码器,布局器和追加器中配置<includeContext>false<includeContext>来禁用。

请注意,logback的1.1.10之前的版本在上下文中默认包含HOSTNAME属性。从1.1.10开始,HOSTNAME被延迟计算,不再被默认包含在其中。

请求者信息字段

编码器吗,布局器和追加器默认的不包含请求者信息。请求者信息是昂贵的计算,应该在繁忙的的环境中避免使用。

如果要打开请求者信息功能,在配置中包含属性<includeCallerData>

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <includeCallerData>true</includeCallerData>
</encoder>

如果编码器包含了异步追加器,比如AsyncAppenderLoggingEventAsyncDisruptorAppender,或者LogstashTcpSocketAppender,追加器的includeCallerData属性必须设置为true。

当开启了请求者信息字段功能,日志事件包含以下字段:

字段 描述
caller_class_name 输出日志事件的类的全量名称
caller_method_name 输出日志事件的方法名称
caller_file_name 输出日志事件的行的名称
caller_line_number 输出日志事件的行号

自定义字段

除了上述的字段,你能在日志事件中全域添加其他字段,或者在逐个事件基础上添加。

全局自定义字段

在日志事件中添加自定义字段如下:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <customFields>{"appname":"myWebservice","roles":["customerorder","auth"],"buildinfo":{"version":"Version 0.1.0-SNAPSHOT","lastcommit":"75473700d5befa953c45f630c6d9105413c16fe1"}}</customFields>
</encoder>

或者在访问日志事件中:

<encoder class="net.logstash.logback.encoder.LogstashAccessEncoder">
  <customFields>{"appname":"myWebservice","roles":["customerorder","auth"],"buildinfo":{"version":"Version 0.1.0-SNAPSHOT","lastcommit":"75473700d5befa953c45f630c6d9105413c16fe1"}}</customFields>
</encoder>
具体事件定义字段

记录消息时,你能通过使用

  • StructuredArguments提供的结构化字段,或
  • Markers提供的标记

来添加字段来输出到JSON。

这两种方式的不同主要有一下两点:

  • StructuredArguments在日志事件的格式化消息中(当消息中有这个参数时)和JSON输出中
  • 如果使用复合编码器和布局器提供参数,那JSON输出中包含StructuredArguments
  • Markers只会写入到JSON输出中,而不会在日志事件的格式化消息中。
  • 如果使用LogstashEncoder/Layout,或者使用logstashMarkers提供的复合encoder/layouts,那么JSON输出中会包含Markers

即使消息没有包含参数的值,你也能使用StructureArguments。在这个例子中,参数会被写入到JSON输出和非格式化消息中(Markers能提供相同的有效行为)。你一般来说你应该使用StructureArguments,除非你有标志计数器和参数计数不匹配的静态解释器。

StructuredArgmentsmarkers需要构建其他对象。因此,最好的做法是用logger.isXXXEnable()来包含日志行。如果禁用日志级别那么避免构建对象。

使用StructuredArgment的例子:

 import static net.logstash.logback.argument.StructuredArguments.*

/*
 * Add "name":"value" to the JSON output,
 * but only add the value to the formatted message.
 *
 * The formatted message will be `log message value`
 */
logger.info("log message {}", value("name", "value"));

/*
 * Add "name":"value" to the JSON output,
 * and add name=value to the formatted message.
 *
 * The formatted message will be `log message name=value`
 */
logger.info("log message {}", keyValue("name", "value"));

/*
 * Add "name":"value" ONLY to the JSON output.
 *
 * Since there is no parameter for the argument,
 * the formatted message will NOT contain the key/value.
 *
 * If this looks funny to you or to static analyzers,
 * consider using Markers instead.
 */
logger.info("log message", keyValue("name", "value"));

/*
 * Add multiple key value pairs to both JSON and formatted message
 */
logger.info("log message {} {}", keyValue("name1", "value1"), keyValue("name2", "value2")));

/*
 * Add "name":"value" to the JSON output and
 * add name=[value] to the formatted message using a custom format.
 */
logger.info("log message {}", keyValue("name", "value", "{0}=[{1}]"));

/*
 * In the JSON output, values will be serialized by Jackson's ObjectMapper.
 * In the formatted message, values will follow the same behavior as logback
 * (formatting of an array or if not an array `toString()` is called).
 *
 * Add "foo":{...} to the JSON output and add `foo.toString()` to the formatted message:
 *
 * The formatted message will be `log message <result of foo.toString()>`
 */
Foo foo  = new Foo();
logger.info("log message {}", value("foo", foo));

/*
 * Add "name1":"value1","name2":"value2" to the JSON output by using a Map,
 * and add `myMap.toString()` to the formatted message.
 *
 * Note the values can be any object that can be serialized by Jackson's ObjectMapper
 * (e.g. other Maps, JsonNodes, numbers, arrays, etc)
 */
Map myMap = new HashMap();
myMap.put("name1", "value1");
myMap.put("name2", "value2");
logger.info("log message {}", entries(myMap));

/*
 * Add "array":[1,2,3] to the JSON output,
 * and array=[1,2,3] to the formatted message.
 */
logger.info("log message {}", array("array", 1, 2, 3));

/*
 * Add fields of any object that can be unwrapped by Jackson's UnwrappableBeanSerializer to the JSON output.
 * i.e. The fields of an object can be written directly into the JSON output.
 * This is similar to the @JsonUnwrapped annotation.
 *
 * The formatted message will contain `myobject.toString()`
 */
logger.info("log message {}", fields(myobject));

/*
 * In order to normalize a field object name, static helper methods can be created.
 * For example, `foo(Foo)` calls `value("foo" , foo)`
 */
logger.info("log message {}", foo(foo));

针对所有的结构化参数类型,简单的方法是可用的。比如,代替keyValue(key,value),你能使用kv(key,value)

比如使用Markers

import static net.logstash.logback.marker.Markers.*

/*
 * Add "name":"value" to the JSON output.
 */
logger.info(append("name", "value"), "log message");

/*
 * Add "name1":"value1","name2":"value2" to the JSON output by using multiple markers.
 */
logger.info(append("name1", "value1").and(append("name2", "value2")), "log message");

/*
 * Add "name1":"value1","name2":"value2" to the JSON output by using a map.
 *
 * Note the values can be any object that can be serialized by Jackson's ObjectMapper
 * (e.g. other Maps, JsonNodes, numbers, arrays, etc)
 */
Map myMap = new HashMap();
myMap.put("name1", "value1");
myMap.put("name2", "value2");
logger.info(appendEntries(myMap), "log message");

/*
 * Add "array":[1,2,3] to the JSON output
 */
logger.info(appendArray("array", 1, 2, 3), "log message");

/*
 * Add "array":[1,2,3] to the JSON output by using raw json.
 * This allows you to use your own json seralization routine to construct the json output
 */
logger.info(appendRaw("array", "[1,2,3]"), "log message");

/*
 * Add any object that can be serialized by Jackson's ObjectMapper
 * (e.g. Maps, JsonNodes, numbers, arrays, etc)
 */
logger.info(append("object", myobject), "log message");

/*
 * Add fields of any object that can be unwrapped by Jackson's UnwrappableBeanSerializer.
 * i.e. The fields of an object can be written directly into the json output.
 * This is similar to the @JsonUnwrapped annotation.
 */
logger.info(appendFields(myobject), "log message");

有关将JSON添加到输出的其他不建议使用的方法,请参阅DEPRECATED.md。

访问事件字段

下面的条目描述了,通过

  • LogstashAccessEncoder
  • LogstashAccessLayout,and
  • logstash的访问追加器
    编写的AccessEvent的JSON输出中默认包含的字段。

如果你使用了复合的编码器和布局器,那么写入的字段会根据你提供的配置有所不同。

标准字段

如果没有另外的说明,这些字段将会展示在每个日志事件中。这里列出的字段名称是默认的字段名称。这些字段能被自定义(详见下文)

字段 描述
@timestamp 日志事件的时间。(yyyy-MM-dd'T'HH:mm:ss.SSSZZ)详见定制时区
@version logstash格式版本 详见定制版本
@message ${remoteHost} - ${remoteUser} [${timestamp}] "${requestUrl}" ${statusCode} ${contentLength}形式的消息
@fields.method HTTP方法
@fields.protocol HTTP协议
@fields.status_code HTTP状态码
@fields.requested_url 请求路径
@fields.requested_uri 请求URI
@fields.remote_host 远程主机
@fields.HOSTNAME 远程主机的另一个字段(不确定是否存存在)
@fields.remote_user 远程用户
@fields.content_length 内容长度
@fields.elapsed_time 逝去的毫秒数

请求头字段

请求和相应的头部不是默认记录的,但是能通过指定特定的字段来开启,像如下:

<encoder class="net.logstash.logback.encoder.LogstashAccessEncoder">
  <fieldNames>
    <fieldsRequestHeaders>@fields.request_headers</fieldsRequestHeaders>
    <fieldsResponseHeaders>@fields.response_headers</fieldsResponseHeaders>
  </fieldNames>
</encoder>

详见下文的定制标准化字段名称。

用小写字母写入头部名称(所以只有大小写不同的头名称被视为是相同),设定lowerCaseFieldName为true,像如下:

<encoder class="net.logstash.logback.encoder.LogstashAccessEncoder">
  <fieldNames>
    <fieldsRequestHeaders>@fields.request_headers</fieldsRequestHeaders>
    <fieldsResponseHeaders>@fields.response_headers</fieldsResponseHeaders>
  </fieldNames>
  <lowerCaseHeaderNames>true</lowerCaseHeaderNames>
</encoder>

定制标准化的字段名称

在编码器或者追加器的配置中使用fieldNames配置字段,能使LoggingEventsAccessEvents上的标准化字段被自定义。

比如:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <fieldNames>
    <timestamp>time</timestamp>
    <message>msg</message>
    ...
  </fieldNames>
</encoder>

通过设定[ignore]字段来忽略输出中的字段。

对于LoggingEvents,参看LogstashFieldName,能定义所有的字段。每个java类中的字段名都是xml节点用于指定字段名的名称(比如,loggerlevelValue)。额外地,一个单独的缩写名称集合可以像这样配置:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <fieldNames class="net.logstash.logback.fieldnames.ShortenedFieldNames"/>
</encoder>

对于LoggingEvents,通过分别指定调用者,mdc和上下文的字段来记录JSON事件中的记录者的信息,MDC属性,上下文属性。

对于访问事件日志,查看LogstashAccessFieldName中,能被配置的所有字段。每个java类中的字段名都是xml节点用于指定字段名的名称(比如,fieldsMethodfieldsProtocol

定制版本

版本字段值默认是1。

值能被改变,如下:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <version>2</version>
</encoder>

也能写成一个字段(代替一个数字):

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <writeVersionAsString>true</writeVersionAsString>
</encoder>

定制时区

默认地,在java平台的默认时区中,时间戳会被记录。你能改变时区:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <timeZone>UTC</timeZone>
</encoder>

通过Java中的TImeZone.getTimeZone(String id)方法,timeZone的值节点能接受任何字符串参数。

定制JSON输出工厂和启动器

通过创建JsonFactoryDecoratorJsonGeneratorDecorator的实例,用于序列化输出的JsonFactoryJsonGenerator能被自定义。

比如,你能够像这样优雅地打印:

public class PrettyPrintingDecorator implements JsonGeneratorDecorator {

    @Override
    public JsonGenerator decorate(JsonGenerator generator) {
        return generator.useDefaultPrettyPrinter();
    }

}

或者自定义mapping对象:

public class ISO8601DateDecorator implements JsonFactoryDecorator  {

    @Override
    public MappingJsonFactory decorate(MappingJsonFactory factory) {
        ObjectMapper codec = factory.getCodec();
        codec.setDateFormat(new ISO8601DateFormat());
        return factory;
    }
}

像下面这样,在logback.xml中指定装饰器:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <jsonGeneratorDecorator class="your.package.PrettyPrintingDecorator"/>
  <jsonFactoryDecorator class="your.package.ISO8601DateDecorator"/>
</encoder>

定制日志的名称长度

对于LoggeringEvents,你能像%logger{36}这样普通匹配风格来缩短日志名称的长度。这里能看到缩短的例子:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <shortenedLoggerNameLength>36</shortenedLoggerNameLength>
</encoder>

定制追踪栈

针对LoggerEvents,追踪栈默认用logback的ExtendThrowableProxyConverter来格式化。但是,为了格式化追踪栈,你能用任何ThrowableHandlingCovert来配置编码器。

在logstash-logback-encoder库中包含一个强大的shortenedThrowableConverter,可以通过以下方式来格式化追踪栈:

  • 限制追踪栈每次抛出的数目(实用于每个独立的异常抛出,比如:caused-bys和suppressed)
  • 限制追踪的字符串的整体长度
  • 缩写类名
  • 基于正则表达式过滤不需要的追踪栈元素
  • 如果追踪栈应该被记录,使用evaluators来决定
  • 普通顺序输出(根记录在最末尾),或者根记录在最开始
  • 计算和内联每个异常堆栈的十六进制散列

比如:

<encoder class="net.logstash.logback.encoder.LogstashEncoder">
  <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
    <maxDepthPerThrowable>30</maxDepthPerThrowable>
    <maxLength>2048</maxLength>
    <shortenedClassNameLength>20</shortenedClassNameLength>
    <exclude>sun\.reflect\..*\.invoke.*</exclude>
    <exclude>net\.sf\.cglib\.proxy\.MethodProxy\.invoke</exclude>
    <evaluator class="myorg.MyCustomEvaluator"/>
    <rootCauseFirst>true</rootCauseFirst>
    <inlineHash>true</inlineHash>
  </throwableConverter>
</encoder>

若果你需要,在任何非JSON格式的输出中,ShortenedthrowableConvert能在PatternLayout中格式化追踪栈。

前缀和后缀

你能指定前缀(在JSON对象之前写入)或后缀(在JSON对象之后写入),也可以同时指定。这可能是你使用日志管道所需要的。例如:

  • 如果你使用the Common Event Expression格式的syslog,你需要添加@cee的前缀。
  • 如果你使用其他syslog的目的地,你需要添加标准的syslog头部消息。
  • 如果你使用Loggly,你也许需要添加自定义的token。

比如,为UDPsyslog添加标准的syslog头部消息,像如下配置:

<configuration>
  <conversionRule conversionWord="syslogStart" converterClass="ch.qos.logback.classic.pattern.SyslogStartConverter"/>

  <appender name="stash" class="net.logstash.logback.appender.LogstashSocketAppender">
    <host>MyAwesomeSyslogServer</host>
    <!-- port is optional (default value shown) -->
    <port>514</port>
    <prefix class="ch.qos.logback.classic.PatternLayout">
      <pattern>%syslogStart{USER}</pattern>
    </prefix>
  </appender>

  ...
</configuration>

当使用LogstashEncoderLogstashAccessEncoder或者自定义的encoder,那么前缀是Encoder,而不是Layout,所以你需要在LayoutWrappingEncoder中包含PatternLayout,像如下:

<configuration>
  ...
  <appender ...>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      ...
      <prefix class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="ch.qos.logback.classic.PatternLayout">
          <pattern>@cee:</pattern>
        </layout>
      </prefix>    
    </encoder>
  </appender>
</configuration>

注意,logback.xml的配置读取器会忽略xml元素中的空白。因此,如果你想用空白来匹配前缀或后缀,那首先添加空白,然后添加像%mdc{keyThatDoesNotExist}这样的串,之后会像<pattern>your pattern %mdc{keyThatDoesNotExist}</pattern>一样。这将会导致logback根据需要输出空格,然后是不存在MDC秘钥的空白字符串。

复合编码器/样式布局器

如果你想在JSON格式和LoggingEventAccessEvents数据中高度自定义,使用LoggingEventCompositeJsonEncoderAccessEventCompositeJsonEncoder(或者对应的layouts)。

这些encoder/layouts由一个或多个提供JSON输出的JSON生成器复合组成。在复合encoders/layouts中没有提供者被默认配置。你必须自己添加你想要的。

比如:

<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
  <providers>
    <mdc/>
    <pattern>
      <pattern>
        {
          "timestamp": "%date{ISO8601}",
          "myCustomField": "fieldValue",
          "relative": "#asLong{%relative}"
        }
      </pattern>
    </pattern>
    <stackTrace>
      <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
        <maxDepthPerThrowable>30</maxDepthPerThrowable>
        <maxLength>2048</maxLength>
        <shortenedClassNameLength>20</shortenedClassNameLength>
        <exclude>^sun\.reflect\..*\.invoke</exclude>
        <exclude>^net\.sf\.cglib\.proxy\.MethodProxy\.invoke</exclude>
        <evaluator class="myorg.MyCustomEvaluator"/>
        <rootCauseFirst>true</rootCauseFirst>
      </throwableConverter>
    </stackTrace>
  </providers>
</encoder>

logstash-logback-encoder库包含了多个开箱即用的提供器。你甚至能把你自己导出的JsonProvider做成插件。每个提供者都有自己配置项来进一步定制他。

LoggerEvents提供者

对于LoggingEvents,可用的提供器和配置属性(默认附带地)如下:

提供者 描述和属性
timestamp 时间戳事件
fieldName - 输出的字段名(@timestamp
pattern - 输出格式(yyyy-MM-dd 'T' HH:mm:ss.SSSZZ)
timeZone - 时区(当地时间)
version LogstashJSON的格式版本
fieldName - 输出的字段名(@version
version - 输出值1
writeAsString - 版本号写成的字符串值(false=写成一个数字)
message 格式化的记录事件信息
fieldName - 输出的字段名(message
rawMessage 原生的记录事件消息,和参数被解析的格式化日志相对
fieldName - 输出的字段名(raw_message
loggerName 原生的记录事件消息,和参数被解析的格式化日志相对
fieldName - 输出的字段名(raw_message
threadName 记录消息事件的线程名称
fieldName - 输出的字段名(thread_name
logLevel 记录级别文本(比如INFO,WARN等)
fieldName - 输出的字段名(level
logLevelValue 记录级别的数字记录
fieldName - 输出的字段名(level_value
callerData 关于日志输出输出者的位置消息(类/方法/文件/行)
fieldName - 子对象的字段名(没有子对象)
classFieldName - 类名的字段名(caller_class_name
methodFieldName - 方法名的字段名(caller_method_name
fileFieldName - 文件名的字段名(caller_file_name
lineFieldName - 行号的字段名(caller_line_number
stackTrace 事件的抛出记录的堆栈追踪。堆栈信息通过空行来分隔。
fieldName - 输出的字段名(stack_trace
throwableConverter - ThrowableHandlingConverter用于格式化追踪栈。
stackHash (只有抛出被记录)计算和输出抛出栈的十六进制hash码
帮助定义同时发生的相同错误
fieldName - 输出的字段名(stack_hash
exclude - 在计算错误hash时,正则表达式匹配要排除的追踪栈元素
exclusions - 在计算错误hash时,匹配了要排除的追踪栈元素的正则表达式列表。
context 输出事件的logback上下文。
fieldName - 子对象的名称(不再有更多子对象)
contextName logback上下文的输出字段名称
fieldName - 输出的字段名(context
mdc 来自the Mapped Diagnostic Context(MDC)的输出事件。默认包含所有事件。当包含了关键字段,所有其他的字段将会排除。当排除了关键字段,其他关键字段将会被包含。同时包含排除和包含字段是一种错误的配置。
fieldName - 子对象名称(不再有更多的子对象
includeMdckeyName - 包含的关键字名称(all
excludeMdckeyName - 包含的关键字名称(none
tags 作为一个分隔号列表的logback输出标记
fieldName - 输出的字段名(tags
logstashMarkers 在事件定制化字段中,用于输出指定的Logstash标记。
nestedField 被配置的字段中嵌套的JSON对象。
嵌套对象经常通过其他提供者添加到这种提供者中。
详见嵌套式的json提供器
fieldName - 输出的字段名
providers - 被嵌套对象填充的提供者。
pattern 当logback的PatternLayout替换支持的模式,配置的JSON对象字符串的输出字段。
详见模式化的json提供器
pattern - JSON对象字符串(不是默认的
arguments 参数数组对象的输出字段
详见具体事件定义字段
fieldName - 子对象的字段名(没有更多的子对象
includeNonStructureArguments - 包含的参数,且不是StructuredArgument的实体。(默认是false)对象字段名称将是参数索引的nonStructuredArgument前置。
nonStructuredArgment - 对象字段名称的前缀(默认是arg)
uuid 作为字段值输出的随机UUID,为想提供记录行唯一身份认证提供了便利。
fieldName - 输出的字段名(uuid
strategy - UUID的普遍性策略(random)。支持的字段有random,time
ethernet - 只有“time”策略能用。当被定义后,MAC地址会是UUID的组成之一。将他设置为接口值以使用真实的底层网络接口或特定值(如 00:C0:F0:3D:5B:7C)。
AccessEvents提供者

对于AccessEvents,可用的提供器和配置属性(默认附带地)如下:

提供者 描述和属性
timestamp 时间戳事件
fieldName - 输出的字段名(@timestamp
pattern - 输出格式(yyyy-MM-dd 'T' HH:mm:ss.SSSZZ)
timeZone - 时区(当地时间)
version LogstashJSON的格式版本
fieldName - 输出的字段名(@version
version - 输出值1
writeAsString - 版本号写成的字符串值(false=写成一个数字)
message 形如${remoteHost} - ${remoteUser} [${timestamp}] "${requestUrl}" ${statusCode} ${contentLength}的消息。
fieldName - 输出的字段名(@message
method HTTP的方法。
fieldName - 输出的字段名(@fields.method
protocol HTTP协议
fieldName - 输出的字段名(@fields.protocol
statusCode HTTP状态码
fieldName - 输出的字段名(@fields.status_code
requestedUrl 请求的URL
fieldName - 输出的字段名(@fields.requested_url
requestedUri 请求的URI
fieldName - 输出的字段名(@fields.requested_uri
remoteHost 远端地址
fieldName - 输出的字段名(@fields.remote_host
remoteUser 远端用户
fieldName - 输出的字段名(@fields.remote_user
contentLength 内容长度
fieldName - 输出的字段名(@fields.content_length
elapsedTime 逝去的毫秒数
fieldName - 输出的字段名(@fields.elapsed_time)
requestHeaders 请求头
fieldName - 输出的字段名(没有默认,必须提供)
lowerCaseHeaderNames - 小写字母写入头名称(false)
responseHeaders 相应头
fieldName - 输出的字段名(没有默认,必须提供)
lowerCaseHeaderNames - 小写字母写入头名称(false)
nestedField 被配置的字段中嵌套的JSON对象。
嵌套对象经常通过其他提供者添加到这种提供者中。
详见嵌套式的json提供器
fieldName - 输出的字段名
providers - 被嵌套对象填充的提供者。
pattern 当logback的PatternLayout替换支持的模式,配置的JSON对象字符串的输出字段。
详见模式化的json提供器
pattern - JSON对象字符串(不是默认的
pattern 当logback access的PatternLayout替换支持的模式,配置的JSON对象字符串的输出字段。
pattern - JSON对象字符串(没有默认)

嵌套式的json提供器

在JSON事件输出中,使用嵌套字段提供器,创建子对象。

比如:

<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
  <providers>
    <timestamp/>
    <nestedField>
      <fieldName>@fields</fieldName>
      <providers>
        <logLevel/>
      </providers>
    </nestedField>
  </providers>
</encoder>

将会产生如下的结果:

{
  "@timestamp":"...",
  "@fields":{
    "level": "DEBUG"
  }
}

模式化的json提供器

当使用复合式JSON encoder/layout时,patternJSON提供者能用来对部分记录JSON的输出定义模板。在模板中,encoder/layout会填充值。模板中的每个值都被视作的logack标准PatternLayout的模式。所以能复合字符串(一些常量)和变量转换符(像是日期中的%d)。

模式字符串必须是JSON格式(在模式提供者中被配置)。JSON记录输出包含了JSON对象常量。

比如:

<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
  <providers>
    <!-- provides the timestamp -->
    <timestamp/>

    <!-- provides the version -->
    <version/>

    <!-- provides the fields in the configured pattern -->
    <pattern>
      <!-- the pattern that defines what to include -->
      <pattern>
        { "level": "%level" }
      </pattern>
    </pattern>
  </providers>
</encoder>

会产生如下的结果:

{
  "@timestamp":"...",
  "@version": 1,
  "level": "DEBUG"
}

正真厉害的事实是有很多标准转换符,所以你能自定义你记录什么和如何记录。比如:你能从MDC中通过%mdc{mykey}记录单个指定的值。或者,对于访问日志,你可以用%i{User-Agent}记录单个请求头。

你能在你的模式中使用嵌套对象和数组

如果你在一个模式中使用null,数字,或者布尔常量,这些会在JSON结果保持类型不变。然而对于转换模式来说,只有字符串的值将会被搜索。因为这些模式是通过PatternLayout发送的。结果总是一个字符串,甚至是有些你觉得会是数字的值,就像%b(字节发送,在访问日志中)。

你能在logstash一侧处理类型转换或是通过这些编码器使用指定转换提供者。

  • #asLong{...} - 评估大括号中的内容,把字符串转换为long类型(或者转换失败变为null)
  • #asDouble{...} - 评估大括号中的内容,把字符串转换为Double类型(或者转换失败变为null)
  • #asJson{...} - 评估大括号中的内容,把字符串转换为json类型(或者转换失败变为null)

举例:

<pattern>
  {
    "line_str": "%line",
    "line_long": "#asLong{%line}",
    "has_message": "#asJson{%mdc{hasMessage}}",
    "json_message": "#asJson{%message}"
  }
</pattern>

这是记录代码:

MDC.put("hasMessage", "true");
LOGGER.info("{\"type\":\"example\",\"msg\":\"example of json message with type\"}");

将会产生如下的结果:

{
  "line_str":"97",
  "line_long":97,
  "has_message":true,
  "json_message":{"type":"example","msg":"example of json message with type"}
}

注意,发送给line_long的值是数字类型,即使在你的模式中这是一个应用文本。json_message字段是json对象,不是字符串。

普通日志事件的格式化

对于记录事件,推荐logback的经典模式PatternLayout

比如:

<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
  <providers>
    <timestamp/>
    <pattern>
      <pattern>
        {
        "custom_constant": "123",
        "tags": ["one", "two"],
        "logger": "%logger",
        "level": "%level",
        "thread": "%thread",
        "message": "%message",
...
        }
      </pattern>
    </pattern>
  </providers>
</encoder>
访问日志事件的格式化

对于访问事件,推荐logback访问事件的经典模式PatternLayout

比如:

<encoder class="net.logstash.logback.encoder.AccessEventCompositeJsonEncoder">
  <providers>
    <pattern>
      <pattern>
        {
        "custom_constant": "123",
        "tags": ["one", "two"],
        "remote_ip": "%a",
        "status_code": "%s",
        "elapsed_time": "%D",
        "user_agent": "%i{User-Agent}",
        "accept": "%i{Accept}",
        "referer": "%i{Referer}",
        "session": "%requestCookie{JSESSIONID}",
...
        }
      </pattern>
    </pattern>
  </providers>
</encoder>

这也是被AccessEVents使用的特定操作符:

  • #nullNA{...} - 如果大括号中的值为破折号,将会被null替代。

你会这么做,因为很多的来自lobback-access的PatternLayout转换符会被解释成没有任何值“-”(比如cookie,头信息或者请求属性)。

所以用这种模式:

<pattern>
  {
    "default_cookie": "%requestCookie{MISSING}",
    "filtered_cookie": "#nullNA{%requestCookie{MISSING}}"
  }
</pattern>

将会产生:

{
  "default_cookie": "-",
  "filtered_cookie": null
}

如何debug

在执行过程中,在logstash-logback-encoder中的encoders/appenders/layouts将会添加logback的状态信息到logback的StatusManager

默认地,配置阶段中logabck只能在控制台中显示 WARN/ERROR 状态消息。 在真事环境中没有信息会显示(甚至是 WARN/ERROR)。

如果你对定义问题有麻烦(比如事件没有获得),你能打开logback的debug模式,或者在logback手册中添加状态监听器作为详细说明。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,358评论 6 343
  • 1 一直想让自己成为一个大气的女生,但有时总会觉得自己小肚鸡肠。作为女生,我认为,女生必须要对自己负责,要学会对自...
    我是王慧阅读 1,707评论 6 14
  • 搓腿动作
    英姿飒爽0920阅读 292评论 0 0
  • 下班回到家,儿子还在睡觉,我没有去打扰,儿子应该很累,熬夜伤身,他是在过美国时间,我平静的煮了饺子吃,开始...
    紫玉_b836阅读 209评论 1 0