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