Split off -core module.

Change-Id: I64d3c195db94e92da44c7e4971f5e85991ac30c8
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;