Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
feafdf7
HTML span observer
iamdanfox Aug 23, 2019
5ffe9db
Orders
iamdanfox Aug 23, 2019
a2e2c4e
ASCII
iamdanfox Aug 23, 2019
fce185f
checkstyle
iamdanfox Aug 23, 2019
3deb6ec
Refactor a bit
iamdanfox Aug 23, 2019
3e2d922
Factor into dedicated, publishable project
iamdanfox Aug 23, 2019
a7cea03
less dumb names
iamdanfox Aug 23, 2019
6d268eb
--write-locks
iamdanfox Aug 23, 2019
7e54be9
Workaround unclosed root span
iamdanfox Aug 23, 2019
8f7dfe4
Cleaner explanations
iamdanfox Aug 23, 2019
a96216c
Omit the fake root span from visualizations
iamdanfox Aug 23, 2019
3afa243
Omit a single bar
iamdanfox Aug 23, 2019
91cb67f
Don't need synchronized & nice time measurement
iamdanfox Aug 24, 2019
508a077
Nicer colours :)
iamdanfox Aug 24, 2019
0ab0c7c
Reorder
iamdanfox Aug 24, 2019
f844991
JUnit5 @Rule equivalent not @ClassRule
iamdanfox Aug 24, 2019
2703fed
Use common bounds
iamdanfox Aug 24, 2019
5d44b6b
One colour per traceId
iamdanfox Aug 24, 2019
084593c
Include raw spans
iamdanfox Aug 24, 2019
29d58e0
Detect collisions
iamdanfox Aug 24, 2019
10a1805
Include a nice displayname
iamdanfox Aug 24, 2019
84e2b2d
Give em a nice title & date
iamdanfox Aug 24, 2019
7f3d16e
Checkstyle
iamdanfox Aug 24, 2019
fc317ad
Cleanup
iamdanfox Aug 24, 2019
bb2e499
Allow plain timeline sorting
iamdanfox Aug 27, 2019
72eabc6
Pull in c-j-r jackson
iamdanfox Aug 27, 2019
0e54aae
@TestTracing
iamdanfox Aug 27, 2019
4bde231
Assertions can fail
iamdanfox Aug 27, 2019
9680a0c
Factor out & guava 28
iamdanfox Aug 27, 2019
6261c1d
Use factored out thingy
iamdanfox Aug 27, 2019
9339267
Graph validates
iamdanfox Aug 27, 2019
be4815e
Inline
iamdanfox Aug 27, 2019
e10aad9
Collecting problem spanids
iamdanfox Aug 27, 2019
f3dd473
Rip out HtmlFormatter
iamdanfox Aug 27, 2019
da29d56
Check in a log-receiver trace
iamdanfox Aug 27, 2019
bf091cb
Factor out single span.html
iamdanfox Aug 27, 2019
c5e98ce
Tidying
iamdanfox Aug 27, 2019
c0e04ba
Builder
iamdanfox Aug 27, 2019
2a539f4
Use fields
iamdanfox Aug 27, 2019
560c05b
Make the problem spanids flash
iamdanfox Aug 27, 2019
8fad42d
Render expected and actual
iamdanfox Aug 27, 2019
2ec4aa7
Oops
iamdanfox Aug 27, 2019
d3954f2
Checkstyle
iamdanfox Aug 27, 2019
fe2e71f
TODOs
iamdanfox Aug 27, 2019
39b4e08
Ensure single root span
Aug 28, 2019
0b8507a
cleanup spanalyzer
Aug 28, 2019
c858079
Structured errors from comparison
Aug 28, 2019
503dae5
Generate reports in build directory
Aug 28, 2019
81079f1
HtmlFormatter takes in renderConfig
Aug 28, 2019
c75b744
'Sudoku' validation for overlapping child spans
iamdanfox Aug 28, 2019
db95518
Nicer
iamdanfox Aug 28, 2019
d3bc940
UNBOUNDED
iamdanfox Aug 28, 2019
195af75
LayoutStrategy
iamdanfox Aug 28, 2019
38e68d8
Decide based on annotation
iamdanfox Aug 28, 2019
cb1ed3d
Tidying
iamdanfox Aug 28, 2019
91e3cb0
Die checkstyle
iamdanfox Aug 28, 2019
ba7d440
Minor move
iamdanfox Aug 28, 2019
8d9087a
Refactor comparison into spanalyzer
Aug 28, 2019
0c78e65
Add tests to HTML formatter
Aug 28, 2019
45c5aa7
check in snapshots
Aug 28, 2019
1643798
README
iamdanfox Aug 28, 2019
0c4f35a
Actually make stable
Aug 28, 2019
464dbd4
Merge branch 'dfox/render-spans' of github.com:palantir/tracing-java …
Aug 28, 2019
dd7fba6
add support for circle artifacts
Aug 28, 2019
0f02a21
Mention JUnit4
iamdanfox Aug 28, 2019
9dda204
checkstyle
Aug 28, 2019
7e3b818
Filter out fake root spans
iamdanfox Aug 28, 2019
62490be
Include spanid not traceid
iamdanfox Aug 28, 2019
188865d
add changelog
Aug 28, 2019
cc88f76
Simpler comparator
iamdanfox Aug 28, 2019
34cd3e5
Support parameterized junit5 tests
iamdanfox Aug 28, 2019
118d894
Merge branch 'dfox/render-spans' of github.com:palantir/tracing-java …
iamdanfox Aug 28, 2019
0963fd4
Checkstyle
iamdanfox Aug 28, 2019
25bbeb0
Autorelease 3.2.0-rc1
iamdanfox Aug 28, 2019
223a5fa
oops
iamdanfox Aug 28, 2019
601628d
Fix vacuous spans
iamdanfox Aug 28, 2019
ac95394
fix overlapping span detection
Aug 28, 2019
a69f10a
refactor
Aug 28, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ repositories {
}

