All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy