blob: f6167f6be2c679d41ab1c9f78a435e1fac1eac7b [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
102 for (var parameterProvider : parameterProviders) {
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200103 var parameter = parameterProvider.getParameter();
Matthias Andreas Benkard82d7e442021-08-29 08:34:11 +0200104 if (parameter != null) {
105 parameters.add(parameter);
106 }
107 }
108
Matthias Andreas Benkard692f48d2021-08-31 21:06:50 +0200109 for (var labelProvider : labelProviders) {
110 var providedLabels = labelProvider.getLabels();
111 if (providedLabels != null) {
112 for (var label : providedLabels) {
113 labels.put(label.key(), label.value());
114 }
115 }
116 }
117
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200118 if (logRecord.getParameters() != null) {
119 for (var parameter : logRecord.getParameters()) {
Matthias Andreas Benkard121a6312021-05-12 05:41:25 +0200120 if (parameter instanceof StructuredParameter) {
121 parameters.add((StructuredParameter) parameter);
122 } else if (parameter instanceof Label) {
123 var label = (Label) parameter;
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200124 labels.put(label.key(), label.value());
125 }
126 }
127 }
128
129 var mdc = logRecord.getMdcCopy();
130 var ndc = logRecord.getNdc();
131
132 var sourceLocation =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200133 new LogEntry.SourceLocation(
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200134 logRecord.getSourceFileName(),
135 String.valueOf(logRecord.getSourceLineNumber()),
136 String.format(
137 "%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName()));
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200138
139 var entry =
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200140 new LogEntry(
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200141 message,
142 severityOf(logRecord.getLevel()),
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200143 new LogEntry.Timestamp(logRecord.getInstant()),
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200144 null,
145 null,
146 sourceLocation,
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200147 labels,
148 parameters,
149 mdc,
150 ndc,
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200151 logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null);
152
Matthias Andreas Benkardb8fbc372021-05-11 06:50:45 +0200153 return entry.json().build().toString() + "\n";
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200154 }
155
156 /**
157 * Formats the log message corresponding to {@code logRecord} including a stack trace of the
158 * {@link ExtLogRecord#getThrown()} exception if any.
159 */
160 private String formatMessageWithStackTrace(ExtLogRecord logRecord) {
161 var messageStringWriter = new StringWriter();
162 var messagePrintWriter = new PrintWriter(messageStringWriter);
163 messagePrintWriter.append(this.formatMessage(logRecord));
164
165 if (logRecord.getThrown() != null) {
166 messagePrintWriter.println();
167 logRecord.getThrown().printStackTrace(messagePrintWriter);
168 }
169
170 messagePrintWriter.close();
171 return messageStringWriter.toString();
172 }
173
Matthias Andreas Benkard4bae5f12021-05-03 19:16:48 +0200174 /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */
Matthias Andreas Benkardc8144a92021-05-03 08:04:53 +0200175 private static String severityOf(Level level) {
176 if (level.intValue() < 500) {
177 return TRACE_LEVEL;
178 } else if (level.intValue() < 700) {
179 return DEBUG_LEVEL;
180 } else if (level.intValue() < 900) {
181 return INFO_LEVEL;
182 } else if (level.intValue() < 1000) {
183 return WARNING_LEVEL;
184 } else {
185 return ERROR_LEVEL;
186 }
187 }
188}