diff options
author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2022-01-15 10:39:30 +0100 |
---|---|---|
committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2022-01-15 10:39:30 +0100 |
commit | 20210245e619658c2459c77223d9abe3c643a882 (patch) | |
tree | 7b5dbdab7bdd5fb3422afbbe616feee6084127c9 /core/src | |
parent | 85d5b06d724b36232349e1b2cefe100f2f9ac598 (diff) |
Split off -core module.
Change-Id: I64d3c195db94e92da44c7e4971f5e85991ac30c8
Diffstat (limited to 'core/src')
8 files changed, 901 insertions, 0 deletions
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; |