diff --git a/pom.xml b/pom.xml
index 8c14a64e4..8dbaf7069 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,7 @@
spring-cloud-sleuth-core
spring-cloud-sleuth-zipkin
spring-cloud-sleuth-stream
+ spring-cloud-sleuth-zipkin-stream
spring-cloud-sleuth-samples
spring-cloud-starter-sleuth
spring-cloud-starter-zipkin
@@ -102,6 +103,11 @@
spring-cloud-sleuth-stream
${project.version}
+
+ org.springframework.cloud
+ spring-cloud-sleuth-zipkin-stream
+ ${project.version}
+
org.springframework.cloud
spring-cloud-starter-zipkin
@@ -220,7 +226,7 @@
- 3.1.0
+ 3.2.0
1.1.0.BUILD-SNAPSHOT
1.8.4
1.0-groovy-2.4
diff --git a/spring-cloud-sleuth-zipkin-stream/README.md b/spring-cloud-sleuth-zipkin-stream/README.md
new file mode 100644
index 000000000..f7cb1929d
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/README.md
@@ -0,0 +1,41 @@
+# Running Zipkin a Query Server
+
+There are 4 parts to Zipkin: the instrumented client apps, the web UI, the backend database and the query server. The database for this implementation is MySQL.
+
+> There is a running instance on PWS: http://zipkin-web.cfapps.io. It is backed by a `zipkin-java-server` with a MySQL backend and RabbitMQ (Spring Cloud Stream) for span transport.
+
+## Instrumenting Apps
+
+Depend on [Spring Cloud Sleuth Stream](https://github.com/spring-cloud-spring-cloud-sleuth). Bind to a rabbit service (or redis if you prefer - normal Spring Cloud Stream process).
+
+## Query Server
+
+Code is [here](https://github.com/spring-cloud-spring-cloud-sleuth/spring-cloud-sleuth-zipkin-stream).
+
+Bind to MySQL and the same Stream service that you did in the apps (rabbit, redis, kafka). Set `spring.datasource.initialize=true` the first time you start to initialize the database. Alternatively, curl it:
+
+```
+$ curl zipkin-server.cfapps.io/env -d endpoints.restart.enabled=true
+$ curl zipkin-server.cfapps.io/env -d spring.datasource.initialize=true
+$ curl zipkin-server.cfapps.io/restart -d {}
+```
+
+Uses the `zipkin-server` jar from the [OSS](https://github.com/openzipkin/zipkin-java) as well as `spring-cloud-sleuth-stream`.
+
+> NOTE: running in the "test" profile you don't need MySQL (the span store is in memory). You could even run in PWS without MySQL.
+
+## Web UI
+
+Get the jar from the [OSS](https://github.com/openzipkin/zipkin) and push it:
+
+```
+$ cf push zipkin-web -p zipkin-web/build/libs/zipkin-web*all.jar
+```
+
+It needs an environment variable to set the command line args:
+
+```
+$ cf set-env zipkin-web JBP_CONFIG_JAVA_MAIN '{arguments: "-zipkin.web.port=:\$PORT -zipkin.web.rootUrl=/ -zipkin.web.query.dest=zipkin-server.cfapps.io:80 -zipkin.web.resourcesRoot=."}'
+```
+
+NOTE: `JBP_CONFIG_JAVA_MAIN` only works with Java buildpack v3.2 and above (so not in PEZ Heritage right now).
diff --git a/spring-cloud-sleuth-zipkin-stream/docker-compose.query.yml b/spring-cloud-sleuth-zipkin-stream/docker-compose.query.yml
new file mode 100644
index 000000000..101a558f5
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/docker-compose.query.yml
@@ -0,0 +1,20 @@
+query:
+ image: springio/zipkin-server
+ environment:
+ - MYSQL_HOST=storage
+ expose:
+ - 9411
+ ports:
+ - 9411:9411
+ links:
+ - mysql:storage
+ - rabbitmq:messages
+web:
+ image: openzipkin/zipkin-web:1.25.0
+ ports:
+ - 8080:8080
+ environment:
+ - TRANSPORT_TYPE=http
+ - WEB_LOG_LEVEL=DEBUG
+ links:
+ - query
diff --git a/spring-cloud-sleuth-zipkin-stream/docker-compose.yml b/spring-cloud-sleuth-zipkin-stream/docker-compose.yml
new file mode 100644
index 000000000..08fe0f294
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/docker-compose.yml
@@ -0,0 +1,14 @@
+mysql:
+ image: mysql
+ ports:
+ - "3306:3306"
+ environment:
+ - MYSQL_ROOT_PASSWORD=root
+ - MYSQL_DATABASE=test
+ volumes:
+ - "./conf.d:/etc/mysql/conf.d:ro"
+rabbitmq:
+ image: rabbitmq:management
+ ports:
+ - 5672:5672
+ - 15672:15672
diff --git a/spring-cloud-sleuth-zipkin-stream/pom.xml b/spring-cloud-sleuth-zipkin-stream/pom.xml
new file mode 100644
index 000000000..263242e93
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/pom.xml
@@ -0,0 +1,133 @@
+
+
+ 4.0.0
+
+ spring-cloud-sleuth-zipkin-stream
+ jar
+
+ spring-cloud-sleuth-zipkin-stream
+ Spring Boot Zipkin Server
+
+
+ org.springframework.cloud
+ spring-cloud-sleuth
+ 1.0.0.BUILD-SNAPSHOT
+
+
+
+ springio
+ UTF-8
+ 1.8
+ 0.1.0
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+ org.springframework.cloud
+ spring-cloud-starter-stream-rabbit
+
+
+ objenesis
+ org.objenesis
+ compile
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.springframework.cloud
+ spring-cloud-starter-sleuth
+
+
+ org.springframework.cloud
+ spring-cloud-sleuth-stream
+
+
+ org.springframework.boot
+ spring-boot-starter-cloud-connectors
+
+
+ io.zipkin
+ zipkin-java-server
+ ${zipkin-java.version}
+
+
+ io.zipkin
+ zipkin-java-jdbc
+ ${zipkin-java.version}
+
+
+ com.h2database
+ h2
+ true
+
+
+ mysql
+ mysql-connector-java
+ true
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.cloud
+ spring-cloud-stream-test-support
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ objenesis
+ org.objenesis
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ com.spotify
+ docker-maven-plugin
+ 0.3.5
+
+ ${docker.image.prefix}/${project.artifactId}
+ src/main/docker
+
+
+ /
+ ${project.build.directory}
+ ${project.build.finalName}.jar
+
+
+
+
+
+
+
+
diff --git a/spring-cloud-sleuth-zipkin-stream/src/main/docker/Dockerfile b/spring-cloud-sleuth-zipkin-stream/src/main/docker/Dockerfile
new file mode 100644
index 000000000..309c566fc
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/src/main/docker/Dockerfile
@@ -0,0 +1,5 @@
+FROM frolvlad/alpine-oraclejdk8
+VOLUME /tmp
+ADD spring-cloud-sleuth-zipkin-stream-1.0.0.BUILD-SNAPSHOT.jar app.jar
+RUN sh -c 'touch /app.jar'
+CMD ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
diff --git a/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/EnableZipkinStreamServer.java b/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/EnableZipkinStreamServer.java
new file mode 100644
index 000000000..6970618c3
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/EnableZipkinStreamServer.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2015 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.zipkin.stream;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.cloud.sleuth.stream.SleuthSink;
+import org.springframework.cloud.stream.annotation.EnableBinding;
+import org.springframework.context.annotation.Import;
+
+import io.zipkin.server.EnableZipkinServer;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@EnableBinding(SleuthSink.class)
+@EnableZipkinServer
+@Import(ZipkinMessageListener.class)
+public @interface EnableZipkinStreamServer {
+
+}
diff --git a/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinMessageListener.java b/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinMessageListener.java
new file mode 100644
index 000000000..b3f15a566
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinMessageListener.java
@@ -0,0 +1,223 @@
+package org.springframework.cloud.sleuth.zipkin.stream;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.apache.thrift.TException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
+import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.Cloud;
+import org.springframework.cloud.CloudFactory;
+import org.springframework.cloud.sleuth.Span;
+import org.springframework.cloud.sleuth.TimelineAnnotation;
+import org.springframework.cloud.sleuth.stream.Host;
+import org.springframework.cloud.sleuth.stream.SleuthSink;
+import org.springframework.cloud.sleuth.stream.Spans;
+import org.springframework.cloud.sleuth.zipkin.stream.ZipkinMessageListener.NotSleuthStreamClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.context.annotation.Conditional;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+import org.springframework.integration.annotation.MessageEndpoint;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.util.StringUtils;
+
+import io.zipkin.Annotation;
+import io.zipkin.BinaryAnnotation;
+import io.zipkin.BinaryAnnotation.Type;
+import io.zipkin.Endpoint;
+import io.zipkin.Span.Builder;
+import io.zipkin.SpanStore;
+import lombok.extern.apachecommons.CommonsLog;
+
+@MessageEndpoint
+@CommonsLog
+@Conditional(NotSleuthStreamClient.class)
+public class ZipkinMessageListener {
+
+ @Autowired
+ SpanStore spanStore;
+
+ @ServiceActivator(inputChannel = SleuthSink.INPUT)
+ public void sink(Spans input) throws TException {
+ List spans = new ArrayList<>();
+ for (Span span : input.getSpans()) {
+ if (!span.getName().equals("message/" + SleuthSink.INPUT)) {
+ spans.add(convert(span, input.getHost()));
+ }
+ else {
+ log.warn("Message tracing cycle detected for: " + span);
+ }
+ }
+ if (!spans.isEmpty()) {
+ this.spanStore.accept(spans);
+ }
+ }
+
+ /**
+ * Converts a given Sleuth span to a Zipkin Span.
+ *
+ * - Set ids, etc
+ *
- Create timeline annotations based on data from Span object.
+ *
- Create binary annotations based on data from Span object.
+ *
+ */
+ public io.zipkin.Span convert(Span span, Host host) {
+ Builder zipkinSpan = new io.zipkin.Span.Builder();
+
+ Endpoint ep = Endpoint.create(host.getServiceName(), host.getIpv4(),
+ host.getPort().shortValue());
+ List annotationList = createZipkinAnnotations(span, ep);
+ List binaryAnnotationList = createZipkinBinaryAnnotations(span,
+ ep);
+ zipkinSpan.traceId(hash(span.getTraceId()));
+ if (span.getParents().size() > 0) {
+ if (span.getParents().size() > 1) {
+ log.error("zipkin doesn't support spans with multiple parents. Omitting "
+ + "other parents for " + span);
+ }
+ zipkinSpan.parentId(hash(span.getParents().get(0)));
+ }
+ zipkinSpan.id(hash(span.getSpanId()));
+ if (StringUtils.hasText(span.getName())) {
+ zipkinSpan.name(span.getName());
+ }
+ for (Annotation annotation : annotationList) {
+ zipkinSpan.addAnnotation(annotation);
+ }
+ for (BinaryAnnotation annotation : binaryAnnotationList) {
+ zipkinSpan.addBinaryAnnotation(annotation);
+ }
+ return zipkinSpan.build();
+ }
+
+ /**
+ * Add annotations from the sleuth Span.
+ */
+ private List createZipkinAnnotations(Span span, Endpoint endpoint) {
+ List annotationList = new ArrayList<>();
+ for (TimelineAnnotation ta : span.getTimelineAnnotations()) {
+ Annotation zipkinAnnotation = createZipkinAnnotation(ta.getMsg(),
+ ta.getTime(), endpoint, true);
+ annotationList.add(zipkinAnnotation);
+ }
+ return annotationList;
+ }
+
+ /**
+ * Creates a list of Annotations that are present in sleuth Span object.
+ *
+ * @return list of Annotations that could be added to Zipkin Span.
+ */
+ private List createZipkinBinaryAnnotations(Span span,
+ Endpoint endpoint) {
+ List l = new ArrayList<>();
+ for (Map.Entry e : span.getAnnotations().entrySet()) {
+ BinaryAnnotation.Builder binaryAnn = new BinaryAnnotation.Builder();
+ binaryAnn.type(Type.STRING);
+ binaryAnn.key(e.getKey());
+ try {
+ binaryAnn.value(e.getValue().getBytes("UTF-8"));
+ }
+ catch (UnsupportedEncodingException ex) {
+ log.error("Error encoding string as UTF-8", ex);
+ }
+ binaryAnn.endpoint(endpoint);
+ l.add(binaryAnn.build());
+ }
+ return l;
+ }
+
+ /**
+ * Create an annotation with the correct times and endpoint.
+ *
+ * @param value Annotation value
+ * @param time timestamp will be extracted
+ * @param endpoint the endpoint this annotation will be associated with.
+ * @param sendRequest use the first or last timestamp.
+ */
+ private static Annotation createZipkinAnnotation(String value, long time,
+ Endpoint endpoint, boolean sendRequest) {
+ Annotation.Builder annotation = new Annotation.Builder();
+ annotation.endpoint(endpoint);
+
+ // Zipkin is in microseconds
+ if (sendRequest) {
+ annotation.timestamp(time * 1000);
+ }
+ else {
+ annotation.timestamp(time * 1000);
+ }
+ annotation.value(value);
+ return annotation.build();
+ }
+
+ private static long hash(String string) {
+ long h = 1125899906842597L;
+ if (string == null) {
+ return h;
+ }
+ int len = string.length();
+
+ for (int i = 0; i < len; i++) {
+ h = 31 * h + string.charAt(i);
+ }
+ return h;
+ }
+
+ protected static class NotSleuthStreamClient extends SpringBootCondition {
+
+ @Override
+ public ConditionOutcome getMatchOutcome(ConditionContext context,
+ AnnotatedTypeMetadata metadata) {
+ Environment environment = context.getEnvironment();
+ if ("true".equals(environment
+ .resolvePlaceholders("${spring.sleuth.stream.enabled:}"))) {
+ return ConditionOutcome
+ .noMatch("Found spring.sleuth.stream.enabled=true");
+ }
+ if (environment instanceof ConfigurableEnvironment) {
+ ConfigurableEnvironment configurable = (ConfigurableEnvironment) environment;
+ configurable.getPropertySources()
+ .addLast(
+ new MapPropertySource("spring.sleuth.stream",
+ Collections.singletonMap(
+ "spring.sleuth.stream.enabled",
+ "false")));
+ }
+ return ConditionOutcome.match("Not found: spring.sleuth.stream.enabled");
+ }
+
+ }
+
+ @Configuration
+ @Profile("cloud")
+ protected static class CloudDataSourceConfiguration {
+
+ @Bean
+ public Cloud cloud() {
+ return new CloudFactory().getCloud();
+ }
+
+ @Bean
+ @ConfigurationProperties(DataSourceProperties.PREFIX)
+ public DataSource dataSource() {
+ return cloud().getSingletonServiceConnector(DataSource.class, null);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinQueryServerApplication.java b/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinQueryServerApplication.java
new file mode 100644
index 000000000..b43c8456f
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/src/main/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinQueryServerApplication.java
@@ -0,0 +1,15 @@
+package org.springframework.cloud.sleuth.zipkin.stream;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+
+@SpringBootApplication
+@EnableZipkinStreamServer
+public class ZipkinQueryServerApplication {
+
+ public static void main(String[] args) throws Exception {
+ new SpringApplicationBuilder(ZipkinQueryServerApplication.class)
+ .properties("spring.config.name=zipkin-server").run(args);
+ }
+
+}
diff --git a/spring-cloud-sleuth-zipkin-stream/src/main/resources/zipkin-server.yml b/spring-cloud-sleuth-zipkin-stream/src/main/resources/zipkin-server.yml
new file mode 100644
index 000000000..dd709d36c
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/src/main/resources/zipkin-server.yml
@@ -0,0 +1,24 @@
+server:
+ port: 9411
+
+spring:
+ datasource:
+ schema: classpath:/mysql.sql
+ url: jdbc:mysql://${MYSQL_HOST:localhost}/test
+ username: root
+ password: root
+# Switch this on to create the schema on startup:
+ initialize: false
+ continueOnError: true
+ sleuth:
+ enabled: false
+zipkin:
+ store:
+ type: mysql # default is inMemory
+
+---
+spring:
+ profiles: test
+zipkin:
+ store:
+ type: mem # default is inMemory
diff --git a/spring-cloud-sleuth-zipkin-stream/src/test/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinServerApplicationTests.java b/spring-cloud-sleuth-zipkin-stream/src/test/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinServerApplicationTests.java
new file mode 100644
index 000000000..2e7be9bc8
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/src/test/java/org/springframework/cloud/sleuth/zipkin/stream/ZipkinServerApplicationTests.java
@@ -0,0 +1,31 @@
+package org.springframework.cloud.sleuth.zipkin.stream;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.cloud.sleuth.zipkin.stream.ZipkinQueryServerApplication;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = ZipkinQueryServerApplication.class)
+@IntegrationTest({ "server.port=0", "spring.datasource.initialize=true" })
+@ActiveProfiles("test")
+public class ZipkinServerApplicationTests {
+
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ @Test
+ public void contextLoads() {
+ int count = this.jdbcTemplate.queryForObject("SELECT COUNT(*) FROM zipkin_spans",
+ Integer.class);
+ assertEquals(0, count);
+ }
+
+}
diff --git a/spring-cloud-sleuth-zipkin-stream/src/test/resources/drop.sql b/spring-cloud-sleuth-zipkin-stream/src/test/resources/drop.sql
new file mode 100644
index 000000000..507f4f27f
--- /dev/null
+++ b/spring-cloud-sleuth-zipkin-stream/src/test/resources/drop.sql
@@ -0,0 +1,5 @@
+drop table zipkin_spans;
+drop table zipkin_annotations;
+drop table zipkin_binary_annotations;
+drop table zipkin_dependencies;
+drop table zipkin_dependency_links;
\ No newline at end of file