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

io.sentry.logback.SentryAppender Maven / Gradle / Ivy

There is a newer version: 8.0.0-alpha.4
Show newest version
package io.sentry.logback;

import static io.sentry.TypeCheckHint.LOGBACK_LOGGING_EVENT;
import static io.sentry.TypeCheckHint.SENTRY_SYNTHETIC_EXCEPTION;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import ch.qos.logback.core.encoder.Encoder;
import com.jakewharton.nopen.annotation.Open;
import io.sentry.Breadcrumb;
import io.sentry.DateUtils;
import io.sentry.Hint;
import io.sentry.HubAdapter;
import io.sentry.ITransportFactory;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryIntegrationPackageStorage;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.Message;
import io.sentry.protocol.SdkVersion;
import io.sentry.util.CollectionUtils;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** Appender for logback in charge of sending the logged events to a Sentry server. */
@Open
public class SentryAppender extends UnsynchronizedAppenderBase {
  public static final String MECHANISM_TYPE = "LogbackSentryAppender";
  // WARNING: Do not use these options in here, they are only to be used for startup
  private @NotNull SentryOptions options = new SentryOptions();
  private @Nullable ITransportFactory transportFactory;
  private @NotNull Level minimumBreadcrumbLevel = Level.INFO;
  private @NotNull Level minimumEventLevel = Level.ERROR;
  private @Nullable Encoder encoder;

  @Override
  public void start() {
    // NOTE: logback.xml properties will only be applied if the SDK has not yet been initialized
    if (!Sentry.isEnabled()) {
      if (options.getDsn() == null || !options.getDsn().endsWith("_IS_UNDEFINED")) {
        options.setEnableExternalConfiguration(true);
        options.setSentryClientName(
            BuildConfig.SENTRY_LOGBACK_SDK_NAME + "/" + BuildConfig.VERSION_NAME);
        options.setSdkVersion(createSdkVersion(options));
        Optional.ofNullable(transportFactory).ifPresent(options::setTransportFactory);
        try {
          Sentry.init(options);
        } catch (IllegalArgumentException e) {
          addWarn("Failed to init Sentry during appender initialization: " + e.getMessage());
        }
      } else {
        options
            .getLogger()
            .log(SentryLevel.WARNING, "DSN is null. SentryAppender is not being initialized");
      }
    }
    addPackageAndIntegrationInfo();
    super.start();
  }

  @Override
  protected void append(@NotNull ILoggingEvent eventObject) {
    if (eventObject.getLevel().isGreaterOrEqual(minimumEventLevel)) {
      final Hint hint = new Hint();
      hint.set(SENTRY_SYNTHETIC_EXCEPTION, eventObject);

      Sentry.captureEvent(createEvent(eventObject), hint);
    }
    if (eventObject.getLevel().isGreaterOrEqual(minimumBreadcrumbLevel)) {
      final Hint hint = new Hint();
      hint.set(LOGBACK_LOGGING_EVENT, eventObject);

      Sentry.addBreadcrumb(createBreadcrumb(eventObject), hint);
    }
  }

  /**
   * Creates {@link SentryEvent} from Logback's {@link ILoggingEvent}.
   *
   * @param loggingEvent the logback event
   * @return the sentry event
   */
  // for the Android compatibility we must use old Java Date class
  @SuppressWarnings("JdkObsolete")
  protected @NotNull SentryEvent createEvent(@NotNull ILoggingEvent loggingEvent) {
    final SentryEvent event = new SentryEvent(DateUtils.getDateTime(loggingEvent.getTimeStamp()));
    final Message message = new Message();

    // if encoder is set we treat message+params as PII as encoders may be used to mask/strip PII
    if (encoder == null || options.isSendDefaultPii()) {
      message.setMessage(loggingEvent.getMessage());
      message.setParams(toParams(loggingEvent.getArgumentArray()));
    }

    message.setFormatted(formatted(loggingEvent));
    event.setMessage(message);
    event.setLogger(loggingEvent.getLoggerName());
    event.setLevel(formatLevel(loggingEvent.getLevel()));

    final ThrowableProxy throwableInformation = (ThrowableProxy) loggingEvent.getThrowableProxy();
    if (throwableInformation != null) {
      final Mechanism mechanism = new Mechanism();
      mechanism.setType(MECHANISM_TYPE);
      final Throwable mechanismException =
          new ExceptionMechanismException(
              mechanism, throwableInformation.getThrowable(), Thread.currentThread());
      event.setThrowable(mechanismException);
    }

    if (loggingEvent.getThreadName() != null) {
      event.setExtra("thread_name", loggingEvent.getThreadName());
    }

    if (loggingEvent.getMarker() != null) {
      event.setExtra("marker", loggingEvent.getMarker().toString());
    }

    // remove keys with null values, there is no sense to send these keys to Sentry
    final Map mdcProperties =
        CollectionUtils.filterMapEntries(
            loggingEvent.getMDCPropertyMap(), entry -> entry.getValue() != null);
    if (!mdcProperties.isEmpty()) {
      // get tags from HubAdapter options to allow getting the correct tags if Sentry has been
      // initialized somewhere else
      final List contextTags = HubAdapter.getInstance().getOptions().getContextTags();
      if (!contextTags.isEmpty()) {
        for (final String contextTag : contextTags) {
          // if mdc tag is listed in SentryOptions, apply as event tag
          if (mdcProperties.containsKey(contextTag)) {
            event.setTag(contextTag, mdcProperties.get(contextTag));
            // remove from all tags applied to logging event
            mdcProperties.remove(contextTag);
          }
        }
      }
      // put the rest of mdc tags in contexts
      if (!mdcProperties.isEmpty()) {
        event.getContexts().put("MDC", mdcProperties);
      }
    }

    return event;
  }

