diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogSyslogFormatBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogSyslogFormatBuildItem.java new file mode 100644 index 0000000000000..f20812149a246 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LogSyslogFormatBuildItem.java @@ -0,0 +1,36 @@ +package io.quarkus.deployment.builditem; + +import java.util.Optional; +import java.util.logging.Formatter; + +import org.wildfly.common.Assert; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * The syslog format build item. Producing this item will cause the logging subsystem to disregard its + * syslog logging formatting configuration and use the formatter provided instead. If multiple formatters + * are enabled at runtime, a warning message is printed and only one is used. + */ +public final class LogSyslogFormatBuildItem extends MultiBuildItem { + private final RuntimeValue> formatterValue; + + /** + * Construct a new instance. + * + * @param formatterValue the optional formatter runtime value to use (must not be {@code null}) + */ + public LogSyslogFormatBuildItem(final RuntimeValue> formatterValue) { + this.formatterValue = Assert.checkNotNullParam("formatterValue", formatterValue); + } + + /** + * Get the formatter value. + * + * @return the formatter value + */ + public RuntimeValue> getFormatterValue() { + return formatterValue; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index e6b3d19bea0ea..38882e3ec7950 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -65,6 +65,7 @@ import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; import io.quarkus.deployment.builditem.LogFileFormatBuildItem; import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.deployment.builditem.LogSyslogFormatBuildItem; import io.quarkus.deployment.builditem.NamedLogHandlersBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.ShutdownListenerBuildItem; @@ -231,6 +232,7 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe List namedHandlerBuildItems, List consoleFormatItems, List fileFormatItems, + List syslogFormatItems, Optional possibleBannerBuildItem, List logStreamBuildItems, BuildProducer shutdownListenerBuildItemBuildProducer, @@ -271,8 +273,13 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe alwaysEnableLogStream = true; } + List>> possibleConsoleFormatters = consoleFormatItems.stream() + .map(LogConsoleFormatBuildItem::getFormatterValue).collect(Collectors.toList()); List>> possibleFileFormatters = fileFormatItems.stream() .map(LogFileFormatBuildItem::getFormatterValue).collect(Collectors.toList()); + List>> possibleSyslogFormatters = syslogFormatItems.stream() + .map(LogSyslogFormatBuildItem::getFormatterValue).collect(Collectors.toList()); + context.registerSubstitution(InheritableLevel.ActualLevel.class, String.class, InheritableLevel.Substitution.class); context.registerSubstitution(InheritableLevel.Inherited.class, String.class, InheritableLevel.Substitution.class); @@ -289,9 +296,7 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe recorder.initializeLogging(log, buildLog, discoveredLogComponents, categoryMinLevelDefaults.content, alwaysEnableLogStream, wsDevUiLogHandler, streamingDevUiLogHandler, handlers, namedHandlers, - consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue) - .collect(Collectors.toList()), - possibleFileFormatters, + possibleConsoleFormatters, possibleFileFormatters, possibleSyslogFormatters, possibleSupplier, launchModeBuildItem.getLaunchMode(), true))); LogConfig logConfig = new LogConfig(); ConfigInstantiator.handleObject(logConfig); diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 366bf49910516..69b8cba631cb2 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -82,6 +82,7 @@ public static void handleFailedStart(RuntimeValue>> ba Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), banner, LaunchMode.DEVELOPMENT, false); } @@ -95,6 +96,7 @@ public ShutdownListener initializeLogging(LogConfig config, LogBuildTimeConfig b final List>> additionalNamedHandlers, final List>> possibleConsoleFormatters, final List>> possibleFileFormatters, + final List>> possibleSyslogFormatters, final RuntimeValue>> possibleBannerSupplier, LaunchMode launchMode, boolean validateFilters) { @@ -173,7 +175,7 @@ public void close() throws SecurityException { if (config.syslog.enable) { final Handler syslogHandler = configureSyslogHandler(config.syslog, errorManager, cleanupFiler, - namedFilters, validateFilters); + namedFilters, possibleSyslogFormatters, validateFilters); if (syslogHandler != null) { handlers.add(syslogHandler); } @@ -213,7 +215,8 @@ public void close() throws SecurityException { Map namedHandlers = shouldCreateNamedHandlers(config, additionalNamedHandlers) ? createNamedHandlers(config, consoleRuntimeConfig.getValue(), additionalNamedHandlers, - possibleConsoleFormatters, possibleFileFormatters, errorManager, cleanupFiler, namedFilters, launchMode, + possibleConsoleFormatters, possibleFileFormatters, possibleSyslogFormatters, + errorManager, cleanupFiler, namedFilters, launchMode, shutdownNotifier, false) : Collections.emptyMap(); if (!categories.isEmpty()) { @@ -323,7 +326,7 @@ public static void initializeBuildTimeLogging(LogConfig config, LogBuildTimeConf } Map namedHandlers = createNamedHandlers(config, consoleConfig, Collections.emptyList(), - Collections.emptyList(), Collections.emptyList(), errorManager, logCleanupFilter, + Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), errorManager, logCleanupFilter, Collections.emptyMap(), launchMode, dummy, true); for (Map.Entry entry : categories.entrySet()) { @@ -409,6 +412,7 @@ private static Map createNamedHandlers(LogConfig config, Consol List>> additionalNamedHandlers, List>> possibleConsoleFormatters, List>> possibleFileFormatters, + List>> possibleSyslogFormatters, ErrorManager errorManager, LogCleanupFilter cleanupFilter, Map namedFilters, LaunchMode launchMode, ShutdownNotifier shutdownHandler, boolean validateFilters) { @@ -438,7 +442,7 @@ private static Map createNamedHandlers(LogConfig config, Consol continue; } final Handler syslogHandler = configureSyslogHandler(namedSyslogConfig, errorManager, cleanupFilter, - namedFilters, validateFilters); + namedFilters, possibleSyslogFormatters, validateFilters); if (syslogHandler != null) { addToNamedHandlers(namedHandlers, syslogHandler, sysLogConfigEntry.getKey()); } @@ -692,7 +696,9 @@ private static void applyFilter(boolean validateFilters, ErrorManager errorManag private static Handler configureSyslogHandler(final SyslogConfig config, final ErrorManager errorManager, final LogCleanupFilter logCleanupFilter, - final Map namedFilters, final boolean validateFilters) { + final Map namedFilters, + final List>> possibleSyslogFormatters, + final boolean validateFilters) { try { final SyslogHandler handler = new SyslogHandler(config.endpoint.getHostString(), config.endpoint.getPort()); handler.setAppName(config.appName.orElse(getProcessName())); @@ -704,11 +710,32 @@ private static Handler configureSyslogHandler(final SyslogConfig config, final E handler.setTruncate(config.truncate); handler.setUseCountingFraming(config.useCountingFraming); handler.setLevel(config.level); - final PatternFormatter formatter = new PatternFormatter(config.format); + + Formatter formatter = null; + boolean formatterWarning = false; + for (RuntimeValue> value : possibleSyslogFormatters) { + if (formatter != null) { + formatterWarning = true; + } + final Optional val = value.getValue(); + if (val.isPresent()) { + formatter = val.get(); + } + } + if (formatter == null) { + formatter = new PatternFormatter(config.format); + } handler.setFormatter(formatter); + handler.setErrorManager(errorManager); handler.setFilter(logCleanupFilter); applyFilter(validateFilters, errorManager, logCleanupFilter, config.filter, namedFilters, handler); + + if (formatterWarning) { + handler.getErrorManager().error("Multiple syslog formatters were activated", null, + ErrorManager.GENERIC_FAILURE); + } + if (config.async.enable) { return createAsyncHandler(config.async, config.level, handler); } diff --git a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java index b313ae8d92465..47ef0ba81a332 100644 --- a/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java +++ b/extensions/logging-json/deployment/src/main/java/io/quarkus/logging/json/deployment/LoggingJsonProcessor.java @@ -5,6 +5,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.LogConsoleFormatBuildItem; import io.quarkus.deployment.builditem.LogFileFormatBuildItem; +import io.quarkus.deployment.builditem.LogSyslogFormatBuildItem; import io.quarkus.logging.json.runtime.JsonLogConfig; import io.quarkus.logging.json.runtime.LoggingJsonRecorder; @@ -21,4 +22,10 @@ public LogConsoleFormatBuildItem setUpConsoleFormatter(LoggingJsonRecorder recor public LogFileFormatBuildItem setUpFileFormatter(LoggingJsonRecorder recorder, JsonLogConfig config) { return new LogFileFormatBuildItem(recorder.initializeFileJsonLogging(config)); } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + public LogSyslogFormatBuildItem setUpSyslogFormatter(LoggingJsonRecorder recorder, JsonLogConfig config) { + return new LogSyslogFormatBuildItem(recorder.initializeSyslogJsonLogging(config)); + } } diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SyslogJsonFormatterCustomConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SyslogJsonFormatterCustomConfigTest.java new file mode 100644 index 0000000000000..809e036e3f78c --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SyslogJsonFormatterCustomConfigTest.java @@ -0,0 +1,72 @@ +package io.quarkus.logging.json; + +import static io.quarkus.logging.json.SyslogJsonFormatterDefaultConfigTest.getJsonFormatter; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.assertj.core.api.Assertions; +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.logging.json.runtime.AdditionalFieldConfig; +import io.quarkus.logging.json.runtime.JsonFormatter; +import io.quarkus.test.QuarkusUnitTest; + +public class SyslogJsonFormatterCustomConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(SyslogJsonFormatterDefaultConfigTest.class)) + .withConfigurationResource("application-syslog-json-formatter-custom.properties"); + + @Test + public void jsonFormatterCustomConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isTrue(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo("Value(DayOfMonth)' 'Text(MonthOfYear,SHORT)' 'Value(Year,4,19,EXCEEDS_PAD)"); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.of("UTC+05:00")); + assertThat(jsonFormatter.getExceptionOutputType()) + .isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED_AND_FORMATTED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n;"); + assertThat(jsonFormatter.isPrintDetails()).isTrue(); + assertThat(jsonFormatter.getExcludedKeys()).containsExactly("timestamp", "sequence"); + assertThat(jsonFormatter.getAdditionalFields().size()).isEqualTo(2); + assertThat(jsonFormatter.getAdditionalFields().containsKey("foo")).isTrue(); + assertThat(jsonFormatter.getAdditionalFields().get("foo").type).isEqualTo(AdditionalFieldConfig.Type.INT); + assertThat(jsonFormatter.getAdditionalFields().get("foo").value).isEqualTo("42"); + assertThat(jsonFormatter.getAdditionalFields().containsKey("bar")).isTrue(); + assertThat(jsonFormatter.getAdditionalFields().get("bar").type).isEqualTo(AdditionalFieldConfig.Type.STRING); + assertThat(jsonFormatter.getAdditionalFields().get("bar").value).isEqualTo("baz"); + } + + @Test + public void jsonFormatterOutputTest() throws Exception { + JsonFormatter jsonFormatter = getJsonFormatter(); + String line = jsonFormatter.format(new LogRecord(Level.INFO, "Hello, World!")); + + JsonNode node = new ObjectMapper().readTree(line); + // "level" has been renamed to HEY + Assertions.assertThat(node.has("level")).isFalse(); + Assertions.assertThat(node.has("HEY")).isTrue(); + Assertions.assertThat(node.get("HEY").asText()).isEqualTo("INFO"); + + // excluded fields + Assertions.assertThat(node.has("timestamp")).isFalse(); + Assertions.assertThat(node.has("sequence")).isFalse(); + + // additional fields + Assertions.assertThat(node.has("foo")).isTrue(); + Assertions.assertThat(node.get("foo").asInt()).isEqualTo(42); + Assertions.assertThat(node.has("bar")).isTrue(); + Assertions.assertThat(node.get("bar").asText()).isEqualTo("baz"); + Assertions.assertThat(node.get("message").asText()).isEqualTo("Hello, World!"); + } +} diff --git a/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SyslogJsonFormatterDefaultConfigTest.java b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SyslogJsonFormatterDefaultConfigTest.java new file mode 100644 index 0000000000000..37e19c1814f58 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/java/io/quarkus/logging/json/SyslogJsonFormatterDefaultConfigTest.java @@ -0,0 +1,62 @@ +package io.quarkus.logging.json; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import org.jboss.logmanager.formatters.StructuredFormatter; +import org.jboss.logmanager.handlers.SyslogHandler; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.bootstrap.logging.QuarkusDelayedHandler; +import io.quarkus.logging.json.runtime.JsonFormatter; +import io.quarkus.test.QuarkusUnitTest; + +public class SyslogJsonFormatterDefaultConfigTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withConfigurationResource("application-syslog-json-formatter-default.properties"); + + @Test + public void jsonFormatterDefaultConfigurationTest() { + JsonFormatter jsonFormatter = getJsonFormatter(); + assertThat(jsonFormatter.isPrettyPrint()).isFalse(); + assertThat(jsonFormatter.getDateTimeFormatter().toString()) + .isEqualTo(DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.systemDefault()).toString()); + assertThat(jsonFormatter.getDateTimeFormatter().getZone()).isEqualTo(ZoneId.systemDefault()); + assertThat(jsonFormatter.getExceptionOutputType()).isEqualTo(StructuredFormatter.ExceptionOutputType.DETAILED); + assertThat(jsonFormatter.getRecordDelimiter()).isEqualTo("\n"); + assertThat(jsonFormatter.isPrintDetails()).isFalse(); + assertThat(jsonFormatter.getExcludedKeys()).isEmpty(); + assertThat(jsonFormatter.getAdditionalFields().entrySet()).isEmpty(); + } + + public static JsonFormatter getJsonFormatter() { + LogManager logManager = LogManager.getLogManager(); + assertThat(logManager).isInstanceOf(org.jboss.logmanager.LogManager.class); + + QuarkusDelayedHandler delayedHandler = InitialConfigurator.DELAYED_HANDLER; + assertThat(Logger.getLogger("").getHandlers()).contains(delayedHandler); + assertThat(delayedHandler.getLevel()).isEqualTo(Level.ALL); + + Handler handler = Arrays.stream(delayedHandler.getHandlers()) + .filter(h -> (h instanceof SyslogHandler)) + .findFirst().orElse(null); + assertThat(handler).isNotNull(); + assertThat(handler.getLevel()).isEqualTo(Level.WARNING); + + Formatter formatter = handler.getFormatter(); + assertThat(formatter).isInstanceOf(JsonFormatter.class); + return (JsonFormatter) formatter; + } +} diff --git a/extensions/logging-json/deployment/src/test/resources/application-syslog-json-formatter-custom.properties b/extensions/logging-json/deployment/src/test/resources/application-syslog-json-formatter-custom.properties new file mode 100644 index 0000000000000..cbc205e1a3e79 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-syslog-json-formatter-custom.properties @@ -0,0 +1,16 @@ +quarkus.log.level=INFO +quarkus.log.syslog.enable=true +quarkus.log.syslog.level=WARNING +quarkus.log.syslog.json=true +quarkus.log.syslog.json.pretty-print=true +quarkus.log.syslog.json.date-format=d MMM uuuu +quarkus.log.syslog.json.record-delimiter=\n; +quarkus.log.syslog.json.zone-id=UTC+05:00 +quarkus.log.syslog.json.exception-output-type=DETAILED_AND_FORMATTED +quarkus.log.syslog.json.print-details=true +quarkus.log.syslog.json.key-overrides=level=HEY +quarkus.log.syslog.json.excluded-keys=timestamp,sequence +quarkus.log.syslog.json.additional-field.foo.value=42 +quarkus.log.syslog.json.additional-field.foo.type=int +quarkus.log.syslog.json.additional-field.bar.value=baz +quarkus.log.syslog.json.additional-field.bar.type=string diff --git a/extensions/logging-json/deployment/src/test/resources/application-syslog-json-formatter-default.properties b/extensions/logging-json/deployment/src/test/resources/application-syslog-json-formatter-default.properties new file mode 100644 index 0000000000000..fc2a2ee6fd2a9 --- /dev/null +++ b/extensions/logging-json/deployment/src/test/resources/application-syslog-json-formatter-default.properties @@ -0,0 +1,5 @@ +quarkus.log.level=INFO +quarkus.log.syslog.enable=true +quarkus.log.syslog.level=WARNING +quarkus.log.syslog.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.syslog.json=true diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java index ed0ac718c3dd8..f088bc614cd87 100644 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/JsonLogConfig.java @@ -33,6 +33,13 @@ public class JsonLogConfig { @ConfigItem(name = "file.json") JsonConfig fileJson; + /** + * Syslog logging. + */ + @ConfigDocSection + @ConfigItem(name = "syslog.json") + JsonConfig syslogJson; + @ConfigGroup public static class JsonConfig { /** diff --git a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java index 86ad507b6bd81..872f292569dad 100644 --- a/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java +++ b/extensions/logging-json/runtime/src/main/java/io/quarkus/logging/json/runtime/LoggingJsonRecorder.java @@ -18,6 +18,10 @@ public RuntimeValue> initializeFileJsonLogging(final JsonLog return getFormatter(config.fileJson); } + public RuntimeValue> initializeSyslogJsonLogging(JsonLogConfig config) { + return getFormatter(config.syslogJson); + } + private RuntimeValue> getFormatter(JsonConfig config) { if (!config.enable) { return new RuntimeValue<>(Optional.empty());