• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016 The gRPC 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.grpc.internal.testing;
18 
19 import static com.google.common.base.Charsets.UTF_8;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 
22 import com.google.common.base.Function;
23 import com.google.common.collect.ImmutableMap;
24 import com.google.common.collect.Iterators;
25 import com.google.common.collect.Maps;
26 import io.grpc.Context;
27 import io.opencensus.common.Scope;
28 import io.opencensus.stats.Measure;
29 import io.opencensus.stats.MeasureMap;
30 import io.opencensus.stats.StatsRecorder;
31 import io.opencensus.tags.Tag;
32 import io.opencensus.tags.TagContext;
33 import io.opencensus.tags.TagContextBuilder;
34 import io.opencensus.tags.TagKey;
35 import io.opencensus.tags.TagMetadata;
36 import io.opencensus.tags.TagMetadata.TagTtl;
37 import io.opencensus.tags.TagValue;
38 import io.opencensus.tags.Tagger;
39 import io.opencensus.tags.propagation.TagContextBinarySerializer;
40 import io.opencensus.tags.propagation.TagContextDeserializationException;
41 import io.opencensus.tags.unsafe.ContextUtils;
42 import io.opencensus.trace.Annotation;
43 import io.opencensus.trace.AttributeValue;
44 import io.opencensus.trace.EndSpanOptions;
45 import io.opencensus.trace.Link;
46 import io.opencensus.trace.MessageEvent;
47 import io.opencensus.trace.Sampler;
48 import io.opencensus.trace.Span;
49 import io.opencensus.trace.SpanBuilder;
50 import io.opencensus.trace.SpanContext;
51 import io.opencensus.trace.SpanId;
52 import io.opencensus.trace.TraceId;
53 import io.opencensus.trace.TraceOptions;
54 import java.util.EnumSet;
55 import java.util.Iterator;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Random;
59 import java.util.concurrent.BlockingQueue;
60 import java.util.concurrent.LinkedBlockingQueue;
61 import java.util.concurrent.TimeUnit;
62 import javax.annotation.Nullable;
63 
64 public class StatsTestUtils {
StatsTestUtils()65   private StatsTestUtils() {
66   }
67 
68   public static class MetricsRecord {
69 
70     public final ImmutableMap<TagKey, TagValue> tags;
71     public final ImmutableMap<Measure, Number> metrics;
72 
MetricsRecord( ImmutableMap<TagKey, TagValue> tags, ImmutableMap<Measure, Number> metrics)73     private MetricsRecord(
74         ImmutableMap<TagKey, TagValue> tags, ImmutableMap<Measure, Number> metrics) {
75       this.tags = tags;
76       this.metrics = metrics;
77     }
78 
79     /**
80      * Returns the value of a metric, or {@code null} if not found.
81      */
82     @Nullable
getMetric(Measure measure)83     public Double getMetric(Measure measure) {
84       for (Map.Entry<Measure, Number> m : metrics.entrySet()) {
85         if (m.getKey().equals(measure)) {
86           Number value = m.getValue();
87           if (value instanceof Double) {
88             return (Double) value;
89           } else if (value instanceof Long) {
90             return (double) (Long) value;
91           }
92           throw new AssertionError("Unexpected measure value type: " + value.getClass().getName());
93         }
94       }
95       return null;
96     }
97 
98     /**
99      * Returns the value of a metric converted to long, or throw if not found.
100      */
getMetricAsLongOrFail(Measure measure)101     public long getMetricAsLongOrFail(Measure measure) {
102       Double doubleValue = getMetric(measure);
103       checkNotNull(doubleValue, "Measure not found: %s", measure.getName());
104       long longValue = (long) (Math.abs(doubleValue) + 0.0001);
105       if (doubleValue < 0) {
106         longValue = -longValue;
107       }
108       return longValue;
109     }
110 
111     @Override
toString()112     public String toString() {
113       return "[tags=" + tags + ", metrics=" + metrics + "]";
114     }
115   }
116 
117   /**
118    * This tag will be propagated by {@link FakeTagger} on the wire.
119    */
120   public static final TagKey EXTRA_TAG = TagKey.create("/rpc/test/extratag");
121 
122   private static final String EXTRA_TAG_HEADER_VALUE_PREFIX = "extratag:";
123 
124   /**
125    * A {@link Tagger} implementation that saves metrics records to be accessible from {@link
126    * #pollRecord()} and {@link #pollRecord(long, TimeUnit)}, until {@link #rolloverRecords} is
127    * called.
128    */
129   public static final class FakeStatsRecorder extends StatsRecorder {
130 
131     private BlockingQueue<MetricsRecord> records;
132 
FakeStatsRecorder()133     public FakeStatsRecorder() {
134       rolloverRecords();
135     }
136 
137     @Override
newMeasureMap()138     public MeasureMap newMeasureMap() {
139       return new FakeStatsRecord(this);
140     }
141 
pollRecord()142     public MetricsRecord pollRecord() {
143       return getCurrentRecordSink().poll();
144     }
145 
pollRecord(long timeout, TimeUnit unit)146     public MetricsRecord pollRecord(long timeout, TimeUnit unit) throws InterruptedException {
147       return getCurrentRecordSink().poll(timeout, unit);
148     }
149 
150     /**
151      * Disconnect this tagger with the contexts it has created so far.  The records from those
152      * contexts will not show up in {@link #pollRecord}.  Useful for isolating the records between
153      * test cases.
154      */
155     // This needs to be synchronized with getCurrentRecordSink() which may run concurrently.
rolloverRecords()156     public synchronized void rolloverRecords() {
157       records = new LinkedBlockingQueue<>();
158     }
159 
getCurrentRecordSink()160     private synchronized BlockingQueue<MetricsRecord> getCurrentRecordSink() {
161       return records;
162     }
163   }
164 
165   public static final class FakeTagger extends Tagger {
166 
167     @Override
empty()168     public FakeTagContext empty() {
169       return FakeTagContext.EMPTY;
170     }
171 
172     @Override
getCurrentTagContext()173     public TagContext getCurrentTagContext() {
174       return ContextUtils.getValue(Context.current());
175     }
176 
177     @Override
emptyBuilder()178     public TagContextBuilder emptyBuilder() {
179       return new FakeTagContextBuilder(ImmutableMap.<TagKey, TagValue>of());
180     }
181 
182     @Override
toBuilder(TagContext tags)183     public FakeTagContextBuilder toBuilder(TagContext tags) {
184       return new FakeTagContextBuilder(getTags(tags));
185     }
186 
187     @Override
currentBuilder()188     public TagContextBuilder currentBuilder() {
189       throw new UnsupportedOperationException();
190     }
191 
192     @Override
withTagContext(TagContext tags)193     public Scope withTagContext(TagContext tags) {
194       throw new UnsupportedOperationException();
195     }
196   }
197 
198   public static final class FakeTagContextBinarySerializer extends TagContextBinarySerializer {
199 
200     private final FakeTagger tagger = new FakeTagger();
201 
202     @Override
fromByteArray(byte[] bytes)203     public TagContext fromByteArray(byte[] bytes) throws TagContextDeserializationException {
204       String serializedString = new String(bytes, UTF_8);
205       if (serializedString.startsWith(EXTRA_TAG_HEADER_VALUE_PREFIX)) {
206         return tagger.emptyBuilder()
207             .putLocal(EXTRA_TAG,
208                 TagValue.create(serializedString.substring(EXTRA_TAG_HEADER_VALUE_PREFIX.length())))
209             .build();
210       } else {
211         throw new TagContextDeserializationException("Malformed value");
212       }
213     }
214 
215     @Override
toByteArray(TagContext tags)216     public byte[] toByteArray(TagContext tags) {
217       TagValue extraTagValue = getTags(tags).get(EXTRA_TAG);
218       if (extraTagValue == null) {
219         throw new UnsupportedOperationException("TagContext must contain EXTRA_TAG");
220       }
221       return (EXTRA_TAG_HEADER_VALUE_PREFIX + extraTagValue.asString()).getBytes(UTF_8);
222     }
223   }
224 
225   public static final class FakeStatsRecord extends MeasureMap {
226 
227     private final BlockingQueue<MetricsRecord> recordSink;
228     public final Map<Measure, Number> metrics = Maps.newHashMap();
229 
FakeStatsRecord(FakeStatsRecorder statsRecorder)230     private FakeStatsRecord(FakeStatsRecorder statsRecorder) {
231       this.recordSink = statsRecorder.getCurrentRecordSink();
232     }
233 
234     @Override
put(Measure.MeasureDouble measure, double value)235     public MeasureMap put(Measure.MeasureDouble measure, double value) {
236       metrics.put(measure, value);
237       return this;
238     }
239 
240     @Override
put(Measure.MeasureLong measure, long value)241     public MeasureMap put(Measure.MeasureLong measure, long value) {
242       metrics.put(measure, value);
243       return this;
244     }
245 
246     @Override
record(TagContext tags)247     public void record(TagContext tags) {
248       recordSink.add(new MetricsRecord(getTags(tags), ImmutableMap.copyOf(metrics)));
249     }
250 
251     @Override
record()252     public void record() {
253       throw new UnsupportedOperationException();
254     }
255   }
256 
257   public static final class FakeTagContext extends TagContext {
258 
259     private static final FakeTagContext EMPTY =
260         new FakeTagContext(ImmutableMap.<TagKey, TagValue>of());
261 
262     private static final TagMetadata METADATA_PROPAGATING =
263         TagMetadata.create(TagTtl.UNLIMITED_PROPAGATION);
264 
265     private final ImmutableMap<TagKey, TagValue> tags;
266 
FakeTagContext(ImmutableMap<TagKey, TagValue> tags)267     private FakeTagContext(ImmutableMap<TagKey, TagValue> tags) {
268       this.tags = tags;
269     }
270 
getTags()271     public ImmutableMap<TagKey, TagValue> getTags() {
272       return tags;
273     }
274 
275     @Override
toString()276     public String toString() {
277       return "[tags=" + tags + "]";
278     }
279 
280     @Override
getIterator()281     protected Iterator<Tag> getIterator() {
282       return Iterators.transform(
283           tags.entrySet().iterator(),
284           new Function<Map.Entry<TagKey, TagValue>, Tag>() {
285             @Override
286             public Tag apply(@Nullable Map.Entry<TagKey, TagValue> entry) {
287               return Tag.create(entry.getKey(), entry.getValue(), METADATA_PROPAGATING);
288             }
289           });
290     }
291   }
292 
293   public static class FakeTagContextBuilder extends TagContextBuilder {
294 
295     private final Map<TagKey, TagValue> tagsBuilder = Maps.newHashMap();
296 
297     private FakeTagContextBuilder(Map<TagKey, TagValue> tags) {
298       tagsBuilder.putAll(tags);
299     }
300 
301     @SuppressWarnings("deprecation")
302     @Override
303     public TagContextBuilder put(TagKey key, TagValue value) {
304       tagsBuilder.put(key, value);
305       return this;
306     }
307 
308     @Override
309     public TagContextBuilder remove(TagKey key) {
310       tagsBuilder.remove(key);
311       return this;
312     }
313 
314     @Override
315     public TagContext build() {
316       FakeTagContext context = new FakeTagContext(ImmutableMap.copyOf(tagsBuilder));
317       return context;
318     }
319 
320     @Override
321     public Scope buildScoped() {
322       throw new UnsupportedOperationException();
323     }
324   }
325 
326   // This method handles the default TagContext, which isn't an instance of FakeTagContext.
327   private static ImmutableMap<TagKey, TagValue> getTags(TagContext tags) {
328     return tags instanceof FakeTagContext
329         ? ((FakeTagContext) tags).getTags()
330         : ImmutableMap.<TagKey, TagValue>of();
331   }
332 
333   // TODO(bdrutu): Remove this class after OpenCensus releases support for this class.
334   public static class MockableSpan extends Span {
335     /**
336      * Creates a MockableSpan with a random trace ID and span ID.
337      */
338     @SuppressWarnings("deprecation")
339     public static MockableSpan generateRandomSpan(Random random) {
340       return new MockableSpan(
341           SpanContext.create(
342               TraceId.generateRandomId(random),
343               SpanId.generateRandomId(random),
344               TraceOptions.DEFAULT),
345           null);
346     }
347 
348     @Override
349     public void putAttributes(Map<String, AttributeValue> attributes) {}
350 
351     @Override
352     public void addAnnotation(String description, Map<String, AttributeValue> attributes) {}
353 
354     @Override
355     public void addAnnotation(Annotation annotation) {}
356 
357     @Override
358     public void addMessageEvent(MessageEvent messageEvent) {}
359 
360     @Override
361     public void addLink(Link link) {}
362 
363     @Override
364     public void end(EndSpanOptions options) {}
365 
366     private MockableSpan(SpanContext context, @Nullable EnumSet<Options> options) {
367       super(context, options);
368     }
369 
370     /**
371      * Mockable implementation for the {@link SpanBuilder} class.
372      *
373      * <p>Not {@code final} to allow easy mocking.
374      *
375      */
376     public static class Builder extends SpanBuilder {
377 
378       @Override
379       public SpanBuilder setSampler(Sampler sampler) {
380         return this;
381       }
382 
383       @Override
384       public SpanBuilder setParentLinks(List<Span> parentLinks) {
385         return this;
386       }
387 
388       @Override
389       public SpanBuilder setRecordEvents(boolean recordEvents) {
390         return this;
391       }
392 
393       @Override
394       public Span startSpan() {
395         return null;
396       }
397     }
398   }
399 }
400