diff options
11 files changed, 415 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..887007b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.idea + +build/ +target/ + +*~ +*.class +*.iml diff --git a/deployment/pom.xml b/deployment/pom.xml new file mode 100644 index 0000000..5d34a10 --- /dev/null +++ b/deployment/pom.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>eu.mulk.quarkus-observability</groupId> + <artifactId>quarkus-googlecloud-jsonlogging-parent</artifactId> + <version>1.0.0-SNAPSHOT</version> + </parent> + <artifactId>quarkus-googlecloud-jsonlogging-deployment</artifactId> + <name>Quarkus Google Cloud JSON Logging Extension - Deployment</name> + + <dependencies> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-arc-deployment</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-jsonb-deployment</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-jsonb-spi</artifactId> + </dependency> + <dependency> + <groupId>eu.mulk.quarkus-observability</groupId> + <artifactId>quarkus-googlecloud-jsonlogging</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-junit5-internal</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <annotationProcessorPaths> + <path> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-extension-processor</artifactId> + <version>${quarkus.version}</version> + </path> + </annotationProcessorPaths> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/deployment/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingProcessor.java b/deployment/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingProcessor.java new file mode 100644 index 0000000..8ecf000 --- /dev/null +++ b/deployment/src/main/java/eu/mulk/quarkus/observability/googlecloud/jsonlogging/GoogleCloudLoggingProcessor.java @@ -0,0 +1,23 @@ +package eu.mulk.quarkus.observability.googlecloud.jsonlogging; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; + +class GoogleCloudLoggingProcessor { + + private static final String FEATURE = "googlecloud-jsonlogging"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + LogConsoleFormatBuildItem setUpFormatter(GoogleCloudLoggingRecorder recorder) { + return new LogConsoleFormatBuildItem(recorder.initialize()); + } +} @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + <modelVersion>4.0.0</modelVersion> + + <groupId>eu.mulk.quarkus-observability</groupId> + <artifactId>quarkus-googlecloud-jsonlogging-parent</artifactId> + <version>1.0.0-SNAPSHOT</version> + <packaging>pom</packaging> + <name>Quarkus Google Cloud JSON Logging Extension - Parent</name> + + <modules> + <module>deployment</module> + <module>runtime</module> + </modules> + + <properties> + <maven.compiler.parameters>true</maven.compiler.parameters> + <maven.compiler.release>16</maven.compiler.release> + + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + + <compiler-plugin.version>3.8.1</compiler-plugin.version> + <failsafe-plugin.version>${surefire-plugin.version}</failsafe-plugin.version> + <quarkus.version>1.13.3.Final</quarkus.version> + <surefire-plugin.version>3.0.0-M5</surefire-plugin.version> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-bom</artifactId> + <version>${quarkus.version}</version> + <type>pom</type> + <scope>import</scope> + </dependency> + </dependencies> + </dependencyManagement> + + <build> + <pluginManagement> + <plugins> + <plugin> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-maven-plugin</artifactId> + <version>${quarkus.version}</version> + </plugin> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <version>${surefire-plugin.version}</version> + <configuration> + <systemPropertyVariables> + <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> + <maven.home>${maven.home}</maven.home> + <maven.repo>${settings.localRepository}</maven.repo> + </systemPropertyVariables> + </configuration> + </plugin> + <plugin> + <artifactId>maven-failsafe-plugin</artifactId> + <version>${failsafe-plugin.version}</version> + <configuration> + <systemPropertyVariables> + <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> + <maven.home>${maven.home}</maven.home> + <maven.repo>${settings.localRepository}</maven.repo> + </systemPropertyVariables> + </configuration> + </plugin> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <version>${compiler-plugin.version}</version> + </plugin> + </plugins> + </pluginManagement> + </build> + +</project> diff --git a/runtime/pom.xml b/runtime/pom.xml new file mode 100644 index 0000000..e8254f9 --- /dev/null +++ b/runtime/pom.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>eu.mulk.quarkus-observability</groupId> + <artifactId>quarkus-googlecloud-jsonlogging-parent</artifactId> + <version>1.0.0-SNAPSHOT</version> + </parent> + <artifactId>quarkus-googlecloud-jsonlogging</artifactId> + <name>Quarkus Google Cloud JSON Logging Extension - Runtime</name> + + <dependencies> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-arc</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-jsonb</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-bootstrap-maven-plugin</artifactId> + <version>${quarkus.version}</version> + <executions> + <execution> + <phase>compile</phase> + <goals> + <goal>extension-descriptor</goal> + </goals> + <configuration> + <deployment>${project.groupId}:${project.artifactId}-deployment:${project.version}</deployment> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <annotationProcessorPaths> + <path> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-extension-processor</artifactId> + <version>${quarkus.version}</version> + </path> + </annotationProcessorPaths> + </configuration> + </plugin> + </plugins> + </build> + +</project> 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) {} diff --git a/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..7aa7a2a --- /dev/null +++ b/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: Google Cloud JSON Logging +description: Logs to standard output in Google-Cloud-compatible JSON format. +metadata: + keywords: + - logging + categories: + - "miscellaneous" + status: "preview" + # guide: ... |