Skip to content

Commit d04b86b

Browse files
Implement Logback Configuration for KSML (#99)
Added tests
1 parent 7b2668c commit d04b86b

File tree

8 files changed

+305
-0
lines changed

8 files changed

+305
-0
lines changed

ksml-runner/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@
6161
<groupId>org.junit.jupiter</groupId>
6262
<artifactId>junit-jupiter-params</artifactId>
6363
</dependency>
64+
<dependency>
65+
<groupId>org.junit-pioneer</groupId>
66+
<artifactId>junit-pioneer</artifactId>
67+
<scope>test</scope>
68+
</dependency>
6469
<dependency>
6570
<groupId>org.mockito</groupId>
6671
<artifactId>mockito-core</artifactId>
@@ -81,6 +86,14 @@
8186
<plugin>
8287
<groupId>org.apache.maven.plugins</groupId>
8388
<artifactId>maven-surefire-plugin</artifactId>
89+
<!-- Needed for JUnit Pioneer -->
90+
<configuration>
91+
<argLine>
92+
${surefireArgLine}
93+
--add-opens java.base/java.util=ALL-UNNAMED
94+
--add-opens java.base/java.lang=ALL-UNNAMED
95+
</argLine>
96+
</configuration>
8497
</plugin>
8598
<plugin>
8699
<groupId>org.apache.maven.plugins</groupId>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package io.axual.ksml.runner.logging;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.net.MalformedURLException;
6+
import java.net.URI;
7+
import java.net.URISyntaxException;
8+
import java.net.URL;
9+
import java.util.Set;
10+
11+
import ch.qos.logback.classic.ClassicConstants;
12+
import ch.qos.logback.classic.LoggerContext;
13+
import ch.qos.logback.classic.spi.ConfiguratorRank;
14+
import ch.qos.logback.classic.util.DefaultJoranConfigurator;
15+
import ch.qos.logback.core.joran.spi.JoranException;
16+
import ch.qos.logback.core.status.InfoStatus;
17+
import ch.qos.logback.core.status.StatusManager;
18+
import ch.qos.logback.core.status.WarnStatus;
19+
import ch.qos.logback.core.util.Loader;
20+
import ch.qos.logback.core.util.OptionHelper;
21+
22+
@ConfiguratorRank(value = ConfiguratorRank.CUSTOM_HIGH_PRIORITY)
23+
public class KSMLLogbackConfigurator extends DefaultJoranConfigurator {
24+
public static final String CONFIG_FILE_ENV_PROPERTY = "LOGBACK_CONFIGURATION_FILE";
25+
public static final String CONFIG_FILE_SYS_PROPERTY = ClassicConstants.CONFIG_FILE_PROPERTY;
26+
27+
@Override
28+
public ExecutionStatus configure(LoggerContext context) {
29+
ClassLoader classLoader = Loader.getClassLoaderOfObject(this);
30+
// System properties should take precedence
31+
URL url = findConfigFileURLFromSystemProperties(classLoader);
32+
if (url == null) {
33+
// Get the configuration from environment variables, if set
34+
url = findConfigFileURLFromEnvironmentVariables(classLoader);
35+
}
36+
if (url != null) {
37+
try {
38+
configureByResource(url);
39+
} catch (JoranException e) {
40+
context.getStatusManager().add(new WarnStatus("Could not configure KSML logging", this, e));
41+
}
42+
43+
return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY;
44+
}
45+
46+
// No environment variable URL found, use DefaultJoran logic
47+
return super.configure(context);
48+
}
49+
50+
private URL findConfigFileURLFromEnvironmentVariables(ClassLoader classLoader) {
51+
return findConfigFileURL(classLoader, OptionHelper.getEnv(CONFIG_FILE_ENV_PROPERTY));
52+
}
53+
54+
private URL findConfigFileURLFromSystemProperties(ClassLoader classLoader) {
55+
return findConfigFileURL(classLoader, OptionHelper.getSystemProperty(CONFIG_FILE_SYS_PROPERTY));
56+
}
57+
58+
private URL findConfigFileURL(ClassLoader classLoader, String logbackConfigFile) {
59+
if (logbackConfigFile != null && !logbackConfigFile.isBlank()) {
60+
URL url = null;
61+
try {
62+
url = new URI(logbackConfigFile.trim()).toURL();
63+
return url;
64+
} catch (URISyntaxException | MalformedURLException | IllegalArgumentException e) {
65+
// so, resource is not a URL:
66+
// attempt to get the resource from the class path
67+
url = Loader.getResource(logbackConfigFile, classLoader);
68+
if (url != null) {
69+
return url;
70+
}
71+
// OK, check if the config is a file?
72+
File f = new File(logbackConfigFile);
73+
if (f.exists() && f.isFile()) {
74+
try {
75+
url = f.toURI().toURL();
76+
return url;
77+
} catch (MalformedURLException e1) {
78+
// Eat exception
79+
}
80+
}
81+
} finally {
82+
StatusManager sm = context.getStatusManager();
83+
if (url == null) {
84+
// Information, could not find the resource
85+
sm.add(new InfoStatus("Could NOT find resource [" + logbackConfigFile + "]", context));
86+
} else {
87+
// OK, a resource url was found
88+
sm.add(new InfoStatus("Found resource [" + logbackConfigFile + "] at [" + url + "]", context));
89+
Set<URL> urlSet = null;
90+
try {
91+
// Get all resources with the name
92+
urlSet = Loader.getResources(logbackConfigFile, classLoader);
93+
} catch (IOException e) {
94+
// Error on getting the resources
95+
addError("Failed to get url list for resource [" + logbackConfigFile + "]", e);
96+
}
97+
if (urlSet != null && urlSet.size() > 1) {
98+
// Multiple resources found, raise general warning and for each of the resources
99+
addWarn("Resource [" + logbackConfigFile + "] occurs multiple times on the classpath.");
100+
for (URL urlFromSet : urlSet) {
101+
addWarn("Resource [" + logbackConfigFile + "] occurs at [" + urlFromSet.toString() + "]");
102+
}
103+
}
104+
}
105+
}
106+
}
107+
return null;
108+
}
109+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.axual.ksml.runner.logging.KSMLLogbackConfigurator
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package io.axual.ksml.runner.logging;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
import org.junitpioneer.jupiter.ClearEnvironmentVariable;
6+
import org.junitpioneer.jupiter.RestoreEnvironmentVariables;
7+
import org.junitpioneer.jupiter.SetEnvironmentVariable;
8+
import org.junitpioneer.jupiter.SetSystemProperty;
9+
10+
import java.lang.reflect.Field;
11+
import java.net.URL;
12+
import java.util.Collection;
13+
import java.util.Map;
14+
15+
import ch.qos.logback.classic.LoggerContext;
16+
import lombok.SneakyThrows;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
21+
class KSMLLogbackConfiguratorTest {
22+
23+
LoggerContext spiedContext = new LoggerContext();
24+
25+
@Test
26+
@DisplayName("The configuration file is loaded from an environment variable reference pointing to a resource")
27+
@SetEnvironmentVariable(key = "LOGBACK_CONFIGURATION_FILE", value = "logback-custom-testing.xml")
28+
@SetSystemProperty(key = "logback.test.id", value = "testEnvToResource")
29+
void configureWithEnvironmentVariableToResource() {
30+
KSMLLogbackConfigurator configurator = new KSMLLogbackConfigurator();
31+
configurator.setContext(spiedContext);
32+
configurator.configure(spiedContext);
33+
Collection<MockAppender> appender = MockAppender.APPENDERS.get("testEnvToResource");
34+
assertNotNull(appender);
35+
assertEquals(1, appender.size());
36+
}
37+
38+
@Test
39+
@DisplayName("The configuration file is loaded from an environment variable reference in URL format")
40+
@RestoreEnvironmentVariables
41+
@SetSystemProperty(key = "logback.test.id", value = "testEnvToResourceUrl")
42+
void configureWithEnvironmentVariableToResourceURL() {
43+
// Get value for environment variable
44+
URL resourceUrl = getClass().getClassLoader().getResource("logback-custom-testing.xml");
45+
assertNotNull(resourceUrl);
46+
setEnvVar("LOGBACK_CONFIGURATION_FILE", resourceUrl.toExternalForm());
47+
48+
// Run test
49+
KSMLLogbackConfigurator configurator = new KSMLLogbackConfigurator();
50+
configurator.setContext(spiedContext);
51+
configurator.configure(spiedContext);
52+
Collection<MockAppender> appender = MockAppender.APPENDERS.get("testEnvToResourceUrl");
53+
assertNotNull(appender);
54+
assertEquals(1, appender.size());
55+
}
56+
57+
@Test
58+
@DisplayName("The configuration file is loaded from an environment variable reference as an absolute filepath")
59+
@RestoreEnvironmentVariables
60+
@SetSystemProperty(key = "logback.test.id", value = "testEnvToFile")
61+
void configureWithEnvironmentVariableToFile() {
62+
// Get value for environment variable
63+
URL resourceUrl = getClass().getClassLoader().getResource("logback-custom-testing.xml");
64+
assertNotNull(resourceUrl);
65+
setEnvVar("LOGBACK_CONFIGURATION_FILE", resourceUrl.getPath());
66+
67+
// Run test
68+
KSMLLogbackConfigurator configurator = new KSMLLogbackConfigurator();
69+
configurator.setContext(spiedContext);
70+
configurator.configure(spiedContext);
71+
Collection<MockAppender> appender = MockAppender.APPENDERS.get("testEnvToFile");
72+
assertNotNull(appender);
73+
assertEquals(1, appender.size());
74+
}
75+
76+
@Test
77+
@DisplayName("The configuration file should not be loaded, but fall back to the default setting")
78+
@ClearEnvironmentVariable(key = "LOGBACK_CONFIGURATION_FILE")
79+
@SetSystemProperty(key = "logback.test.id", value = "shouldNotAppear")
80+
void configureWithoutEnvironmentVariable() {
81+
KSMLLogbackConfigurator configurator = new KSMLLogbackConfigurator();
82+
configurator.setContext(spiedContext);
83+
configurator.configure(spiedContext);
84+
85+
// This id comes from the logback-test.xml, which should be loaded now and is hardcoded
86+
Collection<MockAppender> appender = MockAppender.APPENDERS.get("fixed-from-standard-joran-lookup");
87+
assertNotNull(appender);
88+
assertEquals(1, appender.size());
89+
90+
// This id is set, but since the default logback-test.xml is used it should never be set
91+
appender = MockAppender.APPENDERS.get("shouldNotAppear");
92+
assertNotNull(appender);
93+
assertEquals(0, appender.size());
94+
}
95+
96+
@SneakyThrows
97+
@SuppressWarnings("unchecked")
98+
void setEnvVar(String key, String value) {
99+
Class<?> classOfMap = System.getenv().getClass();
100+
Field field = classOfMap.getDeclaredField("m");
101+
field.setAccessible(true);
102+
Map<String, String> writeableEnvironmentVariables = (Map<String, String>) field.get(System.getenv());
103+
writeableEnvironmentVariables.put(key, value);
104+
}
105+
106+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.axual.ksml.runner.logging;
2+
3+
import com.google.common.collect.HashMultimap;
4+
import com.google.common.collect.Multimap;
5+
6+
import ch.qos.logback.classic.spi.ILoggingEvent;
7+
import ch.qos.logback.core.AppenderBase;
8+
9+
public class MockAppender extends AppenderBase<ILoggingEvent> {
10+
11+
public static final Multimap<String, MockAppender> APPENDERS = HashMultimap.create();
12+
13+
public MockAppender() {
14+
super();
15+
}
16+
17+
@Override
18+
protected void append(ILoggingEvent eventObject) {
19+
// Eat it
20+
}
21+
22+
private String testId;
23+
24+
25+
public String getTestId() {
26+
return testId;
27+
}
28+
29+
public void setTestId(String testId) {
30+
if (testId != null) {
31+
APPENDERS.put(testId, this);
32+
}
33+
this.testId = testId;
34+
}
35+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration scan="false">
3+
<appender name="MOCK"
4+
class="io.axual.ksml.runner.logging.MockAppender">
5+
<encoder>
6+
<pattern>%date{"yyyy-MM-dd'T'HH:mm:ss,SSSXXX", UTC} %-5level %-36logger{36} %msg%n
7+
</pattern>
8+
</encoder>
9+
<testId>${logback.test.id:-unknown}</testId>
10+
</appender>
11+
12+
<root level="INFO">
13+
<appender-ref ref="MOCK"/>
14+
</root>
15+
16+
<logger name="io.axual.ksml" level="TRACE"/>
17+
</configuration>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration scan="false">
3+
<appender name="MOCK"
4+
class="io.axual.ksml.runner.logging.MockAppender">
5+
<encoder>
6+
<pattern>%date{"yyyy-MM-dd'T'HH:mm:ss,SSSXXX", UTC} %-5level %-36logger{36} %msg%n
7+
</pattern>
8+
</encoder>
9+
<testId>fixed-from-standard-joran-lookup</testId>
10+
</appender>
11+
12+
<root level="INFO">
13+
<appender-ref ref="MOCK"/>
14+
</root>
15+
16+
<logger name="io.axual.ksml" level="TRACE"/>
17+
</configuration>

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
<hamcrest.version>2.2</hamcrest.version>
8383
<mockito-core.version>5.7.0</mockito-core.version>
8484
<mockito-junit-jupiter.version>5.7.0</mockito-junit-jupiter.version>
85+
<junit-pioneer.version>2.2.0</junit-pioneer.version>
8586

8687
<!-- project and java version settings -->
8788
<java.source.version>21</java.source.version>
@@ -319,6 +320,12 @@
319320
<version>${jupiter.version}</version>
320321
<scope>test</scope>
321322
</dependency>
323+
<dependency>
324+
<groupId>org.junit-pioneer</groupId>
325+
<artifactId>junit-pioneer</artifactId>
326+
<version>${junit-pioneer.version}</version>
327+
<scope>test</scope>
328+
</dependency>
322329

323330
<dependency>
324331
<groupId>org.mockito</groupId>

0 commit comments

Comments
 (0)