diff options
Diffstat (limited to 'core/src/main/java')
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; | 
