aboutsummaryrefslogtreecommitdiff
path: root/impl_core/src
diff options
context:
space:
mode:
Diffstat (limited to 'impl_core/src')
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/CurrentStatsState.java61
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java86
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java60
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java122
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java185
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java361
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java588
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java72
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java90
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/StatsRecorderImpl.java36
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.java48
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java73
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTaggingState.java41
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/NoopTagContextBuilder.java51
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.java64
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java84
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/TagContextUtils.java33
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java114
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java56
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java171
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java48
-rw-r--r--impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java34
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/CurrentStatsStateTest.java65
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java120
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java147
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/MeasureToViewMapTest.java69
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java249
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java147
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java73
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java207
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java183
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java924
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTagContextUtilsTest.java103
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTaggingStateTest.java43
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java110
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java112
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/TaggerImplTest.java318
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.java49
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java32
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java91
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java199
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java63
-rw-r--r--impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java118
43 files changed, 5900 insertions, 0 deletions
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/CurrentStatsState.java b/impl_core/src/main/java/io/opencensus/implcore/stats/CurrentStatsState.java
new file mode 100644
index 00000000..88a1f79a
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/CurrentStatsState.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * The current {@link StatsCollectionState} for a {@link StatsComponent}.
+ *
+ * <p>This class allows different stats classes to share the state in a thread-safe way.
+ */
+@ThreadSafe
+public final class CurrentStatsState {
+
+ @GuardedBy("this")
+ private StatsCollectionState currentState = StatsCollectionState.ENABLED;
+
+ @GuardedBy("this")
+ private boolean isRead;
+
+ public synchronized StatsCollectionState get() {
+ isRead = true;
+ return getInternal();
+ }
+
+ synchronized StatsCollectionState getInternal() {
+ return currentState;
+ }
+
+ // Sets current state to the given state. Returns true if the current state is changed, false
+ // otherwise.
+ synchronized boolean set(StatsCollectionState state) {
+ checkState(!isRead, "State was already read, cannot set state.");
+ if (state == currentState) {
+ return false;
+ } else {
+ currentState = checkNotNull(state, "state");
+ return true;
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java b/impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java
new file mode 100644
index 00000000..68380791
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/IntervalBucket.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static io.opencensus.implcore.stats.MutableViewData.toMillis;
+
+import com.google.common.collect.Maps;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.tags.TagValue;
+import java.util.List;
+import java.util.Map;
+
+/** The bucket with aggregated {@code MeasureValue}s used for {@code IntervalViewData}. */
+final class IntervalBucket {
+
+ private static final Duration ZERO = Duration.create(0, 0);
+
+ private final Timestamp start;
+ private final Duration duration;
+ private final Aggregation aggregation;
+ private final Map<List<TagValue>, MutableAggregation> tagValueAggregationMap = Maps.newHashMap();
+
+ IntervalBucket(Timestamp start, Duration duration, Aggregation aggregation) {
+ checkNotNull(start, "Start");
+ checkNotNull(duration, "Duration");
+ checkArgument(duration.compareTo(ZERO) > 0, "Duration must be positive");
+ checkNotNull(aggregation, "Aggregation");
+ this.start = start;
+ this.duration = duration;
+ this.aggregation = aggregation;
+ }
+
+ Map<List<TagValue>, MutableAggregation> getTagValueAggregationMap() {
+ return tagValueAggregationMap;
+ }
+
+ Timestamp getStart() {
+ return start;
+ }
+
+ // Puts a new value into the internal MutableAggregations, based on the TagValues.
+ void record(List<TagValue> tagValues, double value) {
+ if (!tagValueAggregationMap.containsKey(tagValues)) {
+ tagValueAggregationMap.put(tagValues, MutableViewData.createMutableAggregation(aggregation));
+ }
+ tagValueAggregationMap.get(tagValues).add(value);
+ }
+
+ /*
+ * Returns how much fraction of duration has passed in this IntervalBucket. For example, if this
+ * bucket starts at 10s and has a duration of 20s, and now is 15s, then getFraction() should
+ * return (15 - 10) / 20 = 0.25.
+ *
+ * This IntervalBucket must be current, i.e. the current timestamp must be within
+ * [this.start, this.start + this.duration).
+ */
+ double getFraction(Timestamp now) {
+ Duration elapsedTime = now.subtractTimestamp(start);
+ checkArgument(
+ elapsedTime.compareTo(ZERO) >= 0 && elapsedTime.compareTo(duration) < 0,
+ "This bucket must be current.");
+ return ((double) toMillis(elapsedTime)) / toMillis(duration);
+ }
+
+ void clearStats() {
+ tagValueAggregationMap.clear();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java
new file mode 100644
index 00000000..a156747f
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016-17, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.unsafe.ContextUtils;
+
+/** Implementation of {@link MeasureMap}. */
+final class MeasureMapImpl extends MeasureMap {
+ private final StatsManager statsManager;
+ private final MeasureMapInternal.Builder builder = MeasureMapInternal.builder();
+
+ static MeasureMapImpl create(StatsManager statsManager) {
+ return new MeasureMapImpl(statsManager);
+ }
+
+ private MeasureMapImpl(StatsManager statsManager) {
+ this.statsManager = statsManager;
+ }
+
+ @Override
+ public MeasureMapImpl put(MeasureDouble measure, double value) {
+ builder.put(measure, value);
+ return this;
+ }
+
+ @Override
+ public MeasureMapImpl put(MeasureLong measure, long value) {
+ builder.put(measure, value);
+ return this;
+ }
+
+ @Override
+ public void record() {
+ // Use the context key directly, to avoid depending on the tags implementation.
+ record(ContextUtils.TAG_CONTEXT_KEY.get());
+ }
+
+ @Override
+ public void record(TagContext tags) {
+ statsManager.record(tags, builder.build());
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java
new file mode 100644
index 00000000..c68b4aff
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureMapInternal.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016-17, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Measurement;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+// TODO(songya): consider combining MeasureMapImpl and this class.
+/** A map from {@link Measure}'s to measured values. */
+final class MeasureMapInternal {
+
+ /** Returns a {@link Builder} for the {@link MeasureMapInternal} class. */
+ static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns an {@link Iterator} over the measure/value mappings in this {@link MeasureMapInternal}.
+ * The {@code Iterator} does not support {@link Iterator#remove()}.
+ */
+ Iterator<Measurement> iterator() {
+ return new MeasureMapInternalIterator();
+ }
+
+ private final ArrayList<Measurement> measurements;
+
+ private MeasureMapInternal(ArrayList<Measurement> measurements) {
+ this.measurements = measurements;
+ }
+
+ /** Builder for the {@link MeasureMapInternal} class. */
+ static class Builder {
+ /**
+ * Associates the {@link MeasureDouble} with the given value. Subsequent updates to the same
+ * {@link MeasureDouble} will overwrite the previous value.
+ *
+ * @param measure the {@link MeasureDouble}
+ * @param value the value to be associated with {@code measure}
+ * @return this
+ */
+ Builder put(MeasureDouble measure, double value) {
+ measurements.add(Measurement.MeasurementDouble.create(measure, value));
+ return this;
+ }
+
+ /**
+ * Associates the {@link MeasureLong} with the given value. Subsequent updates to the same
+ * {@link MeasureLong} will overwrite the previous value.
+ *
+ * @param measure the {@link MeasureLong}
+ * @param value the value to be associated with {@code measure}
+ * @return this
+ */
+ Builder put(MeasureLong measure, long value) {
+ measurements.add(Measurement.MeasurementLong.create(measure, value));
+ return this;
+ }
+
+ /** Constructs a {@link MeasureMapInternal} from the current measurements. */
+ MeasureMapInternal build() {
+ // Note: this makes adding measurements quadratic but is fastest for the sizes of
+ // MeasureMapInternals that we should see. We may want to go to a strategy of sort/eliminate
+ // for larger MeasureMapInternals.
+ for (int i = measurements.size() - 1; i >= 0; i--) {
+ for (int j = i - 1; j >= 0; j--) {
+ if (measurements.get(i).getMeasure() == measurements.get(j).getMeasure()) {
+ measurements.remove(j);
+ j--;
+ }
+ }
+ }
+ return new MeasureMapInternal(measurements);
+ }
+
+ private final ArrayList<Measurement> measurements = new ArrayList<Measurement>();
+
+ private Builder() {}
+ }
+
+ // Provides an unmodifiable Iterator over this instance's measurements.
+ private final class MeasureMapInternalIterator implements Iterator<Measurement> {
+ @Override
+ public boolean hasNext() {
+ return position < length;
+ }
+
+ @Override
+ public Measurement next() {
+ if (position >= measurements.size()) {
+ throw new NoSuchElementException();
+ }
+ return measurements.get(position++);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ private final int length = measurements.size();
+ private int position = 0;
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java
new file mode 100644
index 00000000..50cb236a
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MeasureToViewMap.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import io.opencensus.common.Clock;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measurement;
+import io.opencensus.stats.Measurement.MeasurementDouble;
+import io.opencensus.stats.Measurement.MeasurementLong;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+/** A class that stores a singleton map from {@code MeasureName}s to {@link MutableViewData}s. */
+final class MeasureToViewMap {
+
+ /*
+ * A synchronized singleton map that stores the one-to-many mapping from Measures
+ * to MutableViewDatas.
+ */
+ @GuardedBy("this")
+ private final Multimap<String, MutableViewData> mutableMap =
+ HashMultimap.<String, MutableViewData>create();
+
+ @GuardedBy("this")
+ private final Map<View.Name, View> registeredViews = new HashMap<View.Name, View>();
+
+ // TODO(songya): consider adding a Measure.Name class
+ @GuardedBy("this")
+ private final Map<String, Measure> registeredMeasures = Maps.newHashMap();
+
+ /** Returns a {@link ViewData} corresponding to the given {@link View.Name}. */
+ synchronized ViewData getView(View.Name viewName, Clock clock, StatsCollectionState state) {
+ MutableViewData view = getMutableViewData(viewName);
+ return view == null ? null : view.toViewData(clock.now(), state);
+ }
+
+ @Nullable
+ private synchronized MutableViewData getMutableViewData(View.Name viewName) {
+ View view = registeredViews.get(viewName);
+ if (view == null) {
+ return null;
+ }
+ Collection<MutableViewData> views = mutableMap.get(view.getMeasure().getName());
+ for (MutableViewData viewData : views) {
+ if (viewData.getView().getName().equals(viewName)) {
+ return viewData;
+ }
+ }
+ throw new AssertionError(
+ "Internal error: Not recording stats for view: \""
+ + viewName
+ + "\" registeredViews="
+ + registeredViews
+ + ", mutableMap="
+ + mutableMap);
+ }
+
+ /** Enable stats collection for the given {@link View}. */
+ synchronized void registerView(View view, Clock clock) {
+ View existing = registeredViews.get(view.getName());
+ if (existing != null) {
+ if (existing.equals(view)) {
+ // Ignore views that are already registered.
+ return;
+ } else {
+ throw new IllegalArgumentException(
+ "A different view with the same name is already registered: " + existing);
+ }
+ }
+ Measure measure = view.getMeasure();
+ Measure registeredMeasure = registeredMeasures.get(measure.getName());
+ if (registeredMeasure != null && !registeredMeasure.equals(measure)) {
+ throw new IllegalArgumentException(
+ "A different measure with the same name is already registered: " + registeredMeasure);
+ }
+ registeredViews.put(view.getName(), view);
+ if (registeredMeasure == null) {
+ registeredMeasures.put(measure.getName(), measure);
+ }
+ mutableMap.put(view.getMeasure().getName(), MutableViewData.create(view, clock.now()));
+ }
+
+ // Records stats with a set of tags.
+ synchronized void record(TagContext tags, MeasureMapInternal stats, Timestamp timestamp) {
+ Iterator<Measurement> iterator = stats.iterator();
+ while (iterator.hasNext()) {
+ Measurement measurement = iterator.next();
+ Measure measure = measurement.getMeasure();
+ if (!measure.equals(registeredMeasures.get(measure.getName()))) {
+ // unregistered measures will be ignored.
+ return;
+ }
+ Collection<MutableViewData> views = mutableMap.get(measure.getName());
+ for (MutableViewData view : views) {
+ measurement.match(
+ new RecordDoubleValueFunc(tags, view, timestamp),
+ new RecordLongValueFunc(tags, view, timestamp),
+ Functions.<Void>throwAssertionError());
+ }
+ }
+ }
+
+ // Clear stats for all the current MutableViewData
+ synchronized void clearStats() {
+ for (Entry<String, Collection<MutableViewData>> entry : mutableMap.asMap().entrySet()) {
+ for (MutableViewData mutableViewData : entry.getValue()) {
+ mutableViewData.clearStats();
+ }
+ }
+ }
+
+ // Resume stats collection for all MutableViewData.
+ synchronized void resumeStatsCollection(Timestamp now) {
+ for (Entry<String, Collection<MutableViewData>> entry : mutableMap.asMap().entrySet()) {
+ for (MutableViewData mutableViewData : entry.getValue()) {
+ mutableViewData.resumeStatsCollection(now);
+ }
+ }
+ }
+
+ private static final class RecordDoubleValueFunc implements Function<MeasurementDouble, Void> {
+ @Override
+ public Void apply(MeasurementDouble arg) {
+ view.record(tags, arg.getValue(), timestamp);
+ return null;
+ }
+
+ private final TagContext tags;
+ private final MutableViewData view;
+ private final Timestamp timestamp;
+
+ private RecordDoubleValueFunc(TagContext tags, MutableViewData view, Timestamp timestamp) {
+ this.tags = tags;
+ this.view = view;
+ this.timestamp = timestamp;
+ }
+ }
+
+ private static final class RecordLongValueFunc implements Function<MeasurementLong, Void> {
+ @Override
+ public Void apply(MeasurementLong arg) {
+ view.record(tags, arg.getValue(), timestamp);
+ return null;
+ }
+
+ private final TagContext tags;
+ private final MutableViewData view;
+ private final Timestamp timestamp;
+
+ private RecordLongValueFunc(TagContext tags, MutableViewData view, Timestamp timestamp) {
+ this.tags = tags;
+ this.view = view;
+ this.timestamp = timestamp;
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java
new file mode 100644
index 00000000..deabdce7
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableAggregation.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Function;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.BucketBoundaries;
+
+/** Mutable version of {@link Aggregation} that supports adding values. */
+abstract class MutableAggregation {
+
+ private MutableAggregation() {}
+
+ // Tolerance for double comparison.
+ private static final double TOLERANCE = 1e-6;
+
+ /**
+ * Put a new value into the MutableAggregation.
+ *
+ * @param value new value to be added to population
+ */
+ abstract void add(double value);
+
+ /**
+ * Combine the internal values of this MutableAggregation and value of the given
+ * MutableAggregation, with the given fraction. Then set the internal value of this
+ * MutableAggregation to the combined value.
+ *
+ * @param other the other {@code MutableAggregation}. The type of this and other {@code
+ * MutableAggregation} must match.
+ * @param fraction the fraction that the value in other {@code MutableAggregation} should
+ * contribute. Must be within [0.0, 1.0].
+ */
+ abstract void combine(MutableAggregation other, double fraction);
+
+ /** Applies the given match function to the underlying data type. */
+ abstract <T> T match(
+ Function<? super MutableSum, T> p0,
+ Function<? super MutableCount, T> p1,
+ Function<? super MutableMean, T> p2,
+ Function<? super MutableDistribution, T> p3);
+
+ /** Calculate sum on aggregated {@code MeasureValue}s. */
+ static final class MutableSum extends MutableAggregation {
+
+ private double sum = 0.0;
+
+ private MutableSum() {}
+
+ /**
+ * Construct a {@code MutableSum}.
+ *
+ * @return an empty {@code MutableSum}.
+ */
+ static MutableSum create() {
+ return new MutableSum();
+ }
+
+ @Override
+ void add(double value) {
+ sum += value;
+ }
+
+ @Override
+ void combine(MutableAggregation other, double fraction) {
+ checkArgument(other instanceof MutableSum, "MutableSum expected.");
+ this.sum += fraction * ((MutableSum) other).getSum();
+ }
+
+ /**
+ * Returns the aggregated sum.
+ *
+ * @return the aggregated sum.
+ */
+ double getSum() {
+ return sum;
+ }
+
+ @Override
+ final <T> T match(
+ Function<? super MutableSum, T> p0,
+ Function<? super MutableCount, T> p1,
+ Function<? super MutableMean, T> p2,
+ Function<? super MutableDistribution, T> p3) {
+ return p0.apply(this);
+ }
+ }
+
+ /** Calculate count on aggregated {@code MeasureValue}s. */
+ static final class MutableCount extends MutableAggregation {
+
+ private long count = 0;
+
+ private MutableCount() {}
+
+ /**
+ * Construct a {@code MutableCount}.
+ *
+ * @return an empty {@code MutableCount}.
+ */
+ static MutableCount create() {
+ return new MutableCount();
+ }
+
+ @Override
+ void add(double value) {
+ count++;
+ }
+
+ @Override
+ void combine(MutableAggregation other, double fraction) {
+ checkArgument(other instanceof MutableCount, "MutableCount expected.");
+ this.count += Math.round(fraction * ((MutableCount) other).getCount());
+ }
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ */
+ long getCount() {
+ return count;
+ }
+
+ @Override
+ final <T> T match(
+ Function<? super MutableSum, T> p0,
+ Function<? super MutableCount, T> p1,
+ Function<? super MutableMean, T> p2,
+ Function<? super MutableDistribution, T> p3) {
+ return p1.apply(this);
+ }
+ }
+
+ /** Calculate mean on aggregated {@code MeasureValue}s. */
+ static final class MutableMean extends MutableAggregation {
+
+ private double sum = 0.0;
+ private long count = 0;
+
+ private MutableMean() {}
+
+ /**
+ * Construct a {@code MutableMean}.
+ *
+ * @return an empty {@code MutableMean}.
+ */
+ static MutableMean create() {
+ return new MutableMean();
+ }
+
+ @Override
+ void add(double value) {
+ count++;
+ sum += value;
+ }
+
+ @Override
+ void combine(MutableAggregation other, double fraction) {
+ checkArgument(other instanceof MutableMean, "MutableMean expected.");
+ MutableMean mutableMean = (MutableMean) other;
+ this.count += Math.round(mutableMean.count * fraction);
+ this.sum += mutableMean.sum * fraction;
+ }
+
+ /**
+ * Returns the aggregated mean.
+ *
+ * @return the aggregated mean.
+ */
+ double getMean() {
+ return getCount() == 0 ? 0 : getSum() / getCount();
+ }
+
+ /**
+ * Returns the aggregated count.
+ *
+ * @return the aggregated count.
+ */
+ long getCount() {
+ return count;
+ }
+
+ /**
+ * Returns the aggregated sum.
+ *
+ * @return the aggregated sum.
+ */
+ double getSum() {
+ return sum;
+ }
+
+ @Override
+ final <T> T match(
+ Function<? super MutableSum, T> p0,
+ Function<? super MutableCount, T> p1,
+ Function<? super MutableMean, T> p2,
+ Function<? super MutableDistribution, T> p3) {
+ return p2.apply(this);
+ }
+ }
+
+ /** Calculate distribution stats on aggregated {@code MeasureValue}s. */
+ static final class MutableDistribution extends MutableAggregation {
+
+ private double sum = 0.0;
+ private double mean = 0.0;
+ private long count = 0;
+ private double sumOfSquaredDeviations = 0.0;
+
+ // Initial "impossible" values, that will get reset as soon as first value is added.
+ private double min = Double.POSITIVE_INFINITY;
+ private double max = Double.NEGATIVE_INFINITY;
+
+ private final BucketBoundaries bucketBoundaries;
+ private final long[] bucketCounts;
+
+ private MutableDistribution(BucketBoundaries bucketBoundaries) {
+ this.bucketBoundaries = bucketBoundaries;
+ this.bucketCounts = new long[bucketBoundaries.getBoundaries().size() + 1];
+ }
+
+ /**
+ * Construct a {@code MutableDistribution}.
+ *
+ * @return an empty {@code MutableDistribution}.
+ */
+ static MutableDistribution create(BucketBoundaries bucketBoundaries) {
+ checkNotNull(bucketBoundaries, "bucketBoundaries should not be null.");
+ return new MutableDistribution(bucketBoundaries);
+ }
+
+ @Override
+ void add(double value) {
+ sum += value;
+ count++;
+
+ /*
+ * Update the sum of squared deviations from the mean with the given value. For values
+ * x_i this is Sum[i=1..n]((x_i - mean)^2)
+ *
+ * Computed using Welfords method (see
+ * https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance, or Knuth, "The Art of
+ * Computer Programming", Vol. 2, page 323, 3rd edition)
+ */
+ double deltaFromMean = value - mean;
+ mean += deltaFromMean / count;
+ double deltaFromMean2 = value - mean;
+ sumOfSquaredDeviations += deltaFromMean * deltaFromMean2;
+
+ if (value < min) {
+ min = value;
+ }
+ if (value > max) {
+ max = value;
+ }
+
+ for (int i = 0; i < bucketBoundaries.getBoundaries().size(); i++) {
+ if (value < bucketBoundaries.getBoundaries().get(i)) {
+ bucketCounts[i]++;
+ return;
+ }
+ }
+ bucketCounts[bucketCounts.length - 1]++;
+ }
+
+ // We don't compute fractional MutableDistribution, it's either whole or none.
+ @Override
+ void combine(MutableAggregation other, double fraction) {
+ checkArgument(other instanceof MutableDistribution, "MutableDistribution expected.");
+ if (Math.abs(1.0 - fraction) > TOLERANCE) {
+ return;
+ }
+
+ MutableDistribution mutableDistribution = (MutableDistribution) other;
+ checkArgument(
+ this.bucketBoundaries.equals(mutableDistribution.bucketBoundaries),
+ "Bucket boundaries should match.");
+
+ // Algorithm for calculating the combination of sum of squared deviations:
+ // https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Parallel_algorithm.
+ if (this.count + mutableDistribution.count > 0) {
+ double delta = mutableDistribution.mean - this.mean;
+ this.sumOfSquaredDeviations =
+ this.sumOfSquaredDeviations
+ + mutableDistribution.sumOfSquaredDeviations
+ + Math.pow(delta, 2)
+ * this.count
+ * mutableDistribution.count
+ / (this.count + mutableDistribution.count);
+ }
+
+ this.count += mutableDistribution.count;
+ this.sum += mutableDistribution.sum;
+ this.mean = this.sum / this.count;
+
+ if (mutableDistribution.min < this.min) {
+ this.min = mutableDistribution.min;
+ }
+ if (mutableDistribution.max > this.max) {
+ this.max = mutableDistribution.max;
+ }
+
+ long[] bucketCounts = mutableDistribution.getBucketCounts();
+ for (int i = 0; i < bucketCounts.length; i++) {
+ this.bucketCounts[i] += bucketCounts[i];
+ }
+ }
+
+ double getMean() {
+ return mean;
+ }
+
+ long getCount() {
+ return count;
+ }
+
+ double getMin() {
+ return min;
+ }
+
+ double getMax() {
+ return max;
+ }
+
+ // Returns the aggregated sum of squared deviations.
+ double getSumOfSquaredDeviations() {
+ return sumOfSquaredDeviations;
+ }
+
+ long[] getBucketCounts() {
+ return bucketCounts;
+ }
+
+ @Override
+ final <T> T match(
+ Function<? super MutableSum, T> p0,
+ Function<? super MutableCount, T> p1,
+ Function<? super MutableMean, T> p2,
+ Function<? super MutableDistribution, T> p3) {
+ return p3.apply(this);
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java
new file mode 100644
index 00000000..1347a721
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/MutableViewData.java
@@ -0,0 +1,588 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.stats.MutableAggregation.MutableCount;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSum;
+import io.opencensus.implcore.tags.TagContextImpl;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** A mutable version of {@link ViewData}, used for recording stats and start/end time. */
+abstract class MutableViewData {
+
+ private static final long MILLIS_PER_SECOND = 1000L;
+ private static final long NANOS_PER_MILLI = 1000 * 1000;
+
+ @VisibleForTesting static final TagValue UNKNOWN_TAG_VALUE = null;
+ @VisibleForTesting static final Timestamp ZERO_TIMESTAMP = Timestamp.create(0, 0);
+
+ private final View view;
+
+ private MutableViewData(View view) {
+ this.view = view;
+ }
+
+ /**
+ * Constructs a new {@link MutableViewData}.
+ *
+ * @param view the {@code View} linked with this {@code MutableViewData}.
+ * @param start the start {@code Timestamp}.
+ * @return a {@code MutableViewData}.
+ */
+ static MutableViewData create(final View view, final Timestamp start) {
+ return view.getWindow()
+ .match(
+ new CreateCumulative(view, start),
+ new CreateInterval(view, start),
+ Functions.<MutableViewData>throwAssertionError());
+ }
+
+ /** The {@link View} associated with this {@link ViewData}. */
+ View getView() {
+ return view;
+ }
+
+ /** Record double stats with the given tags. */
+ abstract void record(TagContext context, double value, Timestamp timestamp);
+
+ /** Record long stats with the given tags. */
+ void record(TagContext tags, long value, Timestamp timestamp) {
+ // TODO(songya): shall we check for precision loss here?
+ record(tags, (double) value, timestamp);
+ }
+
+ /** Convert this {@link MutableViewData} to {@link ViewData}. */
+ abstract ViewData toViewData(Timestamp now, StatsCollectionState state);
+
+ // Clear recorded stats.
+ abstract void clearStats();
+
+ // Resume stats collection, and reset Start Timestamp (for CumulativeMutableViewData), or refresh
+ // bucket list (for InternalMutableViewData).
+ abstract void resumeStatsCollection(Timestamp now);
+
+ private static Map<TagKey, TagValue> getTagMap(TagContext ctx) {
+ if (ctx instanceof TagContextImpl) {
+ return ((TagContextImpl) ctx).getTags();
+ } else {
+ Map<TagKey, TagValue> tags = Maps.newHashMap();
+ for (Iterator<Tag> i = InternalUtils.getTags(ctx); i.hasNext(); ) {
+ Tag tag = i.next();
+ tags.put(tag.getKey(), tag.getValue());
+ }
+ return tags;
+ }
+ }
+
+ @VisibleForTesting
+ static List<TagValue> getTagValues(
+ Map<? extends TagKey, ? extends TagValue> tags, List<? extends TagKey> columns) {
+ List<TagValue> tagValues = new ArrayList<TagValue>(columns.size());
+ // Record all the measures in a "Greedy" way.
+ // Every view aggregates every measure. This is similar to doing a GROUPBY view’s keys.
+ for (int i = 0; i < columns.size(); ++i) {
+ TagKey tagKey = columns.get(i);
+ if (!tags.containsKey(tagKey)) {
+ // replace not found key values by “unknown/not set”.
+ tagValues.add(UNKNOWN_TAG_VALUE);
+ } else {
+ tagValues.add(tags.get(tagKey));
+ }
+ }
+ return tagValues;
+ }
+
+ // Returns the milliseconds representation of a Duration.
+ static long toMillis(Duration duration) {
+ return duration.getSeconds() * MILLIS_PER_SECOND + duration.getNanos() / NANOS_PER_MILLI;
+ }
+
+ /**
+ * Create an empty {@link MutableAggregation} based on the given {@link Aggregation}.
+ *
+ * @param aggregation {@code Aggregation}.
+ * @return an empty {@code MutableAggregation}.
+ */
+ @VisibleForTesting
+ static MutableAggregation createMutableAggregation(Aggregation aggregation) {
+ return aggregation.match(
+ CreateMutableSum.INSTANCE,
+ CreateMutableCount.INSTANCE,
+ CreateMutableMean.INSTANCE,
+ CreateMutableDistribution.INSTANCE,
+ Functions.<MutableAggregation>throwIllegalArgumentException());
+ }
+
+ /**
+ * Create an {@link AggregationData} snapshot based on the given {@link MutableAggregation}.
+ *
+ * @param aggregation {@code MutableAggregation}
+ * @param measure {@code Measure}
+ * @return an {@code AggregationData} which is the snapshot of current summary statistics.
+ */
+ @VisibleForTesting
+ static AggregationData createAggregationData(MutableAggregation aggregation, Measure measure) {
+ return aggregation.match(
+ new CreateSumData(measure),
+ CreateCountData.INSTANCE,
+ CreateMeanData.INSTANCE,
+ CreateDistributionData.INSTANCE);
+ }
+
+ // Covert a mapping from TagValues to MutableAggregation, to a mapping from TagValues to
+ // AggregationData.
+ private static <T> Map<T, AggregationData> createAggregationMap(
+ Map<T, MutableAggregation> tagValueAggregationMap, Measure measure) {
+ Map<T, AggregationData> map = Maps.newHashMap();
+ for (Entry<T, MutableAggregation> entry : tagValueAggregationMap.entrySet()) {
+ map.put(entry.getKey(), createAggregationData(entry.getValue(), measure));
+ }
+ return map;
+ }
+
+ private static final class CumulativeMutableViewData extends MutableViewData {
+
+ private Timestamp start;
+ private final Map<List<TagValue>, MutableAggregation> tagValueAggregationMap =
+ Maps.newHashMap();
+
+ private CumulativeMutableViewData(View view, Timestamp start) {
+ super(view);
+ this.start = start;
+ }
+
+ @Override
+ void record(TagContext context, double value, Timestamp timestamp) {
+ List<TagValue> tagValues = getTagValues(getTagMap(context), super.view.getColumns());
+ if (!tagValueAggregationMap.containsKey(tagValues)) {
+ tagValueAggregationMap.put(
+ tagValues, createMutableAggregation(super.view.getAggregation()));
+ }
+ tagValueAggregationMap.get(tagValues).add(value);
+ }
+
+ @Override
+ ViewData toViewData(Timestamp now, StatsCollectionState state) {
+ if (state == StatsCollectionState.ENABLED) {
+ return ViewData.create(
+ super.view,
+ createAggregationMap(tagValueAggregationMap, super.view.getMeasure()),
+ CumulativeData.create(start, now));
+ } else {
+ // If Stats state is DISABLED, return an empty ViewData.
+ return ViewData.create(
+ super.view,
+ Collections.<List<TagValue>, AggregationData>emptyMap(),
+ CumulativeData.create(ZERO_TIMESTAMP, ZERO_TIMESTAMP));
+ }
+ }
+
+ @Override
+ void clearStats() {
+ tagValueAggregationMap.clear();
+ }
+
+ @Override
+ void resumeStatsCollection(Timestamp now) {
+ start = now;
+ }
+ }
+
+ /*
+ * For each IntervalView, we always keep a queue of N + 1 buckets (by default N is 4).
+ * Each bucket has a duration which is interval duration / N.
+ * Ideally:
+ * 1. the buckets should always be up-to-date,
+ * 2. current time should always be within the latest bucket, currently recorded stats should fall
+ * into the latest bucket,
+ * 3. there are always N buckets before the current one, which holds the stats in the past
+ * interval duration.
+ *
+ * When getView() is called, we will extract and combine the stats from the current and past
+ * buckets (part of the stats from the oldest bucket could have expired).
+ *
+ * However, in reality, we couldn't track the status of buckets all the time (keep monitoring and
+ * updating the bucket queue will be expensive). When we call record() or getView(), some or all
+ * of the buckets might be outdated, and we will need to "pad" new buckets to the queue and remove
+ * outdated ones. After refreshing buckets, the bucket queue will able to maintain the three
+ * invariants in the ideal situation.
+ *
+ * For example:
+ * 1. We have an IntervalView which has a duration of 8 seconds, we register this view at 10s.
+ * 2. Initially there will be 5 buckets: [2.0, 4.0), [4.0, 6.0), ..., [10.0, 12.0).
+ * 3. If users don't call record() or getView(), bucket queue will remain as it is, and some
+ * buckets could expire.
+ * 4. Suppose record() is called at 15s, now we need to refresh the bucket queue. We need to add
+ * two new buckets [12.0, 14.0) and [14.0, 16.0), and remove two expired buckets [2.0, 4.0)
+ * and [4.0, 6.0)
+ * 5. Suppose record() is called again at 30s, all the current buckets should have expired. We add
+ * 5 new buckets [22.0, 24.0) ... [30.0, 32.0) and remove all the previous buckets.
+ * 6. Suppose users call getView() at 35s, again we need to add two new buckets and remove two
+ * expired one, so that bucket queue is up-to-date. Now we combine stats from all buckets and
+ * return the combined IntervalViewData.
+ */
+ private static final class IntervalMutableViewData extends MutableViewData {
+
+ // TODO(songya): allow customizable bucket size in the future.
+ private static final int N = 4; // IntervalView has N + 1 buckets
+
+ private final LinkedList<IntervalBucket> buckets = new LinkedList<IntervalBucket>();
+ private final Duration totalDuration; // Duration of the whole interval.
+ private final Duration bucketDuration; // Duration of a single bucket (totalDuration / N)
+
+ private IntervalMutableViewData(View view, Timestamp start) {
+ super(view);
+ Duration totalDuration = ((Interval) view.getWindow()).getDuration();
+ this.totalDuration = totalDuration;
+ this.bucketDuration = Duration.fromMillis(toMillis(totalDuration) / N);
+
+ // When initializing. add N empty buckets prior to the start timestamp of this
+ // IntervalMutableViewData, so that the last bucket will be the current one in effect.
+ shiftBucketList(N + 1, start);
+ }
+
+ @Override
+ void record(TagContext context, double value, Timestamp timestamp) {
+ List<TagValue> tagValues = getTagValues(getTagMap(context), super.view.getColumns());
+ refreshBucketList(timestamp);
+ // It is always the last bucket that does the recording.
+ buckets.peekLast().record(tagValues, value);
+ }
+
+ @Override
+ ViewData toViewData(Timestamp now, StatsCollectionState state) {
+ refreshBucketList(now);
+ if (state == StatsCollectionState.ENABLED) {
+ return ViewData.create(
+ super.view, combineBucketsAndGetAggregationMap(now), IntervalData.create(now));
+ } else {
+ // If Stats state is DISABLED, return an empty ViewData.
+ return ViewData.create(
+ super.view,
+ Collections.<List<TagValue>, AggregationData>emptyMap(),
+ IntervalData.create(ZERO_TIMESTAMP));
+ }
+ }
+
+ @Override
+ void clearStats() {
+ for (IntervalBucket bucket : buckets) {
+ bucket.clearStats();
+ }
+ }
+
+ @Override
+ void resumeStatsCollection(Timestamp now) {
+ // Refresh bucket list to be ready for stats recording, so that if record() is called right
+ // after stats state is turned back on, record() will be faster.
+ refreshBucketList(now);
+ }
+
+ // Add new buckets and remove expired buckets by comparing the current timestamp with
+ // timestamp of the last bucket.
+ private void refreshBucketList(Timestamp now) {
+ if (buckets.size() != N + 1) {
+ throw new AssertionError("Bucket list must have exactly " + (N + 1) + " buckets.");
+ }
+ Timestamp startOfLastBucket = buckets.peekLast().getStart();
+ // TODO(songya): decide what to do when time goes backwards
+ checkArgument(
+ now.compareTo(startOfLastBucket) >= 0,
+ "Current time must be within or after the last bucket.");
+ long elapsedTimeMillis = toMillis(now.subtractTimestamp(startOfLastBucket));
+ long numOfPadBuckets = elapsedTimeMillis / toMillis(bucketDuration);
+
+ shiftBucketList(numOfPadBuckets, now);
+ }
+
+ // Add specified number of new buckets, and remove expired buckets
+ private void shiftBucketList(long numOfPadBuckets, Timestamp now) {
+ Timestamp startOfNewBucket;
+
+ if (!buckets.isEmpty()) {
+ startOfNewBucket = buckets.peekLast().getStart().addDuration(bucketDuration);
+ } else {
+ // Initialize bucket list. Should only enter this block once.
+ startOfNewBucket = subtractDuration(now, totalDuration);
+ }
+
+ if (numOfPadBuckets > N + 1) {
+ // All current buckets expired, need to add N + 1 new buckets. The start time of the latest
+ // bucket will be current time.
+ startOfNewBucket = subtractDuration(now, totalDuration);
+ numOfPadBuckets = N + 1;
+ }
+
+ for (int i = 0; i < numOfPadBuckets; i++) {
+ buckets.add(
+ new IntervalBucket(startOfNewBucket, bucketDuration, super.view.getAggregation()));
+ startOfNewBucket = startOfNewBucket.addDuration(bucketDuration);
+ }
+
+ // removed expired buckets
+ while (buckets.size() > N + 1) {
+ buckets.pollFirst();
+ }
+ }
+
+ // Combine stats within each bucket, aggregate stats by tag values, and return the mapping from
+ // tag values to aggregation data.
+ private Map<List<TagValue>, AggregationData> combineBucketsAndGetAggregationMap(Timestamp now) {
+ Multimap<List<TagValue>, MutableAggregation> multimap = HashMultimap.create();
+ LinkedList<IntervalBucket> shallowCopy = new LinkedList<IntervalBucket>(buckets);
+ Aggregation aggregation = super.view.getAggregation();
+ putBucketsIntoMultiMap(shallowCopy, multimap, aggregation, now);
+ Map<List<TagValue>, MutableAggregation> singleMap =
+ aggregateOnEachTagValueList(multimap, aggregation);
+ return createAggregationMap(singleMap, super.getView().getMeasure());
+ }
+
+ // Put stats within each bucket to a multimap. Each tag value list (map key) could have multiple
+ // mutable aggregations (map value) from different buckets.
+ private static void putBucketsIntoMultiMap(
+ LinkedList<IntervalBucket> buckets,
+ Multimap<List<TagValue>, MutableAggregation> multimap,
+ Aggregation aggregation,
+ Timestamp now) {
+ // Put fractional stats of the head (oldest) bucket.
+ IntervalBucket head = buckets.peekFirst();
+ IntervalBucket tail = buckets.peekLast();
+ double fractionTail = tail.getFraction(now);
+ // TODO(songya): decide what to do when time goes backwards
+ checkArgument(
+ 0.0 <= fractionTail && fractionTail <= 1.0,
+ "Fraction " + fractionTail + " should be within [0.0, 1.0].");
+ double fractionHead = 1.0 - fractionTail;
+ putFractionalMutableAggregationsToMultiMap(
+ head.getTagValueAggregationMap(), multimap, aggregation, fractionHead);
+
+ // Put whole data of other buckets.
+ boolean shouldSkipFirst = true;
+ for (IntervalBucket bucket : buckets) {
+ if (shouldSkipFirst) {
+ shouldSkipFirst = false;
+ continue; // skip the first bucket
+ }
+ for (Entry<List<TagValue>, MutableAggregation> entry :
+ bucket.getTagValueAggregationMap().entrySet()) {
+ multimap.put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ // Put stats within one bucket into multimap, multiplied by a given fraction.
+ private static <T> void putFractionalMutableAggregationsToMultiMap(
+ Map<T, MutableAggregation> mutableAggrMap,
+ Multimap<T, MutableAggregation> multimap,
+ Aggregation aggregation,
+ double fraction) {
+ for (Entry<T, MutableAggregation> entry : mutableAggrMap.entrySet()) {
+ // Initially empty MutableAggregations.
+ MutableAggregation fractionalMutableAgg = createMutableAggregation(aggregation);
+ fractionalMutableAgg.combine(entry.getValue(), fraction);
+ multimap.put(entry.getKey(), fractionalMutableAgg);
+ }
+ }
+
+ // For each tag value list (key of AggregationMap), combine mutable aggregations into one
+ // mutable aggregation, thus convert the multimap into a single map.
+ private static <T> Map<T, MutableAggregation> aggregateOnEachTagValueList(
+ Multimap<T, MutableAggregation> multimap, Aggregation aggregation) {
+ Map<T, MutableAggregation> map = Maps.newHashMap();
+ for (T tagValues : multimap.keySet()) {
+ // Initially empty MutableAggregations.
+ MutableAggregation combinedAggregation = createMutableAggregation(aggregation);
+ for (MutableAggregation mutableAggregation : multimap.get(tagValues)) {
+ combinedAggregation.combine(mutableAggregation, 1.0);
+ }
+ map.put(tagValues, combinedAggregation);
+ }
+ return map;
+ }
+
+ // Subtract a Duration from a Timestamp, and return a new Timestamp.
+ private static Timestamp subtractDuration(Timestamp timestamp, Duration duration) {
+ return timestamp.addDuration(Duration.create(-duration.getSeconds(), -duration.getNanos()));
+ }
+ }
+
+ // static inner Function classes
+
+ private static final class CreateMutableSum implements Function<Sum, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(Sum arg) {
+ return MutableSum.create();
+ }
+
+ private static final CreateMutableSum INSTANCE = new CreateMutableSum();
+ }
+
+ private static final class CreateMutableCount implements Function<Count, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(Count arg) {
+ return MutableCount.create();
+ }
+
+ private static final CreateMutableCount INSTANCE = new CreateMutableCount();
+ }
+
+ private static final class CreateMutableMean implements Function<Mean, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(Mean arg) {
+ return MutableMean.create();
+ }
+
+ private static final CreateMutableMean INSTANCE = new CreateMutableMean();
+ }
+
+ private static final class CreateMutableDistribution
+ implements Function<Distribution, MutableAggregation> {
+ @Override
+ public MutableAggregation apply(Distribution arg) {
+ return MutableDistribution.create(arg.getBucketBoundaries());
+ }
+
+ private static final CreateMutableDistribution INSTANCE = new CreateMutableDistribution();
+ }
+
+ private static final class CreateSumData implements Function<MutableSum, AggregationData> {
+
+ private final Measure measure;
+
+ private CreateSumData(Measure measure) {
+ this.measure = measure;
+ }
+
+ @Override
+ public AggregationData apply(final MutableSum arg) {
+ return measure.match(
+ Functions.<AggregationData>returnConstant(SumDataDouble.create(arg.getSum())),
+ Functions.<AggregationData>returnConstant(SumDataLong.create(Math.round(arg.getSum()))),
+ Functions.<AggregationData>throwAssertionError());
+ }
+ }
+
+ private static final class CreateCountData implements Function<MutableCount, AggregationData> {
+ @Override
+ public AggregationData apply(MutableCount arg) {
+ return CountData.create(arg.getCount());
+ }
+
+ private static final CreateCountData INSTANCE = new CreateCountData();
+ }
+
+ private static final class CreateMeanData implements Function<MutableMean, AggregationData> {
+ @Override
+ public AggregationData apply(MutableMean arg) {
+ return MeanData.create(arg.getMean(), arg.getCount());
+ }
+
+ private static final CreateMeanData INSTANCE = new CreateMeanData();
+ }
+
+ private static final class CreateDistributionData
+ implements Function<MutableDistribution, AggregationData> {
+ @Override
+ public AggregationData apply(MutableDistribution arg) {
+ List<Long> boxedBucketCounts = new ArrayList<Long>();
+ for (long bucketCount : arg.getBucketCounts()) {
+ boxedBucketCounts.add(bucketCount);
+ }
+ return DistributionData.create(
+ arg.getMean(),
+ arg.getCount(),
+ arg.getMin(),
+ arg.getMax(),
+ arg.getSumOfSquaredDeviations(),
+ boxedBucketCounts);
+ }
+
+ private static final CreateDistributionData INSTANCE = new CreateDistributionData();
+ }
+
+ private static final class CreateCumulative implements Function<Cumulative, MutableViewData> {
+ @Override
+ public MutableViewData apply(Cumulative arg) {
+ return new CumulativeMutableViewData(view, start);
+ }
+
+ private final View view;
+ private final Timestamp start;
+
+ private CreateCumulative(View view, Timestamp start) {
+ this.view = view;
+ this.start = start;
+ }
+ }
+
+ private static final class CreateInterval implements Function<Interval, MutableViewData> {
+ @Override
+ public MutableViewData apply(Interval arg) {
+ return new IntervalMutableViewData(view, start);
+ }
+
+ private final View view;
+ private final Timestamp start;
+
+ private CreateInterval(View view, Timestamp start) {
+ this.view = view;
+ this.start = start;
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java
new file mode 100644
index 00000000..5079e27c
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsComponentImplBase.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import com.google.common.base.Preconditions;
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+
+/** Base implementation of {@link StatsComponent}. */
+public class StatsComponentImplBase extends StatsComponent {
+
+ // The StatsCollectionState shared between the StatsComponent, StatsRecorder and ViewManager.
+ private final CurrentStatsState state = new CurrentStatsState();
+
+ private final ViewManagerImpl viewManager;
+ private final StatsRecorderImpl statsRecorder;
+
+ /**
+ * Creates a new {@code StatsComponentImplBase}.
+ *
+ * @param queue the queue implementation.
+ * @param clock the clock to use when recording stats.
+ */
+ public StatsComponentImplBase(EventQueue queue, Clock clock) {
+ StatsManager statsManager = new StatsManager(queue, clock, state);
+ this.viewManager = new ViewManagerImpl(statsManager);
+ this.statsRecorder = new StatsRecorderImpl(statsManager);
+ }
+
+ @Override
+ public ViewManagerImpl getViewManager() {
+ return viewManager;
+ }
+
+ @Override
+ public StatsRecorderImpl getStatsRecorder() {
+ return statsRecorder;
+ }
+
+ @Override
+ public StatsCollectionState getState() {
+ return state.get();
+ }
+
+ @Override
+ public void setState(StatsCollectionState newState) {
+ boolean stateChanged = state.set(Preconditions.checkNotNull(newState, "newState"));
+ if (stateChanged) {
+ if (newState == StatsCollectionState.DISABLED) {
+ viewManager.clearStats();
+ } else {
+ viewManager.resumeStatsCollection();
+ }
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java
new file mode 100644
index 00000000..63fcd587
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsManager.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Clock;
+import io.opencensus.implcore.internal.EventQueue;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.tags.TagContext;
+
+/** Object that stores all views and stats. */
+final class StatsManager {
+
+ private final EventQueue queue;
+
+ // clock used throughout the stats implementation
+ private final Clock clock;
+
+ private final CurrentStatsState state;
+ private final MeasureToViewMap measureToViewMap = new MeasureToViewMap();
+
+ StatsManager(EventQueue queue, Clock clock, CurrentStatsState state) {
+ checkNotNull(queue, "EventQueue");
+ checkNotNull(clock, "Clock");
+ checkNotNull(state, "state");
+ this.queue = queue;
+ this.clock = clock;
+ this.state = state;
+ }
+
+ void registerView(View view) {
+ measureToViewMap.registerView(view, clock);
+ }
+
+ ViewData getView(View.Name viewName) {
+ return measureToViewMap.getView(viewName, clock, state.getInternal());
+ }
+
+ void record(TagContext tags, MeasureMapInternal measurementValues) {
+ // TODO(songya): consider exposing No-op MeasureMap and use it when stats state is DISABLED, so
+ // that we don't need to create actual MeasureMapImpl.
+ if (state.getInternal() == StatsCollectionState.ENABLED) {
+ queue.enqueue(new StatsEvent(this, tags, measurementValues));
+ }
+ }
+
+ void clearStats() {
+ measureToViewMap.clearStats();
+ }
+
+ void resumeStatsCollection() {
+ measureToViewMap.resumeStatsCollection(clock.now());
+ }
+
+ // An EventQueue entry that records the stats from one call to StatsManager.record(...).
+ private static final class StatsEvent implements EventQueue.Entry {
+ private final TagContext tags;
+ private final MeasureMapInternal stats;
+ private final StatsManager statsManager;
+
+ StatsEvent(StatsManager statsManager, TagContext tags, MeasureMapInternal stats) {
+ this.statsManager = statsManager;
+ this.tags = tags;
+ this.stats = stats;
+ }
+
+ @Override
+ public void process() {
+ // Add Timestamp to value after it went through the DisruptorQueue.
+ statsManager.measureToViewMap.record(tags, stats, statsManager.clock.now());
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/StatsRecorderImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsRecorderImpl.java
new file mode 100644
index 00000000..f9ebea41
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/StatsRecorderImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.stats.StatsRecorder;
+
+/** Implementation of {@link StatsRecorder}. */
+public final class StatsRecorderImpl extends StatsRecorder {
+ private final StatsManager statsManager;
+
+ StatsRecorderImpl(StatsManager statsManager) {
+ checkNotNull(statsManager, "StatsManager");
+ this.statsManager = statsManager;
+ }
+
+ @Override
+ public MeasureMapImpl newMeasureMap() {
+ return MeasureMapImpl.create(statsManager);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.java
new file mode 100644
index 00000000..eb716689
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/stats/ViewManagerImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewManager;
+
+/** Implementation of {@link ViewManager}. */
+public final class ViewManagerImpl extends ViewManager {
+ private final StatsManager statsManager;
+
+ ViewManagerImpl(StatsManager statsManager) {
+ this.statsManager = statsManager;
+ }
+
+ @Override
+ public void registerView(View view) {
+ statsManager.registerView(view);
+ }
+
+ @Override
+ public ViewData getView(View.Name viewName) {
+ return statsManager.getView(viewName);
+ }
+
+ void clearStats() {
+ statsManager.clearStats();
+ }
+
+ void resumeStatsCollection() {
+ statsManager.resumeStatsCollection();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java b/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java
new file mode 100644
index 00000000..1a4ef81b
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTagContextUtils.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.unsafe.ContextUtils;
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for accessing the {@link TagContext} contained in the {@link io.grpc.Context}.
+ */
+final class CurrentTagContextUtils {
+
+ private CurrentTagContextUtils() {}
+
+ /**
+ * Returns the {@link TagContext} from the current context.
+ *
+ * @return the {@code TagContext} from the current context.
+ */
+ @Nullable
+ static TagContext getCurrentTagContext() {
+ return ContextUtils.TAG_CONTEXT_KEY.get();
+ }
+
+ /**
+ * Enters the scope of code where the given {@link TagContext} is in the current context and
+ * returns an object that represents that scope. The scope is exited when the returned object is
+ * closed.
+ *
+ * @param tags the {@code TagContext} to be set to the current context.
+ * @return an object that defines a scope where the given {@code TagContext} is set to the current
+ * context.
+ */
+ static Scope withTagContext(TagContext tags) {
+ return new WithTagContext(tags);
+ }
+
+ private static final class WithTagContext implements Scope {
+
+ private final Context orig;
+
+ /**
+ * Constructs a new {@link WithTagContext}.
+ *
+ * @param tags the {@code TagContext} to be added to the current {@code Context}.
+ */
+ private WithTagContext(TagContext tags) {
+ orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, tags).attach();
+ }
+
+ @Override
+ public void close() {
+ Context.current().detach(orig);
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTaggingState.java b/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTaggingState.java
new file mode 100644
index 00000000..ab60ffa1
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/CurrentTaggingState.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * The current {@link TaggingState} for a {@link TagsComponent}.
+ *
+ * <p>This class allows different tagging classes to share the state in a thread-safe way.
+ */
+@ThreadSafe
+public final class CurrentTaggingState {
+ private volatile TaggingState currentState = TaggingState.ENABLED;
+
+ public TaggingState get() {
+ return currentState;
+ }
+
+ void set(TaggingState state) {
+ currentState = checkNotNull(state, "state");
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/NoopTagContextBuilder.java b/impl_core/src/main/java/io/opencensus/implcore/tags/NoopTagContextBuilder.java
new file mode 100644
index 00000000..eae54c5d
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/NoopTagContextBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.NoopScope;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+
+/** {@link TagContextBuilder} that is used when tagging is disabled. */
+final class NoopTagContextBuilder extends TagContextBuilder {
+ static final NoopTagContextBuilder INSTANCE = new NoopTagContextBuilder();
+
+ private NoopTagContextBuilder() {}
+
+ @Override
+ public TagContextBuilder put(TagKey key, TagValue value) {
+ return this;
+ }
+
+ @Override
+ public TagContextBuilder remove(TagKey key) {
+ return this;
+ }
+
+ @Override
+ public TagContext build() {
+ return TagContextImpl.EMPTY;
+ }
+
+ @Override
+ public Scope buildScoped() {
+ return NoopScope.getInstance();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.java
new file mode 100644
index 00000000..1f473454
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextBuilderImpl.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.common.Scope;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.HashMap;
+import java.util.Map;
+
+final class TagContextBuilderImpl extends TagContextBuilder {
+ private final Map<TagKey, TagValue> tags;
+
+ TagContextBuilderImpl(Map<TagKey, TagValue> tags) {
+ this.tags = new HashMap<TagKey, TagValue>(tags);
+ }
+
+ TagContextBuilderImpl() {
+ this.tags = new HashMap<TagKey, TagValue>();
+ }
+
+ @Override
+ public TagContextBuilderImpl put(TagKey key, TagValue value) {
+ return setInternal(key, checkNotNull(value, "value"));
+ }
+
+ private TagContextBuilderImpl setInternal(TagKey key, TagValue value) {
+ tags.put(checkNotNull(key), value);
+ return this;
+ }
+
+ @Override
+ public TagContextBuilderImpl remove(TagKey key) {
+ tags.remove(key);
+ return this;
+ }
+
+ @Override
+ public TagContextImpl build() {
+ return new TagContextImpl(tags);
+ }
+
+ @Override
+ public Scope buildScoped() {
+ return CurrentTagContextUtils.withTagContext(build());
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java
new file mode 100644
index 00000000..177ce639
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextImpl.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public final class TagContextImpl extends TagContext {
+
+ public static final TagContextImpl EMPTY =
+ new TagContextImpl(Collections.<TagKey, TagValue>emptyMap());
+
+ // The types of the TagKey and value must match for each entry.
+ private final Map<TagKey, TagValue> tags;
+
+ public TagContextImpl(Map<? extends TagKey, ? extends TagValue> tags) {
+ this.tags = Collections.unmodifiableMap(new HashMap<TagKey, TagValue>(tags));
+ }
+
+ public Map<TagKey, TagValue> getTags() {
+ return tags;
+ }
+
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return new TagIterator(tags);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ // Directly compare the tags when both objects are TagContextImpls, for efficiency.
+ if (other instanceof TagContextImpl) {
+ return getTags().equals(((TagContextImpl) other).getTags());
+ }
+ return super.equals(other);
+ }
+
+ private static final class TagIterator implements Iterator<Tag> {
+ Iterator<Map.Entry<TagKey, TagValue>> iterator;
+
+ TagIterator(Map<TagKey, TagValue> tags) {
+ iterator = tags.entrySet().iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public Tag next() {
+ final Entry<TagKey, TagValue> next = iterator.next();
+ return Tag.create(next.getKey(), next.getValue());
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("TagIterator.remove()");
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextUtils.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextUtils.java
new file mode 100644
index 00000000..5fbc5050
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagContextUtils.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import io.opencensus.tags.Tag;
+
+final class TagContextUtils {
+ private TagContextUtils() {}
+
+ /**
+ * Add a {@code Tag} of any type to a builder.
+ *
+ * @param tag tag containing the key and value to set.
+ * @param builder the builder to update.
+ */
+ static void addTagToBuilder(Tag tag, TagContextBuilderImpl builder) {
+ builder.put(tag.getKey(), tag.getValue());
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java
new file mode 100644
index 00000000..82c1d8cf
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TaggerImpl.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.NoopScope;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TaggingState;
+import java.util.Iterator;
+
+public final class TaggerImpl extends Tagger {
+ // All methods in this class use TagContextImpl and TagContextBuilderImpl. For example,
+ // withTagContext(...) always puts a TagContextImpl into scope, even if the argument is another
+ // TagContext subclass.
+
+ private final CurrentTaggingState state;
+
+ TaggerImpl(CurrentTaggingState state) {
+ this.state = state;
+ }
+
+ @Override
+ public TagContextImpl empty() {
+ return TagContextImpl.EMPTY;
+ }
+
+ @Override
+ public TagContextImpl getCurrentTagContext() {
+ return state.get() == TaggingState.DISABLED
+ ? TagContextImpl.EMPTY
+ : toTagContextImpl(CurrentTagContextUtils.getCurrentTagContext());
+ }
+
+ @Override
+ public TagContextBuilder emptyBuilder() {
+ return state.get() == TaggingState.DISABLED
+ ? NoopTagContextBuilder.INSTANCE
+ : new TagContextBuilderImpl();
+ }
+
+ @Override
+ public TagContextBuilder currentBuilder() {
+ return state.get() == TaggingState.DISABLED
+ ? NoopTagContextBuilder.INSTANCE
+ : toBuilder(CurrentTagContextUtils.getCurrentTagContext());
+ }
+
+ @Override
+ public TagContextBuilder toBuilder(TagContext tags) {
+ return state.get() == TaggingState.DISABLED
+ ? NoopTagContextBuilder.INSTANCE
+ : toTagContextBuilderImpl(tags);
+ }
+
+ @Override
+ public Scope withTagContext(TagContext tags) {
+ return state.get() == TaggingState.DISABLED
+ ? NoopScope.getInstance()
+ : CurrentTagContextUtils.withTagContext(toTagContextImpl(tags));
+ }
+
+ private static TagContextImpl toTagContextImpl(TagContext tags) {
+ if (tags instanceof TagContextImpl) {
+ return (TagContextImpl) tags;
+ } else {
+ Iterator<Tag> i = InternalUtils.getTags(tags);
+ if (!i.hasNext()) {
+ return TagContextImpl.EMPTY;
+ }
+ TagContextBuilderImpl builder = new TagContextBuilderImpl();
+ while (i.hasNext()) {
+ Tag tag = i.next();
+ if (tag != null) {
+ TagContextUtils.addTagToBuilder(tag, builder);
+ }
+ }
+ return builder.build();
+ }
+ }
+
+ private static TagContextBuilderImpl toTagContextBuilderImpl(TagContext tags) {
+ // Copy the tags more efficiently in the expected case, when the TagContext is a TagContextImpl.
+ if (tags instanceof TagContextImpl) {
+ return new TagContextBuilderImpl(((TagContextImpl) tags).getTags());
+ } else {
+ TagContextBuilderImpl builder = new TagContextBuilderImpl();
+ for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext(); ) {
+ Tag tag = i.next();
+ if (tag != null) {
+ TagContextUtils.addTagToBuilder(tag, builder);
+ }
+ }
+ return builder;
+ }
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java b/impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java
new file mode 100644
index 00000000..216afdc9
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/TagsComponentImplBase.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import io.opencensus.implcore.tags.propagation.TagPropagationComponentImpl;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+
+/** Base implementation of {@link TagsComponent}. */
+public class TagsComponentImplBase extends TagsComponent {
+
+ // The TaggingState shared between the TagsComponent, Tagger, and TagPropagationComponent
+ private final CurrentTaggingState state = new CurrentTaggingState();
+
+ private final Tagger tagger = new TaggerImpl(state);
+ private final TagPropagationComponent tagPropagationComponent =
+ new TagPropagationComponentImpl(state);
+
+ @Override
+ public Tagger getTagger() {
+ return tagger;
+ }
+
+ @Override
+ public TagPropagationComponent getTagPropagationComponent() {
+ return tagPropagationComponent;
+ }
+
+ @Override
+ public TaggingState getState() {
+ return state.get();
+ }
+
+ @Override
+ public void setState(TaggingState newState) {
+ state.set(checkNotNull(newState, "newState"));
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java
new file mode 100644
index 00000000..d889a4b8
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/SerializationUtils.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2016-17, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags.propagation;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteArrayDataOutput;
+import com.google.common.io.ByteStreams;
+import io.opencensus.implcore.internal.VarInt;
+import io.opencensus.implcore.tags.TagContextImpl;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Methods for serializing and deserializing {@link TagContext}s.
+ *
+ * <p>The format defined in this class is shared across all implementations of OpenCensus. It allows
+ * tags to propagate across requests.
+ *
+ * <p>OpenCensus tag context encoding:
+ *
+ * <ul>
+ * <li>Tags are encoded in single byte sequence. The version 0 format is:
+ * <li>{@code <version_id><encoded_tags>}
+ * <li>{@code <version_id> == a single byte, value 0}
+ * <li>{@code <encoded_tags> == (<tag_field_id><tag_encoding>)*}
+ * <ul>
+ * <li>{@code <tag_field_id>} == a single byte, value 0
+ * <li>{@code <tag_encoding>}:
+ * <ul>
+ * <li>{@code <tag_key_len><tag_key><tag_val_len><tag_val>}
+ * <ul>
+ * <li>{@code <tag_key_len>} == varint encoded integer
+ * <li>{@code <tag_key>} == tag_key_len bytes comprising tag key name
+ * <li>{@code <tag_val_len>} == varint encoded integer
+ * <li>{@code <tag_val>} == tag_val_len bytes comprising UTF-8 string
+ * </ul>
+ * </ul>
+ * </ul>
+ * </ul>
+ */
+final class SerializationUtils {
+ private SerializationUtils() {}
+
+ @VisibleForTesting static final int VERSION_ID = 0;
+ @VisibleForTesting static final int TAG_FIELD_ID = 0;
+
+ // Serializes a TagContext to the on-the-wire format.
+ // Encoded tags are of the form: <version_id><encoded_tags>
+ static byte[] serializeBinary(TagContext tags) {
+ // Use a ByteArrayDataOutput to avoid needing to handle IOExceptions.
+ final ByteArrayDataOutput byteArrayDataOutput = ByteStreams.newDataOutput();
+ byteArrayDataOutput.write(VERSION_ID);
+ for (Iterator<Tag> i = InternalUtils.getTags(tags); i.hasNext(); ) {
+ Tag tag = i.next();
+ encodeTag(tag, byteArrayDataOutput);
+ }
+ return byteArrayDataOutput.toByteArray();
+ }
+
+ // Deserializes input to TagContext based on the binary format standard.
+ // The encoded tags are of the form: <version_id><encoded_tags>
+ static TagContextImpl deserializeBinary(byte[] bytes) throws TagContextDeserializationException {
+ try {
+ if (bytes.length == 0) {
+ // Does not allow empty byte array.
+ throw new TagContextDeserializationException("Input byte[] can not be empty.");
+ }
+
+ ByteBuffer buffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
+ int versionId = buffer.get();
+ if (versionId != VERSION_ID) {
+ throw new TagContextDeserializationException(
+ "Wrong Version ID: " + versionId + ". Currently supported version is: " + VERSION_ID);
+ }
+ return new TagContextImpl(parseTags(buffer));
+ } catch (BufferUnderflowException exn) {
+ throw new TagContextDeserializationException(exn.toString()); // byte array format error.
+ }
+ }
+
+ private static Map<TagKey, TagValue> parseTags(ByteBuffer buffer)
+ throws TagContextDeserializationException {
+ Map<TagKey, TagValue> tags = new HashMap<TagKey, TagValue>();
+ int limit = buffer.limit();
+ while (buffer.position() < limit) {
+ int type = buffer.get();
+ if (type == TAG_FIELD_ID) {
+ TagKey key = createTagKey(decodeString(buffer));
+ TagValue val = createTagValue(key, decodeString(buffer));
+ tags.put(key, val);
+ } else {
+ // Stop parsing at the first unknown field ID, since there is no way to know its length.
+ // TODO(sebright): Consider storing the rest of the byte array in the TagContext.
+ return tags;
+ }
+ }
+ return tags;
+ }
+
+ // TODO(sebright): Consider exposing a TagKey name validation method to avoid needing to catch an
+ // IllegalArgumentException here.
+ private static final TagKey createTagKey(String name) throws TagContextDeserializationException {
+ try {
+ return TagKey.create(name);
+ } catch (IllegalArgumentException e) {
+ throw new TagContextDeserializationException("Invalid tag key: " + name, e);
+ }
+ }
+
+ // TODO(sebright): Consider exposing a TagValue validation method to avoid needing to catch
+ // an IllegalArgumentException here.
+ private static final TagValue createTagValue(TagKey key, String value)
+ throws TagContextDeserializationException {
+ try {
+ return TagValue.create(value);
+ } catch (IllegalArgumentException e) {
+ throw new TagContextDeserializationException(
+ "Invalid tag value for key " + key + ": " + value, e);
+ }
+ }
+
+ private static final void encodeTag(Tag tag, ByteArrayDataOutput byteArrayDataOutput) {
+ byteArrayDataOutput.write(TAG_FIELD_ID);
+ encodeString(tag.getKey().getName(), byteArrayDataOutput);
+ encodeString(tag.getValue().asString(), byteArrayDataOutput);
+ }
+
+ private static final void encodeString(String input, ByteArrayDataOutput byteArrayDataOutput) {
+ putVarInt(input.length(), byteArrayDataOutput);
+ byteArrayDataOutput.write(input.getBytes(Charsets.UTF_8));
+ }
+
+ private static final void putVarInt(int input, ByteArrayDataOutput byteArrayDataOutput) {
+ byte[] output = new byte[VarInt.varIntSize(input)];
+ VarInt.putVarInt(input, output, 0);
+ byteArrayDataOutput.write(output);
+ }
+
+ private static final String decodeString(ByteBuffer buffer) {
+ int length = VarInt.getVarInt(buffer);
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ builder.append((char) buffer.get());
+ }
+ return builder.toString();
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java
new file mode 100644
index 00000000..eea24a33
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImpl.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags.propagation;
+
+import io.opencensus.implcore.tags.CurrentTaggingState;
+import io.opencensus.implcore.tags.TagContextImpl;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+
+final class TagContextBinarySerializerImpl extends TagContextBinarySerializer {
+ private static final byte[] EMPTY_BYTE_ARRAY = {};
+
+ private final CurrentTaggingState state;
+
+ TagContextBinarySerializerImpl(CurrentTaggingState state) {
+ this.state = state;
+ }
+
+ @Override
+ public byte[] toByteArray(TagContext tags) {
+ return state.get() == TaggingState.DISABLED
+ ? EMPTY_BYTE_ARRAY
+ : SerializationUtils.serializeBinary(tags);
+ }
+
+ @Override
+ public TagContext fromByteArray(byte[] bytes) throws TagContextDeserializationException {
+ return state.get() == TaggingState.DISABLED
+ ? TagContextImpl.EMPTY
+ : SerializationUtils.deserializeBinary(bytes);
+ }
+}
diff --git a/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java
new file mode 100644
index 00000000..5def0758
--- /dev/null
+++ b/impl_core/src/main/java/io/opencensus/implcore/tags/propagation/TagPropagationComponentImpl.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags.propagation;
+
+import io.opencensus.implcore.tags.CurrentTaggingState;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagPropagationComponent;
+
+public final class TagPropagationComponentImpl extends TagPropagationComponent {
+ private final TagContextBinarySerializer tagContextBinarySerializer;
+
+ public TagPropagationComponentImpl(CurrentTaggingState state) {
+ tagContextBinarySerializer = new TagContextBinarySerializerImpl(state);
+ }
+
+ @Override
+ public TagContextBinarySerializer getBinarySerializer() {
+ return tagContextBinarySerializer;
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/CurrentStatsStateTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/CurrentStatsStateTest.java
new file mode 100644
index 00000000..2fd1ddc7
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/CurrentStatsStateTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.stats.StatsCollectionState;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link CurrentStatsState}. */
+@RunWith(JUnit4.class)
+public final class CurrentStatsStateTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void defaultState() {
+ assertThat(new CurrentStatsState().get()).isEqualTo(StatsCollectionState.ENABLED);
+ }
+
+ @Test
+ public void setState() {
+ CurrentStatsState state = new CurrentStatsState();
+ assertThat(state.set(StatsCollectionState.DISABLED)).isTrue();
+ assertThat(state.getInternal()).isEqualTo(StatsCollectionState.DISABLED);
+ assertThat(state.set(StatsCollectionState.ENABLED)).isTrue();
+ assertThat(state.getInternal()).isEqualTo(StatsCollectionState.ENABLED);
+ assertThat(state.set(StatsCollectionState.ENABLED)).isFalse();
+ }
+
+ @Test
+ public void preventNull() {
+ CurrentStatsState state = new CurrentStatsState();
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("state");
+ state.set(null);
+ }
+
+ @Test
+ public void preventSettingStateAfterReadingState() {
+ CurrentStatsState state = new CurrentStatsState();
+ state.get();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ state.set(StatsCollectionState.DISABLED);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java
new file mode 100644
index 00000000..10bce262
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/IntervalBucketTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.tags.TagValue;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link IntervalBucket}. */
+@RunWith(JUnit4.class)
+public class IntervalBucketTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final double TOLERANCE = 1e-6;
+ private static final Duration MINUTE = Duration.create(60, 0);
+ private static final Duration NEGATIVE_TEN_SEC = Duration.create(-10, 0);
+ private static final Timestamp START = Timestamp.create(60, 0);
+ private static final Mean MEAN = Mean.create();
+
+ @Test
+ public void preventNullStartTime() {
+ thrown.expect(NullPointerException.class);
+ new IntervalBucket(null, MINUTE, MEAN);
+ }
+
+ @Test
+ public void preventNullDuration() {
+ thrown.expect(NullPointerException.class);
+ new IntervalBucket(START, null, MEAN);
+ }
+
+ @Test
+ public void preventNegativeDuration() {
+ thrown.expect(IllegalArgumentException.class);
+ new IntervalBucket(START, NEGATIVE_TEN_SEC, MEAN);
+ }
+
+ @Test
+ public void preventNullAggregationList() {
+ thrown.expect(NullPointerException.class);
+ new IntervalBucket(START, MINUTE, null);
+ }
+
+ @Test
+ public void testGetTagValueAggregationMap_empty() {
+ assertThat(new IntervalBucket(START, MINUTE, MEAN).getTagValueAggregationMap()).isEmpty();
+ }
+
+ @Test
+ public void testGetStart() {
+ assertThat(new IntervalBucket(START, MINUTE, MEAN).getStart()).isEqualTo(START);
+ }
+
+ @Test
+ public void testRecord() {
+ IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN);
+ List<TagValue> tagValues1 = Arrays.<TagValue>asList(TagValue.create("VALUE1"));
+ List<TagValue> tagValues2 = Arrays.<TagValue>asList(TagValue.create("VALUE2"));
+ bucket.record(tagValues1, 5.0);
+ bucket.record(tagValues1, 15.0);
+ bucket.record(tagValues2, 10.0);
+ assertThat(bucket.getTagValueAggregationMap().keySet()).containsExactly(tagValues1, tagValues2);
+ MutableMean mutableMean1 = (MutableMean) bucket.getTagValueAggregationMap().get(tagValues1);
+ MutableMean mutableMean2 = (MutableMean) bucket.getTagValueAggregationMap().get(tagValues2);
+ assertThat(mutableMean1.getSum()).isWithin(TOLERANCE).of(20);
+ assertThat(mutableMean2.getSum()).isWithin(TOLERANCE).of(10);
+ assertThat(mutableMean1.getCount()).isEqualTo(2);
+ assertThat(mutableMean2.getCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void testGetFraction() {
+ Timestamp thirtySecondsAfterStart = Timestamp.create(90, 0);
+ assertThat(new IntervalBucket(START, MINUTE, MEAN).getFraction(thirtySecondsAfterStart))
+ .isWithin(TOLERANCE)
+ .of(0.5);
+ }
+
+ @Test
+ public void preventCallingGetFractionOnPastBuckets() {
+ IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN);
+ Timestamp twoMinutesAfterStart = Timestamp.create(180, 0);
+ thrown.expect(IllegalArgumentException.class);
+ bucket.getFraction(twoMinutesAfterStart);
+ }
+
+ @Test
+ public void preventCallingGetFractionOnFutureBuckets() {
+ IntervalBucket bucket = new IntervalBucket(START, MINUTE, MEAN);
+ Timestamp thirtySecondsBeforeStart = Timestamp.create(30, 0);
+ thrown.expect(IllegalArgumentException.class);
+ bucket.getFraction(thirtySecondsBeforeStart);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java
new file mode 100644
index 00000000..c57baa0b
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureMapInternalTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2016-17, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Lists;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.Measurement;
+import io.opencensus.stats.Measurement.MeasurementDouble;
+import io.opencensus.stats.Measurement.MeasurementLong;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MeasureMapInternal}. */
+@RunWith(JUnit4.class)
+public class MeasureMapInternalTest {
+
+ @Test
+ public void testPutDouble() {
+ MeasureMapInternal metrics = MeasureMapInternal.builder().put(M1, 44.4).build();
+ assertContains(metrics, MeasurementDouble.create(M1, 44.4));
+ }
+
+ @Test
+ public void testPutLong() {
+ MeasureMapInternal metrics = MeasureMapInternal.builder().put(M3, 9999L).put(M4, 8888L).build();
+ assertContains(metrics, MeasurementLong.create(M3, 9999L), MeasurementLong.create(M4, 8888L));
+ }
+
+ @Test
+ public void testCombination() {
+ MeasureMapInternal metrics =
+ MeasureMapInternal.builder()
+ .put(M1, 44.4)
+ .put(M2, 66.6)
+ .put(M3, 9999L)
+ .put(M4, 8888L)
+ .build();
+ assertContains(
+ metrics,
+ MeasurementDouble.create(M1, 44.4),
+ MeasurementDouble.create(M2, 66.6),
+ MeasurementLong.create(M3, 9999L),
+ MeasurementLong.create(M4, 8888L));
+ }
+
+ @Test
+ public void testBuilderEmpty() {
+ MeasureMapInternal metrics = MeasureMapInternal.builder().build();
+ assertContains(metrics);
+ }
+
+ @Test
+ public void testBuilder() {
+ ArrayList<Measurement> expected = new ArrayList<Measurement>(10);
+ MeasureMapInternal.Builder builder = MeasureMapInternal.builder();
+ for (int i = 1; i <= 10; i++) {
+ expected.add(MeasurementDouble.create(makeSimpleMeasureDouble("m" + i), i * 11.1));
+ builder.put(makeSimpleMeasureDouble("m" + i), i * 11.1);
+ assertContains(builder.build(), expected.toArray(new Measurement[i]));
+ }
+ }
+
+ @Test
+ public void testDuplicateMeasureDoubles() {
+ assertContains(
+ MeasureMapInternal.builder().put(M1, 1.0).put(M1, 2.0).build(),
+ MeasurementDouble.create(M1, 2.0));
+ assertContains(
+ MeasureMapInternal.builder().put(M1, 1.0).put(M1, 2.0).put(M1, 3.0).build(),
+ MeasurementDouble.create(M1, 3.0));
+ assertContains(
+ MeasureMapInternal.builder().put(M1, 1.0).put(M2, 2.0).put(M1, 3.0).build(),
+ MeasurementDouble.create(M1, 3.0),
+ MeasurementDouble.create(M2, 2.0));
+ assertContains(
+ MeasureMapInternal.builder().put(M1, 1.0).put(M1, 2.0).put(M2, 2.0).build(),
+ MeasurementDouble.create(M1, 2.0),
+ MeasurementDouble.create(M2, 2.0));
+ }
+
+ @Test
+ public void testDuplicateMeasureLongs() {
+ assertContains(
+ MeasureMapInternal.builder().put(M3, 100L).put(M3, 100L).build(),
+ MeasurementLong.create(M3, 100L));
+ assertContains(
+ MeasureMapInternal.builder().put(M3, 100L).put(M3, 200L).put(M3, 300L).build(),
+ MeasurementLong.create(M3, 300L));
+ assertContains(
+ MeasureMapInternal.builder().put(M3, 100L).put(M4, 200L).put(M3, 300L).build(),
+ MeasurementLong.create(M3, 300L),
+ MeasurementLong.create(M4, 200L));
+ assertContains(
+ MeasureMapInternal.builder().put(M3, 100L).put(M3, 200L).put(M4, 200L).build(),
+ MeasurementLong.create(M3, 200L),
+ MeasurementLong.create(M4, 200L));
+ }
+
+ @Test
+ public void testDuplicateMeasures() {
+ assertContains(
+ MeasureMapInternal.builder().put(M3, 100L).put(M1, 1.0).put(M3, 300L).build(),
+ MeasurementLong.create(M3, 300L),
+ MeasurementDouble.create(M1, 1.0));
+ assertContains(
+ MeasureMapInternal.builder().put(M2, 2.0).put(M3, 100L).put(M2, 3.0).build(),
+ MeasurementDouble.create(M2, 3.0),
+ MeasurementLong.create(M3, 100L));
+ }
+
+ private static final MeasureDouble M1 = makeSimpleMeasureDouble("m1");
+ private static final MeasureDouble M2 = makeSimpleMeasureDouble("m2");
+ private static final MeasureLong M3 = makeSimpleMeasureLong("m3");
+ private static final MeasureLong M4 = makeSimpleMeasureLong("m4");
+
+ private static MeasureDouble makeSimpleMeasureDouble(String measure) {
+ return Measure.MeasureDouble.create(measure, measure + " description", "1");
+ }
+
+ private static MeasureLong makeSimpleMeasureLong(String measure) {
+ return Measure.MeasureLong.create(measure, measure + " description", "1");
+ }
+
+ private static void assertContains(MeasureMapInternal metrics, Measurement... measurements) {
+ assertThat(Lists.newArrayList(metrics.iterator())).containsExactly((Object[]) measurements);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureToViewMapTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureToViewMapTest.java
new file mode 100644
index 00000000..0a198b0c
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MeasureToViewMapTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Timestamp;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.tags.TagKey;
+import io.opencensus.testing.common.TestClock;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MeasureToViewMap}. */
+@RunWith(JUnit4.class)
+public class MeasureToViewMapTest {
+
+ private static final Measure MEASURE =
+ Measure.MeasureDouble.create("my measurement", "measurement description", "By");
+
+ private static final Name VIEW_NAME = View.Name.create("my view");
+
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+
+ private static final View VIEW =
+ View.create(
+ VIEW_NAME,
+ "view description",
+ MEASURE,
+ Mean.create(),
+ Arrays.asList(TagKey.create("my key")),
+ CUMULATIVE);
+
+ @Test
+ public void testRegisterAndGetView() {
+ MeasureToViewMap measureToViewMap = new MeasureToViewMap();
+ TestClock clock = TestClock.create(Timestamp.create(10, 20));
+ measureToViewMap.registerView(VIEW, clock);
+ clock.setTime(Timestamp.create(30, 40));
+ ViewData viewData = measureToViewMap.getView(VIEW_NAME, clock, StatsCollectionState.ENABLED);
+ assertThat(viewData.getView()).isEqualTo(VIEW);
+ assertThat(viewData.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(10, 20), Timestamp.create(30, 40)));
+ assertThat(viewData.getAggregationMap()).isEmpty();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java
new file mode 100644
index 00000000..054a5b44
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableAggregationTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.common.Function;
+import io.opencensus.implcore.stats.MutableAggregation.MutableCount;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSum;
+import io.opencensus.stats.BucketBoundaries;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link io.opencensus.implcore.stats.MutableAggregation}. */
+@RunWith(JUnit4.class)
+public class MutableAggregationTest {
+
+ @Rule public ExpectedException thrown = ExpectedException.none();
+
+ private static final double TOLERANCE = 1e-6;
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(Arrays.asList(-10.0, 0.0, 10.0));
+
+ @Test
+ public void testCreateEmpty() {
+ assertThat(MutableSum.create().getSum()).isWithin(TOLERANCE).of(0);
+ assertThat(MutableCount.create().getCount()).isEqualTo(0);
+ assertThat(MutableMean.create().getMean()).isWithin(TOLERANCE).of(0);
+
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(0.1, 2.2, 33.3));
+ MutableDistribution mutableDistribution = MutableDistribution.create(bucketBoundaries);
+ assertThat(mutableDistribution.getMean()).isWithin(TOLERANCE).of(0);
+ assertThat(mutableDistribution.getCount()).isEqualTo(0);
+ assertThat(mutableDistribution.getMin()).isPositiveInfinity();
+ assertThat(mutableDistribution.getMax()).isNegativeInfinity();
+ assertThat(mutableDistribution.getSumOfSquaredDeviations()).isWithin(TOLERANCE).of(0);
+ assertThat(mutableDistribution.getBucketCounts()).isEqualTo(new long[4]);
+ }
+
+ @Test
+ public void testNullBucketBoundaries() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("bucketBoundaries should not be null.");
+ MutableDistribution.create(null);
+ }
+
+ @Test
+ public void testNoBoundaries() {
+ List<Double> buckets = Arrays.asList();
+ MutableDistribution noBoundaries = MutableDistribution.create(BucketBoundaries.create(buckets));
+ assertThat(noBoundaries.getBucketCounts().length).isEqualTo(1);
+ assertThat(noBoundaries.getBucketCounts()[0]).isEqualTo(0);
+ }
+
+ @Test
+ public void testAdd() {
+ List<MutableAggregation> aggregations =
+ Arrays.asList(
+ MutableSum.create(),
+ MutableCount.create(),
+ MutableMean.create(),
+ MutableDistribution.create(BUCKET_BOUNDARIES));
+
+ List<Double> values = Arrays.asList(-1.0, 1.0, -5.0, 20.0, 5.0);
+
+ for (double value : values) {
+ for (MutableAggregation aggregation : aggregations) {
+ aggregation.add(value);
+ }
+ }
+
+ for (MutableAggregation aggregation : aggregations) {
+ aggregation.match(
+ new Function<MutableSum, Void>() {
+ @Override
+ public Void apply(MutableSum arg) {
+ assertThat(arg.getSum()).isWithin(TOLERANCE).of(20.0);
+ return null;
+ }
+ },
+ new Function<MutableCount, Void>() {
+ @Override
+ public Void apply(MutableCount arg) {
+ assertThat(arg.getCount()).isEqualTo(5);
+ return null;
+ }
+ },
+ new Function<MutableMean, Void>() {
+ @Override
+ public Void apply(MutableMean arg) {
+ assertThat(arg.getMean()).isWithin(TOLERANCE).of(4.0);
+ return null;
+ }
+ },
+ new Function<MutableDistribution, Void>() {
+ @Override
+ public Void apply(MutableDistribution arg) {
+ assertThat(arg.getBucketCounts()).isEqualTo(new long[] {0, 2, 2, 1});
+ return null;
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testMatch() {
+ List<MutableAggregation> aggregations =
+ Arrays.asList(
+ MutableSum.create(),
+ MutableCount.create(),
+ MutableMean.create(),
+ MutableDistribution.create(BUCKET_BOUNDARIES));
+
+ List<String> actual = new ArrayList<String>();
+ for (MutableAggregation aggregation : aggregations) {
+ actual.add(
+ aggregation.match(
+ new Function<MutableSum, String>() {
+ @Override
+ public String apply(MutableSum arg) {
+ return "SUM";
+ }
+ },
+ new Function<MutableCount, String>() {
+ @Override
+ public String apply(MutableCount arg) {
+ return "COUNT";
+ }
+ },
+ new Function<MutableMean, String>() {
+ @Override
+ public String apply(MutableMean arg) {
+ return "MEAN";
+ }
+ },
+ new Function<MutableDistribution, String>() {
+ @Override
+ public String apply(MutableDistribution arg) {
+ return "DISTRIBUTION";
+ }
+ }));
+ }
+
+ assertThat(actual).isEqualTo(Arrays.asList("SUM", "COUNT", "MEAN", "DISTRIBUTION"));
+ }
+
+ @Test
+ public void testCombine_SumCountMean() {
+ // combine() for Mutable Sum, Count and Mean will pick up fractional stats
+ List<MutableAggregation> aggregations1 =
+ Arrays.asList(MutableSum.create(), MutableCount.create(), MutableMean.create());
+ List<MutableAggregation> aggregations2 =
+ Arrays.asList(MutableSum.create(), MutableCount.create(), MutableMean.create());
+
+ for (double val : Arrays.asList(-1.0, -5.0)) {
+ for (MutableAggregation aggregation : aggregations1) {
+ aggregation.add(val);
+ }
+ }
+ for (double val : Arrays.asList(10.0, 50.0)) {
+ for (MutableAggregation aggregation : aggregations2) {
+ aggregation.add(val);
+ }
+ }
+
+ List<MutableAggregation> combined =
+ Arrays.asList(MutableSum.create(), MutableCount.create(), MutableMean.create());
+ double fraction1 = 1.0;
+ double fraction2 = 0.6;
+ for (int i = 0; i < combined.size(); i++) {
+ combined.get(i).combine(aggregations1.get(i), fraction1);
+ combined.get(i).combine(aggregations2.get(i), fraction2);
+ }
+
+ assertThat(((MutableSum) combined.get(0)).getSum()).isWithin(TOLERANCE).of(30);
+ assertThat(((MutableCount) combined.get(1)).getCount()).isEqualTo(3);
+ assertThat(((MutableMean) combined.get(2)).getMean()).isWithin(TOLERANCE).of(10);
+ }
+
+ @Test
+ public void testCombine_Distribution() {
+ // combine() for Mutable Distribution will ignore fractional stats
+ MutableDistribution distribution1 = MutableDistribution.create(BUCKET_BOUNDARIES);
+ MutableDistribution distribution2 = MutableDistribution.create(BUCKET_BOUNDARIES);
+ MutableDistribution distribution3 = MutableDistribution.create(BUCKET_BOUNDARIES);
+
+ for (double val : Arrays.asList(5.0, -5.0)) {
+ distribution1.add(val);
+ }
+ for (double val : Arrays.asList(10.0, 20.0)) {
+ distribution2.add(val);
+ }
+ for (double val : Arrays.asList(-10.0, 15.0, -15.0, -20.0)) {
+ distribution3.add(val);
+ }
+
+ MutableDistribution combined = MutableDistribution.create(BUCKET_BOUNDARIES);
+ combined.combine(distribution1, 1.0); // distribution1 will be combined
+ combined.combine(distribution2, 0.6); // distribution2 will be ignored
+ verifyMutableDistribution(combined, 0, 2, -5, 5, 50.0, new long[] {0, 1, 1, 0}, TOLERANCE);
+
+ combined.combine(distribution2, 1.0); // distribution2 will be combined
+ verifyMutableDistribution(combined, 7.5, 4, -5, 20, 325.0, new long[] {0, 1, 1, 2}, TOLERANCE);
+
+ combined.combine(distribution3, 1.0); // distribution3 will be combined
+ verifyMutableDistribution(combined, 0, 8, -20, 20, 1500.0, new long[] {2, 2, 1, 3}, TOLERANCE);
+ }
+
+ private static void verifyMutableDistribution(
+ MutableDistribution mutableDistribution,
+ double mean,
+ long count,
+ double min,
+ double max,
+ double sumOfSquaredDeviations,
+ long[] bucketCounts,
+ double tolerance) {
+ assertThat(mutableDistribution.getMean()).isWithin(tolerance).of(mean);
+ assertThat(mutableDistribution.getCount()).isEqualTo(count);
+ assertThat(mutableDistribution.getMin()).isWithin(tolerance).of(min);
+ assertThat(mutableDistribution.getMax()).isWithin(tolerance).of(max);
+ assertThat(mutableDistribution.getSumOfSquaredDeviations())
+ .isWithin(tolerance)
+ .of(sumOfSquaredDeviations);
+ assertThat(mutableDistribution.getBucketCounts()).isEqualTo(bucketCounts);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java
new file mode 100644
index 00000000..b4b4c85b
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/MutableViewDataTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.MutableViewData.toMillis;
+
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.stats.MutableAggregation.MutableCount;
+import io.opencensus.implcore.stats.MutableAggregation.MutableDistribution;
+import io.opencensus.implcore.stats.MutableAggregation.MutableMean;
+import io.opencensus.implcore.stats.MutableAggregation.MutableSum;
+import io.opencensus.stats.Aggregation.Count;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link MutableViewData}. */
+@RunWith(JUnit4.class)
+public class MutableViewDataTest {
+
+ private static final double EPSILON = 1e-7;
+
+ private static final TagKey ORIGINATOR = TagKey.create("originator");
+ private static final TagKey CALLER = TagKey.create("caller");
+ private static final TagKey METHOD = TagKey.create("method");
+ private static final TagValue CALLER_V = TagValue.create("some caller");
+ private static final TagValue METHOD_V = TagValue.create("some method");
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("measure1", "description", "1");
+ private static final MeasureLong MEASURE_LONG =
+ MeasureLong.create("measure2", "description", "1");
+
+ @Test
+ public void testConstants() {
+ assertThat(MutableViewData.UNKNOWN_TAG_VALUE).isNull();
+ assertThat(MutableViewData.ZERO_TIMESTAMP).isEqualTo(Timestamp.create(0, 0));
+ }
+
+ @Test
+ public void testGetTagValues() {
+ List<TagKey> columns = Arrays.asList(CALLER, METHOD, ORIGINATOR);
+ Map<TagKey, TagValue> tags = ImmutableMap.of(CALLER, CALLER_V, METHOD, METHOD_V);
+
+ assertThat(MutableViewData.getTagValues(tags, columns))
+ .containsExactly(CALLER_V, METHOD_V, MutableViewData.UNKNOWN_TAG_VALUE)
+ .inOrder();
+ }
+
+ @Test
+ public void createMutableAggregation() {
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(-1.0, 0.0, 1.0));
+
+ assertThat(((MutableSum) MutableViewData.createMutableAggregation(Sum.create())).getSum())
+ .isWithin(EPSILON)
+ .of(0.0);
+ assertThat(((MutableCount) MutableViewData.createMutableAggregation(Count.create())).getCount())
+ .isEqualTo(0L);
+ assertThat(((MutableMean) MutableViewData.createMutableAggregation(Mean.create())).getMean())
+ .isWithin(EPSILON)
+ .of(0D);
+
+ MutableDistribution mutableDistribution =
+ (MutableDistribution)
+ MutableViewData.createMutableAggregation(Distribution.create(bucketBoundaries));
+ assertThat(mutableDistribution.getMin()).isPositiveInfinity();
+ assertThat(mutableDistribution.getMax()).isNegativeInfinity();
+ assertThat(mutableDistribution.getSumOfSquaredDeviations()).isWithin(EPSILON).of(0);
+ assertThat(mutableDistribution.getBucketCounts()).isEqualTo(new long[4]);
+ }
+
+ @Test
+ public void createAggregationData() {
+ BucketBoundaries bucketBoundaries = BucketBoundaries.create(Arrays.asList(-1.0, 0.0, 1.0));
+ List<MutableAggregation> mutableAggregations =
+ Arrays.asList(
+ MutableCount.create(),
+ MutableMean.create(),
+ MutableDistribution.create(bucketBoundaries));
+ List<AggregationData> aggregates = new ArrayList<AggregationData>();
+
+ aggregates.add(MutableViewData.createAggregationData(MutableSum.create(), MEASURE_DOUBLE));
+ aggregates.add(MutableViewData.createAggregationData(MutableSum.create(), MEASURE_LONG));
+ for (MutableAggregation mutableAggregation : mutableAggregations) {
+ aggregates.add(MutableViewData.createAggregationData(mutableAggregation, MEASURE_DOUBLE));
+ }
+
+ assertThat(aggregates)
+ .containsExactly(
+ SumDataDouble.create(0),
+ SumDataLong.create(0),
+ CountData.create(0),
+ MeanData.create(0, 0),
+ DistributionData.create(
+ 0,
+ 0,
+ Double.POSITIVE_INFINITY,
+ Double.NEGATIVE_INFINITY,
+ 0,
+ Arrays.asList(new Long[] {0L, 0L, 0L, 0L})))
+ .inOrder();
+ }
+
+ @Test
+ public void testDurationToMillis() {
+ assertThat(toMillis(Duration.create(0, 0))).isEqualTo(0);
+ assertThat(toMillis(Duration.create(0, 987000000))).isEqualTo(987);
+ assertThat(toMillis(Duration.create(3, 456000000))).isEqualTo(3456);
+ assertThat(toMillis(Duration.create(0, -1000000))).isEqualTo(-1);
+ assertThat(toMillis(Duration.create(-1, 0))).isEqualTo(-1000);
+ assertThat(toMillis(Duration.create(-3, -456000000))).isEqualTo(-3456);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java
new file mode 100644
index 00000000..49131d8a
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsComponentImplBaseTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+import io.opencensus.testing.common.TestClock;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StatsComponentImplBase}. */
+@RunWith(JUnit4.class)
+public final class StatsComponentImplBaseTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private final StatsComponent statsComponent =
+ new StatsComponentImplBase(new SimpleEventQueue(), TestClock.create());
+
+ @Test
+ public void defaultState() {
+ assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.ENABLED);
+ }
+
+ @Test
+ public void setState_Disabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.DISABLED);
+ }
+
+ @Test
+ public void setState_Enabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ statsComponent.setState(StatsCollectionState.ENABLED);
+ assertThat(statsComponent.getState()).isEqualTo(StatsCollectionState.ENABLED);
+ }
+
+ @Test
+ public void setState_DisallowsNull() {
+ thrown.expect(NullPointerException.class);
+ thrown.expectMessage("newState");
+ statsComponent.setState(null);
+ }
+
+ @Test
+ public void preventSettingStateAfterGettingState() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ statsComponent.getState();
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("State was already read, cannot set state.");
+ statsComponent.setState(StatsCollectionState.ENABLED);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java
new file mode 100644
index 00000000..b0533d0d
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsRecorderImplTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.MutableViewData.ZERO_TIMESTAMP;
+import static io.opencensus.implcore.stats.StatsTestUtil.createEmptyViewData;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import io.grpc.Context;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.StatsComponent;
+import io.opencensus.stats.StatsRecorder;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewManager;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.unsafe.ContextUtils;
+import io.opencensus.testing.common.TestClock;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link StatsRecorderImpl}. */
+@RunWith(JUnit4.class)
+public final class StatsRecorderImplTest {
+ private static final TagKey KEY = TagKey.create("KEY");
+ private static final TagValue VALUE = TagValue.create("VALUE");
+ private static final TagValue VALUE_2 = TagValue.create("VALUE_2");
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create("my measurement", "description", "us");
+ private static final View.Name VIEW_NAME = View.Name.create("my view");
+
+ private final StatsComponent statsComponent =
+ new StatsComponentImplBase(new SimpleEventQueue(), TestClock.create());
+
+ private final ViewManager viewManager = statsComponent.getViewManager();
+ private final StatsRecorder statsRecorder = statsComponent.getStatsRecorder();
+
+ @Test
+ public void record_CurrentContextNotSet() {
+ View view =
+ View.create(
+ VIEW_NAME,
+ "description",
+ MEASURE_DOUBLE,
+ Sum.create(),
+ Arrays.asList(KEY),
+ Cumulative.create());
+ viewManager.registerView(view);
+ statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 1.0).record();
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+
+ // record() should have used the default TagContext, so the tag value should be null.
+ assertThat(viewData.getAggregationMap().keySet())
+ .containsExactly(Arrays.asList((TagValue) null));
+ }
+
+ @Test
+ public void record_CurrentContextSet() {
+ View view =
+ View.create(
+ VIEW_NAME,
+ "description",
+ MEASURE_DOUBLE,
+ Sum.create(),
+ Arrays.asList(KEY),
+ Cumulative.create());
+ viewManager.registerView(view);
+ Context orig =
+ Context.current()
+ .withValue(ContextUtils.TAG_CONTEXT_KEY, new SimpleTagContext(Tag.create(KEY, VALUE)))
+ .attach();
+ try {
+ statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 1.0).record();
+ } finally {
+ Context.current().detach(orig);
+ }
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+
+ // record() should have used the given TagContext.
+ assertThat(viewData.getAggregationMap().keySet()).containsExactly(Arrays.asList(VALUE));
+ }
+
+ @Test
+ public void recordTwice() {
+ View view =
+ View.create(
+ VIEW_NAME,
+ "description",
+ MEASURE_DOUBLE,
+ Sum.create(),
+ Arrays.asList(KEY),
+ Cumulative.create());
+ viewManager.registerView(view);
+ MeasureMap statsRecord = statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 1.0);
+ statsRecord.record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+ statsRecord.record(new SimpleTagContext(Tag.create(KEY, VALUE_2)));
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+
+ // There should be two entries.
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 1.0),
+ Arrays.asList(VALUE_2),
+ StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 1.0)),
+ 1e-6);
+ }
+
+ @Test
+ public void record_StatsDisabled() {
+ View view =
+ View.create(
+ VIEW_NAME,
+ "description",
+ MEASURE_DOUBLE,
+ Sum.create(),
+ Arrays.asList(KEY),
+ Cumulative.create());
+
+ viewManager.registerView(view);
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.0)
+ .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+ assertThat(viewManager.getView(VIEW_NAME)).isEqualTo(createEmptyViewData(view));
+ }
+
+ @Test
+ public void record_StatsReenabled() {
+ View view =
+ View.create(
+ VIEW_NAME,
+ "description",
+ MEASURE_DOUBLE,
+ Sum.create(),
+ Arrays.asList(KEY),
+ Cumulative.create());
+ viewManager.registerView(view);
+
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.0)
+ .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+ assertThat(viewManager.getView(VIEW_NAME)).isEqualTo(createEmptyViewData(view));
+
+ statsComponent.setState(StatsCollectionState.ENABLED);
+ assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+ assertThat(viewManager.getView(VIEW_NAME).getWindowData())
+ .isNotEqualTo(CumulativeData.create(ZERO_TIMESTAMP, ZERO_TIMESTAMP));
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 4.0)
+ .record(new SimpleTagContext(Tag.create(KEY, VALUE)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(VIEW_NAME).getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(Sum.create(), MEASURE_DOUBLE, 4.0)),
+ 1e-6);
+ }
+
+ private static final class SimpleTagContext extends TagContext {
+ private final List<Tag> tags;
+
+ SimpleTagContext(Tag... tags) {
+ this.tags = Collections.unmodifiableList(Lists.newArrayList(tags));
+ }
+
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return tags.iterator();
+ }
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java
new file mode 100644
index 00000000..3a714af4
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/StatsTestUtil.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.MutableViewData.ZERO_TIMESTAMP;
+
+import com.google.common.collect.Iterables;
+import io.opencensus.common.Function;
+import io.opencensus.common.Functions;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.CountData;
+import io.opencensus.stats.AggregationData.DistributionData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.View;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagValue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/** Stats test utilities. */
+final class StatsTestUtil {
+
+ private StatsTestUtil() {}
+
+ /**
+ * Creates an {@link AggregationData} by adding the given sequence of values, based on the
+ * definition of the given {@link Aggregation}.
+ *
+ * @param aggregation the {@code Aggregation} to apply the values to.
+ * @param values the values to add to the {@code MutableAggregation}s.
+ * @return an {@code AggregationData}.
+ */
+ static AggregationData createAggregationData(
+ Aggregation aggregation, Measure measure, double... values) {
+ MutableAggregation mutableAggregation = MutableViewData.createMutableAggregation(aggregation);
+ for (double value : values) {
+ mutableAggregation.add(value);
+ }
+ return MutableViewData.createAggregationData(mutableAggregation, measure);
+ }
+
+ /**
+ * Compare the actual and expected AggregationMap within the given tolerance.
+ *
+ * @param expected the expected map.
+ * @param actual the actual mapping from {@code List<TagValue>} to {@code AggregationData}.
+ * @param tolerance the tolerance used for {@code double} comparison.
+ */
+ static void assertAggregationMapEquals(
+ Map<? extends List<? extends TagValue>, ? extends AggregationData> actual,
+ Map<? extends List<? extends TagValue>, ? extends AggregationData> expected,
+ double tolerance) {
+ assertThat(actual.keySet()).containsExactlyElementsIn(expected.keySet());
+ for (List<? extends TagValue> tagValues : actual.keySet()) {
+ assertAggregationDataEquals(expected.get(tagValues), actual.get(tagValues), tolerance);
+ }
+ }
+
+ /**
+ * Compare the expected and actual {@code AggregationData} within the given tolerance.
+ *
+ * @param expected the expected {@code AggregationData}.
+ * @param actual the actual {@code AggregationData}.
+ * @param tolerance the tolerance used for {@code double} comparison.
+ */
+ static void assertAggregationDataEquals(
+ AggregationData expected, final AggregationData actual, final double tolerance) {
+ expected.match(
+ new Function<SumDataDouble, Void>() {
+ @Override
+ public Void apply(SumDataDouble arg) {
+ assertThat(actual).isInstanceOf(SumDataDouble.class);
+ assertThat(((SumDataDouble) actual).getSum()).isWithin(tolerance).of(arg.getSum());
+ return null;
+ }
+ },
+ new Function<SumDataLong, Void>() {
+ @Override
+ public Void apply(SumDataLong arg) {
+ assertThat(actual).isInstanceOf(SumDataLong.class);
+ assertThat(((SumDataLong) actual).getSum()).isEqualTo(arg.getSum());
+ return null;
+ }
+ },
+ new Function<CountData, Void>() {
+ @Override
+ public Void apply(CountData arg) {
+ assertThat(actual).isInstanceOf(CountData.class);
+ assertThat(((CountData) actual).getCount()).isEqualTo(arg.getCount());
+ return null;
+ }
+ },
+ new Function<MeanData, Void>() {
+ @Override
+ public Void apply(MeanData arg) {
+ assertThat(actual).isInstanceOf(MeanData.class);
+ assertThat(((MeanData) actual).getMean()).isWithin(tolerance).of(arg.getMean());
+ return null;
+ }
+ },
+ new Function<DistributionData, Void>() {
+ @Override
+ public Void apply(DistributionData arg) {
+ assertThat(actual).isInstanceOf(DistributionData.class);
+ assertDistributionDataEquals(arg, (DistributionData) actual, tolerance);
+ return null;
+ }
+ },
+ Functions.<Void>throwIllegalArgumentException());
+ }
+
+ // Create an empty ViewData with the given View.
+ static ViewData createEmptyViewData(View view) {
+ return ViewData.create(
+ view,
+ Collections.<List<TagValue>, AggregationData>emptyMap(),
+ view.getWindow()
+ .match(
+ Functions.<AggregationWindowData>returnConstant(
+ CumulativeData.create(ZERO_TIMESTAMP, ZERO_TIMESTAMP)),
+ Functions.<AggregationWindowData>returnConstant(
+ IntervalData.create(ZERO_TIMESTAMP)),
+ Functions.<AggregationWindowData>throwAssertionError()));
+ }
+
+ // Compare the expected and actual DistributionData within the given tolerance.
+ private static void assertDistributionDataEquals(
+ DistributionData expected, DistributionData actual, double tolerance) {
+ assertThat(actual.getMean()).isWithin(tolerance).of(expected.getMean());
+ assertThat(actual.getCount()).isEqualTo(expected.getCount());
+ assertThat(actual.getMean()).isWithin(tolerance).of(expected.getMean());
+ assertThat(actual.getSumOfSquaredDeviations())
+ .isWithin(tolerance)
+ .of(expected.getSumOfSquaredDeviations());
+
+ if (expected.getMax() == Double.NEGATIVE_INFINITY
+ && expected.getMin() == Double.POSITIVE_INFINITY) {
+ assertThat(actual.getMax()).isNegativeInfinity();
+ assertThat(actual.getMin()).isPositiveInfinity();
+ } else {
+ assertThat(actual.getMax()).isWithin(tolerance).of(expected.getMax());
+ assertThat(actual.getMin()).isWithin(tolerance).of(expected.getMin());
+ }
+
+ assertThat(removeTrailingZeros((actual).getBucketCounts()))
+ .isEqualTo(removeTrailingZeros(expected.getBucketCounts()));
+ }
+
+ private static List<Long> removeTrailingZeros(List<Long> longs) {
+ if (longs == null) {
+ return null;
+ }
+ List<Long> truncated = new ArrayList<Long>(longs);
+ while (!truncated.isEmpty() && Iterables.getLast(truncated) == 0) {
+ truncated.remove(truncated.size() - 1);
+ }
+ return truncated;
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java
new file mode 100644
index 00000000..5eaa34e5
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/stats/ViewManagerImplTest.java
@@ -0,0 +1,924 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.stats;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.stats.StatsTestUtil.assertAggregationMapEquals;
+import static io.opencensus.implcore.stats.StatsTestUtil.createAggregationData;
+import static io.opencensus.implcore.stats.StatsTestUtil.createEmptyViewData;
+
+import com.google.common.collect.ImmutableMap;
+import io.opencensus.common.Duration;
+import io.opencensus.common.Timestamp;
+import io.opencensus.implcore.internal.SimpleEventQueue;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.stats.Aggregation;
+import io.opencensus.stats.Aggregation.Distribution;
+import io.opencensus.stats.Aggregation.Mean;
+import io.opencensus.stats.Aggregation.Sum;
+import io.opencensus.stats.AggregationData;
+import io.opencensus.stats.AggregationData.MeanData;
+import io.opencensus.stats.AggregationData.SumDataDouble;
+import io.opencensus.stats.AggregationData.SumDataLong;
+import io.opencensus.stats.BucketBoundaries;
+import io.opencensus.stats.Measure;
+import io.opencensus.stats.Measure.MeasureDouble;
+import io.opencensus.stats.Measure.MeasureLong;
+import io.opencensus.stats.MeasureMap;
+import io.opencensus.stats.StatsCollectionState;
+import io.opencensus.stats.View;
+import io.opencensus.stats.View.AggregationWindow.Cumulative;
+import io.opencensus.stats.View.AggregationWindow.Interval;
+import io.opencensus.stats.View.Name;
+import io.opencensus.stats.ViewData;
+import io.opencensus.stats.ViewData.AggregationWindowData;
+import io.opencensus.stats.ViewData.AggregationWindowData.CumulativeData;
+import io.opencensus.stats.ViewData.AggregationWindowData.IntervalData;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.testing.common.TestClock;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link ViewManagerImpl}. */
+@RunWith(JUnit4.class)
+public class ViewManagerImplTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private static final TagKey KEY = TagKey.create("KEY");
+
+ private static final TagValue VALUE = TagValue.create("VALUE");
+ private static final TagValue VALUE_2 = TagValue.create("VALUE_2");
+
+ private static final String MEASURE_NAME = "my measurement";
+
+ private static final String MEASURE_NAME_2 = "my measurement 2";
+
+ private static final String MEASURE_UNIT = "us";
+
+ private static final String MEASURE_DESCRIPTION = "measure description";
+
+ private static final MeasureDouble MEASURE_DOUBLE =
+ MeasureDouble.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT);
+
+ private static final MeasureLong MEASURE_LONG =
+ MeasureLong.create(MEASURE_NAME_2, MEASURE_DESCRIPTION, MEASURE_UNIT);
+
+ private static final Name VIEW_NAME = Name.create("my view");
+ private static final Name VIEW_NAME_2 = Name.create("my view 2");
+
+ private static final String VIEW_DESCRIPTION = "view description";
+
+ private static final Cumulative CUMULATIVE = Cumulative.create();
+
+ private static final double EPSILON = 1e-7;
+ private static final int MILLIS_PER_SECOND = 1000;
+ private static final Duration TEN_SECONDS = Duration.create(10, 0);
+
+ private static final BucketBoundaries BUCKET_BOUNDARIES =
+ BucketBoundaries.create(
+ Arrays.asList(
+ 0.0, 0.2, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 7.0, 10.0, 15.0, 20.0, 30.0, 40.0, 50.0));
+
+ private static final Sum SUM = Sum.create();
+ private static final Mean MEAN = Mean.create();
+ private static final Distribution DISTRIBUTION = Distribution.create(BUCKET_BOUNDARIES);
+
+ private final TestClock clock = TestClock.create();
+
+ private final StatsComponentImplBase statsComponent =
+ new StatsComponentImplBase(new SimpleEventQueue(), clock);
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+
+ private final Tagger tagger = tagsComponent.getTagger();
+ private final ViewManagerImpl viewManager = statsComponent.getViewManager();
+ private final StatsRecorderImpl statsRecorder = statsComponent.getStatsRecorder();
+
+ private static View createCumulativeView() {
+ return createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+ }
+
+ private static View createCumulativeView(
+ View.Name name, Measure measure, Aggregation aggregation, List<TagKey> keys) {
+ return View.create(name, VIEW_DESCRIPTION, measure, aggregation, keys, CUMULATIVE);
+ }
+
+ @Test
+ public void testRegisterAndGetCumulativeView() {
+ View view = createCumulativeView();
+ viewManager.registerView(view);
+ assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view);
+ assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+ assertThat(viewManager.getView(VIEW_NAME).getWindowData()).isInstanceOf(CumulativeData.class);
+ }
+
+ @Test
+ public void testRegisterAndGetIntervalView() {
+ View intervalView =
+ View.create(
+ VIEW_NAME,
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ Interval.create(Duration.create(60, 0)));
+ viewManager.registerView(intervalView);
+ assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(intervalView);
+ assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+ assertThat(viewManager.getView(VIEW_NAME).getWindowData()).isInstanceOf(IntervalData.class);
+ }
+
+ @Test
+ public void allowRegisteringSameViewTwice() {
+ View view = createCumulativeView();
+ viewManager.registerView(view);
+ viewManager.registerView(view);
+ assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view);
+ }
+
+ @Test
+ public void preventRegisteringDifferentViewWithSameName() {
+ View view1 =
+ View.create(
+ VIEW_NAME,
+ "View description.",
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ View view2 =
+ View.create(
+ VIEW_NAME,
+ "This is a different description.",
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ testFailedToRegisterView(
+ view1, view2, "A different view with the same name is already registered");
+ }
+
+ @Test
+ public void preventRegisteringDifferentMeasureWithSameName() {
+ MeasureDouble measure1 = MeasureDouble.create("measure", "description", "1");
+ MeasureLong measure2 = MeasureLong.create("measure", "description", "1");
+ View view1 =
+ View.create(
+ VIEW_NAME, VIEW_DESCRIPTION, measure1, DISTRIBUTION, Arrays.asList(KEY), CUMULATIVE);
+ View view2 =
+ View.create(
+ VIEW_NAME_2, VIEW_DESCRIPTION, measure2, DISTRIBUTION, Arrays.asList(KEY), CUMULATIVE);
+ testFailedToRegisterView(
+ view1, view2, "A different measure with the same name is already registered");
+ }
+
+ private void testFailedToRegisterView(View view1, View view2, String message) {
+ viewManager.registerView(view1);
+ try {
+ thrown.expect(IllegalArgumentException.class);
+ thrown.expectMessage(message);
+ viewManager.registerView(view2);
+ } finally {
+ assertThat(viewManager.getView(VIEW_NAME).getView()).isEqualTo(view1);
+ }
+ }
+
+ @Test
+ public void returnNullWhenGettingNonexistentViewData() {
+ assertThat(viewManager.getView(VIEW_NAME)).isNull();
+ }
+
+ @Test
+ public void testRecordDouble_distribution_cumulative() {
+ testRecordCumulative(MEASURE_DOUBLE, DISTRIBUTION, 10.0, 20.0, 30.0, 40.0);
+ }
+
+ @Test
+ public void testRecordLong_distribution_cumulative() {
+ testRecordCumulative(MEASURE_LONG, DISTRIBUTION, 1000, 2000, 3000, 4000);
+ }
+
+ @Test
+ public void testRecordDouble_sum_cumulative() {
+ testRecordCumulative(MEASURE_DOUBLE, SUM, 11.1, 22.2, 33.3, 44.4);
+ }
+
+ @Test
+ public void testRecordLong_sum_cumulative() {
+ testRecordCumulative(MEASURE_LONG, SUM, 1000, 2000, 3000, 4000);
+ }
+
+ private void testRecordCumulative(Measure measure, Aggregation aggregation, double... values) {
+ View view = createCumulativeView(VIEW_NAME, measure, aggregation, Arrays.asList(KEY));
+ clock.setTime(Timestamp.create(1, 2));
+ viewManager.registerView(view);
+ TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+ for (double val : values) {
+ putToMeasureMap(statsRecorder.newMeasureMap(), measure, val).record(tags);
+ }
+ clock.setTime(Timestamp.create(3, 4));
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData.getView()).isEqualTo(view);
+ assertThat(viewData.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(1, 2), Timestamp.create(3, 4)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(aggregation, measure, values)),
+ EPSILON);
+ }
+
+ @Test
+ public void testRecordDouble_mean_interval() {
+ testRecordInterval(
+ MEASURE_DOUBLE,
+ MEAN,
+ new double[] {20.0, -1.0, 1.0, -5.0, 5.0},
+ 9.0,
+ 30.0,
+ MeanData.create((19 * 0.6 + 1) / 4, 4),
+ MeanData.create(0.2 * 5 + 9, 1),
+ MeanData.create(30.0, 1));
+ }
+
+ @Test
+ public void testRecordLong_mean_interval() {
+ testRecordInterval(
+ MEASURE_LONG,
+ MEAN,
+ new double[] {1000, 2000, 3000, 4000, 5000},
+ -5000,
+ 30,
+ MeanData.create((3000 * 0.6 + 12000) / 4, 4),
+ MeanData.create(-4000, 1),
+ MeanData.create(30, 1));
+ }
+
+ @Test
+ public void testRecordDouble_sum_interval() {
+ testRecordInterval(
+ MEASURE_DOUBLE,
+ SUM,
+ new double[] {20.0, -1.0, 1.0, -5.0, 5.0},
+ 9.0,
+ 30.0,
+ SumDataDouble.create(19 * 0.6 + 1),
+ SumDataDouble.create(0.2 * 5 + 9),
+ SumDataDouble.create(30.0));
+ }
+
+ @Test
+ public void testRecordLong_sum_interval() {
+ testRecordInterval(
+ MEASURE_LONG,
+ SUM,
+ new double[] {1000, 2000, 3000, 4000, 5000},
+ -5000,
+ 30,
+ SumDataLong.create(Math.round(3000 * 0.6 + 12000)),
+ SumDataLong.create(-4000),
+ SumDataLong.create(30));
+ }
+
+ private void testRecordInterval(
+ Measure measure,
+ Aggregation aggregation,
+ double[] initialValues, /* There are 5 initial values recorded before we call getView(). */
+ double value6,
+ double value7,
+ AggregationData expectedValues1,
+ AggregationData expectedValues2,
+ AggregationData expectedValues3) {
+ // The interval is 10 seconds, i.e. values should expire after 10 seconds.
+ // Each bucket has a duration of 2.5 seconds.
+ View view =
+ View.create(
+ VIEW_NAME,
+ VIEW_DESCRIPTION,
+ measure,
+ aggregation,
+ Arrays.asList(KEY),
+ Interval.create(TEN_SECONDS));
+ long startTimeMillis = 30 * MILLIS_PER_SECOND; // start at 30s
+ clock.setTime(Timestamp.fromMillis(startTimeMillis));
+ viewManager.registerView(view);
+
+ TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+
+ for (int i = 1; i <= 5; i++) {
+ /*
+ * Add each value in sequence, at 31s, 32s, 33s, etc.
+ * 1st and 2nd values should fall into the first bucket [30.0, 32.5),
+ * 3rd and 4th values should fall into the second bucket [32.5, 35.0),
+ * 5th value should fall into the third bucket [35.0, 37.5).
+ */
+ clock.setTime(Timestamp.fromMillis(startTimeMillis + i * MILLIS_PER_SECOND));
+ putToMeasureMap(statsRecorder.newMeasureMap(), measure, initialValues[i - 1]).record(tags);
+ }
+
+ clock.setTime(Timestamp.fromMillis(startTimeMillis + 8 * MILLIS_PER_SECOND));
+ // 38s, no values should have expired
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(VIEW_NAME).getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(aggregation, measure, initialValues)),
+ EPSILON);
+
+ clock.setTime(Timestamp.fromMillis(startTimeMillis + 11 * MILLIS_PER_SECOND));
+ // 41s, 40% of the values in the first bucket should have expired (1 / 2.5 = 0.4).
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(VIEW_NAME).getAggregationMap(),
+ ImmutableMap.of(Arrays.asList(VALUE), expectedValues1),
+ EPSILON);
+
+ clock.setTime(Timestamp.fromMillis(startTimeMillis + 12 * MILLIS_PER_SECOND));
+ // 42s, add a new value value1, should fall into bucket [40.0, 42.5)
+ putToMeasureMap(statsRecorder.newMeasureMap(), measure, value6).record(tags);
+
+ clock.setTime(Timestamp.fromMillis(startTimeMillis + 17 * MILLIS_PER_SECOND));
+ // 47s, values in the first and second bucket should have expired, and 80% of values in the
+ // third bucket should have expired. The new value should persist.
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(VIEW_NAME).getAggregationMap(),
+ ImmutableMap.of(Arrays.asList(VALUE), expectedValues2),
+ EPSILON);
+
+ clock.setTime(Timestamp.fromMillis(60 * MILLIS_PER_SECOND));
+ // 60s, all previous values should have expired, add another value value2
+ putToMeasureMap(statsRecorder.newMeasureMap(), measure, value7).record(tags);
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(VIEW_NAME).getAggregationMap(),
+ ImmutableMap.of(Arrays.asList(VALUE), expectedValues3),
+ EPSILON);
+
+ clock.setTime(Timestamp.fromMillis(100 * MILLIS_PER_SECOND));
+ // 100s, all values should have expired
+ assertThat(viewManager.getView(VIEW_NAME).getAggregationMap()).isEmpty();
+ }
+
+ @Test
+ public void getViewDoesNotClearStats() {
+ View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+ clock.setTime(Timestamp.create(10, 0));
+ viewManager.registerView(view);
+ TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+ statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 0.1).record(tags);
+ clock.setTime(Timestamp.create(11, 0));
+ ViewData viewData1 = viewManager.getView(VIEW_NAME);
+ assertThat(viewData1.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(10, 0), Timestamp.create(11, 0)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData1.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 0.1)),
+ EPSILON);
+
+ statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 0.2).record(tags);
+ clock.setTime(Timestamp.create(12, 0));
+ ViewData viewData2 = viewManager.getView(VIEW_NAME);
+
+ // The second view should have the same start time as the first view, and it should include both
+ // recorded values:
+ assertThat(viewData2.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(10, 0), Timestamp.create(12, 0)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData2.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 0.1, 0.2)),
+ EPSILON);
+ }
+
+ @Test
+ public void testRecordCumulativeMultipleTagValues() {
+ viewManager.registerView(
+ createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY)));
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 10.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 30.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 50.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0),
+ Arrays.asList(VALUE_2),
+ createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 30.0, 50.0)),
+ EPSILON);
+ }
+
+ @Test
+ public void testRecordIntervalMultipleTagValues() {
+ // The interval is 10 seconds, i.e. values should expire after 10 seconds.
+ View view =
+ View.create(
+ VIEW_NAME,
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ Interval.create(TEN_SECONDS));
+ clock.setTime(Timestamp.create(10, 0)); // Start at 10s
+ viewManager.registerView(view);
+
+ // record for TagValue1 at 11s
+ clock.setTime(Timestamp.fromMillis(11 * MILLIS_PER_SECOND));
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 10.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+
+ // record for TagValue2 at 15s
+ clock.setTime(Timestamp.fromMillis(15 * MILLIS_PER_SECOND));
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 30.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 50.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+
+ // get ViewData at 19s, no stats should have expired.
+ clock.setTime(Timestamp.fromMillis(19 * MILLIS_PER_SECOND));
+ ViewData viewData1 = viewManager.getView(VIEW_NAME);
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData1.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0),
+ Arrays.asList(VALUE_2),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 30.0, 50.0)),
+ EPSILON);
+
+ // record for TagValue2 again at 20s
+ clock.setTime(Timestamp.fromMillis(20 * MILLIS_PER_SECOND));
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 40.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE_2).build());
+
+ // get ViewData at 25s, stats for TagValue1 should have expired.
+ clock.setTime(Timestamp.fromMillis(25 * MILLIS_PER_SECOND));
+ ViewData viewData2 = viewManager.getView(VIEW_NAME);
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData2.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE_2),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 30.0, 50.0, 40.0)),
+ EPSILON);
+
+ // get ViewData at 30s, the first two values for TagValue2 should have expired.
+ clock.setTime(Timestamp.fromMillis(30 * MILLIS_PER_SECOND));
+ ViewData viewData3 = viewManager.getView(VIEW_NAME);
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData3.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE_2),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 40.0)),
+ EPSILON);
+
+ // get ViewData at 40s, all stats should have expired.
+ clock.setTime(Timestamp.fromMillis(40 * MILLIS_PER_SECOND));
+ ViewData viewData4 = viewManager.getView(VIEW_NAME);
+ assertThat(viewData4.getAggregationMap()).isEmpty();
+ }
+
+ // This test checks that MeasureMaper.record(...) does not throw an exception when no views are
+ // registered.
+ @Test
+ public void allowRecordingWithoutRegisteringMatchingViewData() {
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 10)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ }
+
+ @Test
+ public void testRecordWithEmptyStatsContext() {
+ viewManager.registerView(
+ createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY)));
+ // DEFAULT doesn't have tags, but the view has tag key "KEY".
+ statsRecorder.newMeasureMap().put(MEASURE_DOUBLE, 10.0).record(tagger.empty());
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ // Tag is missing for associated measureValues, should use default tag value
+ // "unknown/not set".
+ Arrays.asList(MutableViewData.UNKNOWN_TAG_VALUE),
+ // Should record stats with default tag value: "KEY" : "unknown/not set".
+ createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0)),
+ EPSILON);
+ }
+
+ @Test
+ public void testRecord_MeasureNameNotMatch() {
+ testRecord_MeasureNotMatch(
+ MeasureDouble.create(MEASURE_NAME, "measure", MEASURE_UNIT),
+ MeasureDouble.create(MEASURE_NAME_2, "measure", MEASURE_UNIT),
+ 10.0);
+ }
+
+ @Test
+ public void testRecord_MeasureTypeNotMatch() {
+ testRecord_MeasureNotMatch(
+ MeasureLong.create(MEASURE_NAME, "measure", MEASURE_UNIT),
+ MeasureDouble.create(MEASURE_NAME, "measure", MEASURE_UNIT),
+ 10.0);
+ }
+
+ private void testRecord_MeasureNotMatch(Measure measure1, Measure measure2, double value) {
+ viewManager.registerView(createCumulativeView(VIEW_NAME, measure1, MEAN, Arrays.asList(KEY)));
+ TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+ putToMeasureMap(statsRecorder.newMeasureMap(), measure2, value).record(tags);
+ ViewData view = viewManager.getView(VIEW_NAME);
+ assertThat(view.getAggregationMap()).isEmpty();
+ }
+
+ @Test
+ public void testRecordWithTagsThatDoNotMatchViewData() {
+ viewManager.registerView(
+ createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY)));
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 10.0)
+ .record(tagger.emptyBuilder().put(TagKey.create("wrong key"), VALUE).build());
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 50.0)
+ .record(tagger.emptyBuilder().put(TagKey.create("another wrong key"), VALUE).build());
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ // Won't record the unregistered tag key, for missing registered keys will use default
+ // tag value : "unknown/not set".
+ Arrays.asList(MutableViewData.UNKNOWN_TAG_VALUE),
+ // Should record stats with default tag value: "KEY" : "unknown/not set".
+ createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 10.0, 50.0)),
+ EPSILON);
+ }
+
+ @Test
+ public void testViewDataWithMultipleTagKeys() {
+ TagKey key1 = TagKey.create("Key-1");
+ TagKey key2 = TagKey.create("Key-2");
+ viewManager.registerView(
+ createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(key1, key2)));
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.1)
+ .record(
+ tagger
+ .emptyBuilder()
+ .put(key1, TagValue.create("v1"))
+ .put(key2, TagValue.create("v10"))
+ .build());
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 2.2)
+ .record(
+ tagger
+ .emptyBuilder()
+ .put(key1, TagValue.create("v1"))
+ .put(key2, TagValue.create("v20"))
+ .build());
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 3.3)
+ .record(
+ tagger
+ .emptyBuilder()
+ .put(key1, TagValue.create("v2"))
+ .put(key2, TagValue.create("v10"))
+ .build());
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 4.4)
+ .record(
+ tagger
+ .emptyBuilder()
+ .put(key1, TagValue.create("v1"))
+ .put(key2, TagValue.create("v10"))
+ .build());
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(TagValue.create("v1"), TagValue.create("v10")),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 1.1, 4.4),
+ Arrays.asList(TagValue.create("v1"), TagValue.create("v20")),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 2.2),
+ Arrays.asList(TagValue.create("v2"), TagValue.create("v10")),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 3.3)),
+ EPSILON);
+ }
+
+ @Test
+ public void testMultipleViewSameMeasure() {
+ final View view1 =
+ createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+ final View view2 =
+ createCumulativeView(VIEW_NAME_2, MEASURE_DOUBLE, DISTRIBUTION, Arrays.asList(KEY));
+ clock.setTime(Timestamp.create(1, 1));
+ viewManager.registerView(view1);
+ clock.setTime(Timestamp.create(2, 2));
+ viewManager.registerView(view2);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 5.0)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ clock.setTime(Timestamp.create(3, 3));
+ ViewData viewData1 = viewManager.getView(VIEW_NAME);
+ clock.setTime(Timestamp.create(4, 4));
+ ViewData viewData2 = viewManager.getView(VIEW_NAME_2);
+ assertThat(viewData1.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(1, 1), Timestamp.create(3, 3)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData1.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 5.0)),
+ EPSILON);
+ assertThat(viewData2.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(2, 2), Timestamp.create(4, 4)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData2.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, MEASURE_DOUBLE, 5.0)),
+ EPSILON);
+ }
+
+ @Test
+ public void testMultipleViews_DifferentMeasureNames() {
+ testMultipleViews_DifferentMeasures(
+ MeasureDouble.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT),
+ MeasureDouble.create(MEASURE_NAME_2, MEASURE_DESCRIPTION, MEASURE_UNIT),
+ 1.1,
+ 2.2);
+ }
+
+ @Test
+ public void testMultipleViews_DifferentMeasureTypes() {
+ testMultipleViews_DifferentMeasures(
+ MeasureDouble.create(MEASURE_NAME, MEASURE_DESCRIPTION, MEASURE_UNIT),
+ MeasureLong.create(MEASURE_NAME_2, MEASURE_DESCRIPTION, MEASURE_UNIT),
+ 1.1,
+ 5000);
+ }
+
+ private void testMultipleViews_DifferentMeasures(
+ Measure measure1, Measure measure2, double value1, double value2) {
+ final View view1 = createCumulativeView(VIEW_NAME, measure1, DISTRIBUTION, Arrays.asList(KEY));
+ final View view2 =
+ createCumulativeView(VIEW_NAME_2, measure2, DISTRIBUTION, Arrays.asList(KEY));
+ clock.setTime(Timestamp.create(1, 0));
+ viewManager.registerView(view1);
+ clock.setTime(Timestamp.create(2, 0));
+ viewManager.registerView(view2);
+ TagContext tags = tagger.emptyBuilder().put(KEY, VALUE).build();
+ MeasureMap measureMap = statsRecorder.newMeasureMap();
+ putToMeasureMap(measureMap, measure1, value1);
+ putToMeasureMap(measureMap, measure2, value2);
+ measureMap.record(tags);
+ clock.setTime(Timestamp.create(3, 0));
+ ViewData viewData1 = viewManager.getView(VIEW_NAME);
+ clock.setTime(Timestamp.create(4, 0));
+ ViewData viewData2 = viewManager.getView(VIEW_NAME_2);
+ assertThat(viewData1.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(1, 0), Timestamp.create(3, 0)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData1.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, measure1, value1)),
+ EPSILON);
+ assertThat(viewData2.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(2, 0), Timestamp.create(4, 0)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData2.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(DISTRIBUTION, measure2, value2)),
+ EPSILON);
+ }
+
+ @Test
+ public void testGetCumulativeViewDataWithEmptyBucketBoundaries() {
+ Aggregation noHistogram =
+ Distribution.create(BucketBoundaries.create(Collections.<Double>emptyList()));
+ View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, noHistogram, Arrays.asList(KEY));
+ clock.setTime(Timestamp.create(1, 0));
+ viewManager.registerView(view);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.1)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ clock.setTime(Timestamp.create(3, 0));
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(1, 0), Timestamp.create(3, 0)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(noHistogram, MEASURE_DOUBLE, 1.1)),
+ EPSILON);
+ }
+
+ @Test
+ public void testGetCumulativeViewDataWithoutBucketBoundaries() {
+ View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+ clock.setTime(Timestamp.create(1, 0));
+ viewManager.registerView(view);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.1)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ clock.setTime(Timestamp.create(3, 0));
+ ViewData viewData = viewManager.getView(VIEW_NAME);
+ assertThat(viewData.getWindowData())
+ .isEqualTo(CumulativeData.create(Timestamp.create(1, 0), Timestamp.create(3, 0)));
+ StatsTestUtil.assertAggregationMapEquals(
+ viewData.getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE), StatsTestUtil.createAggregationData(MEAN, MEASURE_DOUBLE, 1.1)),
+ EPSILON);
+ }
+
+ @Test
+ public void registerRecordAndGetView_StatsDisabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+ viewManager.registerView(view);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.1)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ assertThat(viewManager.getView(VIEW_NAME)).isEqualTo(createEmptyViewData(view));
+ }
+
+ @Test
+ public void registerRecordAndGetView_StatsReenabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ statsComponent.setState(StatsCollectionState.ENABLED);
+ View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+ viewManager.registerView(view);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.1)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(VIEW_NAME).getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE), StatsTestUtil.createAggregationData(MEAN, MEASURE_DOUBLE, 1.1)),
+ EPSILON);
+ }
+
+ @Test
+ public void registerViewWithStatsDisabled_RecordAndGetViewWithStatsEnabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ View view = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+ viewManager.registerView(view); // view will still be registered.
+
+ statsComponent.setState(StatsCollectionState.ENABLED);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.1)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(VIEW_NAME).getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE), StatsTestUtil.createAggregationData(MEAN, MEASURE_DOUBLE, 1.1)),
+ EPSILON);
+ }
+
+ @Test
+ public void registerDifferentViewWithSameNameWithStatsDisabled() {
+ statsComponent.setState(StatsCollectionState.DISABLED);
+ View view1 =
+ View.create(
+ VIEW_NAME,
+ "View description.",
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ View view2 =
+ View.create(
+ VIEW_NAME,
+ "This is a different description.",
+ MEASURE_DOUBLE,
+ DISTRIBUTION,
+ Arrays.asList(KEY),
+ CUMULATIVE);
+ testFailedToRegisterView(
+ view1, view2, "A different view with the same name is already registered");
+ }
+
+ @Test
+ public void settingStateToDisabledWillClearStats_Cumulative() {
+ View cumulativeView = createCumulativeView(VIEW_NAME, MEASURE_DOUBLE, MEAN, Arrays.asList(KEY));
+ settingStateToDisabledWillClearStats(cumulativeView);
+ }
+
+ @Test
+ public void settingStateToDisabledWillClearStats_Interval() {
+ View intervalView =
+ View.create(
+ VIEW_NAME_2,
+ VIEW_DESCRIPTION,
+ MEASURE_DOUBLE,
+ MEAN,
+ Arrays.asList(KEY),
+ Interval.create(Duration.create(60, 0)));
+ settingStateToDisabledWillClearStats(intervalView);
+ }
+
+ private void settingStateToDisabledWillClearStats(View view) {
+ Timestamp timestamp1 = Timestamp.create(1, 0);
+ clock.setTime(timestamp1);
+ viewManager.registerView(view);
+ statsRecorder
+ .newMeasureMap()
+ .put(MEASURE_DOUBLE, 1.1)
+ .record(tagger.emptyBuilder().put(KEY, VALUE).build());
+ StatsTestUtil.assertAggregationMapEquals(
+ viewManager.getView(view.getName()).getAggregationMap(),
+ ImmutableMap.of(
+ Arrays.asList(VALUE),
+ StatsTestUtil.createAggregationData(view.getAggregation(), view.getMeasure(), 1.1)),
+ EPSILON);
+
+ Timestamp timestamp2 = Timestamp.create(2, 0);
+ clock.setTime(timestamp2);
+ statsComponent.setState(StatsCollectionState.DISABLED); // This will clear stats.
+ assertThat(viewManager.getView(view.getName())).isEqualTo(createEmptyViewData(view));
+
+ Timestamp timestamp3 = Timestamp.create(3, 0);
+ clock.setTime(timestamp3);
+ statsComponent.setState(StatsCollectionState.ENABLED);
+
+ Timestamp timestamp4 = Timestamp.create(4, 0);
+ clock.setTime(timestamp4);
+ // This ViewData does not have any stats, but it should not be an empty ViewData, since it has
+ // non-zero TimeStamps.
+ ViewData viewData = viewManager.getView(view.getName());
+ assertThat(viewData.getAggregationMap()).isEmpty();
+ AggregationWindowData windowData = viewData.getWindowData();
+ if (windowData instanceof CumulativeData) {
+ assertThat(windowData).isEqualTo(CumulativeData.create(timestamp3, timestamp4));
+ } else {
+ assertThat(windowData).isEqualTo(IntervalData.create(timestamp4));
+ }
+ }
+
+ private static MeasureMap putToMeasureMap(MeasureMap measureMap, Measure measure, double value) {
+ if (measure instanceof MeasureDouble) {
+ return measureMap.put((MeasureDouble) measure, value);
+ } else if (measure instanceof MeasureLong) {
+ return measureMap.put((MeasureLong) measure, Math.round(value));
+ } else {
+ // Future measures.
+ throw new AssertionError();
+ }
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTagContextUtilsTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTagContextUtilsTest.java
new file mode 100644
index 00000000..1a14ac6e
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTagContextUtilsTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.tags.TagsTestUtil.tagContextToList;
+
+import com.google.common.collect.ImmutableSet;
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.unsafe.ContextUtils;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link CurrentTagContextUtils}. */
+@RunWith(JUnit4.class)
+public class CurrentTagContextUtilsTest {
+ private static final Tag TAG = Tag.create(TagKey.create("key"), TagValue.create("value"));
+
+ private final TagContext tagContext =
+ new TagContext() {
+
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return ImmutableSet.<Tag>of(TAG).iterator();
+ }
+ };
+
+ @Test
+ public void testGetCurrentTagContext_DefaultContext() {
+ TagContext tags = CurrentTagContextUtils.getCurrentTagContext();
+ assertThat(tags).isNotNull();
+ assertThat(tagContextToList(tags)).isEmpty();
+ }
+
+ @Test
+ public void testGetCurrentTagContext_ContextSetToNull() {
+ Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, null).attach();
+ try {
+ TagContext tags = CurrentTagContextUtils.getCurrentTagContext();
+ assertThat(tags).isNotNull();
+ assertThat(tagContextToList(tags)).isEmpty();
+ } finally {
+ Context.current().detach(orig);
+ }
+ }
+
+ @Test
+ public void testWithTagContext() {
+ assertThat(tagContextToList(CurrentTagContextUtils.getCurrentTagContext())).isEmpty();
+ Scope scopedTags = CurrentTagContextUtils.withTagContext(tagContext);
+ try {
+ assertThat(CurrentTagContextUtils.getCurrentTagContext()).isSameAs(tagContext);
+ } finally {
+ scopedTags.close();
+ }
+ assertThat(tagContextToList(CurrentTagContextUtils.getCurrentTagContext())).isEmpty();
+ }
+
+ @Test
+ public void testWithTagContextUsingWrap() {
+ Runnable runnable;
+ Scope scopedTags = CurrentTagContextUtils.withTagContext(tagContext);
+ try {
+ assertThat(CurrentTagContextUtils.getCurrentTagContext()).isSameAs(tagContext);
+ runnable =
+ Context.current()
+ .wrap(
+ new Runnable() {
+ @Override
+ public void run() {
+ assertThat(CurrentTagContextUtils.getCurrentTagContext())
+ .isSameAs(tagContext);
+ }
+ });
+ } finally {
+ scopedTags.close();
+ }
+ assertThat(tagContextToList(CurrentTagContextUtils.getCurrentTagContext())).isEmpty();
+ // When we run the runnable we will have the TagContext in the current Context.
+ runnable.run();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTaggingStateTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTaggingStateTest.java
new file mode 100644
index 00000000..244b6711
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/CurrentTaggingStateTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.tags.TaggingState;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link CurrentTaggingState}. */
+@RunWith(JUnit4.class)
+public final class CurrentTaggingStateTest {
+
+ @Test
+ public void defaultState() {
+ assertThat(new CurrentTaggingState().get()).isEqualTo(TaggingState.ENABLED);
+ }
+
+ @Test
+ public void setState() {
+ CurrentTaggingState state = new CurrentTaggingState();
+ state.set(TaggingState.DISABLED);
+ assertThat(state.get()).isEqualTo(TaggingState.DISABLED);
+ state.set(TaggingState.ENABLED);
+ assertThat(state.get()).isEqualTo(TaggingState.ENABLED);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java
new file mode 100644
index 00000000..c24cccb9
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/ScopedTagContextsTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.tags.TagsTestUtil.tagContextToList;
+
+import io.opencensus.common.Scope;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for the methods in {@link TaggerImpl} and {@link TagContextBuilderImpl} that interact
+ * with the current {@link TagContext}.
+ */
+@RunWith(JUnit4.class)
+public class ScopedTagContextsTest {
+ private static final TagKey KEY_1 = TagKey.create("key 1");
+ private static final TagKey KEY_2 = TagKey.create("key 2");
+
+ private static final TagValue VALUE_1 = TagValue.create("value 1");
+ private static final TagValue VALUE_2 = TagValue.create("value 2");
+
+ private final Tagger tagger = new TaggerImpl(new CurrentTaggingState());
+
+ @Test
+ public void defaultTagContext() {
+ TagContext defaultTagContext = tagger.getCurrentTagContext();
+ assertThat(tagContextToList(defaultTagContext)).isEmpty();
+ assertThat(defaultTagContext).isInstanceOf(TagContextImpl.class);
+ }
+
+ @Test
+ public void withTagContext() {
+ assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+ TagContext scopedTags = tagger.emptyBuilder().put(KEY_1, VALUE_1).build();
+ Scope scope = tagger.withTagContext(scopedTags);
+ try {
+ assertThat(tagger.getCurrentTagContext()).isSameAs(scopedTags);
+ } finally {
+ scope.close();
+ }
+ assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+ }
+
+ @Test
+ public void createBuilderFromCurrentTags() {
+ TagContext scopedTags = tagger.emptyBuilder().put(KEY_1, VALUE_1).build();
+ Scope scope = tagger.withTagContext(scopedTags);
+ try {
+ TagContext newTags = tagger.currentBuilder().put(KEY_2, VALUE_2).build();
+ assertThat(tagContextToList(newTags))
+ .containsExactly(Tag.create(KEY_1, VALUE_1), Tag.create(KEY_2, VALUE_2));
+ assertThat(tagger.getCurrentTagContext()).isSameAs(scopedTags);
+ } finally {
+ scope.close();
+ }
+ }
+
+ @Test
+ public void setCurrentTagsWithBuilder() {
+ assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+ Scope scope = tagger.emptyBuilder().put(KEY_1, VALUE_1).buildScoped();
+ try {
+ assertThat(tagContextToList(tagger.getCurrentTagContext()))
+ .containsExactly(Tag.create(KEY_1, VALUE_1));
+ } finally {
+ scope.close();
+ }
+ assertThat(tagContextToList(tagger.getCurrentTagContext())).isEmpty();
+ }
+
+ @Test
+ public void addToCurrentTagsWithBuilder() {
+ TagContext scopedTags = tagger.emptyBuilder().put(KEY_1, VALUE_1).build();
+ Scope scope1 = tagger.withTagContext(scopedTags);
+ try {
+ Scope scope2 = tagger.currentBuilder().put(KEY_2, VALUE_2).buildScoped();
+ try {
+ assertThat(tagContextToList(tagger.getCurrentTagContext()))
+ .containsExactly(Tag.create(KEY_1, VALUE_1), Tag.create(KEY_2, VALUE_2));
+ } finally {
+ scope2.close();
+ }
+ assertThat(tagger.getCurrentTagContext()).isSameAs(scopedTags);
+ } finally {
+ scope1.close();
+ }
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java
new file mode 100644
index 00000000..b70d8b4e
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagContextImplTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.tags.TagsTestUtil.tagContextToList;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+import com.google.common.testing.EqualsTester;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TagContextImpl} and {@link TagContextBuilderImpl}. */
+// TODO(sebright): Add more tests once the API is finalized.
+@RunWith(JUnit4.class)
+public class TagContextImplTest {
+ private final Tagger tagger = new TaggerImpl(new CurrentTaggingState());
+
+ private static final TagKey KS1 = TagKey.create("k1");
+ private static final TagKey KS2 = TagKey.create("k2");
+
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void testSet() {
+ TagContext tags = tagger.emptyBuilder().put(KS1, V1).build();
+ assertThat(tagContextToList(tagger.toBuilder(tags).put(KS1, V2).build()))
+ .containsExactly(Tag.create(KS1, V2));
+ assertThat(tagContextToList(tagger.toBuilder(tags).put(KS2, V2).build()))
+ .containsExactly(Tag.create(KS1, V1), Tag.create(KS2, V2));
+ }
+
+ @Test
+ public void testClear() {
+ TagContext tags = tagger.emptyBuilder().put(KS1, V1).build();
+ assertThat(tagContextToList(tagger.toBuilder(tags).remove(KS1).build())).isEmpty();
+ assertThat(tagContextToList(tagger.toBuilder(tags).remove(KS2).build()))
+ .containsExactly(Tag.create(KS1, V1));
+ }
+
+ @Test
+ public void testIterator() {
+ TagContext tags = tagger.emptyBuilder().put(KS1, V1).put(KS2, V2).build();
+ Iterator<Tag> i = InternalUtils.getTags(tags);
+ assertTrue(i.hasNext());
+ Tag tag1 = i.next();
+ assertTrue(i.hasNext());
+ Tag tag2 = i.next();
+ assertFalse(i.hasNext());
+ assertThat(Arrays.asList(tag1, tag2)).containsExactly(Tag.create(KS1, V1), Tag.create(KS2, V2));
+ thrown.expect(NoSuchElementException.class);
+ i.next();
+ }
+
+ @Test
+ public void disallowCallingRemoveOnIterator() {
+ TagContext tags = tagger.emptyBuilder().put(KS1, V1).put(KS2, V2).build();
+ Iterator<Tag> i = InternalUtils.getTags(tags);
+ i.next();
+ thrown.expect(UnsupportedOperationException.class);
+ i.remove();
+ }
+
+ @Test
+ public void testEquals() {
+ new EqualsTester()
+ .addEqualityGroup(
+ tagger.emptyBuilder().put(KS1, V1).put(KS2, V2).build(),
+ tagger.emptyBuilder().put(KS1, V1).put(KS2, V2).build(),
+ tagger.emptyBuilder().put(KS2, V2).put(KS1, V1).build(),
+ new TagContext() {
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return Lists.<Tag>newArrayList(Tag.create(KS1, V1), Tag.create(KS2, V2)).iterator();
+ }
+ })
+ .addEqualityGroup(tagger.emptyBuilder().put(KS1, V1).put(KS2, V1).build())
+ .addEqualityGroup(tagger.emptyBuilder().put(KS1, V2).put(KS2, V1).build())
+ .testEquals();
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TaggerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TaggerImplTest.java
new file mode 100644
index 00000000..4ca2ae76
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TaggerImplTest.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static io.opencensus.implcore.tags.TagsTestUtil.tagContextToList;
+
+import com.google.common.collect.Lists;
+import io.grpc.Context;
+import io.opencensus.common.Scope;
+import io.opencensus.implcore.internal.NoopScope;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.unsafe.ContextUtils;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TaggerImpl}. */
+@RunWith(JUnit4.class)
+public class TaggerImplTest {
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+ private final Tagger tagger = tagsComponent.getTagger();
+
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final TagKey K3 = TagKey.create("k3");
+
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final TagValue V3 = TagValue.create("v3");
+
+ private static final Tag TAG1 = Tag.create(K1, V1);
+ private static final Tag TAG2 = Tag.create(K2, V2);
+ private static final Tag TAG3 = Tag.create(K3, V3);
+
+ @Test
+ public void empty() {
+ assertThat(tagContextToList(tagger.empty())).isEmpty();
+ assertThat(tagger.empty()).isInstanceOf(TagContextImpl.class);
+ }
+
+ @Test
+ public void empty_TaggingDisabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagContextToList(tagger.empty())).isEmpty();
+ assertThat(tagger.empty()).isInstanceOf(TagContextImpl.class);
+ }
+
+ @Test
+ public void emptyBuilder() {
+ TagContextBuilder builder = tagger.emptyBuilder();
+ assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+ assertThat(tagContextToList(builder.build())).isEmpty();
+ }
+
+ @Test
+ public void emptyBuilder_TaggingDisabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagger.emptyBuilder()).isSameAs(NoopTagContextBuilder.INSTANCE);
+ }
+
+ @Test
+ public void emptyBuilder_TaggingReenabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagger.emptyBuilder()).isSameAs(NoopTagContextBuilder.INSTANCE);
+ tagsComponent.setState(TaggingState.ENABLED);
+ TagContextBuilder builder = tagger.emptyBuilder();
+ assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+ assertThat(tagContextToList(builder.put(K1, V1).build())).containsExactly(Tag.create(K1, V1));
+ }
+
+ @Test
+ public void currentBuilder() {
+ TagContext tags = new SimpleTagContext(TAG1, TAG2, TAG3);
+ TagContextBuilder result = getResultOfCurrentBuilder(tags);
+ assertThat(result).isInstanceOf(TagContextBuilderImpl.class);
+ assertThat(tagContextToList(result.build())).containsExactly(TAG1, TAG2, TAG3);
+ }
+
+ @Test
+ public void currentBuilder_DefaultIsEmpty() {
+ TagContextBuilder currentBuilder = tagger.currentBuilder();
+ assertThat(currentBuilder).isInstanceOf(TagContextBuilderImpl.class);
+ assertThat(tagContextToList(currentBuilder.build())).isEmpty();
+ }
+
+ @Test
+ public void currentBuilder_RemoveDuplicateTags() {
+ Tag tag1 = Tag.create(K1, V1);
+ Tag tag2 = Tag.create(K1, V2);
+ TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+ TagContextBuilder result = getResultOfCurrentBuilder(tagContextWithDuplicateTags);
+ assertThat(tagContextToList(result.build())).containsExactly(tag2);
+ }
+
+ @Test
+ public void currentBuilder_SkipNullTag() {
+ TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+ TagContextBuilder result = getResultOfCurrentBuilder(tagContextWithNullTag);
+ assertThat(tagContextToList(result.build())).containsExactly(TAG1, TAG2);
+ }
+
+ @Test
+ public void currentBuilder_TaggingDisabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(getResultOfCurrentBuilder(new SimpleTagContext(TAG1)))
+ .isSameAs(NoopTagContextBuilder.INSTANCE);
+ }
+
+ @Test
+ public void currentBuilder_TaggingReenabled() {
+ TagContext tags = new SimpleTagContext(TAG1);
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(getResultOfCurrentBuilder(tags)).isSameAs(NoopTagContextBuilder.INSTANCE);
+ tagsComponent.setState(TaggingState.ENABLED);
+ TagContextBuilder builder = getResultOfCurrentBuilder(tags);
+ assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+ assertThat(tagContextToList(builder.build())).containsExactly(TAG1);
+ }
+
+ private TagContextBuilder getResultOfCurrentBuilder(TagContext tagsToSet) {
+ Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, tagsToSet).attach();
+ try {
+ return tagger.currentBuilder();
+ } finally {
+ Context.current().detach(orig);
+ }
+ }
+
+ @Test
+ public void toBuilder_ConvertUnknownTagContextToTagContextImpl() {
+ TagContext unknownTagContext = new SimpleTagContext(TAG1, TAG2, TAG3);
+ TagContext newTagContext = tagger.toBuilder(unknownTagContext).build();
+ assertThat(tagContextToList(newTagContext)).containsExactly(TAG1, TAG2, TAG3);
+ assertThat(newTagContext).isInstanceOf(TagContextImpl.class);
+ }
+
+ @Test
+ public void toBuilder_RemoveDuplicatesFromUnknownTagContext() {
+ Tag tag1 = Tag.create(K1, V1);
+ Tag tag2 = Tag.create(K1, V2);
+ TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+ TagContext newTagContext = tagger.toBuilder(tagContextWithDuplicateTags).build();
+ assertThat(tagContextToList(newTagContext)).containsExactly(tag2);
+ }
+
+ @Test
+ public void toBuilder_SkipNullTag() {
+ TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+ TagContext newTagContext = tagger.toBuilder(tagContextWithNullTag).build();
+ assertThat(tagContextToList(newTagContext)).containsExactly(TAG1, TAG2);
+ }
+
+ @Test
+ public void toBuilder_TaggingDisabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagger.toBuilder(new SimpleTagContext(TAG1)))
+ .isSameAs(NoopTagContextBuilder.INSTANCE);
+ }
+
+ @Test
+ public void toBuilder_TaggingReenabled() {
+ TagContext tags = new SimpleTagContext(TAG1);
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagger.toBuilder(tags)).isSameAs(NoopTagContextBuilder.INSTANCE);
+ tagsComponent.setState(TaggingState.ENABLED);
+ TagContextBuilder builder = tagger.toBuilder(tags);
+ assertThat(builder).isInstanceOf(TagContextBuilderImpl.class);
+ assertThat(tagContextToList(builder.build())).containsExactly(TAG1);
+ }
+
+ @Test
+ public void getCurrentTagContext_DefaultIsEmptyTagContextImpl() {
+ TagContext currentTagContext = tagger.getCurrentTagContext();
+ assertThat(tagContextToList(currentTagContext)).isEmpty();
+ assertThat(currentTagContext).isInstanceOf(TagContextImpl.class);
+ }
+
+ @Test
+ public void getCurrentTagContext_ConvertUnknownTagContextToTagContextImpl() {
+ TagContext unknownTagContext = new SimpleTagContext(TAG1, TAG2, TAG3);
+ TagContext result = getResultOfGetCurrentTagContext(unknownTagContext);
+ assertThat(result).isInstanceOf(TagContextImpl.class);
+ assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2, TAG3);
+ }
+
+ @Test
+ public void getCurrentTagContext_RemoveDuplicatesFromUnknownTagContext() {
+ Tag tag1 = Tag.create(K1, V1);
+ Tag tag2 = Tag.create(K1, V2);
+ TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+ TagContext result = getResultOfGetCurrentTagContext(tagContextWithDuplicateTags);
+ assertThat(tagContextToList(result)).containsExactly(tag2);
+ }
+
+ @Test
+ public void getCurrentTagContext_SkipNullTag() {
+ TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+ TagContext result = getResultOfGetCurrentTagContext(tagContextWithNullTag);
+ assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2);
+ }
+
+ @Test
+ public void getCurrentTagContext_TaggingDisabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagContextToList(getResultOfGetCurrentTagContext(new SimpleTagContext(TAG1))))
+ .isEmpty();
+ }
+
+ @Test
+ public void getCurrentTagContext_TaggingReenabled() {
+ TagContext tags = new SimpleTagContext(TAG1);
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagContextToList(getResultOfGetCurrentTagContext(tags))).isEmpty();
+ tagsComponent.setState(TaggingState.ENABLED);
+ assertThat(tagContextToList(getResultOfGetCurrentTagContext(tags))).containsExactly(TAG1);
+ }
+
+ private TagContext getResultOfGetCurrentTagContext(TagContext tagsToSet) {
+ Context orig = Context.current().withValue(ContextUtils.TAG_CONTEXT_KEY, tagsToSet).attach();
+ try {
+ return tagger.getCurrentTagContext();
+ } finally {
+ Context.current().detach(orig);
+ }
+ }
+
+ @Test
+ public void withTagContext_ConvertUnknownTagContextToTagContextImpl() {
+ TagContext unknownTagContext = new SimpleTagContext(TAG1, TAG2, TAG3);
+ TagContext result = getResultOfWithTagContext(unknownTagContext);
+ assertThat(result).isInstanceOf(TagContextImpl.class);
+ assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2, TAG3);
+ }
+
+ @Test
+ public void withTagContext_RemoveDuplicatesFromUnknownTagContext() {
+ Tag tag1 = Tag.create(K1, V1);
+ Tag tag2 = Tag.create(K1, V2);
+ TagContext tagContextWithDuplicateTags = new SimpleTagContext(tag1, tag2);
+ TagContext result = getResultOfWithTagContext(tagContextWithDuplicateTags);
+ assertThat(tagContextToList(result)).containsExactly(tag2);
+ }
+
+ @Test
+ public void withTagContext_SkipNullTag() {
+ TagContext tagContextWithNullTag = new SimpleTagContext(TAG1, null, TAG2);
+ TagContext result = getResultOfWithTagContext(tagContextWithNullTag);
+ assertThat(tagContextToList(result)).containsExactly(TAG1, TAG2);
+ }
+
+ @Test
+ public void withTagContext_ReturnsNoopScopeWhenTaggingIsDisabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagger.withTagContext(new SimpleTagContext(TAG1))).isSameAs(NoopScope.getInstance());
+ }
+
+ @Test
+ public void withTagContext_TaggingDisabled() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagContextToList(getResultOfWithTagContext(new SimpleTagContext(TAG1)))).isEmpty();
+ }
+
+ @Test
+ public void withTagContext_TaggingReenabled() {
+ TagContext tags = new SimpleTagContext(TAG1);
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagContextToList(getResultOfWithTagContext(tags))).isEmpty();
+ tagsComponent.setState(TaggingState.ENABLED);
+ assertThat(tagContextToList(getResultOfWithTagContext(tags))).containsExactly(TAG1);
+ }
+
+ private TagContext getResultOfWithTagContext(TagContext tagsToSet) {
+ Scope scope = tagger.withTagContext(tagsToSet);
+ try {
+ return ContextUtils.TAG_CONTEXT_KEY.get();
+ } finally {
+ scope.close();
+ }
+ }
+
+ private static final class SimpleTagContext extends TagContext {
+ private final List<Tag> tags;
+
+ SimpleTagContext(Tag... tags) {
+ this.tags = Collections.unmodifiableList(Lists.newArrayList(tags));
+ }
+
+ @Override
+ protected Iterator<Tag> getIterator() {
+ return tags.iterator();
+ }
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.java
new file mode 100644
index 00000000..e75a86d0
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsComponentImplBaseTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link TagsComponentImplBase}. */
+@RunWith(JUnit4.class)
+public class TagsComponentImplBaseTest {
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+
+ @Test
+ public void defaultState() {
+ assertThat(tagsComponent.getState()).isEqualTo(TaggingState.ENABLED);
+ }
+
+ @Test
+ public void setState() {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(tagsComponent.getState()).isEqualTo(TaggingState.DISABLED);
+ tagsComponent.setState(TaggingState.ENABLED);
+ assertThat(tagsComponent.getState()).isEqualTo(TaggingState.ENABLED);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void setState_DisallowsNull() {
+ tagsComponent.setState(null);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java
new file mode 100644
index 00000000..c64eced1
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/TagsTestUtil.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags;
+
+import com.google.common.collect.Lists;
+import io.opencensus.tags.InternalUtils;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import java.util.Collection;
+
+/** Test utilities for tagging. */
+public class TagsTestUtil {
+
+ /** Returns a collection of all tags in a {@link TagContext}. */
+ public static Collection<Tag> tagContextToList(TagContext tags) {
+ return Lists.newArrayList(InternalUtils.getTags(tags));
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java
new file mode 100644
index 00000000..7e9bead1
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextBinarySerializerImplTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.implcore.tags.TagsTestUtil;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.TaggingState;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for {@link TagContextBinarySerializerImpl}.
+ *
+ * <p>Thorough serialization/deserialization tests are in {@link TagContextSerializationTest},
+ * {@link TagContextDeserializationTest}, and {@link TagContextRoundtripTest}.
+ */
+@RunWith(JUnit4.class)
+public final class TagContextBinarySerializerImplTest {
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+ private final TagContextBinarySerializer serializer =
+ tagsComponent.getTagPropagationComponent().getBinarySerializer();
+
+ private final TagContext tagContext =
+ new TagContext() {
+ @Override
+ public Iterator<Tag> getIterator() {
+ return ImmutableSet.<Tag>of(Tag.create(TagKey.create("key"), TagValue.create("value")))
+ .iterator();
+ }
+ };
+
+ @Test
+ public void toByteArray_TaggingDisabled() throws TagContextSerializationException {
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(serializer.toByteArray(tagContext)).isEmpty();
+ }
+
+ @Test
+ public void toByteArray_TaggingReenabled() throws TagContextSerializationException {
+ final byte[] serialized = serializer.toByteArray(tagContext);
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(serializer.toByteArray(tagContext)).isEmpty();
+ tagsComponent.setState(TaggingState.ENABLED);
+ assertThat(serializer.toByteArray(tagContext)).isEqualTo(serialized);
+ }
+
+ @Test
+ public void fromByteArray_TaggingDisabled()
+ throws TagContextDeserializationException, TagContextSerializationException {
+ byte[] serialized = serializer.toByteArray(tagContext);
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(TagsTestUtil.tagContextToList(serializer.fromByteArray(serialized))).isEmpty();
+ }
+
+ @Test
+ public void fromByteArray_TaggingReenabled()
+ throws TagContextDeserializationException, TagContextSerializationException {
+ byte[] serialized = serializer.toByteArray(tagContext);
+ tagsComponent.setState(TaggingState.DISABLED);
+ assertThat(TagsTestUtil.tagContextToList(serializer.fromByteArray(serialized))).isEmpty();
+ tagsComponent.setState(TaggingState.ENABLED);
+ assertThat(serializer.fromByteArray(serialized)).isEqualTo(tagContext);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java
new file mode 100644
index 00000000..14118be0
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextDeserializationTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016-17, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteArrayDataOutput;
+import com.google.common.io.ByteStreams;
+import io.opencensus.implcore.internal.VarInt;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextDeserializationException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for deserializing tags with {@link SerializationUtils} and {@link
+ * TagContextBinarySerializerImpl}.
+ */
+@RunWith(JUnit4.class)
+public class TagContextDeserializationTest {
+
+ @Rule public final ExpectedException thrown = ExpectedException.none();
+
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+ private final TagContextBinarySerializer serializer =
+ tagsComponent.getTagPropagationComponent().getBinarySerializer();
+ private final Tagger tagger = tagsComponent.getTagger();
+
+ @Test
+ public void testVersionAndValueTypeConstants() {
+ // Refer to the JavaDoc on SerializationUtils for the definitions on these constants.
+ assertThat(SerializationUtils.VERSION_ID).isEqualTo(0);
+ assertThat(SerializationUtils.TAG_FIELD_ID).isEqualTo(0);
+ }
+
+ @Test
+ public void testDeserializeNoTags() throws TagContextDeserializationException {
+ TagContext expected = tagger.empty();
+ TagContext actual =
+ serializer.fromByteArray(
+ new byte[] {SerializationUtils.VERSION_ID}); // One byte that represents Version ID.
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void testDeserializeEmptyByteArrayThrowException()
+ throws TagContextDeserializationException {
+ thrown.expect(TagContextDeserializationException.class);
+ thrown.expectMessage("Input byte[] can not be empty.");
+ serializer.fromByteArray(new byte[0]);
+ }
+
+ @Test
+ public void testDeserializeInvalidTagKey() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+
+ // Encode an invalid tag key and a valid tag value:
+ encodeTagToOutput("\2key", "value", output);
+ final byte[] bytes = output.toByteArray();
+
+ thrown.expect(TagContextDeserializationException.class);
+ thrown.expectMessage("Invalid tag key: \2key");
+ serializer.fromByteArray(bytes);
+ }
+
+ @Test
+ public void testDeserializeInvalidTagValue() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+
+ // Encode a valid tag key and an invalid tag value:
+ encodeTagToOutput("my key", "val\3", output);
+ final byte[] bytes = output.toByteArray();
+
+ thrown.expect(TagContextDeserializationException.class);
+ thrown.expectMessage("Invalid tag value for key TagKey{name=my key}: val\3");
+ serializer.fromByteArray(bytes);
+ }
+
+ @Test
+ public void testDeserializeOneTag() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ encodeTagToOutput("Key", "Value", output);
+ TagContext expected =
+ tagger.emptyBuilder().put(TagKey.create("Key"), TagValue.create("Value")).build();
+ assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+ }
+
+ @Test
+ public void testDeserializeMultipleTags() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ encodeTagToOutput("Key1", "Value1", output);
+ encodeTagToOutput("Key2", "Value2", output);
+ TagContext expected =
+ tagger
+ .emptyBuilder()
+ .put(TagKey.create("Key1"), TagValue.create("Value1"))
+ .put(TagKey.create("Key2"), TagValue.create("Value2"))
+ .build();
+ assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+ }
+
+ @Test
+ public void stopParsingAtUnknownField() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+ encodeTagToOutput("Key1", "Value1", output);
+ encodeTagToOutput("Key2", "Value2", output);
+
+ // Write unknown field ID 1.
+ output.write(1);
+ output.write(new byte[] {1, 2, 3, 4});
+
+ encodeTagToOutput("Key3", "Value3", output);
+
+ // key 3 should not be included
+ TagContext expected =
+ tagger
+ .emptyBuilder()
+ .put(TagKey.create("Key1"), TagValue.create("Value1"))
+ .put(TagKey.create("Key2"), TagValue.create("Value2"))
+ .build();
+ assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(expected);
+ }
+
+ @Test
+ public void stopParsingAtUnknownTagAtStart() throws TagContextDeserializationException {
+ ByteArrayDataOutput output = ByteStreams.newDataOutput();
+ output.write(SerializationUtils.VERSION_ID);
+
+ // Write unknown field ID 1.
+ output.write(1);
+ output.write(new byte[] {1, 2, 3, 4});
+
+ encodeTagToOutput("Key", "Value", output);
+ assertThat(serializer.fromByteArray(output.toByteArray())).isEqualTo(tagger.empty());
+ }
+
+ @Test
+ public void testDeserializeWrongFormat() throws TagContextDeserializationException {
+ // encoded tags should follow the format <version_id>(<tag_field_id><tag_encoding>)*
+ thrown.expect(TagContextDeserializationException.class);
+ serializer.fromByteArray(new byte[3]);
+ }
+
+ @Test
+ public void testDeserializeWrongVersionId() throws TagContextDeserializationException {
+ thrown.expect(TagContextDeserializationException.class);
+ thrown.expectMessage("Wrong Version ID: 1. Currently supported version is: 0");
+ serializer.fromByteArray(new byte[] {(byte) (SerializationUtils.VERSION_ID + 1)});
+ }
+
+ // <tag_encoding> ==
+ // <tag_key_len><tag_key><tag_val_len><tag_val>
+ // <tag_key_len> == varint encoded integer
+ // <tag_key> == tag_key_len bytes comprising tag key name
+ // <tag_val_len> == varint encoded integer
+ // <tag_val> == tag_val_len bytes comprising UTF-8 string
+ private static void encodeTagToOutput(String key, String value, ByteArrayDataOutput output) {
+ output.write(SerializationUtils.TAG_FIELD_ID);
+ encodeString(key, output);
+ encodeString(value, output);
+ }
+
+ private static void encodeString(String input, ByteArrayDataOutput output) {
+ int length = input.length();
+ byte[] bytes = new byte[VarInt.varIntSize(length)];
+ VarInt.putVarInt(length, bytes, 0);
+ output.write(bytes);
+ output.write(input.getBytes(Charsets.UTF_8));
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java
new file mode 100644
index 00000000..88abcc1d
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextRoundtripTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2017, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.TagContext;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for roundtrip serialization with {@link TagContextBinarySerializerImpl}. */
+@RunWith(JUnit4.class)
+public class TagContextRoundtripTest {
+
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final TagKey K3 = TagKey.create("k3");
+
+ private static final TagValue V_EMPTY = TagValue.create("");
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final TagValue V3 = TagValue.create("v3");
+
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+ private final TagContextBinarySerializer serializer =
+ tagsComponent.getTagPropagationComponent().getBinarySerializer();
+ private final Tagger tagger = tagsComponent.getTagger();
+
+ @Test
+ public void testRoundtripSerialization() throws Exception {
+ testRoundtripSerialization(tagger.empty());
+ testRoundtripSerialization(tagger.emptyBuilder().put(K1, V1).build());
+ testRoundtripSerialization(tagger.emptyBuilder().put(K1, V1).put(K2, V2).put(K3, V3).build());
+ testRoundtripSerialization(tagger.emptyBuilder().put(K1, V_EMPTY).build());
+ }
+
+ private void testRoundtripSerialization(TagContext expected) throws Exception {
+ byte[] bytes = serializer.toByteArray(expected);
+ TagContext actual = serializer.fromByteArray(bytes);
+ assertThat(actual).isEqualTo(expected);
+ }
+}
diff --git a/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java
new file mode 100644
index 00000000..6227fe54
--- /dev/null
+++ b/impl_core/src/test/java/io/opencensus/implcore/tags/propagation/TagContextSerializationTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016-17, OpenCensus Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.opencensus.implcore.tags.propagation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Collections2;
+import io.opencensus.implcore.internal.VarInt;
+import io.opencensus.implcore.tags.TagsComponentImplBase;
+import io.opencensus.tags.Tag;
+import io.opencensus.tags.TagContextBuilder;
+import io.opencensus.tags.TagKey;
+import io.opencensus.tags.TagValue;
+import io.opencensus.tags.Tagger;
+import io.opencensus.tags.TagsComponent;
+import io.opencensus.tags.propagation.TagContextBinarySerializer;
+import io.opencensus.tags.propagation.TagContextSerializationException;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Tests for serializing tags with {@link SerializationUtils} and {@link
+ * TagContextBinarySerializerImpl}.
+ */
+@RunWith(JUnit4.class)
+public class TagContextSerializationTest {
+
+ private static final int VERSION_ID = 0;
+ private static final int TAG_FIELD_ID = 0;
+
+ private static final TagKey K1 = TagKey.create("k1");
+ private static final TagKey K2 = TagKey.create("k2");
+ private static final TagKey K3 = TagKey.create("k3");
+ private static final TagKey K4 = TagKey.create("k4");
+
+ private static final TagValue V1 = TagValue.create("v1");
+ private static final TagValue V2 = TagValue.create("v2");
+ private static final TagValue V3 = TagValue.create("v3");
+ private static final TagValue V4 = TagValue.create("v4");
+
+ private static final Tag T1 = Tag.create(K1, V1);
+ private static final Tag T2 = Tag.create(K2, V2);
+ private static final Tag T3 = Tag.create(K3, V3);
+ private static final Tag T4 = Tag.create(K4, V4);
+
+ private final TagsComponent tagsComponent = new TagsComponentImplBase();
+ private final TagContextBinarySerializer serializer =
+ tagsComponent.getTagPropagationComponent().getBinarySerializer();
+ private final Tagger tagger = tagsComponent.getTagger();
+
+ @Test
+ public void testSerializeDefault() throws Exception {
+ testSerialize();
+ }
+
+ @Test
+ public void testSerializeWithOneTag() throws Exception {
+ testSerialize(T1);
+ }
+
+ @Test
+ public void testSerializeWithMultipleTags() throws Exception {
+ testSerialize(T1, T2, T3, T4);
+ }
+
+ private void testSerialize(Tag... tags) throws IOException, TagContextSerializationException {
+ TagContextBuilder builder = tagger.emptyBuilder();
+ for (Tag tag : tags) {
+ builder.put(tag.getKey(), tag.getValue());
+ }
+
+ byte[] actual = serializer.toByteArray(builder.build());
+
+ Collection<List<Tag>> tagPermutation = Collections2.permutations(Arrays.asList(tags));
+ Set<String> possibleOutputs = new HashSet<String>();
+ for (List<Tag> list : tagPermutation) {
+ ByteArrayOutputStream expected = new ByteArrayOutputStream();
+ expected.write(VERSION_ID);
+ for (Tag tag : list) {
+ expected.write(TAG_FIELD_ID);
+ encodeString(tag.getKey().getName(), expected);
+ encodeString(tag.getValue().asString(), expected);
+ }
+ possibleOutputs.add(expected.toString());
+ }
+
+ assertThat(possibleOutputs).contains(new String(actual, Charsets.UTF_8));
+ }
+
+ private static void encodeString(String input, ByteArrayOutputStream byteArrayOutputStream)
+ throws IOException {
+ VarInt.putVarInt(input.length(), byteArrayOutputStream);
+ byteArrayOutputStream.write(input.getBytes(Charsets.UTF_8));
+ }
+}