From b8fbc37809dac5102292e6aa83668e149ed27102 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Tue, 11 May 2021 06:50:45 +0200 Subject: Restructure, rename, modularize. Change-Id: I4aa6cfb486fe002cf65405ec9c2876e3114aaf46 --- .../quarkus/googlecloud/jsonlogging/Formatter.java | 106 +++++++++++++++++++ .../GoogleCloudJsonLoggingRecorder.java | 12 +++ .../googlecloud/jsonlogging/KeyValueParameter.java | 43 ++++++++ .../quarkus/googlecloud/jsonlogging/Label.java | 8 ++ .../quarkus/googlecloud/jsonlogging/LogEntry.java | 103 ++++++++++++++++++ .../jsonlogging/StructuredParameter.java | 7 ++ .../jsonlogging/GoogleCloudLogEntry.java | 37 ------- .../jsonlogging/GoogleCloudLoggingFormatter.java | 116 --------------------- .../jsonlogging/GoogleCloudLoggingRecorder.java | 16 --- .../googlecloud/jsonlogging/KeyValueParameter.java | 3 - .../googlecloud/jsonlogging/Label.java | 3 - runtime/src/main/java/module-info.java | 9 ++ 12 files changed, 288 insertions(+), 175 deletions(-) create mode 100644 runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java create mode 100644 runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/GoogleCloudJsonLoggingRecorder.java create mode 100644 runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java create mode 100644 runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java create mode 100644 runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java create mode 100644 runtime/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameter.java delete mode 100644 runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLogEntry.java delete mode 100644 runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingFormatter.java delete mode 100644 runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingRecorder.java delete mode 100644 runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/KeyValueParameter.java delete mode 100644 runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/Label.java create mode 100644 runtime/src/main/java/module-info.java (limited to 'runtime/src') 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. + * + *

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 parameters = new ArrayList<>(); + Map 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> 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. + * + *

Roughly (but not quite) corresponds to Google Cloud Logging's LogEntry + * structure. + * + *

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 labels, + List parameters, + Map 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 stringMap) { + return stringMap.entrySet().stream() + .reduce( + Json.createObjectBuilder(), + (acc, x) -> acc.add(x.getKey(), x.getValue()), + JsonObjectBuilder::addAll); + } + + private static JsonObjectBuilder jsonOfParameterMap(List 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(); +} 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 deleted file mode 100644 index 0450d0c..0000000 --- a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLogEntry.java +++ /dev/null @@ -1,37 +0,0 @@ -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. - * - *

Roughly (but not quite) corresponds to Google Cloud Logging's LogEntry - * structure. - */ -public record GoogleCloudLogEntry( - String getMessage, - String getSeverity, - Timestamp getTimestamp, - @Nullable String getTrace, - @Nullable String getSpanId, - @Nullable SourceLocation getSourceLocation, - @Nullable Map getLabels, - @Nullable Map getParameters, - @Nullable Map getMappedDiagnosticContext, - @Nullable String getNestedDiagnosticContext, - @Nullable @JsonbProperty("@type") String getType) { - - public static record SourceLocation( - @Nullable String getFile, @Nullable String getLine, @Nullable String getFunction) {} - - public static 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 deleted file mode 100644 index 3ec1fcc..0000000 --- a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingFormatter.java +++ /dev/null @@ -1,116 +0,0 @@ -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. - * - *

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(); - var labels = new HashMap(); - 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 deleted file mode 100644 index 9ae3ae1..0000000 --- a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingRecorder.java +++ /dev/null @@ -1,16 +0,0 @@ -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> 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 deleted file mode 100644 index 358e470..0000000 --- a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/KeyValueParameter.java +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 0c13739..0000000 --- a/runtime/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/Label.java +++ /dev/null @@ -1,3 +0,0 @@ -package eu.mulk.quarkus.observability.googlecloud.jsonlogging; - -public record Label(String key, String value) {} diff --git a/runtime/src/main/java/module-info.java b/runtime/src/main/java/module-info.java new file mode 100644 index 0000000..56b44fc --- /dev/null +++ b/runtime/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module quarkus.googlecloud.jsonlogging { + requires java.logging; + requires java.json; + requires jboss.logmanager.embedded; + requires quarkus.core; + requires smallrye.common.constraint; + + exports eu.mulk.quarkus.googlecloud.jsonlogging; +} -- cgit v1.2.3