summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/pom.xml150
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Formatter.java81
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/InsertId.java83
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/KeyValueParameter.java25
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/Label.java3
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LabelProvider.java5
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/LogEntry.java251
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/ProviderContext.java5
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/StructuredParameterProvider.java6
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/logmanager/package-info.java3
-rw-r--r--core/src/main/java/eu/mulk/quarkus/googlecloud/jsonlogging/package-info.java7
-rw-r--r--core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterBenchmark.java60
-rw-r--r--core/src/test/java/eu/mulk/quarkus/googlecloud/jsonlogging/FormatterTest.java204
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;
+ }
+}