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
4 changes: 4 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ object Config {
val lifecycleVersion = "2.2.0"
val lifecycleProcess = "androidx.lifecycle:lifecycle-process:$lifecycleVersion"
val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"

val logbackVersion = "1.2.3"
val logbackClassic = "ch.qos.logback:logback-classic:$logbackVersion"
}

object TestLibs {
Expand Down Expand Up @@ -62,6 +65,7 @@ object Config {
object Sentry {
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 group = "io.sentry"
val description = "SDK for sentry.io"
val website = "https://sentry.io"
Expand Down
3 changes: 2 additions & 1 deletion sentry-core/src/main/java/io/sentry/core/SentryEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ public Date getTimestamp() {
}

@Nullable
Throwable getThrowable() {
@ApiStatus.Internal
public Throwable getThrowable() {
return throwable;
}

Expand Down
102 changes: 102 additions & 0 deletions sentry-logback/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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.logbackClassic)

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.logback")
buildConfigField("String", "SENTRY_LOGBACK_SDK_NAME", "\"${Config.Sentry.SENTRY_LOGBACK_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
}

200 changes: 200 additions & 0 deletions sentry-logback/src/main/java/io/sentry/logback/SentryAppender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package io.sentry.logback;

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 io.sentry.core.DateUtils;
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 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 io.sentry.core.transport.ITransport;
import io.sentry.core.util.CollectionUtils;
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. */
public final class SentryAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private @Nullable String dsn;
private @Nullable String environment;
private @Nullable Integer maxBreadcrumbs;
private @Nullable Integer shutdownTimeoutMillis;
private @Nullable Integer flushTimeoutMillis;
private @Nullable Integer readTimeoutMillis;
private @Nullable Double sampleRate;
private @Nullable Boolean bypassSecurity;
private @Nullable Boolean debug;
private @Nullable Boolean attachThreads;
private @Nullable Boolean attachStacktrace;
private @Nullable ITransport transport;

@Override
public void start() {
if (dsn != null) {
Copy link
Member

Choose a reason for hiding this comment

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

We need some config here to allow a user to opt out for initializing thought the sdk.

It's confusing when the 3 integrations are on and folks don't know what is initing it.

If we can also check if the sdk is already initializing and then warn about it. (Like Sentry.isEnabled) would be best. Last init wins is OK so dispose previous bound hub l/Clientand get it init again ur it's often a mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Passing config here is optional. If user has at the same time Spring Boot starter.and configures Sentry through it he can omit completely passing options through Logback

Copy link
Member

Choose a reason for hiding this comment

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

I see. Though there's no way to avoid initializing the SDK via this integration, right?
So if a user adds the dsn to the appender configuration file, it will init the SDK even if it was already initialized.

That's why the suggestion:

If we can also check if the sdk is already initializing and then warn about it. (Like Sentry.isEnabled) would be best. Last init wins is OK so dispose previous bound hub l/Clientand get it init again ur it's often a mistake.

Or at least log a warning with the diagnostic logger

Sentry.init(
options -> {
options.setDsn(dsn);
Optional.ofNullable(maxBreadcrumbs).ifPresent(options::setMaxBreadcrumbs);
Optional.ofNullable(environment).ifPresent(options::setEnvironment);
Optional.ofNullable(shutdownTimeoutMillis).ifPresent(options::setShutdownTimeout);
Optional.ofNullable(flushTimeoutMillis).ifPresent(options::setFlushTimeoutMillis);
Optional.ofNullable(readTimeoutMillis).ifPresent(options::setReadTimeoutMillis);
Optional.ofNullable(sampleRate).ifPresent(options::setSampleRate);
Optional.ofNullable(bypassSecurity).ifPresent(options::setBypassSecurity);
Optional.ofNullable(debug).ifPresent(options::setDebug);
Optional.ofNullable(attachThreads).ifPresent(options::setAttachThreads);
Optional.ofNullable(attachStacktrace).ifPresent(options::setAttachStacktrace);
options.setSentryClientName(BuildConfig.SENTRY_LOGBACK_SDK_NAME);
options.setSdkVersion(createSdkVersion(options));
Optional.ofNullable(transport).ifPresent(options::setTransport);
});
}
super.start();
}

@Override
protected void append(@NotNull ILoggingEvent eventObject) {
Sentry.captureEvent(createEvent(eventObject));
}

/**
* Creates {@link SentryEvent} from Logback's {@link ILoggingEvent}.
*
* @param loggingEvent the logback event
* @return the sentry event
*/
@SuppressWarnings("JdkObsolete")
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this suppress? Maybe a note will help folks reading the code

Suggested change
@SuppressWarnings("JdkObsolete")
// Ignoring the obsolete because .... ?
@SuppressWarnings("JdkObsolete")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we're using java.util.Date and error prone forbids using this class. Ideally we should avoid using this class but I believe we do use it because newer Date classes are not supported on Android.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or at least log a warning with the diagnostic logger

We cannot access diagnostic logger from this level as it's a part of SentryOptions. Perhaps we should add this check isSentryEnabled and log warning inside Sentry.init() itself?

Copy link
Member

Choose a reason for hiding this comment

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

re: Obsolete. That description you gave here would fit well as a comment. So it's clear why we ignored it.

re: logger. Sounds good to at least log on sentry.init!

final @NotNull SentryEvent createEvent(@NotNull ILoggingEvent loggingEvent) {
final SentryEvent event = new SentryEvent(DateUtils.getDateTime(new Date(loggingEvent.getTimeStamp())));
final Message message = new Message();
message.setMessage(loggingEvent.getMessage());
message.setFormatted(loggingEvent.getFormattedMessage());
message.setParams(toParams(loggingEvent.getArgumentArray()));
event.setMessage(message);
event.setLogger(loggingEvent.getLoggerName());
event.setLevel(formatLevel(loggingEvent.getLevel()));

final ThrowableProxy throwableInformation = (ThrowableProxy) loggingEvent.getThrowableProxy();
if (throwableInformation != null) {
event.setThrowable(throwableInformation.getThrowable());
}

if (loggingEvent.getThreadName() != null) {
Copy link
Member

Choose a reason for hiding this comment

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

We actually have a threads model which takes a name. Maybe we can add it here though without stacktrace isn't that useful.

We tend to avoid using extra. The reason is that it has no "meaning". We'd prefer using contexts and setting the key to something more meaningful.
If not using threads model and thread-name, this being a one-off value, it's OK to leave it on Extra though

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, got the point. The question is - do we drop it and rely on attachThreads or add Thread section to contexts. To be honest I do see value in having a thread name, but I don't see value for typical backend application to attach list of all threads. Especially when there can be 100 http threads running at once.

My preference would be to add Thread to contexts and attach there fields: id, name.

Copy link
Member

Choose a reason for hiding this comment

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

what about we add a single thread to the list then? with id, name? When the attachThreads is false that is

event.setExtra("thread_name", loggingEvent.getThreadName());
}

final Map<String, String> mdcProperties = CollectionUtils.shallowCopy(loggingEvent.getMDCPropertyMap());
if (!mdcProperties.isEmpty()) {
event.getContexts().put("MDC", mdcProperties);
}

return event;
}

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

/**
* 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();

if (sdkVersion == null) {
sdkVersion = new SdkVersion();
}

sdkVersion.setName(BuildConfig.SENTRY_LOGBACK_SDK_NAME);
final String version = BuildConfig.VERSION_NAME;
sdkVersion.setVersion(version);
sdkVersion.addPackage("maven:sentry-logback", version);

return sdkVersion;
}

public void setDsn(@Nullable String dsn) {
this.dsn = dsn;
}

public void setEnvironment(@Nullable String environment) {
this.environment = environment;
}

public void setMaxBreadcrumbs(@Nullable Integer maxBreadcrumbs) {
this.maxBreadcrumbs = maxBreadcrumbs;
}

public void setShutdownTimeoutMillis(@Nullable Integer shutdownTimeoutMillis) {
this.shutdownTimeoutMillis = shutdownTimeoutMillis;
}

public void setFlushTimeoutMillis(@Nullable Integer flushTimeoutMillis) {
this.flushTimeoutMillis = flushTimeoutMillis;
}

public void setReadTimeoutMillis(@Nullable Integer readTimeoutMillis) {
this.readTimeoutMillis = readTimeoutMillis;
}

public void setSampleRate(@Nullable Double sampleRate) {
this.sampleRate = sampleRate;
}

public void setBypassSecurity(@Nullable Boolean bypassSecurity) {
this.bypassSecurity = bypassSecurity;
}

public void setDebug(@Nullable Boolean debug) {
this.debug = debug;
}

public void setAttachThreads(@Nullable Boolean attachThreads) {
this.attachThreads = attachThreads;
}

public void setAttachStacktrace(@Nullable Boolean attachStacktrace) {
this.attachStacktrace = attachStacktrace;
}

@ApiStatus.Internal
void setTransport(@Nullable ITransport transport) {
this.transport = transport;
}
}
Loading