Skip to content
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