allprojects {
version gitVersion()
version System.env.CIRCLE_TAG ?: gitVersion()
group 'com.palantir.tracing'
}

Expand All @@ -68,7 +68,6 @@ subprojects {

tasks.withType(JavaCompile) {
options.compilerArgs += ['-Xlint:deprecation', '-Werror']
options.errorprone.disableWarningsInGeneratedCode = true
options.errorprone.errorproneArgs += [
'-Werror',
'-Xlint:deprecation',
Expand Down
5 changes: 5 additions & 0 deletions changelog/3.2.0-rc1/pr-235.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: feature
feature:
description: Create new tracing-test-utils package which allows you to render traces and snapshot test them.
links:
- https://github.com/palantir/tracing-java/pull/235
44 changes: 44 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- **com.palantir.tracing:tracing-okhttp3** - `OkhttpTraceInterceptor`, which adds the appropriate headers to outgoing requests.
- **com.palantir.tracing:tracing-jersey** - `TraceEnrichingFilter`, a jaxrs filter which reads headers from incoming requests and writes headers to outgoing responses. A traceId is stored in the jaxrs request context under the key `com.palantir.tracing.traceId`.
- **com.palantir.tracing:tracing-undertow** - `TracedOperationHandler`, an Undertow handler reads headers from incoming requests and writes headers to outgoing responses.
- **com.palantir.tracing:tracing-test-utils** - JUnit classes to render traces and also allow snapshot testing them.

Clients and servers propagate call trace ids across JVM boundaries according to the
[Zipkin](https://github.com/openzipkin/zipkin) specification. In particular, clients insert `X-B3-TraceId: <Trace ID>`
Expand Down Expand Up @@ -60,6 +61,49 @@ Note that span observers are static; a server typically subscribes span observer
Libraries should never register span observers (since they can trample observers registered by consumers of the library
whose themselves register observers).


## tracing-test-utils

You can set up 'snapshot testing' by adding the `@TestTracing` annotation to a test method (this requires JUnit 5).

```diff
import org.junit.jupiter.api.Test;
import com.palantir.tracing.TestTracing;

public class MyTest {
@Test
+ @TestTracing(snapshot = true)
public void foo() {
}
}
```

When you run this test for the first time, it will capture all spans and write them to a file `src/test/resources/tracing/MyTest/foo`, which should be checked-in to Git. This file will be used as a 'golden master', and all future runs will be compared against it.

```
{"traceId":"7e1014caf8a7278e","parentSpanId":"972f9b3a09431b67","spanId":"f701b7f815176ec2","operation":"healthcheck: SERVICE_DEPENDENCY","startTimeMicroSeconds":1566902887342052,"durationNanoSeconds":20377272,"metadata":{}}
...
```

If your production code changes and starts producing different spans, the test will fail and render two HTML visualizations: `expected.html` and `actual.html`.

Snapshot-testing is not available in JUnit4, but you can still see a HTML visualization of your traces using the `RenderTracingRule`:

```diff
import org.junit.Test;
import com.palantir.tracing.RenderTracingRule;

public class MyTest {

+ @Rule
+ public final RenderTracingRule rule = new RenderTracingRule();

@Test
public void foo() {
}
}
```

## License

This repository is made available under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ include 'tracing-jersey'
include 'tracing-servlet'
include 'tracing-undertow'
include 'tracing-benchmarks'
include 'tracing-test-utils'
25 changes: 25 additions & 0 deletions tracing-test-utils/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apply from: "${rootDir}/gradle/publish-jar.gradle"

versionsLock {
testProject()
}

dependencies {
implementation project(":tracing")
compileOnly 'junit:junit'
compileOnly 'org.junit.jupiter:junit-jupiter-api'
implementation 'com.palantir.conjure.java.runtime:conjure-java-jackson-serialization'
implementation 'com.spotify.dataenum:dataenum'

annotationProcessor 'com.spotify.dataenum:dataenum-processor'
annotationProcessor "org.immutables:value"
compileOnly "org.immutables:value::annotations"

testImplementation "junit:junit"
testCompileOnly "org.immutables:value::annotations"
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly("org.junit.vintage:junit-vintage-engine") {
because 'allows JUnit 3 and JUnit 4 tests to run'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.tracing;

import com.palantir.tracing.api.Span;
import com.spotify.dataenum.DataEnum;
import com.spotify.dataenum.dataenum_case;
import java.util.List;

@SuppressWarnings("checkstyle:TypeName")
@DataEnum
interface ComparisonFailure_dataenum {
dataenum_case unequalOperation(Span expected, Span actual);

dataenum_case unequalChildren(
Span expected, Span actual, List<Span> expectedChildren, List<Span> actualChildren);

dataenum_case incompatibleStructure(Span expected, Span actual);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.tracing;

import com.google.common.collect.ImmutableMap;
import com.google.common.hash.Hashing;
import com.google.common.io.Resources;
import com.palantir.tracing.api.Serialization;
import com.palantir.tracing.api.Span;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Comparator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.immutables.value.Value;

final class HtmlFormatter {
private RenderConfig config;

private HtmlFormatter(RenderConfig config) {
this.config = config;
}

@Value.Immutable
interface RenderConfig {
Collection<Span> spans();
Path path();
String displayName();
LayoutStrategy layoutStrategy();
Set<String> problemSpanIds();

@Value.Derived
default TimeBounds bounds() {
return TimeBounds.fromSpans(spans());
}

class Builder extends ImmutableRenderConfig.Builder {}

static Builder builder() {
return new Builder();
}
}

public static void render(RenderConfig config) throws IOException {
HtmlFormatter formatter = new HtmlFormatter(config);

StringBuilder sb = new StringBuilder();
formatter.header(sb);

switch (config.layoutStrategy()) {
case CHRONOLOGICAL:
formatter.renderChronological(sb);
break;
case SPLIT_BY_TRACE:
formatter.renderSplitByTraceId(sb);
break;
}

formatter.rawSpanJson(sb);

Files.write(config.path(), sb.toString().getBytes(StandardCharsets.UTF_8));
}

private void renderChronological(StringBuilder sb) {
config.spans().stream()
.sorted(Comparator.comparingLong(Span::getStartTimeMicroSeconds))
.forEachOrdered(span -> formatSpan(span, false, sb));
}

private void renderSplitByTraceId(StringBuilder sb) {
Map<String, SpanAnalyzer.Result> analyzedByTraceId = SpanAnalyzer.analyzeByTraceId(config.spans());
analyzedByTraceId.entrySet()
.stream()
.sorted(Comparator.comparingLong(e1 -> e1.getValue().bounds().startMicros()))
.forEachOrdered(entry -> {
SpanAnalyzer.Result analysis = entry.getValue();
renderAllSpansForOneTraceId(entry.getKey(), analysis, sb);
});
}

private void renderAllSpansForOneTraceId(String traceId, SpanAnalyzer.Result analysis, StringBuilder sb) {
sb.append("<div style=\"border-top: 1px solid #E1E8ED\" title=\"" + traceId + "\">\n");
analysis.orderedSpans()
.stream()
.filter(s -> !SpanAnalyzer.isSyntheticRoot(s))
.forEach(span -> {
boolean suspectedCollision = analysis.collisions().contains(span);
formatSpan(span, suspectedCollision, sb);
});
sb.append("</div>\n");
}

private void header(StringBuilder sb) throws IOException {
OffsetDateTime startTime = Instant.ofEpochMilli(
TimeUnit.MILLISECONDS.convert(config.bounds().startMicros(), TimeUnit.MICROSECONDS))
.atOffset(ZoneOffset.UTC);
OffsetDateTime endTime = Instant.ofEpochMilli(
TimeUnit.MILLISECONDS.convert(config.bounds().endNanos(), TimeUnit.NANOSECONDS))
.atOffset(ZoneOffset.UTC);
sb.append(template("header.html", ImmutableMap.<String, String>builder()
.put("{{DISPLAY_NAME}}", config.displayName())
.put("{{START_TIME}}", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(startTime))
.put("{{END_TIME}}", DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(endTime))
.build()));
}

private void formatSpan(Span span, boolean suspectedCollision, StringBuilder sb) {
long transposedStartMicros = span.getStartTimeMicroSeconds() - config.bounds().startMicros();

long hue = Hashing.adler32().hashString(span.getTraceId(), StandardCharsets.UTF_8).padToLong() % 360;

sb.append(template("span.html", ImmutableMap.<String, String>builder()
.put("{{LEFT}}", Float.toString(Utils.percent(
transposedStartMicros, config.bounds().durationMicros())))
.put("{{WIDTH}}", Float.toString(Utils.percent(
span.getDurationNanoSeconds(), config.bounds().durationNanos())))
.put("{{HUE}}", Long.toString(hue))
.put("{{SPANID}}", span.getSpanId())
.put("{{CLASS}}", config.problemSpanIds().contains(span.getSpanId()) ? "problem-span" : "")
.put("{{START}}", Utils.renderDuration(transposedStartMicros, TimeUnit.MICROSECONDS))
.put("{{FINISH}}", Utils.renderDuration(transposedStartMicros + TimeUnit.MICROSECONDS.convert(
span.getDurationNanoSeconds(),
TimeUnit.NANOSECONDS), TimeUnit.MICROSECONDS))
.put("{{OPERATION}}", span.getOperation())
.put("{{DURATION}}", Utils.renderDuration(span.getDurationNanoSeconds(), TimeUnit.NANOSECONDS))
.put("{{COLLISION}}", suspectedCollision ? " (collision)" : "")
.build()));
}


private void rawSpanJson(StringBuilder sb) {
sb.append("\n<pre style=\"background: #CED9E0;"
+ "color: #738694;"
+ "padding: 30px;"
+ "overflow-x: scroll;"
+ "margin-top: 100px;\">");
config.spans().stream()
.sorted(Comparator.comparingLong(Span::getStartTimeMicroSeconds))
.forEach(s -> {
sb.append('\n');
sb.append(Serialization.toString(s));
});
sb.append("\n</pre>");
}

private static String template(String resourceName, Map<String, String> values) {
try {
String template = Resources.toString(Resources.getResource(resourceName), StandardCharsets.UTF_8);
for (Map.Entry<String, String> entry : values.entrySet()) {
template = template.replace(entry.getKey(), entry.getValue());
}
return template;
} catch (IOException e) {
throw new UncheckedIOException("Unable to read resource " + resourceName, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* (c) Copyright 2019 Palantir Technologies Inc. All rights reserved.
*
* 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 com.palantir.tracing;

public enum LayoutStrategy {
CHRONOLOGICAL,
SPLIT_BY_TRACE
}
Loading