/**
* Copyright 2015-2016 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 zipkin.internal;
import java.io.EOFException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import zipkin.Annotation;
import zipkin.BinaryAnnotation;
import zipkin.Codec;
import zipkin.DependencyLink;
import zipkin.Endpoint;
import zipkin.Span;
import static zipkin.internal.Util.UTF_8;
import static zipkin.internal.Util.assertionError;
import static zipkin.internal.Util.checkArgument;
/**
* This is a hard-coded thrift codec, which allows us to include thrift marshalling in a minified
* core jar. The hard coding not only keeps us with a single data-model, it also allows the minified
* core jar free of SLFJ classes otherwise included in generated types.
*
* <p> This directly implements TBinaryProtocol so as to reduce dependencies and array duplication.
* While reads internally use {@link ByteBuffer}, writes use {@link Buffer} as the latter can grow.
*/
public final class ThriftCodec implements Codec {
// break vs decode huge structs, like > 1MB strings or 10k spans in a trace.
static final int STRING_LENGTH_LIMIT = 1 * 1024 * 1024;
static final int CONTAINER_LENGTH_LIMIT = 10 * 1000;
// break vs recursing infinitely when skipping data
static final int MAX_SKIP_DEPTH = 2147483647;
// taken from org.apache.thrift.protocol.TType
static final byte TYPE_STOP = 0;
static final byte TYPE_BOOL = 2;
static final byte TYPE_BYTE = 3;
static final byte TYPE_DOUBLE = 4;
static final byte TYPE_I16 = 6;
static final byte TYPE_I32 = 8;
static final byte TYPE_I64 = 10;
static final byte TYPE_STRING = 11;
static final byte TYPE_STRUCT = 12;
static final byte TYPE_MAP = 13;
static final byte TYPE_SET = 14;
static final byte TYPE_LIST = 15;
/**
* Added for DataStax Cassandra driver, which returns data in ByteBuffers. The implementation
* takes care not to re-buffer the data.
*
* @throws {@linkplain IllegalArgumentException} if the span couldn't be decoded
*/
public Span readSpan(ByteBuffer bytes) {
return read(SPAN_ADAPTER, bytes);
}
@Override
public Span readSpan(byte[] bytes) {
return read(SPAN_ADAPTER, ByteBuffer.wrap(bytes));
}
@Override public int sizeInBytes(Span value) {
return SPAN_ADAPTER.sizeInBytes(value);
}
@Override
public byte[] writeSpan(Span value) {
return write(SPAN_ADAPTER, value);
}
@Override
public List<Span> readSpans(byte[] bytes) {
return read(SPANS_ADAPTER, ByteBuffer.wrap(bytes));
}
@Override
public byte[] writeSpans(List<Span> value) {
return write(SPANS_ADAPTER, value);
}
@Override
public byte[] writeTraces(List<List<Span>> value) {
return write(TRACES_ADAPTER, value);
}
interface ThriftReader<T> {
T read(ByteBuffer bytes);
}
interface ThriftAdapter<T> extends ThriftReader<T>, Buffer.Writer<T> {
}
static final ThriftAdapter<Endpoint> ENDPOINT_ADAPTER = new ThriftAdapter<Endpoint>() {
final Field IPV4 = new Field(TYPE_I32, 1);
final Field PORT = new Field(TYPE_I16, 2);
final Field SERVICE_NAME = new Field(TYPE_STRING, 3);
final Field IPV6 = new Field(TYPE_STRING, 4);
@Override
public Endpoint read(ByteBuffer bytes) {
Endpoint.Builder result = Endpoint.builder();
Field field;
while (true) {
field = Field.read(bytes);
if (field.type == TYPE_STOP) break;
if (field.isEqualTo(IPV4)) {
result.ipv4(bytes.getInt());
} else if (field.isEqualTo(PORT)) {
result.port(Short.valueOf(bytes.getShort()));
} else if (field.isEqualTo(SERVICE_NAME)) {
result.serviceName(readUtf8(bytes));
} else if (field.isEqualTo(IPV6)) {
result.ipv6(readByteArray(bytes));
} else {
skip(bytes, field.type);
}
}
return result.build();
}
@Override public int sizeInBytes(Endpoint value) {
int sizeInBytes = 0;
sizeInBytes += 3 + 4;// IPV4
sizeInBytes += 3 + 2;// PORT
sizeInBytes += 3 + 4 + Buffer.utf8SizeInBytes(value.serviceName);
if (value.ipv6 != null) sizeInBytes += 3 + 4 + 16;
sizeInBytes++; //TYPE_STOP
return sizeInBytes;
}
@Override
public void write(Endpoint value, Buffer buffer) {
IPV4.write(buffer);
buffer.writeInt(value.ipv4);
PORT.write(buffer);
buffer.writeShort(value.port == null ? 0 : value.port);
SERVICE_NAME.write(buffer);
buffer.writeLengthPrefixed(value.serviceName);
if (value.ipv6 != null) {
IPV6.write(buffer);
assert value.ipv6.length == 16;
buffer.writeInt(value.ipv6.length);
buffer.write(value.ipv6);
}
buffer.writeByte(TYPE_STOP);
}
};
static final ThriftAdapter<Annotation> ANNOTATION_ADAPTER = new ThriftAdapter<Annotation>() {
final Field TIMESTAMP = new Field(TYPE_I64, 1);
final Field VALUE = new Field(TYPE_STRING, 2);
final Field ENDPOINT = new Field(TYPE_STRUCT, 3);
@Override
public Annotation read(ByteBuffer bytes) {
Annotation.Builder result = Annotation.builder();
Field field;
while (true) {
field = Field.read(bytes);
if (field.type == TYPE_STOP) break;
if (field.isEqualTo(TIMESTAMP)) {
result.timestamp(bytes.getLong());
} else if (field.isEqualTo(VALUE)) {
result.value(readUtf8(bytes));
} else if (field.isEqualTo(ENDPOINT)) {
result.endpoint(ENDPOINT_ADAPTER.read(bytes));
} else {
skip(bytes, field.type);
}
}
return result.build();
}
@Override public int sizeInBytes(Annotation value) {
int sizeInBytes = 0;
sizeInBytes += 3 + 8;// TIMESTAMP
sizeInBytes += 3 + 4 + Buffer.utf8SizeInBytes(value.value);
if (value.endpoint != null) sizeInBytes += 3 + ENDPOINT_ADAPTER.sizeInBytes(value.endpoint);
sizeInBytes++; //TYPE_STOP
return sizeInBytes;
}
@Override
public void write(Annotation value, Buffer buffer) {
TIMESTAMP.write(buffer);
buffer.writeLong(value.timestamp);
VALUE.write(buffer);
buffer.writeLengthPrefixed(value.value);
if (value.endpoint != null) {
ENDPOINT.write(buffer);
ENDPOINT_ADAPTER.write(value.endpoint, buffer);
}
buffer.writeByte(TYPE_STOP);
}
};
static final ThriftAdapter<BinaryAnnotation> BINARY_ANNOTATION_ADAPTER = new ThriftAdapter<BinaryAnnotation>() {
final Field KEY = new Field(TYPE_STRING, 1);
final Field VALUE = new Field(TYPE_STRING, 2);
final Field TYPE = new Field(TYPE_I32, 3);
final Field ENDPOINT = new Field(TYPE_STRUCT, 4);
@Override
public BinaryAnnotation read(ByteBuffer bytes) {
BinaryAnnotation.Builder result = BinaryAnnotation.builder();
Field field;
while (true) {
field = Field.read(bytes);
if (field.type == TYPE_STOP) break;
if (field.isEqualTo(KEY)) {
result.key(readUtf8(bytes));
} else if (field.isEqualTo(VALUE)) {
result.value(readByteArray(bytes));
} else if (field.isEqualTo(TYPE)) {
result.type(BinaryAnnotation.Type.fromValue(bytes.getInt()));
} else if (field.isEqualTo(ENDPOINT)) {
result.endpoint(ENDPOINT_ADAPTER.read(bytes));
} else {
skip(bytes, field.type);
}
}
return result.build();
}
@Override public int sizeInBytes(BinaryAnnotation value) {
int sizeInBytes = 0;
sizeInBytes += 3 + 4 + Buffer.utf8SizeInBytes(value.key);
sizeInBytes += 3 + 4 + value.value.length;
sizeInBytes += 3 + 4; // TYPE
if (value.endpoint != null) sizeInBytes += 3 + ENDPOINT_ADAPTER.sizeInBytes(value.endpoint);
sizeInBytes++; //TYPE_STOP
return sizeInBytes;
}
@Override
public void write(BinaryAnnotation value, Buffer buffer) {
KEY.write(buffer);
buffer.writeLengthPrefixed(value.key);
VALUE.write(buffer);
buffer.writeInt(value.value.length);
buffer.write(value.value);
TYPE.write(buffer);
buffer.writeInt(value.type.value);
if (value.endpoint != null) {
ENDPOINT.write(buffer);
ENDPOINT_ADAPTER.write(value.endpoint, buffer);
}
buffer.writeByte(TYPE_STOP);
}
};
static final ThriftAdapter<List<Annotation>> ANNOTATIONS_ADAPTER =
new ListAdapter<>(ANNOTATION_ADAPTER);
static final ThriftAdapter<List<BinaryAnnotation>> BINARY_ANNOTATIONS_ADAPTER =
new ListAdapter<>(BINARY_ANNOTATION_ADAPTER);
static final ThriftAdapter<Span> SPAN_ADAPTER = new ThriftAdapter<Span>() {
final Field TRACE_ID = new Field(TYPE_I64, 1);
final Field TRACE_ID_HIGH = new Field(TYPE_I64, 12);
final Field NAME = new Field(TYPE_STRING, 3);
final Field ID = new Field(TYPE_I64, 4);
final Field PARENT_ID = new Field(TYPE_I64, 5);
final Field ANNOTATIONS = new Field(TYPE_LIST, 6);
final Field BINARY_ANNOTATIONS = new Field(TYPE_LIST, 8);
final Field DEBUG = new Field(TYPE_BOOL, 9);
final Field TIMESTAMP = new Field(TYPE_I64, 10);
final Field DURATION = new Field(TYPE_I64, 11);
@Override
public Span read(ByteBuffer bytes) {
Span.Builder result = Span.builder();
Field field;
while (true) {
field = Field.read(bytes);
if (field.type == TYPE_STOP) break;
if (field.isEqualTo(TRACE_ID_HIGH)) {
result.traceIdHigh(bytes.getLong());
} else if (field.isEqualTo(TRACE_ID)) {
result.traceId(bytes.getLong());
} else if (field.isEqualTo(NAME)) {
result.name(readUtf8(bytes));
} else if (field.isEqualTo(ID)) {
result.id(bytes.getLong());
} else if (field.isEqualTo(PARENT_ID)) {
result.parentId(bytes.getLong());
} else if (field.isEqualTo(ANNOTATIONS)) {
result.annotations(ANNOTATIONS_ADAPTER.read(bytes));
} else if (field.isEqualTo(BINARY_ANNOTATIONS)) {
result.binaryAnnotations(BINARY_ANNOTATIONS_ADAPTER.read(bytes));
} else if (field.isEqualTo(DEBUG)) {
result.debug(bytes.get() == 1);
} else if (field.isEqualTo(TIMESTAMP)) {
result.timestamp(bytes.getLong());
} else if (field.isEqualTo(DURATION)) {
result.duration(bytes.getLong());
} else {
skip(bytes, field.type);
}
}
return result.build();
}
@Override public int sizeInBytes(Span value) {
int sizeInBytes = 0;
if (value.traceIdHigh != 0) sizeInBytes += 3 + 8;
sizeInBytes += 3 + 8;// TRACE_ID
sizeInBytes += 3 + 4 + Buffer.utf8SizeInBytes(value.name);
sizeInBytes += 3 + 8;// ID
if (value.parentId != null) sizeInBytes += 3 + 8;
sizeInBytes += 3 + ANNOTATIONS_ADAPTER.sizeInBytes(value.annotations);
sizeInBytes += 3 + BINARY_ANNOTATIONS_ADAPTER.sizeInBytes(value.binaryAnnotations);
if (value.debug != null && value.debug) sizeInBytes += 3 + 1;
if (value.timestamp != null) sizeInBytes += 3 + 8;
if (value.duration != null) sizeInBytes += 3 + 8;
sizeInBytes++; //TYPE_STOP
return sizeInBytes;
}
@Override
public void write(Span value, Buffer buffer) {
TRACE_ID.write(buffer);
buffer.writeLong(value.traceId);
NAME.write(buffer);
buffer.writeLengthPrefixed(value.name);
ID.write(buffer);
buffer.writeLong(value.id);
if (value.parentId != null) {
PARENT_ID.write(buffer);
buffer.writeLong(value.parentId);
}
ANNOTATIONS.write(buffer);
ANNOTATIONS_ADAPTER.write(value.annotations, buffer);
BINARY_ANNOTATIONS.write(buffer);
BINARY_ANNOTATIONS_ADAPTER.write(value.binaryAnnotations, buffer);
if (value.debug != null && value.debug) {
DEBUG.write(buffer);
buffer.writeByte(1);
}
if (value.timestamp != null) {
TIMESTAMP.write(buffer);
buffer.writeLong(value.timestamp);
}
if (value.duration != null) {
DURATION.write(buffer);
buffer.writeLong(value.duration);
}
if (value.traceIdHigh != 0) {
TRACE_ID_HIGH.write(buffer);
buffer.writeLong(value.traceIdHigh);
}
buffer.writeByte(TYPE_STOP);
}
@Override
public String toString() {
return "Span";
}
};
static final ThriftAdapter<List<Span>> SPANS_ADAPTER = new ListAdapter<>(SPAN_ADAPTER);
static final ThriftAdapter<List<List<Span>>> TRACES_ADAPTER = new ListAdapter<>(SPANS_ADAPTER);
static final ThriftAdapter<DependencyLink> DEPENDENCY_LINK_ADAPTER = new ThriftAdapter<DependencyLink>() {
final Field PARENT = new Field(TYPE_STRING, 1);
final Field CHILD = new Field(TYPE_STRING, 2);
final Field CALL_COUNT = new Field(TYPE_I64, 4);
@Override
public DependencyLink read(ByteBuffer bytes) {
DependencyLink.Builder result = DependencyLink.builder();
Field field;
while (true) {
field = Field.read(bytes);
if (field.type == TYPE_STOP) break;
if (field.isEqualTo(PARENT)) {
result.parent(readUtf8(bytes));
} else if (field.isEqualTo(CHILD)) {
result.child(readUtf8(bytes));
} else if (field.isEqualTo(CALL_COUNT)) {
result.callCount(bytes.getLong());
} else {
skip(bytes, field.type);
}
}
return result.build();
}
@Override public int sizeInBytes(DependencyLink value) {
int sizeInBytes = 0;
sizeInBytes += 3 + 4 + Buffer.utf8SizeInBytes(value.parent);
sizeInBytes += 3 + 4 + Buffer.utf8SizeInBytes(value.child);
sizeInBytes += 3 + 8; // CALL_COUNT
sizeInBytes++; //TYPE_STOP
return sizeInBytes;
}
@Override
public void write(DependencyLink value, Buffer buffer) {
PARENT.write(buffer);
buffer.writeLengthPrefixed(value.parent);
CHILD.write(buffer);
buffer.writeLengthPrefixed(value.child);
CALL_COUNT.write(buffer);
buffer.writeLong(value.callCount);
buffer.writeByte(TYPE_STOP);
}
@Override
public String toString() {
return "DependencyLink";
}
};
static final ThriftAdapter<List<DependencyLink>> DEPENDENCY_LINKS_ADAPTER =
new ListAdapter<>(DEPENDENCY_LINK_ADAPTER);
@Override
public DependencyLink readDependencyLink(byte[] bytes) {
return read(DEPENDENCY_LINK_ADAPTER, ByteBuffer.wrap(bytes));
}
@Override
public byte[] writeDependencyLink(DependencyLink value) {
return write(DEPENDENCY_LINK_ADAPTER, value);
}
/**
* Added for DataStax Cassandra driver, which returns data in ByteBuffers. The implementation
* takes care not to re-buffer the data.
*
* @throws {@linkplain IllegalArgumentException} if the links couldn't be decoded
*/
public List<DependencyLink> readDependencyLinks(ByteBuffer bytes) {
return read(DEPENDENCY_LINKS_ADAPTER, bytes);
}
@Override
public List<DependencyLink> readDependencyLinks(byte[] bytes) {
return read(DEPENDENCY_LINKS_ADAPTER, ByteBuffer.wrap(bytes));
}
@Override
public byte[] writeDependencyLinks(List<DependencyLink> value) {
return write(DEPENDENCY_LINKS_ADAPTER, value);
}
static <T> T read(ThriftReader<T> reader, ByteBuffer bytes) {
checkArgument(bytes.remaining() > 0, "Empty input reading %s", reader);
try {
return reader.read(bytes);
} catch (RuntimeException e) {
throw exceptionReading(reader.toString(), e);
}
}
/** Inability to encode is a programming bug. */
static <T> byte[] write(Buffer.Writer<T> writer, T value) {
Buffer buffer = new Buffer(writer.sizeInBytes(value));
try {
writer.write(value, buffer);
} catch (RuntimeException e) {
throw assertionError("Could not write " + value + " as TBinary", e);
}
return buffer.toByteArray();
}
static <T> List<T> readList(ThriftReader<T> reader, ByteBuffer bytes) {
byte ignoredType = bytes.get();
int length = guardLength(bytes, CONTAINER_LENGTH_LIMIT);
if (length == 0) return Collections.emptyList();
if (length == 1) return Collections.singletonList(reader.read(bytes));
List<T> result = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
result.add(reader.read(bytes));
}
return result;
}
static <T> void writeList(Buffer.Writer<T> writer, List<T> value, Buffer buffer) {
int length = value.size();
writeListBegin(buffer, length);
for (int i = 0; i < length; i++) {
writer.write(value.get(i), buffer);
}
}
static final class ListAdapter<T> implements ThriftAdapter<List<T>> {
final ThriftAdapter<T> adapter;
ListAdapter(ThriftAdapter<T> adapter) {
this.adapter = adapter;
}
@Override
public List<T> read(ByteBuffer bytes) {
return readList(adapter, bytes);
}
@Override public int sizeInBytes(List<T> value) {
int sizeInBytes = 5; // TYPE_STRUCT + length prefix
for (int i = 0, length = value.size(); i < length; i++) {
sizeInBytes += adapter.sizeInBytes(value.get(i));
}
return sizeInBytes;
}
@Override
public void write(List<T> value, Buffer buffer) {
writeList(adapter, value, buffer);
}
@Override
public String toString() {
return "List<" + adapter + ">";
}
}
static IllegalArgumentException exceptionReading(String type, Exception e) {
String cause = e.getMessage() == null ? "Error" : e.getMessage();
if (e instanceof EOFException) cause = "EOF";
if (e instanceof IllegalStateException || e instanceof BufferUnderflowException) cause = "Malformed";
String message = String.format("%s reading %s from TBinary", cause, type);
throw new IllegalArgumentException(message, e);
}
static final class Field {
final byte type;
final int id;
Field(byte type, int id) {
this.type = type;
this.id = id;
}
void write(Buffer buffer) {
buffer.writeByte(type);
buffer.writeShort(id);
}
static Field read(ByteBuffer bytes) {
byte type = bytes.get();
return new Field(type, type == TYPE_STOP ? TYPE_STOP : bytes.getShort());
}
boolean isEqualTo(Field that) {
return this.type == that.type && this.id == that.id;
}
}
static void skip(ByteBuffer bytes, byte type) {
skip(bytes, type, MAX_SKIP_DEPTH);
}
static void skip(ByteBuffer bytes, byte type, int maxDepth) {
if (maxDepth <= 0) throw new IllegalStateException("Maximum skip depth exceeded");
switch (type) {
case TYPE_BOOL:
case TYPE_BYTE:
skip(bytes, 1);
break;
case TYPE_I16:
skip(bytes, 2);
break;
case TYPE_I32:
skip(bytes, 4);
break;
case TYPE_DOUBLE:
case TYPE_I64:
skip(bytes, 8);
break;
case TYPE_STRING:
int size = guardLength(bytes, STRING_LENGTH_LIMIT);
skip(bytes, size);
break;
case TYPE_STRUCT:
while (true) {
Field field = Field.read(bytes);
if (field.type == TYPE_STOP) return;
skip(bytes, field.type, maxDepth - 1);
}
case TYPE_MAP:
byte keyType = bytes.get();
byte valueType = bytes.get();
for (int i = 0, length = guardLength(bytes, CONTAINER_LENGTH_LIMIT); i < length; i++) {
skip(bytes, keyType, maxDepth - 1);
skip(bytes, valueType, maxDepth - 1);
}
break;
case TYPE_SET:
case TYPE_LIST:
byte elemType = bytes.get();
for (int i = 0, length = guardLength(bytes, CONTAINER_LENGTH_LIMIT); i < length; i++) {
skip(bytes, elemType, maxDepth - 1);
}
break;
default: // types that don't need explicit skipping
break;
}
}
static void skip(ByteBuffer bytes, int count) {
bytes.position(bytes.position() + count);
}
static byte[] readByteArray(ByteBuffer bytes) {
byte[] result = new byte[guardLength(bytes, STRING_LENGTH_LIMIT)];
bytes.get(result);
return result;
}
static String readUtf8(ByteBuffer bytes) {
return new String(readByteArray(bytes), UTF_8);
}
static int guardLength(ByteBuffer bytes, int limit) {
int length = bytes.getInt();
if (length > limit) { // don't allocate massive arrays
throw new IllegalStateException(length + " > " + limit + ": possibly malformed thrift");
}
return length;
}
static void writeListBegin(Buffer buffer, int size) {
buffer.writeByte(TYPE_STRUCT);
buffer.writeInt(size);
}
}