-
-
Couldn't load subscription status.
- Fork 32
Add Log4j2 integration. #537
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import com.novoda.gradle.release.PublishExtension | ||
|
|
||
| plugins { | ||
| `java-library` | ||
| kotlin("jvm") | ||
| jacoco | ||
| id(Config.QualityPlugins.errorProne) | ||
| id(Config.Deploy.novodaBintray) | ||
| id(Config.QualityPlugins.gradleVersions) | ||
| id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion | ||
| } | ||
|
|
||
| configure<JavaPluginConvention> { | ||
| sourceCompatibility = JavaVersion.VERSION_1_8 | ||
| targetCompatibility = JavaVersion.VERSION_1_8 | ||
| } | ||
|
|
||
| tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { | ||
| kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() | ||
| } | ||
|
|
||
| dependencies { | ||
| api(project(":sentry-core")) | ||
| implementation(Config.Libs.log4j2Api) | ||
| implementation(Config.Libs.log4j2Core) | ||
|
|
||
| compileOnly(Config.CompileOnly.nopen) | ||
| errorprone(Config.CompileOnly.nopenChecker) | ||
| errorprone(Config.CompileOnly.errorprone) | ||
| errorproneJavac(Config.CompileOnly.errorProneJavac8) | ||
| compileOnly(Config.CompileOnly.jetbrainsAnnotations) | ||
|
|
||
| // tests | ||
| testImplementation(kotlin(Config.kotlinStdLib)) | ||
| testImplementation(Config.TestLibs.kotlinTestJunit) | ||
| testImplementation(Config.TestLibs.mockitoKotlin) | ||
| testImplementation(Config.TestLibs.awaitility) | ||
| } | ||
|
|
||
| configure<SourceSetContainer> { | ||
| test { | ||
| java.srcDir("src/test/java") | ||
| } | ||
| } | ||
|
|
||
| jacoco { | ||
| toolVersion = Config.QualityPlugins.jacocoVersion | ||
| } | ||
|
|
||
| tasks.jacocoTestReport { | ||
| reports { | ||
| xml.isEnabled = true | ||
| html.isEnabled = false | ||
| } | ||
| } | ||
|
|
||
| tasks { | ||
| jacocoTestCoverageVerification { | ||
| violationRules { | ||
| rule { limit { minimum = BigDecimal.valueOf(0.6) } } | ||
| } | ||
| } | ||
| check { | ||
| dependsOn(jacocoTestCoverageVerification) | ||
| dependsOn(jacocoTestReport) | ||
| } | ||
| } | ||
|
|
||
| buildConfig { | ||
| useJavaOutput() | ||
| packageName("io.sentry.log4j2") | ||
| buildConfigField("String", "SENTRY_LOG4J2_SDK_NAME", "\"${Config.Sentry.SENTRY_LOG4J2_SDK_NAME}\"") | ||
| buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") | ||
| } | ||
|
|
||
| val generateBuildConfig by tasks | ||
| tasks.withType<JavaCompile>().configureEach { | ||
| dependsOn(generateBuildConfig) | ||
| } | ||
|
|
||
| //TODO: move these blocks to parent gradle file, DRY | ||
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| configure<PublishExtension> { | ||
| userOrg = Config.Sentry.userOrg | ||
| groupId = project.group.toString() | ||
| publishVersion = project.version.toString() | ||
| desc = Config.Sentry.description | ||
| website = Config.Sentry.website | ||
| repoName = Config.Sentry.repoName | ||
| setLicences(Config.Sentry.licence) | ||
| setLicenceUrls(Config.Sentry.licenceUrl) | ||
| issueTracker = Config.Sentry.issueTracker | ||
| repository = Config.Sentry.repository | ||
| sign = Config.Deploy.sign | ||
| artifactId = project.name | ||
| uploadName = "${project.group}:${project.name}" | ||
| devId = Config.Sentry.userOrg | ||
| devName = Config.Sentry.devName | ||
| devEmail = Config.Sentry.devEmail | ||
| scmConnection = Config.Sentry.scmConnection | ||
| scmDevConnection = Config.Sentry.scmDevConnection | ||
| scmUrl = Config.Sentry.scmUrl | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| package io.sentry.log4j2; | ||
|
|
||
| import io.sentry.core.Breadcrumb; | ||
| import io.sentry.core.DateUtils; | ||
| import io.sentry.core.HubAdapter; | ||
| import io.sentry.core.IHub; | ||
| import io.sentry.core.Sentry; | ||
| import io.sentry.core.SentryEvent; | ||
| import io.sentry.core.SentryLevel; | ||
| import io.sentry.core.SentryOptions; | ||
| import io.sentry.core.protocol.Message; | ||
| import io.sentry.core.protocol.SdkVersion; | ||
| import io.sentry.core.transport.ITransport; | ||
| import io.sentry.core.util.CollectionUtils; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.Date; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.stream.Collectors; | ||
| import org.apache.logging.log4j.Level; | ||
| import org.apache.logging.log4j.core.Filter; | ||
| import org.apache.logging.log4j.core.LogEvent; | ||
| import org.apache.logging.log4j.core.appender.AbstractAppender; | ||
| import org.apache.logging.log4j.core.config.plugins.Plugin; | ||
| import org.apache.logging.log4j.core.config.plugins.PluginAttribute; | ||
| import org.apache.logging.log4j.core.config.plugins.PluginElement; | ||
| import org.apache.logging.log4j.core.config.plugins.PluginFactory; | ||
| import org.apache.logging.log4j.core.impl.ThrowableProxy; | ||
| import org.jetbrains.annotations.NotNull; | ||
| import org.jetbrains.annotations.Nullable; | ||
|
|
||
| /** Appender for Log4j2 in charge of sending the logged events to a Sentry server. */ | ||
| @Plugin(name = "Sentry", category = "Core", elementType = "appender", printObject = true) | ||
| public final class SentryAppender extends AbstractAppender { | ||
| private final @Nullable String dsn; | ||
| private final @Nullable ITransport transport; | ||
| private @NotNull Level minimumBreadcrumbLevel = Level.INFO; | ||
| private @NotNull Level minimumEventLevel = Level.ERROR; | ||
|
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it make sense to add this either to options or to the Level class as static default fields and we just read the default values from them? cus I see these 2 values repeated in all the integrations, like: or something like that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It depends if we want to pollute There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd be fine with that, to not DRY, of course, it depends on a case by case, but this duplication is already in 4 integrations. It'd be something similar, defining default values to be reused elsewhere. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok understood. This would mean that we need to add conversion from SentryLevel to logging-framework-level - as these levels in appenders are are from log4j2/Logback packages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah the converters for sure, it's just to reuse the default levels, nothing more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO I also rather to keep them out since core has nothing to do with min breadcrumb level of a logger integration DRY doesn't mean avoid any and all "duplication" by breaking cohesion |
||
| private final @NotNull IHub hub; | ||
|
|
||
| public SentryAppender( | ||
maciejwalkowiak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| final @NotNull String name, | ||
| final @Nullable Filter filter, | ||
| final @Nullable String dsn, | ||
| final @Nullable Level minimumBreadcrumbLevel, | ||
| final @Nullable Level minimumEventLevel, | ||
| final @Nullable ITransport transport, | ||
| final @NotNull IHub hub) { | ||
| super(name, filter, null, true, null); | ||
| this.dsn = dsn; | ||
| if (minimumBreadcrumbLevel != null) { | ||
| this.minimumBreadcrumbLevel = minimumBreadcrumbLevel; | ||
| } | ||
| if (minimumEventLevel != null) { | ||
| this.minimumEventLevel = minimumEventLevel; | ||
| } | ||
| this.transport = transport; | ||
| this.hub = hub; | ||
| } | ||
|
|
||
| /** | ||
| * Create a Sentry Appender. | ||
| * | ||
| * @param name The name of the Appender. | ||
| * @param filter The filter, if any, to use. | ||
| * @return The SentryAppender. | ||
| */ | ||
| @PluginFactory | ||
| public static SentryAppender createAppender( | ||
| @PluginAttribute("name") final String name, | ||
| @PluginAttribute("minimumBreadcrumbLevel") final Level minimumBreadcrumbLevel, | ||
| @PluginAttribute("minimumEventLevel") final Level minimumEventLevel, | ||
| @PluginAttribute("dsn") final String dsn, | ||
| @PluginElement("filter") final Filter filter) { | ||
|
|
||
| if (name == null) { | ||
| LOGGER.error("No name provided for SentryAppender"); | ||
| return null; | ||
| } | ||
| return new SentryAppender(name, filter, dsn, minimumBreadcrumbLevel, minimumEventLevel, null, HubAdapter.getInstance()); | ||
| } | ||
|
|
||
| @Override | ||
| public void start() { | ||
| if (dsn != null) { | ||
| Sentry.init( | ||
| options -> { | ||
| options.setDsn(dsn); | ||
| options.setSentryClientName(BuildConfig.SENTRY_LOG4J2_SDK_NAME); | ||
| options.setSdkVersion(createSdkVersion(options)); | ||
| Optional.ofNullable(transport).ifPresent(options::setTransport); | ||
| }); | ||
| } | ||
| super.start(); | ||
| } | ||
|
|
||
| @Override | ||
| public void append(final @NotNull LogEvent eventObject) { | ||
| if (eventObject.getLevel().isMoreSpecificThan(minimumEventLevel)) { | ||
| hub.captureEvent(createEvent(eventObject)); | ||
| } | ||
| if (eventObject.getLevel().isMoreSpecificThan(minimumBreadcrumbLevel)) { | ||
| hub.addBreadcrumb(createBreadcrumb(eventObject)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates {@link SentryEvent} from Log4j2 {@link LogEvent}. | ||
| * | ||
| * @param loggingEvent the log4j2 event | ||
| * @return the sentry event | ||
| */ | ||
| // for the Android compatibility we must use old Java Date class | ||
| @SuppressWarnings("JdkObsolete") | ||
maciejwalkowiak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| final @NotNull SentryEvent createEvent(final @NotNull LogEvent loggingEvent) { | ||
| final SentryEvent event = | ||
| new SentryEvent(DateUtils.getDateTime(new Date(loggingEvent.getTimeMillis()))); | ||
| final Message message = new Message(); | ||
| message.setMessage(loggingEvent.getMessage().getFormat()); | ||
| message.setFormatted(loggingEvent.getMessage().getFormattedMessage()); | ||
| message.setParams(toParams(loggingEvent.getMessage().getParameters())); | ||
| event.setMessage(message); | ||
| event.setLogger(loggingEvent.getLoggerName()); | ||
| event.setLevel(formatLevel(loggingEvent.getLevel())); | ||
|
|
||
| final ThrowableProxy throwableInformation = loggingEvent.getThrownProxy(); | ||
| if (throwableInformation != null) { | ||
| event.setThrowable(throwableInformation.getThrowable()); | ||
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| if (loggingEvent.getThreadName() != null) { | ||
| event.setExtra("thread_name", loggingEvent.getThreadName()); | ||
| } | ||
|
|
||
| final Map<String, String> contextData = | ||
| CollectionUtils.shallowCopy(loggingEvent.getContextData().toMap()); | ||
| if (!contextData.isEmpty()) { | ||
| event.getContexts().put("Context Data", contextData); | ||
| } | ||
|
|
||
| return event; | ||
| } | ||
|
|
||
| private @NotNull List<String> toParams(final @Nullable Object[] arguments) { | ||
| if (arguments != null) { | ||
| return Arrays.stream(arguments) | ||
| .filter(Objects::nonNull) | ||
| .map(Object::toString) | ||
bruno-garcia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .collect(Collectors.toList()); | ||
| } else { | ||
| return Collections.emptyList(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates {@link Breadcrumb} from log4j2 {@link LogEvent}. | ||
| * | ||
| * @param loggingEvent the log4j2 event | ||
| * @return the sentry breadcrumb | ||
| */ | ||
| private @NotNull Breadcrumb createBreadcrumb(final @NotNull LogEvent loggingEvent) { | ||
| final Breadcrumb breadcrumb = new Breadcrumb(); | ||
| breadcrumb.setLevel(formatLevel(loggingEvent.getLevel())); | ||
| breadcrumb.setCategory(loggingEvent.getLoggerName()); | ||
| breadcrumb.setMessage(loggingEvent.getMessage().getFormattedMessage()); | ||
| 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(final @NotNull Level level) { | ||
| if (level.isMoreSpecificThan(Level.FATAL)) { | ||
| return SentryLevel.FATAL; | ||
| } else if (level.isMoreSpecificThan(Level.ERROR)) { | ||
| return SentryLevel.ERROR; | ||
| } else if (level.isMoreSpecificThan(Level.WARN)) { | ||
| return SentryLevel.WARNING; | ||
| } else if (level.isMoreSpecificThan(Level.INFO)) { | ||
| return SentryLevel.INFO; | ||
| } else { | ||
| return SentryLevel.DEBUG; | ||
| } | ||
| } | ||
|
|
||
| private @NotNull SdkVersion createSdkVersion(final @NotNull SentryOptions sentryOptions) { | ||
| SdkVersion sdkVersion = sentryOptions.getSdkVersion(); | ||
|
|
||
| if (sdkVersion == null) { | ||
| sdkVersion = new SdkVersion(); | ||
| } | ||
|
|
||
| sdkVersion.setName(BuildConfig.SENTRY_LOG4J2_SDK_NAME); | ||
| final String version = BuildConfig.VERSION_NAME; | ||
| sdkVersion.setVersion(version); | ||
| sdkVersion.addPackage("maven:sentry-log4j2", version); | ||
|
|
||
| return sdkVersion; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.