JSON 模板布局

JsonTemplateLayout 是一种可定制、高效且无垃圾的 JSON 生成布局。它根据提供的 JSON 模板中描述的结构对 LogEvent 进行编码。简而言之,它以其

用法

log4j-layout-template-json 工件添加到您的依赖项列表中,就足以在您的 Log4j 配置中启用对 JsonTemplateLayout 的访问

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-layout-template-json</artifactId>
    <version>2.23.1</version>
</dependency>

例如,假设以下 JSON 模板对 Elastic Common Schema (ECS) 规范 进行建模(可通过 classpath:EcsLayout.json 访问)

{
  "@timestamp": {
    "$resolver": "timestamp",
    "pattern": {
      "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
      "timeZone": "UTC"
    }
  },
  "ecs.version": "1.2.0",
  "log.level": {
    "$resolver": "level",
    "field": "name"
  },
  "message": {
    "$resolver": "message",
    "stringified": true
  },
  "process.thread.name": {
    "$resolver": "thread",
    "field": "name"
  },
  "log.logger": {
    "$resolver": "logger",
    "field": "name"
  },
  "labels": {
    "$resolver": "mdc",
    "flatten": true,
    "stringified": true
  },
  "tags": {
    "$resolver": "ndc"
  },
  "error.type": {
    "$resolver": "exception",
    "field": "className"
  },
  "error.message": {
    "$resolver": "exception",
    "field": "message"
  },
  "error.stack_trace": {
    "$resolver": "exception",
    "field": "stackTrace",
    "stackTrace": {
      "stringified": true
    }
  }
}

结合以下 log4j2.xml 配置

<JsonTemplateLayout eventTemplateUri="classpath:EcsLayout.json"/>

或以下 log4j2.properties 配置

appender.console.layout.type = JsonTemplateLayout
appender.console.layout.eventTemplateUri = classpath:EcsLayout.json

JsonTemplateLayout 生成如下 JSON

{
  "@timestamp": "2017-05-25T19:56:23.370Z",
  "ecs.version": "1.2.0",
  "log.level": "ERROR",
  "message": "Hello, error!",
  "process.thread.name": "main",
  "log.logger": "org.apache.logging.log4j.JsonTemplateLayoutDemo",
  "error.type": "java.lang.RuntimeException",
  "error.message": "test",
  "error.stack_trace": "java.lang.RuntimeException: test\n\tat org.apache.logging.log4j.JsonTemplateLayoutDemo.main(JsonTemplateLayoutDemo.java:11)\n"
}

布局配置

JsonTemplateLayout 使用以下参数进行配置

表 1. JsonTemplateLayout 参数

参数名称

类型

描述

charset

Charset

用于 String 编码的 Charset

locationInfoEnabled

boolean

切换对 LogEvent 源的访问;文件名、行号等(默认为 false,由 log4j.layout.jsonTemplate.locationInfoEnabled 属性设置)

stackTraceEnabled

boolean

切换对堆栈跟踪的访问(默认为 true,由 log4j.layout.jsonTemplate.stackTraceEnabled 属性设置)

eventTemplate

String

用于渲染 LogEvent 的内联 JSON 模板(优先于 eventTemplateUri,默认为 null,由 log4j.layout.jsonTemplate.eventTemplate 属性设置)

eventTemplateUri

String

指向用于渲染 LogEvent 的 JSON 模板的 URI(默认为 classpath:EcsLayout.json,由 log4j.layout.jsonTemplate.eventTemplateUri 属性设置)

eventTemplateRootObjectKey

String

如果存在,则将事件模板放入一个 JSON 对象中,该对象由一个具有提供键的成员组成(默认为 null,由 log4j.layout.jsonTemplate.eventTemplateRootObjectKey 属性设置)

eventTemplateAdditionalField

EventTemplateAdditionalField[]

附加到事件模板根部的附加键值对

stackTraceElementTemplate

String

用于渲染 StackTraceElement 的内联 JSON 模板(优先于 stackTraceElementTemplateUri,默认为 null,由 log4j.layout.jsonTemplate.stackTraceElementTemplate 属性设置)

stackTraceElementTemplateUri

String

指向用于渲染 StackTraceElement 的 JSON 模板的 URI(默认为 classpath:StackTraceElementLayout.json,由 log4j.layout.jsonTemplate.stackTraceElementTemplateUri 属性设置)

eventDelimiter

String

用于分隔渲染的 LogEvent 的分隔符(默认为 System.lineSeparator(),由 log4j.layout.jsonTemplate.eventDelimiter 属性设置)

nullEventDelimiterEnabled

boolean

\0null)字符追加到每个分隔渲染的 LogEventeventDelimiter 的末尾(默认为 false,由 log4j.layout.jsonTemplate.nullEventDelimiterEnabled 属性设置)

maxStringLength

int

截断长度超过指定限制的字符串值(默认为 16384,由 log4j.layout.jsonTemplate.maxStringLength 属性设置)

truncatedStringSuffix

String

追加到由于超过 maxStringLength 而被截断的字符串的后缀(默认为 ,由 log4j.layout.jsonTemplate.truncatedStringSuffix 属性设置)

recyclerFactory

RecyclerFactory

回收策略,可以是 dummythreadLocalqueue(由 log4j.layout.jsonTemplate.recyclerFactory 属性设置)

