Initial checkin: Quarkus Google Cloud JSON Logging.

Change-Id: I264211f56c2bed4002ecdb6ead8a5321ada855fd
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;
+    }
+  }
+}