parsedProps = new HashSet<>();
private final boolean thread;
@@ -57,18 +58,10 @@ public HelidonFormatter() {
}
}
- @Override
- public String format(LogRecord record) {
- String message = thread ? thread() : format;
- for (String parsedKey : parsedProps) {
- String value = HelidonMdc.get(parsedKey).orElse("");
- message = PATTERN_CACHE.computeIfAbsent(parsedKey, key -> Pattern.compile("%X\\{" + key + "}"))
- .matcher(message).replaceAll(value);
- }
- return formatRow(record, message);
- }
-
- private String thread() {
+ /*
+ Replace the thread pattern in the format with the current thread value
+ */
+ static String thread(String format) {
String currentThread = Thread.currentThread().toString();
String message = PATTERN_CACHE.computeIfAbsent(THREAD, key -> Pattern.compile("%X\\{" + THREAD + "}"))
.matcher(format).replaceAll(currentThread);
@@ -76,9 +69,11 @@ private String thread() {
return message;
}
- //Copied from SimpleFormatter
- private String formatRow(LogRecord record, String format) {
- ZonedDateTime zdt = ZonedDateTime.ofInstant(
+ /*
+ All parameters expected by simple formatter (and json formatter as well)
+ */
+ static Object[] parameters(LogRecord record, String formattedMessage) {
+ var timestamp = ZonedDateTime.ofInstant(
record.getInstant(), ZoneId.systemDefault());
String source;
if (record.getSourceClassName() != null) {
@@ -89,7 +84,7 @@ private String formatRow(LogRecord record, String format) {
} else {
source = record.getLoggerName();
}
- String message = formatMessage(record);
+
String throwable = "";
if (record.getThrown() != null) {
StringWriter sw = new StringWriter();
@@ -99,12 +94,31 @@ private String formatRow(LogRecord record, String format) {
pw.close();
throwable = sw.toString();
}
+
+ Object[] result = new Object[6];
+ result[0] = timestamp;
+ result[1] = source;
+ result[2] = record.getLoggerName();
+ result[3] = record.getLevel().getName();
+ result[4] = formattedMessage;
+ result[5] = throwable;
+
+ return result;
+ }
+
+ @Override
+ public String format(LogRecord record) {
+ String message = thread ? thread(format) : format;
+ for (String parsedKey : parsedProps) {
+ String value = HelidonMdc.get(parsedKey).orElse("");
+ message = PATTERN_CACHE.computeIfAbsent(parsedKey, key -> Pattern.compile("%X\\{" + key + "}"))
+ .matcher(message).replaceAll(value);
+ }
+ return formatRow(record, message);
+ }
+
+ private String formatRow(LogRecord record, String format) {
return String.format(format,
- zdt,
- source,
- record.getLoggerName(),
- record.getLevel().getLocalizedName(),
- message,
- throwable);
+ parameters(record, super.formatMessage(record)));
}
}
diff --git a/logging/jul/src/main/java/io/helidon/logging/jul/HelidonJsonFormatter.java b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonJsonFormatter.java
new file mode 100644
index 00000000000..868c8c1b605
--- /dev/null
+++ b/logging/jul/src/main/java/io/helidon/logging/jul/HelidonJsonFormatter.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.logging.jul;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Formatter;
+import java.util.logging.LogManager;
+import java.util.logging.LogRecord;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import io.helidon.logging.common.HelidonMdc;
+import io.helidon.metadata.hson.Hson;
+
+import static io.helidon.logging.jul.HelidonFormatter.JUL_FORMAT_PROP_KEY;
+import static io.helidon.logging.jul.HelidonFormatter.THREAD_TOKEN;
+
+/**
+ * A {@link java.util.logging.Formatter} that stores each log record as a single-line JSON value.
+ * It also replaces all occurrences of MDC tags like {@code %X{value}} with specific values,
+ * and supports replacement of {@code "!thread!"} with the current thread.
+ *
+ * The configuration should be done through property {@value #JSON_FORMAT_PROP_KEY}, that provides comma separated list of name to
+ * fields to log (references are the same as you would use in a {@link java.util.logging.SimpleFormatter}).
+ *
+ * The configuration falls back to property {@value io.helidon.logging.jul.HelidonFormatter#JUL_FORMAT_PROP_KEY},
+ * and analyzes it to provide a "guessed" JSON structure.
+ *
+ * Example (and also the default format):
+ * {@value #DEFAULT_FORMAT}
+ */
+public class HelidonJsonFormatter extends Formatter {
+ static final String DEFAULT_FORMAT = "ts:%1$tQ,date:%1$tY.%1$tm.%1$td,time:%1$tH:%1$tM:%1$tS.%1$tL,level:%4$s,message:%5$s,"
+ + "exception:%6$s,thread:!thread!,logger:%3$s";
+
+ private static final String JSON_FORMAT_PROP_KEY = "io.helidon.logging.jul.HelidonJsonFormatter.fields";
+
+ // formats we understand
+ private static final String EPOCH_MILLIS_FORMAT = "%1$tQ";
+ private static final String YEAR_FORMAT = "%1$tY";
+ private static final String MONTH_FORMAT = "%1$tm";
+ private static final String DAY_FORMAT = "%1$td";
+ private static final String HOUR_FORMAT = "%1$tH";
+ private static final String MINUTE_FORMAT = "%1$tM";
+ private static final String SECOND_FORMAT = "%1$tS";
+ private static final String SOURCE_FORMAT = "%2$s";
+ private static final String LOGGER_FORMAT = "%3$s";
+ private static final String LEVEL_FORMAT = "%4$s";
+ private static final String MESSAGE_FORMAT = "%5$s";
+ private static final String EXCEPTION_FORMAT = "%6$s";
+
+ private final List formatters;
+
+ /**
+ * Create new instance of the {@link io.helidon.logging.jul.HelidonJsonFormatter}.
+ */
+ public HelidonJsonFormatter() {
+ String jsonFormat = LogManager.getLogManager().getProperty(JSON_FORMAT_PROP_KEY);
+ String julFormat = LogManager.getLogManager().getProperty(JUL_FORMAT_PROP_KEY);
+ if (jsonFormat == null && julFormat == null) {
+ jsonFormat = DEFAULT_FORMAT;
+ }
+ if (jsonFormat == null) {
+ this.formatters = guessFromSimpleFormat(julFormat);
+ } else {
+ this.formatters = fromJsonFormat(jsonFormat);
+ }
+ }
+
+ HelidonJsonFormatter(String format, boolean jsonFormat) {
+ this.formatters = jsonFormat ? fromJsonFormat(format) : guessFromSimpleFormat(format);
+ }
+
+ @Override
+ public String format(LogRecord record) {
+ var builder = Hson.Struct.builder();
+ var params = HelidonFormatter.parameters(record, super.formatMessage(record));
+
+ formatters.forEach(formatter -> formatter.update(builder, params));
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ builder.build()
+ .write(pw);
+ pw.println();
+ pw.close();
+ return sw.toString();
+ }
+
+ private List fromJsonFormat(String jsonFormat) {
+ return Stream.of(jsonFormat.split(","))
+ .map(it -> new ValueFormatter(Field.create(jsonFormat, it)))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ private List guessFromSimpleFormat(String julFormat) {
+ List result = new ArrayList<>();
+ Map counters = new HashMap<>();
+ String usedFormat = julFormat.replaceAll("%n", " ");
+ // spaces expected to separate "blocks"
+ for (String block : usedFormat.split(" ")) {
+ if (block.isBlank()) {
+ continue;
+ }
+ // now lets do some "magic"
+ if (block.contains(YEAR_FORMAT) && block.contains(MONTH_FORMAT) && block.contains(DAY_FORMAT)) {
+ if (block.contains(HOUR_FORMAT)) {
+ // full timestamp
+ result.add(new ValueFormatter(new Field(name(counters, "timestamp"), block)));
+ } else {
+ // date only
+ result.add(new ValueFormatter(new Field(name(counters, "date"), block)));
+ }
+ continue;
+ }
+ if (block.contains(HOUR_FORMAT) && block.contains(MINUTE_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "time"), block)));
+ continue;
+ }
+
+ // now only create sections if it only contains one parameter
+ if (block.contains(SOURCE_FORMAT) && block.length() < 7) {
+ result.add(new ValueFormatter(new Field(name(counters, "source"), block)));
+ continue;
+ }
+ if (block.contains(LOGGER_FORMAT) && block.length() < 7) {
+ result.add(new ValueFormatter(new Field(name(counters, "logger"), block)));
+ continue;
+ }
+ if (block.contains(LEVEL_FORMAT) && block.length() < 7) {
+ result.add(new ValueFormatter(new Field(name(counters, "level"), block)));
+ continue;
+ }
+ if (block.contains(MESSAGE_FORMAT) && block.length() < 7) {
+ result.add(new ValueFormatter(new Field(name(counters, "message"), block)));
+ continue;
+ }
+ if (block.contains(EXCEPTION_FORMAT) && block.length() < 7) {
+ result.add(new ValueFormatter(new Field(name(counters, "exception"), block)));
+ continue;
+ }
+ if (block.contains(THREAD_TOKEN) && block.length() < THREAD_TOKEN.length() + 3) {
+ result.add(new ValueFormatter(new Field(name(counters, "thread"), block)));
+ continue;
+ }
+
+ // now let's extract the parts
+ if (block.contains(EPOCH_MILLIS_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "ts"), EPOCH_MILLIS_FORMAT)));
+ }
+ if (block.contains(YEAR_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "year"), YEAR_FORMAT)));
+ }
+ if (block.contains(MONTH_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "month"), MONTH_FORMAT)));
+ }
+ if (block.contains(DAY_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "day"), DAY_FORMAT)));
+ }
+ if (block.contains(HOUR_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "hour"), HOUR_FORMAT)));
+ }
+ if (block.contains(MINUTE_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "minute"), MINUTE_FORMAT)));
+ }
+ if (block.contains(SECOND_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "second"), SECOND_FORMAT)));
+ }
+ if (block.contains(SOURCE_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "source"), SOURCE_FORMAT)));
+ }
+ if (block.contains(LOGGER_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "logger"), LOGGER_FORMAT)));
+ }
+ if (block.contains(LEVEL_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "level"), LEVEL_FORMAT)));
+ }
+ if (block.contains(MESSAGE_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "message"), MESSAGE_FORMAT)));
+ }
+ if (block.contains(EXCEPTION_FORMAT)) {
+ result.add(new ValueFormatter(new Field(name(counters, "exception"), EXCEPTION_FORMAT)));
+ }
+ if (block.contains(THREAD_TOKEN) || block.contains("%X{" + HelidonFormatter.THREAD + "}")) {
+ result.add(new ValueFormatter(new Field(name(counters, "thread"), THREAD_TOKEN)));
+ }
+
+ // MDC support
+ Matcher matcher = HelidonFormatter.X_VALUE.matcher(usedFormat);
+ while (matcher.find()) {
+ String name = matcher.group(2);
+ if (!name.equals(HelidonFormatter.THREAD)) {
+ result.add(new ValueFormatter(new Field(name(counters, "X." + name), "%X{" + name + "}")));
+ }
+ }
+ }
+ return result;
+ }
+
+ private String name(Map counters, String name) {
+ if (counters.containsKey(name)) {
+ return name + "_" + counters.get(name).incrementAndGet();
+ } else {
+ counters.put(name, new AtomicInteger());
+ return name;
+ }
+ }
+
+ private static class ValueFormatter {
+ private final Set parsedProps = new HashSet<>();
+ private final String jsonName;
+ private final String format;
+ private final boolean thread;
+
+ private ValueFormatter(Field field) {
+ this.jsonName = field.name();
+ this.format = field.format();
+
+ this.thread = this.format.contains(THREAD_TOKEN) || this.format.contains("%X{" + HelidonFormatter.THREAD + "}");
+ Matcher matcher = HelidonFormatter.X_VALUE.matcher(this.format);
+ while (matcher.find()) {
+ parsedProps.add(matcher.group(2));
+ }
+ }
+
+ private void update(Hson.Struct.Builder jsonBuilder, Object... parameters) {
+
+ String message = thread ? HelidonFormatter.thread(format) : format;
+ for (String parsedKey : parsedProps) {
+ String value = HelidonMdc.get(parsedKey).orElse("");
+ message = HelidonFormatter.PATTERN_CACHE
+ .computeIfAbsent(parsedKey, key -> Pattern.compile("%X\\{" + key + "}"))
+ .matcher(message)
+ .replaceAll(value);
+ }
+ String formattedValue = String.format(message, parameters);
+ if (!formattedValue.isBlank()) {
+ jsonBuilder.set(jsonName, formattedValue);
+ }
+ }
+ }
+
+ private record Field(String name, String format) {
+ private static Field create(String format, String field) {
+ int index = field.indexOf(':');
+ if (index == -1) {
+ throw new IllegalArgumentException("Invalid format definition for " + HelidonJsonFormatter.class.getSimpleName()
+ + ", each field must have field name followed by a colon with field "
+ + "value,"
+ + " such as 'message:%5$s', but got: '" + field + "'. "
+ + "Full format: " + format);
+ }
+ return new Field(field.substring(0, index), field.substring(index + 1));
+ }
+ }
+}
diff --git a/logging/jul/src/main/java/module-info.java b/logging/jul/src/main/java/module-info.java
index e17f91fd07c..36facfd2116 100644
--- a/logging/jul/src/main/java/module-info.java
+++ b/logging/jul/src/main/java/module-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2020, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,12 +20,13 @@
module io.helidon.logging.jul {
requires io.helidon.common;
+ requires io.helidon.metadata.hson;
requires java.logging;
requires transitive io.helidon.common.context;
requires transitive io.helidon.logging.common;
- exports io.helidon.logging.jul;
+ exports io.helidon.logging.jul;
provides io.helidon.common.context.spi.DataPropagationProvider with io.helidon.logging.jul.JulMdcPropagator;
provides io.helidon.logging.common.spi.MdcProvider with io.helidon.logging.jul.JulMdcProvider;
diff --git a/logging/jul/src/test/java/io/helidon/logging/jul/JulJsonTest.java b/logging/jul/src/test/java/io/helidon/logging/jul/JulJsonTest.java
new file mode 100644
index 00000000000..9183bcb66b1
--- /dev/null
+++ b/logging/jul/src/test/java/io/helidon/logging/jul/JulJsonTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.logging.jul;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+import io.helidon.logging.common.HelidonMdc;
+import io.helidon.metadata.hson.Hson;
+
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.common.testing.junit5.OptionalMatcher.optionalEmpty;
+import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class JulJsonTest {
+ private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy.MM.dd");
+ private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
+ private static final Instant INSTANT = Instant.parse("2007-12-03T10:15:30.00Z");
+ private static final ZonedDateTime DATE_TIME = INSTANT.atZone(ZoneId.systemDefault());
+
+ private static final LogRecord LOG_RECORD;
+ private static final LogRecord LOG_RECORD_WITH_EXCEPTION;
+
+ static {
+ LogRecord record = new LogRecord(Level.WARNING, "Message content");
+ record.setInstant(INSTANT);
+ record.setLoggerName("LoggerName");
+ record.setThrown(new IllegalStateException("Thrown"));
+ record.setSourceClassName("io.helidon.logging.jul.JulJsonTest");
+ record.setSourceMethodName("testJsonDefaultFormat");
+ LOG_RECORD_WITH_EXCEPTION = record;
+
+ record = new LogRecord(Level.WARNING, "Message content");
+ record.setInstant(INSTANT);
+ record.setLoggerName("LoggerName");
+ record.setSourceClassName("io.helidon.logging.jul.JulJsonTest");
+ record.setSourceMethodName("testJsonDefaultFormat");
+ LOG_RECORD = record;
+ }
+
+ @Test
+ public void testJsonDefaultFormat() throws InterruptedException {
+ HelidonJsonFormatter formatter = new HelidonJsonFormatter(HelidonJsonFormatter.DEFAULT_FORMAT, true);
+
+ String threadName = "logging-jul-test-thread";
+
+ AtomicReference resultReference = new AtomicReference<>();
+ Thread.ofVirtual()
+ .name(threadName)
+ .start(() -> {
+ resultReference.set(formatter.format(LOG_RECORD));
+ })
+ .join(Duration.ofSeconds(5));
+
+ String result = resultReference.get();
+ var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8)))
+ .asStruct();
+
+ assertThat(json.stringValue("ts"), optionalValue(is(String.valueOf(INSTANT.toEpochMilli()))));
+ assertThat(json.stringValue("date"), optionalValue(is(DATE_FORMAT.format(DATE_TIME))));
+ assertThat(json.stringValue("time"), optionalValue(is(TIME_FORMAT.format(DATE_TIME))));
+ assertThat(json.stringValue("level"), optionalValue(is("WARNING")));
+ assertThat(json.stringValue("message"), optionalValue(is("Message content")));
+ assertThat(json.stringValue("exception"), optionalEmpty());
+ assertThat(json.stringValue("logger"), optionalValue(is("LoggerName")));
+ assertThat(json.stringValue("thread"), optionalValue(containsString(threadName)));
+ }
+
+ @Test
+ public void testJsonWithExceptionDefaultFormat() throws InterruptedException {
+ HelidonJsonFormatter formatter = new HelidonJsonFormatter(HelidonJsonFormatter.DEFAULT_FORMAT, true);
+
+ String threadName = "logging-jul-test-thread";
+
+ AtomicReference resultReference = new AtomicReference<>();
+ Thread.ofVirtual()
+ .name(threadName)
+ .start(() -> {
+ resultReference.set(formatter.format(LOG_RECORD_WITH_EXCEPTION));
+ })
+ .join(Duration.ofSeconds(1000));
+
+ String result = resultReference.get();
+ var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8)))
+ .asStruct();
+
+ assertThat(json.stringValue("date"), optionalValue(is(DATE_FORMAT.format(DATE_TIME))));
+ assertThat(json.stringValue("time"), optionalValue(is(TIME_FORMAT.format(DATE_TIME))));
+ assertThat(json.stringValue("level"), optionalValue(is("WARNING")));
+ assertThat(json.stringValue("message"), optionalValue(is("Message content")));
+ assertThat(json.stringValue("exception"), optionalValue(containsString("Thrown")));
+ assertThat(json.stringValue("logger"), optionalValue(is("LoggerName")));
+ assertThat(json.stringValue("thread"), optionalValue(containsString(threadName)));
+ }
+
+ @Test
+ public void testJsonWithExceptionSimpleFormat() throws InterruptedException {
+ String simpleFormat = "%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$s %3$s !thread!: %5$s%6$s %X{test}%n";
+ HelidonJsonFormatter formatter = new HelidonJsonFormatter(simpleFormat, false);
+
+ String threadName = "logging-jul-test-thread";
+
+ AtomicReference resultReference = new AtomicReference<>();
+ Thread.ofVirtual()
+ .name(threadName)
+ .start(() -> {
+ HelidonMdc.set("test", "testValue");
+ resultReference.set(formatter.format(LOG_RECORD_WITH_EXCEPTION));
+ })
+ .join(Duration.ofSeconds(1000));
+
+ String result = resultReference.get();
+ var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8)))
+ .asStruct();
+
+ assertThat(json.stringValue("date"), optionalValue(is(DATE_FORMAT.format(DATE_TIME))));
+ assertThat(json.stringValue("time"), optionalValue(is(TIME_FORMAT.format(DATE_TIME))));
+ assertThat(json.stringValue("logger"), optionalValue(is("LoggerName")));
+ assertThat(json.stringValue("level"), optionalValue(is("WARNING")));
+ assertThat(json.stringValue("thread"), optionalValue(containsString(threadName)));
+ assertThat(json.stringValue("message"), optionalValue(is("Message content")));
+ assertThat(json.stringValue("exception"), optionalValue(containsString("Thrown")));
+ assertThat(json.stringValue("X.test"), optionalValue(containsString("testValue")));
+ }
+
+ @Test
+ public void testJsonWithExceptionCustomFormat() throws InterruptedException {
+ String format = "timestamp:%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL,loglevel:%4$s,test:%X{test},msg:%5$s,source:%2$s";
+ HelidonJsonFormatter formatter = new HelidonJsonFormatter(format, true);
+
+ String threadName = "logging-jul-test-thread";
+
+ AtomicReference resultReference = new AtomicReference<>();
+ Thread.ofVirtual()
+ .name(threadName)
+ .start(() -> {
+ HelidonMdc.set("test", "testValue");
+ resultReference.set(formatter.format(LOG_RECORD_WITH_EXCEPTION));
+ })
+ .join(Duration.ofSeconds(1000));
+
+ String result = resultReference.get();
+ var json = Hson.parse(new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8)))
+ .asStruct();
+
+ assertThat(json.stringValue("timestamp"), optionalValue(is(DATE_FORMAT.format(DATE_TIME) + " "
+ + TIME_FORMAT.format(DATE_TIME))));
+ assertThat(json.stringValue("loglevel"), optionalValue(is("WARNING")));
+ assertThat(json.stringValue("msg"), optionalValue(is("Message content")));
+ assertThat(json.stringValue("test"), optionalValue(is("testValue")));
+ assertThat(json.stringValue("source"), optionalValue(is("io.helidon.logging.jul.JulJsonTest"
+ + " testJsonDefaultFormat")));
+ }
+}
+
diff --git a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java
index 6c94cb8e497..54095e99492 100644
--- a/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java
+++ b/metadata/hson/src/main/java/io/helidon/metadata/hson/HsonValues.java
@@ -76,13 +76,15 @@ private String quote(String value) {
}
private String escape(String string) {
- return string.replaceAll("\n", "\\\\n")
- .replaceAll("\"", "\\\\\"")
- .replaceAll("\t", "\\\\\t")
- .replaceAll("\r", "\\\\\r")
- // replace two backslashes with four backslashes
- .replaceAll("\\\\\\\\", "\\\\\\\\\\\\\\\\")
- .replaceAll("\f", "\\\\\f");
+ String result = string.replaceAll("\n", "\\\\n");
+
+ result = result.replaceAll("\"", "\\\\\"");
+ result = result.replaceAll("\t", "\\\\t");
+ result = result.replaceAll("\r", "\\\\r");
+ // replace two backslashes with four backslashes
+ result = result.replaceAll("\\\\\\\\", "\\\\\\\\\\\\\\\\");
+ result = result.replaceAll("\f", "\\\\f");
+ return result;
}
}
diff --git a/tests/integration/packaging/se-1/src/main/resources/logging.properties b/tests/integration/packaging/se-1/src/main/resources/logging.properties
index 48878bf93d9..c19a3babf9c 100644
--- a/tests/integration/packaging/se-1/src/main/resources/logging.properties
+++ b/tests/integration/packaging/se-1/src/main/resources/logging.properties
@@ -19,18 +19,11 @@
# Send messages to the console
handlers=io.helidon.logging.jul.HelidonConsoleHandler
-
-# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
-java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n
+# Use JSON formatter with default format
+io.helidon.logging.jul.HelidonConsoleHandler.formatter=io.helidon.logging.jul.HelidonJsonFormatter
# Global logging level. Can be overridden by specific loggers
.level=INFO
-# Component specific log levels
-#io.helidon.webserver.level=INFO
-#io.helidon.config.level=INFO
-#io.helidon.security.level=INFO
-#io.helidon.common.level=INFO
-
io.helidon.webserver.staticcontent.ClassPathContentHandler.level=FINEST
io.helidon.webserver.staticcontent.FileSystemContentHandler.level=FINEST