Skip to content

Commit

Permalink
Adds B3SinglePropagation for the "b3" header
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
Adrian Cole committed Aug 21, 2018
1 parent 3e138ba commit da2946c
Show file tree
Hide file tree
Showing 7 changed files with 486 additions and 7 deletions.
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();
}
}
15 changes: 15 additions & 0 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 brave.propagation.B3SinglePropagation.B3SingleExtractor;
import java.util.Arrays;
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;

/**
* Implements <a href="https://github.com/openzipkin/b3-propagation">B3 Propagation</a>
Expand Down Expand Up @@ -46,6 +49,8 @@ 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 lowerKey; // only for extraction
final K upperKey; // only for extraction
final K traceIdKey;
final K spanIdKey;
final K parentSpanIdKey;
Expand All @@ -54,6 +59,8 @@ public final class B3Propagation<K> implements Propagation<K> {
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);
Expand Down Expand Up @@ -104,15 +111,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 != 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
168 changes: 168 additions & 0 deletions brave/src/main/java/brave/propagation/B3SingleFormat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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 == Boolean.TRUE) ? '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) {
int flags = parseSampledFlag(b3, 0);
if (flags == 0) return null;
return TraceContextOrSamplingFlags.create(SamplingFlags.toSamplingFlags(flags));
} else if (length == 3) {
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;
}

int pos = 0;
long traceIdHigh, traceId;
if (b3.charAt(32) == '-') {
traceIdHigh = lenientLowerHexToUnsignedLong(b3, 0, 16);
traceId = lenientLowerHexToUnsignedLong(b3, 16, 32);
pos += 33; // traceid128-
} else {
traceIdHigh = 0L;
traceId = lenientLowerHexToUnsignedLong(b3, 0, 16);
pos += 17; // traceid64-
}

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

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 da2946c

Please sign in to comment.