Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds EntrySplitter for use in w3c specs and secondary-sampling #1193

Merged
merged 13 commits into from
May 8, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/
package brave.test.propagation;

import brave.internal.HexCodec;
import brave.internal.codec.HexCodec;
import brave.internal.Nullable;
import brave.propagation.Propagation;
import brave.propagation.Propagation.Getter;
Expand Down
2 changes: 1 addition & 1 deletion brave/src/main/java/brave/Tracing.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import brave.baggage.BaggageField;
import brave.handler.FinishedSpanHandler;
import brave.handler.MutableSpan;
import brave.internal.IpLiteral;
import brave.internal.codec.IpLiteral;
import brave.internal.Nullable;
import brave.internal.Platform;
import brave.internal.handler.NoopAwareFinishedSpanHandler;
Expand Down
4 changes: 2 additions & 2 deletions brave/src/main/java/brave/handler/MutableSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@
import brave.ErrorParser;
import brave.Span.Kind;
import brave.SpanCustomizer;
import brave.internal.IpLiteral;
import brave.internal.codec.IpLiteral;
import brave.internal.Nullable;
import brave.propagation.TraceContext;
import java.lang.ref.WeakReference;
import java.util.ArrayList;

import static brave.internal.InternalPropagation.FLAG_DEBUG;
import static brave.internal.InternalPropagation.FLAG_SHARED;
import static brave.internal.JsonEscaper.jsonEscape;
import static brave.internal.codec.JsonEscaper.jsonEscape;

