kamon.apm.logback.KamonApmAppender Maven / Gradle / Ivy
package kamon.apm.logback;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import kamon.apm.client.KamonApmClient;
import kamon.apm.client.protocol.Log;
import kamon.apm.client.protocol.Tag;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import org.slf4j.event.KeyValuePair;
public class KamonApmAppender extends UnsynchronizedAppenderBase {
private String apiKey;
private String baseUrl = "https://ingestion.apm.kamon.io";
private List resourceTags = new CopyOnWriteArrayList<>();
private int queueSize = 1000;
private int flushIntervalSeconds = 2;
private BlockingQueue pendingEvents;
private KamonApmClient apmClient;
private ScheduledExecutorService flusher;
private ScheduledFuture> flushSchedule;
@Override
public void start() {
Optional serviceNameTag = resourceTags.stream().filter(t -> t.key.equals("service.name")).findFirst();
if (serviceNameTag.isEmpty()) {
addWarn("No service name specified in the Kamon APM appender, defaulting to unknown_service");
resourceTags.add(makeTag("service.name", "unknown_service"));
}
if (apiKey == null) {
addError("No API key set for the Kamon APM appender, the appender will not process any events");
return;
}
this.apmClient = new KamonApmClient(this.baseUrl, this.apiKey, resourceTags.toArray(Tag[]::new));
this.pendingEvents = new ArrayBlockingQueue<>(queueSize);
this.flusher = Executors.newScheduledThreadPool(1);
this.flushSchedule = this.flusher.scheduleAtFixedRate(new Runnable() {
public void run() {
drainAndSend();
}
}, this.flushIntervalSeconds, this.flushIntervalSeconds, TimeUnit.SECONDS);
super.start();
}
@Override
public void stop() {
drainAndSend();
this.flushSchedule.cancel(false);
this.flusher.shutdown();
super.stop();
}
@Override
protected void append(ILoggingEvent event) {
if (!this.pendingEvents.offer(event)) {
// Ideally the pending events queue is big enough that we don't need to
// flush in the appending path but if the queue is full we drain it right
// away.
drainAndSend();
this.pendingEvents.offer(event);
}
}
private void drainAndSend() {
ArrayList eventsToSend = new ArrayList<>(queueSize);
this.pendingEvents.drainTo(eventsToSend);
Log[] apmLogEvents = eventsToSend.stream()
.map(event -> {
Log apmLog = new Log();
apmLog.setTime(event.getInstant());
apmLog.setMessage(event.getFormattedMessage());
apmLog.setSeverity(mapSeverity(event.getLevel()));
List eventTags = new LinkedList<>();
eventTags.add(makeTag("thread.name", event.getThreadName()));
eventTags.add(makeTag("logger.name", event.getLoggerName()));
event.getMDCPropertyMap().forEach((key, value) -> {
if (key != null && value != null)
eventTags.add(makeTag(key, value));
});
List kvPairs = event.getKeyValuePairs();
if (kvPairs != null) {
kvPairs.forEach((pair) -> {
eventTags.add(makeTag(pair.key, pair.value.toString()));
});
}
IThrowableProxy throwable = event.getThrowableProxy();
if (throwable != null) {
String stackTrace = Arrays.stream(throwable.getStackTraceElementProxyArray())
.map(Object::toString)
.collect(Collectors.joining("\n"));
eventTags.add(makeTag("exception.type", throwable.getClassName()));
eventTags.add(makeTag("exception.message", throwable.getMessage()));
eventTags.add(makeTag("exception.stacktrace", stackTrace));
}
apmLog.setTags(eventTags.toArray(Tag[]::new));
return apmLog;
})
.toArray(Log[]::new);
this.apmClient.postLogs(apmLogEvents);
}
private Tag makeTag(String key, String value) {
Tag tag = new Tag();
tag.setKey(key);
tag.setValue(value == null ? "null" : value);
return tag;
}
private byte mapSeverity(Level level) {
if (level == Level.INFO)
return 3;
else if (level == Level.DEBUG)
return 2;
else if (level == Level.WARN)
return 4;
else if (level == Level.ERROR)
return 5;
else if (level == Level.DEBUG)
return 2;
else if (level == Level.TRACE)
return 1;
else
return 7;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void addResourceTag(Tag tag) {
this.resourceTags.add(tag);
}
public void setQueueSize(int queueSize) {
this.queueSize = queueSize;
}
public void setFlushIntervalSeconds(int flushIntervalSeconds) {
this.flushIntervalSeconds = flushIntervalSeconds;
}
}