附加事件模板字段

附加事件模板字段是将自定义字段添加到模板或覆盖现有字段的便捷快捷方式。以下配置将覆盖 GelfLayout.json 模板的 host 字段,并添加两个新的自定义字段

具有附加字段的 XML 配置
<JsonTemplateLayout eventTemplateUri="classpath:GelfLayout.json">
  <EventTemplateAdditionalField key="host" value="www.apache.org"/>
  <EventTemplateAdditionalField key="_serviceName" value="auth-service"/>
  <EventTemplateAdditionalField key="_containerId" value="6ede3f0ca7d9"/>
</JsonTemplateLayout>

添加的新字段的默认 formatString。也可以提供 JSON 格式的附加字段

具有 JSON 格式附加字段的 XML 格式配置
<JsonTemplateLayout eventTemplateUri="classpath:GelfLayout.json">
  <EventTemplateAdditionalField
       key="marker"
       format="JSON"
       value='{"$resolver": "marker", "field": "name"}'/>
  <EventTemplateAdditionalField
       key="aNumber"
       format="JSON"
       value="1"/>
  <EventTemplateAdditionalField
       key="aList"
       format="JSON"
       value='[1, 2, "three"]'/>
</JsonTemplateLayout>

可以使用属性、YAML 和 JSON 格式的配置引入附加事件模板字段

具有 JSON 格式附加字段的属性格式配置
appender.console.layout.type = JsonTemplateLayout
appender.console.layout.eventTemplateUri = classpath:GelfLayout.json
appender.console.layout.eventTemplateAdditionalField[0].type = EventTemplateAdditionalField
appender.console.layout.eventTemplateAdditionalField[0].key = marker
appender.console.layout.eventTemplateAdditionalField[0].value = {"$resolver": "marker", "field": "name"}
appender.console.layout.eventTemplateAdditionalField[0].format = JSON
appender.console.layout.eventTemplateAdditionalField[1].type = EventTemplateAdditionalField
appender.console.layout.eventTemplateAdditionalField[1].key = aNumber
appender.console.layout.eventTemplateAdditionalField[1].value = 1
appender.console.layout.eventTemplateAdditionalField[1].format = JSON
appender.console.layout.eventTemplateAdditionalField[2].type = EventTemplateAdditionalField
appender.console.layout.eventTemplateAdditionalField[2].key = aList
appender.console.layout.eventTemplateAdditionalField[2].value = [1, 2, "three"]
appender.console.layout.eventTemplateAdditionalField[2].format = JSON
具有 JSON 格式附加字段的 YAML 格式配置
JsonTemplateLayout:
  eventTemplateAdditionalField:
    - key: "marker"
      value: '{"$resolver": "marker", "field": "name"}'
      format: "JSON"
    - key: "aNumber"
      value: "1"
      format: "JSON"
    - key: "aList"
      value: '[1, 2, "three"]'
      format: "JSON"
具有 JSON 格式附加字段的 JSON 格式配置
{
  "JsonTemplateLayout": {
    "eventTemplateAdditionalField": [
      {
        "key": "marker",
        "value": "{\"$resolver\": \"marker\", \"field\": \"name\"}",
        "format": "JSON"
      },
      {
        "key": "aNumber",
        "value": "1",
        "format": "JSON"
      },
      {
        "key": "aList",
        "value": "[1, 2, \"three\"]",
        "format": "JSON"
      }
    ]
  }
}

回收策略

RecyclerFactory 在确定布局的内存占用方面起着至关重要的作用。模板解析器使用它来创建它们可以重复使用的对象的回收器。每个 RecyclerFactory 的行为以及何时应该优先选择一个而不是另一个将在下面解释

  • dummy 不执行任何回收,因此每次回收尝试都会导致一个新实例。这显然会给垃圾收集器带来负担。对于日志速率低和中等应用程序来说,这是一个不错的选择。

  • threadLocal 的性能最佳,因为每个实例都存储在 ThreadLocal 中,并且可以无任何同步成本地访问。尽管对于运行数百个或更多线程的应用程序(例如,Web servlet)来说,这可能不是一个理想的选择。

  • queue 是两全其美的选择。它允许回收一定数量的对象(capacity)。当由于过度的并发负载而超过此限制时(例如,capacity 为 50,但有 51 个线程同时尝试记录),它将开始分配。queue 是一种很好的策略,在 threadLocal 不理想的情况下使用。

    queue 还接受可选的 supplier(类型为 java.util.Queue,如果 JCTools 在类路径中,则默认为 org.jctools.queues.MpmcArrayQueue.new;否则为 java.util.concurrent.ArrayBlockingQueue.new)和 capacity(类型为 int,默认为 max(8,2*cpuCount+1))参数

    queue 回收策略的示例配置
    queue:supplier=org.jctools.queues.MpmcArrayQueue.new
    queue:capacity=10
    queue:supplier=java.util.concurrent.ArrayBlockingQueue.new,capacity=50

如果 log4j2.enable.threadlocals=true,则默认的 RecyclerFactorythreadLocal;否则为 queue

有关如何引入自定义 RecyclerFactory 实现的详细信息,请参见 扩展回收器工厂

模板配置

