Skip to content

Commit

Permalink
Support JSON formatter for syslog logging
Browse files Browse the repository at this point in the history
  • Loading branch information
gsmet authored and holly-cummins committed Apr 20, 2023
1 parent 3a302bd commit 272dd24
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -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<Optional<Formatter>> formatterValue;

/**
* Construct a new instance.
*
* @param formatterValue the optional formatter runtime value to use (must not be {@code null})
*/
public LogSyslogFormatBuildItem(final RuntimeValue<Optional<Formatter>> formatterValue) {
this.formatterValue = Assert.checkNotNullParam("formatterValue", formatterValue);
}

/**
* Get the formatter value.
*
* @return the formatter value
*/
public RuntimeValue<Optional<Formatter>> getFormatterValue() {
return formatterValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -231,6 +232,7 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe
List<NamedLogHandlersBuildItem> namedHandlerBuildItems,
List<LogConsoleFormatBuildItem> consoleFormatItems,
List<LogFileFormatBuildItem> fileFormatItems,
List<LogSyslogFormatBuildItem> syslogFormatItems,
Optional<ConsoleFormatterBannerBuildItem> possibleBannerBuildItem,
List<LogStreamBuildItem> logStreamBuildItems,
BuildProducer<ShutdownListenerBuildItem> shutdownListenerBuildItemBuildProducer,
Expand Down Expand Up @@ -271,8 +273,13 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe
alwaysEnableLogStream = true;
}

List<RuntimeValue<Optional<Formatter>>> possibleConsoleFormatters = consoleFormatItems.stream()
.map(LogConsoleFormatBuildItem::getFormatterValue).collect(Collectors.toList());
List<RuntimeValue<Optional<Formatter>>> possibleFileFormatters = fileFormatItems.stream()
.map(LogFileFormatBuildItem::getFormatterValue).collect(Collectors.toList());
List<RuntimeValue<Optional<Formatter>>> 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);

Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public static void handleFailedStart(RuntimeValue<Optional<Supplier<String>>> ba
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyList(), banner, LaunchMode.DEVELOPMENT, false);
}

Expand All @@ -95,6 +96,7 @@ public ShutdownListener initializeLogging(LogConfig config, LogBuildTimeConfig b
final List<RuntimeValue<Map<String, Handler>>> additionalNamedHandlers,
final List<RuntimeValue<Optional<Formatter>>> possibleConsoleFormatters,
final List<RuntimeValue<Optional<Formatter>>> possibleFileFormatters,
final List<RuntimeValue<Optional<Formatter>>> possibleSyslogFormatters,
final RuntimeValue<Optional<Supplier<String>>> possibleBannerSupplier,
LaunchMode launchMode,
boolean validateFilters) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -213,7 +215,8 @@ public void close() throws SecurityException {

Map<String, Handler> 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()) {
Expand Down Expand Up @@ -323,7 +326,7 @@ public static void initializeBuildTimeLogging(LogConfig config, LogBuildTimeConf
}

Map<String, Handler> 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<String, CategoryConfig> entry : categories.entrySet()) {
Expand Down Expand Up @@ -409,6 +412,7 @@ private static Map<String, Handler> createNamedHandlers(LogConfig config, Consol
List<RuntimeValue<Map<String, Handler>>> additionalNamedHandlers,
List<RuntimeValue<Optional<Formatter>>> possibleConsoleFormatters,
List<RuntimeValue<Optional<Formatter>>> possibleFileFormatters,
List<RuntimeValue<Optional<Formatter>>> possibleSyslogFormatters,
ErrorManager errorManager, LogCleanupFilter cleanupFilter,
Map<String, Filter> namedFilters, LaunchMode launchMode,
ShutdownNotifier shutdownHandler, boolean validateFilters) {
Expand Down Expand Up @@ -438,7 +442,7 @@ private static Map<String, Handler> 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());
}
Expand Down Expand Up @@ -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<String, Filter> namedFilters, final boolean validateFilters) {
final Map<String, Filter> namedFilters,
final List<RuntimeValue<Optional<Formatter>>> possibleSyslogFormatters,
final boolean validateFilters) {
try {
final SyslogHandler handler = new SyslogHandler(config.endpoint.getHostString(), config.endpoint.getPort());
handler.setAppName(config.appName.orElse(getProcessName()));
Expand All @@ -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<Optional<Formatter>> value : possibleSyslogFormatters) {
if (formatter != null) {
formatterWarning = true;
}
final Optional<Formatter> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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!");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public RuntimeValue<Optional<Formatter>> initializeFileJsonLogging(final JsonLog
return getFormatter(config.fileJson);
}

public RuntimeValue<Optional<Formatter>> initializeSyslogJsonLogging(JsonLogConfig config) {
return getFormatter(config.syslogJson);
}

private RuntimeValue<Optional<Formatter>> getFormatter(JsonConfig config) {
if (!config.enable) {
return new RuntimeValue<>(Optional.empty());
Expand Down

0 comments on commit 272dd24

Please sign in to comment.