/**
* This represents a span except for its {@link TraceContext}. It is mutable, for late adjustments.
Expand Down
245 changes: 245 additions & 0 deletions brave/src/main/java/brave/internal/codec/EntrySplitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/*
* Copyright 2013-2020 The OpenZipkin Authors
*
* 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 brave.internal.codec;

import brave.internal.Platform;

/**
* Splits a character sequence that's in a delimited string trimming optional whitespace (OWS)
* before or after delimiters.
*
* <p>This is intended to be initialized as a constant, as doing so per-request will add
* unnecessary overhead.
*/
public final class EntrySplitter {
public static Builder newBuilder() {
return new Builder();
}

public static final class Builder {
int maxEntries = Integer.MAX_VALUE;
char entrySeparator = ',', keyValueSeparator = '=';
boolean trimOWSAroundEntrySeparator = true, trimOWSAroundKeyValueSeparator = true;
boolean keyValueSeparatorRequired = true, shouldThrow = false;
codefromthecrypt marked this conversation as resolved.
Show resolved Hide resolved

public Builder maxEntries(int maxEntries) {
if (maxEntries == 0) throw new NullPointerException("maxEntries == 0");
codefromthecrypt marked this conversation as resolved.
Show resolved Hide resolved
this.maxEntries = maxEntries;
return this;
}

public Builder entrySeparator(char entrySeparator) {
if (entrySeparator == 0) throw new NullPointerException("entrySeparator == 0");
this.entrySeparator = entrySeparator;
return this;
}

public Builder keyValueSeparator(char keyValueSeparator) {
if (keyValueSeparator == 0) throw new NullPointerException("keyValueSeparator == 0");
this.keyValueSeparator = keyValueSeparator;
return this;
}

public Builder trimOWSAroundEntrySeparator(boolean trimOWSAroundEntrySeparator) {
this.trimOWSAroundEntrySeparator = trimOWSAroundEntrySeparator;
return this;
}

public Builder trimOWSAroundKeyValueSeparator(boolean trimOWSAroundKeyValueSeparator) {
this.trimOWSAroundKeyValueSeparator = trimOWSAroundKeyValueSeparator;
return this;
}

public Builder keyValueSeparatorRequired(boolean keyValueSeparatorRequired) {
this.keyValueSeparatorRequired = keyValueSeparatorRequired;
return this;
}

/**
* On validation fail, should this throw an exception or log?. The use case to throw is when
* validating input (ex into a builder), or in unit tests.
*/
public Builder shouldThrow(boolean shouldThrow) {
this.shouldThrow = shouldThrow;
return this;
}

public EntrySplitter build() {
return new EntrySplitter(this);
codefromthecrypt marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* This is a callback on offsets to avoid allocating strings for a malformed input {@code input}.
*
* @param <T> target of parsed entries
*/
public interface Handler<T> {
/**
* Called for each valid entry split from the input {@code input}. Return {@code false} after
* logging to stop due to invalid input.
*
* <p>After validating, typically strings will be parsed from the input like so:
* <pre>{@code
* String key = input.substring(beginKey, endKey);
* String value = input.substring(beginValue, endValue);
* }</pre>
*
* @param target receiver of parsed entries
* @param input string including data to parse
* @param beginKey begin index of the entry's key in {@code input}, inclusive
* @param endKey end index of the entry's key in {@code input}, exclusive
* @param beginValue begin index of the entry's value in {@code input}, inclusive
* @param endValue end index of the entry's value in {@code input}, exclusive
* @return true if we reached the {@code endIndex} without failures.
*/
boolean onEntry(
T target, String input, int beginKey, int endKey, int beginValue, int endValue);
}

final char keyValueSeparator, entrySeparator;
int maxEntries;
final boolean trimOWSAroundEntrySeparator, trimOWSAroundKeyValueSeparator;
final boolean keyValueSeparatorRequired, shouldThrow;
final String missingKey, missingKeyValueSeparator, overMaxEntries;

EntrySplitter(Builder builder) {
keyValueSeparator = builder.keyValueSeparator;
entrySeparator = builder.entrySeparator;
maxEntries = builder.maxEntries;
trimOWSAroundEntrySeparator = builder.trimOWSAroundEntrySeparator;
trimOWSAroundKeyValueSeparator = builder.trimOWSAroundKeyValueSeparator;
keyValueSeparatorRequired = builder.keyValueSeparatorRequired;
shouldThrow = builder.shouldThrow;
missingKey = "Invalid input: no key before '" + keyValueSeparator + "'";
missingKeyValueSeparator =
"Invalid input: missing key value separator '" + keyValueSeparator + "'";
overMaxEntries = "Invalid input: over " + maxEntries + " entries";
}

/**
* @param handler parses entries emitted upon success
* @param target receiver of parsed entries
* @param input string including data to parse
* @return true if we reached the {@code endIndex} without failures.
*/
public <T> boolean parse(Handler<T> handler, T target, String input) {
return parse(handler, target, input, 0, input.length());
}

/**
* @param handler parses entries emitted upon success
* @param target receiver of parsed entries
* @param input string including data to parse
* @param beginIndex begin index of the {@code input}, inclusive
* @param endIndex end index of the {@code input}, exclusive
* @return true if we reached the {@code endIndex} without failures.
*/
public <T> boolean parse(
Handler<T> handler, T target, String input, int beginIndex, int endIndex) {
int remainingEntries = maxEntries, beginKey = -1, endKey = -1, beginValue = -1;
for (int i = beginIndex; i < endIndex; i++) {
char c = input.charAt(i);

boolean nextIsEnd = i + 1 == endIndex;
if (c == entrySeparator || nextIsEnd) { // finished an entry
if (c == keyValueSeparator) {
beginValue = i; // empty value: ex "key=" "k1 ="
codefromthecrypt marked this conversation as resolved.
Show resolved Hide resolved
}

if (beginKey == -1 && beginValue == -1) {
continue; // ignore empty entries, like ",,"
} else if (beginKey == -1) {
return logOrThrow(missingKey, shouldThrow); // ex. "=" ",="
} else if (nextIsEnd && beginValue == -1) { // ex "k1" "k1 " "a=b" "..=,"
// We reached the end of a key-only entry, a single character entry or an empty entry
codefromthecrypt marked this conversation as resolved.
Show resolved Hide resolved
beginValue = c == entrySeparator ? i + 1 : i;
}

int endValue;
if (endKey == -1) {
if (keyValueSeparatorRequired && c != keyValueSeparator) {
return logOrThrow(missingKeyValueSeparator, shouldThrow); // throw on "k1" "k1=v2,k2"
}

// Even though we have an empty value, we need to handle whitespace and
// boundary conditions.
//
// For example, using entry separator ',' and KV separator '=':
// "...,k1" and input[i] == 'y', we want i + 1, so that the key includes the 'y'
codefromthecrypt marked this conversation as resolved.
Show resolved Hide resolved
// "...,k1 " and input[i] == ' ', we want i + 1, as the key includes a trailing ' '
// "...,k1=" and input[i] == '=', we want i, bc a KV separator isn't part of the key
// "k1 , k2" and input[i] == ',', we want i, bc an entry separator isn't part of the key
endKey = nextIsEnd && c != keyValueSeparator ? i + 1 : i;

if (trimOWSAroundKeyValueSeparator) {
endKey = rewindOWS(input, beginKey, endKey);
}
beginValue = endValue = endKey; // value is empty
} else {
endValue = nextIsEnd ? i + 1 : i;

if (trimOWSAroundEntrySeparator) {
endValue = rewindOWS(input, beginValue, endValue);
}
}

if (remainingEntries-- == 0) logOrThrow(overMaxEntries, shouldThrow);

if (!handler.onEntry(target, input, beginKey, endKey, beginValue, endValue)) {
return false; // assume handler logs
}

beginKey = endKey = beginValue = -1; // reset for the next entry
} else if (beginKey == -1) {
if (trimOWSAroundEntrySeparator && isOWS(c)) continue; // skip whitespace before key
if (c == keyValueSeparator) {
if (i == beginIndex || input.charAt(i - 1) == entrySeparator) {
return logOrThrow(missingKey, shouldThrow); // ex "=v1" ",=v2"
}
}
beginKey = i;
} else if (endKey == -1 && c == keyValueSeparator) { // only use the first separator for key
endKey = i;
if (trimOWSAroundKeyValueSeparator) {
endKey = rewindOWS(input, beginIndex, endKey);
}
} else if (endKey != -1 && beginValue == -1) { // only look for a value if you have a key
if (trimOWSAroundKeyValueSeparator && isOWS(c)) continue; // skip whitespace before value
if (c == keyValueSeparator) continue; // skip the keyValueSeparator (ex '=')
beginValue = i;
}
}
return true;
}

static int rewindOWS(String input, int beginIndex, int endIndex) {
// endIndex is a boundary. we need to begin looking one character before it.
while (isOWS(input.charAt(endIndex - 1))) {
if (--endIndex == beginIndex) return beginIndex; // trim whitespace
}
return endIndex;
}

// OWS is zero or more spaces or tabs https://httpwg.org/specs/rfc7230.html#rfc.section.3.2
static boolean isOWS(char c) {
return c == ' ' || c == '\t';
}

static boolean logOrThrow(String msg, boolean shouldThrow) {
if (shouldThrow) throw new IllegalArgumentException(msg);
Platform.get().log(msg, null);
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package brave.internal;
package brave.internal.codec;

import brave.internal.RecyclableBuffers;

// code originally imported from zipkin.Util
public final class HexCodec {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package brave.internal;
package brave.internal.codec;

import brave.internal.Nullable;

/** Internal utility class to validate IPv4 or IPv6 literals */
public final class IpLiteral {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package brave.internal;
package brave.internal.codec;

// Initially, a copy of zipkin2.internal.JsonEscaper
public final class JsonEscaper {
Expand Down
2 changes: 1 addition & 1 deletion brave/src/main/java/brave/propagation/B3SingleFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.nio.ByteBuffer;
import java.util.Collections;

import static brave.internal.HexCodec.writeHexLong;
import static brave.internal.codec.HexCodec.writeHexLong;

/**
* This format corresponds to the propagation key "b3" (or "B3"), which delimits fields in the
Expand Down
6 changes: 3 additions & 3 deletions brave/src/main/java/brave/propagation/TraceContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
import java.util.Collections;
import java.util.List;

import static brave.internal.HexCodec.lenientLowerHexToUnsignedLong;
import static brave.internal.HexCodec.toLowerHex;
import static brave.internal.HexCodec.writeHexLong;
import static brave.internal.codec.HexCodec.lenientLowerHexToUnsignedLong;
import static brave.internal.codec.HexCodec.toLowerHex;
import static brave.internal.codec.HexCodec.writeHexLong;
import static brave.internal.InternalPropagation.FLAG_LOCAL_ROOT;
import static brave.internal.InternalPropagation.FLAG_SAMPLED;
import static brave.internal.InternalPropagation.FLAG_SAMPLED_LOCAL;
Expand Down
4 changes: 2 additions & 2 deletions brave/src/main/java/brave/propagation/TraceIdContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import brave.internal.Nullable;
import brave.internal.RecyclableBuffers;

import static brave.internal.HexCodec.toLowerHex;
import static brave.internal.HexCodec.writeHexLong;
import static brave.internal.codec.HexCodec.toLowerHex;
import static brave.internal.codec.HexCodec.writeHexLong;
import static brave.internal.InternalPropagation.FLAG_SAMPLED;
import static brave.internal.InternalPropagation.FLAG_SAMPLED_SET;

Expand Down
18 changes: 11 additions & 7 deletions brave/src/test/java/brave/features/baggage/SingleHeaderCodec.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import brave.baggage.BaggageField;
import brave.baggage.BaggageField.ValueUpdater;
import brave.internal.baggage.BaggageCodec;
import brave.internal.codec.EntrySplitter;
import brave.propagation.TraceContext;
import java.util.Collections;
import java.util.List;
Expand All @@ -26,7 +27,8 @@
*
* <p>See https://github.com/w3c/correlation-context/blob/master/correlation_context/HTTP_HEADER_FORMAT.md
*/
final class SingleHeaderCodec implements BaggageCodec {
final class SingleHeaderCodec implements BaggageCodec, EntrySplitter.Handler<ValueUpdater> {
static final EntrySplitter ENTRY_SPLITTER = EntrySplitter.newBuilder().build();
static final SingleHeaderCodec INSTANCE = new SingleHeaderCodec();

static BaggageCodec get() {
Expand All @@ -45,12 +47,14 @@ static BaggageCodec get() {
}

@Override public boolean decode(ValueUpdater valueUpdater, Object request, String value) {
boolean decoded = false;
for (String entry : value.split(",", -1)) {
String[] keyValue = entry.split("=", 2);
if (valueUpdater.updateValue(BaggageField.create(keyValue[0]), keyValue[1])) decoded = true;
}
return decoded;
return ENTRY_SPLITTER.parse(this, valueUpdater, value);
}

@Override public boolean onEntry(
ValueUpdater target, String buffer, int beginKey, int endKey, int beginValue, int endValue) {
BaggageField field = BaggageField.create(buffer.substring(beginKey, endKey));
String value = buffer.substring(beginValue, endValue);
return target.updateValue(field, value);
}

@Override public String encode(Map<String, String> values, TraceContext context, Object request) {
Expand Down
3 changes: 2 additions & 1 deletion brave/src/test/java/brave/internal/PlatformTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2019 The OpenZipkin Authors
* Copyright 2013-2020 The OpenZipkin Authors
*
* 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
Expand All @@ -13,6 +13,7 @@
*/
package brave.internal;

import brave.internal.codec.HexCodec;
import com.google.common.collect.Sets;
import java.net.Inet6Address;
import java.net.InetAddress;
Expand Down
Loading