blob: e759ff028fee2abd61adb3e8133b3c314bc57a1e [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 Benkard348f2052022-01-15 16:13:01 +010011import java.util.ServiceLoader.Provider;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020012import java.util.logging.Level;
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010013import java.util.stream.Collectors;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020014import org.jboss.logmanager.ExtFormatter;
15import org.jboss.logmanager.ExtLogRecord;
16
17/**
18 * Formats log records as JSON for consumption by Google Cloud Logging.
19 *
20 * <p>Meant to be used in containers running on Google Kubernetes Engine (GKE).
21 *
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020022 * @see LogEntry
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020023 */
Matthias Andreas Benkardc066d892021-05-11 21:27:23 +020024public class Formatter extends ExtFormatter {
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020025
26 private static final String TRACE_LEVEL = "TRACE";
27 private static final String DEBUG_LEVEL = "DEBUG";
28 private static final String INFO_LEVEL = "INFO";
29 private static final String WARNING_LEVEL = "WARNING";
30 private static final String ERROR_LEVEL = "ERROR";
31
32 private static final String ERROR_EVENT_TYPE =
33 "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent";
34
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020035 private final List<StructuredParameterProvider> parameterProviders;
36 private final List<LabelProvider> labelProviders;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020037
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020038 /**
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010039 * Constructs a {@link Formatter} with custom configuration.
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020040 *
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010041 * <p><strong>Note:</strong> This constructor does not automatically discover providers using the
42 * {@link ServiceLoader} mechanism. See {@link #load} for this case use.
43 *
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020044 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
45 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
46 */
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020047 public Formatter(
48 Collection<StructuredParameterProvider> parameterProviders,
49 Collection<LabelProvider> labelProviders) {
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020050 this.parameterProviders = List.copyOf(parameterProviders);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020051 this.labelProviders = List.copyOf(labelProviders);
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020052 }
53
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010054 /**
55 * Constructs a {@link Formatter} with parameter and label providers supplied by {@link
56 * ServiceLoader}.
57 *
58 * <p>In addition to the providers supplied as parameters, this factory method loads all {@link
59 * StructuredParameterProvider}s and {@link LabelProvider}s found through the {@link
60 * ServiceLoader} mechanism.
61 *
62 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
63 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010064 * @return a new formatter.
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010065 */
66 public static Formatter load(
67 Collection<StructuredParameterProvider> parameterProviders,
68 Collection<LabelProvider> labelProviders) {
69 parameterProviders = new ArrayList<>(parameterProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010070 parameterProviders.addAll(loadStructuredParameterProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010071
72 labelProviders = new ArrayList<>(labelProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010073 labelProviders.addAll(loadLabelProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010074
75 return new Formatter(parameterProviders, labelProviders);
76 }
77
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010078 private static List<StructuredParameterProvider> loadStructuredParameterProviders() {
79 return ServiceLoader.load(StructuredParameterProvider.class, Formatter.class.getClassLoader())
80 .stream()
81 .map(Provider::get)
82 .collect(Collectors.toList());
83 }
84
85 private static List<LabelProvider> loadLabelProviders() {
86 return ServiceLoader.load(LabelProvider.class, Formatter.class.getClassLoader()).stream()
87 .map(Provider::get)
88 .collect(Collectors.toList());
89 }
90
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020091 @Override
92 public String format(ExtLogRecord logRecord) {
93 var message = formatMessageWithStackTrace(logRecord);
94
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020095 List<StructuredParameter> parameters = new ArrayList<>();
96 Map<String, String> labels = new HashMap<>();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020097
98 for (var parameterProvider : parameterProviders) {
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020099 var parameter = parameterProvider.getParameter();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200100 if (parameter != null) {
101 parameters.add(parameter);
102 }
103 }
104
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200105 for (var labelProvider : labelProviders) {
106 var providedLabels = labelProvider.getLabels();
107 if (providedLabels != null) {
108 for (var label : providedLabels) {
109 labels.put(label.key(), label.value());
110 }
111 }
112 }
113
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200114 if (logRecord.getParameters() != null) {
115 for (var parameter : logRecord.getParameters()) {
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +0200116 if (parameter instanceof StructuredParameter) {
117 parameters.add((StructuredParameter) parameter);
118 } else if (parameter instanceof Label) {
119 var label = (Label) parameter;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200120 labels.put(label.key(), label.value());
121 }
122 }
123 }
124
125 var mdc = logRecord.getMdcCopy();
126 var ndc = logRecord.getNdc();
127
128 var sourceLocation =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200129 new LogEntry.SourceLocation(
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200130 logRecord.getSourceFileName(),
131 String.valueOf(logRecord.getSourceLineNumber()),
132 String.format(
133 "%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName()));
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200134
135 var entry =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200136 new LogEntry(
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200137 message,
138 severityOf(logRecord.getLevel()),
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200139 new LogEntry.Timestamp(logRecord.getInstant()),
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200140 null,
141 null,
142 sourceLocation,
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200143 labels,
144 parameters,
145 mdc,
146 ndc,
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200147 logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null);
148
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200149 return entry.json().build().toString() + "\n";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200150 }
151
152 /**
153 * Formats the log message corresponding to {@code logRecord} including a stack trace of the
154 * {@link ExtLogRecord#getThrown()} exception if any.
155 */
156 private String formatMessageWithStackTrace(ExtLogRecord logRecord) {
157 var messageStringWriter = new StringWriter();
158 var messagePrintWriter = new PrintWriter(messageStringWriter);
159 messagePrintWriter.append(this.formatMessage(logRecord));
160
161 if (logRecord.getThrown() != null) {
162 messagePrintWriter.println();
163 logRecord.getThrown().printStackTrace(messagePrintWriter);
164 }
165
166 messagePrintWriter.close();
167 return messageStringWriter.toString();
168 }
169
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200170 /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200171 private static String severityOf(Level level) {
172 if (level.intValue() < 500) {
173 return TRACE_LEVEL;
174 } else if (level.intValue() < 700) {
175 return DEBUG_LEVEL;
176 } else if (level.intValue() < 900) {
177 return INFO_LEVEL;
178 } else if (level.intValue() < 1000) {
179 return WARNING_LEVEL;
180 } else {
181 return ERROR_LEVEL;
182 }
183 }
184}