diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/MilliSpan.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/MilliSpan.java index d81da445e..11c1defa9 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/MilliSpan.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/MilliSpan.java @@ -22,30 +22,48 @@ import java.util.List; import java.util.Map; import lombok.Builder; +import lombok.Data; import lombok.Singular; -import lombok.Value; -import lombok.experimental.NonFinal; /** * @author Spencer Gibb */ -@Value +@Data @Builder public class MilliSpan implements Span { - private long begin; - @NonFinal + private final long begin; private long end = 0; - private String name; + private final String name; private final String traceId; @Singular private List parents; private final String spanId; - @NonFinal private boolean remote = false; - private Map kVAnnotations = new LinkedHashMap<>(); + private final Map kVAnnotations = new LinkedHashMap<>(); private final String processId; @Singular - private List timelineAnnotations = new ArrayList<>(); + private final List timelineAnnotations = new ArrayList<>(); + + public MilliSpan(long begin, long end, String name, String traceId, List parents, String spanId, boolean remote, String processId) { + this.begin = begin; + this.end = end; + this.name = name; + this.traceId = traceId; + this.parents = parents; + this.spanId = spanId; + this.remote = remote; + this.processId = processId; + } + + //for serialization + private MilliSpan() { + this.begin = 0; + this.name = null; + this.traceId = null; + this.parents = null; + this.spanId = null; + this.processId = null; + } @Override public synchronized void stop() { diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/TimelineAnnotation.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/TimelineAnnotation.java index 3d3e4a8a6..a7d7974a8 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/TimelineAnnotation.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/TimelineAnnotation.java @@ -17,12 +17,19 @@ package org.springframework.cloud.sleuth; import lombok.Data; +import lombok.RequiredArgsConstructor; /** * @author Spencer Gibb */ @Data +@RequiredArgsConstructor public class TimelineAnnotation { private final long time; private final String msg; + + private TimelineAnnotation() { + this.time = 0; + this.msg = null; + } } diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/slf4j/JsonSlf4jSpanListener.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/slf4j/JsonSlf4jSpanListener.java new file mode 100644 index 000000000..d0eed77c1 --- /dev/null +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/slf4j/JsonSlf4jSpanListener.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2015 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.slf4j; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import lombok.Data; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.cloud.sleuth.event.SpanStoppedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Spencer Gibb + */ +@Slf4j +@Order(Ordered.LOWEST_PRECEDENCE) +@Data +public class JsonSlf4jSpanListener { + + private final String prefix; + private final String suffix; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public JsonSlf4jSpanListener() { + prefix = "[span]"; + suffix = "[endspan]"; + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @SneakyThrows + @EventListener(SpanStoppedEvent.class) + public void stop(SpanStoppedEvent event) { + log.info("{}{}{}", prefix, + objectMapper.writeValueAsString(event.getSpan()), + suffix); + } + +} diff --git a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/slf4j/SleuthSlf4jAutoConfiguration.java b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/slf4j/SleuthSlf4jAutoConfiguration.java index 31fd58e97..f3145c8a8 100644 --- a/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/slf4j/SleuthSlf4jAutoConfiguration.java +++ b/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/slf4j/SleuthSlf4jAutoConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.cloud.sleuth.slf4j; import org.slf4j.MDC; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,8 +30,15 @@ import org.springframework.context.annotation.Configuration; public class SleuthSlf4jAutoConfiguration { @Bean + @ConditionalOnProperty(value = "spring.cloud.sleuth.listener.slf4j.enabled", matchIfMissing = true) public Slf4jSpanListener slf4jSpanStartedListener() { return new Slf4jSpanListener(); } + @Bean + @ConditionalOnProperty("spring.cloud.sleuth.listener.json.slf4j.enabled") + public JsonSlf4jSpanListener jsonSlf4jSpanListener() { + return new JsonSlf4jSpanListener(); + } + } diff --git a/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/slf4j/JsonSlf4jSpanListenerTests.java b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/slf4j/JsonSlf4jSpanListenerTests.java new file mode 100644 index 000000000..1daf508b3 --- /dev/null +++ b/spring-cloud-sleuth-core/src/test/java/org/springframework/cloud/sleuth/slf4j/JsonSlf4jSpanListenerTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2015 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.slf4j; + +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.boot.test.OutputCapture; +import org.springframework.cloud.sleuth.MilliSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.event.SpanStoppedEvent; + +import java.io.IOException; + +/** + * @author Spencer Gibb + */ +public class JsonSlf4jSpanListenerTests { + @Rule + public final OutputCapture output = new OutputCapture(); + + @Test + public void jsonSpanIsOnOneLine() throws IOException { + JsonSlf4jSpanListener listener = new JsonSlf4jSpanListener(); + Span span = MilliSpan.builder() + .name("testSpan") + .spanId("spanId1") + .parent("parentId1") + .traceId("traceId1") + .begin(1) + .end(10) + .build(); + span.addKVAnnotation("myKey", "myVal"); + span.addTimelineAnnotation("myTimelineAnnotation"); + listener.stop(new SpanStoppedEvent(this, span)); + + String output = this.output.toString().trim(); + assertTrue("output doesn't container prefix", output.contains(listener.getPrefix())); + assertTrue("output doesn't container suffix", output.contains(listener.getSuffix())); + + int prefixIndex = output.indexOf(listener.getPrefix()); + int suffixIndex = output.indexOf(listener.getSuffix()); + String json = output.substring(prefixIndex + listener.getPrefix().length(), suffixIndex); + assertFalse("json contains linefeed", output.contains("\n")); + assertFalse("json contains carriage return", output.contains("\r")); + + MilliSpan read = listener.getObjectMapper().readValue(json, MilliSpan.class); + assertEquals("span not equals", read, span); + } +}