Skip to content
This repository was archived by the owner on Aug 30, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ object Config {
val logbackVersion = "1.2.3"
val logbackClassic = "ch.qos.logback:logback-classic:$logbackVersion"

val log4j2Version = "2.13.3"
val log4j2Api = "org.apache.logging.log4j:log4j-api:$log4j2Version"
val log4j2Core = "org.apache.logging.log4j:log4j-core:$log4j2Version"

val springBootStarter = "org.springframework.boot:spring-boot-starter:$springBootVersion"

val springWeb = "org.springframework:spring-webmvc"
Expand Down Expand Up @@ -84,6 +88,7 @@ object Config {
val SENTRY_JAVA_SDK_NAME = "sentry.java"
val SENTRY_ANDROID_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.android"
val SENTRY_LOGBACK_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.logback"
val SENTRY_LOG4J2_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.log4j2"
val SENTRY_SPRING_BOOT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot"
val group = "io.sentry"
val description = "SDK for sentry.io"
Expand Down
103 changes: 103 additions & 0 deletions sentry-log4j2/build.gradle.kts
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
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
}

206 changes: 206 additions & 0 deletions sentry-log4j2/src/main/java/io/sentry/log4j2/SentryAppender.java
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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:

SentryOptions.DEFAULT_BREADCRUMB_LEVEL
SentryOptions.DEFAULT_EVENT_LEVEL

or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends if we want to pollute sentry-core with integration specific code. At this stage core is not aware of the whole mechanism of attaching breadcrumbs to events in logging integrations.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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(
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")
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());
}

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)
.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;
}
}
Loading