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 a6ca972c4..dc1a19537 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ riptide-httpclient riptide-idempotency riptide-metrics + riptide-opentracing riptide-problem riptide-soap riptide-spring-boot-autoconfigure @@ -133,6 +134,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 32a2ab2f1..36d924ea8 100644 --- a/riptide-bom/pom.xml +++ b/riptide-bom/pom.xml @@ -68,6 +68,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 fae12ffb7..e4db73296 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,6 +10,7 @@ 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; @@ -27,6 +28,8 @@ @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; @@ -56,18 +59,17 @@ public RequestExecution aroundDispatch(final RequestExecution execution) { return Failsafe.with(select(arguments)) .with(scheduler) - .getStageAsync(() -> execution.execute(arguments)); + .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; } @@ -84,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-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..e5764b56e --- /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 + + + + + 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 bbce69004..00cb356e3 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. @@ -335,7 +372,7 @@ For a complete overview of available properties, they type and default value ple | `│   │   │   ├── max-delay` | `TimeSpan` | none, requires `backoff.delay` | | `│   │   │   └── delay-factor` | `double` | `2.0` | | `│   │   ├── max-retries` | `int` | none | -| `│   │   ├── max-duration` | `TimeSpan` | `5 seconds` | +| `│   │   ├── max-duration` | `TimeSpan` | none | | `│   │   ├── jitter-factor` | `double` | none, mutually exclusive to `jitter` | | `│   │   └── jitter` | `TimeSpan` | none, mutually exclusive to `jitter-factor` | | `│   ├── circuit-breaker` | | | @@ -366,18 +403,6 @@ For a complete overview of available properties, they type and default value ple | `│   │    ├── enabled` | `boolean` | `false` | | `│   │       ├── coefficient` | `double` | `0.1` | | `│   │       └── default-life-time` | `TimeSpan` | `0 seconds`, disabled | -| `│   ├── chaos` | | | -| `│   │   ├── latency` | | | -| `│   │ │   ├── enabled` | `boolean` | `false` | -| `│   │ │   ├── probability` | `double` | `0.01` | -| `│   │ │   └── delay` | `TimeSpan` | `1 second` | -| `│   │   ├── exceptions` | | | -| `│   │ │   ├── enabled` | `boolean` | `false` | -| `│   │ │   └── probability` | `double` | `0.01` | -| `│   │   └── error-responses` | | | -| `│   │    ├── enabled` | `boolean` | `false` | -| `│   │    ├── probability` | `double` | `0.01` | -| `│   │    └── status-codes` | `int[]` | `[500, 503]` | | `│ └── soap` | | | | `│       ├── enabled` | `boolean` | `false` | | `│       └── protocol` | `String` | `1.1` (possible other value: `1.2`) | @@ -390,13 +415,12 @@ For a complete overview of available properties, they type and default value ple | `        │  ├── socket-timeout` | `TimeSpan` | see `defaults` | | `        │  ├── time-to-live` | `TimeSpan` | see `defaults` | | `        │  ├── max-per-route` | `int` | see `defaults` | -| `        │  ├── max-total` | `int` | see `defaults` | -| `        │  └── mode` | `String` | see `defaults` | -| `        ├── threads` | | | -| `        │   ├── min-size` | `int` | see `defaults` | -| `        │   ├── max-size` | `int` | see `defaults` | -| `        │   ├── keep-alive` | `TimeSpan` | see `defaults` | -| `        │   └── queue-size` | `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` | @@ -446,6 +470,10 @@ For a complete overview of available properties, they type and default value ple | `    │    ├── 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` | | `    ├── chaos` | | | | `    │   ├── latency` | | | | `    │ │   ├── enabled` | `boolean` | see `defaults` | @@ -489,10 +517,10 @@ Besides `Http`, you can also alternatively inject any of the following types per - `HttpClient` - `ClientHttpMessageConverters` -### 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): @@ -597,9 +625,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 f96c70d04..80e00d8b1 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-RC.1 @@ -100,6 +100,19 @@ true + + org.zalando + riptide-opentracing + + true + + + io.opentracing.contrib + opentracing-concurrent + 0.2.0 + + true + org.zalando riptide-soap @@ -119,7 +132,7 @@ org.zalando - tracer-spring-boot-starter + tracer-spring-boot-autoconfigure ${tracer.version} true @@ -225,6 +238,12 @@ 2.3.1 test + + io.opentracing + opentracing-mock + 0.32.0 + 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 1bc28a79c..de0996a80 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; @@ -46,11 +48,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.xml.soap.SOAPConstants; import java.net.SocketTimeoutException; @@ -100,7 +103,7 @@ private void registerHttp(final String id, final Client client) { return genericBeanDefinition(HttpFactory.class) .setFactoryMethod("create") - .addConstructorArgValue(registerExecutor(id, client)) + .addConstructorArgReference(registerExecutor(id, client)) .addConstructorArgReference(registerClientHttpRequestFactory(id, client)) .addConstructorArgValue(client.getBaseUrl()) .addConstructorArgValue(client.getUrlResolution()) @@ -118,7 +121,7 @@ private String registerClientHttpRequestFactory(final String id, final Client cl }); } - 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, () -> { @@ -144,7 +147,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 { @@ -219,9 +229,10 @@ private List registerPlugins(final String id, final Client client registerChaosPlugin(id, client), 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)); @@ -295,12 +306,13 @@ private Optional registerChaosPlugin(final String id, final Client clien 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); } @@ -309,25 +321,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(); @@ -338,22 +380,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(); @@ -361,14 +392,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(); @@ -376,8 +408,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(); @@ -401,7 +435,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"; @@ -426,7 +460,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) { @@ -434,7 +475,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; @@ -500,23 +541,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); @@ -552,9 +576,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 d35c0b3c5..f595287c5 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 @@ -18,10 +18,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; @@ -67,6 +70,7 @@ private static Defaults merge(final Defaults defaults) { defaults.getRequestCompression(), defaults.getCertificatePinning(), defaults.getCaching(), + defaults.getTracing(), defaults.getChaos(), defaults.getSoap() ); @@ -101,6 +105,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.getChaos(), defaults.getChaos(), Defaulting::merge), merge(base.getSoap(), defaults.getSoap(), Defaulting::merge) ); @@ -240,6 +245,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 Chaos merge(final Chaos base, final Chaos defaults) { return new Chaos( merge(base.getLatency(), defaults.getLatency(), Defaulting::merge), 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 a6cb2f34f..9ce352407 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.CompositeDelayFunction; @@ -33,7 +35,7 @@ private FailsafePluginFactory() { } - public static Plugin createFailsafePlugin( + public static Plugin create( final ScheduledExecutorService scheduler, @Nullable final RetryPolicy retryPolicy, @Nullable final CircuitBreaker circuitBreaker, @@ -53,9 +55,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)); @@ -87,7 +91,10 @@ 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 CompositeDelayFunction<>(Arrays.asList( @@ -98,7 +105,7 @@ public static RetryPolicy createRetryPolicy(final RiptidePro 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 b2186c204..26aec109d 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/RiptideAutoConfiguration.java b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/RiptideAutoConfiguration.java index 4009d5705..92ba59d1b 100644 --- a/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/RiptideAutoConfiguration.java +++ b/riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/RiptideAutoConfiguration.java @@ -28,14 +28,12 @@ @AutoConfigureAfter(name = { "org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration", "org.zalando.logbook.spring.LogbookAutoConfiguration", - "org.zalando.tracer.spring.TracerAutoConfiguration", - "org.zalando.tracer.spring.TracerSchedulingAutoConfiguration", // only needed for tracer < 0.12.0, - "io.micrometer.spring.autoconfigure.CompositeMeterRegistryAutoConfiguration", // Spring Boot 1.x - "org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration", // Spring Boot 2.x + "org.zalando.tracer.autoconfigure.TracerAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration", }) @AutoConfigureBefore(name = { "org.springframework.scheduling.annotation.SchedulingConfiguration", - "org.zalando.failsafeactuator.config.FailsafeInjectionConfiguration" + "org.zalando.actuate.autoconfigure.failsafe.CircuitBreakersEndpointAutoConfiguration" }) public class RiptideAutoConfiguration { 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 fada3fdab..90544d21b 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 @@ -24,6 +24,7 @@ import java.util.List; 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; @@ -105,6 +106,9 @@ public static final class Defaults { ) ); + @NestedConfigurationProperty + private Tracing tracing = new Tracing(false, emptyMap(), false); + @NestedConfigurationProperty private Chaos chaos = new Chaos( new Latency(false, 0.01, TimeSpan.of(1, SECONDS)), @@ -165,6 +169,9 @@ public static final class Client { @NestedConfigurationProperty private Caching caching; + @NestedConfigurationProperty + private Tracing tracing; + @NestedConfigurationProperty private Chaos chaos; @@ -337,6 +344,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/CachingTest.java b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/CachingTest.java index 3094678c6..6bc2a16c6 100644 --- a/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/CachingTest.java +++ b/riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/CachingTest.java @@ -17,7 +17,7 @@ import org.springframework.test.context.ActiveProfiles; import org.zalando.logbook.spring.LogbookAutoConfiguration; import org.zalando.riptide.Http; -import org.zalando.tracer.spring.TracerAutoConfiguration; +import org.zalando.tracer.autoconfigure.TracerAutoConfiguration; import static com.github.restdriver.clientdriver.RestClientDriver.giveResponse; import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo; 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..36c9fc56f 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 @@ -5,7 +5,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ActiveProfiles; import org.zalando.logbook.spring.LogbookAutoConfiguration; -import org.zalando.tracer.spring.TracerAutoConfiguration; +import org.zalando.tracer.autoconfigure.TracerAutoConfiguration; @Configuration @ImportAutoConfiguration({ @@ -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 5bdabe553..fb1058773 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,8 +4,15 @@ 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; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.ssl.SSLContexts; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -17,6 +24,9 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; +import org.zalando.logbook.Logbook; +import org.zalando.logbook.httpclient.LogbookHttpRequestInterceptor; +import org.zalando.logbook.httpclient.LogbookHttpResponseInterceptor; import org.zalando.logbook.spring.LogbookAutoConfiguration; import org.zalando.riptide.Http; import org.zalando.riptide.OriginalStackTracePlugin; @@ -39,15 +49,18 @@ import org.zalando.riptide.failsafe.metrics.MetricsRetryListener; import org.zalando.riptide.faults.TransientFaultException; import org.zalando.riptide.faults.TransientFaultPlugin; +import org.zalando.riptide.httpclient.ApacheClientHttpRequestFactory; +import org.zalando.riptide.httpclient.GzipHttpRequestInterceptor; import org.zalando.riptide.idempotency.IdempotencyPredicate; import org.zalando.riptide.metrics.MetricsPlugin; +import org.zalando.riptide.opentracing.OpenTracingPlugin; 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.Tracer; -import org.zalando.tracer.concurrent.TracingExecutors; -import org.zalando.tracer.spring.TracerAutoConfiguration; +import org.zalando.tracer.Flow; +import org.zalando.tracer.autoconfigure.TracerAutoConfiguration; +import org.zalando.tracer.httpclient.FlowHttpRequestInterceptor; import java.net.SocketTimeoutException; import java.time.Clock; @@ -67,6 +80,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; +import static javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier; import static org.zalando.riptide.chaos.FailureInjection.composite; @Configuration @@ -97,7 +111,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 Executor executor) { final CircuitBreakerListener listener = new MetricsCircuitBreakerListener(meterRegistry) @@ -120,6 +134,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() @@ -152,9 +167,35 @@ public List examplePlugins(final MeterRegistry meterRegistry, new CustomPlugin()); } + @Bean + public ApacheClientHttpRequestFactory exampleAsyncClientHttpRequestFactory( + final Flow flow, final Logbook logbook) throws Exception { + return new ApacheClientHttpRequestFactory( + HttpClientBuilder.create() + .setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(5000) + .setSocketTimeout(5000) + .build()) + .setConnectionTimeToLive(30, SECONDS) + .setMaxConnPerRoute(2) + .setMaxConnTotal(20) + .addInterceptorFirst(new FlowHttpRequestInterceptor(flow)) + .addInterceptorLast(new LogbookHttpRequestInterceptor(logbook)) + .addInterceptorLast(new GzipHttpRequestInterceptor()) + .addInterceptorLast(new LogbookHttpResponseInterceptor()) + .setSSLSocketFactory(new SSLConnectionSocketFactory( + SSLContexts.custom() + .loadTrustMaterial( + getClass().getClassLoader().getResource("example.keystore"), + "password".toCharArray()) + .build(), + getDefaultHostnameVerifier())) + .build()); + } + @Bean(destroyMethod = "shutdown") public ExecutorService executor(final Tracer tracer) { - return TracingExecutors.preserve( + return new TracedExecutorService( new ThreadPoolExecutor( 1, 20, 1, MINUTES, new ArrayBlockingQueue<>(0), @@ -165,7 +206,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..d79150dd1 --- /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.autoconfigure.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..4911d06be 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,9 +14,10 @@ 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; +import org.zalando.tracer.autoconfigure.TracerAutoConfiguration; import java.util.List; import java.util.stream.Collectors; @@ -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..83c7752ee 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,14 +5,18 @@ 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.tracer.spring.TracerAutoConfiguration; +import org.zalando.riptide.opentracing.span.HttpUrlSpanDecorator; +import org.zalando.riptide.opentracing.span.SpanDecorator; +import org.zalando.tracer.autoconfigure.TracerAutoConfiguration; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @@ -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 db1f4b293..2b980ce5b 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 chaos: latency: enabled: true @@ -57,8 +68,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