Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Socket Logging Handler #23128

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2454,6 +2454,16 @@
<artifactId>quarkus-logging-gelf-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-logging-socket</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-logging-socket-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public final class LogConfig {
@ConfigDocSection
public SyslogConfig syslog;

/**
* Socket logging.
* <p>
* Logging to a socket is also supported but not enabled by default.
*/
@ConfigDocSection
public SocketConfig socket;

/**
* Logging categories.
* <p>
Expand Down Expand Up @@ -95,6 +103,15 @@ public final class LogConfig {
@ConfigDocSection
public Map<String, SyslogConfig> syslogHandlers;

/**
* Socket handlers.
* <p>
* The named socket handlers configured here can be linked on one or more categories.
*/
@ConfigItem(name = "handler.socket")
@ConfigDocSection
public Map<String, SocketConfig> socketHandlers;

/**
* Log cleanup filters - internal use.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -25,17 +26,21 @@

import org.graalvm.nativeimage.ImageInfo;
import org.jboss.logmanager.EmbeddedConfigurator;
import org.jboss.logmanager.ExtLogRecord;
import org.jboss.logmanager.LogContext;
import org.jboss.logmanager.Logger;
import org.jboss.logmanager.errormanager.OnlyOnceErrorManager;
import org.jboss.logmanager.formatters.ColorPatternFormatter;
import org.jboss.logmanager.formatters.JsonFormatter;
import org.jboss.logmanager.formatters.PatternFormatter;
import org.jboss.logmanager.formatters.StructuredFormatter;
import org.jboss.logmanager.handlers.AsyncHandler;
import org.jboss.logmanager.handlers.ConsoleHandler;
import org.jboss.logmanager.handlers.FileHandler;
import org.jboss.logmanager.handlers.PeriodicRotatingFileHandler;
import org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler;
import org.jboss.logmanager.handlers.SizeRotatingFileHandler;
import org.jboss.logmanager.handlers.SocketHandler;
import org.jboss.logmanager.handlers.SyslogHandler;

import io.quarkus.bootstrap.logging.InitialConfigurator;
Expand Down Expand Up @@ -160,6 +165,13 @@ public void close() throws SecurityException {
}
}

if (config.socket.enable) {
final Handler socketHandler = configureSocketHandler(config.socket, errorManager, cleanupFiler);
if (socketHandler != null) {
handlers.add(socketHandler);
}
}

if ((launchMode.isDevOrTest() || enableWebStream)
&& devUiConsoleHandler != null
&& devUiConsoleHandler.getValue().isPresent()) {
Expand Down Expand Up @@ -348,6 +360,16 @@ private static Map<String, Handler> createNamedHandlers(LogConfig config, Consol
addToNamedHandlers(namedHandlers, syslogHandler, sysLogConfigEntry.getKey());
}
}
for (Entry<String, SocketConfig> socketConfigEntry : config.socketHandlers.entrySet()) {
SocketConfig namedSocketConfig = socketConfigEntry.getValue();
if (!namedSocketConfig.enable) {
continue;
}
final Handler socketHandler = configureSocketHandler(namedSocketConfig, errorManager, cleanupFilter);
if (socketHandler != null) {
addToNamedHandlers(namedHandlers, socketHandler, socketConfigEntry.getKey());
}
}
return namedHandlers;
}

Expand Down Expand Up @@ -546,6 +568,78 @@ private static Handler configureSyslogHandler(final SyslogConfig config,
}
}

private static Handler configureSocketHandler(final SocketConfig config,
final ErrorManager errorManager,
final LogCleanupFilter logCleanupFilter) {
try {
final SocketHandler handler = new SocketHandler(config.endpoint.getHostString(), config.endpoint.getPort());
handler.setProtocol(config.protocol);
handler.setBlockOnReconnect(config.blockOnReconnect);
handler.setLevel(config.level);
switch (config.formatter) {
case "json":
handler.setFormatter(createJsonFormatter(config.keyoverrides, config.additionalField, errorManager));
break;
case "pattern":
handler.setFormatter(new PatternFormatter(config.format));
break;
default:
errorManager.error("Unexpected formatter type, expected are: json, pattern", null,
ErrorManager.GENERIC_FAILURE);
return null;
}
handler.setErrorManager(errorManager);
handler.setFilter(logCleanupFilter);
if (config.async.enable) {
return createAsyncHandler(config.async, config.level, handler);
}
return handler;
} catch (IOException e) {
errorManager.error("Failed to create socket handler", e, ErrorManager.OPEN_FAILURE);
return null;
}
}

private static JsonFormatter createJsonFormatter(String keyOverrides,
Map<String, SocketConfig.AdditionalFieldConfig> additionalField, ErrorManager errorManager) {
Map<StructuredFormatter.Key, String> keyOverrideMap = null;
if (!keyOverrides.equals("<NA>")) {
keyOverrideMap = new HashMap<>();
for (String pair : keyOverrides.split(",")) {
String[] split = pair.split("=", 2);
if (split.length == 2) {
StructuredFormatter.Key key = translateToKey(split[0], errorManager);
if (key != null) {
keyOverrideMap.put(key, split[1]);
}
} else {
errorManager.error(
"Key override pair '" + pair + "' is invalid, key and value should be separated by = character",
null, ErrorManager.GENERIC_FAILURE);
}
}
}
if (!additionalField.isEmpty()) {
Map<String, String> additionalFields = new HashMap<>();
for (Map.Entry<String, SocketConfig.AdditionalFieldConfig> field : additionalField.entrySet()) {
additionalFields.put(field.getKey(), field.getValue().value);
}
return new CustomFieldsJsonFormatter(keyOverrideMap, additionalFields);
}
return new JsonFormatter(keyOverrideMap);
}

private static StructuredFormatter.Key translateToKey(String name, ErrorManager errorManager) {
try {
return StructuredFormatter.Key.valueOf(name);
} catch (IllegalArgumentException e) {
errorManager.error(
"Invalid key: " + name + ". Valid values are: " + Arrays.toString(StructuredFormatter.Key.values()), e,
ErrorManager.GENERIC_FAILURE);
return null;
}
}

private static AsyncHandler createAsyncHandler(AsyncConfig asyncConfig, Level level, Handler handler) {
final AsyncHandler asyncHandler = new AsyncHandler(asyncConfig.queueLength);
asyncHandler.setOverflowAction(asyncConfig.overflow);
Expand Down Expand Up @@ -602,4 +696,23 @@ public void accept(String name, Handler handler) {
handler.setFilter(new LogCleanupFilter(filterElements));
}
}

public static class CustomFieldsJsonFormatter extends JsonFormatter {
public Map<String, String> additionalFields;

public CustomFieldsJsonFormatter(Map<Key, String> keyOverrides, Map<String, String> additionalFields) {
super(keyOverrides);
this.additionalFields = additionalFields;
}

@Override
protected void after(Generator generator, ExtLogRecord record) throws Exception {
super.after(generator, record);
if (!additionalFields.isEmpty()) {
for (Map.Entry<String, String> entry : additionalFields.entrySet())
generator.add(entry.getKey(), entry.getValue());
}
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.quarkus.runtime.logging;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.logging.Level;

import org.jboss.logmanager.handlers.SocketHandler.Protocol;

import io.quarkus.runtime.annotations.ConfigDocMapKey;
import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;

@ConfigGroup
public class SocketConfig {

/**
* If socket logging should be enabled
*/
@ConfigItem
boolean enable;

/**
*
* The IP address and port of the server receiving the logs
*/
@ConfigItem(defaultValue = "localhost:4560")
InetSocketAddress endpoint;

/**
* Sets the protocol used to connect to the server
*/
@ConfigItem(defaultValue = "tcp")
Protocol protocol;

/**
* Enables or disables blocking when attempting to reconnect a
* {@link Protocol#TCP
* TCP} or {@link Protocol#SSL_TCP SSL TCP} protocol
*/
@ConfigItem
boolean blockOnReconnect;

/**
* The log message formatter. json or pattern is supported
*/
@ConfigItem(defaultValue = "pattern")
String formatter;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this added vs what SysLogConfig?

Copy link
Author

@JozefDropco JozefDropco Jan 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added as the output where you are sending messages can require formatted output differently than the Pattern. In our case we have logstash which has input tcp and json_lines. We need custom attributes/fields to add to the json formatted log meesage. If you have some other suggestion how to achieve it - I am happy to modify it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gsmet WDYT about this? Does this use case warrant doing something different than what our SysLog handling does?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I thought I will use logging-json extension however this extension is limited to console logging. Maybe it would be more beneficial to extend that support instead and keep socket logging clean

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think that's a better approach

Copy link
Author

@JozefDropco JozefDropco Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI @geoand, @gsmet ,
I have reworked the code completely. Now socket handler in core has just pattern and logging-json was extended to support all handlers not just console as it was before. Now file, console, syslog, socket and also named handlers can log as json. Previous implementation of consoleFormatter was opiniated and if JSON was added as extension basic console but also all named handlers were forced to log as JSON. I kept the original behavior but personally I am not fan of it. With "json=false" you can set console to use PatternFormatter and some named console handlers to use JSON. But you cannot use console as JSON and some named console handler to use Pattern. Its just because implementation should be backward compatible.
If users add logging-json extension

  • all handlers will start log to JSON (even file,syslog,socket) . Not sure if its ok?
  • Additional fields can be defined as quarkus.log.socket.json.additional-field.myownfield.value=testingvalue
  • Keyoverrides was exposed too

I changed integration test of logging-json as previous one was not doing anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @JozefDropco.

I'll have a look soon

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somehow missed this PR but I wonder if the work I did in #25505 isn't somehow related.
I probably need to have another look at it.


/**
* The log message format
*/
@ConfigItem(defaultValue = "%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n")
String format;
/**
* The log level specifying, which message levels will be logged by socket logger
*/
@ConfigItem(defaultValue = "ALL")
Level level;
/**
* comma-seperated key=value pairs
* possible keys are listed {@link org.jboss.logmanager.formatters.StructuredFormatter.Key}
* e.g. HOST_NAME=host,LEVEL=severity
*/
@ConfigItem(defaultValue = "<NA>")
public String keyoverrides;
/**
* Post additional fields only for JSON formatted messages
* You can add static fields to each log event in the following form:
*
* <pre>
* quarkus.log.handler.socket.additional-field.field1.value=value1
* quarkus.log.handler.socket.additional-field.field1.type=String
* </pre>
*/
@ConfigItem
@ConfigDocMapKey("field-name")
@ConfigDocSection
public Map<String, AdditionalFieldConfig> additionalField;

/**
* Syslog async logging config
*/
AsyncConfig async;

@ConfigGroup
public class AdditionalFieldConfig {
/**
* Additional field value.
*/
@ConfigItem
public String value;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
quarkus.log.level=INFO
quarkus.log.socket.enable=true
quarkus.log.socket.endpoint=localhost:5140
quarkus.log.socket.protocol=TCP
quarkus.log.socket.formatter=json
quarkus.log.socket.level=WARNING
quarkus.log.socket.async=true
quarkus.log.socket.async.queue-length=256
quarkus.log.socket.async.overflow=discard
# Resource path to DSAPublicKey base64 encoded bytes
quarkus.root.dsa-key-location=/DSAPublicKey.encoded
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
quarkus.log.level=INFO
quarkus.log.socket.enable=true
quarkus.log.socket.endpoint=localhost:5140
quarkus.log.socket.protocol=TCP
quarkus.log.socket.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
quarkus.log.socket.level=WARNING
# Resource path to DSAPublicKey base64 encoded bytes
quarkus.root.dsa-key-location=/DSAPublicKey.encoded
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkus.logging;

import static io.quarkus.logging.LoggingTestsHelper.getHandler;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.logging.Handler;
import java.util.logging.Level;

import org.jboss.logmanager.formatters.JsonFormatter;
import org.jboss.logmanager.formatters.PatternFormatter;
import org.jboss.logmanager.handlers.AsyncHandler;
import org.jboss.logmanager.handlers.SocketHandler;
import org.jboss.logmanager.handlers.SyslogHandler;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class AsyncSocketHandlerTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource("application-async-socket-output.properties")
.withApplicationRoot((jar) -> jar
.addClass(LoggingTestsHelper.class)
.addAsManifestResource("application.properties", "microprofile-config.properties"))
.setLogFileName("AsyncSyslogHandlerTest.log");

@Test
public void asyncSyslogHandlerConfigurationTest() throws NullPointerException {
Handler handler = getHandler(AsyncHandler.class);
assertThat(handler.getLevel()).isEqualTo(Level.WARNING);

AsyncHandler asyncHandler = (AsyncHandler) handler;
assertThat(asyncHandler.getHandlers()).isNotEmpty();
assertThat(asyncHandler.getQueueLength()).isEqualTo(256);
assertThat(asyncHandler.getOverflowAction()).isEqualTo(AsyncHandler.OverflowAction.DISCARD);

Handler nestedSyslogHandler = Arrays.stream(asyncHandler.getHandlers())
.filter(h -> (h instanceof SocketHandler))
.findFirst().get();

SocketHandler socketHandler = (SocketHandler) nestedSyslogHandler;
assertThat(socketHandler.getPort()).isEqualTo(5140);
assertThat(socketHandler.getAddress().getHostAddress()).isEqualTo("127.0.0.1");
assertThat(socketHandler.getProtocol()).isEqualTo(SocketHandler.Protocol.TCP);
assertThat(socketHandler.isBlockOnReconnect()).isEqualTo(false);
assertThat(socketHandler.getFormatter()).isInstanceOf(JsonFormatter.class);
}
}
Loading