模板通过以下 JsonTemplateLayout 参数进行配置

  • eventTemplate[Uri](用于序列化 LogEvent

  • stackTraceElementTemplate[Uri](用于序列化 StackStraceElement

  • eventTemplateAdditionalField(用于扩展使用的事件模板)

事件模板

eventTemplate[Uri] 描述了 JsonTemplateLayout 用于序列化 LogEvent 的 JSON 结构。默认配置(可通过 log4j.layout.jsonTemplate.eventTemplate[Uri] 属性访问)设置为 log4j-layout-template-json 工件提供的 classpath:EcsLayout.json,其中包含以下预定义的事件模板

事件模板解析器

事件模板解析器使用 LogEvent 并渲染其在 JSON 中声明位置的特定属性。例如,marker 解析器渲染事件的标记,level 解析器渲染级别,等等。事件模板解析器用一个包含 $resolver 键的特殊对象表示

演示 level 解析器用法的示例事件模板
{
  "version": "1.0",
  "level": {
    "$resolver": "level",
    "field": "name"
  }
}

这里 version 字段将按原样渲染,而 level 字段将由 level 解析器填充。也就是说,此模板将生成类似于以下内容的 JSON

从演示的事件模板生成的示例 JSON
{
  "version": "1.0",
  "level": "INFO"
}

下面将详细提供所有可用事件模板解析器的完整列表。

counter
config      = [ start ] , [ overflowing ] , [ stringified ]
start       = "start" -> number
overflowing = "overflowing" -> boolean
stringified = "stringified" -> boolean

从内部计数器解析一个数字。

除非提供,否则 startoverflowing 分别默认设置为零和 true

stringified 启用时,默认情况下设置为 false,解析的数字将转换为字符串。

警告

overflowing 设置为 true 时,内部计数器使用 long 创建,在递增时会发生溢出,但不会产生垃圾。否则,将使用 BigInteger,它不会溢出,但会产生分配成本。

示例

解析从 0 开始的数字序列。一旦达到 Long.MAX_VALUE,计数器将溢出到 Long.MIN_VALUE

{
  "$resolver": "counter"
}

解析从 1000 开始的数字序列。一旦达到 Long.MAX_VALUE,计数器将溢出到 Long.MIN_VALUE

{
  "$resolver": "counter",
  "start": 1000
}

解析从 0 开始的数字序列,并只要 JVM 堆允许就一直进行下去。

{
  "$resolver": "counter",
  "overflowing": false
}
caseConverter
config                = case , input , [ locale ] , [ errorHandlingStrategy ]
input                 = JSON
case                  = "case" -> ( "upper" | "lower" )
locale                = "locale" -> (
                            language                                   |
                          ( language , "_" , country )                 |
                          ( language , "_" , country , "_" , variant )
                        )
errorHandlingStrategy = "errorHandlingStrategy" -> (
                          "fail"    |
                          "pass"    |
                          "replace"
                        )
replacement           = "replacement" -> JSON

转换字符串值的案例。

input 可以是任何可用的模板值;例如,JSON 文字、查找字符串、指向另一个解析器的对象。

除非提供,否则 locale 指向由 JsonTemplateLayoutDefaults.getLocale() 返回的区域设置,该区域设置由 log4j.layout.jsonTemplate.locale 系统属性配置,默认情况下设置为默认系统区域设置。

errorHandlingStrategy 确定输入无法解析为字符串值或大小写转换引发异常时的行为

  • fail 传播失败

  • pass 导致解析的值按原样传递

  • replace 抑制失败并将其替换为 replacement,默认情况下设置为 null

errorHandlingStrategy 默认情况下设置为 replace

大多数情况下,JSON 日志会持久化到存储解决方案(例如 Elasticsearch)中,该解决方案在字段上保留静态类型索引。因此,如果始终期望字段为字符串类型,则在 errorHandlingStrategy 中使用非字符串 replacementpass 可能会导致存储级别出现类型不兼容问题。

警告

除非输入值被完整 passreplace,否则大小写转换不会产生垃圾。

示例

将解析的日志级别字符串转换为大写

{
  "$resolver": "caseConverter",
  "case": "upper",
  "input": {
    "$resolver": "level",
    "field": "name"
  }
}

使用 nl_NL 区域设置将解析的 USER 环境变量转换为小写

{
  "$resolver": "caseConverter",
  "case": "lower",
  "locale": "nl_NL",
  "input": "${env:USER}"
}

将解析的 sessionId 线程上下文数据 (MDC) 转换为小写

{
  "$resolver": "caseConverter",
  "case": "lower",
  "input": {
    "$resolver": "mdc",
    "key": "sessionId"
  }
}

在上面,如果 sessionId MDC 解析为一个数字,例如,大小写转换将失败。由于 errorHandlingStrategy 设置为 replace,并且替换默认情况下设置为 null,因此解析的值将为 null。可以抑制此行为,并让解析的 sessionId 数字保持原样

{
  "$resolver": "caseConverter",
  "case": "lower",
  "input": {
    "$resolver": "mdc",
    "key": "sessionId"
  },
  "errorHandlingStrategy": "pass"
}

或将其替换为自定义字符串

{
  "$resolver": "caseConverter",
  "case": "lower",
  "input": {
    "$resolver": "mdc",
    "key": "sessionId"
  },
  "errorHandlingStrategy": "replace",
  "replacement": "unknown"
}
endOfBatch
{
  "$resolver": "endOfBatch"
}

解析 logEvent.isEndOfBatch() 布尔标志。

exception
config              = field , [ stringified ] , [ stackTrace ]
field               = "field" -> ( "className" | "message" | "stackTrace" )

stackTrace          = "stackTrace" -> (
                        [ stringified ]
                      , [ elementTemplate ]
                      )

stringified         = "stringified" -> ( boolean | truncation )
truncation          = "truncation" -> (
                        [ suffix ]
                      , [ pointMatcherStrings ]
                      , [ pointMatcherRegexes ]
                      )
suffix              = "suffix" -> string
pointMatcherStrings = "pointMatcherStrings" -> string[]
pointMatcherRegexes = "pointMatcherRegexes" -> string[]

elementTemplate     = "elementTemplate" -> object

解析由 logEvent.getThrown() 返回的 Throwable 的字段。

stringified 默认情况下设置为 false。根级别的 stringified 已被 stackTrace.stringified 弃用,如果两者都提供,则 stackTrace.stringified 优先。

pointMatcherStringspointMatcherRegexes 允许在给定匹配点后截断字符串化的堆栈跟踪。如果两个参数都提供,则首先检查 pointMatcherStrings

如果发生字符串化的堆栈跟踪截断,则将使用 suffix 指示,默认情况下设置为布局中配置的 truncatedStringSuffix,除非显式提供。每个截断后缀都以换行符为前缀。

字符串化的堆栈跟踪截断在 Caused by:Suppressed: 标签块中进行。也就是说,匹配器在每个标签中独立执行。

elementTemplate 是一个对象,描述在解析 StackTraceElement 数组时要使用的模板。如果 stringified 设置为 true,则 elementTemplate 将被丢弃。默认情况下,elementTemplate 设置为 null,而是从布局配置中填充。也就是说,堆栈跟踪元素模板也可以使用 stackTraceElementTemplate[Uri] 布局配置参数提供。要使用的模板将按以下顺序确定

  1. 解析器配置中提供的 elementTemplate

  2. 布局配置中的 stackTraceElementTemplate 参数(默认情况下从 log4j.layout.jsonTemplate.stackTraceElementTemplate 系统属性填充)

  3. 布局配置中的 stackTraceElementTemplateUri 参数(默认情况下从 log4j.layout.jsonTemplate.stackTraceElementTemplateUri 系统属性填充)

有关堆栈跟踪元素模板中可用解析器的列表,请参见 堆栈跟踪元素模板

请注意,此解析器由 log4j.layout.jsonTemplate.stackTraceEnabled 属性切换。

警告

由于 Throwable#getStackTrace() 克隆了原始 StackTraceElement[],因此对堆栈跟踪的访问(以及渲染)不会产生垃圾。

每个 pointMatcherRegexes 项目都会触发 Pattern#matcher() 调用,这也不会产生垃圾。

示例

解析 logEvent.getThrown().getClass().getCanonicalName()

{
  "$resolver": "exception",
  "field": "className"
}

将堆栈跟踪解析为 StackTraceElement 对象列表

{
  "$resolver": "exception",
  "field": "stackTrace"
}

将堆栈跟踪解析为字符串字段

{
  "$resolver": "exception",
  "field": "stackTrace",
  "stackTrace": {
    "stringified": true
  }
}

将堆栈跟踪解析为字符串字段,以便在给定点匹配器后截断内容

{
  "$resolver": "exception",
  "field": "stackTrace",
  "stackTrace": {
    "stringified": {
      "truncation": {
        "suffix": "... [truncated]",
        "pointMatcherStrings": ["at javax.servlet.http.HttpServlet.service"]
      }
    }
  }
}

将堆栈跟踪解析为由提供的堆栈跟踪元素模板描述的对象

{
  "$resolver": "exception",
  "field": "stackTrace",
  "stackTrace": {
    "elementTemplate": {
      "class": {
       "$resolver": "stackTraceElement",
       "field": "className"
      },
      "method": {
       "$resolver": "stackTraceElement",
       "field": "methodName"
      },
      "file": {
       "$resolver": "stackTraceElement",
       "field": "fileName"
      },
      "line": {
       "$resolver": "stackTraceElement",
       "field": "lineNumber"
      }
    }
  }
}

有关 StackTraceElement 模板中可用解析器的更多详细信息,请参见 堆栈跟踪元素模板

exceptionRootCause

解析由 logEvent.getThrown() 返回的最内部 Throwable 的字段。其语法和垃圾占用与 exception 解析器相同。

level
config         = field , [ severity ]
field          = "field" -> ( "name" | "severity" )
severity       = severity-field
severity-field = "field" -> ( "keyword" | "code" )

解析 logEvent.getLevel() 的字段。

示例

解析级别名称

{
  "$resolver": "level",
  "field": "name"
}

解析 Syslog 严重性 关键字

{
  "$resolver": "level",
  "field": "severity",
  "severity": {
    "field": "keyword"
  }
}

解析 Syslog 严重性 代码

{
  "$resolver": "level",
  "field": "severity",
  "severity": {
    "field": "code"
  }
}
logger
config = "field" -> ( "name" | "fqcn" )

解析 logEvent.getLoggerFqcn()logEvent.getLoggerName()

示例

解析记录器名称

{
  "$resolver": "logger",
  "field": "name"
}

解析记录器的完全限定类名

{
  "$resolver": "logger",
  "field": "fqcn"
}
main
config = ( index | key )
index  = "index" -> number
key    = "key" -> string

对给定的 indexkey 执行 主参数查找

示例

解析第一个 main() 方法参数

{
  "$resolver": "main",
  "index": 0
}

解析紧随 --userId 后的参数

{
  "$resolver": "main",
  "key": "--userId"
}
map

解析 MapMessage。有关详细信息,请参见 地图解析器模板

marker
config = "field" -> ( "name" | "parents" )

解析 logEvent.getMarker()

示例

解析标记名称

{
  "$resolver": "marker",
  "field": "name"
}

解析标记父级的名称

{
  "$resolver": "marker",
  "field": "parents"
}
mdc

解析映射诊断上下文 (MDC),也称为线程上下文数据。有关详细信息,请参见 地图解析器模板

警告

需要打开 log4j2.garbagefreeThreadContextMap 标志才能在没有分配的情况下迭代地图。

message
config      = [ stringified ] , [ fallbackKey ]
stringified = "stringified" -> boolean
fallbackKey = "fallbackKey" -> string

解析 logEvent.getMessage()

警告

对于简单的字符串消息,解析在没有分配的情况下执行。对于 ObjectMessageMultiformatMessage,则取决于情况。

示例

将消息解析为字符串

{
  "$resolver": "message",
  "stringified": true
}

解析消息,以便如果它是 ObjectMessage 或具有 JSON 支持的 MultiformatMessage,则会保留其类型(字符串、列表、对象等)

{
  "$resolver": "message"
}

鉴于上述配置,SimpleMessage 将生成 "sample log message",而 MapMessage 将生成 {"action": "login", "sessionId": "87asd97a"}。某些索引日志存储系统(例如 Elasticsearch)不允许这两个值共存,因为类型不匹配:一个是 string,另一个是 object。这里可以使用 fallbackKey 来解决此问题

{
  "$resolver": "message",
  "fallbackKey": "formattedMessage"
}

使用此配置,SimpleMessage 将生成 {"formattedMessage": "sample log message"},而 MapMessage 将生成 {"action": "login", "sessionId": "87asd97a"}。请注意,两个发出的 JSON 都是 object 类型,并且没有类型冲突的字段。

messageParameter
config      = [ stringified ] , [ index ]
stringified = "stringified" -> boolean
index       = "index" -> number

解析 logEvent.getMessage().getParameters()

警告

关于垃圾占用,stringified 标志转换为 String.valueOf(value),因此请注意非 String 类型的值。此外,logEvent.getMessage() 预计实现 ParameterVisitable 接口,如果 log4j2.enableThreadLocals 属性设置为 true,则情况如此。

示例

将消息参数解析为数组

{
  "$resolver": "messageParameter"
}

将所有消息参数的字符串表示形式解析为数组

{
  "$resolver": "messageParameter",
  "stringified": true
}

解析第一个消息参数

{
  "$resolver": "messageParameter",
  "index": 0
}

解析第一个消息参数的字符串表示形式

{
  "$resolver": "messageParameter",
  "index": 0,
  "stringified": true
}
ndc
config  = [ pattern ]
pattern = "pattern" -> string

解析嵌套诊断上下文 (NDC),也称为线程上下文堆栈,由 logEvent.getContextStack() 返回的 String[]

示例

将所有 NDC 值解析为列表

{
  "$resolver": "ndc"
}

解析与 pattern 正则表达式匹配的所有 NDC 值

{
  "$resolver": "ndc",
  "pattern": "user(Role|Rank):\\w+"
}
pattern
config            = pattern , [ stackTraceEnabled ]
pattern           = "pattern" -> string
stackTraceEnabled = "stackTraceEnabled" -> boolean

委托给 PatternLayout 的解析器。

stackTraceEnabled 的默认值从父 JsonTemplateLayout 继承。

示例

解析由 %p %c{1.} [%t] %X{userId} %X %m%ex 模式生成的字符串

{
  "$resolver": "pattern",
  "pattern": "%p %c{1.} [%t] %X{userId} %X %m%ex"
}
source
config = "field" -> (
           "className"  |
           "fileName"   |
           "methodName" |
           "lineNumber" )

解析由 logEvent.getSource() 返回的 StackTraceElement 的字段。

请注意,此解析器由 log4j.layout.jsonTemplate.locationInfoEnabled 属性切换。

示例

解析行号

{
  "$resolver": "source",
  "field": "lineNumber"
}
thread
config = "field" -> ( "name" | "id" | "priority" )

解析 logEvent.getThreadId()logEvent.getThreadName()logEvent.getThreadPriority()

示例

解析线程名称

{
  "$resolver": "thread",
  "field": "name"
}
timestamp
config        = [ patternConfig | epochConfig ]

patternConfig = "pattern" -> ( [ format ] , [ timeZone ] , [ locale ] )
format        = "format" -> string
timeZone      = "timeZone" -> string
locale        = "locale" -> (
                   language                                   |
                 ( language , "_" , country )                 |
                 ( language , "_" , country , "_" , variant )
                )

epochConfig   = "epoch" -> ( unit , [ rounded ] )
unit          = "unit" -> (
                   "nanos"         |
                   "millis"        |
                   "secs"          |
                   "millis.nanos"  |
                   "secs.nanos"    |
                )
rounded       = "rounded" -> boolean

解析各种形式的 logEvent.getInstant()

示例
表 2. timestamp 模板解析器示例

配置

输出

{
  "$resolver": "timestamp"
}

2020-02-07T13:38:47.098+02:00

{
  "$resolver": "timestamp",
  "pattern": {
    "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
    "timeZone": "UTC",
    "locale": "en_US"
  }
}

2020-02-07T13:38:47.098Z

{
  "$resolver": "timestamp",
  "epoch": {
    "unit": "secs"
  }
}

1581082727.982123456

{
  "$resolver": "timestamp",
  "epoch": {
    "unit": "secs",
    "rounded": true
  }
}

1581082727

{
  "$resolver": "timestamp",
  "epoch": {
    "unit": "secs.nanos"
  }
}

982123456

{
  "$resolver": "timestamp",
  "epoch": {
    "unit": "millis"
  }
}

1581082727982.123456

{
  "$resolver": "timestamp",
  "epoch": {
    "unit": "millis",
    "rounded": true
  }
}

1581082727982

{
  "$resolver": "timestamp",
  "epoch": {
    "unit": "millis.nanos"
  }
}

123456

{
  "$resolver": "timestamp",
  "epoch": {
    "unit": "nanos"
  }
}

1581082727982123456

映射解析器模板

ReadOnlyStringMap 是 Log4j 的 Map<String, Object> 等效项,具有无垃圾访问器,并在整个代码库中广泛使用。它是映射诊断上下文 (MDC)(也称为线程上下文数据)和 MapMessage 实现的底层数据结构。因此,这两个的模板解析器由单个后端提供:ReadOnlyStringMapResolver。换句话说,mdcmap 解析器都支持相同的配置、行为和垃圾占用,这些将在下面详细说明。

config        = singleAccess | multiAccess

singleAccess  = key , [ stringified ]
key           = "key" -> string
stringified   = "stringified" -> boolean

multiAccess   = [ pattern ] , [ replacement ] , [ flatten ] , [ stringified ]
pattern       = "pattern" -> string
replacement   = "replacement" -> string
flatten       = "flatten" -> ( boolean | flattenConfig )
flattenConfig = [ flattenPrefix ]
flattenPrefix = "prefix" -> string

singleAccess 解析单个字段,而 multiAccess 解析多个字段。如果提供 flattenmultiAccess 会将字段与父级合并,否则会创建一个包含这些值的新 JSON 对象。

启用 stringified 标志会将每个值转换为其字符串表示形式。

pattern 中提供的正则表达式用于与键匹配。如果提供,replacement 将用于替换匹配的键。这两个实际上等效于 Pattern.compile(pattern).matcher(key).matches()Pattern.compile(pattern).matcher(key).replaceAll(replacement) 调用。

警告

关于垃圾占用,stringified 标志转换为 String.valueOf(value),因此请注意非 String 类型的值。

patternreplacement 会产生模式匹配器分配成本。

将某些非基本类型的值(例如,BigDecimalSet 等)写入 JSON 会产生垃圾,但大多数(例如,intlongStringListboolean[] 等)不会。

在以下示例中省略了 "$resolver",因为它将由实际解析器定义,例如,mapmdc

解析键为 user:role 的字段的值

{
  "$resolver": "…",
  "key": "user:role"
}

解析 user:rank 字段值的字符串表示形式

{
  "$resolver": "…",
  "key": "user:rank",
  "stringified": true
}

将所有字段解析为一个对象

{
  "$resolver": "…"
}

将所有字段解析为一个对象,以便将值转换为字符串

{
  "$resolver": "…",
  "stringified": true
}

将所有键与 user:(role|rank) 正则表达式匹配的字段解析为一个对象

{
  "$resolver": "…",
  "pattern": "user:(role|rank)"
}

在从键中删除 user: 前缀后,将所有键与 user:(role|rank) 正则表达式匹配的字段解析为一个对象

{
  "$resolver": "…",
  "pattern": "user:(role|rank)",
  "replacement": "$1"
}

将所有键与 user:(role|rank) 正则表达式匹配的字段合并到父级中

{
  "$resolver": "…",
  "flatten": true,
  "pattern": "user:(role|rank)"
}

在将相应字段值转换为字符串后,将所有字段合并到父级中,以便键以 _ 为前缀

{
  "$resolver": "…",
  "stringified": true,
  "flatten": {
    "prefix": "_"
  }
}

堆栈跟踪元素模板

exceptionexceptionRootCause 事件模板解析器可以将异常堆栈跟踪(即 Throwable#getStackTrace() 返回的 StackTraceElement[])序列化为 JSON 数组。在此过程中,再次使用 JSON 模板基础设施。

stackTraceElement[Uri] 描述了 JsonTemplateLayout 用于格式化 StackTraceElement 的 JSON 结构。默认配置(可以通过 log4j.layout.jsonTemplate.stackTraceElementTemplate[Uri] 属性访问)设置为 log4j-layout-template-json 工件提供的 classpath:StackTraceElementLayout.json

{
  "class": {
    "$resolver": "stackTraceElement",
    "field": "className"
  },
  "method": {
    "$resolver": "stackTraceElement",
    "field": "methodName"
  },
  "file": {
    "$resolver": "stackTraceElement",
    "field": "fileName"
  },
  "line": {
    "$resolver": "stackTraceElement",
    "field": "lineNumber"
  }
}

允许的模板配置语法如下

config = "field" -> (
           "className"  |
           "fileName"   |
           "methodName" |
           "lineNumber" )

以上所有对 StackTraceElement 的访问都是无垃圾的。

扩展

JsonTemplateLayout 依赖于 Log4j 插件系统 来构建其提供的功能。这使得用户可以轻松地自定义功能。截至目前,以下功能是通过插件实现的

  • 事件模板解析器(例如,exceptionmessagelevel 事件模板解析器)

  • 事件模板拦截器(例如,注入 eventTemplateAdditionalField

  • 回收器工厂

以下部分将详细介绍这些内容。

插件预备知识

Log4j 插件系统是各种 Log4j 组件(包括 JsonTemplateLayout)采用的事实上的扩展机制。插件使可扩展组件能够接收功能实现,而无需两者之间有任何显式链接。它类似于 依赖注入 框架,但针对 Log4j 特定的需求进行了调整。

简而言之,您使用 @Plugin 注释您的类,并使用 @PluginFactory 注释其(static)创建器方法。最后,您通知 Log4j 插件系统发现这些自定义类。这可以通过在 Log4j 配置中声明的 packages插件系统文档 中描述的各种其他方式来完成。

扩展事件解析器

所有可用的 事件模板解析器 都是 JsonTemplateLayout 使用的简单插件。要添加新的解析器,只需创建自己的 EventResolver 并通过 @Plugin 注释的 EventResolverFactory 类指示其注入。

出于演示目的,下面我们将创建一个 randomNumber 事件解析器。让我们从实际的解析器开始

自定义随机数事件解析器
package com.acme.logging.log4j.layout.template.json;

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.layout.template.json.resolver.EventResolver;
import org.apache.logging.log4j.layout.template.json.util.JsonWriter;

/**
 * Resolves a random floating point number.
 *
 * <h3>Configuration</h3>
 *
 * <pre>
 * config = ( [ range ] )
 * range  = number[]
 * </pre>
 *
 * {@code range} is a number array with two elements, where the first number
 * denotes the start (inclusive) and the second denotes the end (exclusive).
 * {@code range} is optional and by default set to {@code [0, 1]}.
 *
 * <h3>Examples</h3>
 *
 * Resolve a random number between 0 and 1:
 *
 * <pre>
 * {
 *   "$resolver": "randomNumber"
 * }
 * </pre>
 *
 * Resolve a random number between -0.123 and 0.123:
 *
 * <pre>
 * {
 *   "$resolver": "randomNumber",
 *   "range": [-0.123, 0.123]
 * }
 * </pre>
 */
public final class RandomNumberResolver implements EventResolver {

    private final double loIncLimit;

    private final double hiExcLimit;

    RandomNumberResolver(final TemplateResolverConfig config) {
        final List<Number> rangeArray = config.getList("range", Number.class);
        if (rangeArray == null) {
            this.loIncLimit = 0D;
            this.hiExcLimit = 1D;
        } else if (rangeArray.size() != 2) {
            throw new IllegalArgumentException(
                    "range array must be of size two: " + config);
        } else {
            this.loIncLimit = rangeArray.get(0).doubleValue();
            this.hiExcLimit = rangeArray.get(1).doubleValue();
            if (loIncLimit > hiExcLimit) {
                throw new IllegalArgumentException("invalid range: " + config);
            }
        }
    }

    static String getName() {
        return "randomNumber";
    }

    @Override
    public void resolve(
            final LogEvent value,
            final JsonWriter jsonWriter) {
        final double randomNumber =
                loIncLimit + (hiExcLimit - loIncLimit) * Math.random();
        jsonWriter.writeNumber(randomNumber);
    }

}

接下来,创建一个 EventResolverFactory 类,将 RandomNumberResolver 注册到 Log4j 插件系统中。

解析器工厂类,用于将 RandomNumberResolver 注册到 Log4j 插件系统中
package com.acme.logging.log4j.layout.template.json;

import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverFactory;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolver;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverConfig;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverFactory;

/**
 * {@link RandomNumberResolver} factory.
 */
@Plugin(name = "RandomNumberResolverFactory", category = TemplateResolverFactory.CATEGORY)
public final class RandomNumberResolverFactory implements EventResolverFactory {

    private static final RandomNumberResolverFactory INSTANCE =
            new RandomNumberResolverFactory();

    private RandomNumberResolverFactory() {}

    @PluginFactory
    public static RandomNumberResolverFactory getInstance() {
        return INSTANCE;
    }

    @Override
    public String getName() {
        return RandomNumberResolver.getName();
    }

    @Override
    public RandomNumberResolver create(
            final EventResolverContext context,
            final TemplateResolverConfig config) {
        return new RandomNumberResolver(config);
    }

}

几乎完成了。最后,我们需要通知 Log4j 插件系统发现这些自定义类

使用自定义 randomNumber 解析器的 Log4j 配置
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <!-- ... -->
  <JsonTemplateLayout>
    <EventTemplateAdditionalField
        key="id"
        format="JSON"
        value='{"$resolver": "randomNumber", "range": [0, 1000000]}'/>
  </JsonTemplateLayout>
  <!-- ... -->
</Configuration>

所有可用的事件模板解析器都位于 org.apache.logging.log4j.layout.template.json.resolver 包中。在实现新的解析器时,这是一个相当丰富的资源,可以从中获得灵感。

拦截模板解析器编译器

JsonTemplateLayout 允许拦截模板解析器编译,这是将模板转换为执行 JSON 序列化的 Java 函数的过程。这种拦截机制在内部用于实现 eventTemplateRootObjectKeyeventTemplateAdditionalField 功能。简而言之,您需要创建一个扩展 EventResolverInterceptor 接口的 @Plugin 注释的类。

要查看拦截的实际操作,请查看 EventRootObjectKeyInterceptor 类,该类负责实现 eventTemplateRootObjectKey 功能

事件拦截器,用于添加 eventTemplateRootObjectKey(如果存在)
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverContext;
import org.apache.logging.log4j.layout.template.json.resolver.EventResolverInterceptor;
import org.apache.logging.log4j.layout.template.json.resolver.TemplateResolverInterceptor;

/**
 * Interceptor to add a root object key to the event template.
 */
@Plugin(name = "EventRootObjectKeyInterceptor", category = TemplateResolverInterceptor.CATEGORY)
public class EventRootObjectKeyInterceptor implements EventResolverInterceptor {

    private static final EventRootObjectKeyInterceptor INSTANCE =
            new EventRootObjectKeyInterceptor();

    private EventRootObjectKeyInterceptor() {}

    @PluginFactory
    public static EventRootObjectKeyInterceptor getInstance() {
        return INSTANCE;
    }

    @Override
    public Object processTemplateBeforeResolverInjection(
            final EventResolverContext context,
            final Object node) {
        String eventTemplateRootObjectKey = context.getEventTemplateRootObjectKey();
        return eventTemplateRootObjectKey != null
                ? Collections.singletonMap(eventTemplateRootObjectKey, node)
                : node;
    }

}

在这里,processTemplateBeforeResolverInjection() 方法检查用户是否提供了 eventTemplateRootObjectKey。如果是,它会用一个新对象包装根 node;否则,按原样返回 node。请注意,node 指的是由 JsonReader 读取的事件模板的根 Java 对象。

扩展回收器工厂

从布局配置中读取的 recyclerFactory 输入 String 会使用扩展 TypeConverter<RecyclerFactory> 的默认 RecyclerFactoryConverter 转换为 RecyclerFactory。如果要更改此行为,只需添加自己的 TypeConverter<RecyclerFactory> 实现 Comparable<TypeConverter<?>> 以优先考虑您的自定义转换器。

RecyclerFactory 的自定义 TypeConverter
package com.acme.logging.log4j.layout.template.json;

import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.convert.TypeConverter;
import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;

@Plugin(name = "AcmeRecyclerFactoryConverter", category = TypeConverters.CATEGORY)
public final class AcmeRecyclerFactoryConverter
        implements TypeConverter<RecyclerFactory>, Comparable<TypeConverter<?>> {

    @Override
    public RecyclerFactory convert(final String recyclerFactorySpec) {
        return AcmeRecyclerFactory.ofSpec(recyclerFactorySpec);
    }

    @Override
    public int compareTo(final TypeConverter<?> ignored) {
        return -1;
    }

}

请注意,compareTo() 始终返回 -1,以使其排名高于其他匹配的转换器。

功能

以下是 JsonTemplateLayout 与替代方案的功能比较矩阵。

表 3. 功能比较矩阵

功能

JsonTemplateLayout

JsonLayout

GelfLayout

EcsLayout

Java 版本

8

8

8

6

依赖项

Jackson

架构自定义?

时间戳自定义?

(几乎)无垃圾?

自定义类型 Message 序列化?

?[1]

自定义类型 MDC 值序列化?

将堆栈跟踪呈现为数组?

堆栈跟踪截断?

JSON 美化打印?

其他字符串字段?

其他 JSON 字段?

自定义解析器?

常见问题解答

模板中是否支持查找?

是的,模板的字符串文字中支持 查找(例如,${java:version}${env:USER}${date:MM-dd-yyyy})。但请注意,它们不是无垃圾的。

是否支持递归集合?

否。考虑一个包含递归值的 Message,如下所示

Object[] recursiveCollection = new Object[1];
recursiveCollection[0] = recursiveCollection;

虽然确切的异常可能有所不同,但您很可能会在尝试将 recursiveCollection 渲染为 String 时收到 StackOverflowError。请注意,这也是其他 Java 标准库方法(例如,Arrays.toString())的默认行为。因此,在记录时请注意自引用。

JsonTemplateLayout 是否无垃圾?

是的,如果启用了无垃圾布局行为切换属性 log4j2.enableDirectEncoderslog4j2.garbagefreeThreadContextMap。请考虑以下注意事项

  • 配置的 回收策略 可能不是无垃圾的。

  • 由于 Throwable#getStackTrace() 克隆了原始 StackTraceElement[],因此对堆栈跟踪的访问(以及渲染)不会产生垃圾。

  • MapMessageObjectMessage 的序列化大部分是无垃圾的,但某些类型(例如,BigDecimalBigIntegerCollection,除了 List)除外。

  • 查找(即,${…​} 变量)不是无垃圾的。

不要忘记查看您在模板中使用的 解析器垃圾占用说明


1. 仅适用于 ObjectMessage,如果类路径中存在 Jackson。