在云启用应用程序中使用 Log4j
十二要素应用程序
有关 十二要素应用程序 的日志记录指南指出,所有日志都应无缓冲地路由到 stdout。由于这是最低公分母,因此它保证适用于所有应用程序。但是,与任何一组一般指南一样,选择最低公分母方法会付出代价。Java 应用程序中的一些成本包括
- Java 堆栈跟踪是多行日志消息。标准 docker 日志驱动程序无法正确处理这些消息。请参阅 Docker 问题 #22920,该问题已关闭,消息为“不关心”。对此的解决方案是:a. 使用支持多行日志消息的 docker 日志驱动程序,b. 使用不会生成多行消息的日志格式,c. 从 Log4j 直接记录到日志转发器或聚合器,并绕过 docker 日志驱动程序。
- 在 Docker 中记录到 stdout 时,日志事件会通过 Java 的标准输出处理,然后传递到操作系统,以便输出可以被管道传输到文件。所有这些的开销明显慢于直接写入文件,这可以在这些基准测试结果中看到,其中记录到 stdout 在重复运行中比直接记录到文件慢 16-20 倍。以下结果是通过在配备 2.9GHz 英特尔酷睿 i9 处理器和 1TB SSD 的 2018 款 MacBook Pro 上运行 输出基准测试 获得的。但是,仅凭这些结果不足以反驳写入标准输出流,因为它们仅相当于每次日志记录调用约 14-25 微秒,而写入文件时为 1.5 微秒。
Benchmark Mode Cnt Score Error Units OutputBenchmark.console thrpt 20 39291.885 ± 3370.066 ops/s OutputBenchmark.file thrpt 20 654584.309 ± 59399.092 ops/s OutputBenchmark.redirect thrpt 20 70284.576 ± 7452.167 ops/s
- 在使用 log4j-audit 等框架执行审计日志记录时,需要保证审计事件的传递。许多输出写入选项(包括写入标准输出流)不保证传递。在这些情况下,事件必须传递到“转发器”,该转发器仅在将事件放入持久存储后才确认收到,例如 Apache Flume 或 Apache Kafka 将执行的操作。
日志记录方法
本页讨论的所有解决方案都以以下理念为前提:日志文件不能永久驻留在文件系统中,并且所有日志事件都应路由到一个或多个日志分析工具,这些工具将用于报告和警报。有许多方法可以转发和收集事件以发送到日志分析工具。
请注意,任何绕过 Docker 日志驱动程序的方法都需要 Log4j 的 Docker 查找,以允许将 Docker 属性注入日志事件。
记录到标准输出流
如上所述,这是在 docker 容器中运行的应用程序的推荐十二要素方法。Log4j 团队不推荐这种方法,因为它存在性能问题。
使用 Docker Fluentd 日志驱动程序记录到标准输出流
Docker 提供了备用 日志驱动程序,例如 gelf 或 fluentd,可用于将标准输出流重定向到日志转发器或日志聚合器。
在路由到日志转发器时,预计转发器将与应用程序具有相同的生命周期。如果转发器出现故障,则预计管理工具也会终止依赖于转发器的其他容器。
作为替代方案,日志驱动程序可以配置为将事件直接路由到日志聚合器。这通常不是一个好主意,因为日志驱动程序只允许配置单个主机和端口。docker 文档没有明确说明,但推断出当无法传递日志事件时,日志事件将被丢弃,因此如果需要高可用性解决方案,则不应使用此方法。
记录到文件
虽然这不是推荐的十二要素方法,但它的性能非常好。但是,它要求应用程序声明一个日志文件将驻留的卷,然后配置日志转发器来尾随这些文件。还必须注意自动管理用于日志的磁盘空间,Log4j 可以通过 滚动文件附加器 上的“删除”操作来执行此操作。
通过 TCP 直接发送到日志转发器
将日志直接发送到日志转发器很简单,因为它通常只需要在具有适当布局的套接字附加器上配置转发器的主机和端口。
通过 TCP 直接发送到日志聚合器
与将日志发送到转发器类似,日志也可以发送到聚合器集群。但是,设置起来并不那么简单,因为为了实现高可用性,必须使用聚合器集群。但是,套接字附加器目前只能配置一个主机和端口。为了允许在主聚合器出现故障时进行故障转移,套接字附加器必须包含在 故障转移附加器 中,该附加器还将配置辅助聚合器。另一种选择是让套接字附加器指向可以转发到日志聚合器的高可用性代理。
如果使用的日志聚合器是 Apache Flume 或 Apache Kafka(或类似的),则这些聚合器的附加器支持配置主机和端口列表,因此高可用性不是问题。
使用 Elasticsearch、Logstash 和 Kibana 进行日志记录
有各种方法,具有不同的权衡,用于将日志摄取到 ELK 堆栈中。在这里,我们将简要介绍如何首先将 Log4j 生成的事件转发到 Logstash,然后转发到 Elasticsearch。
Log4j 配置
JsonTemplateLayout
Log4j 提供了许多 JSON 生成布局。特别是,JSON 模板布局 允许完全的模式自定义,并且默认情况下捆绑了 ELK 特定的布局,这使其非常适合这种情况。使用下面所示的 EcsLayout 模板将生成 Kibana 中的数据,其中显示的消息与传递给 Log4j 的消息完全匹配,并且大多数事件属性(包括任何异常)都作为可以显示的单个属性存在。请注意,堆栈跟踪将以不带换行符的格式进行格式化。
<Socket name="Logstash"
host="${sys:logstash.host}"
port="12345"
protocol="tcp"
bufferedIo="true">
<JsonTemplateLayout eventTemplateUri="classpath:EcsLayout.json">
<EventTemplateAdditionalField key="containerId" value="${docker:containerId:-}"/>
<EventTemplateAdditionalField key="application" value="${lower:${spring:spring.application.name:-spring}}"/>
<EventTemplateAdditionalField key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
<EventTemplateAdditionalField key="kubernetes.containerId" value="${k8s:containerId:-}"/>
<EventTemplateAdditionalField key="kubernetes.containerName" value="${k8s:containerName:-}"/>
<EventTemplateAdditionalField key="kubernetes.host" value="${k8s:host:-}"/>
<EventTemplateAdditionalField key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
<EventTemplateAdditionalField key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
<EventTemplateAdditionalField key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
<EventTemplateAdditionalField key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
<EventTemplateAdditionalField key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
<EventTemplateAdditionalField key="kubernetes.podID" value="${k8s:podId:-}"/>
<EventTemplateAdditionalField key="kubernetes.podIP" value="${k8s:podIp:-}"/>
<EventTemplateAdditionalField key="kubernetes.podName" value="${k8s:podName:-}"/>
<EventTemplateAdditionalField key="kubernetes.imageId" value="${k8s:imageId:-}"/>
<EventTemplateAdditionalField key="kubernetes.imageName" value="${k8s:imageName:-}"/>
</JsonTemplateLayout>
</Socket>
Gelft 模板
JsonTemplateLayout 也可以用来生成符合 GELF 规范的 JSON,它可以根据 PatternLayout 中的模式格式化消息属性。例如,以下名为 EnhancedGelf.json 的模板可用于生成符合 GELF 规范的数据,这些数据可以传递给 Logstash。使用此模板,消息属性将包含线程 ID、级别、特定 ThreadContext 属性、类名、方法名和行号,以及消息。如果包含异常,它也将包含在带有换行符的异常中。此格式与您在使用 PatternLayout 的磁盘上的典型日志文件中看到的格式非常接近,但它还具有将属性作为可以查询的单独字段包含的额外优势。
{
"version": "1.1",
"host": "${hostName}",
"short_message": {
"$resolver": "message",
"stringified": true
},
"full_message": {
"$resolver": "message",
"pattern": "[%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress, corpAcctNumber} %C{1.}.%M:%L - %m",
"stringified": true
},
"timestamp": {
"$resolver": "timestamp",
"epoch": {
"unit": "secs"
}
},
"level": {
"$resolver": "level",
"field": "severity",
"severity": {
"field": "code"
}
},
"_logger": {
"$resolver": "logger",
"field": "name"
},
"_thread": {
"$resolver": "thread",
"field": "name"
},
"_mdc": {
"$resolver": "mdc",
"flatten": {
"prefix": "_"
},
"stringified": true
}
}
使用此模板的日志记录配置将是
<Socket name="Elastic"
host="\${sys:logstash.search.host}"
port="12222"
protocol="tcp"
bufferedIo="true">
<JsonTemplateLayout eventTemplateUri="classpath:EnhancedGelf.json" nullEventDelimiterEnabled="true">
<EventTemplateAdditionalField key="containerId" value="${docker:containerId:-}"/>
<EventTemplateAdditionalField key="application" value="${lower:${spring:spring.application.name:-spring}}"/>
<EventTemplateAdditionalField key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
<EventTemplateAdditionalField key="kubernetes.containerId" value="${k8s:containerId:-}"/>
<EventTemplateAdditionalField key="kubernetes.containerName" value="${k8s:containerName:-}"/>
<EventTemplateAdditionalField key="kubernetes.host" value="${k8s:host:-}"/>
<EventTemplateAdditionalField key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
<EventTemplateAdditionalField key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
<EventTemplateAdditionalField key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
<EventTemplateAdditionalField key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
<EventTemplateAdditionalField key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
<EventTemplateAdditionalField key="kubernetes.podID" value="${k8s:podId:-}"/>
<EventTemplateAdditionalField key="kubernetes.podIP" value="${k8s:podIp:-}"/>
<EventTemplateAdditionalField key="kubernetes.podName" value="${k8s:podName:-}"/>
<EventTemplateAdditionalField key="kubernetes.imageId" value="${k8s:imageId:-}"/>
<EventTemplateAdditionalField key="kubernetes.imageName" value="${k8s:imageName:-}"/>
</JsonTemplateLayout>
</Socket>
此配置与第一个示例的显着区别在于它引用了自定义模板,并且它指定了空字符 ('\0') 的事件分隔符;
注意:通过上述模板传递的级别并不严格符合 GELF 规范,因为传递的级别是 Log4j 级别,而不是 GELF 规范中定义的级别。但是,测试表明 Logstash、Elk 和 Kibana 对传递给它的任何数据都相当宽容。
自定义模板
另一种选择是使用自定义模板,可能基于标准模板之一。以下模板松散地基于 ECS,但 a) 添加了 Spring Boot 应用程序名称,b) 使用 PatternLayout 格式化消息,将 Map 消息格式化为 event.data 属性,同时根据事件中包含的任何标记设置事件操作,包括所有 ThreadContext 属性。
注意:Json 模板布局对控制序列进行转义,因此包含 '\n' 的消息会将这些控制序列复制为“\n”到文本中,而不是转换为换行符。这绕过了许多与 Log Forwarders(如 Filebeat 和 FluentBit/Fluentd)一起出现的问题。Kibana 将正确地将这些序列解释为换行符并正确地显示它们。另请注意,消息模式不包含时间戳。Kibana 将在其自己的列中显示时间戳字段,因此将其放在消息中将是多余的。
{
"@timestamp": {
"$resolver": "timestamp",
"pattern": {
"format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
"timeZone": "UTC"
}
},
"ecs.version": "1.11.0",
"log.level": {
"$resolver": "level",
"field": "name"
},
"application": "\${lower:\${spring:spring.application.name}}",
"short_message": {
"$resolver": "message",
"stringified": true
},
"message": {
"$resolver": "pattern",
"pattern": "[%t] %X{requestId, sessionId, loginId, userId, ipAddress, accountNumber} %C{1.}.%M:%L - %m%n"
},
"process.thread.name": {
"$resolver": "thread",
"field": "name"
},
"log.logger": {
"$resolver": "logger",
"field": "name"
},
"event.action": {
"$resolver": "marker",
"field": "name"
},
"event.data": {
"$resolver": "map",
"stringified": true
},
"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
}
}
}
最后,GelfLayout 可用于生成符合 GELF 规范的输出。与 JsonTemplateLayout 不同,它严格遵守 GELF 规范。
<Socket name="Elastic" host="${sys:elastic.search.host}" port="12222" protocol="tcp" bufferedIo="true">
<GelfLayout includeStackTrace="true" host="${hostName}" includeThreadContext="true" includeNullDelimiter="true"
compressionType="OFF">
<ThreadContextIncludes>requestId,sessionId,loginId,userId,ipAddress,callingHost</ThreadContextIncludes>
<MessagePattern>%d [%t] %-5p %X{requestId, sessionId, loginId, userId, ipAddress} %C{1.}.%M:%L - %m%n</MessagePattern>
<KeyValuePair key="containerId" value="${docker:containerId:-}"/>
<KeyValuePair key="application" value="${lower:${spring:spring.application.name:-spring}}"/>
<KeyValuePair key="kubernetes.serviceAccountName" value="${k8s:accountName:-}"/>
<KeyValuePair key="kubernetes.containerId" value="${k8s:containerId:-}"/>
<KeyValuePair key="kubernetes.containerName" value="${k8s:containerName:-}"/>
<KeyValuePair key="kubernetes.host" value="${k8s:host:-}"/>
<KeyValuePair key="kubernetes.labels.app" value="${k8s:labels.app:-}"/>
<KeyValuePair key="kubernetes.labels.pod-template-hash" value="${k8s:labels.podTemplateHash:-}"/>
<KeyValuePair key="kubernetes.master_url" value="${k8s:masterUrl:-}"/>
<KeyValuePair key="kubernetes.namespaceId" value="${k8s:namespaceId:-}"/>
<KeyValuePair key="kubernetes.namespaceName" value="${k8s:namespaceName:-}"/>
<KeyValuePair key="kubernetes.podID" value="${k8s:podId:-}"/>
<KeyValuePair key="kubernetes.podIP" value="${k8s:podIp:-}"/>
<KeyValuePair key="kubernetes.podName" value="${k8s:podName:-}"/>
<KeyValuePair key="kubernetes.imageId" value="${k8s:imageId:-}"/>
<KeyValuePair key="kubernetes.imageName" value="${k8s:imageName:-}"/>
</GelfLayout>
</Socket>
使用 Gelf 的 Logstash 配置
我们将配置 Logstash 在 TCP 端口 12345 上监听类型为 JSON 的有效负载,然后将这些有效负载转发到(控制台和/或)Elasticsearch 服务器。
input {
tcp {
port => 12345
codec => "json"
}
}
output {
# (Un)comment for debugging purposes.
# stdout { codec => rubydebug }
# Modify the hosts value to reflect where elasticsearch is installed.
elasticsearch {
hosts => ["http://localhost:9200/"]
index => "app-%{application}-%{+YYYY.MM.dd}"
}
}
使用 JsonTemplateLayout 的 Logstash 配置
当使用 GELF 兼容格式之一时,Logstash 应配置为
gelf { host => “localhost” use_tcp => true use_udp => false port => 12222 type => “gelf” }
filter {
# These are GELF/Syslog logging levels as defined in RFC 3164. Map the integer level to its human readable format.
translate {
field => "[level]"
destination => "[levelName]"
dictionary => {
"0" => "EMERG"
"1" => "ALERT"
"2" => "CRITICAL"
"3" => "ERROR"
"4" => "WARN"
"5" => "NOTICE"
"6" => "INFO"
"7" => "DEBUG"
}
}
}
output {
# (Un)comment for debugging purposes
# stdout { codec => rubydebug }
# Modify the hosts value to reflect where elasticsearch is installed.
elasticsearch {
hosts => ["http://localhost:9200/"]
index => "app-%{application}-%{+YYYY.MM.dd}"
}
}
使用 JsonTemplateLayout 的 Filebeat 配置
当使用符合 ECS 的 JsonTemplateLayout(或类似于之前显示的自定义模板)时,filebeat 的配置很简单。
filebeat.inputs:
- type: log
enabled: true
json.keys_under_root: true
paths:
- /var/log/apps/*.log
Kibana
使用 EnhancedGelf 模板、GelfLayout 或自定义模板,上述配置中的消息字段将包含一个完全格式化的日志事件,就像它出现在文件 Appender 中一样。ThreadContext 属性、自定义字段、线程名称等都将作为每个日志事件上的属性可用,这些属性可用于过滤。结果将类似于
管理日志记录配置
Spring Boot 提供了另一种最小公分母方法来进行日志记录配置。它允许您设置应用程序中各种 Logger 的日志级别,这些级别可以通过 Spring 提供的 REST 端点动态更新。虽然这在许多情况下都有效,但它不支持 Log4j 的任何更高级的过滤功能。例如,由于它不能添加或修改除 Logger 的日志级别之外的任何过滤器,因此无法进行更改以允许特定用户或客户的所有日志事件临时记录(请参阅 DynamicThresholdFilter 或 ThreadContextMapFilter)或对过滤器的任何其他类型的更改。此外,在微服务、集群环境中,这些更改很可能需要同时传播到多个服务器。尝试通过 REST 调用实现这一点可能很困难。
自首次发布以来,Log4j 一直支持通过文件进行重新配置。从 Log4j 2.12.0 开始,Log4j 还支持通过 HTTP(S) 访问配置,并通过使用 HTTP“If-Modified-Since”标头监控文件更改。从版本 2.0.3 和 2.1.1 开始,一个补丁也已集成到 Spring Cloud Config 中,以使其尊重 If-Modified-Since 标头。此外,log4j-spring-cloud-config 项目将监听 Spring Cloud Bus 发布的更新事件,然后验证配置文件是否已修改,因此不需要通过 HTTP 轮询。
Log4j 还支持复合配置。分布在微服务中的分布式应用程序可以共享一个公共配置文件,该文件可用于控制诸如为特定用户启用调试日志记录等内容。
虽然更新日志记录的标准 Spring Boot REST 端点仍然有效,但如果 Log4j 由于日志记录配置文件中的更改而重新配置自身,则通过这些 REST 端点进行的任何更改都将丢失。
有关 log4j-spring-cloud-config-client 集成的更多信息,请参阅 Log4j Spring Cloud Config Client。
与 Spring Boot 集成
Log4j 通过两种方式与 Spring Boot 集成
- Spring Lookup 可用于从 Log4j 配置文件访问 Spring 应用程序配置。
- Log4j 在尝试解析 log4j 系统属性时将访问 Spring 配置。
这两者都需要在应用程序中包含 log4j-spring-cloud-client jar。
与 Docker 集成
使用 Docker 日志记录驱动程序进行日志记录的 Docker 容器中的应用程序可以在格式化的日志事件中包含特殊属性,如 Customize Log Driver Output 中所述。Log4j 通过 Docker Lookup 提供类似的功能。有关 Log4j 的 Docker 支持的更多信息,也可以在 Log4j-Docker 中找到。
与 Kubernetes 集成
由 Kubernetes 管理的应用程序可以通过 Docker/Kubernetes 日志记录基础设施,直接记录到 sidecar 转发器或日志聚合器集群,同时仍然使用 Log4j 2 Kubernetes Lookup 包含所有 kubernetes 属性。有关 Log4j 的 Kubernetes 支持的更多信息,也可以在 Log4j-Kubernetes 中找到。
Appender 性能
下表中的数字表示应用程序调用 logger.debug(...)
100,000 次所需的时间(以秒为单位)。这些数字仅包括传递到特定提到的端点所需的时间,可能不包括它们可用以查看之前实际所需的时间。所有测量都在配备 2.9GHz Intel Core I9 处理器(具有 6 个物理核心和 12 个逻辑核心)、32GB 2400MHz DDR4 RAM 和 1TB Apple SSD 存储的 MacBook Pro 上进行。Docker 使用的 VM 由 VMWare Fusion 管理,并具有 4 个 CPU 和 2GB RAM。这些数字应用于相对性能比较,因为其他系统上的结果可能会有很大差异。
使用的示例应用程序可以在 Log4j 源代码库 中的 log4j-spring-cloud-config/log4j-spring-cloud-config-samples 目录下找到。
测试 | 1 个线程 | 2 个线程 | 4 个线程 | 8 个线程 |
---|---|---|---|---|
Flume Avro | ||||
- 批次大小 1 - JSON | 49.11 | 46.54 | 46.70 | 44.92 |
- 批次大小 1 - RFC5424 | 48.30 | 45.79 | 46.31 | 45.50 |
- 批次大小 100 - JSON | 6.33 | 3.87 | 3.57 | 3.84 |
- 批次大小 100 - RFC5424 | 6.08 | 3.69 | 3.22 | 3.11 |
- 批次大小 1000 - JSON | 4.83 | 3.20 | 3.02 | 2.11 |
- 批次大小 1000 - RFC5424 | 4.70 | 2.40 | 2.37 | 2.37 |
Flume 嵌入式 | ||||
- RFC5424 | 3.58 | 2.10 | 2.10 | 2.70 |
- JSON | 4.20 | 2.49 | 3.53 | 2.90 |
Kafka 本地 JSON | ||||
- sendSync 为真 | 58.46 | 38.55 | 19.59 | 19.01 |
- sendSync 为假 | 9.8 | 10.8 | 12.23 | 11.36 |
控制台 | ||||
- JSON / Kubernetes | 3.03 | 3.11 | 3.04 | 2.51 |
- JSON | 2.80 | 2.74 | 2.54 | 2.35 |
- Docker fluentd 驱动程序 | 10.65 | 9.92 | 10.42 | 10.27 |
滚动文件 | ||||
- RFC5424 | 1.65 | 0.94 | 1.22 | 1.55 |
- JSON | 1.90 | 0.95 | 1.57 | 1.94 |
TCP - Fluent Bit - JSON | 2.34 | 2.167 | 1.67 | 2.50 |
异步记录器 | ||||
- TCP - Fluent Bit - JSON | 0.90 | 0.58 | 0.36 | 0.48 |
- 控制台 - JSON | 0.83 | 0.57 | 0.55 | 0.61 |
- Flume Avro - 1000 - JSON | 0.76 | 0.37 | 0.45 | 0.68 |
说明
- Flume Avro - 缓冲由批次大小控制。每次发送在远程确认批次已写入其通道后完成。这些数字似乎表明 Flume Avro 可以从使用 RPCClients 池中获益,至少对于批次大小为 1 的情况而言。
- Flume 嵌入式 - 这本质上是异步的,因为它写入内存缓冲区。目前尚不清楚为什么性能与 AsyncLogger 结果不接近。
- Kafka 在与应用程序相同的笔记本电脑上以独立模式运行。请参阅 sendSync 设置为 true 需要等待 Kafka 对每个日志事件的确认。
- 控制台 - System.out 由 Docker 重定向到文件。测试表明,如果它写入终端屏幕,它会慢得多。
- 滚动文件 - 测试使用默认缓冲区大小 8K。
- TCP 到 Fluent Bit - Socket Appender 使用默认缓冲区大小 8K。
- 异步记录器 - 这些记录器都写入循环缓冲区并返回到应用程序。实际的 I/O 将在单独的线程上进行。如果写入事件的速度比创建事件的速度慢,最终缓冲区将填满,日志记录将以与写入日志事件相同的速率执行。
日志记录建议
- 除非绝对需要保证交付,否则使用异步日志记录。正如性能数字所示,只要日志记录量不足以填满循环缓冲区,日志记录的开销对应用程序来说几乎是不可察觉的。
- 如果整体性能是一个考虑因素,或者您需要正确处理多行事件(如堆栈跟踪),那么通过 TCP 记录到充当日志转发器的伴侣容器或直接记录到日志聚合器,如上面 使用 ELK 进行日志记录 中所示。使用
Log4j Docker Lookup 将容器信息添加到每个日志事件中。 - 每当需要保证交付时,请使用批次大小为 1 的 Flume Avro 或其他 Appender(如 Kafka Appender,其 syncSend 设置为 true,仅在接收方确认收到事件后才返回控制)。请注意,使用逐个写入每个事件的 Appender 应尽量减少,因为它比发送缓冲事件慢得多。
- 不建议将日志记录到容器中的文件。这样做需要在 Docker 配置中声明一个卷,并且需要由日志转发器跟踪该文件。但是,它的性能比记录到标准输出流要好。如果通过 TCP 记录不是一种选择,并且需要正确处理多行,那么请考虑此选项。