Skip to content

Commit bb80476

Browse files
committed
Support evaluation of expressions for component, environment and custom fields
The evaluation uses system properties and environment variables to eval expressions in the form ${key} and ${key:defaultValue}. This also defaults the component in a K8s environment using the hostname.
1 parent 68ecbe5 commit bb80476

File tree

4 files changed

+162
-7
lines changed

4 files changed

+162
-7
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.avaje.logback.encoder;
2+
3+
/**
4+
* Helper to evaluate expressions like {@code "${my.property}"}, {@code "${MY_PROPERTY:someDefaultValue}"} etc.
5+
*/
6+
final class Eval {
7+
8+
/**
9+
* Return the default component value using environment variables.
10+
* <p>
11+
* For K8s this derives the component name from the HOSTNAME.
12+
*/
13+
static String defaultComponent() {
14+
String component = System.getenv("COMPONENT");
15+
if (component != null) {
16+
return component;
17+
}
18+
if (System.getenv("KUBERNETES_PORT") != null) {
19+
// in k8s we can default off the hostname
20+
return k8sComponent(System.getenv("HOSTNAME"));
21+
}
22+
return null;
23+
}
24+
25+
static String k8sComponent(String hostname) {
26+
if (hostname == null) {
27+
return null;
28+
}
29+
int p0 = hostname.lastIndexOf('-');
30+
if (p0 > 1) {
31+
int p1 = hostname.lastIndexOf('-', p0 - 1);
32+
if (p1 > 0) {
33+
return hostname.substring(0, p1);
34+
}
35+
}
36+
return null;
37+
}
38+
39+
/**
40+
* Evaluate the expression and otherwise return the original value.
41+
* <p>
42+
* Expressions are in the form {@code ${key:defaultValue}}
43+
* <p>
44+
* Examples:
45+
* <pre>{@code
46+
*
47+
* ${APP_ENV:localDev}
48+
* ${system.name:unknown}
49+
* ${MY_COMPONENT:myDefaultValue}
50+
*
51+
* }</pre>
52+
*/
53+
static String eval(String value) {
54+
if (value == null || !value.startsWith("${") || !value.endsWith("}")) {
55+
return value;
56+
}
57+
String raw = value.substring(2, value.length() - 1);
58+
String[] split = raw.split(":", 2);
59+
String key = split[0];
60+
String val = System.getProperty(key);
61+
if (val != null) {
62+
return val;
63+
}
64+
val = System.getenv(key);
65+
if (val != null) {
66+
return val;
67+
}
68+
val = System.getProperty(toSystemPropertyKey(key));
69+
if (val != null) {
70+
return val;
71+
}
72+
return split.length == 2 ? split[1] : value;
73+
}
74+
75+
static String toSystemPropertyKey(String key) {
76+
return key.replace('_', '.').toLowerCase();
77+
}
78+
}

src/main/java/io/avaje/logback/encoder/JsonEncoder.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public final class JsonEncoder extends EncoderBase<ILoggingEvent> {
3737
public JsonEncoder() {
3838
this.json = JsonStream.builder().build();
3939
this.properties = json.properties("component", "env", "timestamp", "level", "logger", "message", "thread", "stackhash", "stacktrace");
40-
this.component = System.getenv("COMPONENT");
40+
this.component = Eval.defaultComponent();
4141
this.environment = System.getenv("ENVIRONMENT");
4242
this.stackHasher = new StackHasher(StackElementFilter.builder().allFilters().build());
4343
}
@@ -131,11 +131,11 @@ public void setIncludeStackHash(boolean includeStackHash) {
131131
}
132132

133133
public void setComponent(String component) {
134-
this.component = component;
134+
this.component = Eval.eval(component);
135135
}
136136

137137
public void setEnvironment(String environment) {
138-
this.environment = environment;
138+
this.environment = Eval.eval(environment);
139139
}
140140

141141
public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) {
@@ -147,7 +147,12 @@ public void setCustomFields(String customFields) {
147147
return;
148148
}
149149
var mapper = SimpleMapper.builder().jsonStream(json).build();
150-
mapper.map().fromJson(customFields).forEach((k, v) -> customFieldsMap.put(k, mapper.toJson(v)));
150+
mapper.map().fromJson(customFields).forEach((key, value) -> {
151+
if (value instanceof String) {
152+
value = Eval.eval((String) value);
153+
}
154+
customFieldsMap.put(key, mapper.toJson(value));
155+
});
151156
}
152157

