Skip to content

Commit 76b2d92

Browse files
authored
W3C : Tracecontext classes (#775)
* W3C Tracecontext * adding comments * addressing PR comments
1 parent c135c15 commit 76b2d92

File tree

4 files changed

+371
-0
lines changed

4 files changed

+371
-0
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.microsoft.applicationinsights.web.internal.correlation.tracecontext;
2+
3+
import java.util.concurrent.ThreadLocalRandom;
4+
5+
import com.google.common.annotations.VisibleForTesting;
6+
import org.apache.http.annotation.Experimental;
7+
8+
/**
9+
* This class represents the Traceparent data structure based on
10+
*
11+
* @author Reily Yang
12+
* @author Dhaval Doshi
13+
* @link https://github.com/w3c/trace-context/blob/master/trace_context/HTTP_HEADER_FORMAT.md
14+
*/
15+
@Experimental
16+
public class Traceparent {
17+
18+
/**
19+
* Version number between range [0,255] inclusive
20+
*/
21+
@VisibleForTesting
22+
final int version;
23+
24+
/**
25+
* 16 byte trace-id that is used to uniquely identify a distributed trace
26+
*/
27+
@VisibleForTesting
28+
final String traceId;
29+
30+
/**
31+
* It is a 8 byte ID that represents the caller span
32+
*/
33+
@VisibleForTesting
34+
final String spanId;
35+
36+
/**
37+
* An 8-bit field that controls tracing flags such as sampling, trace level etc.
38+
*/
39+
@VisibleForTesting
40+
final int traceFlags;
41+
42+
private Traceparent(int version, String traceId, String spanId, int traceFlags, boolean check) {
43+
if (check) {
44+
validate(version, traceId, spanId, traceFlags);
45+
}
46+
this.version = version;
47+
this.traceId = traceId;
48+
this.spanId = spanId;
49+
this.traceFlags = traceFlags;
50+
}
51+
52+
/**
53+
* The constructor that tries to create Traceparent Object from given version, traceId, spanID
54+
* and traceFlags.
55+
*/
56+
public Traceparent(int version, String traceId, String spanId, int traceFlags) {
57+
this(version, traceId != null ? traceId : randomHex(16),
58+
spanId != null ? spanId : randomHex(8),
59+
traceFlags, true);
60+
}
61+
62+
/**
63+
* This constructor creates a new Traceparent object having new traceId. It should only be used
64+
* if the call is the starting point of distributed trace.
65+
*/
66+
public Traceparent() {
67+
this(0, randomHex(16), randomHex(8), 0, false);
68+
}
69+
70+
public String getTraceId() {
71+
return traceId;
72+
}
73+
74+
public int getTraceFlags() {
75+
return traceFlags;
76+
}
77+
78+
public String getSpanId() {
79+
return spanId;
80+
}
81+
82+
/**
83+
* Validates the given input based on W3C specifications.
84+
*/
85+
private static void validate(int version, String traceId, String spanId, int traceFlags)
86+
throws IllegalArgumentException {
87+
if (version < 0 || version > 254) {
88+
throw new IllegalArgumentException("version must be within range [0, 255)");
89+
}
90+
if (!isHex(traceId, 32)) {
91+
throw new IllegalArgumentException("invalid traceId");
92+
}
93+
if (traceId.equals("00000000000000000000000000000000")) {
94+
throw new IllegalArgumentException("invalid traceId");
95+
}
96+
if (!isHex(spanId, 16)) {
97+
throw new IllegalArgumentException("invalid spanId");
98+
}
99+
if (spanId.equals("0000000000000000")) {
100+
throw new IllegalArgumentException("invalid spanId");
101+
}
102+
if (traceFlags < 0 || traceFlags > 255) {
103+
throw new IllegalArgumentException("traceFlags must be within range [0, 255]");
104+
}
105+
}
106+
107+
/**
108+
* Converts the Traceparent object to header format Eg: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
109+
*
110+
* @return traceparent
111+
*/
112+
@Override
113+
public String toString() {
114+
return String.format("%02x-%s-%s-%02x", version, traceId, spanId, traceFlags);
115+
}
116+
117+
/**
118+
* Helper method to create a random hexadecimal string of n bytes.
119+
*
120+
* @return n byte hexadecimal string
121+
*/
122+
@VisibleForTesting
123+
static String randomHex(int n) {
124+
byte[] bytes = new byte[n];
125+
ThreadLocalRandom.current().nextBytes(bytes);
126+
StringBuilder sb = new StringBuilder();
127+
for (byte b : bytes) {
128+
sb.append(String.format("%02x", b));
129+
}
130+
return sb.toString();
131+
}
132+
133+
/**
134+
* Helper method to check if a given string of n bytes is hexadecimal
135+
*
136+
* @return boolean
137+
*/
138+
private static boolean isHex(String s, int n) {
139+
if (s == null || s.length() == 0) {
140+
return false;
141+
}
142+
if (s.length() != n) {
143+
return false;
144+
}
145+
for (int i = 0; i < n; i++) {
146+
char c = s.charAt(i);
147+
if ('0' <= c && c <= '9') {
148+
continue;
149+
}
150+
if ('a' <= c && c <= 'f') {
151+
continue;
152+
}
153+
return false;
154+
}
155+
return true;
156+
}
157+
158+
/**
159+
* converts traceparent from String to Traceparent object
160+
*
161+
* @return Traceparent
162+
*/
163+
public static Traceparent fromString(String s) {
164+
if (s == null || s.length() == 0) {
165+
return null;
166+
}
167+
String[] arr = s.split("-");
168+
if (arr.length != 4) {
169+
return null;
170+
}
171+
if (!isHex(arr[0], 2)) {
172+
return null;
173+
}
174+
if (!isHex(arr[3], 2)) {
175+
return null;
176+
}
177+
178+
return new Traceparent(
179+
(Character.digit(arr[0].charAt(0), 16) << 4) + Character.digit(arr[0].charAt(1), 16),
180+
arr[1],
181+
arr[2],
182+
(Character.digit(arr[3].charAt(0), 16) << 4) + Character.digit(arr[3].charAt(1), 16));
183+
}
184+
185+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.microsoft.applicationinsights.web.internal.correlation.tracecontext;
2+
3+
import org.apache.http.annotation.Experimental;
4+
5+
/**
6+
* Class that represents Tracestate header based on
7+
*
8+
* @author Reiley Yang
9+
* @author Dhaval Doshi
10+
* @link https://github.com/w3c/trace-context/blob/master/trace_context/HTTP_HEADER_FORMAT.md
11+
*
12+
* Implementations can add vendor specific details here.
13+
*/
14+
@Experimental
15+
public class Tracestate {
16+
17+
/**
18+
* Field to capture tracestate header
19+
*/
20+
public final String reserved;
21+
22+
/**
23+
* Ctor that creates tracestate object from given value
24+
*/
25+
public Tracestate(String value) {
26+
if (value == null || value.length() == 0) {
27+
throw new IllegalArgumentException("invalid spanId");
28+
}
29+
reserved = value;
30+
}
31+
32+
@Override
33+
public String toString() {
34+
return reserved;
35+
}
36+
37+
/**
38+
* Converts Tracestate header to Object representation
39+
*
40+
* @return Tracestate
41+
*/
42+
public static Tracestate fromString(String s) {
43+
return new Tracestate(s);
44+
}
45+
46+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.microsoft.applicationinsights.web.internal.correlation.tracecontext;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
6+
public class TraceparentTests {
7+
8+
@Test
9+
public void canCreateValidTraceParentWithDefaultConstructor() {
10+
Traceparent traceparent = new Traceparent();
11+
Assert.assertNotNull(traceparent.traceId);
12+
Assert.assertNotNull(traceparent.spanId);
13+
Assert.assertEquals(0, traceparent.version);
14+
Assert.assertNotNull(traceparent.traceFlags);
15+
}
16+
17+
@Test
18+
public void testTraceParentUniqueness() {
19+
Traceparent t1 = new Traceparent();
20+
Traceparent t2 = new Traceparent();
21+
Assert.assertNotEquals(t1.traceId, t2.traceId);
22+
Assert.assertNotEquals(t1.spanId, t2.spanId);
23+
24+
// version is always 0
25+
Assert.assertEquals(t1.version, t2.version);
26+
27+
// flags are currently 0, may change in future
28+
Assert.assertEquals(t1.traceFlags, t2.traceFlags);
29+
30+
}
31+
32+
@Test
33+
public void canCreateTraceParentWithProvidedValues() {
34+
String traceId = Traceparent.randomHex(16);
35+
String spanId = Traceparent.randomHex(8);
36+
Traceparent t1 = new Traceparent(0, traceId, spanId, 0);
37+
Assert.assertEquals(traceId, t1.traceId);
38+
Assert.assertEquals(spanId, t1.spanId);
39+
Assert.assertEquals(0, t1.version);
40+
Assert.assertEquals(0, t1.traceFlags);
41+
}
42+
43+
@Test(expected = IllegalArgumentException.class)
44+
public void throwsWhenCreatingTraceParentWithIllegalTraceId() {
45+
String invalidTraceId = Traceparent.randomHex(32);
46+
String spanId = Traceparent.randomHex(8);
47+
Traceparent t1 = new Traceparent(0, invalidTraceId, spanId, 0);
48+
}
49+
50+
@Test(expected = IllegalArgumentException.class)
51+
public void throwsWhenCreatingTraceParentWithIllegalSpanId() {
52+
String traceId = Traceparent.randomHex(16);
53+
String invalidSpanId = Traceparent.randomHex(16);
54+
Traceparent t1 = new Traceparent(0, traceId, invalidSpanId, 0);
55+
}
56+
57+
@Test(expected = IllegalArgumentException.class)
58+
public void throwsWhenVersionNumberIsOutOfRange() {
59+
String traceId = Traceparent.randomHex(16);
60+
String spanId = Traceparent.randomHex(8);
61+
Traceparent t1 = new Traceparent(256, traceId, spanId, 0);
62+
}
63+
64+
@Test(expected = IllegalArgumentException.class)
65+
public void throwsWhenVersionNumberIsOutOfLowerRange() {
66+
String traceId = Traceparent.randomHex(16);
67+
String spanId = Traceparent.randomHex(8);
68+
Traceparent t1 = new Traceparent(-1, traceId, spanId, 0);
69+
}
70+
71+
@Test(expected = IllegalArgumentException.class)
72+
public void throwsWhenFlagIsOutOfLowerRange() {
73+
String traceId = Traceparent.randomHex(16);
74+
String spanId = Traceparent.randomHex(8);
75+
Traceparent t1 = new Traceparent(0, traceId, spanId, -1);
76+
}
77+
78+
@Test(expected = IllegalArgumentException.class)
79+
public void throwsWhenFlagIsOutOfUpperRange() {
80+
String traceId = Traceparent.randomHex(16);
81+
String spanId = Traceparent.randomHex(8);
82+
Traceparent t1 = new Traceparent(0, traceId, spanId, 256);
83+
}
84+
85+
@Test
86+
public void canCreateTraceParentFromString() {
87+
String traceId = Traceparent.randomHex(16);
88+
String spanId = Traceparent.randomHex(8);
89+
Traceparent t1 = new Traceparent(0, traceId, spanId, 0);
90+
91+
Traceparent t2 = Traceparent.fromString(t1.toString());
92+
Assert.assertEquals(t1.version, t2.version);
93+
Assert.assertEquals(t1.traceId, t2.traceId);
94+
Assert.assertEquals(t1.spanId, t2.spanId);
95+
Assert.assertEquals(t1.traceFlags, t2.traceFlags);
96+
97+
// memory reference should be different
98+
Assert.assertFalse(t1 == t2);
99+
}
100+
101+
@Test(expected = IllegalArgumentException.class)
102+
public void throwsWhenTryingToCreateWithMalformedTraceparentString() {
103+
String invalidTraceId = Traceparent.randomHex(32);
104+
String invalidSpanId = Traceparent.randomHex(16);
105+
String invalidTraceparent = String.format("%02x-%s-%s-%02x", 0, invalidTraceId,
106+
invalidSpanId, 0);
107+
108+
Traceparent t1 = Traceparent.fromString(invalidTraceparent);
109+
}
110+
111+
@Test
112+
public void returnsNullTraceParentWhenTryingToCreateFromEmptyString() {
113+
Traceparent t1 = Traceparent.fromString("");
114+
Assert.assertNull(t1);
115+
}
116+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.microsoft.applicationinsights.web.internal.correlation.tracecontext;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
6+
public class TracestateTests {
7+
8+
@Test(expected = IllegalArgumentException.class)
9+
public void throwsWhenTracestateIsNull() {
10+
new Tracestate(null);
11+
}
12+
13+
@Test(expected = IllegalArgumentException.class)
14+
public void throwsWhenTracestateIsEmpty() {
15+
new Tracestate("");
16+
}
17+
18+
@Test
19+
public void canCreateTraceStateWithString() {
20+
String tracestate = "az=cid-v1:120";
21+
Tracestate t1 = new Tracestate(tracestate);
22+
Assert.assertEquals(tracestate, t1.reserved);
23+
}
24+
}

0 commit comments

Comments
 (0)