diff --git a/core/src/main/java/pl/tkowalcz/tjahzi/http/RequestAndResponseHandler.java b/core/src/main/java/pl/tkowalcz/tjahzi/http/RequestAndResponseHandler.java
index bf963c5..d5811d1 100644
--- a/core/src/main/java/pl/tkowalcz/tjahzi/http/RequestAndResponseHandler.java
+++ b/core/src/main/java/pl/tkowalcz/tjahzi/http/RequestAndResponseHandler.java
@@ -38,7 +38,6 @@ public void channelRead(ChannelHandlerContext ctx, Object object) {
monitoringModule.incrementHttpResponses();
if (msg.status().codeClass() != HttpStatusClass.SUCCESS) {
- System.out.println(msg.content().toString(Charset.defaultCharset()));
monitoringModule.incrementHttpErrors(
msg.status().code(),
msg.content().toString(Charset.defaultCharset())
diff --git a/pom.xml b/pom.xml
index 24f78db..8674760 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,8 @@
core
loki-protobuf
+ reload4j-appender
+
log4j2-appender
log4j2-appender-nodep
@@ -58,11 +60,11 @@
UTF-8
- 8
- 8
+ 11
+ 11
- 9
- 9
+ 11
+ 11
${env.GPG_PASSPHRASE}
diff --git a/reload4j-appender/pom.xml b/reload4j-appender/pom.xml
new file mode 100644
index 0000000..740f2c1
--- /dev/null
+++ b/reload4j-appender/pom.xml
@@ -0,0 +1,145 @@
+
+
+ 4.0.0
+
+
+ pl.tkowalcz.tjahzi
+ tjahzi-parent
+ 0.9.33-SNAPSHOT
+
+
+ reload4j-appender
+ reload4j-appender
+ jar
+
+
+
+ pl.tkowalcz.tjahzi
+ core
+ 0.9.33-SNAPSHOT
+
+
+
+ ch.qos.reload4j
+ reload4j
+ 1.2.25
+
+
+
+ io.dropwizard.metrics
+ metrics-core
+ 4.1.17
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-reload4j
+ 2.0.7
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 3.9.0
+ test
+
+
+
+ com.google.code.java-allocation-instrumenter
+ java-allocation-instrumenter
+ 3.3.0
+ test
+
+
+ io.rest-assured
+ rest-assured
+ 4.3.1
+ test
+
+
+ org.awaitility
+ awaitility
+ 4.0.3
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.15.0
+ test
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.6.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ 5.6.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.6.2
+ test
+
+
+ org.testcontainers
+ testcontainers
+ 1.14.3
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ 1.14.3
+ test
+
+
+ org.testcontainers
+ nginx
+ 1.14.3
+ test
+
+
+ com.alibaba
+ dns-cache-manipulator
+ 1.8.1
+ test
+
+
+
+
+
+ allocation-profiling
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ -javaagent:"${settings.localRepository}/com/google/code/java-allocation-instrumenter/java-allocation-instrumenter/3.3.0/java-allocation-instrumenter-3.3.0.jar"
+
+
+
+
+ com.google.code.java-allocation-instrumenter
+ java-allocation-instrumenter
+ 3.3.0
+
+
+
+
+
+
+
+
diff --git a/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/CommaDelimitedListParser.java b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/CommaDelimitedListParser.java
new file mode 100644
index 0000000..5859d19
--- /dev/null
+++ b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/CommaDelimitedListParser.java
@@ -0,0 +1,42 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+import org.apache.log4j.helpers.LogLog;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+
+public class CommaDelimitedListParser {
+
+ public static List parseString(
+ String commaDelimitedList,
+ BiFunction converter) {
+ if (commaDelimitedList == null || commaDelimitedList.trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ ArrayList result = new ArrayList<>();
+ String[] keyValuePairs = commaDelimitedList.split(",");
+ for (String keyValuePair : keyValuePairs) {
+ String[] keyAndValue = keyValuePair.split(":", 2);
+
+ if (keyAndValue.length != 2) {
+ LogLog.warn("Invalid key-value pair format: " + keyValuePair);
+ continue;
+ }
+
+ String key = keyAndValue[0].trim();
+ String value = keyAndValue[1].trim();
+
+ if (key.isEmpty() || value.isEmpty()) {
+ LogLog.warn("Key or value cannot be empty: " + keyValuePair);
+ continue;
+ }
+
+ result.add(converter.apply(key, value));
+ }
+
+ return result;
+ }
+}
diff --git a/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/Header.java b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/Header.java
new file mode 100644
index 0000000..d9d4ae8
--- /dev/null
+++ b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/Header.java
@@ -0,0 +1,28 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+public class Header {
+
+ private String name;
+ private String value;
+
+ public Header(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+}
diff --git a/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/Label.java b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/Label.java
new file mode 100644
index 0000000..f28ce0c
--- /dev/null
+++ b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/Label.java
@@ -0,0 +1,43 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+import java.util.regex.Pattern;
+
+public class Label {
+
+ private static final Pattern LABEL_NAME_PATTER = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");
+
+ private String name;
+ private String value;
+
+ public static Label createLabel(String name, String value) {
+ Label result = new Label();
+ result.setName(name);
+ result.setValue(value);
+
+ return result;
+ }
+
+ public boolean hasValidName() {
+ return hasValidName(getName());
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public static boolean hasValidName(String label) {
+ return LABEL_NAME_PATTER.matcher(label).matches();
+ }
+}
diff --git a/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LabelFactory.java b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LabelFactory.java
new file mode 100644
index 0000000..91aa0f5
--- /dev/null
+++ b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LabelFactory.java
@@ -0,0 +1,149 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+import org.apache.log4j.helpers.LogLog;
+import pl.tkowalcz.tjahzi.github.GitHubDocs;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.counting;
+
+public class LabelFactory {
+
+ private final String logLevelLabel;
+ private final String loggerNameLabel;
+ private final String threadNameLabel;
+ private final Label[] labels;
+
+ public LabelFactory(
+ String logLevelLabel,
+ String loggerNameLabel,
+ String threadNameLabel,
+ Label... labels
+ ) {
+ this.logLevelLabel = logLevelLabel;
+ this.loggerNameLabel = loggerNameLabel;
+ this.threadNameLabel = threadNameLabel;
+ this.labels = labels;
+ }
+
+ public HashMap convertLabelsDroppingInvalid() {
+ detectAndLogDuplicateLabels();
+ return convertAndLogViolations();
+ }
+
+ public String validateLogLevelLabel(HashMap existingLabels) {
+ if (logLevelLabel != null) {
+ return validatePredefinedLabelAgainst(
+ existingLabels,
+ logLevelLabel,
+ "log level label"
+ );
+ }
+
+ return null;
+ }
+
+ public String validateLoggerNameLabel(HashMap existingLabels) {
+ if (loggerNameLabel != null) {
+ return validatePredefinedLabelAgainst(
+ existingLabels,
+ loggerNameLabel,
+ "logger name label"
+ );
+ }
+
+ return null;
+ }
+
+ public String validateThreadNameLabel(HashMap existingLabels) {
+ if (threadNameLabel != null) {
+ return validatePredefinedLabelAgainst(
+ existingLabels,
+ threadNameLabel,
+ "thread name label"
+ );
+ }
+
+ return null;
+ }
+
+ private void detectAndLogDuplicateLabels() {
+ List duplicatedLabels = stream(labels)
+ .collect(Collectors.groupingBy(Label::getName, counting()))
+ .entrySet()
+ .stream().filter(entry -> entry.getValue() > 1)
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+
+ if (!duplicatedLabels.isEmpty()) {
+ LogLog.warn(
+ String.format(
+ "There are duplicated labels which is not allowed by Loki. " +
+ "These labels will be deduplicated non-deterministically: %s\n",
+ duplicatedLabels
+ )
+ );
+ }
+ }
+
+ private HashMap convertAndLogViolations() {
+ HashMap lokiLabels = new HashMap<>();
+
+ stream(labels)
+ .flatMap(label -> {
+ if (label.hasValidName()) {
+ return Stream.of(label);
+ }
+
+ LogLog.warn(
+ String.format(
+ "Ignoring label '%s' - contains invalid characters. %s\n",
+ label.getName(),
+ GitHubDocs.LABEL_NAMING.getLogMessage()
+ )
+ );
+
+ return Stream.of();
+ }
+ )
+ .forEach(__ -> lokiLabels.put(__.getName(), __.getValue()));
+
+ return lokiLabels;
+ }
+
+ private String validatePredefinedLabelAgainst(
+ Map existingLabels,
+ String predefinedLabel,
+ String predefinedLabelDescription
+ ) {
+ if (!Label.hasValidName(predefinedLabel)) {
+ LogLog.warn(
+ String.format(
+ "Ignoring %s '%s' - contains invalid characters. %s\n",
+ predefinedLabelDescription,
+ predefinedLabel,
+ GitHubDocs.LABEL_NAMING.getLogMessage()
+ )
+ );
+
+ return null;
+ }
+
+ if (existingLabels.remove(predefinedLabel) != null) {
+ LogLog.warn(
+ String.format(
+ "Ignoring %s '%s' - conflicts with label defined in configuration.\n",
+ predefinedLabelDescription,
+ predefinedLabel
+ )
+ );
+ }
+
+ return predefinedLabel;
+ }
+}
diff --git a/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppender.java b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppender.java
new file mode 100644
index 0000000..8e03de6
--- /dev/null
+++ b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppender.java
@@ -0,0 +1,118 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.spi.LoggingEvent;
+import pl.tkowalcz.tjahzi.LabelSerializer;
+import pl.tkowalcz.tjahzi.LabelSerializers;
+import pl.tkowalcz.tjahzi.LoggingSystem;
+import pl.tkowalcz.tjahzi.TjahziLogger;
+import pl.tkowalcz.tjahzi.stats.MonitoringModule;
+import pl.tkowalcz.tjahzi.stats.MutableMonitoringModuleWrapper;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class LokiAppender extends LokiAppenderConfigurator {
+
+ private LoggingSystem loggingSystem;
+ private TjahziLogger logger;
+
+ private String logLevelLabel;
+ private String loggerNameLabel;
+ private String threadNameLabel;
+ private List mdcLogLabels;
+
+ private MutableMonitoringModuleWrapper monitoringModuleWrapper;
+
+ /**
+ * This is an entry point to set monitoring (statistics) hooks for this appender. This
+ * API is in beta and is subject to change (and probably will).
+ */
+ public void setMonitoringModule(MonitoringModule monitoringModule) {
+ monitoringModuleWrapper.setMonitoringModule(monitoringModule);
+ }
+
+ // @VisibleForTesting
+ public LoggingSystem getLoggingSystem() {
+ return loggingSystem;
+ }
+
+ @Override
+ public void activateOptions() {
+ LokiAppenderFactory lokiAppenderFactory = new LokiAppenderFactory(this);
+ loggingSystem = lokiAppenderFactory.createAppender();
+ logLevelLabel = lokiAppenderFactory.getLogLevelLabel();
+ loggerNameLabel = lokiAppenderFactory.getLoggerNameLabel();
+ threadNameLabel = lokiAppenderFactory.getThreadNameLabel();
+ mdcLogLabels = lokiAppenderFactory.getMdcLogLabels();
+ monitoringModuleWrapper = lokiAppenderFactory.getMonitoringModuleWrapper();
+
+ logger = loggingSystem.createLogger();
+ loggingSystem.start();
+ }
+
+ @Override
+ protected void append(LoggingEvent event) {
+ String logLevel = event.getLevel().toString();
+ String loggerName = event.getLoggerName();
+ String threadName = event.getThreadName();
+
+ String message = layout.format(event);
+
+ LabelSerializer labelSerializer = LabelSerializers.threadLocal();
+ appendLogLabel(labelSerializer, logLevel);
+ appendLoggerLabel(labelSerializer, loggerName);
+ appendThreadLabel(labelSerializer, threadName);
+ appendMdcLogLabels(labelSerializer, event);
+
+ logger.log(
+ event.getTimeStamp(),
+ 0L,
+ labelSerializer,
+ ByteBuffer.wrap(message.getBytes())
+ );
+ }
+
+ private void appendLogLabel(LabelSerializer labelSerializer, String logLevel) {
+ if (logLevelLabel != null) {
+ labelSerializer.appendLabel(logLevelLabel, logLevel);
+ }
+ }
+
+ private void appendLoggerLabel(LabelSerializer labelSerializer, String loggerName) {
+ if (loggerNameLabel != null) {
+ labelSerializer.appendLabel(loggerNameLabel, loggerName);
+ }
+ }
+
+ private void appendThreadLabel(LabelSerializer labelSerializer, String threadName) {
+ if (threadNameLabel != null) {
+ labelSerializer.appendLabel(threadNameLabel, threadName);
+ }
+ }
+
+ @SuppressWarnings("ForLoopReplaceableByForEach") // Allocator goes brrrr
+ private void appendMdcLogLabels(LabelSerializer serializer,
+ LoggingEvent mdcPropertyMap) {
+ for (int i = 0; i < mdcLogLabels.size(); i++) {
+ String mdcLogLabel = mdcLogLabels.get(i);
+
+ Object mdcValue = mdcPropertyMap.getMDC(mdcLogLabel);
+ if (mdcValue != null) {
+ serializer.appendLabel(mdcLogLabel, mdcValue.toString());
+ }
+ }
+ }
+
+ public void close() {
+ loggingSystem.close(
+ (int) TimeUnit.SECONDS.toMillis(getShutdownTimeoutSeconds()),
+ thread -> LogLog.error("Loki appender was unable to stop thread on shutdown: " + thread)
+ );
+ }
+
+ public boolean requiresLayout() {
+ return true;
+ }
+}
diff --git a/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppenderConfigurator.java b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppenderConfigurator.java
new file mode 100644
index 0000000..f09a670
--- /dev/null
+++ b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppenderConfigurator.java
@@ -0,0 +1,239 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+import org.apache.log4j.AppenderSkeleton;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class LokiAppenderConfigurator extends AppenderSkeleton {
+
+ static final int BYTES_IN_MEGABYTE = 1024 * 1024;
+
+ private String url;
+ private String logEndpoint;
+
+ private String host;
+ private int port;
+
+ private boolean useSSL;
+
+ private boolean useDaemonThreads;
+
+ private String username;
+
+ private String password;
+
+ private int connectTimeoutMillis = 5000;
+ private int readTimeoutMillis = 60_000;
+ private int maxRetries = 3;
+
+ private int bufferSizeMegabytes = 32;
+ private boolean useOffHeapBuffer = true;
+
+ private String logLevelLabel;
+ private String loggerNameLabel;
+ private String threadNameLabel;
+ private final List mdcLogLabels = new ArrayList<>();
+
+ private long batchSize = 10_2400;
+ private long batchWait = 5;
+ private long shutdownTimeoutSeconds = 10;
+ private long logShipperWakeupIntervalMillis = 10;
+
+ private int maxRequestsInFlight = 100;
+
+ private String headers;
+ private String labels;
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getLogEndpoint() {
+ return logEndpoint;
+ }
+
+ public void setLogEndpoint(String logEndpoint) {
+ this.logEndpoint = logEndpoint;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public boolean isUseSSL() {
+ return useSSL;
+ }
+
+ public void setUseSSL(boolean useSSL) {
+ this.useSSL = useSSL;
+ }
+
+ public boolean isUseDaemonThreads() {
+ return useDaemonThreads;
+ }
+
+ public void setUseDaemonThreads(boolean useDaemonThreads) {
+ this.useDaemonThreads = useDaemonThreads;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public int getConnectTimeoutMillis() {
+ return connectTimeoutMillis;
+ }
+
+ public void setConnectTimeoutMillis(int connectTimeoutMillis) {
+ this.connectTimeoutMillis = connectTimeoutMillis;
+ }
+
+ public int getReadTimeoutMillis() {
+ return readTimeoutMillis;
+ }
+
+ public void setReadTimeoutMillis(int readTimeoutMillis) {
+ this.readTimeoutMillis = readTimeoutMillis;
+ }
+
+ public int getMaxRetries() {
+ return maxRetries;
+ }
+
+ public void setMaxRetries(int maxRetries) {
+ this.maxRetries = maxRetries;
+ }
+
+ public int getBufferSizeMegabytes() {
+ return bufferSizeMegabytes;
+ }
+
+ public void setBufferSizeMegabytes(int bufferSizeMegabytes) {
+ this.bufferSizeMegabytes = bufferSizeMegabytes;
+ }
+
+ public boolean isUseOffHeapBuffer() {
+ return useOffHeapBuffer;
+ }
+
+ public void setUseOffHeapBuffer(boolean useOffHeapBuffer) {
+ this.useOffHeapBuffer = useOffHeapBuffer;
+ }
+
+ public String getLogLevelLabel() {
+ return logLevelLabel;
+ }
+
+ public void setLogLevelLabel(String logLevelLabel) {
+ this.logLevelLabel = logLevelLabel;
+ }
+
+ public String getLoggerNameLabel() {
+ return loggerNameLabel;
+ }
+
+ public void setLoggerNameLabel(String loggerNameLabel) {
+ this.loggerNameLabel = loggerNameLabel;
+ }
+
+ public String getThreadNameLabel() {
+ return threadNameLabel;
+ }
+
+ public void setThreadNameLabel(String threadNameLabel) {
+ this.threadNameLabel = threadNameLabel;
+ }
+
+ public void setBatchSize(long batchSize) {
+ this.batchSize = batchSize;
+ }
+
+ public void setBatchWait(long batchWait) {
+ this.batchWait = batchWait;
+ }
+
+ public long getBatchSize() {
+ return batchSize;
+ }
+
+ public long getBatchWait() {
+ return batchWait;
+ }
+
+ public long getShutdownTimeoutSeconds() {
+ return shutdownTimeoutSeconds;
+ }
+
+ public void setShutdownTimeoutSeconds(long shutdownTimeoutSeconds) {
+ this.shutdownTimeoutSeconds = shutdownTimeoutSeconds;
+ }
+
+ public long getLogShipperWakeupIntervalMillis() {
+ return logShipperWakeupIntervalMillis;
+ }
+
+ public void setLogShipperWakeupIntervalMillis(long logShipperWakeupIntervalMillis) {
+ this.logShipperWakeupIntervalMillis = logShipperWakeupIntervalMillis;
+ }
+
+ public int getMaxRequestsInFlight() {
+ return maxRequestsInFlight;
+ }
+
+ public void setMaxRequestsInFlight(int maxRequestsInFlight) {
+ this.maxRequestsInFlight = maxRequestsInFlight;
+ }
+
+ public List getMdcLogLabels() {
+ return mdcLogLabels;
+ }
+
+ public void addMdcLogLabel(String mdcLogLabel) {
+ this.mdcLogLabels.add(mdcLogLabel);
+ }
+
+ public String getHeaders() {
+ return headers;
+ }
+
+ public void setHeaders(String headers) {
+ this.headers = headers;
+ }
+
+ public String getLabels() {
+ return labels;
+ }
+
+ public void setLabels(String labels) {
+ this.labels = labels;
+ }
+}
diff --git a/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppenderFactory.java b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppenderFactory.java
new file mode 100644
index 0000000..8fdf8a8
--- /dev/null
+++ b/reload4j-appender/src/main/java/pl/tkowalcz/tjahzi/reload4j/LokiAppenderFactory.java
@@ -0,0 +1,122 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+import org.apache.log4j.helpers.LogLog;
+import pl.tkowalcz.tjahzi.LoggingSystem;
+import pl.tkowalcz.tjahzi.TjahziInitializer;
+import pl.tkowalcz.tjahzi.github.GitHubDocs;
+import pl.tkowalcz.tjahzi.http.ClientConfiguration;
+import pl.tkowalcz.tjahzi.http.HttpClientFactory;
+import pl.tkowalcz.tjahzi.http.NettyHttpClient;
+import pl.tkowalcz.tjahzi.stats.MutableMonitoringModuleWrapper;
+import pl.tkowalcz.tjahzi.stats.StandardMonitoringModule;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+
+public class LokiAppenderFactory {
+
+ private final LokiAppenderConfigurator configurator;
+
+ private final HashMap lokiLabels;
+ private final String logLevelLabel;
+ private final String loggerNameLabel;
+ private final String threadNameLabel;
+ private final List mdcLogLabels;
+ private final MutableMonitoringModuleWrapper monitoringModuleWrapper;
+
+ public LokiAppenderFactory(LokiAppenderConfigurator configurator) {
+ this.configurator = configurator;
+
+ LabelFactory labelFactory = new LabelFactory(
+ configurator.getLogLevelLabel(),
+ configurator.getLoggerNameLabel(),
+ configurator.getThreadNameLabel(),
+ CommaDelimitedListParser
+ .parseString(configurator.getLabels(), Label::createLabel)
+ .toArray(Label[]::new)
+ );
+
+ lokiLabels = labelFactory.convertLabelsDroppingInvalid();
+ logLevelLabel = labelFactory.validateLogLevelLabel(lokiLabels);
+ loggerNameLabel = labelFactory.validateLoggerNameLabel(lokiLabels);
+ threadNameLabel = labelFactory.validateThreadNameLabel(lokiLabels);
+ mdcLogLabels = configurator.getMdcLogLabels();
+ monitoringModuleWrapper = new MutableMonitoringModuleWrapper();
+ }
+
+ public LoggingSystem createAppender() {
+ ClientConfiguration configurationBuilder = ClientConfiguration.builder()
+ .withUrl(configurator.getUrl())
+ .withLogEndpoint(configurator.getLogEndpoint())
+ .withHost(configurator.getHost())
+ .withPort(configurator.getPort())
+ .withUseSSL(configurator.isUseSSL())
+ .withUsername(configurator.getUsername())
+ .withPassword(configurator.getPassword())
+ .withConnectionTimeoutMillis(configurator.getConnectTimeoutMillis())
+ .withMaxRetries(configurator.getMaxRetries())
+ .withRequestTimeoutMillis(configurator.getReadTimeoutMillis())
+ .withMaxRequestsInFlight(configurator.getMaxRequestsInFlight())
+ .build();
+
+ String[] additionalHeaders = CommaDelimitedListParser.parseString(configurator.getHeaders(), Header::new).stream()
+ .flatMap(header -> Stream.of(header.getName(), header.getValue()))
+ .toArray(String[]::new);
+
+ monitoringModuleWrapper.setMonitoringModule(new StandardMonitoringModule());
+
+ NettyHttpClient httpClient = HttpClientFactory
+ .defaultFactory()
+ .getHttpClient(
+ configurationBuilder,
+ monitoringModuleWrapper,
+ additionalHeaders
+ );
+
+ int bufferSizeBytes = configurator.getBufferSizeMegabytes() * LokiAppenderConfigurator.BYTES_IN_MEGABYTE;
+ if (!TjahziInitializer.isCorrectSize(bufferSizeBytes)) {
+ LogLog.warn(
+ String.format(
+ "Invalid log buffer size %d - using nearest power of two greater than provided value, no less than 1MB. %s\n",
+ bufferSizeBytes,
+ GitHubDocs.LOG_BUFFER_SIZING.getLogMessage()
+ )
+ );
+ }
+
+ return new TjahziInitializer().createLoggingSystem(
+ httpClient,
+ monitoringModuleWrapper,
+ lokiLabels,
+ configurator.getBatchSize(),
+ TimeUnit.SECONDS.toMillis(configurator.getBatchWait()),
+ bufferSizeBytes,
+ configurator.getLogShipperWakeupIntervalMillis(),
+ TimeUnit.SECONDS.toMillis(configurator.getShutdownTimeoutSeconds()),
+ configurator.isUseOffHeapBuffer(),
+ configurator.isUseDaemonThreads()
+ );
+ }
+
+ public String getLogLevelLabel() {
+ return logLevelLabel;
+ }
+
+ public String getLoggerNameLabel() {
+ return loggerNameLabel;
+ }
+
+ public String getThreadNameLabel() {
+ return threadNameLabel;
+ }
+
+ public MutableMonitoringModuleWrapper getMonitoringModuleWrapper() {
+ return monitoringModuleWrapper;
+ }
+
+ public List getMdcLogLabels() {
+ return mdcLogLabels;
+ }
+}
diff --git a/reload4j-appender/src/test/java/pl/tkowalcz/tjahzi/reload4j/CommaDelimitedListParserTest.java b/reload4j-appender/src/test/java/pl/tkowalcz/tjahzi/reload4j/CommaDelimitedListParserTest.java
new file mode 100644
index 0000000..b616c40
--- /dev/null
+++ b/reload4j-appender/src/test/java/pl/tkowalcz/tjahzi/reload4j/CommaDelimitedListParserTest.java
@@ -0,0 +1,107 @@
+package pl.tkowalcz.tjahzi.reload4j;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class CommaDelimitedListParserTest {
+
+ @Test
+ public void testHappyPathScenario() {
+ String input = "server-address:10.1.1.2,organisation:foo,namespace:bar";
+ Label[] labels = CommaDelimitedListParser.parseString(input, Label::createLabel).toArray(new Label[0]);
+
+ assertEquals(3, labels.length);
+ assertEquals("server-address", labels[0].getName());
+ assertEquals("10.1.1.2", labels[0].getValue());
+ assertEquals("organisation", labels[1].getName());
+ assertEquals("foo", labels[1].getValue());
+ assertEquals("namespace", labels[2].getName());
+ assertEquals("bar", labels[2].getValue());
+ }
+
+ @Test
+ public void testEmptyStringScenario() {
+ String input = "";
+ List