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