Skip to content

Commit 01359ec

Browse files
authored
Merge pull request #19 from avaje/feature/external-config
Use logger.config system property to load external logger configuration
2 parents a13c73a + 1ed258b commit 01359ec

File tree

5 files changed

+191
-18
lines changed

5 files changed

+191
-18
lines changed

README.md

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
# avaje-simple-logger
22

3-
A SLF4J Logger built primarily for use with GraalVM Native image that writes JSON structured logs to `System.out`.
4-
It is designed to be used by applications that will be run in **K8s** or **Lambda**.
3+
A SLF4J Logger built primarily for use with GraalVM Native image that writes JSON structured logs (or plain format)
4+
to `System.out`. It is designed to be used by applications that will be run in **K8s** or **Lambda**.
55

6+
It supports GraalVM Native image, and also supports dynamic log levels. It is built with simple configuration
7+
in mind based on properties (no reflection).
68

7-
## Background
8-
9-
This logger has been created from the Logback JSON Encoder from [avaje-logback-encoder](https://github.com/avaje/avaje-logback-encoder),
10-
and turned into a SLF4J Logger, thus removing the dependency on Logback and its associated XML configuration.
9+
It is not intended to be a full featured logging framework with appenders, filters, etc (sticking to `System.out`).
1110

1211

1312
## How to use it
@@ -18,7 +17,7 @@ and turned into a SLF4J Logger, thus removing the dependency on Logback and its
1817
<dependency>
1918
<groupId>io.avaje</groupId>
2019
<artifactId>avaje-simple-logger</artifactId>
21-
<version>1.0</version>
20+
<version>1.3</version>
2221
</dependency>
2322
```
2423

@@ -55,6 +54,33 @@ log.level.io.ebean.DDL=TRACE
5554
#log.level.io.ebean.TXN=DEBUG
5655
```
5756

57+
## External configuration
58+
59+
We can override the configuration via setting a system property `logger.config`.
60+
61+
```sh
62+
java -Dlogger.config=/path/to/my-logger.properties -jar myapp.jar
63+
```
64+
65+
Note that `~` can be used to represent the user home directory like:
66+
```sh
67+
java -Dlogger.config=~/my-logger.properties -jar myapp.jar
68+
```
69+
70+
When we specify an external configuration file via `logger.config`, then it is used *instead of*
71+
the default `avaje-logger.properties` and `avaje-logger-test.properties` files.
72+
73+
74+
## GraalVM Native Image
75+
76+
When building a GraalVM native image, the *log format* (JSON or plain) is initialised at BUILD time.
77+
This means that at runtime we cannot change the format via an external configuration file.
78+
79+
The reason for this, is that GraalVM native image wants to initialize as much as possible at build time,
80+
and specifically the `System.Logger` is initialised at build time. So as we want avaje-simple-logger to
81+
be the `System.Logger` implementation, we need to fix the log format at build time.
82+
83+
5884
## Debugging
5985

6086
To debug avaje-simple-logger set the log level for `io.avaje.simplelogger` to `DEBUG`

avaje-simple-json-logger/src/main/java/io/avaje/simplelogger/encoder/Bootstrap.java

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import io.avaje.simplelogger.LoggerContext;
44
import org.slf4j.helpers.Reporter;
55

6-
import java.io.IOException;
7-
import java.io.InputStream;
8-
import java.io.PrintStream;
6+
import java.io.*;
97
import java.time.format.DateTimeFormatter;
108
import java.util.*;
119

@@ -14,12 +12,14 @@
1412
*/
1513
public final class Bootstrap {
1614

15+
private static final int LOG_LEVEL_PREFIX_LENGTH = "log.level.".length();
16+
1717
public static LoggerContext init() {
1818
Properties properties = loadProperties();
1919
LogWriter logWriter = createWriter(properties, logFormat(properties));
2020

21-
String info = properties.getProperty("logger.defaultLogLevel", "info");
22-
int defaultLevel = SimpleLoggerFactory.stringToLevel(info);
21+
String defaultLogLevel = properties.getProperty("logger.defaultLogLevel", "info");
22+
int defaultLevel = SimpleLoggerFactory.stringToLevel(defaultLogLevel);
2323

2424
String nameLength = properties.getProperty("logger.nameTargetLength", "full");
2525
Abbreviator abbreviator = Abbreviator.create(nameLength);
@@ -38,7 +38,7 @@ private static Map<String, String> initialNameLevels(Properties properties) {
3838
if (key.startsWith("log.level.")) {
3939
String val = properties.getProperty(key);
4040
if (val != null) {
41-
nameLevels.put(key.substring(10), val);
41+
nameLevels.put(key.substring(LOG_LEVEL_PREFIX_LENGTH), val);
4242
}
4343
}
4444
}
@@ -73,17 +73,77 @@ private static boolean propertyShowThreadName(Properties properties) {
7373
return Boolean.parseBoolean(Eval.eval(properties.getProperty("logger.showThreadName", "true")));
7474
}
7575

76-
private static Properties loadProperties() {
76+
static Properties loadProperties() {
77+
String config = System.getProperty("logger.config", System.getenv("LOGGER_CONFIG"));
78+
if (config != null && !config.isEmpty()) {
79+
final var properties = loadExternal(config);
80+
if (isMergeDefaultProperties(properties)) {
81+
// merge with default properties from avaje-logger.properties
82+
for (Map.Entry<Object, Object> entry : defaultProperties().entrySet()) {
83+
properties.putIfAbsent(entry.getKey(), entry.getValue());
84+
}
85+
}
86+
return properties;
87+
}
88+
return loadTestProperties(defaultProperties());
89+
}
90+
91+
private static boolean isMergeDefaultProperties(Properties properties) {
92+
return Optional.ofNullable(System.getProperty("logger.config.merge"))
93+
.or(() -> Optional.ofNullable(System.getenv("LOGGER_CONFIG_MERGE")))
94+
.or(() -> Optional.ofNullable(properties.getProperty("logger.config.merge")))
95+
.orElse("true")
96+
.equalsIgnoreCase("true");
97+
}
98+
99+
static Properties defaultProperties() {
77100
final var properties = new Properties();
78-
load(properties, "avaje-logger.properties");
79-
load(properties, "avaje-logger-test.properties");
101+
loadResource(properties, "avaje-logger.properties");
102+
return properties;
103+
}
104+
105+
static Properties loadTestProperties(Properties properties) {
106+
loadResource(properties, "avaje-logger-test.properties");
80107
return properties;
81108
}
82109

83-
private static void load(Properties properties, String resourceName) {
110+
static Properties loadExternal(String fileName) {
111+
final var properties = new Properties();
112+
if (fileName.startsWith("~/")) {
113+
fileName = System.getProperty("user.home") + fileName.substring(1);
114+
}
115+
if (loadFile(fileName, properties)) {
116+
return properties;
117+
}
118+
if (!fileName.endsWith(".properties") && loadFile(fileName + ".properties", properties)) {
119+
return properties;
120+
}
121+
Reporter.error("External logger config file not found: " + fileName);
122+
return properties;
123+
}
124+
125+
private static boolean loadFile(String fileName, Properties properties) {
126+
var file = new File(fileName);
127+
if (!file.exists()) {
128+
return false;
129+
} else {
130+
loadFileIntoProperties(file, properties);
131+
return true;
132+
}
133+
}
134+
135+
private static void loadFileIntoProperties(File file, Properties properties) {
136+
try (var is = new FileInputStream(file)) {
137+
properties.load(is);
138+
} catch (IOException e) {
139+
Reporter.error("Error loading external logger config file: " + file, e);
140+
}
141+
}
142+
143+
private static void loadResource(Properties properties, String resourceName) {
84144
InputStream is = resource(resourceName);
85145
if (is != null) {
86-
try {
146+
try (is) {
87147
properties.load(is);
88148
} catch (IOException e) {
89149
Reporter.error("Error loading " + resourceName, e);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.avaje.simplelogger.encoder;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.Properties;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
class BootstrapTest {
10+
11+
12+
@Test
13+
void loadProperties_when_mergeNoClash() {
14+
System.setProperty("logger.config", "src/test/resources/test-external.properties");
15+
Properties props = Bootstrap.loadProperties();
16+
System.clearProperty("logger.config");
17+
18+
// merged all entries, no clashes
19+
assertThat(props).hasSize(3);
20+
21+
assertThat(props.getProperty("log.level.io.banana")).isEqualTo("trace");
22+
// entries from default properties
23+
assertThat(props.getProperty("logger.defaultLogLevel")).isEqualTo("debug");
24+
assertThat(props.getProperty("log.level.io.avaje")).isEqualTo("warn");
25+
}
26+
27+
@Test
28+
void loadProperties_when_merge() {
29+
System.setProperty("logger.config", "src/test/resources/test-external-merge.properties");
30+
Properties props = Bootstrap.loadProperties();
31+
System.clearProperty("logger.config");
32+
33+
// one merged entry from default avaje-logger.properties
34+
assertThat(props).hasSize(4);
35+
36+
assertThat(props.getProperty("log.level.io.apple")).isEqualTo("trace");
37+
assertThat(props.getProperty("log.level.io.banana")).isEqualTo("trace");
38+
assertThat(props.getProperty("logger.defaultLogLevel")).isEqualTo("info");
39+
// entries from default properties
40+
assertThat(props.getProperty("log.level.io.avaje")).isEqualTo("warn");
41+
}
42+
43+
@Test
44+
void loadProperties_when_override() {
45+
System.setProperty("logger.config", "src/test/resources/test-external-merge.properties");
46+
System.setProperty("logger.config.merge", "false");
47+
Properties props = Bootstrap.loadProperties();
48+
System.clearProperty("logger.config");
49+
System.clearProperty("logger.config.merge");
50+
51+
// no entries from default avaje-logger.properties
52+
assertThat(props).hasSize(3);
53+
54+
assertThat(props.getProperty("log.level.io.apple")).isEqualTo("trace");
55+
assertThat(props.getProperty("log.level.io.banana")).isEqualTo("trace");
56+
assertThat(props.getProperty("logger.defaultLogLevel")).isEqualTo("info");
57+
}
58+
59+
@Test
60+
void loadExternal() {
61+
Properties props = Bootstrap.loadExternal("src/test/resources/test-external.properties");
62+
63+
assertThat(props).hasSize(1);
64+
65+
String value = props.getProperty("log.level.io.banana");
66+
assertThat(value).isEqualTo("trace");
67+
}
68+
69+
@Test
70+
void loadExternal_when_missingPropertiesSuffix_expect_addsPropertiesExtension() {
71+
Properties props = Bootstrap.loadExternal("src/test/resources/test-external");
72+
73+
assertThat(props).hasSize(1);
74+
75+
String value = props.getProperty("log.level.io.banana");
76+
assertThat(value).isEqualTo("trace");
77+
}
78+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Externalized test configuration properties
2+
logger.defaultLogLevel=info
3+
4+
log.level.io.apple=trace
5+
log.level.io.banana=trace
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## Externalized test configuration properties
2+
3+
log.level.io.banana=trace
4+

0 commit comments

Comments
 (0)