153158
public void setTimestampPattern(String pattern) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.avaje.logback.encoder;
2+
3+
import org.junit.jupiter.api.BeforeAll;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
class EvalTest {
9+
10+
@BeforeAll
11+
static void beforeAll() {
12+
System.setProperty("my.property", "x42");
13+
}
14+
15+
@Test
16+
void eval_basic() {
17+
assertThat(Eval.eval("${my.property}")).isEqualTo("x42");
18+
}
19+
20+
@Test
21+
void eval_defaultValue() {
22+
assertThat(Eval.eval("${my.property:someDefaultValue}")).isEqualTo("x42");
23+
assertThat(Eval.eval("${my.other:someDefaultValue}")).isEqualTo("someDefaultValue");
24+
assertThat(Eval.eval("${my.other}")).isEqualTo("${my.other}");
25+
}
26+
27+
@Test
28+
void eval_nonMatchingEnds() {
29+
assertThat(Eval.eval("my.property")).isEqualTo("my.property");
30+
assertThat(Eval.eval("${my.property")).isEqualTo("${my.property");
31+
assertThat(Eval.eval("my.property}")).isEqualTo("my.property}");
32+
}
33+
34+
@Test
35+
void toSystemPropertyKey() {
36+
assertThat(Eval.toSystemPropertyKey("FOO")).isEqualTo("foo");
37+
assertThat(Eval.toSystemPropertyKey("MY_FOO")).isEqualTo("my.foo");
38+
assertThat(Eval.toSystemPropertyKey("A_MY_FOO")).isEqualTo("a.my.foo");
39+
assertThat(Eval.toSystemPropertyKey("my.foo")).isEqualTo("my.foo");
40+
}
41+
42+
@Test
43+
void k8sComponent_expected() {
44+
assertThat(Eval.k8sComponent("some-x-y")).isEqualTo("some");
45+
assertThat(Eval.k8sComponent("my-some-x-y")).isEqualTo("my-some");
46+
assertThat(Eval.k8sComponent("foo-bar-some-x-y")).isEqualTo("foo-bar-some");
47+
}
48+
49+
@Test
50+
void k8sComponent_unexpected() {
51+
assertThat(Eval.k8sComponent(null)).isNull();
52+
assertThat(Eval.k8sComponent("")).isNull();
53+
assertThat(Eval.k8sComponent("some")).isNull();
54+
assertThat(Eval.k8sComponent("some-x")).isNull();
55+
}
56+
}

src/test/java/io/avaje/logback/encoder/JsonEncoderTest.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,33 @@ void encode_component() {
6868
}
6969

7070
@Test
71-
void throwable_usingDefault() {
71+
void customFieldsEval() {
72+
System.setProperty("some.custom.property", "Hi!");
7273
JsonEncoder encoder = new JsonEncoder();
74+
encoder.setCustomFields("{\"my-custom\":\"${some.custom.property}\", \"other\": \"myLiteral\", \"more\": 12}");
7375
encoder.start();
7476

7577
byte[] bytes = encoder.encode(createLogEvent(createThrowable()));
7678
SimpleMapper simpleMapper = SimpleMapper.builder().build();
7779
Map<String, Object> asMap = simpleMapper.map().fromJson(bytes);
7880

79-
assertThat((String)asMap.get("stacktrace")).startsWith("java.lang.NullPointerException: ");
81+
assertThat((String)asMap.get("my-custom")).isEqualTo("Hi!");
82+
assertThat((String)asMap.get("other")).isEqualTo("myLiteral");
83+
assertThat((Long)asMap.get("more")).isEqualTo(12L);
8084
}
8185

86+
@Test
87+
void throwable_usingDefault() {
88+
JsonEncoder encoder = new JsonEncoder();
89+
encoder.start();
90+
91+
byte[] bytes = encoder.encode(createLogEvent(createThrowable()));
92+
SimpleMapper simpleMapper = SimpleMapper.builder().build();
93+
Map<String, Object> asMap = simpleMapper.map().fromJson(bytes);
94+
95+
assertThat((String)asMap.get("stacktrace")).startsWith("java.lang.NullPointerException: ");
96+
}
97+
8298
@Test
8399
void throwable_usingConverter() {
84100
final TrimPackageAbbreviator trimPackages = new TrimPackageAbbreviator();
@@ -120,7 +136,7 @@ void throwable_usingConverter_includeStackHash() {
120136
Map<String, Object> asMap = simpleMapper.map().fromJson(bytes);
121137

122138
assertThat((String)asMap.get("stacktrace")).startsWith("j.l.NullPointerException: ");
123-
assertThat((String)asMap.get("stackhash")).isEqualTo("2a4a23a6");
139+
assertThat((String)asMap.get("stackhash")).isEqualTo("cd6925a6");
124140
}
125141

126142
@Test

0 commit comments

Comments
 (0)