summaryrefslogtreecommitdiff
path: root/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingFormatter.java
blob: a44e8b57e573521c23396f228cd25f35bfd5d834 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package eu.mulk.quarkus.observability.googlecloud.jsonlogging;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.logging.Level;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbException;
import org.jboss.logmanager.ExtFormatter;
import org.jboss.logmanager.ExtLogRecord;

/**
 * Formats log records as JSON for consumption by Google Cloud Logging.
 *
 * <p>Meant to be used in containers running on Google Kubernetes Engine (GKE).
 *
 * @see GoogleCloudLogEntry
 */
class GoogleCloudLoggingFormatter extends ExtFormatter {

  private static final String TRACE_LEVEL = "TRACE";
  private static final String DEBUG_LEVEL = "DEBUG";
  private static final String INFO_LEVEL = "INFO";
  private static final String WARNING_LEVEL = "WARNING";
  private static final String ERROR_LEVEL = "ERROR";

  private static final String ERROR_EVENT_TYPE =
      "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent";

  private final Jsonb jsonb;

  GoogleCloudLoggingFormatter(Jsonb jsonb) {
    this.jsonb = jsonb;
  }

  @Override
  public String format(ExtLogRecord logRecord) {
    var message = formatMessageWithStackTrace(logRecord);

    var parameters = new HashMap<String, Object>();
    var labels = new HashMap<String, String>();
    if (logRecord.getParameters() != null) {
      for (var parameter : logRecord.getParameters()) {
        if (parameter instanceof KeyValueParameter kvparam) {
          parameters.put(kvparam.key(), kvparam.value());
        } else if (parameter instanceof Label label) {
          labels.put(label.key(), label.value());
        }
      }
    }

    var mdc = logRecord.getMdcCopy();
    var ndc = logRecord.getNdc();

    var sourceLocation =
        new GoogleCloudLogEntry.SourceLocation(
            logRecord.getSourceFileName(), String.valueOf(logRecord.getSourceLineNumber()), String.format("%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName()));

    var entry =
        new GoogleCloudLogEntry(
            message,
            severityOf(logRecord.getLevel()),
            new GoogleCloudLogEntry.Timestamp(logRecord.getInstant()),
            null,
            null,
            sourceLocation,
            labels.isEmpty() ? null : labels,
            parameters.isEmpty() ? null : parameters,
            mdc.isEmpty() ? null : mdc,
            ndc.isEmpty() ? null : ndc,
            logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null);

    try {
      return jsonb.toJson(entry) + "\n";
    } catch (JsonbException e) {
      e.printStackTrace();
      return message + "\n";
    }
  }

  /**
   * Formats the log message corresponding to {@code logRecord} including a stack trace of the
   * {@link ExtLogRecord#getThrown()} exception if any.
   */
  private String formatMessageWithStackTrace(ExtLogRecord logRecord) {
    var messageStringWriter = new StringWriter();
    var messagePrintWriter = new PrintWriter(messageStringWriter);
    messagePrintWriter.append(this.formatMessage(logRecord));

    if (logRecord.getThrown() != null) {
      messagePrintWriter.println();
      logRecord.getThrown().printStackTrace(messagePrintWriter);
    }

    messagePrintWriter.close();
    return messageStringWriter.toString();
  }

  /**
   * Computes the Google Cloud Logging severity corresponding to a given {@link Level}.
   */
  private static String severityOf(Level level) {
    if (level.intValue() < 500) {
      return TRACE_LEVEL;
    } else if (level.intValue() < 700) {
      return DEBUG_LEVEL;
    } else if (level.intValue() < 900) {
      return INFO_LEVEL;
    } else if (level.intValue() < 1000) {
      return WARNING_LEVEL;
    } else {
      return ERROR_LEVEL;
    }
  }
}