blob: 61a2dea40c9cbfb543ef759bf88f9439ab0300a9 [file] [log] [blame]
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +02001package eu.mulk.quarkus.googlecloud.jsonlogging;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +02002
3import java.io.PrintWriter;
4import java.io.StringWriter;
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +02005import java.util.ArrayList;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +02006import java.util.Collection;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +02007import java.util.HashMap;
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +02008import java.util.List;
9import java.util.Map;
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010010import java.util.ServiceLoader;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020011import java.util.logging.Level;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020012import org.jboss.logmanager.ExtFormatter;
13import org.jboss.logmanager.ExtLogRecord;
14
15/**
16 * Formats log records as JSON for consumption by Google Cloud Logging.
17 *
18 * <p>Meant to be used in containers running on Google Kubernetes Engine (GKE).
19 *
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020020 * @see LogEntry
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020021 */
Matthias Andreas Benkardc066d892021-05-11 21:27:23 +020022public class Formatter extends ExtFormatter {
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020023
24 private static final String TRACE_LEVEL = "TRACE";
25 private static final String DEBUG_LEVEL = "DEBUG";
26 private static final String INFO_LEVEL = "INFO";
27 private static final String WARNING_LEVEL = "WARNING";
28 private static final String ERROR_LEVEL = "ERROR";
29
30 private static final String ERROR_EVENT_TYPE =
31 "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent";
32
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020033 private final List<StructuredParameterProvider> parameterProviders;
34 private final List<LabelProvider> labelProviders;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020035
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020036 /**
37 * Constructs a {@link Formatter}.
38 *
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010039 * <p><strong>Note:</strong> This constructor does not automatically discover providers using the
40 * {@link ServiceLoader} mechanism. See {@link #load} for this case use.
41 *
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020042 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
43 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
44 */
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020045 public Formatter(
46 Collection<StructuredParameterProvider> parameterProviders,
47 Collection<LabelProvider> labelProviders) {
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020048 this.parameterProviders = List.copyOf(parameterProviders);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020049 this.labelProviders = List.copyOf(labelProviders);
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020050 }
51
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010052 /**
53 * Constructs a {@link Formatter} with parameter and label providers supplied by {@link
54 * ServiceLoader}.
55 *
56 * <p>In addition to the providers supplied as parameters, this factory method loads all {@link
57 * StructuredParameterProvider}s and {@link LabelProvider}s found through the {@link
58 * ServiceLoader} mechanism.
59 *
60 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
61 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
62 */
63 public static Formatter load(
64 Collection<StructuredParameterProvider> parameterProviders,
65 Collection<LabelProvider> labelProviders) {
66 parameterProviders = new ArrayList<>(parameterProviders);
67 ServiceLoader.load(StructuredParameterProvider.class, Formatter.class.getClassLoader())
68 .forEach(parameterProviders::add);
69
70 labelProviders = new ArrayList<>(labelProviders);
71 ServiceLoader.load(LabelProvider.class, Formatter.class.getClassLoader())
72 .forEach(labelProviders::add);
73
74 return new Formatter(parameterProviders, labelProviders);
75 }
76
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020077 @Override
78 public String format(ExtLogRecord logRecord) {
79 var message = formatMessageWithStackTrace(logRecord);
80
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020081 List<StructuredParameter> parameters = new ArrayList<>();
82 Map<String, String> labels = new HashMap<>();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020083
84 for (var parameterProvider : parameterProviders) {
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020085 var parameter = parameterProvider.getParameter();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020086 if (parameter != null) {
87 parameters.add(parameter);
88 }
89 }
90
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020091 for (var labelProvider : labelProviders) {
92 var providedLabels = labelProvider.getLabels();
93 if (providedLabels != null) {
94 for (var label : providedLabels) {
95 labels.put(label.key(), label.value());
96 }
97 }
98 }
99
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200100 if (logRecord.getParameters() != null) {
101 for (var parameter : logRecord.getParameters()) {
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +0200102 if (parameter instanceof StructuredParameter) {
103 parameters.add((StructuredParameter) parameter);
104 } else if (parameter instanceof Label) {
105 var label = (Label) parameter;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200106 labels.put(label.key(), label.value());
107 }
108 }
109 }
110
111 var mdc = logRecord.getMdcCopy();
112 var ndc = logRecord.getNdc();
113
114 var sourceLocation =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200115 new LogEntry.SourceLocation(
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200116 logRecord.getSourceFileName(),
117 String.valueOf(logRecord.getSourceLineNumber()),
118 String.format(
119 "%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName()));
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200120
121 var entry =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200122 new LogEntry(
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200123 message,
124 severityOf(logRecord.getLevel()),
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200125 new LogEntry.Timestamp(logRecord.getInstant()),
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200126 null,
127 null,
128 sourceLocation,
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200129 labels,
130 parameters,
131 mdc,
132 ndc,
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200133 logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null);
134
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200135 return entry.json().build().toString() + "\n";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200136 }
137
138 /**
139 * Formats the log message corresponding to {@code logRecord} including a stack trace of the
140 * {@link ExtLogRecord#getThrown()} exception if any.
141 */
142 private String formatMessageWithStackTrace(ExtLogRecord logRecord) {
143 var messageStringWriter = new StringWriter();
144 var messagePrintWriter = new PrintWriter(messageStringWriter);
145 messagePrintWriter.append(this.formatMessage(logRecord));
146
147 if (logRecord.getThrown() != null) {
148 messagePrintWriter.println();
149 logRecord.getThrown().printStackTrace(messagePrintWriter);
150 }
151
152 messagePrintWriter.close();
153 return messageStringWriter.toString();
154 }
155
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200156 /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200157 private static String severityOf(Level level) {
158 if (level.intValue() < 500) {
159 return TRACE_LEVEL;
160 } else if (level.intValue() < 700) {
161 return DEBUG_LEVEL;
162 } else if (level.intValue() < 900) {
163 return INFO_LEVEL;
164 } else if (level.intValue() < 1000) {
165 return WARNING_LEVEL;
166 } else {
167 return ERROR_LEVEL;
168 }
169 }
170}