blob: 72b0b50fed5de094e9026cdd2b81885111ab4535 [file] [log] [blame]
Matthias Andreas Benkard80909242022-02-03 20:47:47 +01001// SPDX-FileCopyrightText: © 2021 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
2//
3// SPDX-License-Identifier: LGPL-3.0-or-later
4
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +02005package eu.mulk.quarkus.googlecloud.jsonlogging;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +02006
Matthias Andreas Benkardd2280ad2024-06-23 17:08:58 +02007import jakarta.json.spi.JsonProvider;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +02008import java.io.PrintWriter;
9import java.io.StringWriter;
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020010import java.util.ArrayList;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020011import java.util.Collection;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020012import java.util.HashMap;
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020013import java.util.List;
14import java.util.Map;
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010015import java.util.ServiceLoader;
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010016import java.util.ServiceLoader.Provider;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020017import java.util.logging.Level;
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010018import java.util.stream.Collectors;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020019import org.jboss.logmanager.ExtFormatter;
20import org.jboss.logmanager.ExtLogRecord;
21
22/**
23 * Formats log records as JSON for consumption by Google Cloud Logging.
24 *
25 * <p>Meant to be used in containers running on Google Kubernetes Engine (GKE).
26 *
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020027 * @see LogEntry
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020028 */
Matthias Andreas Benkardc066d892021-05-11 21:27:23 +020029public class Formatter extends ExtFormatter {
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020030
31 private static final String TRACE_LEVEL = "TRACE";
32 private static final String DEBUG_LEVEL = "DEBUG";
33 private static final String INFO_LEVEL = "INFO";
34 private static final String WARNING_LEVEL = "WARNING";
35 private static final String ERROR_LEVEL = "ERROR";
36
37 private static final String ERROR_EVENT_TYPE =
38 "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent";
39
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020040 private final List<StructuredParameterProvider> parameterProviders;
41 private final List<LabelProvider> labelProviders;
Matthias Andreas Benkardd2280ad2024-06-23 17:08:58 +020042 private final JsonProvider json;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020043
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020044 /**
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010045 * Constructs a {@link Formatter} with custom configuration.
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020046 *
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010047 * <p><strong>Note:</strong> This constructor does not automatically discover providers using the
48 * {@link ServiceLoader} mechanism. See {@link #load} for this case use.
49 *
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020050 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
51 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
52 */
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020053 public Formatter(
54 Collection<StructuredParameterProvider> parameterProviders,
55 Collection<LabelProvider> labelProviders) {
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020056 this.parameterProviders = List.copyOf(parameterProviders);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020057 this.labelProviders = List.copyOf(labelProviders);
Matthias Andreas Benkardd2280ad2024-06-23 17:08:58 +020058 this.json = JsonProvider.provider();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020059 }
60
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010061 /**
62 * Constructs a {@link Formatter} with parameter and label providers supplied by {@link
63 * ServiceLoader}.
64 *
65 * <p>In addition to the providers supplied as parameters, this factory method loads all {@link
66 * StructuredParameterProvider}s and {@link LabelProvider}s found through the {@link
67 * ServiceLoader} mechanism.
68 *
69 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
70 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010071 * @return a new formatter.
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010072 */
73 public static Formatter load(
74 Collection<StructuredParameterProvider> parameterProviders,
75 Collection<LabelProvider> labelProviders) {
76 parameterProviders = new ArrayList<>(parameterProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010077 parameterProviders.addAll(loadStructuredParameterProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010078
79 labelProviders = new ArrayList<>(labelProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010080 labelProviders.addAll(loadLabelProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010081
82 return new Formatter(parameterProviders, labelProviders);
83 }
84
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010085 private static List<StructuredParameterProvider> loadStructuredParameterProviders() {
86 return ServiceLoader.load(StructuredParameterProvider.class, Formatter.class.getClassLoader())
87 .stream()
88 .map(Provider::get)
89 .collect(Collectors.toList());
90 }
91
92 private static List<LabelProvider> loadLabelProviders() {
93 return ServiceLoader.load(LabelProvider.class, Formatter.class.getClassLoader()).stream()
94 .map(Provider::get)
95 .collect(Collectors.toList());
96 }
97
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020098 @Override
99 public String format(ExtLogRecord logRecord) {
100 var message = formatMessageWithStackTrace(logRecord);
101
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200102 List<StructuredParameter> parameters = new ArrayList<>();
103 Map<String, String> labels = new HashMap<>();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200104
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200105 var providerContext = new ProviderContext(logRecord);
106
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200107 for (var parameterProvider : parameterProviders) {
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200108 var parameter = parameterProvider.getParameter(providerContext);
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200109 if (parameter != null) {
110 parameters.add(parameter);
111 }
112 }
113
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200114 for (var labelProvider : labelProviders) {
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200115 var providedLabels = labelProvider.getLabels(providerContext);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200116 if (providedLabels != null) {
117 for (var label : providedLabels) {
118 labels.put(label.key(), label.value());
119 }
120 }
121 }
122
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200123 if (logRecord.getParameters() != null) {
124 for (var parameter : logRecord.getParameters()) {
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +0200125 if (parameter instanceof StructuredParameter) {
126 parameters.add((StructuredParameter) parameter);
127 } else if (parameter instanceof Label) {
128 var label = (Label) parameter;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200129 labels.put(label.key(), label.value());
130 }
131 }
132 }
133
134 var mdc = logRecord.getMdcCopy();
135 var ndc = logRecord.getNdc();
136
137 var sourceLocation =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200138 new LogEntry.SourceLocation(
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200139 logRecord.getSourceFileName(),
140 String.valueOf(logRecord.getSourceLineNumber()),
141 String.format(
142 "%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName()));
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200143
144 var entry =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200145 new LogEntry(
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200146 message,
147 severityOf(logRecord.getLevel()),
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200148 new LogEntry.Timestamp(logRecord.getInstant()),
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200149 null,
150 null,
151 sourceLocation,
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200152 labels,
153 parameters,
154 mdc,
155 ndc,
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200156 logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null);
157
Matthias Andreas Benkardd2280ad2024-06-23 17:08:58 +0200158 return entry.json(json).build().toString() + "\n";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200159 }
160
161 /**
162 * Formats the log message corresponding to {@code logRecord} including a stack trace of the
163 * {@link ExtLogRecord#getThrown()} exception if any.
164 */
165 private String formatMessageWithStackTrace(ExtLogRecord logRecord) {
166 var messageStringWriter = new StringWriter();
167 var messagePrintWriter = new PrintWriter(messageStringWriter);
168 messagePrintWriter.append(this.formatMessage(logRecord));
169
170 if (logRecord.getThrown() != null) {
171 messagePrintWriter.println();
172 logRecord.getThrown().printStackTrace(messagePrintWriter);
173 }
174
175 messagePrintWriter.close();
176 return messageStringWriter.toString();
177 }
178
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200179 /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200180 private static String severityOf(Level level) {
181 if (level.intValue() < 500) {
182 return TRACE_LEVEL;
183 } else if (level.intValue() < 700) {
184 return DEBUG_LEVEL;
185 } else if (level.intValue() < 900) {
186 return INFO_LEVEL;
187 } else if (level.intValue() < 1000) {
188 return WARNING_LEVEL;
189 } else {
190 return ERROR_LEVEL;
191 }
192 }
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200193
194 /**
195 * An implementation of {@link LabelProvider.Context} and {@link
196 * StructuredParameterProvider.Context}.
197 */
198 private static class ProviderContext
199 implements LabelProvider.Context, StructuredParameterProvider.Context {
200
201 private final String loggerName;
202 private final long sequenceNumber;
203 private final String threadName;
204
205 private ProviderContext(ExtLogRecord logRecord) {
206 loggerName = logRecord.getLoggerName();
207 sequenceNumber = logRecord.getSequenceNumber();
208 threadName = logRecord.getThreadName();
209 }
210
211 @Override
212 public String loggerName() {
213 return loggerName;
214 }
215
216 @Override
217 public long sequenceNumber() {
218 return sequenceNumber;
219 }
220
221 @Override
222 public String threadName() {
223 return threadName;
224 }
225 }
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200226}