Initial commit
This commit is contained in:
commit
976b2d27a5
7 changed files with 640 additions and 0 deletions
|
@ -0,0 +1,74 @@
|
|||
package io.kokuwa.micronaut.logging;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.MDC;
|
||||
import org.slf4j.Marker;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.turbo.TurboFilter;
|
||||
import ch.qos.logback.core.Context;
|
||||
import ch.qos.logback.core.spi.FilterReply;
|
||||
|
||||
/**
|
||||
* Filter for log levels based on mdc.
|
||||
*
|
||||
* @author Stephan Schnabel
|
||||
*/
|
||||
public class MDCTurboFilter extends TurboFilter {
|
||||
|
||||
private final String key;
|
||||
private final Map<String, Boolean> cache = new HashMap<>();
|
||||
private final Set<String> loggers = new HashSet<>();
|
||||
private final Set<String> values = new HashSet<>();
|
||||
private Level level;
|
||||
|
||||
public MDCTurboFilter(String name, String key, Context context) {
|
||||
this.key = key;
|
||||
this.level = Level.TRACE;
|
||||
this.setName(name);
|
||||
this.setContext(context);
|
||||
}
|
||||
|
||||
public MDCTurboFilter setLoggers(Set<String> loggers) {
|
||||
this.cache.clear();
|
||||
this.loggers.clear();
|
||||
this.loggers.addAll(loggers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MDCTurboFilter setValues(Set<String> values) {
|
||||
this.values.clear();
|
||||
this.values.addAll(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MDCTurboFilter setLevel(Level level) {
|
||||
this.level = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
|
||||
|
||||
if (logger == null || !isStarted() || values.isEmpty() || loggers.isEmpty()) {
|
||||
return FilterReply.NEUTRAL;
|
||||
}
|
||||
|
||||
var value = MDC.get(key);
|
||||
if (value == null || !values.contains(value)) {
|
||||
return FilterReply.NEUTRAL;
|
||||
}
|
||||
|
||||
var isLoggerIncluded = !cache.computeIfAbsent(logger.getName(), k -> loggers.stream().anyMatch(k::startsWith));
|
||||
if (isLoggerIncluded) {
|
||||
return FilterReply.NEUTRAL;
|
||||
}
|
||||
|
||||
return level.isGreaterOrEqual(this.level) ? FilterReply.ACCEPT : FilterReply.NEUTRAL;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package io.kokuwa.micronaut.logging;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import io.micronaut.context.annotation.BootstrapContextCompatible;
|
||||
import io.micronaut.context.annotation.Context;
|
||||
import io.micronaut.context.annotation.Requires;
|
||||
import io.micronaut.context.env.Environment;
|
||||
import io.micronaut.context.event.ApplicationEventListener;
|
||||
import io.micronaut.core.annotation.Internal;
|
||||
import io.micronaut.core.type.Argument;
|
||||
import io.micronaut.runtime.context.scope.refresh.RefreshEvent;
|
||||
|
||||
/**
|
||||
* Configure mdc filter.
|
||||
*
|
||||
* @author Stephan Schnabel
|
||||
*/
|
||||
@BootstrapContextCompatible
|
||||
@Context
|
||||
@Requires(classes = LoggerContext.class)
|
||||
@Requires(property = MDCTurboFilterConfigurer.LOGGER_MDCS_PROPERTY_PREFIX)
|
||||
@Internal
|
||||
public class MDCTurboFilterConfigurer implements ApplicationEventListener<RefreshEvent> {
|
||||
|
||||
public static final String LOGGER_MDCS_PROPERTY_PREFIX = "logger.mdc";
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MDCTurboFilterConfigurer.class);
|
||||
private final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
|
||||
private final Environment environment;
|
||||
|
||||
public MDCTurboFilterConfigurer(Environment environment) {
|
||||
this.environment = environment;
|
||||
configure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(RefreshEvent event) {
|
||||
if (event.getSource().keySet().stream().anyMatch(key -> key.startsWith(LOGGER_MDCS_PROPERTY_PREFIX))) {
|
||||
configure();
|
||||
}
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
for (var name : environment.getPropertyEntries(LOGGER_MDCS_PROPERTY_PREFIX)) {
|
||||
|
||||
var prefix = LOGGER_MDCS_PROPERTY_PREFIX + "." + name + ".";
|
||||
var key = environment.getProperty(prefix + "key", String.class, name);
|
||||
var loggers = environment.getProperty(prefix + "loggers", Argument.setOf(String.class)).orElseGet(Set::of);
|
||||
var values = environment.getProperty(prefix + "values", Argument.setOf(String.class)).orElseGet(Set::of);
|
||||
var level = Level.valueOf(environment.getProperty(prefix + "level", String.class, Level.TRACE.toString()));
|
||||
|
||||
getFilter(name, key).setLoggers(loggers).setValues(values).setLevel(level);
|
||||
|
||||
log.info("Configured MDC filter {} for key {} with level {}.", name, key, level);
|
||||
}
|
||||
}
|
||||
|
||||
private MDCTurboFilter getFilter(String name, String key) {
|
||||
|
||||
// get filter
|
||||
|
||||
var filterName = LOGGER_MDCS_PROPERTY_PREFIX + "." + key;
|
||||
var filterOptional = context.getTurboFilterList().stream()
|
||||
.filter(f -> Objects.equals(filterName, f.getName()))
|
||||
.filter(MDCTurboFilter.class::isInstance)
|
||||
.map(MDCTurboFilter.class::cast)
|
||||
.findAny();
|
||||
if (filterOptional.isPresent()) {
|
||||
return filterOptional.get();
|
||||
}
|
||||
|
||||
// add filter
|
||||
|
||||
var filter = new MDCTurboFilter(name, key, context);
|
||||
filter.start();
|
||||
context.addTurboFilter(filter);
|
||||
return filter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package io.kokuwa.micronaut.logging;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.contrib.jackson.JacksonJsonFormatter;
|
||||
import ch.qos.logback.contrib.json.classic.JsonLayout;
|
||||
import io.micronaut.core.util.StringUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* Stackdriver layout.
|
||||
*
|
||||
* @author Stephan Schnabel
|
||||
* @see "https://cloud.google.com/logging/docs/agent/configuration#process-payload"
|
||||
* @see "https://cloud.google.com/error-reporting/reference/rest/v1beta1/ServiceContext"
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class StackdriverJsonLayout extends JsonLayout {
|
||||
|
||||
private static final String TIMESTAMP_ATTR_NAME = "time";
|
||||
private static final String SEVERITY_ATTR_NAME = "severity";
|
||||
|
||||
private Map<String, String> serviceContext;
|
||||
private String serviceName;
|
||||
private String serviceVersion;
|
||||
private boolean includeExceptionInMessage;
|
||||
|
||||
public StackdriverJsonLayout() {
|
||||
appendLineSeparator = true;
|
||||
includeContextName = false;
|
||||
includeMessage = true;
|
||||
includeExceptionInMessage = true;
|
||||
setJsonFormatter(new JacksonJsonFormatter());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> toJsonMap(ILoggingEvent event) {
|
||||
var map = new LinkedHashMap<String, Object>();
|
||||
add(TIMESTAMP_ATTR_NAME, includeTimestamp, Instant.ofEpochMilli(event.getTimeStamp()).toString(), map);
|
||||
add(SEVERITY_ATTR_NAME, includeLevel, String.valueOf(event.getLevel()), map);
|
||||
add(THREAD_ATTR_NAME, includeThreadName, event.getThreadName(), map);
|
||||
add(CONTEXT_ATTR_NAME, includeContextName, event.getLoggerContextVO().getName(), map);
|
||||
add(LOGGER_ATTR_NAME, includeLoggerName, event.getLoggerName(), map);
|
||||
addMap(MDC_ATTR_NAME, includeMDC, event.getMDCPropertyMap(), map);
|
||||
add(FORMATTED_MESSAGE_ATTR_NAME, includeFormattedMessage, event.getFormattedMessage(), map);
|
||||
add(MESSAGE_ATTR_NAME, includeMessage, event.getMessage(), map);
|
||||
addThrowableInfo(JsonLayout.EXCEPTION_ATTR_NAME, includeException, event, map);
|
||||
addServiceContext(map);
|
||||
return map;
|
||||
}
|
||||
|
||||
private void addServiceContext(Map<String, Object> map) {
|
||||
if (serviceContext == null) {
|
||||
serviceContext = new HashMap<>(2);
|
||||
if (StringUtils.isNotEmpty(serviceName)) {
|
||||
serviceContext.put("service", serviceName);
|
||||
}
|
||||
if (StringUtils.isNotEmpty(serviceVersion)) {
|
||||
serviceContext.put("version", serviceVersion);
|
||||
}
|
||||
}
|
||||
if (!serviceContext.isEmpty()) {
|
||||
map.put("serviceContext", serviceContext);
|
||||
}
|
||||
}
|
||||
}
|
25
src/main/resources/logback.xml
Normal file
25
src/main/resources/logback.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<property resource="META-INF/build-info.properties" />
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<withJansi>true</withJansi>
|
||||
<encoder>
|
||||
<pattern>%cyan(%d{HH:mm:ss.SSS}) %gray(%-6.6thread) %highlight(%-5level) %magenta(%32logger{32}) %mdc %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<appender name="STACKDRIVER" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
|
||||
<layout class="io.kokuwa.micronaut.logging.StackdriverJsonLayout">
|
||||
<serviceName>${serviceName}</serviceName>
|
||||
<serviceVersion>${serviceVersion}</serviceVersion>
|
||||
</layout>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="${LOGBACK_APPENDER:-CONSOLE}" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
Loading…
Add table
Add a link
Reference in a new issue