Skip to content

Commit f61b106

Browse files
authored
Merge pull request #64 from avaje/feature/filter-as-interface
Change StackElementFilter into an interface and add a Builder for common filters
2 parents 1496c0b + 2849338 commit f61b106

File tree

10 files changed

+1111
-40
lines changed

10 files changed

+1111
-40
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+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package io.avaje.logback.encoder;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.regex.Pattern;
6+
7+
final class FilterBuilder implements StackElementFilter.Builder {
8+
9+
private final List<StackElementFilter> filters = new ArrayList<>();
10+
11+
@Override
12+
public StackElementFilter.Builder generated() {
13+
filters.add(new Generated());
14+
return this;
15+
}
16+
17+
@Override
18+
public StackElementFilter.Builder reflectiveInvocation() {
19+
filters.add(new ReflectiveInvocation());
20+
return this;
21+
}
22+
23+
@Override
24+
public StackElementFilter.Builder jdkInternals() {
25+
filters.add(new JDKInternals());
26+
return this;
27+
}
28+
29+
@Override
30+
public StackElementFilter.Builder spring() {
31+
filters.add(new SpringFilter());
32+
return this;
33+
}
34+
35+
@Override
36+
public StackElementFilter.Builder byPattern(List<Pattern> excludes) {
37+
if (excludes != null && !excludes.isEmpty()) {
38+
filters.add(new PatternFilter(excludes));
39+
}
40+
return this;
41+
}
42+
43+
@Override
44+
public StackElementFilter.Builder allFilters() {
45+
generated();
46+
reflectiveInvocation();
47+
jdkInternals();
48+
spring();
49+
return this;
50+
}
51+
52+
@Override
53+
public StackElementFilter build() {
54+
if (filters.isEmpty()) {
55+
return StackElementFilter.any();
56+
}
57+
return new Group(filters.toArray(new StackElementFilter[0]));
58+
}
59+
60+
private static final class Generated implements StackElementFilter {
61+
62+
@Override
63+
public boolean accept(StackTraceElement element) {
64+
String className = element.getClassName();
65+
return !className.contains("$$FastClassByCGLIB$$")
66+
&& !className.contains("$$EnhancerBySpringCGLIB$$");
67+
}
68+
}
69+
70+
private static final class ReflectiveInvocation implements StackElementFilter {
71+
72+
@Override
73+
public boolean accept(StackTraceElement element) {
74+
String methodName = element.getMethodName();
75+
if (methodName.equals("invoke")) {
76+
String className = element.getClassName();
77+
return !className.startsWith("sun.reflect.")
78+
&& !className.startsWith("java.lang.reflect.")
79+
&& !className.startsWith("net.sf.cglib.proxy.MethodProxy");
80+
}
81+
return true;
82+
}
83+
}
84+
85+
private static final class JDKInternals implements StackElementFilter {
86+
87+
@Override
88+
public boolean accept(StackTraceElement element) {
89+
String className = element.getClassName();
90+
return !className.startsWith("com.sun.")
91+
&& !className.startsWith("sun.net.");
92+
}
93+
}
94+
95+
private static final class SpringFilter implements StackElementFilter {
96+
97+
private static final String[] MATCHES = {
98+
"org.springframework.cglib.",
99+
"org.springframework.transaction.",
100+
"org.springframework.validation.",
101+
"org.springframework.app.",
102+
"org.springframework.aop.",
103+
"org.springframework.ws.",
104+
"org.springframework.web.",
105+
"org.springframework.transaction"
106+
};
107+
108+
@Override
109+
public boolean accept(StackTraceElement element) {
110+
String className = element.getClassName();
111+
if (className.startsWith("org.springframework")) {
112+
for (String match : MATCHES) {
113+
if (className.startsWith(match)) {
114+
return false;
115+
}
116+
}
117+
return true;
118+
}
119+
if (className.startsWith("org.apache")) {
120+
return !className.startsWith("org.apache.tomcat.")
121+
&& !className.startsWith("org.apache.catalina.")
122+
&& !className.startsWith("org.apache.coyote.");
123+
}
124+
return true;
125+
}
126+
}
127+
128+
private static final class PatternFilter implements StackElementFilter {
129+
130+
private final Pattern[] excludes;
131+
132+
PatternFilter(final List<Pattern> excludes) {
133+
this.excludes = excludes.toArray(new Pattern[0]);
134+
}
135+
136+
@Override
137+
public boolean accept(StackTraceElement element) {
138+
final String classNameAndMethod = element.getClassName() + "." + element.getMethodName();
139+
for (final Pattern exclusionPattern : excludes) {
140+
if (exclusionPattern.matcher(classNameAndMethod).find()) {
141+
return false;
142+
}
143+
}
144+
return true;
145+
}
146+
}
147+
148+
private static final class Group implements StackElementFilter {
149+
150+
private final StackElementFilter[] filters;
151+
152+
public Group(StackElementFilter[] filters) {
153+
this.filters = filters;
154+
}
155+
156+
@Override
157+
public boolean accept(StackTraceElement element) {
158+
for (StackElementFilter filter : filters) {
159+
if (!filter.accept(element)) {
160+
return false;
161+
}
162+
}
163+
return true;
164+
}
165+
}
166+
167+
}

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import ch.qos.logback.classic.pattern.ThrowableHandlingConverter;
1111
import ch.qos.logback.classic.spi.ILoggingEvent;
12+
import ch.qos.logback.classic.spi.IThrowableProxy;
13+
import ch.qos.logback.classic.spi.ThrowableProxy;
1214
import ch.qos.logback.core.encoder.EncoderBase;
1315
import io.avaje.json.PropertyNames;
1416
import io.avaje.json.simple.SimpleMapper;
@@ -20,6 +22,7 @@ public final class JsonEncoder extends EncoderBase<ILoggingEvent> {
2022
private final JsonStream json;
2123
private final Map<String, String> customFieldsMap = new HashMap<>();
2224
private final PropertyNames properties;
25+
private final StackHasher stackHasher;
2326
private ThrowableHandlingConverter throwableConverter = new ShortenedThrowableConverter();
2427

2528
private DateTimeFormatter formatter;
@@ -29,12 +32,14 @@ public final class JsonEncoder extends EncoderBase<ILoggingEvent> {
2932
private int fieldExtra;
3033
private String component;
3134
private String environment;
35+
private boolean includeStackHash = true;
3236

3337
public JsonEncoder() {
3438
this.json = JsonStream.builder().build();
35-
this.properties = json.properties("component", "env", "timestamp", "level", "logger", "message", "thread", "stacktrace");
36-
this.component = System.getenv("COMPONENT");
39+
this.properties = json.properties("component", "env", "timestamp", "level", "logger", "message", "thread", "stackhash", "stacktrace");
40+
this.component = Eval.defaultComponent();
3741
this.environment = System.getenv("ENVIRONMENT");
42+
this.stackHasher = new StackHasher(StackElementFilter.builder().allFilters().build());
3843
}
3944

4045
@Override
@@ -96,7 +101,15 @@ public byte[] encode(ILoggingEvent event) {
96101
writer.name(6);
97102
writer.value(threadName);
98103
if (!stackTraceBody.isEmpty()) {
99-
writer.name(7);
104+
if (includeStackHash) {
105+
IThrowableProxy throwableProxy = event.getThrowableProxy();
106+
if (throwableProxy instanceof ThrowableProxy) {
107+
String hash = stackHasher.hexHash(((ThrowableProxy) throwableProxy).getThrowable());
108+
writer.name(7);
109+
writer.value(hash);
110+
}
111+
}
112+
writer.name(8);
100113
writer.value(stackTraceBody);
101114
}
102115
customFieldsMap.forEach((k, v) -> {
@@ -113,12 +126,16 @@ public byte[] encode(ILoggingEvent event) {
113126
return outputStream.toByteArray();
114127
}
115128

129+
public void setIncludeStackHash(boolean includeStackHash) {
130+
this.includeStackHash = includeStackHash;
131+
}
132+
116133
public void setComponent(String component) {
117-
this.component = component;
134+
this.component = Eval.eval(component);
118135
}
119136

120137
public void setEnvironment(String environment) {
121-
this.environment = environment;
138+
this.environment = Eval.eval(environment);
122139
}
123140

124141
public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) {
@@ -130,7 +147,12 @@ public void setCustomFields(String customFields) {
130147
return;
131148
}
132149
var mapper = SimpleMapper.builder().jsonStream(json).build();
133-
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+
});
134156
}
135157

136158
public void setTimestampPattern(String pattern) {

0 commit comments

Comments
 (0)