summaryrefslogtreecommitdiff
path: root/runtime/src/main/java/eu/mulk/quarkus/googlecloud
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2021-05-11 06:50:45 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2021-05-11 21:49:26 +0200
commitb8fbc37809dac5102292e6aa83668e149ed27102 (patch)
tree3b71b8a50d745760ec8da2970a22aadde8df0cd6 /runtime/src/main/java/eu/mulk/quarkus/googlecloud
parent4bae5f13fb592f78d5ce714641120a1473700f9a (diff)
Restructure, rename, modularize.
Change-Id: I4aa6cfb486fe002cf65405ec9c2876e3114aaf46
Diffstat (limited to 'runtime/src/main/java/eu/mulk/quarkus/googlecloud')
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java106
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/GoogleCloudJsonLoggingRecorder.java12
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java43
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java8
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java103
-rw-r--r--runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java7
6 files changed, 279 insertions, 0 deletions
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
new file mode 100644
index 0000000..d6eeb0e
--- /dev/null
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java
@@ -0,0 +1,106 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+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
+ */
+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";
+
+ @Override
+ public String format(ExtLogRecord logRecord) {
+ var message = formatMessageWithStackTrace(logRecord);
+
+ List<StructuredParameter> parameters = new ArrayList<>();
+ Map<String, String> labels = new HashMap<>();
+ if (logRecord.getParameters() != null) {
+ for (var parameter : logRecord.getParameters()) {
+ if (parameter instanceof StructuredParameter sparam) {
+ parameters.add(sparam);
+ } else if (parameter instanceof Label label) {
+ 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/GoogleCloudJsonLoggingRecorder.java b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/GoogleCloudJsonLoggingRecorder.java
new file mode 100644
index 0000000..db2c2e9
--- /dev/null
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/GoogleCloudJsonLoggingRecorder.java
@@ -0,0 +1,12 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+import java.util.Optional;
+
+@Recorder
+public class GoogleCloudJsonLoggingRecorder {
+ public RuntimeValue<Optional<java.util.logging.Formatter>> initialize() {
+ return new RuntimeValue<>(Optional.of(new Formatter()));
+ }
+}
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
new file mode 100644
index 0000000..5f582c9
--- /dev/null
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java
@@ -0,0 +1,43 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import javax.json.Json;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+public record KeyValueParameter(String key, JsonValue value) implements StructuredParameter {
+
+ public static KeyValueParameter of(String key, String value) {
+ return new KeyValueParameter(key, Json.createValue(value));
+ }
+
+ public static KeyValueParameter of(String key, int value) {
+ return new KeyValueParameter(key, Json.createValue(value));
+ }
+
+ public static KeyValueParameter of(String key, long value) {
+ return new KeyValueParameter(key, Json.createValue(value));
+ }
+
+ public static KeyValueParameter of(String key, double value) {
+ return new KeyValueParameter(key, Json.createValue(value));
+ }
+
+ public static KeyValueParameter of(String key, BigDecimal value) {
+ return new KeyValueParameter(key, Json.createValue(value));
+ }
+
+ public static KeyValueParameter of(String key, BigInteger value) {
+ return new KeyValueParameter(key, Json.createValue(value));
+ }
+
+ 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);
+ }
+}
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
new file mode 100644
index 0000000..02f7034
--- /dev/null
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java
@@ -0,0 +1,8 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+public record Label(String key, String value) {
+
+ public static Label of(String key, String value) {
+ return new Label(key, value);
+ }
+}
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
new file mode 100644
index 0000000..4394033
--- /dev/null
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java
@@ -0,0 +1,103 @@
+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 treated specially by the fluentd instance running in Google Kubernetes
+ * Engine. All other fields end up in the jsonPayload field on the Google Cloud Logging side.
+ */
+record 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) {
+
+ static record SourceLocation(
+ @Nullable String file, @Nullable String line, @Nullable String function) {
+
+ JsonObject json() {
+ return Json.createObjectBuilder()
+ .add("file", file)
+ .add("line", line)
+ .add("function", function)
+ .build();
+ }
+ }
+
+ static record Timestamp(long seconds, int 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("trace", trace);
+ }
+
+ if (spanId != null) {
+ b.add("spanId", spanId);
+ }
+
+ if (nestedDiagnosticContext != null && !nestedDiagnosticContext.isEmpty()) {
+ b.add("nestedDiagnosticContext", nestedDiagnosticContext);
+ }
+
+ if (!labels.isEmpty()) {
+ b.add("labels", jsonOfStringMap(labels));
+ }
+
+ if (type != null) {
+ b.add("@type", type);
+ }
+
+ return b.add("message", message)
+ .add("severity", severity)
+ .add("timestamp", timestamp.json())
+ .add("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
new file mode 100644
index 0000000..0b4a36e
--- /dev/null
+++ b/runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java
@@ -0,0 +1,7 @@
+package eu.mulk.quarkus.googlecloud.jsonlogging;
+
+import javax.json.JsonObjectBuilder;
+
+public interface StructuredParameter {
+ JsonObjectBuilder json();
+}