summaryrefslogtreecommitdiff
path: root/runtime
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 /runtime
parent85d5b06d724b36232349e1b2cefe100f2f9ac598 (diff)
Split off -core module.
Change-Id: I64d3c195db94e92da44c7e4971f5e85991ac30c8
Diffstat (limited to 'runtime')
-rw-r--r--runtime/pom.xml9
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java141
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java181
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java93
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java45
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java157
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java34
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java51
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java188
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/runtime/GoogleCloudJsonLoggingRecorder.java (renamed from runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/GoogleCloudJsonLoggingRecorder.java)5
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/runtime/package-info.java71
11 files changed, 82 insertions, 893 deletions
diff --git a/runtime/pom.xml b/runtime/pom.xml
index 01c2c2c..beb4bcc 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -15,6 +15,12 @@
<dependencies>
<dependency>
+ <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
+ <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
@@ -63,11 +69,10 @@
<plugin>
<artifactId>maven-jar-plugin</artifactId>
- <version>${jar-plugin.version}</version>
<configuration>
<archive>
<manifestEntries>
- <Automatic-Module-Name>eu.mulk.quarkus.googlecloud.jsonlogging</Automatic-Module-Name>
+ <Automatic-Module-Name>eu.mulk.quarkus.googlecloud.jsonlogging.runtime</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
diff --git a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java
deleted file mode 100644
index 066f709..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java
+++ /dev/null
@@ -1,141 +0,0 @@
-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/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java
deleted file mode 100644
index a5924b4..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java
+++ /dev/null
@@ -1,181 +0,0 @@
-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/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java
deleted file mode 100644
index 33664dd..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java
+++ /dev/null
@@ -1,93 +0,0 @@
-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/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java
deleted file mode 100644
index dbd035c..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package eu.mulk.quarkus.googlecloud.jsonlogging;
-
-import java.util.Collection;
-
-/**
- * A user-supplied provider for {@link Label}s.
- *
- * <p>Any CDI beans registered under this class are applied to each log entry that is logged.
- *
- * <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/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java
deleted file mode 100644
index d108c81..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java
+++ /dev/null
@@ -1,157 +0,0 @@
-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/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java
deleted file mode 100644
index c718080..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java
+++ /dev/null
@@ -1,34 +0,0 @@
-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/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java
deleted file mode 100644
index cacfea6..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package eu.mulk.quarkus.googlecloud.jsonlogging;
-
-/**
- * A user-supplied provider for {@link StructuredParameter}s.
- *
- * <p>Any CDI beans registered under this class are applied to each log entry that is logged.
- *
- * <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/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java
deleted file mode 100644
index 110ab73..0000000
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * 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">Activation</h2>
- *
- * <ul>
- * <li><a href="#sect-activation-maven">Activation with Maven</a>
- * <li><a href="#sect-activation-gradle">Activation 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-activation-maven">Activation with Maven</h3>
- *
- * <pre>{@code
- * <project>
- * ...
- *
- * <dependencies>
- * ...
- *
- * <dependency>
- * <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
- * <artifactId>quarkus-googlecloud-jsonlogging</artifactId>
- * <version>3.1.0</version>
- * </dependency>
- *
- * ...
- * </dependencies>
- *
- * ...
- * </project>
- * }</pre>
- *
- * <h3 id="sect-activation-gradle">Activation with Gradle</h3>
- *
- * <pre>{@code
- * dependencies {
- * ...
- *
- * implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging:3.1.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>Any CDI beans that implement {@link eu.mulk.quarkus.googlecloud.jsonlogging.LabelProvider}s
- * and {@link eu.mulk.quarkus.googlecloud.jsonlogging.StructuredParameterProvider}s are discovered
- * at build time and 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><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;
diff --git a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/GoogleCloudJsonLoggingRecorder.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/runtime/GoogleCloudJsonLoggingRecorder.java
index 7234a71..661b69f 100644
--- a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/GoogleCloudJsonLoggingRecorder.java
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/runtime/GoogleCloudJsonLoggingRecorder.java
@@ -1,5 +1,8 @@
-package eu.mulk.quarkus.googlecloud.jsonlogging;
+package eu.mulk.quarkus.googlecloud.jsonlogging.runtime;
+import eu.mulk.quarkus.googlecloud.jsonlogging.Formatter;
+import eu.mulk.quarkus.googlecloud.jsonlogging.LabelProvider;
+import eu.mulk.quarkus.googlecloud.jsonlogging.StructuredParameterProvider;
import io.quarkus.arc.Arc;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
diff --git a/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/runtime/package-info.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/runtime/package-info.java
new file mode 100644
index 0000000..2fa7499
--- /dev/null
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/runtime/package-info.java
@@ -0,0 +1,71 @@
+/**
+ * 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">Activation</h2>
+ *
+ * <ul>
+ * <li><a href="#sect-activation-maven">Activation with Maven</a>
+ * <li><a href="#sect-activation-gradle">Activation 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-activation-maven">Activation with Maven</h3>
+ *
+ * <pre>{@code
+ * <project>
+ * ...
+ *
+ * <dependencies>
+ * ...
+ *
+ * <dependency>
+ * <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId>
+ * <artifactId>quarkus-googlecloud-jsonlogging</artifactId>
+ * <version>4.0.0</version>
+ * </dependency>
+ *
+ * ...
+ * </dependencies>
+ *
+ * ...
+ * </project>
+ * }</pre>
+ *
+ * <h3 id="sect-activation-gradle">Activation with Gradle</h3>
+ *
+ * <pre>{@code
+ * dependencies {
+ * ...
+ *
+ * implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging:4.0.0")
+ *
+ * ...
+ * }
+ * }</pre>
+ *
+ * <h2 id="sect-usage">Usage</h2>
+ *
+ * <p>See the documentation of the {@link eu.mulk.quarkus.googlecloud.jsonlogging} package for usage
+ * instructions.
+ *
+ * @see eu.mulk.quarkus.googlecloud.jsonlogging
+ */
+package eu.mulk.quarkus.googlecloud.jsonlogging.runtime;