Skip to content

Change StackElementFilter into an interface and add a Builder for common filters #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/main/java/io/avaje/logback/encoder/Eval.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.avaje.logback.encoder;

/**
* Helper to evaluate expressions like {@code "${my.property}"}, {@code "${MY_PROPERTY:someDefaultValue}"} etc.
*/
final class Eval {

/**
* Return the default component value using environment variables.
* <p>
* For K8s this derives the component name from the HOSTNAME.
*/
static String defaultComponent() {
String component = System.getenv("COMPONENT");
if (component != null) {
return component;
}
if (System.getenv("KUBERNETES_PORT") != null) {
// in k8s we can default off the hostname
return k8sComponent(System.getenv("HOSTNAME"));
}
return null;
}

static String k8sComponent(String hostname) {
if (hostname == null) {
return null;
}
int p0 = hostname.lastIndexOf('-');
if (p0 > 1) {
int p1 = hostname.lastIndexOf('-', p0 - 1);
if (p1 > 0) {
return hostname.substring(0, p1);
}
}
return null;
}

/**
* Evaluate the expression and otherwise return the original value.
* <p>
* Expressions are in the form {@code ${key:defaultValue}}
* <p>
* Examples:
* <pre>{@code
*
* ${APP_ENV:localDev}
* ${system.name:unknown}
* ${MY_COMPONENT:myDefaultValue}
*
* }</pre>
*/
static String eval(String value) {
if (value == null || !value.startsWith("${") || !value.endsWith("}")) {
return value;
}
String raw = value.substring(2, value.length() - 1);
String[] split = raw.split(":", 2);
String key = split[0];
String val = System.getProperty(key);
if (val != null) {
return val;
}
val = System.getenv(key);
if (val != null) {
return val;
}
val = System.getProperty(toSystemPropertyKey(key));
if (val != null) {
return val;
}
return split.length == 2 ? split[1] : value;
}

static String toSystemPropertyKey(String key) {
return key.replace('_', '.').toLowerCase();
}
}
167 changes: 167 additions & 0 deletions src/main/java/io/avaje/logback/encoder/FilterBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package io.avaje.logback.encoder;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

