Skip to content

Commit

Permalink
Exposes B3SingleFormat after getting meticulous about parsing (#769)
Browse files Browse the repository at this point in the history
This adds a huge amount of state checks to understand the source of
malform problems. This exposes B3SingleFormat methods so they can be
used in other code, such as w3c tracestate or JMS directly.

See #763
  • Loading branch information
adriancole committed Aug 23, 2018
1 parent 4ade65c commit 4758c1f
Show file tree
Hide file tree
Showing 3 changed files with 450 additions and 74 deletions.
247 changes: 199 additions & 48 deletions brave/src/main/java/brave/propagation/B3SingleFormat.java
Original file line number Original file line Diff line number Diff line change
@@ -1,23 +1,96 @@
package brave.propagation; package brave.propagation;


import brave.internal.HexCodec;
import brave.internal.Nullable; import brave.internal.Nullable;
import java.nio.ByteBuffer;
import java.util.Collections; import java.util.Collections;
import java.util.logging.Logger; import java.util.logging.Logger;


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


/** Implements the progation format described in {@link B3SinglePropagation}. */ /**
final class B3SingleFormat { * This format corresponds to the propagation key "b3" (or "B3"), which delimits fields in the
* following manner.
*
* <pre>{@code
* b3: {x-b3-traceid}-{x-b3-spanid}-{x-b3-sampled}-{x-b3-parentspanid}-{x-b3-flags}
* }</pre>
*
* <p>For example, a sampled root span would look like:
* {@code 4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1}
*
* <p>Like normal B3, it is valid to omit trace identifiers in order to only propagate a sampling
* decision. For example, the following are valid downstream hints:
* <ul>
* <li>sampled - {@code b3: 1}</li>
* <li>unsampled - {@code b3: 0}</li>
* <li>debug - {@code b3: 1-1}</li>
* </ul>
* Note: {@code b3: 0-1} isn't supported as it doesn't make sense. Debug boosts ordinary sampling
* decision to also affect the collector tier. {@code b3: 0-1} would be like saying, don't sample,
* except at the collector tier, which is impossible as if you don't sample locally the data will
* never arrive at a collector.
*
* <p>See <a href="https://github.com/openzipkin/b3-propagation">B3 Propagation</a>
*/
public final class B3SingleFormat {
static final Logger logger = Logger.getLogger(B3SingleFormat.class.getName()); 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 final int FORMAT_MAX_LENGTH = 32 + 1 + 16 + 2 + 16 + 2; // traceid128-spanid-1-parentid-1


static String writeB3SingleFormat(TraceContext context) { /**
char[] result = getCharBuffer(); * Writes all B3 defined fields in the trace context, except {@link TraceContext#parentIdAsLong()
* parent ID}, to a hyphen delimited string.
*
* <p>This is appropriate for receivers who understand "b3" single header format, and always do
* work in a child span. For example, message consumers always do work in child spans, so message
* producers can use this format to save bytes on the wire. On the other hand, RPC clients should
* use {@link #writeB3SingleFormat(TraceContext)} instead, as RPC servers often share a trace ID.
*/
public static String writeB3SingleFormatWithoutParentId(TraceContext context) {
char[] buffer = getCharBuffer();
int length = writeB3SingleFormat(context, 0L, buffer);
return new String(buffer, 0, length);
}

/**
* Like {@link #writeB3SingleFormatWithoutParentId(TraceContext)}, but for carriers with byte
* array or byte buffer values. For example, {@link ByteBuffer#wrap(byte[])} can wrap the result.
*/
public static byte[] writeB3SingleFormatWithoutParentIdAsBytes(TraceContext context) {
char[] buffer = getCharBuffer();
int length = writeB3SingleFormat(context, 0L, buffer);
return asciiToNewByteArray(buffer, length);
}

/**
* Writes all B3 defined fields in the trace context to a hyphen delimited string. This is
* appropriate for receivers who understand "b3" single header format.
*
* <p>The {@link TraceContext#parentIdAsLong() parent ID} is serialized in case the receiver is
* an RPC server. When downstream is known to be a messaging consumer, or a server that does not
* share trace IDs, prefer {@link #writeB3SingleFormatWithoutParentId(TraceContext)}.
*/
public static String writeB3SingleFormat(TraceContext context) {
char[] buffer = getCharBuffer();
int length = writeB3SingleFormat(context, context.parentIdAsLong(), buffer);
return new String(buffer, 0, length);
}

/**
* Like {@link #writeB3SingleFormatAsBytes(TraceContext)}, but for carriers with byte array or
* byte buffer values. For example, {@link ByteBuffer#wrap(byte[])} can wrap the result.
*/
public static byte[] writeB3SingleFormatAsBytes(TraceContext context) {
char[] buffer = getCharBuffer();
int length = writeB3SingleFormat(context, context.parentIdAsLong(), buffer);
return asciiToNewByteArray(buffer, length);
}

static int writeB3SingleFormat(TraceContext context, long parentId, char[] result) {
int pos = 0; int pos = 0;
long traceIdHigh = context.traceIdHigh(); long traceIdHigh = context.traceIdHigh();
if (traceIdHigh != 0L) { if (traceIdHigh != 0L) {
Expand All @@ -36,86 +109,118 @@ static String writeB3SingleFormat(TraceContext context) {
result[pos++] = sampled ? '1' : '0'; result[pos++] = sampled ? '1' : '0';
} }


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


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


static @Nullable TraceContextOrSamplingFlags maybeB3SingleFormat(String b3) { @Nullable
int length = b3.length(); public static TraceContextOrSamplingFlags parseB3SingleFormat(CharSequence b3) {
if (length == 1) { // assume just tracing flag. ex "b3: 1" return parseB3SingleFormat(b3, 0, b3.length());
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" * @param beginIndex the start index, inclusive
int flags = parseSampledFlag(b3, 0); * @param endIndex the end index, exclusive
if (flags == 0) return null; */
flags = parseDebugFlag(b3, 2, flags); @Nullable
if (flags == 0) return null; public static TraceContextOrSamplingFlags parseB3SingleFormat(CharSequence b3, int beginIndex,
return TraceContextOrSamplingFlags.create(SamplingFlags.toSamplingFlags(flags)); int endIndex) {
if (endIndex <= beginIndex + 3) { // possibly sampling flags
return decode(b3, beginIndex, endIndex);
} }


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


long traceIdHigh, traceId; long traceIdHigh, traceId;
boolean traceId128 = b3.charAt(32) == '-'; if (b3.charAt(pos + 32) == '-') {
if (traceId128) { traceIdHigh = tryParse16HexCharacters(b3, pos, endIndex);
traceIdHigh = lenientLowerHexToUnsignedLong(b3, 0, 16); pos += 16; // upper 64 bits of the trace ID
traceId = lenientLowerHexToUnsignedLong(b3, 16, 32); traceId = tryParse16HexCharacters(b3, pos, endIndex);
} else { } else {
traceIdHigh = 0L; traceIdHigh = 0L;
traceId = lenientLowerHexToUnsignedLong(b3, 0, 16); traceId = tryParse16HexCharacters(b3, pos, endIndex);
} }
pos += 16; // traceId
if (!checkHyphen(b3, pos++)) return null;


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


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


int flags = 0; 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; long parentId = 0L;
if (length >= pos + 17) { if (endIndex > pos) {
parentId = lenientLowerHexToUnsignedLong(b3, pos, pos + 16); // If we are at this point, we have more than just traceId-spanId.
if (parentId == 0L) { // If the sampling field is present, we'll have a delimiter 2 characters from now. Ex "-1"
logger.log(FINE, "Invalid input: expected a 16 lower hex parent ID at offset {0}", pos); // If it is absent, but a parent ID is (which is strange), we'll have at least 17 characters.
// Therefore, if we have less than two characters, the input is truncated.
if (endIndex == pos + 1) {
logger.fine("Invalid input: truncated");
return null; return null;
} }
pos += 17; if (!checkHyphen(b3, pos++)) return null;
}


if (length == pos + 1) { // If our position is at the end of the string, or another delimiter is one character past our
flags = parseDebugFlag(b3, pos, flags); // position, try to read sampled status.
if (flags == 0) return null; if (endIndex == pos + 1 || delimiterFollowsPos(b3, pos, endIndex)) {
flags = parseSampledFlag(b3, pos);
if (flags == 0) return null;
pos++; // consume the sampled status
}

if (endIndex > pos) {
// If we are at this point, we have only two possible fields left, parent and/or debug
// If the parent field is present, we'll have at least 17 characters. If it is absent, but debug
// is present, we'll have we'll have a delimiter 2 characters from now. Ex "-1"
// Therefore, if we have less than two characters, the input is truncated.
if (endIndex == pos + 1) {
logger.fine("Invalid input: truncated");
return null;
}

if (endIndex > pos + 2) {
if (!checkHyphen(b3, pos++)) return null;
parentId = tryParse16HexCharacters(b3, pos, endIndex);
if (parentId == 0L) {
logger.log(FINE, "Invalid input: expected a 16 lower hex parent ID at offset {0}", pos);
return null;
}
pos += 16;
}

// the only option at this point is that we have a debug flag
if (endIndex == pos + 2) {
if (!checkHyphen(b3, pos)) return null;
pos++; // consume the hyphen
flags = parseDebugFlag(b3, pos, flags);
if (flags == 0) return null;
}
}
} }


return TraceContextOrSamplingFlags.create(new TraceContext( return TraceContextOrSamplingFlags.create(new TraceContext(
Expand All @@ -128,7 +233,45 @@ static String writeB3SingleFormat(TraceContext context) {
)); ));
} }


static int parseSampledFlag(String b3, int pos) { @Nullable
static TraceContextOrSamplingFlags decode(CharSequence b3, int beginIndex, int endIndex) {
int pos = beginIndex;
if (pos == endIndex) { // empty
logger.log(FINE, "Invalid input: expected 0 or 1 for sampled at offset {0}", pos);
return null;
}

int flags = parseSampledFlag(b3, pos++);
if (flags == 0) return null;
if (endIndex > pos) {
if (!checkHyphen(b3, pos++)) return null;
if (endIndex == pos) {
logger.fine("Invalid input: truncated");
return null;
}
flags = parseDebugFlag(b3, pos, flags);
if (flags == 0) return null;
}
return TraceContextOrSamplingFlags.create(SamplingFlags.toSamplingFlags(flags));
}

static boolean checkHyphen(CharSequence b3, int pos) {
if (b3.charAt(pos) == '-') return true;
logger.log(FINE, "Invalid input: expected a hyphen(-) delimiter offset {0}", pos);
return false;
}

static boolean delimiterFollowsPos(CharSequence b3, int pos, int end) {
return (end >= pos + 2) && b3.charAt(pos + 1) == '-';
}

static long tryParse16HexCharacters(CharSequence lowerHex, int index, int end) {
int endIndex = index + 16;
if (endIndex > end) return 0L;
return HexCodec.lenientLowerHexToUnsignedLong(lowerHex, index, endIndex);
}

static int parseSampledFlag(CharSequence b3, int pos) {
int flags; int flags;
char sampledChar = b3.charAt(pos); char sampledChar = b3.charAt(pos);
if (sampledChar == '1') { if (sampledChar == '1') {
Expand All @@ -142,7 +285,7 @@ static int parseSampledFlag(String b3, int pos) {
return flags; return flags;
} }


static int parseDebugFlag(String b3, int pos, int flags) { static int parseDebugFlag(CharSequence b3, int pos, int flags) {
char lastChar = b3.charAt(pos); char lastChar = b3.charAt(pos);
if (lastChar == '1') { if (lastChar == '1') {
flags = FLAG_DEBUG | FLAG_SAMPLED_SET | FLAG_SAMPLED; flags = FLAG_DEBUG | FLAG_SAMPLED_SET | FLAG_SAMPLED;
Expand All @@ -153,6 +296,14 @@ static int parseDebugFlag(String b3, int pos, int flags) {
return flags; return flags;
} }


static byte[] asciiToNewByteArray(char[] buffer, int length) {
byte[] result = new byte[length];
for (int i = 0; i < length; i++) {
result[i] = (byte) buffer[i];
}
return result;
}

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


static char[] getCharBuffer() { static char[] getCharBuffer() {
Expand Down
28 changes: 2 additions & 26 deletions brave/src/main/java/brave/propagation/B3SinglePropagation.java
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -4,31 +4,7 @@
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;


/** /** Implements the propagation format described in {@link B3SingleFormat}. */
* This format corresponds to the propagation key "b3" (or "B3"), which delimits fields in the
* following manner.
*
* <pre>{@code
* b3: {x-b3-traceid}-{x-b3-spanid}-{x-b3-sampled}-{x-b3-parentspanid}-{x-b3-flags}
* }</pre>
*
* <p>For example, a sampled root span would look like:
* {@code 4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1}
*
* <p>Like normal B3, it is valid to omit trace identifiers in order to only propagate a sampling
* decision. For example, the following are valid downstream hints:
* <ul>
* <li>sampled - {@code b3: 1}</li>
* <li>unsampled - {@code b3: 0}</li>
* <li>debug - {@code b3: 1-1}</li>
* </ul>
* Note: {@code b3: 0-1} isn't supported as it doesn't make sense. Debug boosts ordinary sampling
* decision to also affect the collector tier. {@code b3: 0-1} would be like saying, don't sample,
* except at the collector tier, which is impossible as if you don't sample locally the data will
* never arrive at a collector.
*
* <p>See <a href="https://github.com/openzipkin/b3-propagation">B3 Propagation</a>
*/
public final class B3SinglePropagation<K> implements Propagation<K> { public final class B3SinglePropagation<K> implements Propagation<K> {


public static final Factory FACTORY = new Factory() { public static final Factory FACTORY = new Factory() {
Expand Down Expand Up @@ -102,7 +78,7 @@ static final class B3SingleExtractor<C, K> implements TraceContext.Extractor<C>
if (b3 == null) b3 = getter.get(carrier, upperKey); if (b3 == null) b3 = getter.get(carrier, upperKey);
if (b3 == null) return TraceContextOrSamplingFlags.EMPTY; if (b3 == null) return TraceContextOrSamplingFlags.EMPTY;


TraceContextOrSamplingFlags extracted = B3SingleFormat.maybeB3SingleFormat(b3); TraceContextOrSamplingFlags extracted = B3SingleFormat.parseB3SingleFormat(b3);
// if null, the trace context is malformed so return empty // if null, the trace context is malformed so return empty
if (extracted == null) return TraceContextOrSamplingFlags.EMPTY; if (extracted == null) return TraceContextOrSamplingFlags.EMPTY;
return extracted; return extracted;
Expand Down
Loading

0 comments on commit 4758c1f

Please sign in to comment.