Skip to content

Commit

Permalink
Adds B3SinglePropagation for the "b3" header (#763)
Browse files Browse the repository at this point in the history
This will be used for JMS and other propagation formats such as w3c
tracestate entry. It is notably more efficient than multi-header.

Example header: `b3: 4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1`

To aid in migration, this teaches the normal B3 to attempt to extract
single header variants as well.

See openzipkin/b3-propagation#21
  • Loading branch information
adriancole committed Aug 22, 2018
1 parent 9526c92 commit 4ade65c
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 14 deletions.
14 changes: 14 additions & 0 deletions brave-tests/src/test/java/brave/propagation/B3PropagationTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package brave.propagation;

import brave.internal.HexCodec;
import brave.internal.Nullable;
import brave.test.propagation.PropagationTest;
import java.util.Map;
Expand Down Expand Up @@ -84,4 +85,17 @@ public class B3PropagationTest extends PropagationTest<String> {
assertThat(result.sampled())
.isTrue();
}

@Test public void extractTraceContext_singleHeaderFormat() {
MapEntry mapEntry = new MapEntry();

map.put("b3", "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7");

TraceContext result = propagation().extractor(mapEntry).extract(map).context();

assertThat(result.traceIdString())
.isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736");
assertThat(HexCodec.toLowerHex(result.spanId()))
.isEqualTo("00f067aa0ba902b7");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package brave.propagation;

import brave.internal.Nullable;
import brave.test.propagation.PropagationTest;
import java.util.Map;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class B3SinglePropagationTest extends PropagationTest<String> {
@Override protected Propagation<String> propagation() {
return Propagation.B3_SINGLE_STRING;
}

@Override protected void inject(Map<String, String> map, @Nullable String traceId,
@Nullable String parentId, @Nullable String spanId, @Nullable Boolean sampled,
@Nullable Boolean debug) {
StringBuilder builder = new StringBuilder();
if (traceId == null) {
if (sampled != null) {
builder.append(sampled ? '1' : '0');
if (debug != null) builder.append(debug ? "-1" : "-0");
} else if (Boolean.TRUE.equals(debug)) {
builder.append("1-1");
}
} else {
builder.append(traceId).append('-').append(spanId);
if (sampled != null) builder.append(sampled ? "-1" : "-0");
if (parentId != null) builder.append('-').append(parentId);
if (debug != null) builder.append(debug ? "-1" : "-0");
}
if (builder.length() != 0) map.put("b3", builder.toString());
}

@Override protected void inject(Map<String, String> carrier, SamplingFlags flags) {
if (flags.debug()) {
carrier.put("b3", "1-1");
} else if (flags.sampled() != null) {
carrier.put("b3", flags.sampled() ? "1" : "0");
}
}

@Test public void extractTraceContext_sampledFalse() {
MapEntry mapEntry = new MapEntry();
map.put("b3", "0");

SamplingFlags result = propagation().extractor(mapEntry).extract(map).samplingFlags();

assertThat(result)
.isEqualTo(SamplingFlags.NOT_SAMPLED);
}

@Test public void extractTraceContext_sampledFalseUpperCase() {
MapEntry mapEntry = new MapEntry();
map.put("B3", "0");

SamplingFlags result = propagation().extractor(mapEntry).extract(map).samplingFlags();

assertThat(result)
.isEqualTo(SamplingFlags.NOT_SAMPLED);
}

@Test public void extractTraceContext_malformed() {
MapEntry mapEntry = new MapEntry();
map.put("b3", "not-a-tumor");

SamplingFlags result = propagation().extractor(mapEntry).extract(map).samplingFlags();

assertThat(result)
.isEqualTo(SamplingFlags.EMPTY);
}

@Test public void extractTraceContext_malformed_uuid() {
MapEntry mapEntry = new MapEntry();
map.put("b3", "b970dafd-0d95-40aa-95d8-1d8725aebe40");

SamplingFlags result = propagation().extractor(mapEntry).extract(map).samplingFlags();

assertThat(result)
.isEqualTo(SamplingFlags.EMPTY);
}

@Test public void extractTraceContext_debug_with_ids() {
MapEntry mapEntry = new MapEntry();

map.put("b3", "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1-1");

TraceContext result = propagation().extractor(mapEntry).extract(map).context();

assertThat(result.debug())
.isTrue();
}
}
23 changes: 16 additions & 7 deletions brave/src/main/java/brave/propagation/B3Propagation.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package brave.propagation;

import java.util.Arrays;
import brave.propagation.B3SinglePropagation.B3SingleExtractor;
import java.util.Collections;
import java.util.List;

import static brave.internal.HexCodec.toLowerHex;
import static brave.propagation.B3SinglePropagation.LOWER_NAME;
import static brave.propagation.B3SinglePropagation.UPPER_NAME;
import static java.util.Arrays.asList;

/**
* Implements <a href="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/openzipkin/b3-propagation">B3 Propagation</a>
Expand Down Expand Up @@ -46,21 +49,19 @@ public final class B3Propagation<K> implements Propagation<K> {
* "1" implies sampled and is a request to override collection-tier sampling policy.
*/
static final String FLAGS_NAME = "X-B3-Flags";
final K traceIdKey;
final K spanIdKey;
final K parentSpanIdKey;
final K sampledKey;
final K debugKey;
final K lowerKey, upperKey, traceIdKey, spanIdKey, parentSpanIdKey, sampledKey, debugKey;
final List<K> fields;

B3Propagation(KeyFactory<K> keyFactory) {
this.lowerKey = keyFactory.create(LOWER_NAME);
this.upperKey = keyFactory.create(UPPER_NAME);
this.traceIdKey = keyFactory.create(TRACE_ID_NAME);
this.spanIdKey = keyFactory.create(SPAN_ID_NAME);
this.parentSpanIdKey = keyFactory.create(PARENT_SPAN_ID_NAME);
this.sampledKey = keyFactory.create(SAMPLED_NAME);
this.debugKey = keyFactory.create(FLAGS_NAME);
this.fields = Collections.unmodifiableList(
Arrays.asList(traceIdKey, spanIdKey, parentSpanIdKey, sampledKey, debugKey)
asList(lowerKey, upperKey, traceIdKey, spanIdKey, parentSpanIdKey, sampledKey, debugKey)
);
}

Expand Down Expand Up @@ -104,15 +105,23 @@ static final class B3Injector<C, K> implements TraceContext.Injector<C> {

static final class B3Extractor<C, K> implements TraceContext.Extractor<C> {
final B3Propagation<K> propagation;
final B3SingleExtractor<C, K> singleExtractor;
final Getter<C, K> getter;

B3Extractor(B3Propagation<K> propagation, Getter<C, K> getter) {
this.propagation = propagation;
this.singleExtractor =
new B3SingleExtractor<>(propagation.lowerKey, propagation.upperKey, getter);
this.getter = getter;
}

@Override public TraceContextOrSamplingFlags extract(C carrier) {
if (carrier == null) throw new NullPointerException("carrier == null");

// try to extract single-header format
TraceContextOrSamplingFlags extracted = singleExtractor.extract(carrier);
if (!extracted.equals(TraceContextOrSamplingFlags.EMPTY)) return extracted;

// Start by looking at the sampled state as this is used regardless
// Official sampled value is 1, though some old instrumentation send true
String sampled = getter.get(carrier, propagation.sampledKey);
Expand Down
166 changes: 166 additions & 0 deletions brave/src/main/java/brave/propagation/B3SingleFormat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package brave.propagation;

import brave.internal.Nullable;
import java.util.Collections;
import java.util.logging.Logger;

import static brave.internal.HexCodec.lenientLowerHexToUnsignedLong;
import static brave.internal.HexCodec.writeHexLong;
import static brave.internal.TraceContexts.FLAG_DEBUG;
import static brave.internal.TraceContexts.FLAG_SAMPLED;
import static brave.internal.TraceContexts.FLAG_SAMPLED_SET;
import static java.util.logging.Level.FINE;

/** Implements the progation format described in {@link B3SinglePropagation}. */
final class B3SingleFormat {
static final Logger logger = Logger.getLogger(B3SingleFormat.class.getName());
static final int FORMAT_MAX_LENGTH = 32 + 1 + 16 + 2 + 16 + 2; // traceid128-spanid-1-parentid-1

static String writeB3SingleFormat(TraceContext context) {
char[] result = getCharBuffer();
int pos = 0;
long traceIdHigh = context.traceIdHigh();
if (traceIdHigh != 0L) {
writeHexLong(result, pos, traceIdHigh);
pos += 16;
}
writeHexLong(result, pos, context.traceId());
pos += 16;
result[pos++] = '-';
writeHexLong(result, pos, context.spanId());
pos += 16;

Boolean sampled = context.sampled();
if (sampled != null) {
result[pos++] = '-';
result[pos++] = sampled ? '1' : '0';
}

long b3Id = context.parentIdAsLong();
if (b3Id != 0) {
result[pos++] = '-';
writeHexLong(result, pos, b3Id);
pos += 16;
}

if (context.debug()) {
result[pos++] = '-';
result[pos++] = '1';
}
return new String(result, 0, pos);
}

static @Nullable TraceContextOrSamplingFlags maybeB3SingleFormat(String b3) {
int length = b3.length();
if (length == 1) { // assume just tracing flag. ex "b3: 1"
int flags = parseSampledFlag(b3, 0);
if (flags == 0) return null;
return TraceContextOrSamplingFlags.create(SamplingFlags.toSamplingFlags(flags));
} else if (length == 3) { // assume tracing + debug flag. ex "b3: 1-1"
int flags = parseSampledFlag(b3, 0);
if (flags == 0) return null;
flags = parseDebugFlag(b3, 2, flags);
if (flags == 0) return null;
return TraceContextOrSamplingFlags.create(SamplingFlags.toSamplingFlags(flags));
}

// At this point we minimally expect a traceId-spanId pair
if (length < 16 + 1 + 16 /* traceid64-spanid */) {
logger.fine("Invalid input: truncated");
return null;
} else if (length > FORMAT_MAX_LENGTH) {
logger.fine("Invalid input: too long");
return null;
}

long traceIdHigh, traceId;
boolean traceId128 = b3.charAt(32) == '-';
if (traceId128) {
traceIdHigh = lenientLowerHexToUnsignedLong(b3, 0, 16);
traceId = lenientLowerHexToUnsignedLong(b3, 16, 32);
} else {
traceIdHigh = 0L;
traceId = lenientLowerHexToUnsignedLong(b3, 0, 16);
}

if (traceIdHigh == 0L && traceId == 0L) {
logger.fine("Invalid input: expected a 16 or 32 lower hex trace ID at offset 0");
return null;
}

int pos = traceId128 ? 33 : 17; // traceid-
long spanId = lenientLowerHexToUnsignedLong(b3, pos, pos + 16);
if (spanId == 0L) {
logger.log(FINE, "Invalid input: expected a 16 lower hex span ID at offset {0}", pos);
return null;
}
pos += 17; // spanid-

int flags = 0;
if (length == pos + 1 || (length > pos + 2 && b3.charAt(pos + 1) == '-')) {
flags = parseSampledFlag(b3, pos++);
if (flags == 0) return null;
pos++; // skip the dash
}

long parentId = 0L;
if (length >= pos + 17) {
parentId = lenientLowerHexToUnsignedLong(b3, pos, pos + 16);
if (parentId == 0L) {
logger.log(FINE, "Invalid input: expected a 16 lower hex parent ID at offset {0}", pos);
return null;
}
pos += 17;
}

if (length == pos + 1) {
flags = parseDebugFlag(b3, pos, flags);
if (flags == 0) return null;
}

return TraceContextOrSamplingFlags.create(new TraceContext(
flags,
traceIdHigh,
traceId,
parentId,
spanId,
Collections.emptyList()
));
}

static int parseSampledFlag(String b3, int pos) {
int flags;
char sampledChar = b3.charAt(pos);
if (sampledChar == '1') {
flags = FLAG_SAMPLED_SET | FLAG_SAMPLED;
} else if (sampledChar == '0') {
flags = FLAG_SAMPLED_SET;
} else {
logger.log(FINE, "Invalid input: expected 0 or 1 for sampled at offset {0}", pos);
flags = 0;
}
return flags;
}

static int parseDebugFlag(String b3, int pos, int flags) {
char lastChar = b3.charAt(pos);
if (lastChar == '1') {
flags = FLAG_DEBUG | FLAG_SAMPLED_SET | FLAG_SAMPLED;
} else if (lastChar != '0') { // redundant to say debug false, but whatev
logger.log(FINE, "Invalid input: expected 1 for debug at offset {0}", pos);
flags = 0;
}
return flags;
}

static final ThreadLocal<char[]> CHAR_BUFFER = new ThreadLocal<>();

static char[] getCharBuffer() {
char[] charBuffer = CHAR_BUFFER.get();
if (charBuffer == null) {
charBuffer = new char[FORMAT_MAX_LENGTH];
CHAR_BUFFER.set(charBuffer);
}
return charBuffer;
}
}
Loading

0 comments on commit 4ade65c

Please sign in to comment.