diff options
Diffstat (limited to 'runtime/src/main/java/eu')
5 files changed, 178 insertions, 0 deletions
| diff --git a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLogEntry.java b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLogEntry.java new file mode 100644 index 0000000..3f5a836 --- /dev/null +++ b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLogEntry.java @@ -0,0 +1,41 @@ +package eu.mulk.quarkus.observability.googlecloud.jsonlogging; + +import io.smallrye.common.constraint.Nullable; +import java.time.Instant; +import java.util.Map; +import javax.json.bind.annotation.JsonbProperty; + +/** + * 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. + */ +public record GoogleCloudLogEntry( +    String getMessage, +    String getSeverity, +    Timestamp getTimestamp, +    @Nullable String getTrace, +    @Nullable String getSpanId, +    @Nullable SourceLocation getSourceLocation, +    @Nullable Map<String, String> getLabels, +    @Nullable Map<String, Object> getParameters, +    @Nullable Map<String, String> getMappedDiagnosticContext, +    @Nullable String getNestedDiagnosticContext, +    @Nullable @JsonbProperty("@type") String getType) { + +  static public record SourceLocation( +      @Nullable String getFile, +      @Nullable String getLine, +      @Nullable String getFunction) {} + +  static public record Timestamp( +      long getSeconds, +      int getNanos) { + +    public Timestamp(Instant t) { +      this(t.getEpochSecond(), t.getNano()); +    } +  } +} diff --git a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingFormatter.java b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingFormatter.java new file mode 100644 index 0000000..a44e8b5 --- /dev/null +++ b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingFormatter.java @@ -0,0 +1,115 @@ +package eu.mulk.quarkus.observability.googlecloud.jsonlogging; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.logging.Level; +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbException; +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 GoogleCloudLogEntry + */ +class GoogleCloudLoggingFormatter 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 Jsonb jsonb; + +  GoogleCloudLoggingFormatter(Jsonb jsonb) { +    this.jsonb = jsonb; +  } + +  @Override +  public String format(ExtLogRecord logRecord) { +    var message = formatMessageWithStackTrace(logRecord); + +    var parameters = new HashMap<String, Object>(); +    var labels = new HashMap<String, String>(); +    if (logRecord.getParameters() != null) { +      for (var parameter : logRecord.getParameters()) { +        if (parameter instanceof KeyValueParameter kvparam) { +          parameters.put(kvparam.key(), kvparam.value()); +        } else if (parameter instanceof Label label) { +          labels.put(label.key(), label.value()); +        } +      } +    } + +    var mdc = logRecord.getMdcCopy(); +    var ndc = logRecord.getNdc(); + +    var sourceLocation = +        new GoogleCloudLogEntry.SourceLocation( +            logRecord.getSourceFileName(), String.valueOf(logRecord.getSourceLineNumber()), String.format("%s.%s", logRecord.getSourceClassName(), logRecord.getSourceMethodName())); + +    var entry = +        new GoogleCloudLogEntry( +            message, +            severityOf(logRecord.getLevel()), +            new GoogleCloudLogEntry.Timestamp(logRecord.getInstant()), +            null, +            null, +            sourceLocation, +            labels.isEmpty() ? null : labels, +            parameters.isEmpty() ? null : parameters, +            mdc.isEmpty() ? null : mdc, +            ndc.isEmpty() ? null : ndc, +            logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null); + +    try { +      return jsonb.toJson(entry) + "\n"; +    } catch (JsonbException e) { +      e.printStackTrace(); +      return message + "\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/observability/googlecloud/jsonlogging/GoogleCloudLoggingRecorder.java b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingRecorder.java new file mode 100644 index 0000000..9ae3ae1 --- /dev/null +++ b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingRecorder.java @@ -0,0 +1,16 @@ +package eu.mulk.quarkus.observability.googlecloud.jsonlogging; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import java.util.Optional; +import java.util.logging.Formatter; +import javax.json.bind.spi.JsonbProvider; + +@Recorder +public class GoogleCloudLoggingRecorder { + +  public RuntimeValue<Optional<Formatter>> initialize() { +    var jsonb = JsonbProvider.provider().create().build(); +    return new RuntimeValue<>(Optional.of(new GoogleCloudLoggingFormatter(jsonb))); +  } +} diff --git a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/KeyValueParameter.java b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/KeyValueParameter.java new file mode 100644 index 0000000..358e470 --- /dev/null +++ b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/KeyValueParameter.java @@ -0,0 +1,3 @@ +package eu.mulk.quarkus.observability.googlecloud.jsonlogging; + +public record KeyValueParameter(String key, Object value) {} diff --git a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/Label.java b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/Label.java new file mode 100644 index 0000000..0c13739 --- /dev/null +++ b/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/Label.java @@ -0,0 +1,3 @@ +package eu.mulk.quarkus.observability.googlecloud.jsonlogging; + +public record Label(String key, String value) {} | 
