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