当把应用程序定位到云平台时,我看到的一个关于日志的建议是简单地写到标准输出,平台负责把它发送到适当的日志汇中。这个方法大部分时候都是有效的,除非它不适用--尤其是在分析故障情况时。通常,对于Java应用程序来说,这意味着查看堆栈跟踪,而堆栈跟踪的每一行都被日志汇视为一个单独的日志条目,这就造成了这些问题:
- 将多行输出作为单一堆栈跟踪的一部分进行关联
- 由于应用程序是多线程的,即使是相关的日志,其顺序也不一定正确
- 日志的严重性没有被正确地确定,所以没有找到进入错误报告系统的途径。
这篇文章将介绍在谷歌云平台上从Java应用程序中记录日志的一些方法。
问题
让我再看一遍这个问题,比如说我在Java代码中以如下方式记录。
LOGGER.info("Hello Logging")
在GCP日志控制台中显示如下方式
{
"textPayload": "2022-04-29 22:00:12.057 INFO 1 --- [or-http-epoll-1] org.bk.web.GreetingsController : Hello Logging",
"insertId": "626c5fec0000e25a9b667889",
"resource": {
"type": "cloud_run_revision",
"labels": {
"service_name": "hello-cloud-run-sample",
"configuration_name": "hello-cloud-run-sample",
"project_id": "biju-altostrat-demo",
"revision_name": "hello-cloud-run-sample-00008-qow",
"location": "us-central1"
}
},
"timestamp": "2022-04-29T22:00:12.057946Z",
"labels": {
"instanceId": "instanceid"
},
"logName": "projects/myproject/logs/run.googleapis.com%2Fstdout",
"receiveTimestamp": "2022-04-29T22:00:12.077339403Z"
}
这看起来很合理。现在考虑在出现错误时的日志情况。
{
"textPayload": "\t\tat reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2068) ~[reactor-core-3.4.17.jar:3.4.17]",
"insertId": "626c619b00005956ab868f3f",
"resource": {
"type": "cloud_run_revision",
"labels": {
"revision_name": "hello-cloud-run-sample-00008-qow",
"project_id": "biju-altostrat-demo",
"location": "us-central1",
"configuration_name": "hello-cloud-run-sample",
"service_name": "hello-cloud-run-sample"
}
},
"timestamp": "2022-04-29T22:07:23.022870Z",
"labels": {
"instanceId": "0067430fbd3ad615324262b55e1604eb6acbd21e59fa5fadd15cb4e033adedd66031dba29e1b81d507872b2c3c6cd58a83a7f0794965f8c5f7a97507bb5b27fb33"
},
"logName": "projects/biju-altostrat-demo/logs/run.googleapis.com%2Fstdout",
"receiveTimestamp": "2022-04-29T22:07:23.317981870Z"
}
在GCP日志控制台中会有多个这样的记录,对于堆栈跟踪的每一行,没有办法将它们联系在一起。此外,这些事件没有附加的严重性,所以错误不会在谷歌云错误报告服务中结束。
配置日志
有几种方法可以为要部署到Google Cloud的Java应用程序配置日志。最简单的方法,如果使用Logback
,就是使用 Google Cloud提供的Loggingappender--github.com/googleapis/…
添加appender很简单,配置了appender的logback.xml文件看起来像这样。
<configuration>
<appender name="gcpLoggingAppender" class="com.google.cloud.logging.logback.LoggingAppender">
</appender>
<root level="INFO">
<appender-ref ref="gcpLoggingAppender"/>
</root>
</configuration>
这很好用,但它有一个巨大的缺陷。它需要连接到GCP环境,因为它直接将日志写到云日志系统,这对本地测试来说并不理想。
在GCP环境和本地运行时,一种方法是简单地将输出指向标准输出,这将确保日志以json结构的格式写入,并正确地运送到云日志。
<configuration>
<appender name="gcpLoggingAppender" class="com.google.cloud.logging.logback.LoggingAppender">
<redirectToStdout>true</redirectToStdout>
</appender>
<root level="INFO">
<appender-ref ref="gcpLoggingAppender"/>
</root>
</configuration>
如果你使用Spring Boot作为框架,这个方法甚至可以定制,在本地环境下,日志会以逐行的方式写入标准输出,而当部署到GCP时,日志会被写成Json输出。
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<appender name="gcpLoggingAppender" class="com.google.cloud.logging.logback.LoggingAppender">
<redirectToStdout>true</redirectToStdout>
</appender>
<root level="INFO">
<springProfile name="gcp">
<appender-ref ref="gcpLoggingAppender"/>
</springProfile>
<springProfile name="local">
<appender-ref ref="CONSOLE"/>
</springProfile>
</root>
</configuration>
这很有效...但是
谷歌云的日志应用者工作得很好,但是有一个问题。由于某种原因,它不能捕获堆栈跟踪的全部内容。我有一个问题 ,应该可以解决这个问题。同时,如果在日志中捕获完整的堆栈是很重要的,那么一个不同的方法是使用logback提供的本地json布局简单地写一个json格式的日志。
<appender name="jsonLoggingAppender" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.contrib.json.classic.JsonLayout">
<jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter">
</jsonFormatter>
<timestampFormat>yyyy-MM-dd HH:mm:ss.SSS</timestampFormat>
<appendLineSeparator>true</appendLineSeparator>
</layout>
</appender>
然而,这些字段并不符合GCP推荐的结构化日志格式,特别是严重程度,可以通过实现一个自定义的JsonLayout类来进行快速调整,它看起来像这样。
package org.bk.logback.custom;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.contrib.json.classic.JsonLayout;
import com.google.cloud.logging.Severity;
import java.util.Map;
public class GcpJsonLayout extends JsonLayout {
private static final String SEVERITY_FIELD = "severity";
@Override
protected void addCustomDataToJsonMap(Map<String, Object> map, ILoggingEvent event) {
map.put(SEVERITY_FIELD, severityFor(event.getLevel()));
}
private static Severity severityFor(Level level) {
return switch (level.toInt()) {
// TRACE
case 5000 -> Severity.DEBUG;
// DEBUG
case 10000 -> Severity.DEBUG;
// INFO
case 20000 -> Severity.INFO;
// WARNING
case 30000 -> Severity.WARNING;
// ERROR
case 40000 -> Severity.ERROR;
default -> Severity.DEFAULT;
};
}
}
该类负责为云错误报告提供正确的严重性级别的映射。
结论
使用Google Cloud Logback appender,你就可以了。只有当你认为你缺乏更多的堆栈跟踪时,才考虑其他的方法。