Skip to content

Commit

Permalink
Add OpenFeign support. (#1632)
Browse files Browse the repository at this point in the history
  • Loading branch information
maciejwalkowiak authored Aug 3, 2021
1 parent bb9f248 commit 1afa86e
Show file tree
Hide file tree
Showing 9 changed files with 453 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

* Feat: Spring WebClient integration (#1621)
* Feat: OpenFeign integration (#1632)

## 5.1.0-beta.9

Expand Down
2 changes: 2 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ object Config {
val fragment = "androidx.fragment:fragment-ktx:1.3.5"

val reactorCore = "io.projectreactor:reactor-core:3.4.6"

val feignCore = "io.github.openfeign:feign-core:11.6"
}

object AnnotationProcessors {
Expand Down
15 changes: 15 additions & 0 deletions sentry-openfeign/api/sentry-openfeign.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
public final class io/sentry/openfeign/SentryCapability : feign/Capability {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;Lio/sentry/openfeign/SentryFeignClient$BeforeSpanCallback;)V
public fun enrich (Lfeign/Client;)Lfeign/Client;
}

public final class io/sentry/openfeign/SentryFeignClient : feign/Client {
public fun <init> (Lfeign/Client;Lio/sentry/IHub;Lio/sentry/openfeign/SentryFeignClient$BeforeSpanCallback;)V
public fun execute (Lfeign/Request;Lfeign/Request$Options;)Lfeign/Response;
}

public abstract interface class io/sentry/openfeign/SentryFeignClient$BeforeSpanCallback {
public abstract fun execute (Lio/sentry/ISpan;Lfeign/Request;Lfeign/Response;)Lio/sentry/ISpan;
}

76 changes: 76 additions & 0 deletions sentry-openfeign/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`java-library`
kotlin("jvm")
jacoco
id(Config.QualityPlugins.errorProne)
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<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}

dependencies {
api(project(":sentry"))
implementation(Config.Libs.feignCore)

compileOnly(Config.CompileOnly.nopen)
errorprone(Config.CompileOnly.nopenChecker)
errorprone(Config.CompileOnly.errorprone)
errorprone(Config.CompileOnly.errorProneNullAway)
errorproneJavac(Config.CompileOnly.errorProneJavac8)
compileOnly(Config.CompileOnly.jetbrainsAnnotations)

// tests
testImplementation(project(":sentry-test-support"))
testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.mockitoKotlin)
testImplementation(Config.TestLibs.awaitility)
testImplementation(Config.TestLibs.mockWebserver)
}

configure<SourceSetContainer> {
test {
java.srcDir("src/test/java")
}
}

jacoco {
toolVersion = Config.QualityPlugins.Jacoco.version
}

tasks.jacocoTestReport {
reports {
xml.isEnabled = true
html.isEnabled = false
}
}

tasks {
jacocoTestCoverageVerification {
violationRules {
rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } }
}
}
check {
dependsOn(jacocoTestCoverageVerification)
dependsOn(jacocoTestReport)
}
}

tasks.withType<JavaCompile>().configureEach {
options.errorprone {
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "io.sentry")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.sentry.openfeign;

import feign.Capability;
import feign.Client;
import io.sentry.HubAdapter;
import io.sentry.IHub;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** Adds Sentry tracing capability to Feign clients. */
public final class SentryCapability implements Capability {

private final @NotNull IHub hub;
private final @Nullable SentryFeignClient.BeforeSpanCallback beforeSpan;

public SentryCapability(
final @NotNull IHub hub, final @Nullable SentryFeignClient.BeforeSpanCallback beforeSpan) {
this.hub = hub;
this.beforeSpan = beforeSpan;
}

public SentryCapability() {
this(HubAdapter.getInstance(), null);
}

@Override
public @NotNull Client enrich(final @NotNull Client client) {
return new SentryFeignClient(client, hub, beforeSpan);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package io.sentry.openfeign;

import feign.Client;
import feign.Request;
import feign.Response;
import io.sentry.Breadcrumb;
import io.sentry.IHub;
import io.sentry.ISpan;
import io.sentry.SentryTraceHeader;
import io.sentry.SpanStatus;
import io.sentry.util.Objects;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** A Feign client that creates a span around each executed HTTP call. */
public final class SentryFeignClient implements Client {
private final @NotNull Client delegate;
private final @NotNull IHub hub;
private final @Nullable BeforeSpanCallback beforeSpan;

public SentryFeignClient(
final @NotNull Client delegate,
final @NotNull IHub hub,
final @Nullable BeforeSpanCallback beforeSpan) {
this.delegate = Objects.requireNonNull(delegate, "delegate is required");
this.hub = Objects.requireNonNull(hub, "hub is required");
this.beforeSpan = beforeSpan;
}

@Override
public Response execute(final @NotNull Request request, final @NotNull Request.Options options)
throws IOException {
Response response = null;
try {
final ISpan activeSpan = hub.getSpan();
if (activeSpan == null) {
return delegate.execute(request, options);
}

ISpan span = activeSpan.startChild("http.client");
span.setDescription(request.httpMethod().name() + " " + request.url());

final SentryTraceHeader sentryTraceHeader = span.toSentryTrace();
final RequestWrapper requestWrapper = new RequestWrapper(request);
requestWrapper.header(sentryTraceHeader.getName(), sentryTraceHeader.getValue());
try {
response = delegate.execute(requestWrapper.build(), options);
// handles both success and error responses
span.setStatus(SpanStatus.fromHttpStatusCode(response.status()));
return response;
} catch (Exception e) {
// handles cases like connection errors
span.setThrowable(e);
span.setStatus(SpanStatus.INTERNAL_ERROR);
throw e;
} finally {
if (beforeSpan != null) {
span = beforeSpan.execute(span, request, response);
}
if (span != null) {
span.finish();
}
}
} finally {
addBreadcrumb(request, response);
}
}

private void addBreadcrumb(final @NotNull Request request, final @Nullable Response response) {
final Breadcrumb breadcrumb =
Breadcrumb.http(
request.url(),
request.httpMethod().name(),
response != null ? response.status() : null);
breadcrumb.setData("request_body_size", request.body() != null ? request.body().length : 0);
if (response != null && response.body().length() != null) {
breadcrumb.setData("response_body_size", response.body().length());
}
hub.addBreadcrumb(breadcrumb);
}

static final class RequestWrapper {
private final @NotNull Request delegate;

private final @NotNull Map<String, Collection<String>> headers;

RequestWrapper(final @NotNull Request delegate) {
this.delegate = delegate;
this.headers = new LinkedHashMap<>(delegate.headers());
}

public void header(final @NotNull String name, final @NotNull String value) {
if (!headers.containsKey(name)) {
headers.put(name, Collections.singletonList(value));
}
}

@NotNull
Request build() {
return Request.create(
delegate.httpMethod(),
delegate.url(),
headers,
delegate.body(),
delegate.charset(),
delegate.requestTemplate());
}
}

public interface BeforeSpanCallback {
@Nullable
ISpan execute(@NotNull ISpan span, @NotNull Request request, @Nullable Response response);
}
}
Loading

0 comments on commit 1afa86e

Please sign in to comment.