blob: c4e36de62ac5eb05878e42b52e759cc7ec7ac396 [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
7import java.io.PrintWriter;
8import java.io.StringWriter;
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +02009import java.util.ArrayList;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020010import java.util.Collection;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020011import java.util.HashMap;
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020012import java.util.List;
13import java.util.Map;
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010014import java.util.ServiceLoader;
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010015import java.util.ServiceLoader.Provider;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020016import java.util.logging.Level;
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010017import java.util.stream.Collectors;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020018import org.jboss.logmanager.ExtFormatter;
19import org.jboss.logmanager.ExtLogRecord;
20
21/**
22 * Formats log records as JSON for consumption by Google Cloud Logging.
23 *
24 * <p>Meant to be used in containers running on Google Kubernetes Engine (GKE).
25 *
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020026 * @see LogEntry
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020027 */
Matthias Andreas Benkardc066d892021-05-11 21:27:23 +020028public class Formatter extends ExtFormatter {
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020029
30 private static final String TRACE_LEVEL = "TRACE";
31 private static final String DEBUG_LEVEL = "DEBUG";
32 private static final String INFO_LEVEL = "INFO";
33 private static final String WARNING_LEVEL = "WARNING";
34 private static final String ERROR_LEVEL = "ERROR";
35
36 private static final String ERROR_EVENT_TYPE =
37 "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent";
38
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020039 private final List<StructuredParameterProvider> parameterProviders;
40 private final List<LabelProvider> labelProviders;
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020041
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020042 /**
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010043 * Constructs a {@link Formatter} with custom configuration.
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020044 *
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010045 * <p><strong>Note:</strong> This constructor does not automatically discover providers using the
46 * {@link ServiceLoader} mechanism. See {@link #load} for this case use.
47 *
Matthias Andreas Benkard42da9f12021-09-02 18:47:28 +020048 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
49 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
50 */
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020051 public Formatter(
52 Collection<StructuredParameterProvider> parameterProviders,
53 Collection<LabelProvider> labelProviders) {
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020054 this.parameterProviders = List.copyOf(parameterProviders);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +020055 this.labelProviders = List.copyOf(labelProviders);
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +020056 }
57
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010058 /**
59 * Constructs a {@link Formatter} with parameter and label providers supplied by {@link
60 * ServiceLoader}.
61 *
62 * <p>In addition to the providers supplied as parameters, this factory method loads all {@link
63 * StructuredParameterProvider}s and {@link LabelProvider}s found through the {@link
64 * ServiceLoader} mechanism.
65 *
66 * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry.
67 * @param labelProviders the {@link LabelProvider}s to apply to each log entry.
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010068 * @return a new formatter.
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010069 */
70 public static Formatter load(
71 Collection<StructuredParameterProvider> parameterProviders,
72 Collection<LabelProvider> labelProviders) {
73 parameterProviders = new ArrayList<>(parameterProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010074 parameterProviders.addAll(loadStructuredParameterProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010075
76 labelProviders = new ArrayList<>(labelProviders);
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010077 labelProviders.addAll(loadLabelProviders());
Matthias Andreas Benkard93ecfd12022-01-15 14:03:41 +010078
79 return new Formatter(parameterProviders, labelProviders);
80 }
81
Matthias Andreas Benkard348f2052022-01-15 16:13:01 +010082 private static List<StructuredParameterProvider> loadStructuredParameterProviders() {
83 return ServiceLoader.load(StructuredParameterProvider.class, Formatter.class.getClassLoader())
84 .stream()
85 .map(Provider::get)
86 .collect(Collectors.toList());
87 }
88
89 private static List<LabelProvider> loadLabelProviders() {
90 return ServiceLoader.load(LabelProvider.class, Formatter.class.getClassLoader()).stream()
91 .map(Provider::get)
92 .collect(Collectors.toList());
93 }
94
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +020095 @Override
96 public String format(ExtLogRecord logRecord) {
97 var message = formatMessageWithStackTrace(logRecord);
98
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +020099 List<StructuredParameter> parameters = new ArrayList<>();
100 Map<String, String> labels = new HashMap<>();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200101
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200102 var providerContext = new ProviderContext(logRecord);
103
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200104 for (var parameterProvider : parameterProviders) {
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200105 var parameter = parameterProvider.getParameter(providerContext);
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200106 if (parameter != null) {
107 parameters.add(parameter);
108 }
109 }
110
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200111 for (var labelProvider : labelProviders) {
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200112 var providedLabels = labelProvider.getLabels(providerContext);
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200113 if (providedLabels != null) {
114 for (var label : providedLabels) {
115 labels.put(label.key(), label.value());
116 }
117 }
118 }
119
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200120 if (logRecord.getParameters() != null) {
121 for (var parameter : logRecord.getParameters()) {
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +0200122 if (parameter instanceof StructuredParameter) {
123 parameters.add((StructuredParameter) parameter);
124 } else if (parameter instanceof Label) {
125 var label = (Label) parameter;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200126 labels.put(label.key(), label.value());
127 }
128 }
129 }
130
131 var mdc = logRecord.getMdcCopy();
132 var ndc = logRecord.getNdc();
133
134 var sourceLocation =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200135 new LogEntry.SourceLocation(
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200136 logRecord.getSourceFileName(),
137 String.valueOf(logRecord.getSourceLineNumber()),
138 String.format(
139 "%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName()));
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200140
141 var entry =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200142 new LogEntry(
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200143 message,
144 severityOf(logRecord.getLevel()),
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200145 new LogEntry.Timestamp(logRecord.getInstant()),
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200146 null,
147 null,
148 sourceLocation,
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200149 labels,
150 parameters,
151 mdc,
152 ndc,
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200153 logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null);
154
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200155 return entry.json().build().toString() + "\n";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200156 }
157
158 /**
159 * Formats the log message corresponding to {@code logRecord} including a stack trace of the
160 * {@link ExtLogRecord#getThrown()} exception if any.
161 */
162 private String formatMessageWithStackTrace(ExtLogRecord logRecord) {
163 var messageStringWriter = new StringWriter();
164 var messagePrintWriter = new PrintWriter(messageStringWriter);
165 messagePrintWriter.append(this.formatMessage(logRecord));
166
167 if (logRecord.getThrown() != null) {
168 messagePrintWriter.println();
169 logRecord.getThrown().printStackTrace(messagePrintWriter);
170 }
171
172 messagePrintWriter.close();
173 return messageStringWriter.toString();
174 }
175
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200176 /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200177 private static String severityOf(Level level) {
178 if (level.intValue() < 500) {
179 return TRACE_LEVEL;
180 } else if (level.intValue() < 700) {
181 return DEBUG_LEVEL;
182 } else if (level.intValue() < 900) {
183 return INFO_LEVEL;
184 } else if (level.intValue() < 1000) {
185 return WARNING_LEVEL;
186 } else {
187 return ERROR_LEVEL;
188 }
189 }
Matthias Andreas Benkard2cc18b32022-09-03 10:03:30 +0200190
191 /**
192 * An implementation of {@link LabelProvider.Context} and {@link
193 * StructuredParameterProvider.Context}.
194 */
195 private static class ProviderContext
196 implements LabelProvider.Context, StructuredParameterProvider.Context {
197
198 private final String loggerName;
199 private final long sequenceNumber;
200 private final String threadName;
201
202 private ProviderContext(ExtLogRecord logRecord) {
203 loggerName = logRecord.getLoggerName();
204 sequenceNumber = logRecord.getSequenceNumber();
205 threadName = logRecord.getThreadName();
206 }
207
208 @Override
209 public String loggerName() {
210 return loggerName;
211 }
212
213 @Override
214 public long sequenceNumber() {
215 return sequenceNumber;
216 }
217
218 @Override
219 public String threadName() {
220 return threadName;
221 }
222 }
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200223}