1 /*
2  * Copyright 2019, 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 static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 
22 import com.google.common.annotations.VisibleForTesting;
23 import com.google.common.base.Splitter;
24 import io.opencensus.implcore.internal.CurrentState;
25 import io.opencensus.implcore.internal.CurrentState.State;
26 import io.opencensus.implcore.tags.TagMapImpl;
27 import io.opencensus.implcore.tags.TagValueWithMetadata;
28 import io.opencensus.tags.InternalUtils;
29 import io.opencensus.tags.Tag;
30 import io.opencensus.tags.TagContext;
31 import io.opencensus.tags.TagKey;
32 import io.opencensus.tags.TagMetadata;
33 import io.opencensus.tags.TagMetadata.TagTtl;
34 import io.opencensus.tags.TagValue;
35 import io.opencensus.tags.propagation.TagContextDeserializationException;
36 import io.opencensus.tags.propagation.TagContextSerializationException;
37 import io.opencensus.tags.propagation.TagContextTextFormat;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.Iterator;
41 import java.util.List;
42 import java.util.Map;
43 import javax.annotation.Nullable;
44 
45 /*>>>
46 import org.checkerframework.checker.nullness.qual.NonNull;
47 */
48 
49 /**
50  * Implementation of the W3C correlation context propagation protocol. See <a
51  * href=https://github.com/w3c/correlation-context>w3c/correlation-context</a>.
52  */
53 final class CorrelationContextFormat extends TagContextTextFormat {
54 
55   @VisibleForTesting static final String CORRELATION_CONTEXT = "Correlation-Context";
56   private static final List<String> FIELDS = Collections.singletonList(CORRELATION_CONTEXT);
57 
58   @VisibleForTesting static final int MAX_NUMBER_OF_TAGS = 180;
59   private static final int TAG_SERIALIZED_SIZE_LIMIT = 4096;
60   private static final int TAGCONTEXT_SERIALIZED_SIZE_LIMIT = 8192;
61   private static final char TAG_KEY_VALUE_DELIMITER = '=';
62   private static final char TAG_DELIMITER = ',';
63   private static final Splitter TAG_KEY_VALUE_SPLITTER = Splitter.on(TAG_KEY_VALUE_DELIMITER);
64   private static final Splitter TAG_SPLITTER = Splitter.on(TAG_DELIMITER);
65 
66   // TODO(songya): These constants are for tag metadata. Uncomment them when we decided to support
67   // encoding tag metadata.
68   private static final char TAG_PROPERTIES_DELIMITER = ';';
69   // private static final char TAG_PROPERTIES_KEY_VALUE_DELIMITER = '=';
70 
71   @VisibleForTesting
72   static final TagMetadata METADATA_UNLIMITED_PROPAGATION =
73       TagMetadata.create(TagTtl.UNLIMITED_PROPAGATION);
74 
75   private final CurrentState state;
76 
CorrelationContextFormat(CurrentState state)77   CorrelationContextFormat(CurrentState state) {
78     this.state = state;
79   }
80 
81   @Override
fields()82   public List<String> fields() {
83     return FIELDS;
84   }
85 
86   @Override
inject( TagContext tagContext, C carrier, Setter<C> setter)87   public <C /*>>> extends @NonNull Object*/> void inject(
88       TagContext tagContext, C carrier, Setter<C> setter) throws TagContextSerializationException {
89     checkNotNull(tagContext, "tagContext");
90     checkNotNull(carrier, "carrier");
91     checkNotNull(setter, "setter");
92     if (State.DISABLED.equals(state.getInternal())) {
93       return;
94     }
95 
96     try {
97       StringBuilder stringBuilder = new StringBuilder(TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
98       int totalChars = 0; // Here chars are equivalent to bytes, since we're using ascii chars.
99       int totalTags = 0;
100       for (Iterator<Tag> i = InternalUtils.getTags(tagContext); i.hasNext(); ) {
101         Tag tag = i.next();
102         if (TagTtl.NO_PROPAGATION.equals(tag.getTagMetadata().getTagTtl())) {
103           continue;
104         }
105         if (stringBuilder.length() > 0) {
106           stringBuilder.append(TAG_DELIMITER);
107         }
108         totalTags++;
109         totalChars += encodeTag(tag, stringBuilder);
110       }
111       checkArgument(
112           totalTags <= MAX_NUMBER_OF_TAGS,
113           "Number of tags in the TagContext exceeds limit " + MAX_NUMBER_OF_TAGS);
114       // Note per W3C spec, only the length of tag key and value counts towards the total length.
115       // Length of properties (a.k.a TagMetadata) does not count.
116       checkArgument(
117           totalChars <= TAGCONTEXT_SERIALIZED_SIZE_LIMIT,
118           "Size of TagContext exceeds the maximum serialized size "
119               + TAGCONTEXT_SERIALIZED_SIZE_LIMIT);
120       setter.put(carrier, CORRELATION_CONTEXT, stringBuilder.toString());
121     } catch (IllegalArgumentException e) {
122       throw new TagContextSerializationException("Failed to serialize TagContext", e);
123     }
124   }
125 
126   // Encodes the tag to the given string builder, and returns the length of encoded key-value pair.
encodeTag(Tag tag, StringBuilder stringBuilder)127   private static int encodeTag(Tag tag, StringBuilder stringBuilder) {
128     String key = tag.getKey().getName();
129     String value = tag.getValue().asString();
130     int charsOfTag = key.length() + value.length();
131     // This should never happen with our current constraints (<= 255 chars) on tags.
132     checkArgument(
133         charsOfTag <= TAG_SERIALIZED_SIZE_LIMIT,
134         "Serialized size of tag " + tag + " exceeds limit " + TAG_SERIALIZED_SIZE_LIMIT);
135 
136     // TODO(songy23): do we want to encode TagMetadata?
137     stringBuilder.append(key).append(TAG_KEY_VALUE_DELIMITER).append(value);
138     return charsOfTag;
139   }
140 
141   @Override
extract(C carrier, Getter<C> getter)142   public <C /*>>> extends @NonNull Object*/> TagContext extract(C carrier, Getter<C> getter)
143       throws TagContextDeserializationException {
144     checkNotNull(carrier, "carrier");
145     checkNotNull(getter, "getter");
146     if (State.DISABLED.equals(state.getInternal())) {
147       return TagMapImpl.EMPTY;
148     }
149 
150     @Nullable String correlationContext = getter.get(carrier, CORRELATION_CONTEXT);
151     if (correlationContext == null) {
152       throw new TagContextDeserializationException(CORRELATION_CONTEXT + " not present.");
153     }
154     try {
155       if (correlationContext.isEmpty()) {
156         return TagMapImpl.EMPTY;
157       }
158       Map<TagKey, TagValueWithMetadata> tags = new HashMap<>();
159       List<String> stringTags = TAG_SPLITTER.splitToList(correlationContext);
160       for (String stringTag : stringTags) {
161         decodeTag(stringTag, tags);
162       }
163       return new TagMapImpl(tags);
164     } catch (IllegalArgumentException e) {
165       throw new TagContextDeserializationException("Invalid TagContext: " + correlationContext, e);
166     }
167   }
168 
169   // Decodes tag key, value and metadata from the encoded string tag, then puts it into the tag map.
170   // The format of encoded string tag is name1=value1;properties1=p1;properties2=p2.
decodeTag(String stringTag, Map<TagKey, TagValueWithMetadata> tags)171   private static void decodeTag(String stringTag, Map<TagKey, TagValueWithMetadata> tags) {
172     String keyWithValue;
173     int firstPropertyIndex = stringTag.indexOf(TAG_PROPERTIES_DELIMITER);
174     if (firstPropertyIndex != -1) { // Tag with properties.
175       keyWithValue = stringTag.substring(0, firstPropertyIndex);
176       // TODO(songya): support decoding tag properties.
177     } else { // Tag without properties.
178       keyWithValue = stringTag;
179     }
180     List<String> keyValuePair = TAG_KEY_VALUE_SPLITTER.splitToList(keyWithValue);
181     checkArgument(keyValuePair.size() == 2, "Malformed tag " + stringTag);
182     TagKey key = TagKey.create(keyValuePair.get(0).trim());
183     TagValue value = TagValue.create(keyValuePair.get(1).trim());
184     TagValueWithMetadata valueWithMetadata =
185         TagValueWithMetadata.create(value, METADATA_UNLIMITED_PROPAGATION);
186     tags.put(key, valueWithMetadata);
187   }
188 }
189