diff options
Diffstat (limited to 'core')
13 files changed, 786 insertions, 97 deletions
diff --git a/core/pom.xml b/core/pom.xml index cb61c20..e4a28db 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -14,7 +14,7 @@ SPDX-License-Identifier: LGPL-3.0-or-later <parent> <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId> <artifactId>quarkus-googlecloud-jsonlogging-parent</artifactId> - <version>6.1.1-SNAPSHOT</version> + <version>6.6.1-SNAPSHOT</version> </parent> <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId> @@ -33,14 +33,53 @@ SPDX-License-Identifier: LGPL-3.0-or-later <optional>true</optional> </dependency> <dependency> - <groupId>io.smallrye.common</groupId> - <artifactId>smallrye-common-constraint</artifactId> - <version>2.4.0</version> + <groupId>jakarta.json</groupId> + <artifactId>jakarta.json-api</artifactId> + <version>2.1.3</version> </dependency> <dependency> + <groupId>org.jspecify</groupId> + <artifactId>jspecify</artifactId> + <version>1.0.0</version> + </dependency> + <dependency> + <groupId>io.github.eisop</groupId> + <artifactId>checker-qual</artifactId> + <version>3.42.0-eisop4</version> + <scope>provided</scope> + </dependency> + + <!-- Include Parsson for backwards-compatibility. --> + <dependency> <groupId>org.eclipse.parsson</groupId> <artifactId>parsson</artifactId> - <version>1.1.6</version> + <version>1.1.7</version> + </dependency> + + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.11.3</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-api</artifactId> + <version>5.11.3</version> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>org.openjdk.jmh</groupId> + <artifactId>jmh-core</artifactId> + <version>1.37</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.openjdk.jmh</groupId> + <artifactId>jmh-generator-annprocess</artifactId> + <version>1.37</version> + <scope>test</scope> </dependency> </dependencies> @@ -48,6 +87,58 @@ SPDX-License-Identifier: LGPL-3.0-or-later <plugins> <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <fork>true</fork> + <annotationProcessorPaths> + <path> + <groupId>org.openjdk.jmh</groupId> + <artifactId>jmh-generator-annprocess</artifactId> + <version>1.37</version> + </path> + <path> + <groupId>io.github.eisop</groupId> + <artifactId>checker</artifactId> + <version>3.42.0-eisop4</version> + </path> + </annotationProcessorPaths> + <annotationProcessors> + <annotationProcessor>org.checkerframework.checker.nullness.NullnessChecker</annotationProcessor> + </annotationProcessors> + <compilerArgs> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg> + <arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg> + <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg> + <arg>-Xmaxerrs</arg> + <arg>10000</arg> + <arg>-Xmaxwarns</arg> + <arg>10000</arg> + </compilerArgs> + </configuration> + <executions> + <execution> + <id>default-compile</id> + <configuration> + </configuration> + </execution> + <execution> + <id>default-testCompile</id> + <configuration> + <annotationProcessors> + <annotationProcessor>org.openjdk.jmh.generators.BenchmarkProcessor</annotationProcessor> + </annotationProcessors> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> @@ -58,7 +149,56 @@ SPDX-License-Identifier: LGPL-3.0-or-later </configuration> </plugin> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + </plugin> + </plugins> </build> + <profiles> + + <profile> + <id>benchmark</id> + + <build> + <plugins> + + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <skipTests>true</skipTests> + </configuration> + </plugin> + + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <executions> + <execution> + <id>run-benchmarks</id> + <phase>integration-test</phase> + <goals> + <goal>exec</goal> + </goals> + <configuration> + <classpathScope>test</classpathScope> + <executable>java</executable> + <arguments> + <argument>-classpath</argument> + <classpath /> + <argument>org.openjdk.jmh.Main</argument> + <argument>.*</argument> + </arguments> + </configuration> + </execution> + </executions> + </plugin> + + </plugins> + </build> + </profile> + + </profiles> + </project> diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java index c4e36de..9c66f82 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java @@ -6,17 +6,13 @@ package eu.mulk.quarkus.googlecloud.jsonlogging; import java.io.PrintWriter; import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; +import java.util.*; import java.util.ServiceLoader.Provider; import java.util.logging.Level; import java.util.stream.Collectors; import org.jboss.logmanager.ExtFormatter; import org.jboss.logmanager.ExtLogRecord; +import org.jspecify.annotations.Nullable; /** * Formats log records as JSON for consumption by Google Cloud Logging. @@ -30,14 +26,19 @@ public 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 NOTICE_LEVEL = "NOTICE"; private static final String WARNING_LEVEL = "WARNING"; private static final String ERROR_LEVEL = "ERROR"; + private static final String CRITICAL_LEVEL = "CRITICAL"; + private static final String ALERT_LEVEL = "ALERT"; + private static final String EMERGENCY_LEVEL = "EMERGENCY"; private static final String ERROR_EVENT_TYPE = "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent"; private final List<StructuredParameterProvider> parameterProviders; private final List<LabelProvider> labelProviders; + private final ThreadLocal<@Nullable StringBuilder> stringBuilder; /** * Constructs a {@link Formatter} with custom configuration. @@ -53,6 +54,7 @@ public class Formatter extends ExtFormatter { Collection<LabelProvider> labelProviders) { this.parameterProviders = List.copyOf(parameterProviders); this.labelProviders = List.copyOf(labelProviders); + this.stringBuilder = ThreadLocal.withInitial(StringBuilder::new); } /** @@ -117,13 +119,18 @@ public class Formatter extends ExtFormatter { } } - if (logRecord.getParameters() != null) { - for (var parameter : logRecord.getParameters()) { + String insertId = null; + + var logRecordParameters = logRecord.getParameters(); + if (logRecordParameters != null) { + for (var parameter : logRecordParameters) { if (parameter instanceof StructuredParameter) { parameters.add((StructuredParameter) parameter); } else if (parameter instanceof Label) { var label = (Label) parameter; labels.put(label.key(), label.value()); + } else if (parameter instanceof InsertId) { + insertId = ((InsertId) parameter).value(); } } } @@ -131,12 +138,7 @@ public class Formatter extends ExtFormatter { 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 sourceLocation = sourceLocationOf(logRecord); var entry = new LogEntry( @@ -150,9 +152,31 @@ public class Formatter extends ExtFormatter { parameters, mdc, ndc, - logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null); + logRecord.getLevel().intValue() >= 1000 ? ERROR_EVENT_TYPE : null, + insertId); + + var b = Objects.requireNonNull(stringBuilder.get()); + b.delete(0, b.length()); + b.append("{"); + entry.json(b); + b.append("}\n"); + return b.toString(); + } - return entry.json().build().toString() + "\n"; + private static LogEntry.@Nullable SourceLocation sourceLocationOf(ExtLogRecord logRecord) { + var sourceFileName = logRecord.getSourceFileName(); + var sourceLineNumber = logRecord.getSourceLineNumber(); + var sourceClassName = logRecord.getSourceClassName(); + var sourceMethodName = logRecord.getSourceMethodName(); + return (sourceFileName == null + && sourceLineNumber <= 0 + && sourceClassName == null + && sourceMethodName == null) + ? null + : new LogEntry.SourceLocation( + sourceFileName, + String.valueOf(sourceLineNumber), + String.format("%s.%s", sourceClassName, sourceMethodName)); } /** @@ -164,9 +188,10 @@ public class Formatter extends ExtFormatter { var messagePrintWriter = new PrintWriter(messageStringWriter); messagePrintWriter.append(this.formatMessage(logRecord)); - if (logRecord.getThrown() != null) { + var logRecordThrown = logRecord.getThrown(); + if (logRecordThrown != null) { messagePrintWriter.println(); - logRecord.getThrown().printStackTrace(messagePrintWriter); + logRecordThrown.printStackTrace(messagePrintWriter); } messagePrintWriter.close(); @@ -179,12 +204,20 @@ public class Formatter extends ExtFormatter { return TRACE_LEVEL; } else if (level.intValue() < 700) { return DEBUG_LEVEL; - } else if (level.intValue() < 900) { + } else if (level.intValue() < 850) { return INFO_LEVEL; + } else if (level.intValue() < 900) { + return NOTICE_LEVEL; } else if (level.intValue() < 1000) { return WARNING_LEVEL; - } else { + } else if (level.intValue() < 1100) { return ERROR_LEVEL; + } else if (level.intValue() < 1200) { + return CRITICAL_LEVEL; + } else if (level.intValue() < 1300) { + return ALERT_LEVEL; + } else { + return EMERGENCY_LEVEL; } } @@ -195,9 +228,9 @@ public class Formatter extends ExtFormatter { private static class ProviderContext implements LabelProvider.Context, StructuredParameterProvider.Context { - private final String loggerName; + private final @Nullable String loggerName; private final long sequenceNumber; - private final String threadName; + private final @Nullable String threadName; private ProviderContext(ExtLogRecord logRecord) { loggerName = logRecord.getLoggerName(); @@ -206,7 +239,7 @@ public class Formatter extends ExtFormatter { } @Override - public String loggerName() { + public @Nullable String loggerName() { return loggerName; } @@ -216,7 +249,7 @@ public class Formatter extends ExtFormatter { } @Override - public String threadName() { + public @Nullable String threadName() { return threadName; } } diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/InsertId.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/InsertId.java new file mode 100644 index 0000000..48b376c --- /dev/null +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/InsertId.java @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: © 2024 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +package eu.mulk.quarkus.googlecloud.jsonlogging; + +import java.util.Objects; +import org.jspecify.annotations.Nullable; + +/** + * A unique identifier for a log entry. + * + * <p>Prevents the duplicate insertion of log entries. Also serves as a discriminator to order log + * entries that carry the same time stamp. + * + * <p>Will be generated by Google Cloud Logging if not provided. + * + * <p>Instances of {@link InsertId} can be passed as log parameters to the {@code *f} family of + * logging functions on {@link org.jboss.logging.Logger}. + * + * <p><strong>Example:</strong> + * + * {@snippet : + * logger.logf("Request rejected: unauthorized.", InsertId.of("123")); + * } + * + * <p>Result: + * + * {@snippet lang="json" : + * { + * "textPayload": "Request rejected: unauthorized.", + * "logging.googleapis.com/insertId": "123" + * } + * } + * + * @see Label + * @see StructuredParameter + */ +public final class InsertId { + + private final String value; + + private InsertId(String value) { + this.value = value; + } + + /** + * Constructs an {@link InsertId} from a string. + * + * @param value the value of the insertion ID. + * @return the newly constructed {@link InsertId}, ready to be passed to a logging function. + */ + public static InsertId of(String value) { + return new InsertId(value); + } + + /** + * The value of the label. + * + * @return the value of the label. + */ + public String value() { + return value; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (InsertId) obj; + return Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return "InsertId[value=" + value + ']'; + } +} diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java index 87a889c..9e16aab 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java @@ -4,12 +4,13 @@ package eu.mulk.quarkus.googlecloud.jsonlogging; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; +import jakarta.json.spi.JsonProvider; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Objects; -import jakarta.json.Json; -import jakarta.json.JsonObjectBuilder; -import jakarta.json.JsonValue; +import org.jspecify.annotations.Nullable; /** * A simple single key–value pair forming a {@link StructuredParameter}. @@ -40,6 +41,8 @@ import jakarta.json.JsonValue; */ public final class KeyValueParameter implements StructuredParameter { + private static final JsonProvider JSON = JsonProvider.provider(); + private final String key; private final JsonValue value; @@ -58,7 +61,7 @@ public final class KeyValueParameter implements StructuredParameter { * @return the newly constructed parameter, ready to be passed to a logging function. */ public static KeyValueParameter of(String key, String value) { - return new KeyValueParameter(key, Json.createValue(value)); + return new KeyValueParameter(key, JSON.createValue(value)); } /** @@ -71,7 +74,7 @@ public final class KeyValueParameter implements StructuredParameter { * @return the newly constructed parameter, ready to be passed to a logging function. */ public static KeyValueParameter of(String key, int value) { - return new KeyValueParameter(key, Json.createValue(value)); + return new KeyValueParameter(key, JSON.createValue(value)); } /** @@ -84,7 +87,7 @@ public final class KeyValueParameter implements StructuredParameter { * @return the newly constructed parameter, ready to be passed to a logging function. */ public static KeyValueParameter of(String key, long value) { - return new KeyValueParameter(key, Json.createValue(value)); + return new KeyValueParameter(key, JSON.createValue(value)); } /** @@ -97,7 +100,7 @@ public final class KeyValueParameter implements StructuredParameter { * @return the newly constructed parameter, ready to be passed to a logging function. */ public static KeyValueParameter of(String key, double value) { - return new KeyValueParameter(key, Json.createValue(value)); + return new KeyValueParameter(key, JSON.createValue(value)); } /** @@ -110,7 +113,7 @@ public final class KeyValueParameter implements StructuredParameter { * @return the newly constructed parameter, ready to be passed to a logging function. */ public static KeyValueParameter of(String key, BigDecimal value) { - return new KeyValueParameter(key, Json.createValue(value)); + return new KeyValueParameter(key, JSON.createValue(value)); } /** @@ -123,7 +126,7 @@ public final class KeyValueParameter implements StructuredParameter { * @return the newly constructed parameter, ready to be passed to a logging function. */ public static KeyValueParameter of(String key, BigInteger value) { - return new KeyValueParameter(key, Json.createValue(value)); + return new KeyValueParameter(key, JSON.createValue(value)); } /** @@ -141,7 +144,7 @@ public final class KeyValueParameter implements StructuredParameter { @Override public JsonObjectBuilder json() { - return Json.createObjectBuilder().add(key, value); + return JSON.createObjectBuilder().add(key, value); } /** @@ -166,7 +169,7 @@ public final class KeyValueParameter implements StructuredParameter { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (obj == this) return true; if (obj == null || obj.getClass() != this.getClass()) return false; var that = (KeyValueParameter) obj; diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java index d5a9000..2696185 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java @@ -5,6 +5,7 @@ package eu.mulk.quarkus.googlecloud.jsonlogging; import java.util.Objects; +import org.jspecify.annotations.Nullable; /** * A label usable to tag a log message. @@ -78,7 +79,7 @@ public final class Label { } @Override - public boolean equals(Object obj) { + public boolean equals(@Nullable Object obj) { if (obj == this) return true; if (obj == null || obj.getClass() != this.getClass()) return false; var that = (Label) obj; diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java index 0298042..2bc0349 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java @@ -5,6 +5,7 @@ package eu.mulk.quarkus.googlecloud.jsonlogging; import java.util.Collection; +import org.jspecify.annotations.Nullable; /** * A user-supplied provider for {@link Label}s. @@ -52,7 +53,7 @@ public interface LabelProvider { * @return a collection of {@link Label}s to add to each log entry that is logged. * @see #getLabels(Context) */ - default Collection<Label> getLabels() { + default @Nullable Collection<Label> getLabels() { return null; } @@ -63,7 +64,7 @@ public interface LabelProvider { * * @return a collection of {@link Label}s to add to each log entry that is logged. */ - default Collection<Label> getLabels(Context context) { + default @Nullable Collection<Label> getLabels(Context context) { return getLabels(); } diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java index 69ebc93..d335ee4 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java @@ -4,13 +4,12 @@ package eu.mulk.quarkus.googlecloud.jsonlogging; -import io.smallrye.common.constraint.Nullable; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; import java.time.Instant; import java.util.List; import java.util.Map; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonObjectBuilder; +import org.jspecify.annotations.Nullable; /** * A JSON log entry compatible with Google Cloud Logging. @@ -28,14 +27,15 @@ final class LogEntry { private final String message; private final String severity; private final Timestamp timestamp; - @Nullable private final String trace; - @Nullable private final String spanId; - private final SourceLocation sourceLocation; + private final @Nullable String trace; + private final @Nullable String spanId; + private final @Nullable SourceLocation sourceLocation; private final Map<String, String> labels; private final List<StructuredParameter> parameters; private final Map<String, String> mappedDiagnosticContext; - @Nullable private final String nestedDiagnosticContext; - @Nullable private final String type; + private final @Nullable String nestedDiagnosticContext; + private final @Nullable String type; + private final @Nullable String insertId; LogEntry( String message, @@ -43,12 +43,13 @@ final class LogEntry { Timestamp timestamp, @Nullable String trace, @Nullable String spanId, - SourceLocation sourceLocation, + @Nullable SourceLocation sourceLocation, Map<String, String> labels, List<StructuredParameter> parameters, Map<String, String> mappedDiagnosticContext, @Nullable String nestedDiagnosticContext, - @Nullable String type) { + @Nullable String type, + @Nullable String insertId) { this.message = message; this.severity = severity; this.timestamp = timestamp; @@ -60,13 +61,14 @@ final class LogEntry { this.mappedDiagnosticContext = mappedDiagnosticContext; this.nestedDiagnosticContext = nestedDiagnosticContext; this.type = type; + this.insertId = insertId; } static final class SourceLocation { - @Nullable private final String file; - @Nullable private final String line; - @Nullable private final String function; + private final @Nullable String file; + private final @Nullable String line; + private final @Nullable String function; SourceLocation(@Nullable String file, @Nullable String line, @Nullable String function) { this.file = file; @@ -74,22 +76,31 @@ final class LogEntry { this.function = function; } - JsonObject json() { - var b = Json.createObjectBuilder(); + void json(StringBuilder b) { + var commaNeeded = false; if (file != null) { - b.add("file", file); + b.append("\"file\":"); + appendJsonString(b, file); + commaNeeded = true; } if (line != null) { - b.add("line", line); + if (commaNeeded) { + b.append(","); + } + b.append("\"line\":"); + appendJsonString(b, line); + commaNeeded = true; } if (function != null) { - b.add("function", function); + if (commaNeeded) { + b.append(","); + } + b.append("\"function\":"); + appendJsonString(b, function); } - - return b.build(); } } @@ -107,55 +118,199 @@ final class LogEntry { this(t.getEpochSecond(), t.getNano()); } - JsonObject json() { - return Json.createObjectBuilder().add("seconds", seconds).add("nanos", nanos).build(); + void json(StringBuilder b) { + b.append("\"seconds\":"); + b.append(seconds); + b.append(",\"nanos\":"); + b.append(nanos); } } - JsonObjectBuilder json() { - var b = Json.createObjectBuilder(); + void json(StringBuilder b) { + + if (insertId != null) { + b.append("\"logging.googleapis.com/insertId\":"); + appendJsonString(b, insertId); + b.append(","); + } if (trace != null) { - b.add("logging.googleapis.com/trace", trace); + b.append("\"logging.googleapis.com/trace\":"); + appendJsonString(b, trace); + b.append(","); } if (spanId != null) { - b.add("logging.googleapis.com/spanId", spanId); + b.append("\"logging.googleapis.com/spanId\":"); + appendJsonString(b, spanId); + b.append(","); } if (nestedDiagnosticContext != null && !nestedDiagnosticContext.isEmpty()) { - b.add("nestedDiagnosticContext", nestedDiagnosticContext); + b.append("\"nestedDiagnosticContext\":"); + appendJsonString(b, nestedDiagnosticContext); + b.append(","); } if (!labels.isEmpty()) { - b.add("logging.googleapis.com/labels", jsonOfStringMap(labels)); + b.append("\"logging.googleapis.com/labels\":{"); + + var first = true; + for (var entry : labels.entrySet()) { + if (!first) { + b.append(","); + } else { + first = false; + } + + appendJsonString(b, entry.getKey()); + b.append(":"); + appendJsonString(b, entry.getValue()); + } + + b.append("},"); + } + + for (var entry : mappedDiagnosticContext.entrySet()) { + appendJsonString(b, entry.getKey()); + b.append(":"); + appendJsonString(b, entry.getValue()); + b.append(","); + } + + for (var parameter : parameters) { + var jsonObject = parameter.json().build(); + jsonObject.forEach( + (key, value) -> { + appendJsonString(b, key); + b.append(":"); + appendJsonObject(b, value); + b.append(","); + }); } if (type != null) { - b.add("@type", type); + b.append("\"@type\":"); + appendJsonString(b, type); + b.append(","); + } + + if (sourceLocation != null) { + b.append("\"logging.googleapis.com/sourceLocation\":{"); + sourceLocation.json(b); + b.append("},"); } - return b.add("message", message) - .add("severity", severity) - .add("timestamp", timestamp.json()) - .add("logging.googleapis.com/sourceLocation", sourceLocation.json()) - .addAll(jsonOfStringMap(mappedDiagnosticContext)) - .addAll(jsonOfParameterMap(parameters)); + b.append("\"message\":"); + appendJsonString(b, message); + + b.append(",\"severity\":"); + appendJsonString(b, severity); + + b.append(",\"timestamp\":{"); + timestamp.json(b); + b.append("}"); } - 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 void appendJsonObject(StringBuilder b, JsonValue value) { + switch (value.getValueType()) { + case ARRAY: + b.append("["); + var array = value.asJsonArray(); + for (var i = 0; i < array.size(); i++) { + if (i > 0) { + b.append(","); + } + appendJsonObject(b, array.get(i)); + } + b.append("]"); + break; + + case OBJECT: + b.append("{"); + var object = value.asJsonObject(); + var first = true; + for (var entry : object.entrySet()) { + if (!first) { + b.append(","); + } else { + first = false; + } + appendJsonString(b, entry.getKey()); + b.append(":"); + appendJsonObject(b, entry.getValue()); + } + b.append("}"); + break; + + case STRING: + appendJsonString(b, ((JsonString) value).getString()); + break; + + case NUMBER: + b.append(value); + break; + + case TRUE: + b.append("true"); + break; + + case FALSE: + b.append("false"); + break; + + case NULL: + b.append("null"); + break; + } } - private static JsonObjectBuilder jsonOfParameterMap(List<StructuredParameter> parameters) { - return parameters.stream() - .reduce( - Json.createObjectBuilder(), - (acc, p) -> acc.addAll(p.json()), - JsonObjectBuilder::addAll); + private static void appendJsonString(StringBuilder b, String s) { + b.append('"'); + + for (var i = 0; i < s.length(); i++) { + var c = s.charAt(i); + + switch (c) { + case '"': + b.append("\\\""); + break; + + case '\\': + b.append("\\\\"); + break; + + case '\b': + b.append("\\b"); + break; + + case '\f': + b.append("\\f"); + break; + + case '\n': + b.append("\\n"); + break; + + case '\r': + b.append("\\r"); + break; + + case '\t': + b.append("\\t"); + break; + + default: + if (c < 0x20) { + b.append("\\u00"); + b.append(Character.forDigit((c >> 4) & 0xf, 16)); + b.append(Character.forDigit(c & 0xf, 16)); + } else { + b.append(c); + } + } + } + + b.append('"'); } } diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/ProviderContext.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/ProviderContext.java index 08b399a..93f7a7a 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/ProviderContext.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/ProviderContext.java @@ -5,6 +5,7 @@ package eu.mulk.quarkus.googlecloud.jsonlogging; import org.jboss.logmanager.ExtLogRecord; +import org.jspecify.annotations.Nullable; /** * Contextual data available to {@link StructuredParameterProvider} and {@link LabelProvider}. @@ -19,7 +20,7 @@ public interface ProviderContext { * * @return {@link ExtLogRecord#getLoggerName()}. */ - String loggerName(); + @Nullable String loggerName(); /** * The {@link ExtLogRecord#getSequenceNumber()} property of the log record that is being @@ -34,5 +35,5 @@ public interface ProviderContext { * * @return {@link ExtLogRecord#getThreadName()}. */ - String threadName(); + @Nullable String threadName(); } diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java index d78f0d8..c02516c 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java @@ -4,6 +4,8 @@ package eu.mulk.quarkus.googlecloud.jsonlogging; +import org.jspecify.annotations.Nullable; + /** * A user-supplied provider for {@link StructuredParameter}s. * @@ -58,7 +60,7 @@ public interface StructuredParameterProvider { * @return a {@link StructuredParameter} to add to each log entry that is logged. * @see #getParameter(Context) */ - default StructuredParameter getParameter() { + default @Nullable StructuredParameter getParameter() { return null; } @@ -73,7 +75,7 @@ public interface StructuredParameterProvider { * * @return a {@link StructuredParameter} to add to each log entry that is logged. */ - default StructuredParameter getParameter(Context context) { + default @Nullable StructuredParameter getParameter(Context context) { return getParameter(); } diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/logmanager/package-info.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/logmanager/package-info.java index 3b6f38c..d36dfc1 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/logmanager/package-info.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/logmanager/package-info.java @@ -19,4 +19,7 @@ * when used in conjunction with frameworks other than Quarkus (such as Spring Boot). See the class * documentation for details. */ +@NullMarked package eu.mulk.quarkus.googlecloud.jsonlogging.logmanager; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java index 6c4f51b..17133c4 100644 --- a/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java +++ b/core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java @@ -43,7 +43,7 @@ * <dependency> * <groupId>eu.mulk.quarkus-googlecloud-jsonlogging</groupId> * <artifactId>quarkus-googlecloud-jsonlogging-core</artifactId> - * <version>5.0.0</version> + * <version>6.6.0</version> * </dependency> * * ... @@ -59,7 +59,7 @@ * dependencies { * // ... * - * implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging-core:5.0.0") + * implementation("eu.mulk.quarkus-googlecloud-jsonlogging:quarkus-googlecloud-jsonlogging-core:6.6.0") * * // ... * } @@ -198,4 +198,7 @@ * } * } */ +@NullMarked package eu.mulk.quarkus.googlecloud.jsonlogging; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterBenchmark.java b/core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterBenchmark.java new file mode 100644 index 0000000..7bbdac6 --- /dev/null +++ b/core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterBenchmark.java @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: © 2024 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +package eu.mulk.quarkus.googlecloud.jsonlogging; + +import static java.util.logging.Level.FINEST; + +import java.util.List; +import java.util.logging.LogRecord; +import org.jboss.logmanager.formatters.Formatters; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +@Fork(value = 1) +@State(org.openjdk.jmh.annotations.Scope.Benchmark) +public class FormatterBenchmark { + + private static final LogRecord NULL_LOG_RECORD = new LogRecord(FINEST, ""); + private static final java.util.logging.Formatter NULL_FORMATTER = Formatters.nullFormatter(); + + private LogRecord simpleLogRecord = NULL_LOG_RECORD; + private LogRecord structuredLogRecord = NULL_LOG_RECORD; + private LogRecord massivelyStructuredLogRecord = NULL_LOG_RECORD; + private LogRecord nestedLogRecord = NULL_LOG_RECORD; + private java.util.logging.Formatter formatter = NULL_FORMATTER; + + @Setup + public void setup() { + simpleLogRecord = FormatterTest.makeSimpleRecord(); + structuredLogRecord = FormatterTest.makeStructuredRecord(); + massivelyStructuredLogRecord = FormatterTest.makeMassivelyStructuredRecord(); + nestedLogRecord = FormatterTest.makeNestedRecord(); + formatter = new Formatter(List.of(), List.of()); + } + + @Benchmark + public void simpleLogRecord(Blackhole blackhole) { + blackhole.consume(formatter.format(simpleLogRecord)); + } + + @Benchmark + public void structuredLogRecord(Blackhole blackhole) { + blackhole.consume(formatter.format(structuredLogRecord)); + } + + @Benchmark + public void massivelyStructuredLogRecord(Blackhole blackhole) { + var f = formatter.format(massivelyStructuredLogRecord); + blackhole.consume(f); + } + + @Benchmark + public void nestedLogRecord(Blackhole blackhole) { + var f = formatter.format(nestedLogRecord); + blackhole.consume(f); + } +} diff --git a/core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterTest.java b/core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterTest.java new file mode 100644 index 0000000..33df302 --- /dev/null +++ b/core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterTest.java @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: © 2024 Matthias Andreas Benkard <code@mail.matthias.benkard.de> +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +package eu.mulk.quarkus.googlecloud.jsonlogging; + +import static org.junit.jupiter.api.Assertions.assertLinesMatch; + +import jakarta.json.spi.JsonProvider; +import java.util.Collection; +import java.util.List; +import org.jboss.logmanager.ExtLogRecord; +import org.jboss.logmanager.Level; +import org.junit.jupiter.api.Test; + +class FormatterTest { + + private static final JsonProvider JSON = JsonProvider.provider(); + + @Test + void simpleRecord() { + var logRecord = makeSimpleRecord(); + + var formatter = new Formatter(List.of(), List.of()); + var formattingResult = formatter.format(logRecord); + + assertLinesMatch( + List.of( + "\\{" + + "\"logging.googleapis.com/sourceLocation\":" + + "\\{\"file\":\"ReflectionUtils.java\"," + + "\"line\":\"\\d+\"," + + "\"function\":\"org.junit.platform.commons.util.ReflectionUtils.invokeMethod\"" + + "\\}," + + "\"message\":\"Hello, world!\"," + + "\"severity\":\"INFO\"," + + "\"timestamp\":\\{\"seconds\":\\d+,\"nanos\":\\d+\\}" + + "\\}\n"), + List.of(formattingResult)); + } + + static ExtLogRecord makeSimpleRecord() { + return new ExtLogRecord(Level.INFO, "Hello, world!", FormatterTest.class.getName()); + } + + @Test + void structuredRecord() { + var parameterProvider = + new StructuredParameterProvider() { + @Override + public StructuredParameter getParameter() { + var b = JSON.createObjectBuilder(); + b.add("traceId", "39f9a49a9567a8bd7087b708f8932550"); + b.add("spanId", "c7431b14630b633d"); + return () -> b; + } + }; + + var labelProvider = + new LabelProvider() { + @Override + public Collection<Label> getLabels() { + return List.of(Label.of("requestId", "123")); + } + }; + + var logRecord = makeStructuredRecord(); + + var formatter = new Formatter(List.of(parameterProvider), List.of(labelProvider)); + var formattingResult = formatter.format(logRecord); + assertLinesMatch( + List.of( + "\\{" + + "\"logging.googleapis.com/insertId\":\"123-456-789\"," + + "\"logging.googleapis.com/labels\":\\{\"a\":\"b\",\"requestId\":\"123\"\\}," + + "\"traceId\":\"39f9a49a9567a8bd7087b708f8932550\"," + + "\"spanId\":\"c7431b14630b633d\"," + + "\"one\":1," + + "\"two\":2.0," + + "\"yes\":true," + + "\"logging.googleapis.com/sourceLocation\":" + + "\\{\"file\":\"ReflectionUtils.java\"," + + "\"line\":\"\\d+\"," + + "\"function\":\"org.junit.platform.commons.util.ReflectionUtils.invokeMethod\"" + + "\\}," + + "\"message\":\"Hello, world!\"," + + "\"severity\":\"INFO\"," + + "\"timestamp\":\\{\"seconds\":\\d+,\"nanos\":\\d+\\}" + + "\\}\n"), + List.of(formattingResult)); + } + + static ExtLogRecord makeStructuredRecord() { + var logRecord = makeSimpleRecord(); + logRecord.setParameters( + new Object[] { + (StructuredParameter) + () -> JSON.createObjectBuilder().add("one", 1).add("two", 2.0).add("yes", true), + Label.of("a", "b"), + InsertId.of("123-456-789"), + }); + return logRecord; + } + + @Test + void massivelyStructuredRecord() { + var logRecord = makeMassivelyStructuredRecord(); + + var formatter = new Formatter(List.of(), List.of()); + var formattingResult = formatter.format(logRecord); + assertLinesMatch( + List.of( + "\\{" + + "\"int-0\":0,\"int-1\":1,\"int-2\":2,\"int-3\":3,\"int-4\":4,\"int-5\":5,\"int-6\":6,\"int-7\":7,\"int-8\":8,\"int-9\":9," + + "\"double-10\":10.0,\"double-11\":11.0,\"double-12\":12.0,\"double-13\":13.0,\"double-14\":14.0," + + "\"double-15\":15.0,\"double-16\":16.0,\"double-17\":17.0,\"double-18\":18.0,\"double-19\":19.0," + + "\"boolean-20\":true,\"boolean-21\":false,\"boolean-22\":true,\"boolean-23\":false,\"boolean-24\":true," + + "\"boolean-25\":false,\"boolean-26\":true,\"boolean-27\":false,\"boolean-28\":true,\"boolean-29\":false," + + "\"string-30\":\"30\",\"string-31\":\"31\",\"string-32\":\"32\",\"string-33\":\"33\",\"string-34\":\"34\"," + + "\"string-35\":\"35\",\"string-36\":\"36\",\"string-37\":\"37\",\"string-38\":\"38\",\"string-39\":\"39\"," + + "\"logging.googleapis.com/sourceLocation\":" + + "\\{\"file\":\"ReflectionUtils.java\"," + + "\"line\":\"\\d+\"," + + "\"function\":\"org.junit.platform.commons.util.ReflectionUtils.invokeMethod\"" + + "\\}," + + "\"message\":\"Hello, world!\"," + + "\"severity\":\"INFO\"," + + "\"timestamp\":\\{\"seconds\":\\d+,\"nanos\":\\d+\\}" + + "\\}\n"), + List.of(formattingResult)); + } + + static ExtLogRecord makeMassivelyStructuredRecord() { + var logRecord = FormatterTest.makeSimpleRecord(); + logRecord.setParameters( + new Object[] { + (StructuredParameter) + () -> { + var b = JSON.createObjectBuilder(); + for (int i = 0; i < 10; i++) { + b.add("int-" + i, i); + } + for (int i = 10; i < 20; i++) { + b.add("double-" + i, (double) i); + } + for (int i = 20; i < 30; i++) { + b.add("boolean-" + i, i % 2 == 0); + } + for (int i = 30; i < 40; i++) { + b.add("string-" + i, String.valueOf(i)); + } + return b; + } + }); + return logRecord; + } + + @Test + void nestedRecord() { + var logRecord = makeNestedRecord(); + + var formatter = new Formatter(List.of(), List.of()); + var formattingResult = formatter.format(logRecord); + assertLinesMatch( + List.of( + "\\{" + + "\"anObject\":\\{\"a\":\"b\\\\nc\\\\nd\\\\u0000\\\\u0001\\\\u0002e\"\\}," + + "\"anArray\":\\[1,2,3\\]," + + "\"anArrayOfObjects\":\\[" + + "\\{\"a\":1,\"b\":2\\}," + + "\\{\"b\":2,\"c\":3\\}," + + "\\{\"c\":3,\"d\":4\\}" + + "\\]," + + "\"logging.googleapis.com/sourceLocation\":" + + "\\{\"file\":\"ReflectionUtils.java\"," + + "\"line\":\"\\d+\"," + + "\"function\":\"org.junit.platform.commons.util.ReflectionUtils.invokeMethod\"" + + "\\}," + + "\"message\":\"Hello, world!\"," + + "\"severity\":\"INFO\"," + + "\"timestamp\":\\{\"seconds\":\\d+,\"nanos\":\\d+\\}" + + "\\}\n"), + List.of(formattingResult)); + } + + static ExtLogRecord makeNestedRecord() { + var logRecord = makeSimpleRecord(); + logRecord.setParameters( + new Object[] { + (StructuredParameter) + () -> + JSON.createObjectBuilder() + .add("anObject", JSON.createObjectBuilder().add("a", "b\nc\nd\0\1\2e")) + .add("anArray", JSON.createArrayBuilder().add(1).add(2).add(3)) + .add( + "anArrayOfObjects", + JSON.createArrayBuilder() + .add(JSON.createObjectBuilder().add("a", 1).add("b", 2)) + .add(JSON.createObjectBuilder().add("b", 2).add("c", 3)) + .add(JSON.createObjectBuilder().add("c", 3).add("d", 4))) + }); + return logRecord; + } +} |