From fced0a54608ddb2fbc7bbddd3252e35f477fe057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Willi=20Sch=C3=B6nborn?= Date: Sat, 2 Mar 2019 20:50:11 +0100 Subject: [PATCH] Added OpenTracing plugin --- docs/spider-web.jpg | Bin 0 -> 50551 bytes pom.xml | 6 + riptide-bom/pom.xml | 4 + .../failsafe/CompositeRetryListener.java | 1 + .../riptide/failsafe/FailsafePlugin.java | 20 +- .../faults/TransientFaultExceptionTest.java | 2 +- riptide-opentracing/README.md | 185 ++++++++++++++ riptide-opentracing/pom.xml | 68 +++++ .../riptide/opentracing/ExtensionFields.java | 14 + .../riptide/opentracing/ExtensionTags.java | 61 +++++ .../opentracing/OpenTracingPlugin.java | 175 +++++++++++++ .../riptide/opentracing/package-info.java | 4 + .../span/CallSiteSpanDecorator.java | 25 ++ .../span/ComponentSpanDecorator.java | 29 +++ .../span/CompositeSpanDecorator.java | 45 ++++ .../opentracing/span/ErrorSpanDecorator.java | 36 +++ .../span/HttpMethodSpanDecorator.java | 19 ++ .../span/HttpPathSpanDecorator.java | 25 ++ .../span/HttpStatusCodeSpanDecorator.java | 21 ++ .../span/HttpUrlSpanDecorator.java | 19 ++ .../opentracing/span/PeerSpanDecorator.java | 23 ++ .../opentracing/span/RetrySpanDecorator.java | 26 ++ .../opentracing/span/SpanDecorator.java | 48 ++++ .../span/SpanKindSpanDecorator.java | 29 +++ .../opentracing/span/StaticSpanDecorator.java | 24 ++ .../span/UriVariablesTagSpanDecorator.java | 56 ++++ .../OpenTracingPluginRetryTest.java | 105 ++++++++ .../opentracing/OpenTracingPluginTest.java | 241 ++++++++++++++++++ .../riptide/opentracing/UnpackTest.java | 26 ++ riptide-spring-boot-autoconfigure/README.md | 102 +++++--- riptide-spring-boot-autoconfigure/pom.xml | 23 +- .../DefaultRiptideRegistrar.java | 163 +++++++----- .../riptide/autoconfigure/Defaulting.java | 23 ++ .../autoconfigure/FailsafePluginFactory.java | 15 +- .../OpenTracingPluginFactory.java | 43 ++++ .../riptide/autoconfigure/Registry.java | 5 - .../RiptideAutoConfiguration.java | 8 +- .../autoconfigure/RiptideProperties.java | 18 ++ .../riptide/autoconfigure/CachingTest.java | 2 +- .../DefaultTestConfiguration.java | 3 +- .../autoconfigure/ManualConfiguration.java | 53 +++- .../OpenTracingTestAutoConfiguration.java | 19 ++ .../autoconfigure/metrics/MetricsTest.java | 4 +- .../autoconfigure/url/UrlResolutionTest.java | 12 +- .../test/resources/application-default.yml | 14 + 45 files changed, 1706 insertions(+), 138 deletions(-) create mode 100644 docs/spider-web.jpg create mode 100644 riptide-opentracing/README.md create mode 100644 riptide-opentracing/pom.xml create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionFields.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/ExtensionTags.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/OpenTracingPlugin.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/package-info.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CallSiteSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ComponentSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/CompositeSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/ErrorSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpMethodSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpPathSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpStatusCodeSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/HttpUrlSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/PeerSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/RetrySpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/SpanKindSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/StaticSpanDecorator.java create mode 100644 riptide-opentracing/src/main/java/org/zalando/riptide/opentracing/span/UriVariablesTagSpanDecorator.java create mode 100644 riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginRetryTest.java create mode 100644 riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/OpenTracingPluginTest.java create mode 100644 riptide-opentracing/src/test/java/org/zalando/riptide/opentracing/UnpackTest.java create mode 100644 riptide-spring-boot-autoconfigure/src/main/java/org/zalando/riptide/autoconfigure/OpenTracingPluginFactory.java create mode 100644 riptide-spring-boot-autoconfigure/src/test/java/org/zalando/riptide/autoconfigure/OpenTracingTestAutoConfiguration.java diff --git a/docs/spider-web.jpg b/docs/spider-web.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3d9e5fe3d7acfbfd3f612acfedc9ebf508935bad GIT binary patch literal 50551 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 literal 0 HcmV?d00001 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