com.turbospaces.logging.SentryAppender Maven / Gradle / Ivy
package com.turbospaces.logging;
import java.time.Duration;
import java.util.AbstractMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Date;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import org.apache.commons.lang3.StringUtils;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.sentry.event.Event;
import io.sentry.event.EventBuilder;
import io.sentry.event.interfaces.ExceptionInterface;
import io.sentry.event.interfaces.MessageInterface;
import io.sentry.event.interfaces.SentryException;
import io.sentry.event.interfaces.StackTraceInterface;
public class SentryAppender extends AbstractAppender {
private static final String RATE_LIMITER_SENTRY_APPENDER_KEY_PREFIX = "rate-limiter-sentry-appender.";
private static final String DOT_REGEX = "\\.";
private static final String UNDERSCORE = "_";
public static final Duration PERIOD = Duration.ofMinutes(1);
public static final int COUNT = 50;
@Override
public void start() {
AlertLoggingFilter filter = (AlertLoggingFilter) context.getObject(Logback.SENTRY_LOGGING_FILTER);
if (Objects.nonNull(filter)) {
addFilter(filter);
}
super.start();
//
// ~ mark started
//
started = true;
//
// ~ effectively start
//
for (int i = 0; i < threads; i++) {
WorkerThread worker = workers[i];
worker.start();
}
}
@Override
protected boolean dryRun() {
return alertsDryRun();
}
@Override
protected void sendBulk(List list) {
if (Objects.nonNull(getSentry())) {
for (SequencedDeferredEvent next : list) {
ILoggingEvent event = next.event();
String logEventKey = generateLogEventKey(event);
if (acquirePermission(logEventKey)) {
getSentry().sendEvent(writeBody(next));
} else {
if (Objects.nonNull(event.getThrowableProxy())) {
addError(event.getFormattedMessage());
}
}
}
}
}
private static String generateLogEventKey(ILoggingEvent loggingEvent) {
String logEventKey = loggingEvent.getLoggerName();
if (loggingEvent.getThrowableProxy() != null && StringUtils.isNotBlank(loggingEvent.getThrowableProxy().getClassName())) {
String exceptionClassName = loggingEvent.getThrowableProxy().getClassName();
String[] split = exceptionClassName.split(DOT_REGEX);
String exceptionClassSimpleName = split.length > 0 ? split[split.length - 1] : StringUtils.EMPTY;
logEventKey = logEventKey + UNDERSCORE + exceptionClassSimpleName;
}
return logEventKey;
}
public EventBuilder writeBody(SequencedDeferredEvent data) {
EventBuilder builder = new EventBuilder();
builder.withTimestamp(new Date(data.getTimeStamp()));
builder.withMessage(data.getFormattedMessage());
builder.withLogger(data.getLoggerName());
builder.withLevel(formatLevel(data.getLevel()));
builder.withExtra(Logback.SEQUENCE, data.seq());
//
// ~ write all formatted values
//
for (DocumentProperty field : properties.getProperties()) {
String formatted = field.format(data);
if (StringUtils.isEmpty(formatted)) {} else {
builder.withTag(field.getName(), formatted);
}
}
//
// ~ argument array
//
if (data.getArgumentArray() != null) {
List args = new ArrayList<>();
for (Object argument : data.getArgumentArray()) {
args.add(argument != null ? argument.toString() : null);
}
builder.withSentryInterface(new MessageInterface(data.getMessage(), args, data.getFormattedMessage()));
}
//
// ~ exception
//
if (data.getThrowableProxy() != null) {
builder.withSentryInterface(new ExceptionInterface(extractExceptionQueue(data)));
} else if (data.getCallerData().length > 0) {
builder.withSentryInterface(new StackTraceInterface(data.getCallerData()));
}
//
// ~ write MDC values
//
Map mdc = data.getMDCPropertyMap();
if (mdc != null) {
for (Entry entry : mdc.entrySet()) {
boolean toInclude = true;
if (mdcNames.isEmpty()) {
} else {
toInclude = mdcNames.contains(entry.getKey());
}
if (toInclude) {
if (StringUtils.isNotEmpty(entry.getValue())) {
builder.withTag(entry.getKey(), entry.getValue());
}
}
}
}
return builder;
}
private static Deque extractExceptionQueue(ILoggingEvent event) {
IThrowableProxy throwable = event.getThrowableProxy();
Deque exceptions = new ArrayDeque<>();
Set circularityDetector = new HashSet<>();
StackTraceElement[] enclosingStackTrace = {};
// Stack the exceptions to send them in the reverse order
while (throwable != null) {
if (!circularityDetector.add(throwable)) {
break;
}
StackTraceElement[] stackTraceElements = toStackTraceElements(throwable);
StackTraceInterface stackTrace = new StackTraceInterface(stackTraceElements, enclosingStackTrace);
Map.Entry mapping = extractPackageAndClassName(throwable.getClassName());
exceptions.push(new SentryException(throwable.getMessage(), mapping.getKey(), mapping.getValue(), stackTrace));
enclosingStackTrace = stackTraceElements;
throwable = throwable.getCause();
}
return exceptions;
}
private static StackTraceElement[] toStackTraceElements(IThrowableProxy proxy) {
StackTraceElementProxy[] elementProxies = proxy.getStackTraceElementProxyArray();
StackTraceElement[] elements = new StackTraceElement[elementProxies.length];
for (int i = 0; i < elementProxies.length; i++) {
elements[i] = elementProxies[i].getStackTraceElement();
}
return elements;
}
private static Map.Entry extractPackageAndClassName(String canonicalClassName) {
Map.Entry mapping;
try {
Class> exceptionClass = Class.forName(canonicalClassName);
Package exceptionPackage = exceptionClass.getPackage();
String k = exceptionPackage != null ? exceptionPackage.getName() : SentryException.DEFAULT_PACKAGE_NAME;
String v = exceptionClass.getSimpleName();
mapping = new AbstractMap.SimpleEntry<>(k, v);
} catch (ClassNotFoundException e) {
int lastDot = canonicalClassName.lastIndexOf('.');
if (lastDot != -1) {
String k = canonicalClassName.substring(0, lastDot);
String v = canonicalClassName.substring(lastDot);
mapping = new AbstractMap.SimpleEntry<>(k, v);
} else {
mapping = new AbstractMap.SimpleEntry<>(SentryException.DEFAULT_PACKAGE_NAME, canonicalClassName);
}
}
return mapping;
}
private static Event.Level formatLevel(Level level) {
if (level.isGreaterOrEqual(Level.ERROR)) {
return Event.Level.ERROR;
} else if (level.isGreaterOrEqual(Level.WARN)) {
return Event.Level.WARNING;
} else if (level.isGreaterOrEqual(Level.INFO)) {
return Event.Level.INFO;
} else if (level.isGreaterOrEqual(Level.ALL)) {
return Event.Level.DEBUG;
} else {
return null;
}
}
private boolean acquirePermission(String key) {
String rateLimiterKey = RATE_LIMITER_SENTRY_APPENDER_KEY_PREFIX + key;
RateLimiterConfig current = getRateLimiterRegistry().getConfiguration(rateLimiterKey).orElseGet(new Supplier() {
@Override
public RateLimiterConfig get() {
RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
.limitRefreshPeriod(PERIOD)
.limitForPeriod(COUNT)
.timeoutDuration(Duration.ZERO)
.build();
getRateLimiterRegistry().addConfiguration(rateLimiterKey, rateLimiterConfig);
return rateLimiterConfig;
}
});
return getRateLimiterRegistry().rateLimiter(rateLimiterKey, current).acquirePermission();
}
}