Matthias Andreas Benkard | 8090924 | 2022-02-03 20:47:47 +0100 | [diff] [blame] | 1 | // SPDX-FileCopyrightText: © 2021 Matthias Andreas Benkard <code@mail.matthias.benkard.de> |
| 2 | // |
| 3 | // SPDX-License-Identifier: LGPL-3.0-or-later |
| 4 | |
Matthias Andreas Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 5 | package eu.mulk.quarkus.googlecloud.jsonlogging; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 6 | |
Matthias Andreas Benkard | d2280ad | 2024-06-23 17:08:58 +0200 | [diff] [blame] | 7 | import jakarta.json.spi.JsonProvider; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 8 | import java.io.PrintWriter; |
| 9 | import java.io.StringWriter; |
Matthias Andreas Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 10 | import java.util.ArrayList; |
Matthias Andreas Benkard | 82d7e44 | 2021-08-29 08:34:11 +0200 | [diff] [blame] | 11 | import java.util.Collection; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 12 | import java.util.HashMap; |
Matthias Andreas Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 13 | import java.util.List; |
| 14 | import java.util.Map; |
Matthias Andreas Benkard | 93ecfd1 | 2022-01-15 14:03:41 +0100 | [diff] [blame] | 15 | import java.util.ServiceLoader; |
Matthias Andreas Benkard | 348f205 | 2022-01-15 16:13:01 +0100 | [diff] [blame] | 16 | import java.util.ServiceLoader.Provider; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 17 | import java.util.logging.Level; |
Matthias Andreas Benkard | 348f205 | 2022-01-15 16:13:01 +0100 | [diff] [blame] | 18 | import java.util.stream.Collectors; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 19 | import org.jboss.logmanager.ExtFormatter; |
| 20 | import 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 Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 27 | * @see LogEntry |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 28 | */ |
Matthias Andreas Benkard | c066d89 | 2021-05-11 21:27:23 +0200 | [diff] [blame] | 29 | public class Formatter extends ExtFormatter { |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 30 | |
| 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 Benkard | fd9f00c | 2024-06-25 22:34:55 +0200 | [diff] [blame^] | 34 | private static final String NOTICE_LEVEL = "NOTICE"; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 35 | private static final String WARNING_LEVEL = "WARNING"; |
| 36 | private static final String ERROR_LEVEL = "ERROR"; |
Matthias Andreas Benkard | fd9f00c | 2024-06-25 22:34:55 +0200 | [diff] [blame^] | 37 | 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 Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 40 | |
| 41 | private static final String ERROR_EVENT_TYPE = |
| 42 | "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent"; |
| 43 | |
Matthias Andreas Benkard | 692f48d | 2021-08-31 21:06:50 +0200 | [diff] [blame] | 44 | private final List<StructuredParameterProvider> parameterProviders; |
| 45 | private final List<LabelProvider> labelProviders; |
Matthias Andreas Benkard | d2280ad | 2024-06-23 17:08:58 +0200 | [diff] [blame] | 46 | private final JsonProvider json; |
Matthias Andreas Benkard | 82d7e44 | 2021-08-29 08:34:11 +0200 | [diff] [blame] | 47 | |
Matthias Andreas Benkard | 42da9f1 | 2021-09-02 18:47:28 +0200 | [diff] [blame] | 48 | /** |
Matthias Andreas Benkard | 348f205 | 2022-01-15 16:13:01 +0100 | [diff] [blame] | 49 | * Constructs a {@link Formatter} with custom configuration. |
Matthias Andreas Benkard | 42da9f1 | 2021-09-02 18:47:28 +0200 | [diff] [blame] | 50 | * |
Matthias Andreas Benkard | 93ecfd1 | 2022-01-15 14:03:41 +0100 | [diff] [blame] | 51 | * <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 Benkard | 42da9f1 | 2021-09-02 18:47:28 +0200 | [diff] [blame] | 54 | * @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 Benkard | 692f48d | 2021-08-31 21:06:50 +0200 | [diff] [blame] | 57 | public Formatter( |
| 58 | Collection<StructuredParameterProvider> parameterProviders, |
| 59 | Collection<LabelProvider> labelProviders) { |
Matthias Andreas Benkard | 82d7e44 | 2021-08-29 08:34:11 +0200 | [diff] [blame] | 60 | this.parameterProviders = List.copyOf(parameterProviders); |
Matthias Andreas Benkard | 692f48d | 2021-08-31 21:06:50 +0200 | [diff] [blame] | 61 | this.labelProviders = List.copyOf(labelProviders); |
Matthias Andreas Benkard | d2280ad | 2024-06-23 17:08:58 +0200 | [diff] [blame] | 62 | this.json = JsonProvider.provider(); |
Matthias Andreas Benkard | 82d7e44 | 2021-08-29 08:34:11 +0200 | [diff] [blame] | 63 | } |
| 64 | |
Matthias Andreas Benkard | 93ecfd1 | 2022-01-15 14:03:41 +0100 | [diff] [blame] | 65 | /** |
| 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 Benkard | 348f205 | 2022-01-15 16:13:01 +0100 | [diff] [blame] | 75 | * @return a new formatter. |
Matthias Andreas Benkard | 93ecfd1 | 2022-01-15 14:03:41 +0100 | [diff] [blame] | 76 | */ |
| 77 | public static Formatter load( |
| 78 | Collection<StructuredParameterProvider> parameterProviders, |
| 79 | Collection<LabelProvider> labelProviders) { |
| 80 | parameterProviders = new ArrayList<>(parameterProviders); |
Matthias Andreas Benkard | 348f205 | 2022-01-15 16:13:01 +0100 | [diff] [blame] | 81 | parameterProviders.addAll(loadStructuredParameterProviders()); |
Matthias Andreas Benkard | 93ecfd1 | 2022-01-15 14:03:41 +0100 | [diff] [blame] | 82 | |
| 83 | labelProviders = new ArrayList<>(labelProviders); |
Matthias Andreas Benkard | 348f205 | 2022-01-15 16:13:01 +0100 | [diff] [blame] | 84 | labelProviders.addAll(loadLabelProviders()); |
Matthias Andreas Benkard | 93ecfd1 | 2022-01-15 14:03:41 +0100 | [diff] [blame] | 85 | |
| 86 | return new Formatter(parameterProviders, labelProviders); |
| 87 | } |
| 88 | |
Matthias Andreas Benkard | 348f205 | 2022-01-15 16:13:01 +0100 | [diff] [blame] | 89 | 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 Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 102 | @Override |
| 103 | public String format(ExtLogRecord logRecord) { |
| 104 | var message = formatMessageWithStackTrace(logRecord); |
| 105 | |
Matthias Andreas Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 106 | List<StructuredParameter> parameters = new ArrayList<>(); |
| 107 | Map<String, String> labels = new HashMap<>(); |
Matthias Andreas Benkard | 82d7e44 | 2021-08-29 08:34:11 +0200 | [diff] [blame] | 108 | |
Matthias Andreas Benkard | 2cc18b3 | 2022-09-03 10:03:30 +0200 | [diff] [blame] | 109 | var providerContext = new ProviderContext(logRecord); |
| 110 | |
Matthias Andreas Benkard | 82d7e44 | 2021-08-29 08:34:11 +0200 | [diff] [blame] | 111 | for (var parameterProvider : parameterProviders) { |
Matthias Andreas Benkard | 2cc18b3 | 2022-09-03 10:03:30 +0200 | [diff] [blame] | 112 | var parameter = parameterProvider.getParameter(providerContext); |
Matthias Andreas Benkard | 82d7e44 | 2021-08-29 08:34:11 +0200 | [diff] [blame] | 113 | if (parameter != null) { |
| 114 | parameters.add(parameter); |
| 115 | } |
| 116 | } |
| 117 | |
Matthias Andreas Benkard | 692f48d | 2021-08-31 21:06:50 +0200 | [diff] [blame] | 118 | for (var labelProvider : labelProviders) { |
Matthias Andreas Benkard | 2cc18b3 | 2022-09-03 10:03:30 +0200 | [diff] [blame] | 119 | var providedLabels = labelProvider.getLabels(providerContext); |
Matthias Andreas Benkard | 692f48d | 2021-08-31 21:06:50 +0200 | [diff] [blame] | 120 | if (providedLabels != null) { |
| 121 | for (var label : providedLabels) { |
| 122 | labels.put(label.key(), label.value()); |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 127 | if (logRecord.getParameters() != null) { |
| 128 | for (var parameter : logRecord.getParameters()) { |
Matthias Andreas Benkard | 121a631 | 2021-05-12 05:41:25 +0200 | [diff] [blame] | 129 | if (parameter instanceof StructuredParameter) { |
| 130 | parameters.add((StructuredParameter) parameter); |
| 131 | } else if (parameter instanceof Label) { |
| 132 | var label = (Label) parameter; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 133 | labels.put(label.key(), label.value()); |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | var mdc = logRecord.getMdcCopy(); |
| 139 | var ndc = logRecord.getNdc(); |
| 140 | |
Matthias Andreas Benkard | 3af29c3 | 2024-06-25 22:31:04 +0200 | [diff] [blame] | 141 | var sourceLocation = sourceLocationOf(logRecord); |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 142 | |
| 143 | var entry = |
Matthias Andreas Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 144 | new LogEntry( |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 145 | message, |
| 146 | severityOf(logRecord.getLevel()), |
Matthias Andreas Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 147 | new LogEntry.Timestamp(logRecord.getInstant()), |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 148 | null, |
| 149 | null, |
| 150 | sourceLocation, |
Matthias Andreas Benkard | b8fbc37 | 2021-05-11 06:50:45 +0200 | [diff] [blame] | 151 | labels, |
| 152 | parameters, |
| 153 | mdc, |
| 154 | ndc, |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 155 | logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null); |
| 156 | |
Matthias Andreas Benkard | d2280ad | 2024-06-23 17:08:58 +0200 | [diff] [blame] | 157 | return entry.json(json).build().toString() + "\n"; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 158 | } |
| 159 | |
Matthias Andreas Benkard | 3af29c3 | 2024-06-25 22:31:04 +0200 | [diff] [blame] | 160 | 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 Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 176 | /** |
| 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 Benkard | 4bae5f1 | 2021-05-03 19:16:48 +0200 | [diff] [blame] | 194 | /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */ |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 195 | 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 Benkard | fd9f00c | 2024-06-25 22:34:55 +0200 | [diff] [blame^] | 200 | } else if (level.intValue() < 850) { |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 201 | return INFO_LEVEL; |
Matthias Andreas Benkard | fd9f00c | 2024-06-25 22:34:55 +0200 | [diff] [blame^] | 202 | } else if (level.intValue() < 900) { |
| 203 | return NOTICE_LEVEL; |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 204 | } else if (level.intValue() < 1000) { |
| 205 | return WARNING_LEVEL; |
Matthias Andreas Benkard | fd9f00c | 2024-06-25 22:34:55 +0200 | [diff] [blame^] | 206 | } else if (level.intValue() < 1100) { |
Matthias Andreas Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 207 | return ERROR_LEVEL; |
Matthias Andreas Benkard | fd9f00c | 2024-06-25 22:34:55 +0200 | [diff] [blame^] | 208 | } 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 Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 214 | } |
| 215 | } |
Matthias Andreas Benkard | 2cc18b3 | 2022-09-03 10:03:30 +0200 | [diff] [blame] | 216 | |
| 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 Benkard | c8144a9 | 2021-05-03 08:04:53 +0200 | [diff] [blame] | 249 | } |