diff --git a/docs/opentracing-logo.png b/docs/opentracing-logo.png new file mode 100644 index 000000000..768f8f066 Binary files /dev/null and b/docs/opentracing-logo.png differ diff --git a/docs/spider-web.jpg b/docs/spider-web.jpg new file mode 100644 index 000000000..3d9e5fe3d Binary files /dev/null and b/docs/spider-web.jpg differ diff --git a/pom.xml b/pom.xml index 38b2cdd0d..5630cc8d0 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,7 @@ riptide-httpclient riptide-idempotency riptide-metrics + riptide-opentracing riptide-problem riptide-soap riptide-spring-boot-autoconfigure @@ -127,6 +128,11 @@ riptide-metrics ${project.version} + + org.zalando + riptide-opentracing + ${project.version} + org.zalando riptide-problem diff --git a/riptide-bom/pom.xml b/riptide-bom/pom.xml index e7a203a22..41527896c 100644 --- a/riptide-bom/pom.xml +++ b/riptide-bom/pom.xml @@ -62,6 +62,10 @@ riptide-metrics 3.0.0-SNAPSHOT + + org.zalando + riptide-opentracing + org.zalando riptide-problem diff --git a/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/CompositeRetryListener.java b/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/CompositeRetryListener.java index f7e24d250..39b25ad51 100644 --- a/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/CompositeRetryListener.java +++ b/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/CompositeRetryListener.java @@ -10,6 +10,7 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; +// TODO package private? @API(status = EXPERIMENTAL) public final class CompositeRetryListener implements RetryListener { diff --git a/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/FailsafePlugin.java b/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/FailsafePlugin.java index 28ed4266a..a2c9a3c58 100644 --- a/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/FailsafePlugin.java +++ b/riptide-failsafe/src/main/java/org/zalando/riptide/failsafe/FailsafePlugin.java @@ -10,12 +10,12 @@ import net.jodah.failsafe.function.CheckedConsumer; import org.apiguardian.api.API; import org.springframework.http.client.ClientHttpResponse; +import org.zalando.riptide.Attribute; import org.zalando.riptide.Plugin; import org.zalando.riptide.RequestArguments; import org.zalando.riptide.RequestExecution; import org.zalando.riptide.idempotency.IdempotencyPredicate; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Predicate; import java.util.function.UnaryOperator; @@ -23,13 +23,13 @@ import static lombok.AccessLevel.PRIVATE; import static org.apiguardian.api.API.Status.MAINTAINED; -import static org.zalando.riptide.CancelableCompletableFuture.forwardTo; -import static org.zalando.riptide.CancelableCompletableFuture.preserveCancelability; @API(status = MAINTAINED) @AllArgsConstructor(access = PRIVATE) public final class FailsafePlugin implements Plugin { + public static final Attribute ATTEMPTS = Attribute.generate(); + private final ImmutableList> policies; private final ScheduledExecutorService scheduler; private final Predicate predicate; @@ -57,24 +57,19 @@ public RequestExecution aroundDispatch(final RequestExecution execution) { return execution.execute(arguments); } - final CompletableFuture original = Failsafe.with(select(arguments)) + return Failsafe.with(policies) .with(scheduler) - .getStageAsync(() -> execution.execute(arguments)); - - final CompletableFuture cancelable = preserveCancelability(original); - original.whenComplete(forwardTo(cancelable)); - return cancelable; + .getStageAsync(context -> execution + .execute(withAttempts(arguments, context.getAttemptCount()))); }; } - private Policy[] select(final RequestArguments arguments) { final Stream> stream = policies.stream() .filter(skipRetriesIfNeeded(arguments)) .map(withRetryListener(arguments)); - @SuppressWarnings("unchecked") - final Policy[] policies = stream.toArray(Policy[]::new); + @SuppressWarnings("unchecked") final Policy[] policies = stream.toArray(Policy[]::new); return policies; } @@ -91,13 +86,21 @@ private UnaryOperator> withRetryListener(final Reques if (policy instanceof RetryPolicy) { final RetryPolicy retryPolicy = (RetryPolicy) policy; return retryPolicy.copy() - .onRetry(new RetryListenerAdapter(listener, arguments)); + .onFailedAttempt(new RetryListenerAdapter(listener, arguments)); } else { return policy; } }; } + private RequestArguments withAttempts(final RequestArguments arguments, final int attempts) { + if (attempts == 0) { + return arguments; + } + + return arguments.withAttribute(ATTEMPTS, attempts); + } + @VisibleForTesting @AllArgsConstructor static final class RetryListenerAdapter implements CheckedConsumer> { diff --git a/riptide-faults/src/test/java/org/zalando/riptide/faults/TransientFaultExceptionTest.java b/riptide-faults/src/test/java/org/zalando/riptide/faults/TransientFaultExceptionTest.java index 27a3006a0..2f8335d45 100644 --- a/riptide-faults/src/test/java/org/zalando/riptide/faults/TransientFaultExceptionTest.java +++ b/riptide-faults/src/test/java/org/zalando/riptide/faults/TransientFaultExceptionTest.java @@ -5,10 +5,10 @@ import java.io.IOException; import java.util.concurrent.TimeoutException; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItemInArray; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; -import static org.hamcrest.MatcherAssert.assertThat; final class TransientFaultExceptionTest { diff --git a/riptide-metrics/pom.xml b/riptide-metrics/pom.xml index 8174e2f22..1a4a0090b 100644 --- a/riptide-metrics/pom.xml +++ b/riptide-metrics/pom.xml @@ -37,18 +37,6 @@ com.github.rest-driver rest-client-driver - - org.apache.httpcomponents - httpclient - 4.5.7 - test - - - commons-logging - commons-logging - - - diff --git a/riptide-metrics/src/test/java/org/zalando/riptide/metrics/MetricsPluginTest.java b/riptide-metrics/src/test/java/org/zalando/riptide/metrics/MetricsPluginTest.java index efdf64282..088a5decd 100644 --- a/riptide-metrics/src/test/java/org/zalando/riptide/metrics/MetricsPluginTest.java +++ b/riptide-metrics/src/test/java/org/zalando/riptide/metrics/MetricsPluginTest.java @@ -11,7 +11,6 @@ import org.junit.jupiter.api.Test; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import org.zalando.riptide.Http; import javax.annotation.Nullable; @@ -65,7 +64,6 @@ private static ObjectMapper createObjectMapper() { MetricsPluginTest() { this.factory.setReadTimeout(500); - this.factory.setTaskExecutor(new ConcurrentTaskExecutor()); } @Test diff --git a/riptide-opentracing/README.md b/riptide-opentracing/README.md new file mode 100644 index 000000000..50dfe8134 --- /dev/null +++ b/riptide-opentracing/README.md @@ -0,0 +1,185 @@ +# Riptide: OpenTracing + +[![Spider web](../docs/spider-web.jpg)](https://pixabay.com/photos/cobweb-drip-water-mirroring-blue-3725540/) + +[![Build Status](https://img.shields.io/travis/zalando/riptide/master.svg)](https://travis-ci.org/zalando/riptide) +[![Coverage Status](https://img.shields.io/coveralls/zalando/riptide/master.svg)](https://coveralls.io/r/zalando/riptide) +[![Code Quality](https://img.shields.io/codacy/grade/1fbe3d16ca544c0c8589692632d114de/master.svg)](https://www.codacy.com/app/whiskeysierra/riptide) +[![Javadoc](https://www.javadoc.io/badge/org.zalando/riptide-metrics.svg)](http://www.javadoc.io/doc/org.zalando/riptide-metrics) +[![Release](https://img.shields.io/github/release/zalando/riptide.svg)](https://github.com/zalando/riptide/releases) +[![Maven Central](https://img.shields.io/maven-central/v/org.zalando/riptide-metrics.svg)](https://maven-badges.herokuapp.com/maven-central/org.zalando/riptide-metrics) +[![OpenTracing](https://img.shields.io/badge/OpenTracing-enabled-blue.svg)](http://opentracing.io) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/zalando/riptide/master/LICENSE) + +*Riptide: OpenTracing* adds sophisticated [OpenTracing](https://opentracing.io/) support to *Riptide*. + +## Example + +```java +Http.builder() + .plugin(new OpenTracingPlugin(tracer)) + .build(); +``` + +## Features + +- Client span lifecycle management +- Span context injection into HTTP headers of requests +- Extensible span decorators for tags and logs +- Seamless integration with [Riptide: Failsafe](../riptide-failsafe) + +## Dependencies + +- Java 8 +- Riptide Core +- [OpenTracing Java API](https://opentracing.io/guides/java/) +- [Riptide: Failsafe](../riptide-failsafe) (optional) + +## Installation + +Add the following dependency to your project: + +```xml + + org.zalando + riptide-opentracing + ${riptide.version} + +``` + +## Configuration + +```java +Http.builder() + .baseUrl("https://www.example.com") + .plugin(new OpenTracingPlugin(tracer)) + .build(); +``` + +The following tags/logs are supported out of the box: + +| Tag/Log Field | Decorator | Example | +|----------------------|--------------------------------|-----------------------------------| +| `component` | `ComponentSpanDecorator` | `Riptide` | +| `span.kind` | `SpanKindSpanDecorator` | `client` | +| `peer.hostname` | `PeerSpanDecorator` | `www.github.com` | +| `peer.port` | `PeerSpanDecorator` | `80` | +| `http.method` | `HttpMethodSpanDecorator` | `GET` | +| `http.url` | `HttpUrlSpanDecorator` | `https://www.github.com/users/me` | +| `http.path` | `HttpPathSpanDecorator` | `/users/{user_id}` | +| `http.status_code` | `HttpStatusCodeSpanDecorator` | `200` | +| `error` | `ErrorSpanDecorator` | `false` | +| `error.kind` (log) | `ErrorSpanDecorator` | `SocketTimeoutException` | +| `error.object` (log) | `ErrorSpanDecorator` | (exception instance) | +| `retry` | `RetrySpanDecorator` | `true` | +| `retry_number` (log) | `RetrySpanDecorator` | `3` | +| `*` | `CallSiteSpanDecorator` | `admin=true` | +| `*` | `StaticTagSpanDecorator` | `aws.region=eu-central-1` | +| `*` | `UriVariablesTagSpanDecorator` | user_id=me | + +### Notice + +**Be aware**: The `http.url` tag is disabled by default because the full request URI may contain +sensitive, [*personal data*](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation). +As an alternative we introduced the `http.path` tag which favors the URI template over the +already expanded version. That has the additional benefit of a significant lower cardinality +compared to what `http.url` would provide. + +If you still want to enable it, you can do so by just registering the missing span decorator: + +```java +new OpenTracingPlugin(tracer) + .withAdditionalSpanDecorators(new HttpUrlSpanDecorator()) +``` + +### Span Decorators + +Span decorators are a simple, yet powerful tool to manipulate the span, i.e. they allow you to +add tags, logs and baggage to spans. The default set of decorators can be extended by using +`OpenTracingPlugin#withAdditionalSpanDecorators(..)`: + +```java +new OpenTracingPlugin(tracer) + .withAdditionalSpanDecorators(new StaticSpanDecorator(singletonMap( + "environment", "local" + ))) +``` + +If the default span decorators are not desired you can replace them completely using +`OpenTracingPlugin#withSpanDecorators(..)`: + +```java +new OpenTracingPlugin(tracer) + .withSpanDecorators( + new ComponentSpanDecorator("MSIE"), + new SpanKindSpanDecorator(Tags.SPAN_KIND_CONSUMER), + new PeerSpanDecorator(), + new HttpMethodSpanDecorator(), + new HttpPathSpanDecorator(), + new HttpUrlSpanDecorator(), + new HttpStatusCodeSpanDecorator(), + new ErrorSpanDecorator(), + new CallSiteSpanDecorator()) +``` + +## Usage + +Typically you won't need to do anything at the call-site regarding OpenTracing, i.e. +your usages of Riptide should work exactly as before: + +```java +http.get("/users/{id}", userId) + .dispatch(series(), + on(SUCCESSFUL).call(User.class, this::greet), + anySeries().call(problemHandling())) +``` + +### Operation Name + +By default the HTTP method will be used as the operation name, which might not fit your needs. +Since deriving a meaningful operation name from request arguments alone is unreliable, you can +specify the `OpenTracingPlugin.OPERATION_NAME` request attribute to override the default: + +```java +http.get("/users/{id}", userId) + .attribute(OpenTracingPlugin.OPERATION_NAME, "get_user") + .dispatch(series(), + on(SUCCESSFUL).call(User.class, this::greet), + anySeries().call(problemHandling())) +``` + +### Call-Site Tags + +Assuming you have the [`CallSiteSpanDecorator`](#span-decorators) registered (it is by default), you can also +specify custom tags based on context information which wouldn't be available within the plugin +anymore: + +```java +http.get("/users/{id}", userId) + .attribute(OpenTracingPlugin.TAGS, singletonMap("retry", "true")) + .dispatch(series(), + on(SUCCESSFUL).call(User.class, this::greet), + anySeries().call(problemHandling())) +``` + +### URI Variables as Tags + +URI templates are not just safer to use (see [Configuration](#notice)), they can also be used to +generate tags from URI variables. Given you have the `UriVariablesTagSpanDecorator` registered +then the following will produce a `user_id=123` tag: + +```java +http.get("/users/{user_id}", 123) +``` + +The same warning applies as mentioned before regarding [`http.url`](#notice). This feature may +expose *personal data* and should be used with care. + +## Getting Help + +If you have questions, concerns, bug reports, etc., please file an issue in this repository's [Issue Tracker](../../../../issues). + +## Getting Involved/Contributing + +To contribute, simply open a pull request and add a brief description (1-2 sentences) of your addition or change. For +more details, check the [contribution guidelines](../.github/CONTRIBUTING.md). diff --git a/riptide-opentracing/pom.xml b/riptide-opentracing/pom.xml new file mode 100644 index 000000000..da182e94c --- /dev/null +++ b/riptide-opentracing/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + org.zalando + riptide-parent + 3.0.0-SNAPSHOT + + + riptide-opentracing + + Riptide: OpenTracing + Client side response routing + + + 0.32.0-RC2 + + + + + org.zalando + riptide-core + + + io.opentracing + opentracing-api + ${opentracing.version} + + + org.zalando + riptide-failsafe + + true + + + io.opentracing + opentracing-mock + ${opentracing.version} + test + + + com.github.rest-driver + rest-client-driver + + + io.opentracing.contrib + opentracing-concurrent + 0.2.0 + test + + + org.apache.httpcomponents + httpclient + 4.5.7 + test + + + commons-logging + commons-logging + + + + + + diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionFields.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionFields.java new file mode 100644 index 000000000..6249fc534 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionFields.java @@ -0,0 +1,14 @@ +package org.zalando.riptide.opentracing; + +public final class ExtensionFields { + + /** + * In combination with {@link ExtensionTags#RETRY retry tag}, this field holds the number of the retry attempt. + */ + public static final String RETRY_NUMBER = "retry_number"; + + private ExtensionFields() { + + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionTags.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionTags.java new file mode 100644 index 000000000..5bbeeaa9d --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionTags.java @@ -0,0 +1,61 @@ +package org.zalando.riptide.opentracing; + +import io.opentracing.tag.BooleanTag; +import io.opentracing.tag.StringTag; +import io.opentracing.tag.Tag; + +public final class ExtensionTags { + + public static final Tag HTTP_PATH = new StringTag("http.path"); + + /** + * When present on a client span, they represent a span that wraps a retried RPC. If missing no interpretation can + * be made. An explicit value of false would explicitly mean it is a first RPC attempt. + */ + public static final Tag RETRY = new BooleanTag("retry"); + + /** + * The tag should contain an alias or name that allows users to identify the logical location (infrastructure account) + * where the operation took place. This can be the AWS account, or any other cloud provider account. + * E.g., {@code account=aws:zalando-zmon}, {@code account=gcp:zalando-foobar} + */ + public static final Tag ACCOUNT = new StringTag("account"); + + /** + * The tag should contain some information that allows users to associate the physical location of the system where + * the operation took place (i.e. the datacenter). + * E.g., {@code zone=aws:eu-central-1a}, {@code zone=gcp:europe-west3-b}, {@code zone=dc:gth}. + */ + public static final Tag ZONE = new StringTag("zone"); + + /** + * Oauth2 client ids have a certain cardinality but are well known or possible to get using different means. It + * could be helpful for server spans to identify the client making the call. E.g., {@code client_id=cognac} + */ + public static final Tag CLIENT_ID = new StringTag("client_id"); + + /** + * The flow_id tag should contain the request flow ID, typically found in the ingress requests HTTP header X-Flow-ID. + * + * X-Flow-ID Guidelines + */ + public static final Tag FLOW_ID = new StringTag("flow_id"); + + /** + * The tag should contain the artifact version of the running application generating the spans. + * This is, usually, the docker image tag. + */ + public static final Tag ARTIFACT_VERSION = new StringTag("artifact_version"); + + /** + * The tag should contain the unique identifier of the deployment that resulted in the operation of the running + * application generating the spans. This is, usually, the STUPS stack version or the Kubernetes deployment id. + * A deployment is the combination of a given artifact_version and the environment, usually its configuration. + */ + public static final Tag DEPLOYMENT_ID = new StringTag("deployment_id"); + + private ExtensionTags() { + + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/OpenTracingPlugin.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/OpenTracingPlugin.java new file mode 100644 index 000000000..ed6beaf05 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/OpenTracingPlugin.java @@ -0,0 +1,175 @@ +package org.zalando.riptide.opentracing; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Multimaps; +import io.opentracing.Scope; +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.propagation.TextMapAdapter; +import lombok.AllArgsConstructor; +import org.springframework.http.client.ClientHttpResponse; +import org.zalando.fauxpas.ThrowingBiConsumer; +import org.zalando.riptide.Attribute; +import org.zalando.riptide.AttributeStage; +import org.zalando.riptide.Plugin; +import org.zalando.riptide.RequestArguments; +import org.zalando.riptide.RequestExecution; +import org.zalando.riptide.opentracing.span.CallSiteSpanDecorator; +import org.zalando.riptide.opentracing.span.ComponentSpanDecorator; +import org.zalando.riptide.opentracing.span.ErrorSpanDecorator; +import org.zalando.riptide.opentracing.span.HttpMethodSpanDecorator; +import org.zalando.riptide.opentracing.span.HttpPathSpanDecorator; +import org.zalando.riptide.opentracing.span.HttpStatusCodeSpanDecorator; +import org.zalando.riptide.opentracing.span.PeerSpanDecorator; +import org.zalando.riptide.opentracing.span.SpanDecorator; +import org.zalando.riptide.opentracing.span.SpanKindSpanDecorator; + +import javax.annotation.CheckReturnValue; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletionException; +import java.util.function.BiConsumer; + +import static io.opentracing.propagation.Format.Builtin.HTTP_HEADERS; +import static java.util.Objects.nonNull; +import static lombok.AccessLevel.PRIVATE; + +@AllArgsConstructor(access = PRIVATE) +public final class OpenTracingPlugin implements Plugin { + + /** + * Allows to pass a customized {@link Tracer#buildSpan(String) operation name} directly from + * a call site. Defaults to the {@link RequestArguments#getMethod() HTTP method}. + * + * @see AttributeStage#attribute(Attribute, Object) + */ + public static final Attribute OPERATION_NAME = Attribute.generate(); + + /** + * Allows to pass arbitrary span tags directly from a call site. + * + * @see AttributeStage#attribute(Attribute, Object) + */ + public static final Attribute> TAGS = Attribute.generate(); + + /** + * Allows to pass arbitrary span logs directly from a call site. + * + * @see AttributeStage#attribute(Attribute, Object) + */ + public static final Attribute> LOGS = Attribute.generate(); + + private final Tracer tracer; + private final SpanDecorator decorator; + + public OpenTracingPlugin(final Tracer tracer) { + this(tracer, SpanDecorator.composite( + new CallSiteSpanDecorator(), + new ComponentSpanDecorator(), + new ErrorSpanDecorator(), + new HttpMethodSpanDecorator(), + new HttpPathSpanDecorator(), + new HttpStatusCodeSpanDecorator(), + new PeerSpanDecorator(), + new SpanKindSpanDecorator() + )); + } + + /** + * Creates a new {@link OpenTracingPlugin plugin} by combining the {@link SpanDecorator decorator(s)} of + * {@code this} plugin with the supplied ones. + * + * @param first first decorator + * @param decorators optional, remaining decorators + * @return a new {@link OpenTracingPlugin} + */ + @CheckReturnValue + public OpenTracingPlugin withAdditionalSpanDecorators(final SpanDecorator first, + final SpanDecorator... decorators) { + return withSpanDecorators(decorator, SpanDecorator.composite(first, decorators)); + } + + /** + * Creates a new {@link OpenTracingPlugin plugin} by replacing the {@link SpanDecorator decorator(s)} of + * {@code this} plugin with the supplied ones. + * + * @param decorator first decorator + * @param decorators optional, remaining decorators + * @return a new {@link OpenTracingPlugin} + */ + @CheckReturnValue + public OpenTracingPlugin withSpanDecorators(final SpanDecorator decorator, final SpanDecorator... decorators) { + return new OpenTracingPlugin(tracer, SpanDecorator.composite(decorator, decorators)); + } + + @Override + public RequestExecution aroundDispatch(final RequestExecution execution) { + return arguments -> { + final Span span = startSpan(arguments); + final Scope scope = tracer.activateSpan(span); + + return execution.execute(arguments) + .whenComplete(perform(scope::close)) + .whenComplete(perform(span::finish)); + }; + } + + @Override + public RequestExecution aroundNetwork(final RequestExecution execution) { + return arguments -> { + final Span span = tracer.activeSpan(); + + return execution.execute(inject(arguments, span.context())) + .whenComplete(onResponse(span, arguments)) + .whenComplete(onError(span, arguments)); + }; + } + + private Span startSpan(final RequestArguments arguments) { + final String operationName = arguments.getAttribute(OPERATION_NAME) + .orElse(arguments.getMethod().name()); + + final SpanBuilder builder = tracer.buildSpan(operationName); + decorator.onStart(builder, arguments); + final Span span = builder.start(); + decorator.onStarted(span, arguments); + return span; + } + + private RequestArguments inject(final RequestArguments arguments, final SpanContext context) { + final Map headers = new HashMap<>(); + tracer.inject(context, HTTP_HEADERS, new TextMapAdapter(headers)); + return arguments.withHeaders(Multimaps.forMap(headers).asMap()); + } + + private ThrowingBiConsumer onResponse(final Span span, + final RequestArguments arguments) { + + return (response, error) -> { + if (nonNull(response)) { + decorator.onResponse(span, arguments, response); + } + }; + } + + private BiConsumer onError(final Span span, final RequestArguments arguments) { + return (response, error) -> { + if (nonNull(error)) { + decorator.onError(span, arguments, unpack(error)); + } + }; + } + + private static BiConsumer perform(final Runnable runnable) { + return (t, u) -> runnable.run(); + } + + @VisibleForTesting + static Throwable unpack(final Throwable error) { + return error instanceof CompletionException ? error.getCause() : error; + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/package-info.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/package-info.java new file mode 100644 index 000000000..76c7c1377 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/package-info.java @@ -0,0 +1,4 @@ +@ParametersAreNonnullByDefault +package org.zalando.riptide.opentracing; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CallSiteSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CallSiteSpanDecorator.java new file mode 100644 index 000000000..1143555ad --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CallSiteSpanDecorator.java @@ -0,0 +1,25 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Span; +import io.opentracing.Tracer; +import org.zalando.riptide.RequestArguments; +import org.zalando.riptide.opentracing.OpenTracingPlugin; + +import java.util.Collections; + +public final class CallSiteSpanDecorator implements SpanDecorator { + + @Override + public void onStart(final Tracer.SpanBuilder builder, final RequestArguments arguments) { + arguments.getAttribute(OpenTracingPlugin.TAGS) + .orElseGet(Collections::emptyMap) + .forEach(builder::withTag); + } + + @Override + public void onStarted(final Span span, final RequestArguments arguments) { + arguments.getAttribute(OpenTracingPlugin.LOGS) + .ifPresent(span::log); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ComponentSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ComponentSpanDecorator.java new file mode 100644 index 000000000..4faf283a1 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ComponentSpanDecorator.java @@ -0,0 +1,29 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import org.zalando.riptide.RequestArguments; + +/** + * Sets the component span tag, defaults to Riptide. + * + * @see Standard Span Tags + */ +public final class ComponentSpanDecorator implements SpanDecorator { + + private final String component; + + public ComponentSpanDecorator() { + this("Riptide"); + } + + public ComponentSpanDecorator(final String component) { + this.component = component; + } + + @Override + public void onStart(final Tracer.SpanBuilder builder, final RequestArguments arguments) { + builder.withTag(Tags.COMPONENT, component); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CompositeSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CompositeSpanDecorator.java new file mode 100644 index 000000000..0743f41ae --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CompositeSpanDecorator.java @@ -0,0 +1,45 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Span; +import io.opentracing.Tracer.SpanBuilder; +import lombok.Getter; +import org.springframework.http.client.ClientHttpResponse; +import org.zalando.riptide.RequestArguments; + +import java.util.Collection; + +import static org.zalando.fauxpas.FauxPas.throwingConsumer; + +final class CompositeSpanDecorator implements SpanDecorator { + + @Getter + private final Collection decorators; + + CompositeSpanDecorator(final Collection decorators) { + this.decorators = decorators; + } + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + decorators.forEach(decorator -> decorator.onStart(builder, arguments)); + } + + @Override + public void onStarted(final Span span, final RequestArguments arguments) { + decorators.forEach(decorator -> decorator.onStarted(span, arguments)); + + } + + @Override + public void onResponse(final Span span, final RequestArguments arguments, final ClientHttpResponse response) { + decorators.forEach(throwingConsumer(decorator -> { + decorator.onResponse(span, arguments, response); + })); + } + + @Override + public void onError(final Span span, final RequestArguments arguments, final Throwable error) { + decorators.forEach(decorator -> decorator.onError(span, arguments, error)); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ErrorSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ErrorSpanDecorator.java new file mode 100644 index 000000000..d656921df --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ErrorSpanDecorator.java @@ -0,0 +1,36 @@ +package org.zalando.riptide.opentracing.span; + +import com.google.common.collect.ImmutableMap; +import io.opentracing.Span; +import io.opentracing.log.Fields; +import io.opentracing.tag.Tags; +import org.springframework.http.client.ClientHttpResponse; +import org.zalando.riptide.RequestArguments; + +import java.io.IOException; + +/** + * Sets the error span tag as well as the error.kind and error.object span logs. + * + * @see Standard Span Tags + * @see Standard Log Fields + */ +public final class ErrorSpanDecorator implements SpanDecorator { + + @Override + public void onResponse(final Span span, final RequestArguments arguments, final ClientHttpResponse response) throws IOException { + if (response.getStatusCode().is5xxServerError()) { + span.setTag(Tags.ERROR, true); + } + } + + @Override + public void onError(final Span span, final RequestArguments arguments, final Throwable error) { + span.setTag(Tags.ERROR, true); + span.log(ImmutableMap.of( + Fields.ERROR_KIND, error.getClass().getSimpleName(), + Fields.ERROR_OBJECT, error + )); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpMethodSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpMethodSpanDecorator.java new file mode 100644 index 000000000..106c6ee10 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpMethodSpanDecorator.java @@ -0,0 +1,19 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.tag.Tags; +import org.zalando.riptide.RequestArguments; + +/** + * Sets the http.method span tag. + * + * @see Standard Span Tags + */ +public final class HttpMethodSpanDecorator implements SpanDecorator { + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + builder.withTag(Tags.HTTP_METHOD, arguments.getMethod().name()); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpPathSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpPathSpanDecorator.java new file mode 100644 index 000000000..71bbad091 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpPathSpanDecorator.java @@ -0,0 +1,25 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Tracer.SpanBuilder; +import org.zalando.riptide.RequestArguments; +import org.zalando.riptide.opentracing.ExtensionTags; + +import static java.util.Objects.nonNull; + +/** + * Sets the http.path span tag, based on {@link RequestArguments#getUriTemplate()}. + * + * @see ExtensionTags#HTTP_PATH + */ +public final class HttpPathSpanDecorator implements SpanDecorator { + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + final String uriTemplate = arguments.getUriTemplate(); + + if (nonNull(uriTemplate)) { + builder.withTag(ExtensionTags.HTTP_PATH, uriTemplate); + } + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpStatusCodeSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpStatusCodeSpanDecorator.java new file mode 100644 index 000000000..eefabc853 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpStatusCodeSpanDecorator.java @@ -0,0 +1,21 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Span; +import io.opentracing.tag.Tags; +import org.springframework.http.client.ClientHttpResponse; +import org.zalando.riptide.RequestArguments; + +import java.io.IOException; + +/** + * Sets the http.status_code span tag. + * + * @see Standard Span Tags + */ +public final class HttpStatusCodeSpanDecorator implements SpanDecorator { + + @Override + public void onResponse(final Span span, final RequestArguments arguments, final ClientHttpResponse response) throws IOException { + span.setTag(Tags.HTTP_STATUS, response.getRawStatusCode()); + } +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpUrlSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpUrlSpanDecorator.java new file mode 100644 index 000000000..98b954cc9 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpUrlSpanDecorator.java @@ -0,0 +1,19 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.tag.Tags; +import org.zalando.riptide.RequestArguments; + +/** + * Sets the http.url span tag. + * + * @see Standard Span Tags + */ +public final class HttpUrlSpanDecorator implements SpanDecorator { + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + builder.withTag(Tags.HTTP_URL, arguments.getRequestUri().toString()); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/PeerSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/PeerSpanDecorator.java new file mode 100644 index 000000000..45ad2f4f6 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/PeerSpanDecorator.java @@ -0,0 +1,23 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.tag.Tags; +import org.zalando.riptide.RequestArguments; + +import java.net.URI; + +/** + * Sets the peer.hostname and peer.port span tags. + * + * @see Standard Span Tags + */ +public final class PeerSpanDecorator implements SpanDecorator { + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + final URI requestUri = arguments.getRequestUri(); + builder.withTag(Tags.PEER_HOSTNAME, requestUri.getHost()); + builder.withTag(Tags.PEER_PORT, requestUri.getPort()); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/RetrySpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/RetrySpanDecorator.java new file mode 100644 index 000000000..b52e119cd --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/RetrySpanDecorator.java @@ -0,0 +1,26 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Span; +import org.zalando.riptide.RequestArguments; +import org.zalando.riptide.failsafe.FailsafePlugin; +import org.zalando.riptide.opentracing.ExtensionFields; +import org.zalando.riptide.opentracing.ExtensionTags; + +import static java.util.Collections.singletonMap; + +/** + * @see FailsafePlugin#ATTEMPTS + * @see ExtensionTags#RETRY + * @see ExtensionFields#RETRY_NUMBER + */ +public final class RetrySpanDecorator implements SpanDecorator { + + @Override + public void onStarted(final Span span, final RequestArguments arguments) { + arguments.getAttribute(FailsafePlugin.ATTEMPTS).ifPresent(retries -> { + span.setTag(ExtensionTags.RETRY, true); + span.log(singletonMap(ExtensionFields.RETRY_NUMBER, retries)); + }); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanDecorator.java new file mode 100644 index 000000000..7aab4d158 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanDecorator.java @@ -0,0 +1,48 @@ +package org.zalando.riptide.opentracing.span; + +import com.google.common.collect.Lists; +import io.opentracing.Span; +import io.opentracing.Tracer.SpanBuilder; +import org.springframework.http.client.ClientHttpResponse; +import org.zalando.riptide.RequestArguments; + +import java.io.IOException; +import java.util.Collection; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.toList; + +public interface SpanDecorator { + + default void onStart(final SpanBuilder builder, final RequestArguments arguments) { + // nothing to do + } + + default void onStarted(final Span span, final RequestArguments arguments) { + // nothing to do + } + + default void onResponse(final Span span, final RequestArguments arguments, final ClientHttpResponse response) + throws IOException { + // nothing to do + } + + default void onError(final Span span, final RequestArguments arguments, final Throwable error) { + // nothing to do + } + + static SpanDecorator composite(final SpanDecorator decorator, final SpanDecorator... decorators) { + return composite(Lists.asList(decorator, decorators)); + } + + static SpanDecorator composite(final Collection decorators) { + // we flatten first level of nested composite decorators + return decorators.stream() + .flatMap(decorator -> decorator instanceof CompositeSpanDecorator ? + CompositeSpanDecorator.class.cast(decorator).getDecorators().stream() : + Stream.of(decorator)) + .collect(collectingAndThen(toList(), CompositeSpanDecorator::new)); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanKindSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanKindSpanDecorator.java new file mode 100644 index 000000000..826842e9a --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanKindSpanDecorator.java @@ -0,0 +1,29 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Tracer.SpanBuilder; +import io.opentracing.tag.Tags; +import org.zalando.riptide.RequestArguments; + +/** + * Sets the span.kind span tag. + * + * @see Standard Span Tags + */ +public final class SpanKindSpanDecorator implements SpanDecorator { + + private final String kind; + + public SpanKindSpanDecorator() { + this(Tags.SPAN_KIND_CLIENT); + } + + public SpanKindSpanDecorator(final String kind) { + this.kind = kind; + } + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + builder.withTag(Tags.SPAN_KIND, kind); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/StaticSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/StaticSpanDecorator.java new file mode 100644 index 000000000..f281246cc --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/StaticSpanDecorator.java @@ -0,0 +1,24 @@ +package org.zalando.riptide.opentracing.span; + +import io.opentracing.Tracer.SpanBuilder; +import org.zalando.riptide.RequestArguments; + +import java.util.Map; + +/** + * Sets arbitrary, static span tags. + */ +public final class StaticSpanDecorator implements SpanDecorator { + + private final Map tags; + + public StaticSpanDecorator(final Map tags) { + this.tags = tags; + } + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + tags.forEach(builder::withTag); + } + +} diff --git a/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/UriVariablesTagSpanDecorator.java b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/UriVariablesTagSpanDecorator.java new file mode 100644 index 000000000..2f2833223 --- /dev/null +++ b/riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/UriVariablesTagSpanDecorator.java @@ -0,0 +1,56 @@ +package org.zalando.riptide.opentracing.span; + +import com.google.common.collect.ImmutableMap; +import com.google.gag.annotation.remark.Hack; +import io.opentracing.Tracer.SpanBuilder; +import org.springframework.web.util.UriComponentsBuilder; +import org.zalando.riptide.RequestArguments; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * A {@link SpanDecorator decorator} that extracts contextual tags from the used + * {@link RequestArguments#getUriTemplate() URI template} and {@link RequestArguments#getUriVariables() URI variables}. + * + * Using this decorator in conjunction with {@code http.get("/accounts/{account_id}", 792)} + * will produce the tag {@code account_id=792}. + * + * The OpenTracing Semantic Specification: Start a new Span + */ +public final class UriVariablesTagSpanDecorator implements SpanDecorator { + + @Override + public void onStart(final SpanBuilder builder, final RequestArguments arguments) { + final Map variables = extract(arguments); + variables.forEach(builder::withTag); + } + + private Map extract(final RequestArguments arguments) { + @Nullable final String template = arguments.getUriTemplate(); + + if (template == null) { + return ImmutableMap.of(); + } + + return extract(template, arguments.getUriVariables()); + } + + @Hack("Pretty dirty, but I couldn't find any other way...") + private Map extract(final String template, final List values) { + final Map variables = new HashMap<>(values.size()); + final Iterator iterator = values.iterator(); + + UriComponentsBuilder.fromUriString(template).build().expand(name -> { + final Object value = iterator.next(); + variables.put(name, String.valueOf(value)); + return value; + }); + + return variables; + } + +} diff --git a/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginRetryTest.java b/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginRetryTest.java new file mode 100644 index 000000000..8afcac800 --- /dev/null +++ b/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginRetryTest.java @@ -0,0 +1,105 @@ +package org.zalando.riptide.opentracing; + +import com.github.restdriver.clientdriver.ClientDriver; +import com.github.restdriver.clientdriver.ClientDriverFactory; +import com.google.common.collect.ImmutableList; +import io.opentracing.contrib.concurrent.TracedExecutorService; +import io.opentracing.contrib.concurrent.TracedScheduledExecutorService; +import io.opentracing.mock.MockSpan; +import io.opentracing.mock.MockTracer; +import net.jodah.failsafe.RetryPolicy; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.zalando.riptide.Http; +import org.zalando.riptide.Plugin; +import org.zalando.riptide.failsafe.FailsafePlugin; +import org.zalando.riptide.opentracing.span.HttpUrlSpanDecorator; +import org.zalando.riptide.opentracing.span.RetrySpanDecorator; + +import java.util.List; + +import static com.github.restdriver.clientdriver.RestClientDriver.giveEmptyResponse; +import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; +import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.zalando.riptide.PassRoute.pass; + +final class OpenTracingPluginRetryTest { + + private final ClientDriver driver = new ClientDriverFactory().createClientDriver(); + private final MockTracer tracer = new MockTracer(); + + private final Plugin unit = new OpenTracingPlugin(tracer) + .withAdditionalSpanDecorators(new HttpUrlSpanDecorator()) + .withAdditionalSpanDecorators(new RetrySpanDecorator()); + + private final Http http = Http.builder() + .executor(new TracedExecutorService(newSingleThreadExecutor(), tracer)) + .requestFactory(new HttpComponentsClientHttpRequestFactory()) + .baseUrl(driver.getBaseUrl()) + .plugin(unit) + .plugin(new FailsafePlugin( + ImmutableList.of(new RetryPolicy() + .withMaxRetries(1) + .handleResultIf(response -> true)), + new TracedScheduledExecutorService(newSingleThreadScheduledExecutor(), tracer))) + .plugin(unit) + .build(); + + @Test + void shouldTagRetries() { + driver.addExpectation(onRequestTo("/"), giveEmptyResponse().withStatus(200)); + driver.addExpectation(onRequestTo("/"), giveEmptyResponse().withStatus(200)); + + http.get("/").call(pass()).join(); + + final List spans = tracer.finishedSpans(); + + assertThat(spans, hasSize(3)); + + spans.forEach(span -> { + assertThat(span.generatedErrors(), is(empty())); + assertThat(span.tags(), hasKey("http.url")); + }); + + final List roots = spans.stream() + .filter(span -> span.parentId() == 0) + .collect(toList()); + + assertThat(roots, hasSize(1)); + + roots.forEach(root -> + assertThat(root.tags(), not(hasKey("http.status_code")))); + + final List children = spans.stream() + .filter(span -> span.parentId() > 0) + .collect(toList()); + + assertThat(children, hasSize(2)); + + children.forEach(child -> + assertThat(child.tags(), hasKey("http.status_code"))); + + final List retries = spans.stream() + .filter(span -> span.tags().containsKey("retry")) + .collect(toList()); + + assertThat(retries, hasSize(1)); + } + + @AfterEach + void tearDown() { + driver.verify(); + driver.shutdown(); + } + +} diff --git a/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginTest.java b/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginTest.java new file mode 100644 index 000000000..c228564be --- /dev/null +++ b/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginTest.java @@ -0,0 +1,241 @@ +package org.zalando.riptide.opentracing; + +import com.github.restdriver.clientdriver.ClientDriver; +import com.github.restdriver.clientdriver.ClientDriverFactory; +import io.opentracing.Scope; +import io.opentracing.contrib.concurrent.TracedExecutorService; +import io.opentracing.mock.MockSpan; +import io.opentracing.mock.MockSpan.LogEntry; +import io.opentracing.mock.MockTracer; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.zalando.riptide.Http; +import org.zalando.riptide.UnexpectedResponseException; +import org.zalando.riptide.opentracing.span.StaticSpanDecorator; +import org.zalando.riptide.opentracing.span.UriVariablesTagSpanDecorator; + +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +import static com.github.restdriver.clientdriver.RestClientDriver.giveEmptyResponse; +import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; +import static com.google.common.collect.Iterables.getOnlyElement; +import static java.util.Collections.singletonMap; +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.zalando.riptide.NoRoute.noRoute; +import static org.zalando.riptide.PassRoute.pass; + +final class OpenTracingPluginTest { + + private final ClientDriver driver = new ClientDriverFactory().createClientDriver(); + private final MockTracer tracer = new MockTracer(); + + private final Http unit = Http.builder() + .executor(new TracedExecutorService(newSingleThreadExecutor(), tracer)) + .requestFactory(new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create() + .setDefaultRequestConfig(RequestConfig.custom() + .setSocketTimeout(500) + .build()) + .build())) + .baseUrl(driver.getBaseUrl()) + .plugin(new OpenTracingPlugin(tracer) + .withAdditionalSpanDecorators( + new StaticSpanDecorator(singletonMap("test.environment", "JUnit")), + new UriVariablesTagSpanDecorator())) + .build(); + + // TODO set socket timeout and test network error + + @Test + void shouldTraceRequestAndResponse() { + driver.addExpectation(onRequestTo("/users/me") + .withHeader("traceid", notNullValue(String.class)) + .withHeader("spanid", notNullValue(String.class)), + giveEmptyResponse().withStatus(200)); + + final MockSpan parent = tracer.buildSpan("test").start(); + + try (final Scope ignored = tracer.activateSpan(parent)) { + unit.get("/users/{user}", "me") + .attribute(OpenTracingPlugin.TAGS, singletonMap("test", "true")) + .attribute(OpenTracingPlugin.LOGS, singletonMap("retry_number", 1)) + .call(pass()) + .join(); + } finally { + parent.finish(); + } + + final List spans = tracer.finishedSpans(); + assertThat(spans, hasSize(2)); + + assertThat(spans.get(1), is(parent)); + + final MockSpan child = spans.get(0); + assertThat(child.parentId(), is(parent.context().spanId())); + + assertThat(child.tags(), hasEntry("component", "Riptide")); + assertThat(child.tags(), hasEntry("span.kind", "client")); + assertThat(child.tags(), hasEntry("peer.hostname", "localhost")); + assertThat(child.tags(), hasEntry("peer.port", driver.getPort())); + assertThat(child.tags(), hasEntry("http.method", "GET")); + assertThat(child.tags(), hasEntry("http.path", "/users/{user}")); + assertThat(child.tags(), hasEntry("http.status_code", 200)); + assertThat(child.tags(), hasEntry("test", "true")); + assertThat(child.tags(), hasEntry("test.environment", "JUnit")); + assertThat(child.tags(), hasEntry("user", "me")); + + // not active by default + assertThat(child.tags(), not(hasKey("http.url"))); + + final LogEntry log = getOnlyElement(child.logEntries()); + + assertThat(log.fields(), hasEntry("retry_number", 1)); + } + + @Test + void shouldTraceRequestAndServerError() { + driver.addExpectation(onRequestTo("/"), giveEmptyResponse().withStatus(500)); + + final MockSpan parent = tracer.buildSpan("test").start(); + + try (final Scope ignored = tracer.activateSpan(parent)) { + final CompletableFuture future = unit.get(URI.create(driver.getBaseUrl())) + .attribute(OpenTracingPlugin.TAGS, singletonMap("test", "true")) + .attribute(OpenTracingPlugin.LOGS, singletonMap("retry_number", 2)) + .call(noRoute()); + + final CompletionException error = assertThrows(CompletionException.class, future::join); + assertThat(error.getCause(), is(instanceOf(UnexpectedResponseException.class))); + } finally { + parent.finish(); + } + + final List spans = tracer.finishedSpans(); + assertThat(spans, hasSize(2)); + + assertThat(spans.get(1), is(parent)); + + final MockSpan child = spans.get(0); + assertThat(child.parentId(), is(parent.context().spanId())); + + assertThat(child.tags(), hasEntry("component", "Riptide")); + assertThat(child.tags(), hasEntry("span.kind", "client")); + assertThat(child.tags(), hasEntry("peer.hostname", "localhost")); + assertThat(child.tags(), hasEntry("peer.port", driver.getPort())); + assertThat(child.tags(), hasEntry("http.method", "GET")); + assertThat(child.tags(), hasEntry("http.status_code", 500)); + assertThat(child.tags(), hasEntry("error", true)); + assertThat(child.tags(), hasEntry("test", "true")); + assertThat(child.tags(), hasEntry("test.environment", "JUnit")); + + // since we didn't use a uri template + assertThat(child.tags(), not(hasKey("http.path"))); + + final LogEntry log = getOnlyElement(child.logEntries()); + assertThat(log.fields(), hasEntry("retry_number", 2)); + } + + @Test + void shouldTraceRequestAndNetworkError() { + driver.addExpectation(onRequestTo("/"), giveEmptyResponse().after(1, SECONDS)); + + final MockSpan parent = tracer.buildSpan("test").start(); + + try (final Scope ignored = tracer.activateSpan(parent)) { + final CompletableFuture future = unit.get(URI.create(driver.getBaseUrl())) + .call(noRoute()); + + final CompletionException error = assertThrows(CompletionException.class, future::join); + assertThat(error.getCause(), is(instanceOf(SocketTimeoutException.class))); + } finally { + parent.finish(); + } + + final List spans = tracer.finishedSpans(); + assertThat(spans, hasSize(2)); + + assertThat(spans.get(1), is(parent)); + + final MockSpan child = spans.get(0); + assertThat(child.parentId(), is(parent.context().spanId())); + + assertThat(child.tags(), hasEntry("component", "Riptide")); + assertThat(child.tags(), hasEntry("span.kind", "client")); + assertThat(child.tags(), hasEntry("peer.hostname", "localhost")); + assertThat(child.tags(), hasEntry("peer.port", driver.getPort())); + assertThat(child.tags(), hasEntry("http.method", "GET")); + assertThat(child.tags(), hasEntry("error", true)); + + // since we didn't use a uri template + assertThat(child.tags(), not(hasKey("http.path"))); + + // since we didn't get any response + assertThat(child.tags(), not(hasKey("http.status_code"))); + + final LogEntry log = getOnlyElement(child.logEntries()); + assertThat(log.fields(), hasEntry("error.kind", "SocketTimeoutException")); + assertThat(log.fields(), hasEntry(is("error.object"), is(instanceOf(SocketTimeoutException.class)))); + } + + @Test + void shouldTraceRequestAndIgnoreClientError() { + driver.addExpectation(onRequestTo("/"), giveEmptyResponse().withStatus(400)); + + final MockSpan parent = tracer.buildSpan("test").start(); + + try (final Scope ignored = tracer.activateSpan(parent)) { + final CompletableFuture future = unit.get(URI.create(driver.getBaseUrl())) + .call(noRoute()); + + final CompletionException error = assertThrows(CompletionException.class, future::join); + assertThat(error.getCause(), is(instanceOf(UnexpectedResponseException.class))); + } finally { + parent.finish(); + } + + final List spans = tracer.finishedSpans(); + assertThat(spans, hasSize(2)); + + assertThat(spans.get(1), is(parent)); + + final MockSpan child = spans.get(0); + assertThat(child.parentId(), is(parent.context().spanId())); + + assertThat(child.tags(), hasEntry("component", "Riptide")); + assertThat(child.tags(), hasEntry("span.kind", "client")); + assertThat(child.tags(), hasEntry("peer.hostname", "localhost")); + assertThat(child.tags(), hasEntry("peer.port", driver.getPort())); + assertThat(child.tags(), hasEntry("http.method", "GET")); + assertThat(child.tags(), hasEntry("http.status_code", 400)); + + // since we didn't use a uri template + assertThat(child.tags(), not(hasKey("error"))); + + assertThat(child.logEntries(), is(empty())); + } + + @AfterEach + void tearDown() { + driver.verify(); + driver.shutdown(); + } + +} diff --git a/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/UnpackTest.java b/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/UnpackTest.java new file mode 100644 index 000000000..abefcc614 --- /dev/null +++ b/riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/UnpackTest.java @@ -0,0 +1,26 @@ +package org.zalando.riptide.opentracing; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.concurrent.CompletionException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.zalando.riptide.opentracing.OpenTracingPlugin.unpack; + +final class UnpackTest { + + @Test + void shouldUnpackCompletionException() { + final IOException cause = new IOException(); + assertThat(unpack(new CompletionException(cause)), is(cause)); + } + + @Test + void shouldNotUnpackNonCompletionException() { + final RuntimeException exception = new RuntimeException(new IOException()); + assertThat(unpack(exception), is(exception)); + } + +} diff --git a/riptide-spring-boot-autoconfigure/README.md b/riptide-spring-boot-autoconfigure/README.md index 522801411..4373e11f9 100644 --- a/riptide-spring-boot-autoconfigure/README.md +++ b/riptide-spring-boot-autoconfigure/README.md @@ -23,19 +23,28 @@ together whenever interaction with a remote service is required. Spinning up new riptide.clients: example: base-url: http://example.com - connect-timeout: 150 milliseconds - socket-timeout: 100 milliseconds - connection-time-to-live: 30 seconds - max-connections-per-route: 16 + connections: + connect-timeout: 150 milliseconds + socket-timeout: 100 milliseconds + time-to-live: 30 seconds + max-per-route: 16 retry: + enabled: true fixed-delay: 50 milliseconds max-retries: 5 circuit-breaker: + enabled: true failure-threshold: 3 out of 5 delay: 30 seconds success-threshold: 5 out of 5 caching: + enabled: true + shared: false max-cache-entries: 1000 + tracing: + enabled: true + tags: + peer.service: example ``` ```java @@ -111,7 +120,7 @@ Required for `retry` and `circuit-breaker` support. #### [Transient Fault](../riptide-faults) detection -Required when `detect-transient-faults` is enabled. +Required when `transient-fault-detection` is enabled. ```xml @@ -196,7 +205,7 @@ OAuth2 tokens as files in a mounted directory. See #### [Metrics](../riptide-metrics) integration -Required when `record-metrics` is enabled. +Required when `metrics` is enabled. Will activate `micrometer` metrics support for: @@ -239,6 +248,23 @@ Required when `caching` is configured: ``` +#### Tracing + +Required when `tracing` is configured: + +```xml + + org.zalando + riptide-opentracing + ${riptide.version} + + + io.opentracing.contrib + opentracing-concurrent + ${opentracing-concurrent.version} + +``` + ## Configuration You can now define new clients and override default configuration in your `application.yml`: @@ -248,21 +274,30 @@ riptide: defaults: oauth: credentials-directory: /secrets + tracing: + enabled: true + tags: + account: ${CDP_TARGET_INFRASTRUCTURE_ACCOUNT} + zone: ${CDP_TARGET_REGION} + artifact_version: ${CDP_BUILD_VERSION} + deployment_id: ${CDP_DEPLOYMENT_ID} clients: example: base-url: http://example.com connections: - connect-timeout: 150 milliseconds - socket-timeout: 100 milliseconds - time-to-live: 30 seconds - max-per-route: 16 + connect-timeout: 150 milliseconds + socket-timeout: 100 milliseconds + time-to-live: 30 seconds + max-per-route: 16 threads: min-size: 4 max-size: 16 keep-alive: 1 minnute queue-size: 0 + oauth: + enabled: true transient-fault-detection.enabled: true - stack-trace-preservation: true + stack-trace-preservation.enabled: true retry: enabled: true fixed-delay: 50 milliseconds @@ -290,8 +325,10 @@ riptide: enabled: true coefficient: 0.1 default-life-time: 10 minutes - oauth: - enabled: true + tracing: + tags: + peer.service: example + propagate-flow-id: true ``` Clients are identified by a *Client ID*, for instance `example` in the sample above. You can have as many clients as you want. @@ -300,141 +337,149 @@ Clients are identified by a *Client ID*, for instance `example` in the sample ab For a complete overview of available properties, they type and default value please refer to the following table: -| Configuration | Data type | Default / Comment | -|-----------------------------------------|----------------|--------------------------------------------------| -| `riptide` | | | -| `├── defaults` | | | -| `│   ├── url-resolution` | `String` | `rfc`, not applicable to Async/RestTemplate | -| `│   ├── connections` | | | -| `│   │  ├── connect-timeout` | `TimeSpan` | `5 seconds` | -| `│   │  ├── socket-timeout` | `TimeSpan` | `5 seconds` | -| `│   │  ├── time-to-live` | `TimeSpan` | `30 seconds` | -| `│   │  ├── max-per-route` | `int` | `20` | -| `│   │  └── max-total` | `int` | `20` (or at least `max-per-route`) | -| `│   ├── threads` | | | -| `│   │   ├── min-size` | `int` | `1` | -| `│   │   ├── max-size` | `int` | same as `connections.max-total` | -| `│   │   ├── keep-alive` | `TimeSpan` | `1 minute` | -| `│   │   └── queue-size` | `int` | `0` | -| `│   ├── oauth` | | | -| `│   │   ├── enabled` | `boolean` | `false` | -| `│   │   └── credentials-directory` | `Path` | `/meta/credentials` | -| `│   ├── transient-fault-detection` | | | -| `│   │   └── enabled` | `boolean` | `false` | -| `│   ├── stack-trace-preservation` | | | -| `│   │   └── enabled` | `boolean` | `false` | -| `│   ├── metrics` | | | -| `│   │   └── enabled` | `boolean` | `false` | -| `│   ├── retry` | | | -| `│   │   ├── enabled` | `boolean` | `false` | -| `│   │   ├── fixed-delay` | `TimeSpan` | none, mutually exclusive to `backoff` | -| `│   │   ├── backoff` | | none, mutually exclusive to `fixed-delay` | -| `│   │   │ ├── enabled` | `boolean` | `false` | -| `│   │   │   ├── delay` | `TimeSpan` | none, requires `backoff.max-delay` | -| `│   │   │   ├── max-delay` | `TimeSpan` | none, requires `backoff.delay` | -| `│   │   │   └── delay-factor` | `double` | `2.0` | -| `│   │   ├── max-retries` | `int` | none | -| `│   │   ├── max-duration` | `TimeSpan` | none | -| `│   │   ├── jitter-factor` | `double` | none, mutually exclusive to `jitter` | -| `│   │   └── jitter` | `TimeSpan` | none, mutually exclusive to `jitter-factor` | -| `│   ├── circuit-breaker` | | | -| `│   │   ├── enabled` | `boolean` | `false` | -| `│   │   ├── failure-threshold` | `Ratio` | none | -| `│   │   ├── delay` | `TimeSpan` | no delay | -| `│   │   └── success-threshold` | `Ratio` | `failure-threshold` | -| `│   ├── backup-request` | | | -| `│   │   ├── enabled` | `boolean` | `false` | -| `│   │   └── delay` | `TimeSpan` | no delay | -| `│   ├── timeouts` | | | -| `│   │   ├── enabled` | `boolean` | `false` | -| `│   │   └── global` | `TimeSpan` | none | -| `│   ├── request-compression` | | | -| `│   │ └── enabled` | `boolean` | `false` | -| `│   ├── certificate-pinning` | | | -| `│   │   ├── enabled` | `boolean` | `false` | -| `│   │   └── keystore` | | | -| `│   │       ├── path` | `Path` | none | -| `│   │       └── password` | `String` | none | -| `│   ├── caching` | | | -| `│   │   ├── enabled` | `boolean` | `false` | -| `│   │   ├── shared` | `boolean` | `false` | -| `│   │   ├── directory` | `String` | none, *in-memory* caching by default | -| `│   │   ├── max-object-size` | `int` | `8192` | -| `│   │   ├── max-cache-entries` | `int` | `1000` | -| `│   │   └── heuristic` | | If max age was not specified by the server | -| `│   │    ├── enabled` | `boolean` | `false` | -| `│   │       ├── coefficient` | `double` | `0.1` | -| `│   │       └── default-life-time` | `TimeSpan` | `0 seconds`, disabled | -| `│ └── soap` | | | -| `│       ├── enabled` | `boolean` | `false` | -| `│       └── protocol` | `String` | `1.1` (possible other value: `1.2`) | -| `└── clients` | | | -| `    └── ` | `String` | | -| `        ├── base-url` | `URI` | none | -| `        ├── url-resolution` | `String` | see `defaults` | -| `        ├── connections` | | | -| `        │  ├── connect-timeout` | `TimeSpan` | see `defaults` | -| `        │  ├── socket-timeout` | `TimeSpan` | see `defaults` | -| `        │  ├── time-to-live` | `TimeSpan` | see `defaults` | -| `        │  ├── max-per-route` | `int` | see `defaults` | -| `        │  └── max-total` | `int` | see `defaults` | -| `        └── threads` | | | -| `            ├── min-size` | `int` | see `defaults` | -| `            ├── max-size` | `int` | see `defaults` | -| `            ├── keep-alive` | `TimeSpan` | see `defaults` | -| `            └── queue-size` | `int` | see `defaults` | -| `        ├── oauth` | | | -| `        │   ├── enabled` | `boolean` | see `defaults` | -| `        │ └── credentials-directory` | `Path` | see `defaults` | -| `        ├── transient-fault-detection` | | | -| `        │   └── enabled` | `boolean` | see `defaults` | -| `        ├── stack-trace-preservation` | | | -| `        │   └── enabled` | `boolean` | see `defaults` | -| `        ├── metrics` | | | -| `        │   └── enabled` | `boolean` | see `defaults` | -| `        ├── retry` | | | -| `        │ ├── enabled` | `boolean` | see `defaults` | -| `        │   ├── fixed-delay` | `TimeSpan` | see `defaults` | -| `        │   ├── backoff` | | | -| `    │   │ ├── enabled` | `boolean` | see `defaults` | -| `        │   │   ├── delay` | `TimeSpan` | see `defaults` | -| `        │   │   ├── max-delay` | `TimeSpan` | see `defaults` | -| `        │   │   └── delay-factor` | `double` | see `defaults` | -| `        │   ├── max-retries` | `int` | see `defaults` | -| `        │   ├── max-duration` | `TimeSpan` | see `defaults` | -| `        │   ├── jitter-factor` | `double` | see `defaults` | -| `        │   └── jitter` | `TimeSpan` | see `defaults` | -| `        ├── circuit-breaker` | | | -| `        │ ├── enabled` | `boolean` | see `defaults` | -| `        │   ├── failure-threshold` | `Ratio` | see `defaults` | -| `        │   ├── delay` | `TimeSpan` | see `defaults` | -| `        │   └── success-threshold` | `Ratio` | see `defaults` | -| `        ├── backup-request` | | | -| `        │ ├── enabled` | `boolean` | see `defaults` | -| `        │   └── delay` | `TimeSpan` | see `defaults` | -| `        ├── timeouts` | | | -| `        │ ├── enabled` | `boolean` | see `defaults` | -| `        │   └── global` | `TimeSpan` | see `defaults` | -| `        ├── request-compression` | | | -| `        │ └── enabled` | `boolean` | see `defaults` | -| `    ├── certificate-pinning` | | | -| `    │   ├── enabled` | `boolean` | see `defaults` | -| `    │   └── keystore` | | | -| `    │       ├── path` | `Path` | see `defaults` | -| `    │       └── password` | `String` | see `defaults` | -| `        ├── caching` | | see `defaults` | -| `    │   ├── enabled` | `boolean` | see `defaults` | -| `    │   ├── shared` | `boolean` | see `defaults` | -| `    │   ├── directory` | `String` | see `defaults` | -| `    │   ├── max-object-size` | `int` | see `defaults` | -| `    │   ├── max-cache-entries` | `int` | see `defaults` | -| `    │   └── heuristic` | | | -| `    │    ├── enabled` | `boolean` | see `defaults` | -| `    │       ├── coefficient` | `double` | see `defaults` | -| `    │       └── default-life-time` | `TimeSpan` | see `defaults` | -| ` └── soap` | | | -| `       ├── enabled` | `boolean` | see `defaults` | -| `       └── protocol` | `String` | see `defaults` | +| Configuration | Data type | Default / Comment | +|-----------------------------------------|-----------------------|--------------------------------------------------| +| `riptide` | | | +| `├── defaults` | | | +| `│   ├── url-resolution` | `String` | `rfc`, not applicable to Async/RestTemplate | +| `│   ├── connections` | | | +| `│   │  ├── connect-timeout` | `TimeSpan` | `5 seconds` | +| `│   │  ├── socket-timeout` | `TimeSpan` | `5 seconds` | +| `│   │  ├── time-to-live` | `TimeSpan` | `30 seconds` | +| `│   │  ├── max-per-route` | `int` | `20` | +| `│   │  └── max-total` | `int` | `20` (or at least `max-per-route`) | +| `│   ├── threads` | | | +| `│   │   ├── min-size` | `int` | `1` | +| `│   │   ├── max-size` | `int` | same as `connections.max-total` | +| `│   │   ├── keep-alive` | `TimeSpan` | `1 minute` | +| `│   │   └── queue-size` | `int` | `0` | +| `│   ├── oauth` | | | +| `│   │   ├── enabled` | `boolean` | `false` | +| `│   │   └── credentials-directory` | `Path` | `/meta/credentials` | +| `│   ├── transient-fault-detection` | | | +| `│   │   └── enabled` | `boolean` | `false` | +| `│   ├── stack-trace-preservation` | | | +| `│   │   └── enabled` | `boolean` | `false` | +| `│   ├── metrics` | | | +| `│   │   └── enabled` | `boolean` | `false` | +| `│   ├── retry` | | | +| `│   │   ├── enabled` | `boolean` | `false` | +| `│   │   ├── fixed-delay` | `TimeSpan` | none, mutually exclusive to `backoff` | +| `│   │   ├── backoff` | | none, mutually exclusive to `fixed-delay` | +| `│   │   │ ├── enabled` | `boolean` | `false` | +| `│   │   │   ├── delay` | `TimeSpan` | none, requires `backoff.max-delay` | +| `│   │   │   ├── max-delay` | `TimeSpan` | none, requires `backoff.delay` | +| `│   │   │   └── delay-factor` | `double` | `2.0` | +| `│   │   ├── max-retries` | `int` | none | +| `│   │   ├── max-duration` | `TimeSpan` | none | +| `│   │   ├── jitter-factor` | `double` | none, mutually exclusive to `jitter` | +| `│   │   └── jitter` | `TimeSpan` | none, mutually exclusive to `jitter-factor` | +| `│   ├── circuit-breaker` | | | +| `│   │   ├── enabled` | `boolean` | `false` | +| `│   │   ├── failure-threshold` | `Ratio` | none | +| `│   │   ├── delay` | `TimeSpan` | no delay | +| `│   │   └── success-threshold` | `Ratio` | `failure-threshold` | +| `│   ├── backup-request` | | | +| `│   │   ├── enabled` | `boolean` | `false` | +| `│   │   └── delay` | `TimeSpan` | no delay | +| `│   ├── timeouts` | | | +| `│   │   ├── enabled` | `boolean` | `false` | +| `│   │   └── global` | `TimeSpan` | none | +| `│   ├── request-compression` | | | +| `│   │ └── enabled` | `boolean` | `false` | +| `│   ├── certificate-pinning` | | | +| `│   │   ├── enabled` | `boolean` | `false` | +| `│   │   └── keystore` | | | +| `│   │       ├── path` | `Path` | none | +| `│   │       └── password` | `String` | none | +| `│   ├── caching` | | | +| `│   │   ├── enabled` | `boolean` | `false` | +| `│   │   ├── shared` | `boolean` | `true` | +| `│   │   ├── directory` | `String` | none, *in-memory* caching by default | +| `│   │   ├── max-object-size` | `int` | `8192` | +| `│   │   ├── max-cache-entries` | `int` | `1000` | +| `│   │   └── heuristic` | | If max age was not specified by the server | +| `│   │    ├── enabled` | `boolean` | `false` | +| `│   │       ├── coefficient` | `double` | `0.1` | +| `│   |       └── default-life-time` | `TimeSpan` | `0 seconds`, disabled | +| `│   ├── tracing` | | | +| `│   |   ├── enabled` | `boolean` | `false` | +| `│   |   ├── tags` | `Map` | none | +| `│   |   └── propagate-flow-id` | `boolean` | `false` | +| `│ └── soap` | | | +| `│       ├── enabled` | `boolean` | `false` | +| `│       └── protocol` | `String` | `1.1` (possible other value: `1.2`) | +| `└── clients` | | | +| `    └── ` | `String` | | +| `        ├── base-url` | `URI` | none | +| `        ├── url-resolution` | `String` | see `defaults` | +| `        ├── connections` | | | +| `        │  ├── connect-timeout` | `TimeSpan` | see `defaults` | +| `        │  ├── socket-timeout` | `TimeSpan` | see `defaults` | +| `        │  ├── time-to-live` | `TimeSpan` | see `defaults` | +| `        │  ├── max-per-route` | `int` | see `defaults` | +| `        │  └── max-total` | `int` | see `defaults` | +| `        ├── threads` | | | +| `        │   ├── min-size` | `int` | see `defaults` | +| `        │   ├── max-size` | `int` | see `defaults` | +| `        │   ├── keep-alive` | `TimeSpan` | see `defaults` | +| `        │   └── queue-size` | `int` | see `defaults` | +| `        ├── oauth` | | | +| `        │   ├── enabled` | `boolean` | see `defaults` | +| `        │ └── credentials-directory` | `Path` | see `defaults` | +| `        ├── transient-fault-detection` | | | +| `        │   └── enabled` | `boolean` | see `defaults` | +| `        ├── stack-trace-preservation` | | | +| `        │   └── enabled` | `boolean` | see `defaults` | +| `        ├── metrics` | | | +| `        │   └── enabled` | `boolean` | see `defaults` | +| `        ├── retry` | | | +| `        │ ├── enabled` | `boolean` | see `defaults` | +| `        │   ├── fixed-delay` | `TimeSpan` | see `defaults` | +| `        │   ├── backoff` | | | +| `    │   │ ├── enabled` | `boolean` | see `defaults` | +| `        │   │   ├── delay` | `TimeSpan` | see `defaults` | +| `        │   │   ├── max-delay` | `TimeSpan` | see `defaults` | +| `        │   │   └── delay-factor` | `double` | see `defaults` | +| `        │   ├── max-retries` | `int` | see `defaults` | +| `        │   ├── max-duration` | `TimeSpan` | see `defaults` | +| `        │   ├── jitter-factor` | `double` | see `defaults` | +| `        │   └── jitter` | `TimeSpan` | see `defaults` | +| `        ├── circuit-breaker` | | | +| `        │ ├── enabled` | `boolean` | see `defaults` | +| `        │   ├── failure-threshold` | `Ratio` | see `defaults` | +| `        │   ├── delay` | `TimeSpan` | see `defaults` | +| `        │   └── success-threshold` | `Ratio` | see `defaults` | +| `        ├── backup-request` | | | +| `        │ ├── enabled` | `boolean` | see `defaults` | +| `        │   └── delay` | `TimeSpan` | see `defaults` | +| `        ├── timeouts` | | | +| `        │ ├── enabled` | `boolean` | see `defaults` | +| `        │   └── global` | `TimeSpan` | see `defaults` | +| `        ├── request-compression` | | | +| `        │ └── enabled` | `boolean` | see `defaults` | +| `    ├── certificate-pinning` | | | +| `    │   ├── enabled` | `boolean` | see `defaults` | +| `    │   └── keystore` | | | +| `    │       ├── path` | `Path` | see `defaults` | +| `    │       └── password` | `String` | see `defaults` | +| `        ├── caching` | | see `defaults` | +| `    │   ├── enabled` | `boolean` | see `defaults` | +| `    │   ├── shared` | `boolean` | see `defaults` | +| `    │   ├── directory` | `String` | see `defaults` | +| `    │   ├── max-object-size` | `int` | see `defaults` | +| `    │   ├── max-cache-entries` | `int` | see `defaults` | +| `    │   └── heuristic` | | | +| `    │    ├── enabled` | `boolean` | see `defaults` | +| `    │       ├── coefficient` | `double` | see `defaults` | +| `    │       └── default-life-time` | `TimeSpan` | see `defaults` | +| `    ├── tracing` | | | +| `    │   ├── enabled` | `boolean` | see `defaults` | +| `    │   ├── tags` | `Map` | see `defaults` | +| `    │   └── propagate-flow-id` | `boolean` | see `defaults` | +| ` └── soap` | | | +| `       ├── enabled` | `boolean` | see `defaults` | +| `       └── protocol` | `String` | see `defaults` | **Beware** that starting with Spring Boot 1.5.x the property resolution for environment variables changes and properties like `REST_CLIENTS_EXAMPLE_BASEURL` no longer work. As an alternative applications can use the @@ -467,10 +512,10 @@ Besides `Http`, you can also alternatively inject any of the following types per - `ClientHttpMessageConverters` - `AsyncListenableTaskExecutor` -### Trusted Keystore +### Certificate Pinning A client can be configured to only connect to trusted hosts (see -[Certificate Pinning](https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning)) by configuring the `keystore` key. Use +[Certificate Pinning](https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning)) by configuring the `certificate-pinning` key. Use `keystore.path` to refer to a *JKS* keystore on the classpath/filesystem and (optionally) specify the passphrase via `keystore.password`. You can generate a keystore using the [JDK's keytool](http://docs.oracle.com/javase/7/docs/technotes/tools/#security): @@ -577,9 +622,9 @@ final class RiptideTest { private MockRestServiceServer server; @Test - public void shouldAutowireMockedHttp() throws Exception { + public void shouldAutowireMockedHttp() { server.expect(requestTo("https://example.com/bar")).andRespond(withSuccess()); - client.remoteCall() + client.remoteCall() ; server.verify(); } } diff --git a/riptide-spring-boot-autoconfigure/pom.xml b/riptide-spring-boot-autoconfigure/pom.xml index ec9159643..7af2e230f 100644 --- a/riptide-spring-boot-autoconfigure/pom.xml +++ b/riptide-spring-boot-autoconfigure/pom.xml @@ -17,7 +17,7 @@ 1.13.0 - 0.17.0 + 2.0.0-SNAPSHOT @@ -94,6 +94,19 @@ true + + org.zalando + riptide-opentracing + + true + + + io.opentracing.contrib + opentracing-concurrent + 0.2.0 + + true + org.zalando riptide-soap @@ -219,6 +232,12 @@ 2.3.1 test + + io.opentracing + opentracing-mock + 0.32.0-RC2 + test + diff --git a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java index 81e40988a..9beec8896 100644 --- a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/DefaultRiptideRegistrar.java @@ -5,6 +5,8 @@ import com.google.common.collect.ImmutableMap; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics; +import io.opentracing.contrib.concurrent.TracedExecutorService; +import io.opentracing.contrib.concurrent.TracedScheduledExecutorService; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.jodah.failsafe.CircuitBreaker; @@ -45,11 +47,12 @@ import org.zalando.riptide.httpclient.metrics.HttpConnectionPoolMetrics; import org.zalando.riptide.idempotency.IdempotencyPredicate; import org.zalando.riptide.metrics.MetricsPlugin; +import org.zalando.riptide.opentracing.OpenTracingPlugin; +import org.zalando.riptide.opentracing.span.SpanDecorator; import org.zalando.riptide.soap.SOAPFaultHttpMessageConverter; import org.zalando.riptide.soap.SOAPHttpMessageConverter; import org.zalando.riptide.stream.Streams; import org.zalando.riptide.timeout.TimeoutPlugin; -import org.zalando.tracer.concurrent.TracingExecutors; import javax.annotation.Nullable; import javax.xml.soap.SOAPConstants; @@ -101,7 +104,7 @@ private String registerAsyncClientHttpRequestFactory(final String id, final Clie }); } - private BeanMetadataElement registerExecutor(final String id, final Client client) { + private String registerExecutor(final String id, final Client client) { final String name = "http-" + id; final String executorId = registry.registerIfAbsent(id, ExecutorService.class, () -> { @@ -127,7 +130,14 @@ private BeanMetadataElement registerExecutor(final String id, final Client clien .addConstructorArgValue(ImmutableList.of(clientId(id)))); } - return trace(executorId); + if (client.getTracing().getEnabled()) { + return registry.registerIfAbsent(id, TracedExecutorService.class, () -> + genericBeanDefinition(TracedExecutorService.class) + .addConstructorArgReference(executorId) + .addConstructorArgReference("tracer")); + } + + return executorId; } private static final class HttpMessageConverters { @@ -198,7 +208,7 @@ private void registerHttp(final String id, final Client client, final String fac return genericBeanDefinition(HttpFactory.class) .setFactoryMethod("create") - .addConstructorArgValue(registerExecutor(id, client)) + .addConstructorArgReference(registerExecutor(id, client)) .addConstructorArgReference(factoryId) .addConstructorArgValue(client.getBaseUrl()) .addConstructorArgValue(client.getUrlResolution()) @@ -232,7 +242,7 @@ private void registerAsyncRestTemplate(final String id, final String factoryId, genericBeanDefinition(ConcurrentClientHttpRequestFactory.class) .addConstructorArgReference(factoryId) .addConstructorArgValue(genericBeanDefinition(ConcurrentTaskExecutor.class) - .addConstructorArgValue(registerExecutor(id, client)) + .addConstructorArgReference(registerExecutor(id, client)) .getBeanDefinition()))); template.addConstructorArgReference(factoryId); configureTemplate(template, client.getBaseUrl(), converters, plugins); @@ -266,9 +276,10 @@ private List registerPlugins(final String id, final Client client) { final Stream> plugins = Stream.of( registerMetricsPlugin(id, client), registerTransientFaultPlugin(id, client), + registerOpenTracingPlugin(id, client), registerFailsafePlugin(id, client), - registerBackupPlugin(id, client), registerAuthorizationPlugin(id, client), + registerBackupPlugin(id, client), registerTimeoutPlugin(id, client), registerOriginalStackTracePlugin(id, client), registerCustomPlugin(id)); @@ -281,12 +292,13 @@ private List registerPlugins(final String id, final Client client) { private Optional registerMetricsPlugin(final String id, final Client client) { if (client.getMetrics().getEnabled()) { - log.debug("Client [{}]: Registering [{}]", id, MetricsPlugin.class.getSimpleName()); - final String pluginId = registry.registerIfAbsent(id, MetricsPlugin.class, () -> - genericBeanDefinition(MetricsPluginFactory.class) - .setFactoryMethod("createMetricsPlugin") - .addConstructorArgReference("meterRegistry") - .addConstructorArgValue(ImmutableList.of(clientId(id)))); + final String pluginId = registry.registerIfAbsent(id, MetricsPlugin.class, () -> { + log.debug("Client [{}]: Registering [{}]", id, MetricsPlugin.class.getSimpleName()); + return genericBeanDefinition(MetricsPluginFactory.class) + .setFactoryMethod("createMetricsPlugin") + .addConstructorArgReference("meterRegistry") + .addConstructorArgValue(ImmutableList.of(clientId(id))); + }); return Optional.of(pluginId); } @@ -295,25 +307,55 @@ private Optional registerMetricsPlugin(final String id, final Client cli private Optional registerTransientFaultPlugin(final String id, final Client client) { if (client.getTransientFaultDetection().getEnabled()) { - log.debug("Client [{}]: Registering [{}]", id, TransientFaultPlugin.class.getSimpleName()); - final String pluginId = registry.registerIfAbsent(id, TransientFaultPlugin.class, () -> - genericBeanDefinition(TransientFaultPlugin.class) - .addConstructorArgReference(findFaultClassifier(id))); + final String pluginId = registry.registerIfAbsent(id, TransientFaultPlugin.class, () -> { + log.debug("Client [{}]: Registering [{}]", id, TransientFaultPlugin.class.getSimpleName()); + return genericBeanDefinition(TransientFaultPlugin.class) + .addConstructorArgReference(findFaultClassifier(id)); + }); return Optional.of(pluginId); } return Optional.empty(); } + private Optional registerOpenTracingPlugin(final String id, final Client client) { + if (client.getTracing().getEnabled()) { + registry.registerIfAbsent(id, OpenTracingPlugin.class, () -> { + log.debug("Client [{}]: Registering [{}]", id, OpenTracingPlugin.class.getSimpleName()); + + final String decorator = generateBeanName(id, SpanDecorator.class); + return genericBeanDefinition(OpenTracingPluginFactory.class) + .setFactoryMethod("create") + .addConstructorArgReference("tracer") + .addConstructorArgValue(client) + .addConstructorArgValue(registry.isRegistered(decorator) ? ref(decorator) : null); + }); + } + return Optional.empty(); + } + private Optional registerFailsafePlugin(final String id, final Client client) { if (client.getRetry().getEnabled() || client.getCircuitBreaker().getEnabled()) { - log.debug("Client [{}]: Registering [{}]", id, FailsafePlugin.class.getSimpleName()); - final String pluginId = registry.registerIfAbsent(id, FailsafePlugin.class, () -> - genericBeanDefinition(FailsafePluginFactory.class) - .setFactoryMethod("createFailsafePlugin") - .addConstructorArgValue(registerScheduler(id, client)) - .addConstructorArgValue(registerRetryPolicy(id, client)) - .addConstructorArgValue(registerCircuitBreaker(id, client)) - .addConstructorArgReference(registerRetryListener(id, client))); + final String pluginId = registry.registerIfAbsent(id, FailsafePlugin.class, () -> { + log.debug("Client [{}]: Registering [{}]", id, FailsafePlugin.class.getSimpleName()); + return genericBeanDefinition(FailsafePluginFactory.class) + .setFactoryMethod("create") + .addConstructorArgReference(registerScheduler(id, client)) + .addConstructorArgValue(registerRetryPolicy(id, client)) + .addConstructorArgValue(registerCircuitBreaker(id, client)) + .addConstructorArgReference(registerRetryListener(id, client)); + }); + return Optional.of(pluginId); + } + return Optional.empty(); + } + + private Optional registerAuthorizationPlugin(final String id, final Client client) { + if (client.getOauth().getEnabled()) { + final String pluginId = registry.registerIfAbsent(id, AuthorizationPlugin.class, () -> { + log.debug("Client [{}]: Registering [{}]", id, AuthorizationPlugin.class.getSimpleName()); + return genericBeanDefinition(AuthorizationPlugin.class) + .addConstructorArgReference(registerAuthorizationProvider(id, client.getOauth())); + }); return Optional.of(pluginId); } return Optional.empty(); @@ -324,22 +366,11 @@ private Optional registerBackupPlugin(final String id, final Client clie log.debug("Client [{}]: Registering [{}]", id, BackupRequestPlugin.class.getSimpleName()); final String pluginId = registry.registerIfAbsent(id, BackupRequestPlugin.class, () -> genericBeanDefinition(BackupRequestPlugin.class) - .addConstructorArgValue(registerScheduler(id, client)) + .addConstructorArgReference(registerScheduler(id, client)) .addConstructorArgValue(client.getBackupRequest().getDelay().getAmount()) .addConstructorArgValue(client.getBackupRequest().getDelay().getUnit()) .addConstructorArgValue(new IdempotencyPredicate()) - .addConstructorArgValue(registerExecutor(id, client))); - return Optional.of(pluginId); - } - return Optional.empty(); - } - - private Optional registerAuthorizationPlugin(final String id, final Client client) { - if (client.getOauth().getEnabled()) { - log.debug("Client [{}]: Registering [{}]", id, AuthorizationPlugin.class.getSimpleName()); - final String pluginId = registry.registerIfAbsent(id, AuthorizationPlugin.class, () -> - genericBeanDefinition(AuthorizationPlugin.class) - .addConstructorArgReference(registerAuthorizationProvider(id, client.getOauth()))); + .addConstructorArgReference(registerExecutor(id, client))); return Optional.of(pluginId); } return Optional.empty(); @@ -347,14 +378,15 @@ private Optional registerAuthorizationPlugin(final String id, final Clie private Optional registerTimeoutPlugin(final String id, final Client client) { if (client.getTimeouts().getEnabled()) { - log.debug("Client [{}]: Registering [{}]", id, TimeoutPlugin.class.getSimpleName()); - final TimeSpan timeout = client.getTimeouts().getGlobal(); - final String pluginId = registry.registerIfAbsent(id, TimeoutPlugin.class, () -> - genericBeanDefinition(TimeoutPlugin.class) - .addConstructorArgValue(registerScheduler(id, client)) - .addConstructorArgValue(timeout.getAmount()) - .addConstructorArgValue(timeout.getUnit()) - .addConstructorArgValue(registerExecutor(id, client))); + final String pluginId = registry.registerIfAbsent(id, TimeoutPlugin.class, () -> { + log.debug("Client [{}]: Registering [{}]", id, TimeoutPlugin.class.getSimpleName()); + final TimeSpan timeout = client.getTimeouts().getGlobal(); + return genericBeanDefinition(TimeoutPlugin.class) + .addConstructorArgReference(registerScheduler(id, client)) + .addConstructorArgValue(timeout.getAmount()) + .addConstructorArgValue(timeout.getUnit()) + .addConstructorArgReference(registerExecutor(id, client)); + }); return Optional.of(pluginId); } return Optional.empty(); @@ -362,8 +394,10 @@ private Optional registerTimeoutPlugin(final String id, final Client cli private Optional registerOriginalStackTracePlugin(final String id, final Client client) { if (client.getStackTracePreservation().getEnabled()) { - log.debug("Client [{}]: Registering [{}]", id, OriginalStackTracePlugin.class.getSimpleName()); - final String pluginId = registry.registerIfAbsent(id, OriginalStackTracePlugin.class); + final String pluginId = registry.registerIfAbsent(id, OriginalStackTracePlugin.class, () -> { + log.debug("Client [{}]: Registering [{}]", id, OriginalStackTracePlugin.class.getSimpleName()); + return genericBeanDefinition(OriginalStackTracePlugin.class); + }); return Optional.of(pluginId); } return Optional.empty(); @@ -387,7 +421,7 @@ private String findFaultClassifier(final String id) { } } - private BeanMetadataElement registerScheduler(final String id, final Client client) { + private String registerScheduler(final String id, final Client client) { // we allow users to use their own ScheduledExecutorService, but they don't have to configure tracing final String name = "http-" + id + "-scheduler"; @@ -412,7 +446,14 @@ private BeanMetadataElement registerScheduler(final String id, final Client clie .addConstructorArgValue(ImmutableList.of(clientId(id)))); } - return trace(executorId); + if (client.getTracing().getEnabled()) { + return registry.registerIfAbsent(id, TracedScheduledExecutorService.class, () -> + genericBeanDefinition(TracedScheduledExecutorService.class) + .addConstructorArgReference(executorId) + .addConstructorArgReference("tracer")); + } + + return executorId; } private BeanMetadataElement registerRetryPolicy(final String id, final Client client) { @@ -420,7 +461,7 @@ private BeanMetadataElement registerRetryPolicy(final String id, final Client cl return ref(registry.registerIfAbsent(id, RetryPolicy.class, () -> genericBeanDefinition(FailsafePluginFactory.class) .setFactoryMethod("createRetryPolicy") - .addConstructorArgValue(client.getRetry()))); + .addConstructorArgValue(client))); } return null; @@ -486,23 +527,6 @@ private String registerAuthorizationProvider(final String id, final OAuth oauth) .addConstructorArgValue(id)); } - private BeanMetadataElement trace(final String executor) { - final Optional result = ifPresent("org.zalando.tracer.concurrent.TracingExecutors", - () -> { - if (registry.isRegistered("tracer")) { - return genericBeanDefinition(TracingExecutors.class) - .setFactoryMethod("preserve") - .addConstructorArgReference(executor) - .addConstructorArgReference("tracer") - .getBeanDefinition(); - } else { - return null; - } - }); - - return result.orElseGet(() -> ref(executor)); - } - private String registerHttpClient(final String id, final Client client) { return registry.registerIfAbsent(id, HttpClient.class, () -> { log.debug("Client [{}]: Registering HttpClient", id); @@ -538,9 +562,10 @@ private String registerHttpClient(final String id, final Client client) { private List configureFirstRequestInterceptors(final String id, final Client client) { final List interceptors = list(); - if (registry.isRegistered("tracerHttpRequestInterceptor")) { - log.debug("Client [{}]: Registering TracerHttpRequestInterceptor", id); - interceptors.add(ref("tracerHttpRequestInterceptor")); + // TODO theoretically tracing could still be disabled... + if (client.getTracing().getPropagateFlowId()) { + log.debug("Client [{}]: Registering FlowHttpRequestInterceptor", id); + interceptors.add(ref("flowHttpRequestInterceptor")); } return interceptors; diff --git a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Defaulting.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Defaulting.java index d81dba761..015d21d56 100644 --- a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Defaulting.java +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Defaulting.java @@ -14,10 +14,13 @@ import org.zalando.riptide.autoconfigure.RiptideProperties.Soap; import org.zalando.riptide.autoconfigure.RiptideProperties.StackTracePreservation; import org.zalando.riptide.autoconfigure.RiptideProperties.Timeouts; +import org.zalando.riptide.autoconfigure.RiptideProperties.Tracing; import org.zalando.riptide.autoconfigure.RiptideProperties.TransientFaultDetection; import javax.annotation.Nullable; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.BinaryOperator; @@ -63,6 +66,7 @@ private static Defaults merge(final Defaults defaults) { defaults.getRequestCompression(), defaults.getCertificatePinning(), defaults.getCaching(), + defaults.getTracing(), defaults.getSoap() ); } @@ -96,6 +100,7 @@ private static Client merge(final Client base, final Defaults defaults) { merge(base.getRequestCompression(), defaults.getRequestCompression(), Defaulting::merge), merge(base.getCertificatePinning(), defaults.getCertificatePinning(), Defaulting::merge), merge(base.getCaching(), defaults.getCaching(), Defaulting::merge), + merge(base.getTracing(), defaults.getTracing(), Defaulting::merge), merge(base.getSoap(), defaults.getSoap(), Defaulting::merge) ); } @@ -233,6 +238,24 @@ private static Heuristic merge(final Heuristic base, final Heuristic defaults) { ); } + private static Tracing merge(final Tracing base, final Tracing defaults) { + final boolean enabled = either(base.getEnabled(), defaults.getEnabled()); + final boolean propagateFlowId = either(base.getPropagateFlowId(), defaults.getPropagateFlowId()); + + return new Tracing( + enabled, + merge(base.getTags(), defaults.getTags(), Defaulting::merge), + enabled && propagateFlowId + ); + } + + private static Map merge(final Map base, final Map defaults) { + final Map map = new HashMap<>(); + map.putAll(defaults); + map.putAll(base); + return map; + } + private static Soap merge(final Soap base, final Soap defaults) { return new Soap( either(base.getEnabled(), defaults.getEnabled()), diff --git a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/FailsafePluginFactory.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/FailsafePluginFactory.java index fefcf4baa..1264242f5 100644 --- a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/FailsafePluginFactory.java +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/FailsafePluginFactory.java @@ -6,6 +6,8 @@ import net.jodah.failsafe.RetryPolicy; import org.springframework.http.client.ClientHttpResponse; import org.zalando.riptide.Plugin; +import org.zalando.riptide.autoconfigure.RiptideProperties.Client; +import org.zalando.riptide.autoconfigure.RiptideProperties.Retry; import org.zalando.riptide.autoconfigure.RiptideProperties.Retry.Backoff; import org.zalando.riptide.failsafe.CircuitBreakerListener; import org.zalando.riptide.failsafe.FailsafePlugin; @@ -30,7 +32,7 @@ private FailsafePluginFactory() { } - public static Plugin createFailsafePlugin( + public static Plugin create( final ScheduledExecutorService scheduler, @Nullable final RetryPolicy retryPolicy, @Nullable final CircuitBreaker circuitBreaker, @@ -50,9 +52,11 @@ public static Plugin createFailsafePlugin( .withListener(listener); } - public static RetryPolicy createRetryPolicy(final RiptideProperties.Retry config) { + public static RetryPolicy createRetryPolicy(final Client client) { final RetryPolicy policy = new RetryPolicy<>(); + final Retry config = client.getRetry(); + Optional.ofNullable(config.getFixedDelay()) .ifPresent(delay -> delay.applyTo(policy::withDelay)); @@ -84,14 +88,17 @@ public static RetryPolicy createRetryPolicy(final RiptidePro Optional.ofNullable(config.getJitter()) .ifPresent(jitter -> jitter.applyTo(policy::withJitter)); - policy.handle(TransientFaultException.class); + if (client.getTransientFaultDetection().getEnabled()) { + policy.handle(TransientFaultException.class); + } + policy.handle(RetryException.class); policy.withDelay(new RetryAfterDelayFunction(systemUTC())); return policy; } - public static CircuitBreaker createCircuitBreaker(final RiptideProperties.Client client, + public static CircuitBreaker createCircuitBreaker(final Client client, final CircuitBreakerListener listener) { final CircuitBreaker breaker = new CircuitBreaker<>(); diff --git a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/OpenTracingPluginFactory.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/OpenTracingPluginFactory.java new file mode 100644 index 000000000..77d5605a1 --- /dev/null +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/OpenTracingPluginFactory.java @@ -0,0 +1,43 @@ +package org.zalando.riptide.autoconfigure; + +import io.opentracing.Tracer; +import org.zalando.riptide.Plugin; +import org.zalando.riptide.autoconfigure.RiptideProperties.Client; +import org.zalando.riptide.opentracing.OpenTracingPlugin; +import org.zalando.riptide.opentracing.span.RetrySpanDecorator; +import org.zalando.riptide.opentracing.span.SpanDecorator; +import org.zalando.riptide.opentracing.span.StaticSpanDecorator; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + +import static org.zalando.riptide.opentracing.span.SpanDecorator.composite; + +@SuppressWarnings("unused") +final class OpenTracingPluginFactory { + + private OpenTracingPluginFactory() { + + } + + public static Plugin create(final Tracer tracer, final Client client, @Nullable final SpanDecorator decorator) { + final List decorators = new ArrayList<>(); + decorators.add(new StaticSpanDecorator(client.getTracing().getTags())); + + if (client.getRetry().getEnabled()) { + decorators.add(new RetrySpanDecorator()); + } + + return create(tracer, decorator) + .withAdditionalSpanDecorators(composite(decorators)); + } + + private static OpenTracingPlugin create(final Tracer tracer, + @Nullable final SpanDecorator decorator) { + return decorator == null ? + new OpenTracingPlugin(tracer) : + new OpenTracingPlugin(tracer).withSpanDecorators(decorator); + } + +} diff --git a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Registry.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Registry.java index 38c0a9abb..95e8a0fe8 100644 --- a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Registry.java +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/Registry.java @@ -18,7 +18,6 @@ import static com.google.common.base.CaseFormat.LOWER_CAMEL; import static com.google.common.base.CaseFormat.LOWER_HYPHEN; import static com.google.common.base.CaseFormat.UPPER_CAMEL; -import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition; final class Registry { @@ -51,10 +50,6 @@ public String registerIfAbsent(final Class type, final Supplier String registerIfAbsent(final String id, final Class type) { - return registerIfAbsent(id, type, () -> genericBeanDefinition(type)); - } - public String registerIfAbsent(final String id, final Class type, final Supplier factory) { return registerIfAbsent(id, generateBeanName(id, type), factory); diff --git a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/RiptideProperties.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/RiptideProperties.java index e0ddee2ad..96f57961f 100644 --- a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/RiptideProperties.java +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/RiptideProperties.java @@ -18,6 +18,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import static java.util.Collections.emptyMap; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.apiguardian.api.API.Status.INTERNAL; @@ -97,6 +98,9 @@ public static final class Defaults { ) ); + @NestedConfigurationProperty + private Tracing tracing = new Tracing(false, emptyMap(), false); + @NestedConfigurationProperty private Soap soap = new Soap(false, "1.1"); @@ -150,6 +154,9 @@ public static final class Client { @NestedConfigurationProperty private Caching caching; + @NestedConfigurationProperty + private Tracing tracing; + @NestedConfigurationProperty private Soap soap; @@ -318,6 +325,17 @@ public static final class Heuristic { } } + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static final class Tracing { + private Boolean enabled; + private Map tags; + private Boolean propagateFlowId; + } + + @Getter @Setter @NoArgsConstructor diff --git a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/DefaultTestConfiguration.java b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/DefaultTestConfiguration.java index 31d05bf00..e959e9e70 100644 --- a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/DefaultTestConfiguration.java +++ b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/DefaultTestConfiguration.java @@ -13,6 +13,7 @@ JacksonAutoConfiguration.class, LogbookAutoConfiguration.class, TracerAutoConfiguration.class, + OpenTracingTestAutoConfiguration.class, MetricsTestAutoConfiguration.class, }) @ActiveProfiles("default") diff --git a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java index b8b97c0d9..4de85a3e0 100644 --- a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java +++ b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/ManualConfiguration.java @@ -4,6 +4,9 @@ import com.google.common.collect.ImmutableList; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; +import io.opentracing.Tracer; +import io.opentracing.contrib.concurrent.TracedExecutorService; +import io.opentracing.contrib.concurrent.TracedScheduledExecutorService; import net.jodah.failsafe.CircuitBreaker; import net.jodah.failsafe.RetryPolicy; import org.apache.http.client.config.RequestConfig; @@ -47,11 +50,11 @@ import org.zalando.riptide.httpclient.ApacheClientHttpRequestFactory; import org.zalando.riptide.httpclient.GzipHttpRequestInterceptor; import org.zalando.riptide.metrics.MetricsPlugin; +import org.zalando.riptide.opentracing.OpenTracingPlugin; import org.zalando.riptide.stream.Streams; import org.zalando.riptide.timeout.TimeoutPlugin; -import org.zalando.tracer.Tracer; -import org.zalando.tracer.concurrent.TracingExecutors; -import org.zalando.tracer.httpclient.TracerHttpRequestInterceptor; +import org.zalando.tracer.Flow; +import org.zalando.tracer.httpclient.FlowHttpRequestInterceptor; import org.zalando.tracer.spring.TracerAutoConfiguration; import java.time.Duration; @@ -100,7 +103,7 @@ public Http exampleHttp(final Executor executor, final ClientHttpRequestFactory } @Bean - public List examplePlugins(final MeterRegistry meterRegistry, + public List examplePlugins(final MeterRegistry meterRegistry, final Tracer tracer, final ScheduledExecutorService scheduler) { final CircuitBreakerListener listener = new MetricsCircuitBreakerListener(meterRegistry) @@ -110,6 +113,7 @@ public List examplePlugins(final MeterRegistry meterRegistry, new MetricsPlugin(meterRegistry) .withDefaultTags(Tag.of("clientId", "example")), new TransientFaultPlugin(), + new OpenTracingPlugin(tracer), new FailsafePlugin( ImmutableList.of( new RetryPolicy() @@ -169,7 +173,7 @@ public AsyncRestTemplate exampleAsyncRestTemplate(final ClientHttpRequestFactory @Bean public ApacheClientHttpRequestFactory exampleAsyncClientHttpRequestFactory( - final Tracer tracer, final Logbook logbook) throws Exception { + final Flow flow, final Logbook logbook) throws Exception { return new ApacheClientHttpRequestFactory( HttpClientBuilder.create() .setDefaultRequestConfig(RequestConfig.custom() @@ -179,7 +183,7 @@ public ApacheClientHttpRequestFactory exampleAsyncClientHttpRequestFactory( .setConnectionTimeToLive(30, SECONDS) .setMaxConnPerRoute(2) .setMaxConnTotal(20) - .addInterceptorFirst(new TracerHttpRequestInterceptor(tracer)) + .addInterceptorFirst(new FlowHttpRequestInterceptor(flow)) .addInterceptorLast(new LogbookHttpRequestInterceptor(logbook)) .addInterceptorLast(new GzipHttpRequestInterceptor()) .addInterceptorLast(new LogbookHttpResponseInterceptor()) @@ -195,7 +199,7 @@ public ApacheClientHttpRequestFactory exampleAsyncClientHttpRequestFactory( @Bean(destroyMethod = "shutdown") public ExecutorService executor(final Tracer tracer) { - return TracingExecutors.preserve( + return new TracedExecutorService( new ThreadPoolExecutor( 1, 20, 1, MINUTES, new ArrayBlockingQueue<>(0), @@ -206,7 +210,7 @@ public ExecutorService executor(final Tracer tracer) { @Bean(destroyMethod = "shutdown") public ScheduledExecutorService scheduler(final Tracer tracer) { - return TracingExecutors.preserve( + return new TracedScheduledExecutorService( Executors.newScheduledThreadPool( 20, // TODO max-connections-total? new CustomizableThreadFactory("http-example-scheduler-")), diff --git a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/OpenTracingTestAutoConfiguration.java b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/OpenTracingTestAutoConfiguration.java new file mode 100644 index 000000000..5707faa0d --- /dev/null +++ b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/OpenTracingTestAutoConfiguration.java @@ -0,0 +1,19 @@ +package org.zalando.riptide.autoconfigure; + +import io.opentracing.Tracer; +import io.opentracing.mock.MockTracer; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.zalando.tracer.spring.TracerAutoConfiguration; + +@Configuration +@AutoConfigureBefore(TracerAutoConfiguration.class) +public class OpenTracingTestAutoConfiguration { + + @Bean + public Tracer tracer() { + return new MockTracer(); + } + +} diff --git a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/metrics/MetricsTest.java b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/metrics/MetricsTest.java index 7b587f37f..bcb2eea5f 100644 --- a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/metrics/MetricsTest.java +++ b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/metrics/MetricsTest.java @@ -14,6 +14,7 @@ import org.zalando.logbook.spring.LogbookAutoConfiguration; import org.zalando.riptide.Http; import org.zalando.riptide.autoconfigure.MetricsTestAutoConfiguration; +import org.zalando.riptide.autoconfigure.OpenTracingTestAutoConfiguration; import org.zalando.riptide.autoconfigure.RiptideClientTest; import org.zalando.riptide.faults.TransientFaultException; import org.zalando.tracer.spring.TracerAutoConfiguration; @@ -43,6 +44,7 @@ final class MetricsTest { JacksonAutoConfiguration.class, LogbookAutoConfiguration.class, TracerAutoConfiguration.class, + OpenTracingTestAutoConfiguration.class, MetricsTestAutoConfiguration.class, }) static class ContextConfiguration { diff --git a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/url/UrlResolutionTest.java b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/url/UrlResolutionTest.java index c992f19ab..4f850912d 100644 --- a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/url/UrlResolutionTest.java +++ b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/url/UrlResolutionTest.java @@ -5,13 +5,17 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.client.MockRestServiceServer; import org.zalando.logbook.spring.LogbookAutoConfiguration; import org.zalando.riptide.Http; import org.zalando.riptide.autoconfigure.MetricsTestAutoConfiguration; +import org.zalando.riptide.autoconfigure.OpenTracingTestAutoConfiguration; import org.zalando.riptide.autoconfigure.RiptideClientTest; +import org.zalando.riptide.opentracing.span.HttpUrlSpanDecorator; +import org.zalando.riptide.opentracing.span.SpanDecorator; import org.zalando.tracer.spring.TracerAutoConfiguration; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; @@ -27,11 +31,17 @@ final class UrlResolutionTest { JacksonAutoConfiguration.class, LogbookAutoConfiguration.class, TracerAutoConfiguration.class, + OpenTracingTestAutoConfiguration.class, MetricsTestAutoConfiguration.class, }) @ActiveProfiles("default") static class ContextConfiguration { + @Bean + public SpanDecorator exampleSpanDecorator() { + return new HttpUrlSpanDecorator(); + } + } @Autowired diff --git a/riptide-spring-boot-autoconfigure/src/test/resources/application-default.yml b/riptide-spring-boot-autoconfigure/src/test/resources/application-default.yml index 64b0b24ad..ca2b2859e 100644 --- a/riptide-spring-boot-autoconfigure/src/test/resources/application-default.yml +++ b/riptide-spring-boot-autoconfigure/src/test/resources/application-default.yml @@ -3,6 +3,10 @@ riptide: transient-fault-detection.enabled: false stack-trace-preservation.enabled: false metrics.enabled: true + tracing: + tags: + environment: test + propagate-flow-id: false clients: example: base-url: https://example.com/foo @@ -17,12 +21,19 @@ riptide: queue-size: 10 stack-trace-preservation.enabled: true metrics.enabled: true + tracing: + enabled: true + tags: + peer.service: example + propagate-flow-id: true ecb: base-url: http://www.ecb.europa.eu request-compression.enabled: true timeouts: enabled: true global: 1 seconds + tracing: + propagate-flow-id: true soap: enabled: true protocol: 1.2 @@ -45,8 +56,11 @@ riptide: retry: enabled: true max-retries: 3 + tracing: + enabled: true bar: base-url: http://bar + transient-fault-detection.enabled: true retry: enabled: true max-retries: 4