blob: 2daad95ec22e61d3d083df294c3c08ad15ad7de6 [file] [log] [blame]
/*
* Copyright 2016-17, OpenCensus 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 io.opencensus.implcore.tags.propagation;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import io.opencensus.implcore.internal.VarInt;
import io.opencensus.implcore.tags.TagContextImpl;
import io.opencensus.tags.InternalUtils;
import io.opencensus.tags.Tag;
import io.opencensus.tags.TagContext;
import io.opencensus.tags.TagKey;
import io.opencensus.tags.TagValue;
import io.opencensus.tags.propagation.TagContextDeserializationException;
import io.opencensus.tags.propagation.TagContextSerializationException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* Methods for serializing and deserializing {@link TagContext}s.
*
* <p>The format defined in this class is shared across all implementations of OpenCensus. It allows
* tags to propagate across requests.
*
* <p>OpenCensus tag context encoding:
*
* <ul>
* <li>Tags are encoded in single byte sequence. The version 0 format is:
* <li>{@code <version_id><encoded_tags>}
* <li>{@code <version_id> == a single byte, value 0}
* <li>{@code <encoded_tags> == (<tag_field_id><tag_encoding>)*}
* <ul>
* <li>{@code <tag_field_id>} == a single byte, value 0
* <li>{@code <tag_encoding>}:
* <ul>
* <li>{@code <tag_key_len><tag_key><tag_val_len><tag_val>}
* <ul>
* <li>{@code <tag_key_len>} == varint encoded integer
* <li>{@code <tag_key>} == tag_key_len bytes comprising tag key name
* <li>{@code <tag_val_len>} == varint encoded integer
* <li>{@code <tag_val>} == tag_val_len bytes comprising UTF-8 string
* </ul>
* </ul>
* </ul>
* </ul>
*/
final class SerializationUtils {
private SerializationUtils() {}
@VisibleForTesting static final int VERSION_ID = 0;
@VisibleForTesting static final int TAG_FIELD_ID = 0;
// This size limit only applies to the bytes representing tag keys and values.
@VisibleForTesting static final int TAGCONTEXT_SERIALIZED_SIZE_LIMIT = 8192;
// Serializes a TagContext to the on-the-wire format.
// Encoded tags are of the form: <version_id><encoded_tags>
static byte[] serializeBinary(TagContext tags) throws TagContextSerializationException {
// Use a ByteArrayDataOutput to avoid needing to handle IOExceptions.
final ByteArrayDataOutput byteArrayDataOutput = ByteStreams.newDataOutput();
byteArrayDataOutput.write(VERSION_ID);
int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext(); ) {
Tag tag = i.next();
totalChars += tag.getKey().getName().length();
totalChars += tag.getValue().asString().length();
encodeTag(tag, byteArrayDataOutput);
}
if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
throw new TagContextSerializationException(
"Size of TagContext exceeds the maximum serialized size "
+ TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
}
return byteArrayDataOutput.toByteArray();
}
// Deserializes input to TagContext based on the binary format standard.
// The encoded tags are of the form: <version_id><encoded_tags>
static TagContextImpl deserializeBinary(byte[] bytes) throws TagContextDeserializationException {
try {
if (bytes.length == 0) {
// Does not allow empty byte array.
throw new TagContextDeserializationException("Input byte[] can not be empty.");
}
ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
int versionId = buffer.get();
if (versionId > VERSION_ID || versionId < 0) {
throw new TagContextDeserializationException(
"Wrong Version ID: " + versionId + ". Currently supports version up to: " + VERSION_ID);
}
return new TagContextImpl(parseTags(buffer));
} catch (BufferUnderflowException exn) {
throw new TagContextDeserializationException(exn.toString()); // byte array format error.
}
}
private static Map<TagKey, TagValue> parseTags(ByteBuffer buffer)
throws TagContextDeserializationException {
Map<TagKey, TagValue> tags = new HashMap<TagKey, TagValue>();
int limit = buffer.limit();
int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
while (buffer.position() < limit) {
int type = buffer.get();
if (type == TAG_FIELD_ID) {
TagKey key = createTagKey(decodeString(buffer));
TagValue val = createTagValue(key, decodeString(buffer));
totalChars += key.getName().length();
totalChars += val.asString().length();
tags.put(key, val);
} else {
// Stop parsing at the first unknown field ID, since there is no way to know its length.
// TODO(sebright): Consider storing the rest of the byte array in the TagContext.
break;
}
}
if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) {
throw new TagContextDeserializationException(
"Size of TagContext exceeds the maximum serialized size "
+ TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
}
return tags;
}
// TODO(sebright): Consider exposing a TagKey name validation method to avoid needing to catch an
// IllegalArgumentException here.
private static final TagKey createTagKey(String name) throws TagContextDeserializationException {
try {
return TagKey.create(name);
} catch (IllegalArgumentException e) {
throw new TagContextDeserializationException("Invalid tag key: " + name, e);
}
}
// TODO(sebright): Consider exposing a TagValue validation method to avoid needing to catch
// an IllegalArgumentException here.
private static final TagValue createTagValue(TagKey key, String value)
throws TagContextDeserializationException {
try {
return TagValue.create(value);
} catch (IllegalArgumentException e) {
throw new TagContextDeserializationException(
"Invalid tag value for key " + key + ": " + value, e);
}
}
private static final void encodeTag(Tag tag, ByteArrayDataOutput byteArrayDataOutput) {
byteArrayDataOutput.write(TAG_FIELD_ID);
encodeString(tag.getKey().getName(), byteArrayDataOutput);
encodeString(tag.getValue().asString(), byteArrayDataOutput);
}
private static final void encodeString(String input, ByteArrayDataOutput byteArrayDataOutput) {
putVarInt(input.length(), byteArrayDataOutput);
byteArrayDataOutput.write(input.getBytes(Charsets.UTF_8));
}
private static final void putVarInt(int input, ByteArrayDataOutput byteArrayDataOutput) {
byte[] output = new byte[VarInt.varIntSize(input)];
VarInt.putVarInt(input, output, 0);
byteArrayDataOutput.write(output);
}
private static final String decodeString(ByteBuffer buffer) {
int length = VarInt.getVarInt(buffer);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append((char) buffer.get());
}
return builder.toString();
}
}