From 20210245e619658c2459c77223d9abe3c643a882 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sat, 15 Jan 2022 10:39:30 +0100 Subject: Split off -core module. Change-Id: I64d3c195db94e92da44c7e4971f5e85991ac30c8 --- .../quarkus/googlecloud/jsonlogging/Formatter.java | 141 +++++++++++++++ .../googlecloud/jsonlogging/KeyValueParameter.java | 181 +++++++++++++++++++ .../quarkus/googlecloud/jsonlogging/Label.java | 93 ++++++++++ .../googlecloud/jsonlogging/LabelProvider.java | 49 ++++++ .../quarkus/googlecloud/jsonlogging/LogEntry.java | 157 +++++++++++++++++ .../jsonlogging/StructuredParameter.java | 34 ++++ .../jsonlogging/StructuredParameterProvider.java | 55 ++++++ .../googlecloud/jsonlogging/package-info.java | 191 +++++++++++++++++++++ 8 files changed, 901 insertions(+) create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java create mode 100644 core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java (limited to 'core/src') diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java new file mode 100644 index 0000000..066f709 --- /dev/null +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java @@ -0,0 +1,141 @@ +package eu.mulk.quarkus.googlecloud.jsonlogging; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import org.jboss.logmanager.ExtFormatter; +import org.jboss.logmanager.ExtLogRecord; + +/** + * Formats log records as JSON for consumption by Google Cloud Logging. + * + *

Meant to be used in containers running on Google Kubernetes Engine (GKE). + * + * @see LogEntry + */ +public class Formatter extends ExtFormatter { + + private static final String TRACE_LEVEL = "TRACE"; + private static final String DEBUG_LEVEL = "DEBUG"; + private static final String INFO_LEVEL = "INFO"; + private static final String WARNING_LEVEL = "WARNING"; + private static final String ERROR_LEVEL = "ERROR"; + + private static final String ERROR_EVENT_TYPE = + "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent"; + + private final List parameterProviders; + private final List labelProviders; + + /** + * Constructs a {@link Formatter}. + * + * @param parameterProviders the {@link StructuredParameterProvider}s to apply to each log entry. + * @param labelProviders the {@link LabelProvider}s to apply to each log entry. + */ + public Formatter( + Collection parameterProviders, + Collection labelProviders) { + this.parameterProviders = List.copyOf(parameterProviders); + this.labelProviders = List.copyOf(labelProviders); + } + + @Override + public String format(ExtLogRecord logRecord) { + var message = formatMessageWithStackTrace(logRecord); + + List parameters = new ArrayList<>(); + Map labels = new HashMap<>(); + + for (var parameterProvider : parameterProviders) { + var parameter = parameterProvider.getParameter(); + if (parameter != null) { + parameters.add(parameter); + } + } + + for (var labelProvider : labelProviders) { + var providedLabels = labelProvider.getLabels(); + if (providedLabels != null) { + for (var label : providedLabels) { + labels.put(label.key(), label.value()); + } + } + } + + if (logRecord.getParameters() != null) { + for (var parameter : logRecord.getParameters()) { + if (parameter instanceof StructuredParameter) { + parameters.add((StructuredParameter) parameter); + } else if (parameter instanceof Label) { + var label = (Label) parameter; + labels.put(label.key(), label.value()); + } + } + } + + var mdc = logRecord.getMdcCopy(); + var ndc = logRecord.getNdc(); + + var sourceLocation = + new LogEntry.SourceLocation( + logRecord.getSourceFileName(), + String.valueOf(logRecord.getSourceLineNumber()), + String.format( + "%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName())); + + var entry = + new LogEntry( + message, + severityOf(logRecord.getLevel()), + new LogEntry.Timestamp(logRecord.getInstant()), + null, + null, + sourceLocation, + labels, + parameters, + mdc, + ndc, + logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null); + + return entry.json().build().toString() + "\n"; + } + + /** + * Formats the log message corresponding to {@code logRecord} including a stack trace of the + * {@link ExtLogRecord#getThrown()} exception if any. + */ + private String formatMessageWithStackTrace(ExtLogRecord logRecord) { + var messageStringWriter = new StringWriter(); + var messagePrintWriter = new PrintWriter(messageStringWriter); + messagePrintWriter.append(this.formatMessage(logRecord)); + + if (logRecord.getThrown() != null) { + messagePrintWriter.println(); + logRecord.getThrown().printStackTrace(messagePrintWriter); + } + + messagePrintWriter.close(); + return messageStringWriter.toString(); + } + + /** Computes the Google Cloud Logging severity corresponding to a given {@link Level}. */ + private static String severityOf(Level level) { + if (level.intValue() < 500) { + return TRACE_LEVEL; + } else if (level.intValue() < 700) { + return DEBUG_LEVEL; + } else if (level.intValue() < 900) { + return INFO_LEVEL; + } else if (level.intValue() < 1000) { + return WARNING_LEVEL; + } else { + return ERROR_LEVEL; + } + } +} diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java new file mode 100644 index 0000000..a5924b4 --- /dev/null +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java @@ -0,0 +1,181 @@ +package eu.mulk.quarkus.googlecloud.jsonlogging; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Objects; +import javax.json.Json; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; + +/** + * A simple single key–value pair forming a {@link StructuredParameter}. + * + *

This class is suitable for the common case of logging a key–value pair as parameter to the + * {@code *f} family of logging functions on {@link org.jboss.logging.Logger}. For advanced use + * cases, provide your own implementation of {@link StructuredParameter}. + * + *

Example: + * + *

{@code
+ * logger.infof("Application starting.", StructuredParameter.of("version", "1.0"));
+ * }
+ * + * Result: + * + *
{@code
+ * {
+ *   "jsonPayload": {
+ *     "message": "Application starting.",
+ *     "version": "1.0"
+ *   }
+ * }
+ * }
+ * + * @see Label + * @see StructuredParameter + */ +public final class KeyValueParameter implements StructuredParameter { + + private final String key; + private final JsonValue value; + + private KeyValueParameter(String key, JsonValue value) { + this.key = key; + this.value = value; + } + + /** + * Creates a {@link KeyValueParameter} from a {@link String} value. + * + *

The resulting JSON value is of type {@code string}. + * + * @param key the key part of the key–value pair. + * @param value the value part of the key–value pair. + * @return the newly constructed parameter, ready to be passed to a logging function. + */ + public static KeyValueParameter of(String key, String value) { + return new KeyValueParameter(key, Json.createValue(value)); + } + + /** + * Creates a {@link KeyValueParameter} from an {@code int} value. + * + *

The resulting JSON value is of type {@code number}. + * + * @param key the key part of the key–value pair. + * @param value the value part of the key–value pair. + * @return the newly constructed parameter, ready to be passed to a logging function. + */ + public static KeyValueParameter of(String key, int value) { + return new KeyValueParameter(key, Json.createValue(value)); + } + + /** + * Creates a {@link KeyValueParameter} from a {@code long} value. + * + *

The resulting JSON value is of type {@code number}. + * + * @param key the key part of the key–value pair. + * @param value the value part of the key–value pair. + * @return the newly constructed parameter, ready to be passed to a logging function. + */ + public static KeyValueParameter of(String key, long value) { + return new KeyValueParameter(key, Json.createValue(value)); + } + + /** + * Creates a {@link KeyValueParameter} from a {@code double} value. + * + *

The resulting JSON value is of type {@code number}. + * + * @param key the key part of the key–value pair. + * @param value the value part of the key–value pair. + * @return the newly constructed parameter, ready to be passed to a logging function. + */ + public static KeyValueParameter of(String key, double value) { + return new KeyValueParameter(key, Json.createValue(value)); + } + + /** + * Creates a {@link KeyValueParameter} from a {@link BigDecimal} value. + * + *

The resulting JSON value is of type {@code number}. + * + * @param key the key part of the key–value pair. + * @param value the value part of the key–value pair. + * @return the newly constructed parameter, ready to be passed to a logging function. + */ + public static KeyValueParameter of(String key, BigDecimal value) { + return new KeyValueParameter(key, Json.createValue(value)); + } + + /** + * Creates a {@link KeyValueParameter} from a {@link BigInteger} value. + * + *

The resulting JSON value is of type {@code number}. + * + * @param key the key part of the key–value pair. + * @param value the value part of the key–value pair. + * @return the newly constructed parameter, ready to be passed to a logging function. + */ + public static KeyValueParameter of(String key, BigInteger value) { + return new KeyValueParameter(key, Json.createValue(value)); + } + + /** + * Creates a {@link KeyValueParameter} from a {@code boolean} value. + * + *

The resulting JSON value is of type {@code boolean}. + * + * @param key the key part of the key–value pair. + * @param value the value part of the key–value pair. + * @return the newly constructed parameter, ready to be passed to a logging function. + */ + public static KeyValueParameter of(String key, boolean value) { + return new KeyValueParameter(key, value ? JsonValue.TRUE : JsonValue.FALSE); + } + + @Override + public JsonObjectBuilder json() { + return Json.createObjectBuilder().add(key, value); + } + + /** + * The key part of the key–value pair. + * + * @return the key part of the key–value pair. + */ + public String key() { + return key; + } + + /** + * The value part of the key–value pair. + * + *

Can be of any non-composite JSON type (i.e. {@code string}, {@code number}, or {@code + * boolean}). + * + * @return the value pairt of the key–value pair. + */ + public JsonValue value() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (KeyValueParameter) obj; + return Objects.equals(this.key, that.key) && Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return "KeyValueParameter[" + "key=" + key + ", " + "value=" + value + ']'; + } +} diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java new file mode 100644 index 0000000..33664dd --- /dev/null +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java @@ -0,0 +1,93 @@ +package eu.mulk.quarkus.googlecloud.jsonlogging; + +import java.util.Objects; + +/** + * A label usable to tag a log message. + * + *

Instances of {@link Label} can be passed as log parameters to the {@code *f} family of logging + * functions on {@link org.jboss.logging.Logger}. + * + *

Example: + * + *

{@code
+ * logger.logf("Request rejected: unauthorized.", Label.of("requestId", "123"));
+ * }
+ * + * Result: + * + *
{@code
+ * {
+ *   "textPayload": "Request rejected: unauthorized.",
+ *   "labels": {
+ *     "requestId": "123"
+ *   }
+ * }
+ * }
+ * + * @see KeyValueParameter + * @see StructuredParameter + */ +public final class Label { + + private final String key; + private final String value; + + private Label(String key, String value) { + this.key = key; + this.value = value; + } + + /** + * Constructs a {@link Label} from a key (i.e. name) and a value. + * + *

It is often useful for the key to be a {@link String} constant that is shared by multiple + * parts of the program. + * + * @param key the key (name) of the label. + * @param value the value of the label. + * @return the newly constructed {@link Label}, ready to be passed to a logging function. + */ + public static Label of(String key, String value) { + return new Label(key, value); + } + + /** + * The name of the label. + * + *

It is often useful for this to be a {@link String} constant that is shared by multiple parts + * of the program. + * + * @return the name of the label. + */ + public String key() { + return key; + } + + /** + * The value of the label. + * + * @return the value of the label. + */ + public String value() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (Label) obj; + return Objects.equals(this.key, that.key) && Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + @Override + public String toString() { + return "Label[" + "key=" + key + ", " + "value=" + value + ']'; + } +} diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java new file mode 100644 index 0000000..8cf87db --- /dev/null +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java @@ -0,0 +1,49 @@ +package eu.mulk.quarkus.googlecloud.jsonlogging; + +import java.util.Collection; + +/** + * A user-supplied provider for {@link Label}s. + * + *

Instances of this interface that are registered with the {@link Formatter} are applied to each + * log entry that is logged. + * + *

If you are using the Quarkus extension, any CDI beans registered under this interface are + * registered automatically. + * + *

Example: + * + *

{@code
+ * @Singleton
+ * @Unremovable
+ * public final class RequestIdLabelProvider implements LabelProvider {
+ *
+ *   @Override
+ *   public Collection
+ * + * Result: + * + *
{@code
+ * {
+ *   "textPayload": "Request rejected: unauthorized.",
+ *   "labels": {
+ *     "requestId": "123"
+ *   }
+ * }
+ * }
+ * + * @see StructuredParameterProvider + */ +public interface LabelProvider { + + /** + * Provides a collection of {@link Label}s to add to each log entry that is logged. + * + * @return a collection of {@link Label}s to add to each log entry that is logged. + */ + Collection