1 /* 2 * Copyright 2016-17, OpenCensus Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.opencensus.implcore.tags.propagation; 18 19 import com.google.common.annotations.VisibleForTesting; 20 import com.google.common.base.Charsets; 21 import com.google.common.io.ByteArrayDataOutput; 22 import com.google.common.io.ByteStreams; 23 import io.opencensus.implcore.internal.VarInt; 24 import io.opencensus.implcore.tags.TagMapImpl; 25 import io.opencensus.implcore.tags.TagValueWithMetadata; 26 import io.opencensus.tags.InternalUtils; 27 import io.opencensus.tags.Tag; 28 import io.opencensus.tags.TagContext; 29 import io.opencensus.tags.TagKey; 30 import io.opencensus.tags.TagMetadata; 31 import io.opencensus.tags.TagMetadata.TagTtl; 32 import io.opencensus.tags.TagValue; 33 import io.opencensus.tags.propagation.TagContextDeserializationException; 34 import io.opencensus.tags.propagation.TagContextSerializationException; 35 import java.nio.BufferUnderflowException; 36 import java.nio.ByteBuffer; 37 import java.util.HashMap; 38 import java.util.Iterator; 39 import java.util.Map; 40 41 /** 42 * Methods for serializing and deserializing {@link TagContext}s. 43 * 44 * <p>The format defined in this class is shared across all implementations of OpenCensus. It allows 45 * tags to propagate across requests. 46 * 47 * <p>OpenCensus tag context encoding: 48 * 49 * <ul> 50 * <li>Tags are encoded in single byte sequence. The version 0 format is: 51 * <li>{@code <version_id><encoded_tags>} 52 * <li>{@code <version_id> == a single byte, value 0} 53 * <li>{@code <encoded_tags> == (<tag_field_id><tag_encoding>)*} 54 * <ul> 55 * <li>{@code <tag_field_id>} == a single byte, value 0 56 * <li>{@code <tag_encoding>}: 57 * <ul> 58 * <li>{@code <tag_key_len><tag_key><tag_val_len><tag_val>} 59 * <ul> 60 * <li>{@code <tag_key_len>} == varint encoded integer 61 * <li>{@code <tag_key>} == tag_key_len bytes comprising tag key name 62 * <li>{@code <tag_val_len>} == varint encoded integer 63 * <li>{@code <tag_val>} == tag_val_len bytes comprising UTF-8 string 64 * </ul> 65 * </ul> 66 * </ul> 67 * </ul> 68 */ 69 final class BinarySerializationUtils { 70 71 private static final TagMetadata METADATA_UNLIMITED_PROPAGATION = 72 TagMetadata.create(TagTtl.UNLIMITED_PROPAGATION); 73 BinarySerializationUtils()74 private BinarySerializationUtils() {} 75 76 @VisibleForTesting static final int VERSION_ID = 0; 77 @VisibleForTesting static final int TAG_FIELD_ID = 0; 78 // This size limit only applies to the bytes representing tag keys and values. 79 @VisibleForTesting static final int TAGCONTEXT_SERIALIZED_SIZE_LIMIT = 8192; 80 81 // Serializes a TagContext to the on-the-wire format. 82 // Encoded tags are of the form: <version_id><encoded_tags> serializeBinary(TagContext tags)83 static byte[] serializeBinary(TagContext tags) throws TagContextSerializationException { 84 // Use a ByteArrayDataOutput to avoid needing to handle IOExceptions. 85 final ByteArrayDataOutput byteArrayDataOutput = ByteStreams.newDataOutput(); 86 byteArrayDataOutput.write(VERSION_ID); 87 int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars. 88 for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext(); ) { 89 Tag tag = i.next(); 90 if (TagTtl.NO_PROPAGATION.equals(tag.getTagMetadata().getTagTtl())) { 91 continue; 92 } 93 totalChars += tag.getKey().getName().length(); 94 totalChars += tag.getValue().asString().length(); 95 encodeTag(tag, byteArrayDataOutput); 96 } 97 if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) { 98 throw new TagContextSerializationException( 99 "Size of TagContext exceeds the maximum serialized size " 100 + TAGCONTEXT_SERIALIZED_SIZE_LIMIT); 101 } 102 return byteArrayDataOutput.toByteArray(); 103 } 104 105 // Deserializes input to TagContext based on the binary format standard. 106 // The encoded tags are of the form: <version_id><encoded_tags> deserializeBinary(byte[] bytes)107 static TagMapImpl deserializeBinary(byte[] bytes) throws TagContextDeserializationException { 108 try { 109 if (bytes.length == 0) { 110 // Does not allow empty byte array. 111 throw new TagContextDeserializationException("Input byte[] can not be empty."); 112 } 113 114 ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer(); 115 int versionId = buffer.get(); 116 if (versionId > VERSION_ID || versionId < 0) { 117 throw new TagContextDeserializationException( 118 "Wrong Version ID: " + versionId + ". Currently supports version up to: " + VERSION_ID); 119 } 120 return new TagMapImpl(parseTags(buffer)); 121 } catch (BufferUnderflowException exn) { 122 throw new TagContextDeserializationException(exn.toString()); // byte array format error. 123 } 124 } 125 parseTags(ByteBuffer buffer)126 private static Map<TagKey, TagValueWithMetadata> parseTags(ByteBuffer buffer) 127 throws TagContextDeserializationException { 128 Map<TagKey, TagValueWithMetadata> tags = new HashMap<TagKey, TagValueWithMetadata>(); 129 int limit = buffer.limit(); 130 int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars. 131 while (buffer.position() < limit) { 132 int type = buffer.get(); 133 if (type == TAG_FIELD_ID) { 134 TagKey key = createTagKey(decodeString(buffer)); 135 TagValue val = createTagValue(key, decodeString(buffer)); 136 totalChars += key.getName().length(); 137 totalChars += val.asString().length(); 138 tags.put(key, TagValueWithMetadata.create(val, METADATA_UNLIMITED_PROPAGATION)); 139 } else { 140 // Stop parsing at the first unknown field ID, since there is no way to know its length. 141 // TODO(sebright): Consider storing the rest of the byte array in the TagContext. 142 break; 143 } 144 } 145 if (totalChars > TAGCONTEXT_SERIALIZED_SIZE_LIMIT) { 146 throw new TagContextDeserializationException( 147 "Size of TagContext exceeds the maximum serialized size " 148 + TAGCONTEXT_SERIALIZED_SIZE_LIMIT); 149 } 150 return tags; 151 } 152 153 // TODO(sebright): Consider exposing a TagKey name validation method to avoid needing to catch an 154 // IllegalArgumentException here. createTagKey(String name)155 private static final TagKey createTagKey(String name) throws TagContextDeserializationException { 156 try { 157 return TagKey.create(name); 158 } catch (IllegalArgumentException e) { 159 throw new TagContextDeserializationException("Invalid tag key: " + name, e); 160 } 161 } 162 163 // TODO(sebright): Consider exposing a TagValue validation method to avoid needing to catch 164 // an IllegalArgumentException here. createTagValue(TagKey key, String value)165 private static final TagValue createTagValue(TagKey key, String value) 166 throws TagContextDeserializationException { 167 try { 168 return TagValue.create(value); 169 } catch (IllegalArgumentException e) { 170 throw new TagContextDeserializationException( 171 "Invalid tag value for key " + key + ": " + value, e); 172 } 173 } 174 encodeTag(Tag tag, ByteArrayDataOutput byteArrayDataOutput)175 private static final void encodeTag(Tag tag, ByteArrayDataOutput byteArrayDataOutput) { 176 byteArrayDataOutput.write(TAG_FIELD_ID); 177 encodeString(tag.getKey().getName(), byteArrayDataOutput); 178 encodeString(tag.getValue().asString(), byteArrayDataOutput); 179 } 180 encodeString(String input, ByteArrayDataOutput byteArrayDataOutput)181 private static final void encodeString(String input, ByteArrayDataOutput byteArrayDataOutput) { 182 putVarInt(input.length(), byteArrayDataOutput); 183 byteArrayDataOutput.write(input.getBytes(Charsets.UTF_8)); 184 } 185 putVarInt(int input, ByteArrayDataOutput byteArrayDataOutput)186 private static final void putVarInt(int input, ByteArrayDataOutput byteArrayDataOutput) { 187 byte[] output = new byte[VarInt.varIntSize(input)]; 188 VarInt.putVarInt(input, output, 0); 189 byteArrayDataOutput.write(output); 190 } 191 decodeString(ByteBuffer buffer)192 private static final String decodeString(ByteBuffer buffer) { 193 int length = VarInt.getVarInt(buffer); 194 StringBuilder builder = new StringBuilder(); 195 for (int i = 0; i < length; i++) { 196 builder.append((char) buffer.get()); 197 } 198 return builder.toString(); 199 } 200 } 201