From 22d91bac247e8273fa93ffe05f50179182425f72 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Sun, 21 Jan 2018 09:28:34 +0100 Subject: [PATCH] Added OpenTracing support; fixes #599 --- docs/src/main/asciidoc/features.adoc | 4 +- .../main/asciidoc/spring-cloud-sleuth.adoc | 6 + spring-cloud-sleuth-core/pom.xml | 5 + .../OpentracingAutoConfiguration.java | 34 ++ .../SleuthOpentracingProperties.java | 39 ++ .../sleuth/instrument/web/TraceWebFilter.java | 2 +- .../main/resources/META-INF/spring.factories | 3 +- .../opentracing/BraveTracerTest.java | 342 ++++++++++++++++++ spring-cloud-sleuth-dependencies/pom.xml | 8 +- 9 files changed, 438 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/OpentracingAutoConfiguration.java create mode 100644 spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/SleuthOpentracingProperties.java create mode 100644 spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/opentracing/BraveTracerTest.java diff --git a/docs/src/main/asciidoc/features.adoc b/docs/src/main/asciidoc/features.adoc index b5733768dd..a35940028f 100644 --- a/docs/src/main/asciidoc/features.adoc +++ b/docs/src/main/asciidoc/features.adoc @@ -39,14 +39,14 @@ a baggage element then it will be sent downstream either via HTTP or messaging t * Provides a way to create / continue spans and add tags and logs via annotations. -* Provides simple metrics of accepted / dropped spans. - * If `spring-cloud-sleuth-zipkin` then the app will generate and collect Zipkin-compatible traces. By default it sends them via HTTP to a Zipkin server on localhost (port 9411). Configure the location of the service using `spring.zipkin.baseUrl`. - If you depend on `spring-rabbit` or `spring-kafka` your app will send traces to a broker instead of http. - Note: `spring-cloud-sleuth-stream` is deprecated and should no longer be used. +* Spring Cloud Sleuth is http://opentracing.io/[OpenTracing] compatible + IMPORTANT: If using Zipkin, configure the percentage of spans exported using `spring.sleuth.sampler.percentage` (default 0.1, i.e. 10%). *Otherwise you might think that Sleuth is not working cause it's omitting some spans.* diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index 5c4acebf9e..2f5f92eb4a 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -931,6 +931,12 @@ on how to create a Stream Zipkin server. == Integrations +=== OpenTracing + +Spring Cloud Sleuth is http://opentracing.io/[OpenTracing] compatible. If you have +OpenTracing on the classpath we will automatically register the OpenTracing +`Tracer` bean. If you wish to disable this just set `spring.sleuth.opentracing.enabled` to `false` + === Runnable and Callable If you're wrapping your logic in `Runnable` or `Callable` it's enough to wrap those classes in their Sleuth representative. diff --git a/spring-cloud-sleuth-core/pom.xml b/spring-cloud-sleuth-core/pom.xml index 52a8bbee24..0efd9ca733 100644 --- a/spring-cloud-sleuth-core/pom.xml +++ b/spring-cloud-sleuth-core/pom.xml @@ -152,6 +152,11 @@ io.zipkin.brave brave-instrumentation-spring-webmvc + + io.opentracing.brave + brave-opentracing + true + org.springframework.boot diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/OpentracingAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/OpentracingAutoConfiguration.java new file mode 100644 index 0000000000..b15f80a2df --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/OpentracingAutoConfiguration.java @@ -0,0 +1,34 @@ +package org.springframework.cloud.sleuth.instrument.opentracing; + +import brave.Tracing; +import brave.opentracing.BraveTracer; +import io.opentracing.Tracer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration Auto-configuration} + * to enable tracing via Opentracing. + * + * @author Spencer Gibb + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +@Configuration +@ConditionalOnProperty(value="spring.sleuth.opentracing.enabled", matchIfMissing=true) +@ConditionalOnBean(Tracing.class) +@ConditionalOnClass(Tracer.class) +@EnableConfigurationProperties(SleuthOpentracingProperties.class) +public class OpentracingAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + Tracer sleuthOpenTracing(brave.Tracing braveTracing) { + return BraveTracer.create(braveTracing); + } +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/SleuthOpentracingProperties.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/SleuthOpentracingProperties.java new file mode 100644 index 0000000000..a8c874d8bb --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/opentracing/SleuthOpentracingProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.opentracing; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Sleuth Opentracing settings + * + * @since 2.0.0 + */ +@ConfigurationProperties("spring.sleuth.opentracing") +public class SleuthOpentracingProperties { + + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java index 6768bd452c..9ea6b85cce 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java @@ -32,7 +32,7 @@ * @author Marcin Grzejszczak * @since 2.0.0 */ -public class TraceWebFilter implements WebFilter, Ordered { +public final class TraceWebFilter implements WebFilter, Ordered { private static final Log log = LogFactory.getLog(TraceWebFilter.class); diff --git a/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories index c69d343444..6ade660739 100644 --- a/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-core/src/main/resources/META-INF/spring.factories @@ -18,7 +18,8 @@ org.springframework.cloud.sleuth.instrument.reactor.TraceReactorAutoConfiguratio org.springframework.cloud.sleuth.instrument.web.TraceWebFluxAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.zuul.TraceZuulAutoConfiguration,\ org.springframework.cloud.sleuth.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\ -org.springframework.cloud.sleuth.instrument.messaging.websocket.TraceWebSocketAutoConfiguration +org.springframework.cloud.sleuth.instrument.messaging.websocket.TraceWebSocketAutoConfiguration,\ +org.springframework.cloud.sleuth.instrument.opentracing.OpentracingAutoConfiguration # Environment Post Processor org.springframework.boot.env.EnvironmentPostProcessor=\ diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/opentracing/BraveTracerTest.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/opentracing/BraveTracerTest.java new file mode 100644 index 0000000000..e84c58d4fc --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/instrument/opentracing/BraveTracerTest.java @@ -0,0 +1,342 @@ +/** + * Copyright 2016-2018 The OpenZipkin Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.cloud.sleuth.instrument.opentracing; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import brave.Span; +import brave.Tracer; +import brave.Tracer.SpanInScope; +import brave.Tracing; +import brave.opentracing.BraveSpan; +import brave.opentracing.BraveSpanContext; +import brave.opentracing.BraveTracer; +import brave.propagation.B3Propagation; +import brave.propagation.CurrentTraceContext; +import brave.propagation.ExtraFieldPropagation; +import brave.propagation.Propagation; +import brave.propagation.StrictCurrentTraceContext; +import brave.propagation.TraceContext; +import brave.sampler.Sampler; +import io.opentracing.Scope; +import io.opentracing.propagation.Format; +import io.opentracing.propagation.TextMap; +import io.opentracing.propagation.TextMapExtractAdapter; +import io.opentracing.propagation.TextMapInjectAdapter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.util.ArrayListSpanReporter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit4.SpringRunner; +import zipkin2.Annotation; +import zipkin2.Endpoint; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; +import static org.junit.Assert.assertEquals; + +/** + * This shows how one might make an OpenTracing adapter for Brave, and how to navigate in and out of + * the core concepts. + * + * Adopted from: https://github.com/openzipkin-contrib/brave-opentracing/tree/master/src/test/java/brave/opentracing + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, +properties = "spring.sleuth.baggage-keys=country-code,user-id") +public class BraveTracerTest { + + @Autowired ArrayListSpanReporter spans; + @Autowired Tracing brave; + @Autowired BraveTracer opentracing; + + @Test public void startWithOpenTracingAndFinishWithBrave() { + io.opentracing.Span openTracingSpan = opentracing.buildSpan("encode") + .withTag("lc", "codec") + .withStartTimestamp(1L) + .start(); + + Span braveSpan = ((BraveSpan) openTracingSpan).unwrap(); + + braveSpan.annotate(2L, "pump fake"); + braveSpan.finish(3L); + + checkSpanReportedToZipkin(); + } + + @Test public void extractTraceContext() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("X-B3-TraceId", "0000000000000001"); + map.put("X-B3-SpanId", "0000000000000002"); + map.put("X-B3-Sampled", "1"); + + BraveSpanContext openTracingContext = + (BraveSpanContext) opentracing.extract(Format.Builtin.HTTP_HEADERS, + new TextMapExtractAdapter(map)); + + assertThat(openTracingContext.unwrap()) + .isEqualTo(TraceContext.newBuilder() + .traceId(1L) + .spanId(2L) + .sampled(true).build()); + } + + @Test public void extractBaggage() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("X-B3-TraceId", "0000000000000001"); + map.put("X-B3-SpanId", "0000000000000002"); + map.put("X-B3-Sampled", "1"); + map.put("baggage-country-code", "FO"); + + BraveSpanContext openTracingContext = opentracing.extract(Format.Builtin.HTTP_HEADERS, + new TextMapExtractAdapter(map)); + + assertThat(openTracingContext.baggageItems()) + .containsExactly(entry("country-code", "FO")); + } + + @Test public void extractTraceContextTextMap() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("X-B3-TraceId", "0000000000000001"); + map.put("X-B3-SpanId", "0000000000000002"); + map.put("X-B3-Sampled", "1"); + + BraveSpanContext openTracingContext = + (BraveSpanContext) opentracing.extract(Format.Builtin.TEXT_MAP, + new TextMapExtractAdapter(map)); + + assertThat(openTracingContext.unwrap()) + .isEqualTo(TraceContext.newBuilder() + .traceId(1L) + .spanId(2L) + .sampled(true).build()); + } + + @Test public void extractTraceContextCaseInsensitive() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("X-B3-TraceId", "0000000000000001"); + map.put("x-b3-spanid", "0000000000000002"); + map.put("x-b3-SaMpLeD", "1"); + map.put("other", "1"); + + BraveSpanContext openTracingContext = + (BraveSpanContext) opentracing.extract(Format.Builtin.HTTP_HEADERS, + new TextMapExtractAdapter(map)); + + assertThat(openTracingContext.unwrap()) + .isEqualTo(TraceContext.newBuilder() + .traceId(1L) + .spanId(2L) + .sampled(true).build()); + } + + @Test public void extractTraceContextReturnsNull() throws Exception { + Map map = new LinkedHashMap<>(); + map.put("other", "1"); + + BraveSpanContext openTracingContext = opentracing.extract(Format.Builtin.HTTP_HEADERS, + new TextMapExtractAdapter(map)); + + assertThat(openTracingContext).isNull(); + } + + @Test public void injectTraceContext_baggage() throws Exception { + BraveSpan span = opentracing.buildSpan("foo").start(); + span.setBaggageItem("country-code", "FO"); + + Map map = new LinkedHashMap<>(); + TextMapInjectAdapter carrier = new TextMapInjectAdapter(map); + opentracing.inject(span.context(), Format.Builtin.HTTP_HEADERS, carrier); + + assertThat(map).containsEntry("baggage-country-code", "FO"); + } + + void checkSpanReportedToZipkin() { + assertThat(spans.getSpans()).first().satisfies(s -> { + assertThat(s.name()).isEqualTo("encode"); + assertThat(s.timestamp()).isEqualTo(1L); + assertThat(s.annotations()) + .containsExactly(Annotation.create(2L, "pump fake")); + assertThat(s.tags()) + .containsExactly(entry("lc", "codec")); + assertThat(s.duration()).isEqualTo(2L); + } + ); + } + + @Test public void subsequentChildrenNestProperly_OTStyle() { + // this test is semantically identical to subsequentChildrenNestProperly_BraveStyle, but uses + // the OpenTracingAPI instead of the Brave API. + + Long idOfSpanA; + Long shouldBeIdOfSpanA; + Long idOfSpanB; + Long shouldBeIdOfSpanB; + Long parentIdOfSpanB; + Long parentIdOfSpanC; + + try (Scope scopeA = opentracing.buildSpan("spanA").startActive(false)) { + idOfSpanA = getTraceContext(scopeA).spanId(); + try (Scope scopeB = opentracing.buildSpan("spanB").startActive(false)) { + idOfSpanB = getTraceContext(scopeB).spanId(); + parentIdOfSpanB = getTraceContext(scopeB).parentId(); + shouldBeIdOfSpanB = getTraceContext(opentracing.scopeManager().active()).spanId(); + } + shouldBeIdOfSpanA = getTraceContext(opentracing.scopeManager().active()).spanId(); + try (Scope scopeC = opentracing.buildSpan("spanC").startActive(false)) { + parentIdOfSpanC = getTraceContext(scopeC).parentId(); + } + } + + assertEquals("SpanA should have been active again after closing B", idOfSpanA, + shouldBeIdOfSpanA); + assertEquals("SpanB should have been active prior to its closure", idOfSpanB, + shouldBeIdOfSpanB); + assertEquals("SpanB's parent should be SpanA", idOfSpanA, parentIdOfSpanB); + assertEquals("SpanC's parent should be SpanA", idOfSpanA, parentIdOfSpanC); + } + + @Test public void subsequentChildrenNestProperly_BraveStyle() { + // this test is semantically identical to subsequentChildrenNestProperly_OTStyle, but uses + // the Brave API instead of the OpenTracing API. + + Long shouldBeIdOfSpanA; + Long idOfSpanB; + Long shouldBeIdOfSpanB; + Long parentIdOfSpanB; + Long parentIdOfSpanC; + + Span spanA = brave.tracer().newTrace().name("spanA").start(); + Long idOfSpanA = spanA.context().spanId(); + try (SpanInScope scopeA = brave.tracer().withSpanInScope(spanA)) { + + Span spanB = brave.tracer().newChild(spanA.context()).name("spanB").start(); + idOfSpanB = spanB.context().spanId(); + parentIdOfSpanB = spanB.context().parentId(); + try (SpanInScope scopeB = brave.tracer().withSpanInScope(spanB)) { + shouldBeIdOfSpanB = brave.currentTraceContext().get().spanId(); + } finally { + spanB.finish(); + } + + shouldBeIdOfSpanA = brave.currentTraceContext().get().spanId(); + + Span spanC = brave.tracer().newChild(spanA.context()).name("spanC").start(); + parentIdOfSpanC = spanC.context().parentId(); + try (SpanInScope scopeC = brave.tracer().withSpanInScope(spanC)) { + // nothing to do here + } finally { + spanC.finish(); + } + } finally { + spanA.finish(); + } + + assertEquals("SpanA should have been active again after closing B", idOfSpanA, + shouldBeIdOfSpanA); + assertEquals("SpanB should have been active prior to its closure", idOfSpanB, + shouldBeIdOfSpanB); + assertEquals("SpanB's parent should be SpanA", idOfSpanA, parentIdOfSpanB); + assertEquals("SpanC's parent should be SpanA", idOfSpanA, parentIdOfSpanC); + } + + @Test public void implicitParentFromSpanManager_startActive() { + try (Scope scopeA = opentracing.buildSpan("spanA").startActive(true)) { + try (Scope scopeB = opentracing.buildSpan("spanA").startActive(true)) { + assertThat(getTraceContext(scopeB).parentId()) + .isEqualTo(getTraceContext(scopeA).spanId()); + } + } + } + + @Test public void implicitParentFromSpanManager_start() { + try (Scope scopeA = opentracing.buildSpan("spanA").startActive(true)) { + BraveSpan span = opentracing.buildSpan("spanB").start(); + assertThat(span.unwrap().context().parentId()) + .isEqualTo(getTraceContext(scopeA).spanId()); + } + } + + @Test public void implicitParentFromSpanManager_startActive_ignoreActiveSpan() { + try (Scope scopeA = opentracing.buildSpan("spanA").startActive(true)) { + try (Scope scopeB = opentracing.buildSpan("spanA") + .ignoreActiveSpan().startActive(true)) { + assertThat(getTraceContext(scopeB).parentId()) + .isNull(); // new trace + } + } + } + + @Test public void implicitParentFromSpanManager_start_ignoreActiveSpan() { + try (Scope scopeA = opentracing.buildSpan("spanA").startActive(true)) { + BraveSpan span = opentracing.buildSpan("spanB") + .ignoreActiveSpan().start(); + assertThat(span.unwrap().context().parentId()) + .isNull(); // new trace + } + } + + @Test public void ignoresErrorFalseTag_beforeStart() { + opentracing.buildSpan("encode") + .withTag("error", false) + .start().finish(); + + assertThat(spans.getSpans().get(0).tags()) + .isEmpty(); + } + + @Test public void ignoresErrorFalseTag_afterStart() { + opentracing.buildSpan("encode") + .start() + .setTag("error", false) + .finish(); + + assertThat(spans.getSpans().get(0).tags()) + .isEmpty(); + } + + private static TraceContext getTraceContext(Scope scope) { + return ((BraveSpanContext) scope.span().context()).unwrap(); + } + + @Before public void clear() { + this.spans.clear(); + } + + @Configuration + @EnableAutoConfiguration + static class Config { + @Bean Sampler sampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean ArrayListSpanReporter reporter() { + return new ArrayListSpanReporter(); + } + + @Bean CurrentTraceContext currentTraceContext() { + return new StrictCurrentTraceContext(); + } + } +} diff --git a/spring-cloud-sleuth-dependencies/pom.xml b/spring-cloud-sleuth-dependencies/pom.xml index 58dc830b98..71b18b8db5 100644 --- a/spring-cloud-sleuth-dependencies/pom.xml +++ b/spring-cloud-sleuth-dependencies/pom.xml @@ -18,6 +18,7 @@ 1.1.2 2.3.0 4.13.3 + 0.25.0 @@ -87,7 +88,7 @@ brave-instrumentation-spring-webmvc ${brave.version} - + io.zipkin.java zipkin @@ -153,6 +154,11 @@ + + io.opentracing.brave + brave-opentracing + ${brave.opentracing.version} +