blob: a7fd55110ef5f56d3448382606f2fc79ff15e701 [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";
Matthias Andreas Benkardfd9f00c2024-06-25 22:34:55 +020034 private static final String NOTICE_LEVEL = "NOTICE";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020035 private static final String WARNING_LEVEL = "WARNING";
36 private static final String ERROR_LEVEL = "ERROR";
Matthias Andreas Benkardfd9f00c2024-06-25 22:34:55 +020037 private static final String CRITICAL_LEVEL = "CRITICAL";
38 private static final String ALERT_LEVEL = "ALERT";
39 private static final String EMERGENCY_LEVEL = "EMERGENCY";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020040
41 private static final String ERROR_EVENT_TYPE =
42 "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent";
43
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020044 private final List<StructuredParameterProvider> parameterProviders;
45 private final List<LabelProvider> labelProviders;
Matthias Andreas Benkardd2280ad2024-06-23 17:08:58 +020046 private final JsonProvider json;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020047
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020048 /**
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010049 * Constructs a {@link Formatter} with custom configuration.
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020050 *
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010051 * <p><strong>Note:</strong> This constructor does not automatically discover providers using the
52 * {@link ServiceLoader} mechanism. See {@link #load} for this case use.
53 *
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020054 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
55 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
56 */
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020057 public Formatter(
58 Collection<StructuredParameterProvider> parameterProviders,
59 Collection<LabelProvider> labelProviders) {
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020060 this.parameterProviders = List.copyOf(parameterProviders);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020061 this.labelProviders = List.copyOf(labelProviders);
Matthias Andreas Benkardd2280ad2024-06-23 17:08:58 +020062 this.json = JsonProvider.provider();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020063 }
64
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010065 /**
66 * Constructs a {@link Formatter} with parameter and label providers supplied by {@link
67 * ServiceLoader}.
68 *
69 * <p>In addition to the providers supplied as parameters, this factory method loads all {@link
70 * StructuredParameterProvider}s and {@link LabelProvider}s found through the {@link
71 * ServiceLoader} mechanism.
72 *
73 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
74 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010075 * @return a new formatter.
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010076 */
77 public static Formatter load(
78 Collection<StructuredParameterProvider> parameterProviders,
79 Collection<LabelProvider> labelProviders) {
80 parameterProviders = new ArrayList<>(parameterProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010081 parameterProviders.addAll(loadStructuredParameterProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010082
83 labelProviders = new ArrayList<>(labelProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010084 labelProviders.addAll(loadLabelProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010085
86 return new Formatter(parameterProviders, labelProviders);
87 }
88
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010089 private static List<StructuredParameterProvider> loadStructuredParameterProviders() {
90 return ServiceLoader.load(StructuredParameterProvider.class, Formatter.class.getClassLoader())
91 .stream()
92 .map(Provider::get)
93 .collect(Collectors.toList());
94 }
95
96 private static List<LabelProvider> loadLabelProviders() {
97 return ServiceLoader.load(LabelProvider.class, Formatter.class.getClassLoader()).stream()
98 .map(Provider::get)
99 .collect(Collectors.toList());
100 }
101
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200102 @Override
103 public String format(ExtLogRecord logRecord) {
104 var message = formatMessageWithStackTrace(logRecord);
105
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200106 List<StructuredParameter> parameters = new ArrayList<>();
107 Map<String, String> labels = new HashMap<>();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200108
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200109 var providerContext = new ProviderContext(logRecord);
110
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200111 for (var parameterProvider : parameterProviders) {
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200112 var parameter = parameterProvider.getParameter(providerContext);
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200113 if (parameter != null) {
114 parameters.add(parameter);
115 }
116 }
117
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200118 for (var labelProvider : labelProviders) {
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200119 var providedLabels = labelProvider.getLabels(providerContext);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200120 if (providedLabels != null) {
121 for (var label : providedLabels) {
122 labels.put(label.key(), label.value());
123 }
124 }
125 }
126
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200127 if (logRecord.getParameters() != null) {
128 for (var parameter : logRecord.getParameters()) {
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +0200129 if (parameter instanceof StructuredParameter) {
130 parameters.add((StructuredParameter) parameter);
131 } else if (parameter instanceof Label) {
132 var label = (Label) parameter;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200133 labels.put(label.key(), label.value());
134 }
135 }
136 }
137
138 var mdc = logRecord.getMdcCopy();
139 var ndc = logRecord.getNdc();
140
Matthias Andreas Benkard3af29c32024-06-25 22:31:04 +0200141 var sourceLocation = sourceLocationOf(logRecord);
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200142
143 var entry =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200144 new LogEntry(
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200145 message,
146 severityOf(logRecord.getLevel()),
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200147 new LogEntry.Timestamp(logRecord.getInstant()),
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200148 null,
149 null,
150 sourceLocation,
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200151 labels,
152 parameters,
153 mdc,
154 ndc,
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200155 logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null);
156
Matthias Andreas Benkardd2280ad2024-06-23 17:08:58 +0200157 return entry.json(json).build().toString() + "\n";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200158 }
159
Matthias Andreas Benkard3af29c32024-06-25 22:31:04 +0200160 private static LogEntry.SourceLocation sourceLocationOf(ExtLogRecord logRecord) {
161 var sourceFileName = logRecord.getSourceFileName();
162 var sourceLineNumber = logRecord.getSourceLineNumber();
163 var sourceClassName = logRecord.getSourceClassName();
164 var sourceMethodName = logRecord.getSourceMethodName();
165 return (sourceFileName == null
166 && sourceLineNumber <= 0
167 && sourceClassName == null
168 && sourceMethodName == null)
169 ? null
170 : new LogEntry.SourceLocation(
171 sourceFileName,
172 String.valueOf(sourceLineNumber),
173 String.format("%s.%s", sourceClassName, sourceMethodName));
174 }
175
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200176 /**
177 * Formats the log message corresponding to {@code logRecord} including a stack trace of the
178 * {@link ExtLogRecord#getThrown()} exception if any.
179 */
180 private String formatMessageWithStackTrace(ExtLogRecord logRecord) {
181 var messageStringWriter = new StringWriter();
182 var messagePrintWriter = new PrintWriter(messageStringWriter);
183 messagePrintWriter.append(this.formatMessage(logRecord));
184
185 if (logRecord.getThrown() != null) {
186 messagePrintWriter.println();
187 logRecord.getThrown().printStackTrace(messagePrintWriter);
188 }
189
190 messagePrintWriter.close();
191 return messageStringWriter.toString();
192 }
193
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200194 /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200195 private static String severityOf(Level level) {
196 if (level.intValue() < 500) {
197 return TRACE_LEVEL;
198 } else if (level.intValue() < 700) {
199 return DEBUG_LEVEL;
Matthias Andreas Benkardfd9f00c2024-06-25 22:34:55 +0200200 } else if (level.intValue() < 850) {
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200201 return INFO_LEVEL;
Matthias Andreas Benkardfd9f00c2024-06-25 22:34:55 +0200202 } else if (level.intValue() < 900) {
203 return NOTICE_LEVEL;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200204 } else if (level.intValue() < 1000) {
205 return WARNING_LEVEL;
Matthias Andreas Benkardfd9f00c2024-06-25 22:34:55 +0200206 } else if (level.intValue() < 1100) {
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200207 return ERROR_LEVEL;
Matthias Andreas Benkardfd9f00c2024-06-25 22:34:55 +0200208 } else if (level.intValue() < 1200) {
209 return CRITICAL_LEVEL;
210 } else if (level.intValue() < 1300) {
211 return ALERT_LEVEL;
212 } else {
213 return EMERGENCY_LEVEL;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200214 }
215 }
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200216
217 /**
218 * An implementation of {@link LabelProvider.Context} and {@link
219 * StructuredParameterProvider.Context}.
220 */
221 private static class ProviderContext
222 implements LabelProvider.Context, StructuredParameterProvider.Context {
223
224 private final String loggerName;
225 private final long sequenceNumber;
226 private final String threadName;
227
228 private ProviderContext(ExtLogRecord logRecord) {
229 loggerName = logRecord.getLoggerName();
230 sequenceNumber = logRecord.getSequenceNumber();
231 threadName = logRecord.getThreadName();
232 }
233
234 @Override
235 public String loggerName() {
236 return loggerName;
237 }
238
239 @Override
240 public long sequenceNumber() {
241 return sequenceNumber;
242 }
243
244 @Override
245 public String threadName() {
246 return threadName;
247 }
248 }
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200249}