final class FilterBuilder implements StackElementFilter.Builder {

private final List<StackElementFilter> filters = new ArrayList<>();

@Override
public StackElementFilter.Builder generated() {
filters.add(new Generated());
return this;
}

@Override
public StackElementFilter.Builder reflectiveInvocation() {
filters.add(new ReflectiveInvocation());
return this;
}

@Override
public StackElementFilter.Builder jdkInternals() {
filters.add(new JDKInternals());
return this;
}

@Override
public StackElementFilter.Builder spring() {
filters.add(new SpringFilter());
return this;
}

@Override
public StackElementFilter.Builder byPattern(List<Pattern> excludes) {
if (excludes != null && !excludes.isEmpty()) {
filters.add(new PatternFilter(excludes));
}
return this;
}

@Override
public StackElementFilter.Builder allFilters() {
generated();
reflectiveInvocation();
jdkInternals();
spring();
return this;
}

@Override
public StackElementFilter build() {
if (filters.isEmpty()) {
return StackElementFilter.any();
}
return new Group(filters.toArray(new StackElementFilter[0]));
}

private static final class Generated implements StackElementFilter {

@Override
public boolean accept(StackTraceElement element) {
String className = element.getClassName();
return !className.contains("$$FastClassByCGLIB$$")
&& !className.contains("$$EnhancerBySpringCGLIB$$");
}
}

private static final class ReflectiveInvocation implements StackElementFilter {

@Override
public boolean accept(StackTraceElement element) {
String methodName = element.getMethodName();
if (methodName.equals("invoke")) {
String className = element.getClassName();
return !className.startsWith("sun.reflect.")
&& !className.startsWith("java.lang.reflect.")
&& !className.startsWith("net.sf.cglib.proxy.MethodProxy");
}
return true;
}
}

private static final class JDKInternals implements StackElementFilter {

@Override
public boolean accept(StackTraceElement element) {
String className = element.getClassName();
return !className.startsWith("com.sun.")
&& !className.startsWith("sun.net.");
}
}

private static final class SpringFilter implements StackElementFilter {

private static final String[] MATCHES = {
"org.springframework.cglib.",
"org.springframework.transaction.",
"org.springframework.validation.",
"org.springframework.app.",
"org.springframework.aop.",
"org.springframework.ws.",
"org.springframework.web.",
"org.springframework.transaction"
};

@Override
public boolean accept(StackTraceElement element) {
String className = element.getClassName();
if (className.startsWith("org.springframework")) {
for (String match : MATCHES) {
if (className.startsWith(match)) {
return false;
}
}
return true;
}
if (className.startsWith("org.apache")) {
return !className.startsWith("org.apache.tomcat.")
&& !className.startsWith("org.apache.catalina.")
&& !className.startsWith("org.apache.coyote.");
}
return true;
}
}

private static final class PatternFilter implements StackElementFilter {

private final Pattern[] excludes;

PatternFilter(final List<Pattern> excludes) {
this.excludes = excludes.toArray(new Pattern[0]);
}

@Override
public boolean accept(StackTraceElement element) {
final String classNameAndMethod = element.getClassName() + "." + element.getMethodName();
for (final Pattern exclusionPattern : excludes) {
if (exclusionPattern.matcher(classNameAndMethod).find()) {
return false;
}
}
return true;
}
}

private static final class Group implements StackElementFilter {

private final StackElementFilter[] filters;

public Group(StackElementFilter[] filters) {
this.filters = filters;
}

@Override
public boolean accept(StackTraceElement element) {
for (StackElementFilter filter : filters) {
if (!filter.accept(element)) {
return false;
}
}
return true;
}
}

}
34 changes: 28 additions & 6 deletions src/main/java/io/avaje/logback/encoder/JsonEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import ch.qos.logback.classic.pattern.ThrowableHandlingConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.encoder.EncoderBase;
import io.avaje.json.PropertyNames;
import io.avaje.json.simple.SimpleMapper;
Expand All @@ -20,6 +22,7 @@ public final class JsonEncoder extends EncoderBase<ILoggingEvent> {
private final JsonStream json;
private final Map<String, String> customFieldsMap = new HashMap<>();
private final PropertyNames properties;
private final StackHasher stackHasher;
private ThrowableHandlingConverter throwableConverter = new ShortenedThrowableConverter();

private DateTimeFormatter formatter;
Expand All @@ -29,12 +32,14 @@ public final class JsonEncoder extends EncoderBase<ILoggingEvent> {
private int fieldExtra;
private String component;
private String environment;
private boolean includeStackHash = true;

public JsonEncoder() {
this.json = JsonStream.builder().build();
this.properties = json.properties("component", "env", "timestamp", "level", "logger", "message", "thread", "stacktrace");
this.component = System.getenv("COMPONENT");
this.properties = json.properties("component", "env", "timestamp", "level", "logger", "message", "thread", "stackhash", "stacktrace");
this.component = Eval.defaultComponent();
this.environment = System.getenv("ENVIRONMENT");
this.stackHasher = new StackHasher(StackElementFilter.builder().allFilters().build());
}

@Override
Expand Down Expand Up @@ -96,7 +101,15 @@ public byte[] encode(ILoggingEvent event) {
writer.name(6);
writer.value(threadName);
if (!stackTraceBody.isEmpty()) {
writer.name(7);
if (includeStackHash) {
IThrowableProxy throwableProxy = event.getThrowableProxy();
if (throwableProxy instanceof ThrowableProxy) {
String hash = stackHasher.hexHash(((ThrowableProxy) throwableProxy).getThrowable());
writer.name(7);
writer.value(hash);
}
}
writer.name(8);
writer.value(stackTraceBody);
}
customFieldsMap.forEach((k, v) -> {
Expand All @@ -113,12 +126,16 @@ public byte[] encode(ILoggingEvent event) {
return outputStream.toByteArray();
}

public void setIncludeStackHash(boolean includeStackHash) {
this.includeStackHash = includeStackHash;
}

public void setComponent(String component) {
this.component = component;
this.component = Eval.eval(component);
}

public void setEnvironment(String environment) {
this.environment = environment;
this.environment = Eval.eval(environment);
}

public void setThrowableConverter(ThrowableHandlingConverter throwableConverter) {
Expand All @@ -130,7 +147,12 @@ public void setCustomFields(String customFields) {
return;
}
var mapper = SimpleMapper.builder().jsonStream(json).build();
mapper.map().fromJson(customFields).forEach((k, v) -> customFieldsMap.put(k, mapper.toJson(v)));
mapper.map().fromJson(customFields).forEach((key, value) -> {
if (value instanceof String) {
value = Eval.eval((String) value);
}
customFieldsMap.put(key, mapper.toJson(value));
});
}

public void setTimestampPattern(String pattern) {
Expand Down
Loading
Loading