在谷歌云平台上从Java应用程序中记录日志的方法

318 阅读4分钟

当把应用程序定位到云平台时,我看到的一个关于日志的建议是简单地写到标准输出,平台负责把它发送到适当的日志汇中。这个方法大部分时候都是有效的,除非它不适用--尤其是在分析故障情况时。通常,对于Java应用程序来说,这意味着查看堆栈跟踪,而堆栈跟踪的每一行都被日志汇视为一个单独的日志条目,这就造成了这些问题:

  1. 将多行输出作为单一堆栈跟踪的一部分进行关联
  2. 由于应用程序是多线程的,即使是相关的日志,其顺序也不一定正确
  3. 日志的严重性没有被正确地确定,所以没有找到进入错误报告系统的途径。

这篇文章将介绍在谷歌云平台上从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,你就可以了。只有当你认为你缺乏更多的堆栈跟踪时,才考虑其他的方法。