  private String formatted(@NotNull ILoggingEvent loggingEvent) {
    if (encoder != null) {
      try {
        return new String(encoder.encode(loggingEvent), StandardCharsets.UTF_8);
      } catch (final Throwable t) {
        // catch exceptions from possibly incorrectly configured encoder
        // and fallback to default formatted message
        addWarn("Failed to encode logging event", t);
      }
    }
    return loggingEvent.getFormattedMessage();
  }

  private @NotNull List toParams(@Nullable Object[] arguments) {
    if (arguments != null) {
      return Arrays.stream(arguments)
          .filter(Objects::nonNull)
          .map(Object::toString)
          .collect(Collectors.toList());
    } else {
      return Collections.emptyList();
    }
  }

  /**
   * Creates {@link Breadcrumb} from Logback's {@link ILoggingEvent}.
   *
   * @param loggingEvent the logback event
   * @return the sentry breadcrumb
   */
  protected @NotNull Breadcrumb createBreadcrumb(final @NotNull ILoggingEvent loggingEvent) {
    final Breadcrumb breadcrumb = new Breadcrumb();
    breadcrumb.setLevel(formatLevel(loggingEvent.getLevel()));
    breadcrumb.setCategory(loggingEvent.getLoggerName());
    breadcrumb.setMessage(formatted(loggingEvent));
    return breadcrumb;
  }

  /**
   * Transforms a {@link Level} into an {@link SentryLevel}.
   *
   * @param level original level as defined in log4j.
   * @return log level used within sentry.
   */
  private static @NotNull SentryLevel formatLevel(@NotNull Level level) {
    if (level.isGreaterOrEqual(Level.ERROR)) {
      return SentryLevel.ERROR;
    } else if (level.isGreaterOrEqual(Level.WARN)) {
      return SentryLevel.WARNING;
    } else if (level.isGreaterOrEqual(Level.INFO)) {
      return SentryLevel.INFO;
    } else {
      return SentryLevel.DEBUG;
    }
  }

  private @NotNull SdkVersion createSdkVersion(@NotNull SentryOptions sentryOptions) {
    SdkVersion sdkVersion = sentryOptions.getSdkVersion();

    final String name = BuildConfig.SENTRY_LOGBACK_SDK_NAME;
    final String version = BuildConfig.VERSION_NAME;
    sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version);

    return sdkVersion;
  }

  private void addPackageAndIntegrationInfo() {
    SentryIntegrationPackageStorage.getInstance()
        .addPackage("maven:io.sentry:sentry-logback", BuildConfig.VERSION_NAME);
    SentryIntegrationPackageStorage.getInstance().addIntegration("Logback");
  }

  public void setOptions(final @Nullable SentryOptions options) {
    if (options != null) {
      this.options = options;
    }
  }

  public void setMinimumBreadcrumbLevel(final @Nullable Level minimumBreadcrumbLevel) {
    if (minimumBreadcrumbLevel != null) {
      this.minimumBreadcrumbLevel = minimumBreadcrumbLevel;
    }
  }

  public @NotNull Level getMinimumBreadcrumbLevel() {
    return minimumBreadcrumbLevel;
  }

  public void setMinimumEventLevel(final @Nullable Level minimumEventLevel) {
    if (minimumEventLevel != null) {
      this.minimumEventLevel = minimumEventLevel;
    }
  }

  public @NotNull Level getMinimumEventLevel() {
    return minimumEventLevel;
  }

  @ApiStatus.Internal
  void setTransportFactory(final @Nullable ITransportFactory transportFactory) {
    this.transportFactory = transportFactory;
  }

  public void setEncoder(Encoder encoder) {
    this.encoder = encoder;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy