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