summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2022-01-15 10:39:30 +0100
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2022-01-15 10:39:30 +0100
commit20210245e619658c2459c77223d9abe3c643a882 (patch)
tree7b5dbdab7bdd5fb3422afbbe616feee6084127c9 /core
parent85d5b06d724b36232349e1b2cefe100f2f9ac598 (diff)
Split off -core module.
Change-Id: I64d3c195db94e92da44c7e4971f5e85991ac30c8
Diffstat (limited to 'core')
-rw-r--r--core/pom.xml48
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java141
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java181
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java93
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java49
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java157
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java34
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java55
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java191
9 files changed, 949 insertions, 0 deletions
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 0000000..86656ae
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
+ <artifactId>quarkus-googlecloud-jsonlogging-parent</artifactId>
+ <version>3.1.4-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId>
+ <name>Quarkus Google Cloud JSON Logging Extension - JBoss Logging Core</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.logmanager</groupId>
+ <artifactId>jboss-logmanager-embedded</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.smallrye.common</groupId>
+ <artifactId>smallrye-common-constraint</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish</groupId>
+ <artifactId>jakarta.json</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+
+ <plugin>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+ <Automatic-Module-Name>eu.mulk.quarkus.googlecloud.jsonlogging.core</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+
+ </plugins>
+ </build>
+
+</project>
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.
+ *
+ * <p>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<StructuredParameterProvider> parameterProviders;
+ private final List<LabelProvider> 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<StructuredParameterProvider> parameterProviders,
+ Collection<LabelProvider> labelProviders) {
+ this.parameterProviders = List.copyOf(parameterProviders);
+ this.labelProviders = List.copyOf(labelProviders);
+ }
+
+ @Override
+ public String format(ExtLogRecord logRecord) {
+ var message = formatMessageWithStackTrace(logRecord);
+
+ List<StructuredParameter> parameters = new ArrayList<>();
+ Map<String, String> 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}.
+ *
+ * <p>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}.
+ *
+ * <p><strong>Example:</strong>
+ *
+ * <pre>{@code
+ * logger.infof("Application starting.", StructuredParameter.of("version", "1.0"));
+ * }</pre>
+ *
+ * Result:
+ *
+ * <pre>{@code
+ * {
+ * "jsonPayload": {
+ * "message": "Application starting.",
+ * "version": "1.0"
+ * }
+ * }
+ * }</pre>
+ *
+ * @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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>Instances of {@link Label} can be passed as log parameters to the {@code *f} family of logging
+ * functions on {@link org.jboss.logging.Logger}.
+ *
+ * <p><strong>Example:</strong>
+ *
+ * <pre>{@code
+ * logger.logf("Request rejected: unauthorized.", Label.of("requestId", "123"));
+ * }</pre>
+ *
+ * Result:
+ *
+ * <pre>{@code
+ * {
+ * "textPayload": "Request rejected: unauthorized.",
+ * "labels": {
+ * "requestId": "123"
+ * }
+ * }
+ * }</pre>
+ *
+ * @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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>Instances of this interface that are registered with the {@link Formatter} are applied to each
+ * log entry that is logged.
+ *
+ * <p>If you are using the Quarkus extension, any CDI beans registered under this interface are
+ * registered automatically.
+ *
+ * <p><strong>Example:</strong>
+ *
+ * <pre>{@code
+ * @Singleton
+ * @Unremovable
+ * public final class RequestIdLabelProvider implements LabelProvider {
+ *
+ * @Override
+ * public Collection<Label> getLabels() {
+ * return List.of(Label.of("requestId", RequestContext.current().getRequestId()));
+ * }
+ * }
+ * }</pre>
+ *
+ * Result:
+ *
+ * <pre>{@code
+ * {
+ * "textPayload": "Request rejected: unauthorized.",
+ * "labels": {
+ * "requestId": "123"
+ * }
+ * }
+ * }</pre>
+ *
+ * @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<Label> getLabels();
+}
diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java
new file mode 100644
index 0000000..d108c81
--- /dev/null
+++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java
@@ -0,0 +1,157 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+import io.smallrye.common.constraint.Nullable;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+/**
+ * A JSON log entry compatible with Google Cloud Logging.
+ *
+ * <p>Roughly (but not quite) corresponds to Google Cloud Logging's <a
+ * href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">LogEntry</a>
+ * structure.
+ *
+ * <p>A few of the fields are <a href="https://cloud.google.com/logging/docs/structured-logging">
+ * treated specially</a> by the fluentd instance running in Google Kubernetes Engine. All other
+ * fields end up in the jsonPayload field on the Google Cloud Logging side.
+ */
+final class LogEntry {
+
+ private final String message;
+ private final String severity;
+ private final Timestamp timestamp;
+ @Nullable private final String trace;
+ @Nullable private final String spanId;
+ private final SourceLocation sourceLocation;
+ private final Map<String, String> labels;
+ private final List<StructuredParameter> parameters;
+ private final Map<String, String> mappedDiagnosticContext;
+ @Nullable private final String nestedDiagnosticContext;
+ @Nullable private final String type;
+
+ LogEntry(
+ String message,
+ String severity,
+ Timestamp timestamp,
+ @Nullable String trace,
+ @Nullable String spanId,
+ SourceLocation sourceLocation,
+ Map<String, String> labels,
+ List<StructuredParameter> parameters,
+ Map<String, String> mappedDiagnosticContext,
+ @Nullable String nestedDiagnosticContext,
+ @Nullable String type) {
+ this.message = message;
+ this.severity = severity;
+ this.timestamp = timestamp;
+ this.trace = trace;
+ this.spanId = spanId;
+ this.sourceLocation = sourceLocation;
+ this.labels = labels;
+ this.parameters = parameters;
+ this.mappedDiagnosticContext = mappedDiagnosticContext;
+ this.nestedDiagnosticContext = nestedDiagnosticContext;
+ this.type = type;
+ }
+
+ static final class SourceLocation {
+
+ @Nullable private final String file;
+ @Nullable private final String line;
+ @Nullable private final String function;
+
+ SourceLocation(@Nullable String file, @Nullable String line, @Nullable String function) {
+ this.file = file;
+ this.line = line;
+ this.function = function;
+ }
+
+ JsonObject json() {
+ var b = Json.createObjectBuilder();
+
+ if (file != null) {
+ b.add("file", file);
+ }
+
+ if (line != null) {
+ b.add("line", line);
+ }
+
+ if (function != null) {
+ b.add("function", function);
+ }
+
+ return b.build();
+ }
+ }
+
+ static final class Timestamp {
+
+ private final long seconds;
+ private final int nanos;
+
+ Timestamp(long seconds, int nanos) {
+ this.seconds = seconds;
+ this.nanos = nanos;
+ }
+
+ Timestamp(Instant t) {
+ this(t.getEpochSecond(), t.getNano());
+ }
+
+ JsonObject json() {
+ return Json.createObjectBuilder().add("seconds", seconds).add("nanos", nanos).build();
+ }
+ }
+
+ JsonObjectBuilder json() {
+ var b = Json.createObjectBuilder();
+
+ if (trace != null) {
+ b.add("logging.googleapis.com/trace", trace);
+ }
+
+ if (spanId != null) {
+ b.add("logging.googleapis.com/spanId", spanId);
+ }
+
+ if (nestedDiagnosticContext != null && !nestedDiagnosticContext.isEmpty()) {
+ b.add("nestedDiagnosticContext", nestedDiagnosticContext);
+ }
+
+ if (!labels.isEmpty()) {
+ b.add("logging.googleapis.com/labels", jsonOfStringMap(labels));
+ }
+
+ if (type != null) {
+ b.add("@type", type);
+ }
+
+ return b.add("message", message)
+ .add("severity", severity)
+ .add("timestamp", timestamp.json())
+ .add("logging.googleapis.com/sourceLocation", sourceLocation.json())
+ .addAll(jsonOfStringMap(mappedDiagnosticContext))
+ .addAll(jsonOfParameterMap(parameters));
+ }
+
+ private static JsonObjectBuilder jsonOfStringMap(Map<String, String> stringMap) {
+ return stringMap.entrySet().stream()
+ .reduce(
+ Json.createObjectBuilder(),
+ (acc, x) -> acc.add(x.getKey(), x.getValue()),
+ JsonObjectBuilder::addAll);
+ }
+
+ private static JsonObjectBuilder jsonOfParameterMap(List<StructuredParameter> parameters) {
+ return parameters.stream()
+ .reduce(
+ Json.createObjectBuilder(),
+ (acc, p) -> acc.addAll(p.json()),
+ JsonObjectBuilder::addAll);
+ }
+}
diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java
new file mode 100644
index 0000000..c718080
--- /dev/null
+++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java
@@ -0,0 +1,34 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+import javax.json.JsonObjectBuilder;
+
+/**
+ * A structured parameter usable as logging payload.
+ *
+ * <p>Any instance of {@link StructuredParameter} can be passed as a log parameter to the {@code *f}
+ * family of logging functions on {@link org.jboss.logging.Logger}.
+ *
+ * <p>Example:
+ *
+ * <pre>{@code
+ * StructuredParameter p1 = ...;
+ * StructuredParameter p2 = ...;
+ *
+ * logger.logf("Something interesting happened.", p1, p2);
+ * }</pre>
+ *
+ * @see KeyValueParameter
+ * @see Label
+ */
+public interface StructuredParameter {
+
+ /**
+ * The JSON to be embedded in the payload of the log entry.
+ *
+ * <p>May contain multiple keys and values as well as nested objects. Each top-level entry of the
+ * returned object is embedded as a top-level entry in the payload of the log entry.
+ *
+ * @return A {@link JsonObjectBuilder} holding a set of key–value pairs.
+ */
+ JsonObjectBuilder json();
+}
diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java
new file mode 100644
index 0000000..decf937
--- /dev/null
+++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java
@@ -0,0 +1,55 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+/**
+ * A user-supplied provider for {@link StructuredParameter}s.
+ *
+ * <p>Instances of this interface that are registered with the {@link Formatter} are applied to each
+ * log entry that is logged.
+ *
+ * <p>If you are using the Quarkus extension, any CDI beans registered under this interface are
+ * registered automatically.
+ *
+ * <p><strong>Example:</strong>
+ *
+ * <pre>{@code
+ * @Singleton
+ * @Unremovable
+ * public final class TraceLogParameterProvider implements StructuredParameterProvider {
+ *
+ * @Override
+ * public StructuredParameter getParameter() {
+ * var b = Json.createObjectBuilder();
+ * b.add("traceId", Span.current().getSpanContext().getTraceId());
+ * b.add("spanId", Span.current().getSpanContext().getSpanId());
+ * return () -> b;
+ * }
+ * }
+ * }</pre>
+ *
+ * Result:
+ *
+ * <pre>{@code
+ * {
+ * "jsonPayload": {
+ * "message": "Request rejected: unauthorized.",
+ * "traceId": "39f9a49a9567a8bd7087b708f8932550",
+ * "spanId": "c7431b14630b633d"
+ * }
+ * }
+ * }</pre>
+ *
+ * @see LabelProvider
+ */
+public interface StructuredParameterProvider {
+
+ /**
+ * Provides a {@link StructuredParameter} to add to each log entry that is logged.
+ *
+ * <p>It is often useful to return a custom {@link StructuredParameter} rather than a {@link
+ * KeyValueParameter} from this method. This way multiple key–value pairs can be generated by a
+ * single invocation.
+ *
+ * @return a {@link StructuredParameter} to add to each log entry that is logged.
+ */
+ StructuredParameter getParameter();
+}
diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java
new file mode 100644
index 0000000..2f4c7ce
--- /dev/null
+++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java
@@ -0,0 +1,191 @@
+/**
+ * Provides structured logging to standard output according to the Google Cloud Logging
+ * specification.
+ *
+ * <ul>
+ * <li><a href="#sect-summary">Summary</a>
+ * <li><a href="#sect-activation">Activation</a>
+ * <li><a href="#sect-usage">Usage</a>
+ * </ul>
+ *
+ * <h2 id="sect-summary">Summary</h2>
+ *
+ * <p>This package contains a log formatter for JBoss Logging in the form of a Quarkus plugin that
+ * implements the <a href="https://cloud.google.com/logging/docs/structured-logging">Google Cloud
+ * Logging JSON format</a> on standard output.
+ *
+ * <p>It is possible to log unstructured text, structured data, or a mixture of both depending on
+ * the situation.
+ *
+ * <h2 id="sect-activation">Installation</h2>
+ *
+ * <ul>
+ * <li><a href="#sect-installation-maven">Installation with Maven</a>
+ * <li><a href="#sect-installation-gradle">Installation with Gradle</a>
+ * </ul>
+ *
+ * <p>Add the runtime POM to your dependency list. As long as the JAR is on the classpath at both
+ * build time and runtime, the log formatter automatically registers itself on startup.
+ *
+ * <h3 id="sect-installation-maven">Installation with Maven</h3>
+ *
+ * <pre>{@code
+ * <project>
+ * ...
+ *
+ * <dependencies>
+ * ...
+ *
+ * <dependency>
+ * <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
+ * <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId>
+ * <version>4.0.0</version>
+ * </dependency>
+ *
+ * ...
+ * </dependencies>
+ *
+ * ...
+ * </project>
+ * }</pre>
+ *
+ * <h3 id="sect-installation-gradle">Installation with Gradle</h3>
+ *
+ * <pre>{@code
+ * dependencies {
+ * ...
+ *
+ * implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging-core:4.0.0")
+ *
+ * ...
+ * }
+ * }</pre>
+ *
+ * <h2 id="sect-usage">Usage</h2>
+ *
+ * <ul>
+ * <li><a href="#sect-usage-parameter">Using Label and StructuredParameter</a>
+ * <li><a href="#sect-usage-provider">Using LabelProvider and StructuredParameterProvider</a>
+ * <li><a href="#sect-usage-mdc">Using the Mapped Diagnostic Context</a>
+ * </ul>
+ *
+ * <p>Logging unstructured data requires no code changes. All logs are automatically converted to
+ * Google-Cloud-Logging-compatible JSON.
+ *
+ * <p>Structured data can be logged in one of 3 different ways: by passing {@link
+ * eu.mulk.quarkus.googlecloud.jsonlogging.Label}s and {@link
+ * eu.mulk.quarkus.googlecloud.jsonlogging.StructuredParameter}s as parameters to individual log
+ * entries, by supplying {@link eu.mulk.quarkus.googlecloud.jsonlogging.LabelProvider}s and {@link
+ * eu.mulk.quarkus.googlecloud.jsonlogging.StructuredParameterProvider}s, or by using the Mapped
+ * Diagnostic Context.
+ *
+ * <h3 id="sect-usage-parameter">Using Label and StructuredParameter</h3>
+ *
+ * <p>Instances of {@link eu.mulk.quarkus.googlecloud.jsonlogging.Label} and {@link
+ * eu.mulk.quarkus.googlecloud.jsonlogging.StructuredParameter} can be passed as log parameters to
+ * the {@code *f} family of logging functions on JBoss Logging's {@link org.jboss.logging.Logger}.
+ *
+ * <p>Simple key–value pairs are represented by {@link
+ * eu.mulk.quarkus.googlecloud.jsonlogging.KeyValueParameter}.
+ *
+ * <p><strong>Example:</strong>
+ *
+ * <pre>{@code
+ * logger.logf(
+ * "Request rejected: unauthorized.",
+ * Label.of("requestId", "123"),
+ * KeyValueParameter.of("resource", "/users/mulk"),
+ * KeyValueParameter.of("method", "PATCH"),
+ * KeyValueParameter.of("reason", "invalid token"));
+ * }</pre>
+ *
+ * Result:
+ *
+ * <pre>{@code
+ * {
+ * "jsonPayload": {
+ * "message": "Request rejected: unauthorized.",
+ * "resource": "/users/mulk",
+ * "method": "PATCH",
+ * "reason": "invalid token"
+ * },
+ * "labels": {
+ * "requestId": "123"
+ * }
+ * }
+ * }</pre>
+ *
+ * <h3 id="sect-usage-provider">Using LabelProvider and StructuredParameterProvider</h3>
+ *
+ * <p>If you pass {@link eu.mulk.quarkus.googlecloud.jsonlogging.LabelProvider}s and {@link
+ * eu.mulk.quarkus.googlecloud.jsonlogging.StructuredParameterProvider}s to {@link
+ * eu.mulk.quarkus.googlecloud.jsonlogging.Formatter}, then they are consulted to provide labels and
+ * parameters for each message that is logged. This can be used to provide contextual information
+ * such as tracing and request IDs stored in thread-local storage.
+ *
+ * <p>If you are using the Quarkus extension, CDI beans that implement these interfaces are
+ * automatically detected at build time and passed to the formatter on startup.
+ *
+ * <p><strong>Example:</strong>
+ *
+ * <pre>{@code
+ * @Singleton
+ * @Unremovable
+ * public final class TraceLogParameterProvider implements StructuredParameterProvider, LabelProvider {
+ *
+ * @Override
+ * public StructuredParameter getParameter() {
+ * var b = Json.createObjectBuilder();
+ * b.add("traceId", Span.current().getSpanContext().getTraceId());
+ * b.add("spanId", Span.current().getSpanContext().getSpanId());
+ * return () -> b;
+ * }
+ *
+ * @Override
+ * public Collection<Label> getLabels() {
+ * return List.of(Label.of("requestId", "123"));
+ * }
+ * }
+ * }</pre>
+ *
+ * Result:
+ *
+ * <pre>{@code
+ * {
+ * "jsonPayload": {
+ * "message": "Request rejected: unauthorized.",
+ * "traceId": "39f9a49a9567a8bd7087b708f8932550",
+ * "spanId": "c7431b14630b633d"
+ * },
+ * "labels": {
+ * "requestId": "123"
+ * }
+ * }
+ * }</pre>
+ *
+ * <h3 id="sect-usage-mdc">Using the Mapped Diagnostic Context</h3>
+ *
+ * <p>Any key–value pairs in JBoss Logging's thread-local {@link org.jboss.logging.MDC} are added to
+ * the resulting JSON.
+ *
+ * <p><strong>Example:</strong>
+ *
+ * <pre>{@code
+ * MDC.put("resource", "/users/mulk");
+ * MDC.put("method", "PATCH");
+ * logger.logf("Request rejected: unauthorized.");
+ * }</pre>
+ *
+ * Result:
+ *
+ * <pre>{@code
+ * {
+ * "jsonPayload": {
+ * "message": "Request rejected: unauthorized.",
+ * "resource": "/users/mulk",
+ * "method": "PATCH"
+ * }
+ * }
+ * }</pre>
+ */
+package eu.mulk.quarkus.googlecloud.jsonlogging;